Commit 171e9322 by Calen Pennington

Merge branch 'master' into feature/cale/cms-master

Conflicts:
	rakefile
parents e3d65a0c 32303452
# .coveragerc for cms
[run]
data_file = reports/cms/.coverage
source = cms
[report]
ignore_errors = True
[html]
directory = reports/cms/cover
[xml]
output = reports/cms/coverage.xml
...@@ -14,9 +14,7 @@ from path import path ...@@ -14,9 +14,7 @@ from path import path
# Nose Test Runner # Nose Test Runner
INSTALLED_APPS += ('django_nose',) INSTALLED_APPS += ('django_nose',)
NOSE_ARGS = ['--cover-erase', '--with-xunit', '--with-xcoverage', '--cover-html', '--cover-inclusive'] NOSE_ARGS = ['--with-xunit']
for app in os.listdir(PROJECT_ROOT / 'djangoapps'):
NOSE_ARGS += ['--cover-package', app]
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
TEST_ROOT = path('test_root') TEST_ROOT = path('test_root')
......
import csv
import uuid
from collections import defaultdict, OrderedDict
from datetime import datetime
from django.core.management.base import BaseCommand, CommandError
from student.models import TestCenterUser
class Command(BaseCommand):
CSV_TO_MODEL_FIELDS = OrderedDict([
("ClientCandidateID", "client_candidate_id"),
("FirstName", "first_name"),
("LastName", "last_name"),
("MiddleName", "middle_name"),
("Suffix", "suffix"),
("Salutation", "salutation"),
("Email", "email"),
# Skipping optional fields Username and Password
("Address1", "address_1"),
("Address2", "address_2"),
("Address3", "address_3"),
("City", "city"),
("State", "state"),
("PostalCode", "postal_code"),
("Country", "country"),
("Phone", "phone"),
("Extension", "extension"),
("PhoneCountryCode", "phone_country_code"),
("FAX", "fax"),
("FAXCountryCode", "fax_country_code"),
("CompanyName", "company_name"),
# Skipping optional field CustomQuestion
("LastUpdate", "user_updated_at"), # in UTC, so same as what we store
])
args = '<output_file>'
help = """
Export user information from TestCenterUser model into a tab delimited
text file with a format that Pearson expects.
"""
def handle(self, *args, **kwargs):
if len(args) < 1:
print Command.help
return
self.reset_sample_data()
with open(args[0], "wb") as outfile:
writer = csv.DictWriter(outfile,
Command.CSV_TO_MODEL_FIELDS,
delimiter="\t",
quoting=csv.QUOTE_MINIMAL,
extrasaction='ignore')
writer.writeheader()
for tcu in TestCenterUser.objects.order_by('id'):
record = dict((csv_field, getattr(tcu, model_field))
for csv_field, model_field
in Command.CSV_TO_MODEL_FIELDS.items())
record["LastUpdate"] = record["LastUpdate"].strftime("%Y/%m/%d %H:%M:%S")
writer.writerow(record)
def reset_sample_data(self):
def make_sample(**kwargs):
data = dict((model_field, kwargs.get(model_field, ""))
for model_field in Command.CSV_TO_MODEL_FIELDS.values())
return TestCenterUser(**data)
def generate_id():
return "edX{:012}".format(uuid.uuid4().int % (10**12))
# TestCenterUser.objects.all().delete()
samples = [
make_sample(
client_candidate_id=generate_id(),
first_name="Jack",
last_name="Doe",
middle_name="C",
address_1="11 Cambridge Center",
address_2="Suite 101",
city="Cambridge",
state="MA",
postal_code="02140",
country="USA",
phone="(617)555-5555",
phone_country_code="1",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Clyde",
last_name="Smith",
middle_name="J",
suffix="Jr.",
salutation="Mr.",
address_1="1 Penny Lane",
city="Honolulu",
state="HI",
postal_code="96792",
country="USA",
phone="555-555-5555",
phone_country_code="1",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Patty",
last_name="Lee",
salutation="Dr.",
address_1="P.O. Box 555",
city="Honolulu",
state="HI",
postal_code="96792",
country="USA",
phone="808-555-5555",
phone_country_code="1",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Jimmy",
last_name="James",
address_1="2020 Palmer Blvd.",
city="Springfield",
state="MA",
postal_code="96792",
country="USA",
phone="917-555-5555",
phone_country_code="1",
extension="2039",
fax="917-555-5556",
fax_country_code="1",
company_name="ACME Traps",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Yeong-Un",
last_name="Seo",
address_1="Duryu, Lotte 101",
address_2="Apt 55",
city="Daegu",
country="KOR",
phone="917-555-5555",
phone_country_code="011",
user_updated_at=datetime.utcnow()
),
]
for tcu in samples:
tcu.save()
\ No newline at end of file
import csv
import uuid
from collections import defaultdict, OrderedDict
from datetime import datetime
from django.core.management.base import BaseCommand, CommandError
from student.models import TestCenterUser
def generate_id():
return "{:012}".format(uuid.uuid4().int % (10**12))
class Command(BaseCommand):
args = '<output_file>'
help = """
Export user information from TestCenterUser model into a tab delimited
text file with a format that Pearson expects.
"""
FIELDS = [
'AuthorizationTransactionType',
'AuthorizationID',
'ClientAuthorizationID',
'ClientCandidateID',
'ExamAuthorizationCount',
'ExamSeriesCode',
'EligibilityApptDateFirst',
'EligibilityApptDateLast',
'LastUpdate',
]
def handle(self, *args, **kwargs):
if len(args) < 1:
print Command.help
return
# self.reset_sample_data()
with open(args[0], "wb") as outfile:
writer = csv.DictWriter(outfile,
Command.FIELDS,
delimiter="\t",
quoting=csv.QUOTE_MINIMAL,
extrasaction='ignore')
writer.writeheader()
for tcu in TestCenterUser.objects.order_by('id')[:5]:
record = defaultdict(
lambda: "",
AuthorizationTransactionType="Add",
ClientAuthorizationID=generate_id(),
ClientCandidateID=tcu.client_candidate_id,
ExamAuthorizationCount="1",
ExamSeriesCode="6002x001",
EligibilityApptDateFirst="2012/12/15",
EligibilityApptDateLast="2012/12/30",
LastUpdate=datetime.utcnow().strftime("%Y/%m/%d %H:%M:%S")
)
writer.writerow(record)
def reset_sample_data(self):
def make_sample(**kwargs):
data = dict((model_field, kwargs.get(model_field, ""))
for model_field in Command.CSV_TO_MODEL_FIELDS.values())
return TestCenterUser(**data)
# TestCenterUser.objects.all().delete()
samples = [
make_sample(
client_candidate_id=generate_id(),
first_name="Jack",
last_name="Doe",
middle_name="C",
address_1="11 Cambridge Center",
address_2="Suite 101",
city="Cambridge",
state="MA",
postal_code="02140",
country="USA",
phone="(617)555-5555",
phone_country_code="1",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Clyde",
last_name="Smith",
middle_name="J",
suffix="Jr.",
salutation="Mr.",
address_1="1 Penny Lane",
city="Honolulu",
state="HI",
postal_code="96792",
country="USA",
phone="555-555-5555",
phone_country_code="1",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Patty",
last_name="Lee",
salutation="Dr.",
address_1="P.O. Box 555",
city="Honolulu",
state="HI",
postal_code="96792",
country="USA",
phone="808-555-5555",
phone_country_code="1",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Jimmy",
last_name="James",
address_1="2020 Palmer Blvd.",
city="Springfield",
state="MA",
postal_code="96792",
country="USA",
phone="917-555-5555",
phone_country_code="1",
extension="2039",
fax="917-555-5556",
fax_country_code="1",
company_name="ACME Traps",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Yeong-Un",
last_name="Seo",
address_1="Duryu, Lotte 101",
address_2="Apt 55",
city="Daegu",
country="KOR",
phone="917-555-5555",
phone_country_code="011",
user_updated_at=datetime.utcnow()
),
]
for tcu in samples:
tcu.save()
\ No newline at end of file
import uuid
from datetime import datetime
from optparse import make_option
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand, CommandError
from student.models import TestCenterUser
class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option(
'--client_candidate_id',
action='store',
dest='client_candidate_id',
help='ID we assign a user to identify them to Pearson'
),
make_option(
'--first_name',
action='store',
dest='first_name',
),
make_option(
'--last_name',
action='store',
dest='last_name',
),
make_option(
'--address_1',
action='store',
dest='address_1',
),
make_option(
'--city',
action='store',
dest='city',
),
make_option(
'--state',
action='store',
dest='state',
help='Two letter code (e.g. MA)'
),
make_option(
'--postal_code',
action='store',
dest='postal_code',
),
make_option(
'--country',
action='store',
dest='country',
help='Three letter country code (ISO 3166-1 alpha-3), like USA'
),
make_option(
'--phone',
action='store',
dest='phone',
help='Pretty free-form (parens, spaces, dashes), but no country code'
),
make_option(
'--phone_country_code',
action='store',
dest='phone_country_code',
help='Phone country code, just "1" for the USA'
),
)
args = "<student_username>"
help = "Create a TestCenterUser entry for a given Student"
@staticmethod
def is_valid_option(option_name):
base_options = set(option.dest for option in BaseCommand.option_list)
return option_name not in base_options
def handle(self, *args, **options):
username = args[0]
print username
our_options = dict((k, v) for k, v in options.items()
if Command.is_valid_option(k))
student = User.objects.get(username=username)
student.test_center_user = TestCenterUser(**our_options)
student.test_center_user.save()
...@@ -8,7 +8,7 @@ Portal servers that hold the canoncial user information and that user ...@@ -8,7 +8,7 @@ Portal servers that hold the canoncial user information and that user
information is replicated to slave Course server pools. Each Course has a set of information is replicated to slave Course server pools. Each Course has a set of
servers that serves only its content and has users that are relevant only to it. servers that serves only its content and has users that are relevant only to it.
We replicate the following tables into the Course DBs where the user is We replicate the following tables into the Course DBs where the user is
enrolled. Only the Portal servers should ever write to these models. enrolled. Only the Portal servers should ever write to these models.
* UserProfile * UserProfile
* CourseEnrollment * CourseEnrollment
...@@ -41,33 +41,22 @@ import uuid ...@@ -41,33 +41,22 @@ import uuid
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from django_countries import CountryField
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from functools import partial
import comment_client as cc import comment_client as cc
from django_comment_client.models import Role, Permission from django_comment_client.models import Role
import logging
from xmodule.modulestore.django import modulestore
#from cache_toolbox import cache_model, cache_relation
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class UserProfile(models.Model): class UserProfile(models.Model):
"""This is where we store all the user demographic fields. We have a """This is where we store all the user demographic fields. We have a
separate table for this rather than extending the built-in Django auth_user. separate table for this rather than extending the built-in Django auth_user.
Notes: Notes:
* Some fields are legacy ones from the first run of 6.002, from which * Some fields are legacy ones from the first run of 6.002, from which
we imported many users. we imported many users.
* Fields like name and address are intentionally open ended, to account * Fields like name and address are intentionally open ended, to account
for international variations. An unfortunate side-effect is that we for international variations. An unfortunate side-effect is that we
...@@ -133,6 +122,72 @@ class UserProfile(models.Model): ...@@ -133,6 +122,72 @@ class UserProfile(models.Model):
def set_meta(self, js): def set_meta(self, js):
self.meta = json.dumps(js) self.meta = json.dumps(js)
class TestCenterUser(models.Model):
"""This is our representation of the User for in-person testing, and
specifically for Pearson at this point. A few things to note:
* Pearson only supports Latin-1, so we have to make sure that the data we
capture here will work with that encoding.
* While we have a lot of this demographic data in UserProfile, it's much
more free-structured there. We'll try to pre-pop the form with data from
UserProfile, but we'll need to have a step where people who are signing
up re-enter their demographic data into the fields we specify.
* Users are only created here if they register to take an exam in person.
The field names and lengths are modeled on the conventions and constraints
of Pearson's data import system, including oddities such as suffix having
a limit of 255 while last_name only gets 50.
"""
# Our own record keeping...
user = models.ForeignKey(User, unique=True, default=None)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True, db_index=True)
# user_updated_at happens only when the user makes a change to their data,
# and is something Pearson needs to know to manage updates. Unlike
# updated_at, this will not get incremented when we do a batch data import.
user_updated_at = models.DateTimeField(db_index=True)
# Unique ID given to us for this User by the Testing Center. It's null when
# we first create the User entry, and is assigned by Pearson later.
candidate_id = models.IntegerField(null=True, db_index=True)
# Unique ID we assign our user for a the Test Center.
client_candidate_id = models.CharField(max_length=50, db_index=True)
# Name
first_name = models.CharField(max_length=30, db_index=True)
last_name = models.CharField(max_length=50, db_index=True)
middle_name = models.CharField(max_length=30, blank=True)
suffix = models.CharField(max_length=255, blank=True)
salutation = models.CharField(max_length=50, blank=True)
# Address
address_1 = models.CharField(max_length=40)
address_2 = models.CharField(max_length=40, blank=True)
address_3 = models.CharField(max_length=40, blank=True)
city = models.CharField(max_length=32, db_index=True)
# state example: HI -- they have an acceptable list that we'll just plug in
# state is required if you're in the US or Canada, but otherwise not.
state = models.CharField(max_length=20, blank=True, db_index=True)
# postal_code required if you're in the US or Canada
postal_code = models.CharField(max_length=16, blank=True, db_index=True)
# country is a ISO 3166-1 alpha-3 country code (e.g. "USA", "CAN", "MNG")
country = models.CharField(max_length=3, db_index=True)
# Phone
phone = models.CharField(max_length=35)
extension = models.CharField(max_length=8, blank=True, db_index=True)
phone_country_code = models.CharField(max_length=3, db_index=True)
fax = models.CharField(max_length=35, blank=True)
# fax_country_code required *if* fax is present.
fax_country_code = models.CharField(max_length=3, blank=True)
# Company
company_name = models.CharField(max_length=50, blank=True)
@property
def email(self):
return self.user.email
## TODO: Should be renamed to generic UserGroup, and possibly ## TODO: Should be renamed to generic UserGroup, and possibly
# Given an optional field for type of group # Given an optional field for type of group
......
...@@ -19,13 +19,13 @@ from django.core.context_processors import csrf ...@@ -19,13 +19,13 @@ from django.core.context_processors import csrf
from django.core.mail import send_mail from django.core.mail import send_mail
from django.core.validators import validate_email, validate_slug, ValidationError from django.core.validators import validate_email, validate_slug, ValidationError
from django.db import IntegrityError from django.db import IntegrityError
from django.http import HttpResponse, Http404 from django.http import HttpResponse, HttpResponseForbidden, Http404
from django.shortcuts import redirect from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from django.core.cache import cache from django.core.cache import cache
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie, csrf_exempt
from student.models import (Registration, UserProfile, from student.models import (Registration, UserProfile,
PendingNameChange, PendingEmailChange, PendingNameChange, PendingEmailChange,
CourseEnrollment) CourseEnrollment)
...@@ -774,3 +774,26 @@ def accept_name_change(request): ...@@ -774,3 +774,26 @@ def accept_name_change(request):
raise Http404 raise Http404
return accept_name_change_by_id(int(request.POST['id'])) return accept_name_change_by_id(int(request.POST['id']))
# TODO: This is a giant kludge to give Pearson something to test against ASAP.
# Will need to get replaced by something that actually ties into TestCenterUser
@csrf_exempt
def test_center_login(request):
if not settings.MITX_FEATURES.get('ENABLE_PEARSON_HACK_TEST'):
raise Http404
client_candidate_id = request.POST.get("clientCandidateID")
# registration_id = request.POST.get("registrationID")
exit_url = request.POST.get("exitURL")
error_url = request.POST.get("errorURL")
if client_candidate_id == "edX003671291147":
user = authenticate(username=settings.PEARSON_TEST_USER,
password=settings.PEARSON_TEST_PASSWORD)
login(request, user)
return redirect('/courses/MITx/6.002x/2012_Fall/courseware/Final_Exam/Final_Exam_Fall_2012/')
else:
return HttpResponseForbidden()
# .coveragerc for common/lib/capa
[run]
data_file = reports/common/lib/capa/.coverage
source = common/lib/capa
[report]
ignore_errors = True
[html]
directory = reports/common/lib/capa/cover
[xml]
output = reports/common/lib/capa/coverage.xml
# .coveragerc for common/lib/xmodule
[run]
data_file = reports/common/lib/xmodule/.coverage
source = common/lib/xmodule
[report]
ignore_errors = True
[html]
directory = reports/common/lib/xmodule/cover
[xml]
output = reports/common/lib/xmodule/coverage.xml
...@@ -3723,7 +3723,7 @@ schematic = (function() { ...@@ -3723,7 +3723,7 @@ schematic = (function() {
// look for property input fields in the content and give // look for property input fields in the content and give
// them a keypress listener that interprets ENTER as // them a keypress listener that interprets ENTER as
// clicking OK. // clicking OK.
var plist = content.$('.property'); var plist = content.getElementsByClassName('property');
for (var i = plist.length - 1; i >= 0; --i) { for (var i = plist.length - 1; i >= 0; --i) {
var field = plist[i]; var field = plist[i];
field.dialog = dialog; // help event handler find us... field.dialog = dialog; // help event handler find us...
......
...@@ -32,6 +32,7 @@ class VideoModule(XModule): ...@@ -32,6 +32,7 @@ class VideoModule(XModule):
self.position = 0 self.position = 0
self.show_captions = xmltree.get('show_captions', 'true') self.show_captions = xmltree.get('show_captions', 'true')
self.source = self._get_source(xmltree) self.source = self._get_source(xmltree)
self.track = self._get_track(xmltree)
if instance_state is not None: if instance_state is not None:
state = json.loads(instance_state) state = json.loads(instance_state)
...@@ -40,13 +41,25 @@ class VideoModule(XModule): ...@@ -40,13 +41,25 @@ class VideoModule(XModule):
def _get_source(self, xmltree): def _get_source(self, xmltree):
# find the first valid source # find the first valid source
source = None return self._get_first_external(xmltree, 'source')
for element in xmltree.findall('source'):
def _get_track(self, xmltree):
# find the first valid track
return self._get_first_external(xmltree, 'track')
def _get_first_external(self, xmltree, tag):
"""
Will return the first valid element
of the given tag.
'valid' means has a non-empty 'src' attribute
"""
result = None
for element in xmltree.findall(tag):
src = element.get('src') src = element.get('src')
if src: if src:
source = src result = src
break break
return source return result
def handle_ajax(self, dispatch, get): def handle_ajax(self, dispatch, get):
''' '''
...@@ -85,6 +98,7 @@ class VideoModule(XModule): ...@@ -85,6 +98,7 @@ class VideoModule(XModule):
'id': self.location.html_id(), 'id': self.location.html_id(),
'position': self.position, 'position': self.position,
'source': self.source, 'source': self.source,
'track' : self.track,
'display_name': self.display_name, 'display_name': self.display_name,
# TODO (cpennington): This won't work when we move to data that isn't on the filesystem # TODO (cpennington): This won't work when we move to data that isn't on the filesystem
'data_dir': self.metadata['data_dir'], 'data_dir': self.metadata['data_dir'],
......
# Python libraries to install directly from github
-e git://github.com/MITx/django-staticfiles.git@6d2504e5c8#egg=django-staticfiles
-e git://github.com/MITx/django-pipeline.git#egg=django-pipeline
-e git://github.com/MITx/django-wiki.git@e2e84558#egg=django-wiki
-e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
#! /bin/bash
set -e
set -x
# Reset the submodule, in case it changed
git submodule foreach 'git reset --hard HEAD'
# Set the IO encoding to UTF-8 so that askbot will start
export PYTHONIOENCODING=UTF-8
rake clobber
rake pep8 || echo "pep8 failed, continuing"
rake pylint || echo "pylint failed, continuing"
#! /bin/bash
set -e
set -x
# Reset the submodule, in case it changed
git submodule foreach 'git reset --hard HEAD'
# Set the IO encoding to UTF-8 so that askbot will start
export PYTHONIOENCODING=UTF-8
GIT_BRANCH=${GIT_BRANCH/HEAD/master}
pip install -q -r pre-requirements.txt
yes w | pip install -q -r requirements.txt
[ ! -d askbot ] || pip install -q -r askbot/askbot_requirements.txt
rake clobber
TESTS_FAILED=0
rake test_cms[false] || TESTS_FAILED=1
rake test_lms[false] || TESTS_FAILED=1
rake test_common/lib/capa || TESTS_FAILED=1
rake test_common/lib/xmodule || TESTS_FAILED=1
rake phantomjs_jasmine_lms || true
rake phantomjs_jasmine_cms || true
rake coverage:xml coverage:html
[ $TESTS_FAILED == '0' ]
rake autodeploy_properties
\ No newline at end of file
#! /bin/bash
set -e
set -x
# Reset the submodule, in case it changed
git submodule foreach 'git reset --hard HEAD'
# Set the IO encoding to UTF-8 so that askbot will start
export PYTHONIOENCODING=UTF-8
GIT_BRANCH=${GIT_BRANCH/HEAD/master}
pip install -q -r pre-requirements.txt
yes w | pip install -q -r requirements.txt
[ ! -d askbot ] || pip install -q -r askbot/askbot_requirements.txt
rake clobber
TESTS_FAILED=0
rake test_lms[false] || TESTS_FAILED=1
rake test_common/lib/capa || TESTS_FAILED=1
rake test_common/lib/xmodule || TESTS_FAILED=1
rake phantomjs_jasmine_lms || true
rake coverage:xml coverage:html
[ $TESTS_FAILED == '0' ]
rake autodeploy_properties
\ No newline at end of file
# .coveragerc for lms
[run]
data_file = reports/lms/.coverage
source = lms
[report]
ignore_errors = True
[html]
directory = reports/lms/cover
[xml]
output = reports/lms/coverage.xml
...@@ -108,7 +108,6 @@ class StudentModuleCache(object): ...@@ -108,7 +108,6 @@ class StudentModuleCache(object):
else: else:
self.cache = [] self.cache = []
@classmethod @classmethod
def cache_for_descriptor_descendents(cls, course_id, user, descriptor, depth=None, def cache_for_descriptor_descendents(cls, course_id, user, descriptor, depth=None,
descriptor_filter=lambda descriptor: True, descriptor_filter=lambda descriptor: True,
...@@ -138,7 +137,6 @@ class StudentModuleCache(object): ...@@ -138,7 +137,6 @@ class StudentModuleCache(object):
return descriptors return descriptors
descriptors = get_child_descriptors(descriptor, depth, descriptor_filter) descriptors = get_child_descriptors(descriptor, depth, descriptor_filter)
return StudentModuleCache(course_id, user, descriptors, select_for_update) return StudentModuleCache(course_id, user, descriptors, select_for_update)
......
...@@ -85,3 +85,5 @@ CONTENTSTORE = AUTH_TOKENS.get('CONTENTSTORE', CONTENTSTORE) ...@@ -85,3 +85,5 @@ CONTENTSTORE = AUTH_TOKENS.get('CONTENTSTORE', CONTENTSTORE)
if 'COURSE_ID' in ENV_TOKENS: if 'COURSE_ID' in ENV_TOKENS:
ASKBOT_URL = "courses/{0}/discussions/".format(ENV_TOKENS['COURSE_ID']) ASKBOT_URL = "courses/{0}/discussions/".format(ENV_TOKENS['COURSE_ID'])
PEARSON_TEST_USER = "pearsontest"
PEARSON_TEST_PASSWORD = AUTH_TOKENS.get("PEARSON_TEST_PASSWORD")
...@@ -177,3 +177,8 @@ FILE_UPLOAD_HANDLERS = ( ...@@ -177,3 +177,8 @@ FILE_UPLOAD_HANDLERS = (
########################### PIPELINE ################################# ########################### PIPELINE #################################
PIPELINE_SASS_ARGUMENTS = '-r {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT) PIPELINE_SASS_ARGUMENTS = '-r {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT)
########################## PEARSON TESTING ###########################
MITX_FEATURES['ENABLE_PEARSON_HACK_TEST'] = True
PEARSON_TEST_USER = "pearsontest"
PEARSON_TEST_PASSWORD = "12345"
...@@ -27,18 +27,11 @@ SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead ...@@ -27,18 +27,11 @@ SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead
# Nose Test Runner # Nose Test Runner
INSTALLED_APPS += ('django_nose',) INSTALLED_APPS += ('django_nose',)
NOSE_ARGS = []
NOSE_ARGS = [
# Turning off coverage speeds up tests dramatically... until we have better config, '--with-xunit',
# leave it here for manual fiddling. # '-v', '--pdb', # When really stuck, uncomment to start debugger on error
_coverage = True ]
if _coverage:
NOSE_ARGS = ['--cover-erase', '--with-xunit', '--with-xcoverage', '--cover-html',
# '-v', '--pdb', # When really stuck, uncomment to start debugger on error
'--cover-inclusive', '--cover-html-dir',
os.environ.get('NOSE_COVER_HTML_DIR', 'cover_html')]
for app in os.listdir(PROJECT_ROOT / 'djangoapps'):
NOSE_ARGS += ['--cover-package', app]
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
# Local Directories # Local Directories
......
...@@ -18,3 +18,9 @@ ...@@ -18,3 +18,9 @@
<p>Download video <a href="${source}">here</a>.</p> <p>Download video <a href="${source}">here</a>.</p>
</div> </div>
% endif % endif
% if track:
<div class="video-tracks">
<p>Download subtitles <a href="${track}">here</a>.</p>
</div>
% endif
...@@ -26,6 +26,11 @@ urlpatterns = ('', ...@@ -26,6 +26,11 @@ urlpatterns = ('',
url(r'^reject_name_change$', 'student.views.reject_name_change'), url(r'^reject_name_change$', 'student.views.reject_name_change'),
url(r'^pending_name_changes$', 'student.views.pending_name_changes'), url(r'^pending_name_changes$', 'student.views.pending_name_changes'),
url(r'^testcenter/login$', 'student.views.test_center_login'),
# url(r'^testcenter/login$', 'student.test_center_views.login'),
# url(r'^testcenter/logout$', 'student.test_center_views.logout'),
url(r'^event$', 'track.views.user_track'), url(r'^event$', 'track.views.user_track'),
url(r'^t/(?P<template>[^/]*)$', 'static_template_view.views.index'), # TODO: Is this used anymore? What is STATIC_GRAB? url(r'^t/(?P<template>[^/]*)$', 'static_template_view.views.index'), # TODO: Is this used anymore? What is STATIC_GRAB?
......
# Python libraries to install that are local to the mitx repo
-e common/lib/capa
-e common/lib/xmodule
...@@ -29,7 +29,7 @@ PACKAGE_REPO = "packages@gp.mitx.mit.edu:/opt/pkgrepo.incoming" ...@@ -29,7 +29,7 @@ PACKAGE_REPO = "packages@gp.mitx.mit.edu:/opt/pkgrepo.incoming"
NORMALIZED_DEPLOY_NAME = DEPLOY_NAME.downcase().gsub(/[_\/]/, '-') NORMALIZED_DEPLOY_NAME = DEPLOY_NAME.downcase().gsub(/[_\/]/, '-')
INSTALL_DIR_PATH = File.join(DEPLOY_DIR, NORMALIZED_DEPLOY_NAME) INSTALL_DIR_PATH = File.join(DEPLOY_DIR, NORMALIZED_DEPLOY_NAME)
# Set up the clean and clobber tasks # Set up the clean and clobber tasks
CLOBBER.include(BUILD_DIR, REPORT_DIR, 'cover*', '.coverage', 'test_root/*_repo', 'test_root/staticfiles') CLOBBER.include(BUILD_DIR, REPORT_DIR, 'test_root/*_repo', 'test_root/staticfiles')
CLEAN.include("#{BUILD_DIR}/*.deb", "#{BUILD_DIR}/util") CLEAN.include("#{BUILD_DIR}/*.deb", "#{BUILD_DIR}/util")
def select_executable(*cmds) def select_executable(*cmds)
...@@ -78,6 +78,11 @@ def django_for_jasmine(system, django_reload) ...@@ -78,6 +78,11 @@ def django_for_jasmine(system, django_reload)
Process.wait(django_pid) Process.wait(django_pid)
end end
end end
def report_dir_path(dir)
return File.join(REPORT_DIR, dir.to_s)
end
task :default => [:test, :pep8, :pylint] task :default => [:test, :pep8, :pylint]
directory REPORT_DIR directory REPORT_DIR
...@@ -89,16 +94,16 @@ default_options = { ...@@ -89,16 +94,16 @@ default_options = {
task :predjango do task :predjango do
sh("find . -type f -name *.pyc -delete") sh("find . -type f -name *.pyc -delete")
sh('pip install -e common/lib/xmodule -e common/lib/capa') sh('pip install -q --upgrade -r local-requirements.txt')
sh('git submodule update --init') sh('git submodule update --init')
end end
task :clean_test_files do task :clean_test_files do
sh("git clean -fdx test_root") sh("git clean -fqdx test_root")
end end
[:lms, :cms, :common].each do |system| [:lms, :cms, :common].each do |system|
report_dir = File.join(REPORT_DIR, system.to_s) report_dir = report_dir_path(system)
directory report_dir directory report_dir
desc "Run pep8 on all #{system} code" desc "Run pep8 on all #{system} code"
...@@ -141,11 +146,19 @@ end ...@@ -141,11 +146,19 @@ end
$failed_tests = 0 $failed_tests = 0
def run_under_coverage(cmd, root)
cmd0, cmd_rest = cmd.split(" ", 2)
# We use "python -m coverage" so that the proper python will run the importable coverage
# rather than the coverage that OS path finds.
cmd = "python -m coverage run --rcfile=#{root}/.coveragerc `which #{cmd0}` #{cmd_rest}"
return cmd
end
def run_tests(system, report_dir, stop_on_failure=true) def run_tests(system, report_dir, stop_on_failure=true)
ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml") ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
ENV['NOSE_COVER_HTML_DIR'] = File.join(report_dir, "cover")
dirs = Dir["common/djangoapps/*"] + Dir["#{system}/djangoapps/*"] dirs = Dir["common/djangoapps/*"] + Dir["#{system}/djangoapps/*"]
sh(django_admin(system, :test, 'test', *dirs.each)) do |ok, res| cmd = django_admin(system, :test, 'test', '--logging-clear-handlers', *dirs.each)
sh(run_under_coverage(cmd, system)) do |ok, res|
if !ok and stop_on_failure if !ok and stop_on_failure
abort "Test failed!" abort "Test failed!"
end end
...@@ -153,11 +166,10 @@ def run_tests(system, report_dir, stop_on_failure=true) ...@@ -153,11 +166,10 @@ def run_tests(system, report_dir, stop_on_failure=true)
end end
end end
TEST_TASKS = [] TEST_TASK_DIRS = []
[:lms, :cms].each do |system| [:lms, :cms].each do |system|
report_dir = File.join(REPORT_DIR, system.to_s) report_dir = report_dir_path(system)
directory report_dir
# Per System tasks # Per System tasks
desc "Run all django tests on our djangoapps for the #{system}" desc "Run all django tests on our djangoapps for the #{system}"
...@@ -170,7 +182,7 @@ TEST_TASKS = [] ...@@ -170,7 +182,7 @@ TEST_TASKS = []
run_tests(system, report_dir, args.stop_on_failure) run_tests(system, report_dir, args.stop_on_failure)
end end
TEST_TASKS << "test_#{system}" TEST_TASK_DIRS << system
desc <<-desc desc <<-desc
Start the #{system} locally with the specified environment (defaults to dev). Start the #{system} locally with the specified environment (defaults to dev).
...@@ -191,12 +203,15 @@ TEST_TASKS = [] ...@@ -191,12 +203,15 @@ TEST_TASKS = []
desc "Run collectstatic in the specified environment" desc "Run collectstatic in the specified environment"
task "#{system}:collectstatic:#{env}" => :predjango do task "#{system}:collectstatic:#{env}" => :predjango do
sh("#{django_admin(system, env, 'collectstatic', '--noinput')}") sh("#{django_admin(system, env, 'collectstatic', '--noinput')} > /tmp/collectstatic.out") do |ok, status|
if !ok
abort "collectstatic failed!"
end
end
end end
end end
end end
desc "Reset the relational database used by django. WARNING: this will delete all of your existing users" desc "Reset the relational database used by django. WARNING: this will delete all of your existing users"
task :resetdb, [:env] do |t, args| task :resetdb, [:env] do |t, args|
args.with_defaults(:env => 'dev') args.with_defaults(:env => 'dev')
...@@ -210,30 +225,39 @@ task :migrate, [:env] do |t, args| ...@@ -210,30 +225,39 @@ task :migrate, [:env] do |t, args|
sh(django_admin(:lms, args.env, 'migrate')) sh(django_admin(:lms, args.env, 'migrate'))
end end
Dir["common/lib/*"].each do |lib| Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib|
task_name = "test_#{lib}" task_name = "test_#{lib}"
report_dir = File.join(REPORT_DIR, task_name.gsub('/', '_')) report_dir = report_dir_path(lib)
directory report_dir
desc "Run tests for common lib #{lib}" desc "Run tests for common lib #{lib}"
task task_name => report_dir do task task_name => report_dir do
ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml") ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
sh("nosetests #{lib} --cover-erase --with-xunit --with-xcoverage --cover-html --cover-inclusive --cover-package #{File.basename(lib)} --cover-html-dir #{File.join(report_dir, "cover")}") cmd = "nosetests #{lib} --logging-clear-handlers --with-xunit"
sh(run_under_coverage(cmd, lib)) do |ok, res|
$failed_tests += 1 unless ok
end
end end
TEST_TASKS << task_name TEST_TASK_DIRS << lib
desc "Run tests for common lib #{lib} (without coverage)" desc "Run tests for common lib #{lib} (without coverage)"
task "fasttest_#{lib}" do task "fasttest_#{lib}" do
sh("nosetests #{lib}") sh("nosetests #{lib}")
end end
end
task :report_dirs
TEST_TASK_DIRS.each do |dir|
report_dir = report_dir_path(dir)
directory report_dir
task :report_dirs => [REPORT_DIR, report_dir]
end end
task :test do task :test do
TEST_TASKS.each do |task| TEST_TASK_DIRS.each do |dir|
Rake::Task[task].invoke(false) Rake::Task["test_#{dir}"].invoke(false)
end end
if $failed_tests > 0 if $failed_tests > 0
...@@ -241,6 +265,34 @@ task :test do ...@@ -241,6 +265,34 @@ task :test do
end end
end end
namespace :coverage do
desc "Build the html coverage reports"
task :html => :report_dirs do
TEST_TASK_DIRS.each do |dir|
report_dir = report_dir_path(dir)
if !File.file?("#{report_dir}/.coverage")
next
end
sh("coverage html --rcfile=#{dir}/.coveragerc")
end
end
desc "Build the xml coverage reports"
task :xml => :report_dirs do
TEST_TASK_DIRS.each do |dir|
report_dir = report_dir_path(dir)
if !File.file?("#{report_dir}/.coverage")
next
end
# Why doesn't the rcfile control the xml output file properly??
sh("coverage xml -o #{report_dir}/coverage.xml --rcfile=#{dir}/.coveragerc")
end
end
end
task :runserver => :lms task :runserver => :lms
desc "Run django-admin <action> against the specified system and environment" desc "Run django-admin <action> against the specified system and environment"
......
-e git://github.com/MITx/django-staticfiles.git@6d2504e5c8#egg=django-staticfiles -r github-requirements.txt
-e git://github.com/MITx/django-pipeline.git#egg=django-pipeline -r local-requirements.txt
-e git://github.com/MITx/django-wiki.git@e2e84558#egg=django-wiki \ No newline at end of file
-e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
-e common/lib/capa
-e common/lib/xmodule
...@@ -52,5 +52,6 @@ pil ...@@ -52,5 +52,6 @@ pil
nltk nltk
django-debug-toolbar-mongo django-debug-toolbar-mongo
dogstatsd-python dogstatsd-python
MySQL-python # Taking out MySQL-python for now because it requires mysql to be installed, so breaks updates on content folks' envs.
# MySQL-python
sphinx sphinx
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment