Merge pull request #1299 from edx/zoldak/remove-pearson-code
Remove code related to Pearson Testing Centers
This commit is contained in:
@@ -441,7 +441,6 @@ class CourseMetadataEditingTest(CourseTestCase):
|
||||
def test_update_from_json(self):
|
||||
test_model = CourseMetadata.update_from_json(self.course, {
|
||||
"advertised_start": "start A",
|
||||
"testcenter_info": {"c": "test"},
|
||||
"days_early_for_beta": 2
|
||||
})
|
||||
self.update_check(test_model)
|
||||
@@ -464,8 +463,6 @@ class CourseMetadataEditingTest(CourseTestCase):
|
||||
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value")
|
||||
self.assertIn('advertised_start', test_model, 'Missing new advertised_start metadata field')
|
||||
self.assertEqual(test_model['advertised_start'], 'start A', "advertised_start not expected value")
|
||||
self.assertIn('testcenter_info', test_model, 'Missing testcenter_info metadata field')
|
||||
self.assertDictEqual(test_model['testcenter_info'], {"c": "test"}, "testcenter_info not expected value")
|
||||
self.assertIn('days_early_for_beta', test_model, 'Missing days_early_for_beta metadata field')
|
||||
self.assertEqual(test_model['days_early_for_beta'], 2, "days_early_for_beta not expected value")
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ from django.core.exceptions import ValidationError
|
||||
if settings.MITX_FEATURES.get('AUTH_USE_CAS'):
|
||||
from django_cas.views import login as django_cas_login
|
||||
|
||||
from student.models import UserProfile, TestCenterUser, TestCenterRegistration
|
||||
from student.models import UserProfile
|
||||
|
||||
from django.http import HttpResponse, HttpResponseRedirect, HttpRequest, HttpResponseForbidden
|
||||
from django.utils.http import urlquote, is_safe_url
|
||||
@@ -880,146 +880,7 @@ def provider_xrds(request):
|
||||
return response
|
||||
|
||||
|
||||
#-------------------
|
||||
# Pearson
|
||||
#-------------------
|
||||
def course_from_id(course_id):
|
||||
"""Return the CourseDescriptor corresponding to this course_id"""
|
||||
course_loc = CourseDescriptor.id_to_location(course_id)
|
||||
return modulestore().get_instance(course_id, course_loc)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def test_center_login(request):
|
||||
''' Log in students taking exams via Pearson
|
||||
|
||||
Takes a POST request that contains the following keys:
|
||||
- code - a security code provided by Pearson
|
||||
- clientCandidateID
|
||||
- registrationID
|
||||
- exitURL - the url that we redirect to once we're done
|
||||
- vueExamSeriesCode - a code that indicates the exam that we're using
|
||||
'''
|
||||
# Imports from lms/djangoapps/courseware -- these should not be
|
||||
# in a common djangoapps.
|
||||
from courseware.views import get_module_for_descriptor, jump_to
|
||||
from courseware.model_data import FieldDataCache
|
||||
|
||||
# errors are returned by navigating to the error_url, adding a query parameter named "code"
|
||||
# which contains the error code describing the exceptional condition.
|
||||
def makeErrorURL(error_url, error_code):
|
||||
log.error("generating error URL with error code {}".format(error_code))
|
||||
return "{}?code={}".format(error_url, error_code)
|
||||
|
||||
# get provided error URL, which will be used as a known prefix for returning error messages to the
|
||||
# Pearson shell.
|
||||
error_url = request.POST.get("errorURL")
|
||||
|
||||
# TODO: check that the parameters have not been tampered with, by comparing the code provided by Pearson
|
||||
# with the code we calculate for the same parameters.
|
||||
if 'code' not in request.POST:
|
||||
return HttpResponseRedirect(makeErrorURL(error_url, "missingSecurityCode"))
|
||||
code = request.POST.get("code")
|
||||
|
||||
# calculate SHA for query string
|
||||
# TODO: figure out how to get the original query string, so we can hash it and compare.
|
||||
|
||||
if 'clientCandidateID' not in request.POST:
|
||||
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientCandidateID"))
|
||||
client_candidate_id = request.POST.get("clientCandidateID")
|
||||
|
||||
# TODO: check remaining parameters, and maybe at least log if they're not matching
|
||||
# expected values....
|
||||
# registration_id = request.POST.get("registrationID")
|
||||
# exit_url = request.POST.get("exitURL")
|
||||
|
||||
# find testcenter_user that matches the provided ID:
|
||||
try:
|
||||
testcenteruser = TestCenterUser.objects.get(client_candidate_id=client_candidate_id)
|
||||
except TestCenterUser.DoesNotExist:
|
||||
AUDIT_LOG.error("not able to find demographics for cand ID {}".format(client_candidate_id))
|
||||
return HttpResponseRedirect(makeErrorURL(error_url, "invalidClientCandidateID"))
|
||||
|
||||
AUDIT_LOG.info("Attempting to log in test-center user '{}' for test of cand {}".format(testcenteruser.user.username, client_candidate_id))
|
||||
|
||||
# find testcenter_registration that matches the provided exam code:
|
||||
# Note that we could rely in future on either the registrationId or the exam code,
|
||||
# or possibly both. But for now we know what to do with an ExamSeriesCode,
|
||||
# while we currently have no record of RegistrationID values at all.
|
||||
if 'vueExamSeriesCode' not in request.POST:
|
||||
# we are not allowed to make up a new error code, according to Pearson,
|
||||
# so instead of "missingExamSeriesCode", we use a valid one that is
|
||||
# inaccurate but at least distinct. (Sigh.)
|
||||
AUDIT_LOG.error("missing exam series code for cand ID {}".format(client_candidate_id))
|
||||
return HttpResponseRedirect(makeErrorURL(error_url, "missingPartnerID"))
|
||||
exam_series_code = request.POST.get('vueExamSeriesCode')
|
||||
|
||||
registrations = TestCenterRegistration.objects.filter(testcenter_user=testcenteruser, exam_series_code=exam_series_code)
|
||||
if not registrations:
|
||||
AUDIT_LOG.error("not able to find exam registration for exam {} and cand ID {}".format(exam_series_code, client_candidate_id))
|
||||
return HttpResponseRedirect(makeErrorURL(error_url, "noTestsAssigned"))
|
||||
|
||||
# TODO: figure out what to do if there are more than one registrations....
|
||||
# for now, just take the first...
|
||||
registration = registrations[0]
|
||||
|
||||
course_id = registration.course_id
|
||||
course = course_from_id(course_id) # assume it will be found....
|
||||
if not course:
|
||||
AUDIT_LOG.error("not able to find course from ID {} for cand ID {}".format(course_id, client_candidate_id))
|
||||
return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests"))
|
||||
exam = course.get_test_center_exam(exam_series_code)
|
||||
if not exam:
|
||||
AUDIT_LOG.error("not able to find exam {} for course ID {} and cand ID {}".format(exam_series_code, course_id, client_candidate_id))
|
||||
return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests"))
|
||||
location = exam.exam_url
|
||||
log.info("Proceeding with test of cand {} on exam {} for course {}: URL = {}".format(client_candidate_id, exam_series_code, course_id, location))
|
||||
|
||||
# check if the test has already been taken
|
||||
timelimit_descriptor = modulestore().get_instance(course_id, Location(location))
|
||||
if not timelimit_descriptor:
|
||||
log.error("cand {} on exam {} for course {}: descriptor not found for location {}".format(client_candidate_id, exam_series_code, course_id, location))
|
||||
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"))
|
||||
|
||||
timelimit_module_cache = FieldDataCache.cache_for_descriptor_descendents(course_id, testcenteruser.user,
|
||||
timelimit_descriptor, depth=None)
|
||||
timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor,
|
||||
timelimit_module_cache, course_id, position=None)
|
||||
if not timelimit_module.category == 'timelimit':
|
||||
log.error("cand {} on exam {} for course {}: non-timelimit module at location {}".format(client_candidate_id, exam_series_code, course_id, location))
|
||||
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"))
|
||||
|
||||
if timelimit_module and timelimit_module.has_ended:
|
||||
AUDIT_LOG.warning("cand {} on exam {} for course {}: test already over at {}".format(client_candidate_id, exam_series_code, course_id, timelimit_module.ending_at))
|
||||
return HttpResponseRedirect(makeErrorURL(error_url, "allTestsTaken"))
|
||||
|
||||
# check if we need to provide an accommodation:
|
||||
time_accommodation_mapping = {'ET12ET': 'ADDHALFTIME',
|
||||
'ET30MN': 'ADD30MIN',
|
||||
'ETDBTM': 'ADDDOUBLE', }
|
||||
|
||||
time_accommodation_code = None
|
||||
for code in registration.get_accommodation_codes():
|
||||
if code in time_accommodation_mapping:
|
||||
time_accommodation_code = time_accommodation_mapping[code]
|
||||
|
||||
if time_accommodation_code:
|
||||
timelimit_module.accommodation_code = time_accommodation_code
|
||||
AUDIT_LOG.info("cand {} on exam {} for course {}: receiving accommodation {}".format(client_candidate_id, exam_series_code, course_id, time_accommodation_code))
|
||||
|
||||
# UGLY HACK!!!
|
||||
# Login assumes that authentication has occurred, and that there is a
|
||||
# backend annotation on the user object, indicating which backend
|
||||
# against which the user was authenticated. We're authenticating here
|
||||
# against the registration entry, and assuming that the request given
|
||||
# this information is correct, we allow the user to be logged in
|
||||
# without a password. This could all be formalized in a backend object
|
||||
# that does the above checking.
|
||||
# TODO: (brian) create a backend class to do this.
|
||||
# testcenteruser.user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
|
||||
testcenteruser.user.backend = "%s.%s" % ("TestcenterAuthenticationModule", "TestcenterAuthenticationClass")
|
||||
login(request, testcenteruser.user)
|
||||
AUDIT_LOG.info("Logged in user '{}' for test of cand {} on exam {} for course {}: URL = {}".format(testcenteruser.user.username, client_candidate_id, exam_series_code, course_id, location))
|
||||
|
||||
# And start the test:
|
||||
return jump_to(request, course_id, location)
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
from optparse import make_option
|
||||
from json import dump
|
||||
from datetime import datetime
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from student.models import TestCenterRegistration
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
args = '<output JSON file>'
|
||||
help = """
|
||||
Dump information as JSON from TestCenterRegistration tables, including username and status.
|
||||
"""
|
||||
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option('--course_id',
|
||||
action='store',
|
||||
dest='course_id',
|
||||
help='Specify a particular course.'),
|
||||
make_option('--exam_series_code',
|
||||
action='store',
|
||||
dest='exam_series_code',
|
||||
default=None,
|
||||
help='Specify a particular exam, using the Pearson code'),
|
||||
make_option('--accommodation_pending',
|
||||
action='store_true',
|
||||
dest='accommodation_pending',
|
||||
default=False,
|
||||
),
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if len(args) < 1:
|
||||
outputfile = datetime.utcnow().strftime("pearson-dump-%Y%m%d-%H%M%S.json")
|
||||
else:
|
||||
outputfile = args[0]
|
||||
|
||||
# construct the query object to dump:
|
||||
registrations = TestCenterRegistration.objects.all()
|
||||
if 'course_id' in options and options['course_id']:
|
||||
registrations = registrations.filter(course_id=options['course_id'])
|
||||
if 'exam_series_code' in options and options['exam_series_code']:
|
||||
registrations = registrations.filter(exam_series_code=options['exam_series_code'])
|
||||
|
||||
# collect output:
|
||||
output = []
|
||||
for registration in registrations:
|
||||
if 'accommodation_pending' in options and options['accommodation_pending'] and not registration.accommodation_is_pending:
|
||||
continue
|
||||
record = {'username': registration.testcenter_user.user.username,
|
||||
'email': registration.testcenter_user.email,
|
||||
'first_name': registration.testcenter_user.first_name,
|
||||
'last_name': registration.testcenter_user.last_name,
|
||||
'client_candidate_id': registration.client_candidate_id,
|
||||
'client_authorization_id': registration.client_authorization_id,
|
||||
'course_id': registration.course_id,
|
||||
'exam_series_code': registration.exam_series_code,
|
||||
'accommodation_request': registration.accommodation_request,
|
||||
'accommodation_code': registration.accommodation_code,
|
||||
'registration_status': registration.registration_status(),
|
||||
'demographics_status': registration.demographics_status(),
|
||||
'accommodation_status': registration.accommodation_status(),
|
||||
}
|
||||
if len(registration.upload_error_message) > 0:
|
||||
record['registration_error'] = registration.upload_error_message
|
||||
if len(registration.testcenter_user.upload_error_message) > 0:
|
||||
record['demographics_error'] = registration.testcenter_user.upload_error_message
|
||||
if registration.needs_uploading:
|
||||
record['needs_uploading'] = True
|
||||
|
||||
output.append(record)
|
||||
|
||||
# dump output:
|
||||
with open(outputfile, 'w') as outfile:
|
||||
dump(output, outfile, indent=2)
|
||||
@@ -1,111 +0,0 @@
|
||||
import csv
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from optparse import make_option
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from student.models import TestCenterUser
|
||||
from pytz import UTC
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
CSV_TO_MODEL_FIELDS = OrderedDict([
|
||||
# Skipping optional field CandidateID
|
||||
("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
|
||||
])
|
||||
|
||||
# define defaults, even thought 'store_true' shouldn't need them.
|
||||
# (call_command will set None as default value for all options that don't have one,
|
||||
# so one cannot rely on presence/absence of flags in that world.)
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option('--dest-from-settings',
|
||||
action='store_true',
|
||||
dest='dest-from-settings',
|
||||
default=False,
|
||||
help='Retrieve the destination to export to from django.'),
|
||||
make_option('--destination',
|
||||
action='store',
|
||||
dest='destination',
|
||||
default=None,
|
||||
help='Where to store the exported files')
|
||||
)
|
||||
|
||||
def handle(self, **options):
|
||||
# update time should use UTC in order to be comparable to the user_updated_at
|
||||
# field
|
||||
uploaded_at = datetime.now(UTC)
|
||||
|
||||
# if specified destination is an existing directory, then
|
||||
# create a filename for it automatically. If it doesn't exist,
|
||||
# then we will create the directory.
|
||||
# Name will use timestamp -- this is UTC, so it will look funny,
|
||||
# but it should at least be consistent with the other timestamps
|
||||
# used in the system.
|
||||
if 'dest-from-settings' in options and options['dest-from-settings']:
|
||||
if 'LOCAL_EXPORT' in settings.PEARSON:
|
||||
dest = settings.PEARSON['LOCAL_EXPORT']
|
||||
else:
|
||||
raise CommandError('--dest-from-settings was enabled but the'
|
||||
'PEARSON[LOCAL_EXPORT] setting was not set.')
|
||||
elif 'destination' in options and options['destination']:
|
||||
dest = options['destination']
|
||||
else:
|
||||
raise CommandError('--destination or --dest-from-settings must be used')
|
||||
|
||||
if not os.path.isdir(dest):
|
||||
os.makedirs(dest)
|
||||
|
||||
destfile = os.path.join(dest, uploaded_at.strftime("cdd-%Y%m%d-%H%M%S.dat"))
|
||||
|
||||
# strings must be in latin-1 format. CSV parser will
|
||||
# otherwise convert unicode objects to ascii.
|
||||
def ensure_encoding(value):
|
||||
if isinstance(value, unicode):
|
||||
return value.encode('iso-8859-1')
|
||||
else:
|
||||
return value
|
||||
|
||||
# dump_all = options['dump_all']
|
||||
|
||||
with open(destfile, "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'):
|
||||
if tcu.needs_uploading: # or dump_all
|
||||
record = dict((csv_field, ensure_encoding(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)
|
||||
tcu.uploaded_at = uploaded_at
|
||||
tcu.save()
|
||||
@@ -1,103 +0,0 @@
|
||||
import csv
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from optparse import make_option
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from student.models import TestCenterRegistration, ACCOMMODATION_REJECTED_CODE
|
||||
from pytz import UTC
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
CSV_TO_MODEL_FIELDS = OrderedDict([
|
||||
('AuthorizationTransactionType', 'authorization_transaction_type'),
|
||||
('AuthorizationID', 'authorization_id'),
|
||||
('ClientAuthorizationID', 'client_authorization_id'),
|
||||
('ClientCandidateID', 'client_candidate_id'),
|
||||
('ExamAuthorizationCount', 'exam_authorization_count'),
|
||||
('ExamSeriesCode', 'exam_series_code'),
|
||||
('Accommodations', 'accommodation_code'),
|
||||
('EligibilityApptDateFirst', 'eligibility_appointment_date_first'),
|
||||
('EligibilityApptDateLast', 'eligibility_appointment_date_last'),
|
||||
("LastUpdate", "user_updated_at"), # in UTC, so same as what we store
|
||||
])
|
||||
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option('--dest-from-settings',
|
||||
action='store_true',
|
||||
dest='dest-from-settings',
|
||||
default=False,
|
||||
help='Retrieve the destination to export to from django.'),
|
||||
make_option('--destination',
|
||||
action='store',
|
||||
dest='destination',
|
||||
default=None,
|
||||
help='Where to store the exported files'),
|
||||
make_option('--dump_all',
|
||||
action='store_true',
|
||||
dest='dump_all',
|
||||
default=False,
|
||||
),
|
||||
make_option('--force_add',
|
||||
action='store_true',
|
||||
dest='force_add',
|
||||
default=False,
|
||||
),
|
||||
)
|
||||
|
||||
def handle(self, **options):
|
||||
# update time should use UTC in order to be comparable to the user_updated_at
|
||||
# field
|
||||
uploaded_at = datetime.now(UTC)
|
||||
|
||||
# if specified destination is an existing directory, then
|
||||
# create a filename for it automatically. If it doesn't exist,
|
||||
# then we will create the directory.
|
||||
# Name will use timestamp -- this is UTC, so it will look funny,
|
||||
# but it should at least be consistent with the other timestamps
|
||||
# used in the system.
|
||||
if 'dest-from-settings' in options and options['dest-from-settings']:
|
||||
if 'LOCAL_EXPORT' in settings.PEARSON:
|
||||
dest = settings.PEARSON['LOCAL_EXPORT']
|
||||
else:
|
||||
raise CommandError('--dest-from-settings was enabled but the'
|
||||
'PEARSON[LOCAL_EXPORT] setting was not set.')
|
||||
elif 'destination' in options and options['destination']:
|
||||
dest = options['destination']
|
||||
else:
|
||||
raise CommandError('--destination or --dest-from-settings must be used')
|
||||
|
||||
if not os.path.isdir(dest):
|
||||
os.makedirs(dest)
|
||||
|
||||
destfile = os.path.join(dest, uploaded_at.strftime("ead-%Y%m%d-%H%M%S.dat"))
|
||||
|
||||
dump_all = options['dump_all']
|
||||
|
||||
with open(destfile, "wb") as outfile:
|
||||
writer = csv.DictWriter(outfile,
|
||||
Command.CSV_TO_MODEL_FIELDS,
|
||||
delimiter="\t",
|
||||
quoting=csv.QUOTE_MINIMAL,
|
||||
extrasaction='ignore')
|
||||
writer.writeheader()
|
||||
for tcr in TestCenterRegistration.objects.order_by('id'):
|
||||
if dump_all or tcr.needs_uploading:
|
||||
record = dict((csv_field, getattr(tcr, 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")
|
||||
record["EligibilityApptDateFirst"] = record["EligibilityApptDateFirst"].strftime("%Y/%m/%d")
|
||||
record["EligibilityApptDateLast"] = record["EligibilityApptDateLast"].strftime("%Y/%m/%d")
|
||||
if record["Accommodations"] == ACCOMMODATION_REJECTED_CODE:
|
||||
record["Accommodations"] = ""
|
||||
if options['force_add']:
|
||||
record['AuthorizationTransactionType'] = 'Add'
|
||||
|
||||
writer.writerow(record)
|
||||
tcr.uploaded_at = uploaded_at
|
||||
tcr.save()
|
||||
@@ -1,119 +0,0 @@
|
||||
import csv
|
||||
from time import strptime, strftime
|
||||
from datetime import datetime
|
||||
from zipfile import ZipFile, is_zipfile
|
||||
|
||||
from dogapi import dog_http_api
|
||||
from pytz import UTC
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.conf import settings
|
||||
|
||||
import django_startup
|
||||
|
||||
from student.models import TestCenterUser, TestCenterRegistration
|
||||
|
||||
|
||||
django_startup.autostartup()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
args = '<input zip file>'
|
||||
help = """
|
||||
Import Pearson confirmation files and update TestCenterUser
|
||||
and TestCenterRegistration tables with status.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def datadog_error(string, tags):
|
||||
dog_http_api.event("Pearson Import", string, alert_type='error', tags=[tags])
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
if len(args) < 1:
|
||||
print Command.help
|
||||
return
|
||||
|
||||
source_zip = args[0]
|
||||
if not is_zipfile(source_zip):
|
||||
error = "Input file is not a zipfile: \"{}\"".format(source_zip)
|
||||
Command.datadog_error(error, source_zip)
|
||||
raise CommandError(error)
|
||||
|
||||
# loop through all files in zip, and process them based on filename prefix:
|
||||
with ZipFile(source_zip, 'r') as zipfile:
|
||||
for fileinfo in zipfile.infolist():
|
||||
with zipfile.open(fileinfo) as zipentry:
|
||||
if fileinfo.filename.startswith("eac-"):
|
||||
self.process_eac(zipentry)
|
||||
elif fileinfo.filename.startswith("vcdc-"):
|
||||
self.process_vcdc(zipentry)
|
||||
else:
|
||||
error = "Unrecognized confirmation file type\"{}\" in confirmation zip file \"{}\"".format(fileinfo.filename, zipfile)
|
||||
Command.datadog_error(error, source_zip)
|
||||
raise CommandError(error)
|
||||
|
||||
def process_eac(self, eacfile):
|
||||
print "processing eac"
|
||||
reader = csv.DictReader(eacfile, delimiter="\t")
|
||||
for row in reader:
|
||||
client_authorization_id = row['ClientAuthorizationID']
|
||||
if not client_authorization_id:
|
||||
if row['Status'] == 'Error':
|
||||
Command.datadog_error("Error in EAD file processing ({}): {}".format(row['Date'], row['Message']), eacfile.name)
|
||||
else:
|
||||
Command.datadog_error("Encountered bad record: {}".format(row), eacfile.name)
|
||||
else:
|
||||
try:
|
||||
registration = TestCenterRegistration.objects.get(client_authorization_id=client_authorization_id)
|
||||
Command.datadog_error("Found authorization record for user {}".format(registration.testcenter_user.user.username), eacfile.name)
|
||||
# now update the record:
|
||||
registration.upload_status = row['Status']
|
||||
registration.upload_error_message = row['Message']
|
||||
try:
|
||||
registration.processed_at = strftime('%Y-%m-%d %H:%M:%S', strptime(row['Date'], '%Y/%m/%d %H:%M:%S'))
|
||||
except ValueError as ve:
|
||||
Command.datadog_error("Bad Date value found for {}: message {}".format(client_authorization_id, ve), eacfile.name)
|
||||
# store the authorization Id if one is provided. (For debugging)
|
||||
if row['AuthorizationID']:
|
||||
try:
|
||||
registration.authorization_id = int(row['AuthorizationID'])
|
||||
except ValueError as ve:
|
||||
Command.datadog_error("Bad AuthorizationID value found for {}: message {}".format(client_authorization_id, ve), eacfile.name)
|
||||
|
||||
registration.confirmed_at = datetime.now(UTC)
|
||||
registration.save()
|
||||
except TestCenterRegistration.DoesNotExist:
|
||||
Command.datadog_error("Failed to find record for client_auth_id {}".format(client_authorization_id), eacfile.name)
|
||||
|
||||
def process_vcdc(self, vcdcfile):
|
||||
print "processing vcdc"
|
||||
reader = csv.DictReader(vcdcfile, delimiter="\t")
|
||||
for row in reader:
|
||||
client_candidate_id = row['ClientCandidateID']
|
||||
if not client_candidate_id:
|
||||
if row['Status'] == 'Error':
|
||||
Command.datadog_error("Error in CDD file processing ({}): {}".format(row['Date'], row['Message']), vcdcfile.name)
|
||||
else:
|
||||
Command.datadog_error("Encountered bad record: {}".format(row), vcdcfile.name)
|
||||
else:
|
||||
try:
|
||||
tcuser = TestCenterUser.objects.get(client_candidate_id=client_candidate_id)
|
||||
Command.datadog_error("Found demographics record for user {}".format(tcuser.user.username), vcdcfile.name)
|
||||
# now update the record:
|
||||
tcuser.upload_status = row['Status']
|
||||
tcuser.upload_error_message = row['Message']
|
||||
try:
|
||||
tcuser.processed_at = strftime('%Y-%m-%d %H:%M:%S', strptime(row['Date'], '%Y/%m/%d %H:%M:%S'))
|
||||
except ValueError as ve:
|
||||
Command.datadog_error("Bad Date value found for {}: message {}".format(client_candidate_id, ve), vcdcfile.name)
|
||||
# store the candidate Id if one is provided. (For debugging)
|
||||
if row['CandidateID']:
|
||||
try:
|
||||
tcuser.candidate_id = int(row['CandidateID'])
|
||||
except ValueError as ve:
|
||||
Command.datadog_error("Bad CandidateID value found for {}: message {}".format(client_candidate_id, ve), vcdcfile.name)
|
||||
tcuser.confirmed_at = datetime.utcnow()
|
||||
tcuser.save()
|
||||
except TestCenterUser.DoesNotExist:
|
||||
Command.datadog_error(" Failed to find record for client_candidate_id {}".format(client_candidate_id), vcdcfile.name)
|
||||
@@ -1,206 +0,0 @@
|
||||
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, TestCenterRegistration, TestCenterRegistrationForm, get_testcenter_registration
|
||||
from student.views import course_from_id
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
option_list = BaseCommand.option_list + (
|
||||
# registration info:
|
||||
make_option(
|
||||
'--accommodation_request',
|
||||
action='store',
|
||||
dest='accommodation_request',
|
||||
),
|
||||
make_option(
|
||||
'--accommodation_code',
|
||||
action='store',
|
||||
dest='accommodation_code',
|
||||
),
|
||||
make_option(
|
||||
'--client_authorization_id',
|
||||
action='store',
|
||||
dest='client_authorization_id',
|
||||
),
|
||||
# exam info:
|
||||
make_option(
|
||||
'--exam_series_code',
|
||||
action='store',
|
||||
dest='exam_series_code',
|
||||
),
|
||||
make_option(
|
||||
'--eligibility_appointment_date_first',
|
||||
action='store',
|
||||
dest='eligibility_appointment_date_first',
|
||||
help='use YYYY-MM-DD format if overriding existing course values, or YYYY-MM-DDTHH:MM if not using an existing course.'
|
||||
),
|
||||
make_option(
|
||||
'--eligibility_appointment_date_last',
|
||||
action='store',
|
||||
dest='eligibility_appointment_date_last',
|
||||
help='use YYYY-MM-DD format if overriding existing course values, or YYYY-MM-DDTHH:MM if not using an existing course.'
|
||||
),
|
||||
# internal values:
|
||||
make_option(
|
||||
'--authorization_id',
|
||||
action='store',
|
||||
dest='authorization_id',
|
||||
help='ID we receive from Pearson for a particular authorization'
|
||||
),
|
||||
make_option(
|
||||
'--upload_status',
|
||||
action='store',
|
||||
dest='upload_status',
|
||||
help='status value assigned by Pearson'
|
||||
),
|
||||
make_option(
|
||||
'--upload_error_message',
|
||||
action='store',
|
||||
dest='upload_error_message',
|
||||
help='error message provided by Pearson on a failure.'
|
||||
),
|
||||
# control values:
|
||||
make_option(
|
||||
'--ignore_registration_dates',
|
||||
action='store_true',
|
||||
dest='ignore_registration_dates',
|
||||
help='find exam info for course based on exam_series_code, even if the exam is not active.'
|
||||
),
|
||||
make_option(
|
||||
'--create_dummy_exam',
|
||||
action='store_true',
|
||||
dest='create_dummy_exam',
|
||||
help='create dummy exam info for course, even if course exists'
|
||||
),
|
||||
)
|
||||
args = "<student_username course_id>"
|
||||
help = "Create or modify a TestCenterRegistration 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]
|
||||
course_id = args[1]
|
||||
print username, course_id
|
||||
|
||||
our_options = dict((k, v) for k, v in options.items()
|
||||
if Command.is_valid_option(k) and v is not None)
|
||||
try:
|
||||
student = User.objects.get(username=username)
|
||||
except User.DoesNotExist:
|
||||
raise CommandError("User \"{}\" does not exist".format(username))
|
||||
|
||||
try:
|
||||
testcenter_user = TestCenterUser.objects.get(user=student)
|
||||
except TestCenterUser.DoesNotExist:
|
||||
raise CommandError("User \"{}\" does not have an existing demographics record".format(username))
|
||||
|
||||
# get an "exam" object. Check to see if a course_id was specified, and use information from that:
|
||||
exam = None
|
||||
create_dummy_exam = 'create_dummy_exam' in our_options and our_options['create_dummy_exam']
|
||||
if not create_dummy_exam:
|
||||
try:
|
||||
course = course_from_id(course_id)
|
||||
if 'ignore_registration_dates' in our_options:
|
||||
examlist = [exam for exam in course.test_center_exams if exam.exam_series_code == our_options.get('exam_series_code')]
|
||||
exam = examlist[0] if len(examlist) > 0 else None
|
||||
else:
|
||||
exam = course.current_test_center_exam
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
else:
|
||||
# otherwise use explicit values (so we don't have to define a course):
|
||||
exam_name = "Dummy Placeholder Name"
|
||||
exam_info = {'Exam_Series_Code': our_options['exam_series_code'],
|
||||
'First_Eligible_Appointment_Date': our_options['eligibility_appointment_date_first'],
|
||||
'Last_Eligible_Appointment_Date': our_options['eligibility_appointment_date_last'],
|
||||
}
|
||||
exam = CourseDescriptor.TestCenterExam(course_id, exam_name, exam_info)
|
||||
# update option values for date_first and date_last to use YYYY-MM-DD format
|
||||
# instead of YYYY-MM-DDTHH:MM
|
||||
our_options['eligibility_appointment_date_first'] = exam.first_eligible_appointment_date.strftime("%Y-%m-%d")
|
||||
our_options['eligibility_appointment_date_last'] = exam.last_eligible_appointment_date.strftime("%Y-%m-%d")
|
||||
|
||||
if exam is None:
|
||||
raise CommandError("Exam for course_id {} does not exist".format(course_id))
|
||||
|
||||
exam_code = exam.exam_series_code
|
||||
|
||||
UPDATE_FIELDS = ('accommodation_request',
|
||||
'accommodation_code',
|
||||
'client_authorization_id',
|
||||
'exam_series_code',
|
||||
'eligibility_appointment_date_first',
|
||||
'eligibility_appointment_date_last',
|
||||
)
|
||||
|
||||
# create and save the registration:
|
||||
needs_updating = False
|
||||
registrations = get_testcenter_registration(student, course_id, exam_code)
|
||||
if len(registrations) > 0:
|
||||
registration = registrations[0]
|
||||
for fieldname in UPDATE_FIELDS:
|
||||
if fieldname in our_options and registration.__getattribute__(fieldname) != our_options[fieldname]:
|
||||
needs_updating = True;
|
||||
else:
|
||||
accommodation_request = our_options.get('accommodation_request', '')
|
||||
registration = TestCenterRegistration.create(testcenter_user, exam, accommodation_request)
|
||||
needs_updating = True
|
||||
|
||||
|
||||
if needs_updating:
|
||||
# first update the record with the new values, if any:
|
||||
for fieldname in UPDATE_FIELDS:
|
||||
if fieldname in our_options and fieldname not in TestCenterRegistrationForm.Meta.fields:
|
||||
registration.__setattr__(fieldname, our_options[fieldname])
|
||||
|
||||
# the registration form normally populates the data dict with
|
||||
# the accommodation request (if any). But here we want to
|
||||
# specify only those values that might change, so update the dict with existing
|
||||
# values.
|
||||
form_options = dict(our_options)
|
||||
for propname in TestCenterRegistrationForm.Meta.fields:
|
||||
if propname not in form_options:
|
||||
form_options[propname] = registration.__getattribute__(propname)
|
||||
form = TestCenterRegistrationForm(instance=registration, data=form_options)
|
||||
if form.is_valid():
|
||||
form.update_and_save()
|
||||
print "Updated registration information for user's registration: username \"{}\" course \"{}\", examcode \"{}\"".format(student.username, course_id, exam_code)
|
||||
else:
|
||||
if (len(form.errors) > 0):
|
||||
print "Field Form errors encountered:"
|
||||
for fielderror in form.errors:
|
||||
for msg in form.errors[fielderror]:
|
||||
print "Field Form Error: {} -- {}".format(fielderror, msg)
|
||||
if (len(form.non_field_errors()) > 0):
|
||||
print "Non-field Form errors encountered:"
|
||||
for nonfielderror in form.non_field_errors:
|
||||
print "Non-field Form Error: %s" % nonfielderror
|
||||
|
||||
else:
|
||||
print "No changes necessary to make to existing user's registration."
|
||||
|
||||
# override internal values:
|
||||
change_internal = False
|
||||
if 'exam_series_code' in our_options:
|
||||
exam_code = our_options['exam_series_code']
|
||||
registration = get_testcenter_registration(student, course_id, exam_code)[0]
|
||||
for internal_field in ['upload_error_message', 'upload_status', 'authorization_id']:
|
||||
if internal_field in our_options:
|
||||
registration.__setattr__(internal_field, our_options[internal_field])
|
||||
change_internal = True
|
||||
|
||||
if change_internal:
|
||||
print "Updated confirmation information in existing user's registration."
|
||||
registration.save()
|
||||
else:
|
||||
print "No changes necessary to make to confirmation information in existing user's registration."
|
||||
@@ -1,190 +0,0 @@
|
||||
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, TestCenterUserForm
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
option_list = BaseCommand.option_list + (
|
||||
# demographics:
|
||||
make_option(
|
||||
'--first_name',
|
||||
action='store',
|
||||
dest='first_name',
|
||||
),
|
||||
make_option(
|
||||
'--middle_name',
|
||||
action='store',
|
||||
dest='middle_name',
|
||||
),
|
||||
make_option(
|
||||
'--last_name',
|
||||
action='store',
|
||||
dest='last_name',
|
||||
),
|
||||
make_option(
|
||||
'--suffix',
|
||||
action='store',
|
||||
dest='suffix',
|
||||
),
|
||||
make_option(
|
||||
'--salutation',
|
||||
action='store',
|
||||
dest='salutation',
|
||||
),
|
||||
make_option(
|
||||
'--address_1',
|
||||
action='store',
|
||||
dest='address_1',
|
||||
),
|
||||
make_option(
|
||||
'--address_2',
|
||||
action='store',
|
||||
dest='address_2',
|
||||
),
|
||||
make_option(
|
||||
'--address_3',
|
||||
action='store',
|
||||
dest='address_3',
|
||||
),
|
||||
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(
|
||||
'--extension',
|
||||
action='store',
|
||||
dest='extension',
|
||||
),
|
||||
make_option(
|
||||
'--phone_country_code',
|
||||
action='store',
|
||||
dest='phone_country_code',
|
||||
help='Phone country code, just "1" for the USA'
|
||||
),
|
||||
make_option(
|
||||
'--fax',
|
||||
action='store',
|
||||
dest='fax',
|
||||
help='Pretty free-form (parens, spaces, dashes), but no country code'
|
||||
),
|
||||
make_option(
|
||||
'--fax_country_code',
|
||||
action='store',
|
||||
dest='fax_country_code',
|
||||
help='Fax country code, just "1" for the USA'
|
||||
),
|
||||
make_option(
|
||||
'--company_name',
|
||||
action='store',
|
||||
dest='company_name',
|
||||
),
|
||||
# internal values:
|
||||
make_option(
|
||||
'--client_candidate_id',
|
||||
action='store',
|
||||
dest='client_candidate_id',
|
||||
help='ID we assign a user to identify them to Pearson'
|
||||
),
|
||||
make_option(
|
||||
'--upload_status',
|
||||
action='store',
|
||||
dest='upload_status',
|
||||
help='status value assigned by Pearson'
|
||||
),
|
||||
make_option(
|
||||
'--upload_error_message',
|
||||
action='store',
|
||||
dest='upload_error_message',
|
||||
help='error message provided by Pearson on a failure.'
|
||||
),
|
||||
)
|
||||
args = "<student_username>"
|
||||
help = "Create or modify 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) and v is not None)
|
||||
student = User.objects.get(username=username)
|
||||
try:
|
||||
testcenter_user = TestCenterUser.objects.get(user=student)
|
||||
needs_updating = testcenter_user.needs_update(our_options)
|
||||
except TestCenterUser.DoesNotExist:
|
||||
# do additional initialization here:
|
||||
testcenter_user = TestCenterUser.create(student)
|
||||
needs_updating = True
|
||||
|
||||
if needs_updating:
|
||||
# the registration form normally populates the data dict with
|
||||
# all values from the testcenter_user. But here we only want to
|
||||
# specify those values that change, so update the dict with existing
|
||||
# values.
|
||||
form_options = dict(our_options)
|
||||
for propname in TestCenterUser.user_provided_fields():
|
||||
if propname not in form_options:
|
||||
form_options[propname] = testcenter_user.__getattribute__(propname)
|
||||
form = TestCenterUserForm(instance=testcenter_user, data=form_options)
|
||||
if form.is_valid():
|
||||
form.update_and_save()
|
||||
else:
|
||||
errorlist = []
|
||||
if (len(form.errors) > 0):
|
||||
errorlist.append("Field Form errors encountered:")
|
||||
for fielderror in form.errors:
|
||||
errorlist.append("Field Form Error: {}".format(fielderror))
|
||||
if (len(form.non_field_errors()) > 0):
|
||||
errorlist.append("Non-field Form errors encountered:")
|
||||
for nonfielderror in form.non_field_errors:
|
||||
errorlist.append("Non-field Form Error: {}".format(nonfielderror))
|
||||
raise CommandError("\n".join(errorlist))
|
||||
else:
|
||||
print "No changes necessary to make to existing user's demographics."
|
||||
|
||||
# override internal values:
|
||||
change_internal = False
|
||||
testcenter_user = TestCenterUser.objects.get(user=student)
|
||||
for internal_field in ['upload_error_message', 'upload_status', 'client_candidate_id']:
|
||||
if internal_field in our_options:
|
||||
testcenter_user.__setattr__(internal_field, our_options[internal_field])
|
||||
change_internal = True
|
||||
|
||||
if change_internal:
|
||||
testcenter_user.save()
|
||||
print "Updated confirmation information in existing user's demographics."
|
||||
else:
|
||||
print "No changes necessary to make to confirmation information in existing user's demographics."
|
||||
@@ -1,167 +0,0 @@
|
||||
from optparse import make_option
|
||||
import os
|
||||
from stat import S_ISDIR
|
||||
|
||||
import boto
|
||||
from dogapi import dog_http_api, dog_stats_api
|
||||
import paramiko
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
import django_startup
|
||||
|
||||
|
||||
django_startup.autostartup()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = """
|
||||
This command handles the importing and exporting of student records for
|
||||
Pearson. It uses some other Django commands to export and import the
|
||||
files and then uploads over SFTP to Pearson and stuffs the entry in an
|
||||
S3 bucket for archive purposes.
|
||||
|
||||
Usage: ./manage.py pearson-transfer --mode [import|export|both]
|
||||
"""
|
||||
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option('--mode',
|
||||
action='store',
|
||||
dest='mode',
|
||||
default='both',
|
||||
choices=('import', 'export', 'both'),
|
||||
help='mode is import, export, or both'),
|
||||
)
|
||||
|
||||
def handle(self, **options):
|
||||
|
||||
if not hasattr(settings, 'PEARSON'):
|
||||
raise CommandError('No PEARSON entries in auth/env.json.')
|
||||
|
||||
# check settings needed for either import or export:
|
||||
for value in ['SFTP_HOSTNAME', 'SFTP_USERNAME', 'SFTP_PASSWORD', 'S3_BUCKET']:
|
||||
if value not in settings.PEARSON:
|
||||
raise CommandError('No entry in the PEARSON settings'
|
||||
'(env/auth.json) for {0}'.format(value))
|
||||
|
||||
for value in ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY']:
|
||||
if not hasattr(settings, value):
|
||||
raise CommandError('No entry in the AWS settings'
|
||||
'(env/auth.json) for {0}'.format(value))
|
||||
|
||||
# check additional required settings for import and export:
|
||||
if options['mode'] in ('export', 'both'):
|
||||
for value in ['LOCAL_EXPORT', 'SFTP_EXPORT']:
|
||||
if value not in settings.PEARSON:
|
||||
raise CommandError('No entry in the PEARSON settings'
|
||||
'(env/auth.json) for {0}'.format(value))
|
||||
# make sure that the import directory exists or can be created:
|
||||
source_dir = settings.PEARSON['LOCAL_EXPORT']
|
||||
if not os.path.isdir(source_dir):
|
||||
os.makedirs(source_dir)
|
||||
|
||||
if options['mode'] in ('import', 'both'):
|
||||
for value in ['LOCAL_IMPORT', 'SFTP_IMPORT']:
|
||||
if value not in settings.PEARSON:
|
||||
raise CommandError('No entry in the PEARSON settings'
|
||||
'(env/auth.json) for {0}'.format(value))
|
||||
# make sure that the import directory exists or can be created:
|
||||
dest_dir = settings.PEARSON['LOCAL_IMPORT']
|
||||
if not os.path.isdir(dest_dir):
|
||||
os.makedirs(dest_dir)
|
||||
|
||||
|
||||
def sftp(files_from, files_to, mode, deleteAfterCopy=False):
|
||||
with dog_stats_api.timer('pearson.{0}'.format(mode), tags='sftp'):
|
||||
try:
|
||||
t = paramiko.Transport((settings.PEARSON['SFTP_HOSTNAME'], 22))
|
||||
t.connect(username=settings.PEARSON['SFTP_USERNAME'],
|
||||
password=settings.PEARSON['SFTP_PASSWORD'])
|
||||
sftp = paramiko.SFTPClient.from_transport(t)
|
||||
|
||||
if mode == 'export':
|
||||
try:
|
||||
sftp.chdir(files_to)
|
||||
except IOError:
|
||||
raise CommandError('SFTP destination path does not exist: {}'.format(files_to))
|
||||
for filename in os.listdir(files_from):
|
||||
sftp.put(files_from + '/' + filename, filename)
|
||||
if deleteAfterCopy:
|
||||
os.remove(os.path.join(files_from, filename))
|
||||
else:
|
||||
try:
|
||||
sftp.chdir(files_from)
|
||||
except IOError:
|
||||
raise CommandError('SFTP source path does not exist: {}'.format(files_from))
|
||||
for filename in sftp.listdir('.'):
|
||||
# skip subdirectories
|
||||
if not S_ISDIR(sftp.stat(filename).st_mode):
|
||||
sftp.get(filename, files_to + '/' + filename)
|
||||
# delete files from sftp server once they are successfully pulled off:
|
||||
if deleteAfterCopy:
|
||||
sftp.remove(filename)
|
||||
except:
|
||||
dog_http_api.event('pearson {0}'.format(mode),
|
||||
'sftp uploading failed',
|
||||
alert_type='error')
|
||||
raise
|
||||
finally:
|
||||
sftp.close()
|
||||
t.close()
|
||||
|
||||
def s3(files_from, bucket, mode, deleteAfterCopy=False):
|
||||
with dog_stats_api.timer('pearson.{0}'.format(mode), tags='s3'):
|
||||
try:
|
||||
for filename in os.listdir(files_from):
|
||||
source_file = os.path.join(files_from, filename)
|
||||
# use mode as name of directory into which to write files
|
||||
dest_file = os.path.join(mode, filename)
|
||||
upload_file_to_s3(bucket, source_file, dest_file)
|
||||
if deleteAfterCopy:
|
||||
os.remove(files_from + '/' + filename)
|
||||
except:
|
||||
dog_http_api.event('pearson {0}'.format(mode),
|
||||
's3 archiving failed')
|
||||
raise
|
||||
|
||||
def upload_file_to_s3(bucket, source_file, dest_file):
|
||||
"""
|
||||
Upload file to S3
|
||||
"""
|
||||
s3 = boto.connect_s3(settings.AWS_ACCESS_KEY_ID,
|
||||
settings.AWS_SECRET_ACCESS_KEY)
|
||||
from boto.s3.key import Key
|
||||
b = s3.get_bucket(bucket)
|
||||
k = Key(b)
|
||||
k.key = "{filename}".format(filename=dest_file)
|
||||
k.set_contents_from_filename(source_file)
|
||||
|
||||
def export_pearson():
|
||||
options = {'dest-from-settings': True}
|
||||
call_command('pearson_export_cdd', **options)
|
||||
call_command('pearson_export_ead', **options)
|
||||
mode = 'export'
|
||||
sftp(settings.PEARSON['LOCAL_EXPORT'], settings.PEARSON['SFTP_EXPORT'], mode, deleteAfterCopy=False)
|
||||
s3(settings.PEARSON['LOCAL_EXPORT'], settings.PEARSON['S3_BUCKET'], mode, deleteAfterCopy=True)
|
||||
|
||||
def import_pearson():
|
||||
mode = 'import'
|
||||
try:
|
||||
sftp(settings.PEARSON['SFTP_IMPORT'], settings.PEARSON['LOCAL_IMPORT'], mode, deleteAfterCopy=True)
|
||||
s3(settings.PEARSON['LOCAL_IMPORT'], settings.PEARSON['S3_BUCKET'], mode, deleteAfterCopy=False)
|
||||
except Exception as e:
|
||||
dog_http_api.event('Pearson Import failure', str(e))
|
||||
raise e
|
||||
else:
|
||||
for filename in os.listdir(settings.PEARSON['LOCAL_IMPORT']):
|
||||
filepath = os.path.join(settings.PEARSON['LOCAL_IMPORT'], filename)
|
||||
call_command('pearson_import_conf_zip', filepath)
|
||||
os.remove(filepath)
|
||||
|
||||
# actually do the work!
|
||||
if options['mode'] in ('export', 'both'):
|
||||
export_pearson()
|
||||
if options['mode'] in ('import', 'both'):
|
||||
import_pearson()
|
||||
@@ -1,380 +0,0 @@
|
||||
'''
|
||||
Created on Jan 17, 2013
|
||||
|
||||
@author: brian
|
||||
'''
|
||||
import logging
|
||||
import os
|
||||
from tempfile import mkdtemp
|
||||
import cStringIO
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from django.test import TestCase
|
||||
from django.core.management import call_command
|
||||
from nose.plugins.skip import SkipTest
|
||||
|
||||
from student.models import User, TestCenterUser, get_testcenter_registration
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_tc_user(username):
|
||||
user = User.objects.create_user(username, '{}@edx.org'.format(username), 'fakepass')
|
||||
options = {
|
||||
'first_name': 'TestFirst',
|
||||
'last_name': 'TestLast',
|
||||
'address_1': 'Test Address',
|
||||
'city': 'TestCity',
|
||||
'state': 'Alberta',
|
||||
'postal_code': 'A0B 1C2',
|
||||
'country': 'CAN',
|
||||
'phone': '252-1866',
|
||||
'phone_country_code': '1',
|
||||
}
|
||||
call_command('pearson_make_tc_user', username, **options)
|
||||
return TestCenterUser.objects.get(user=user)
|
||||
|
||||
|
||||
def create_tc_registration(username, course_id='org1/course1/term1', exam_code='exam1', accommodation_code=None):
|
||||
|
||||
options = {'exam_series_code': exam_code,
|
||||
'eligibility_appointment_date_first': '2013-01-01T00:00',
|
||||
'eligibility_appointment_date_last': '2013-12-31T23:59',
|
||||
'accommodation_code': accommodation_code,
|
||||
'create_dummy_exam': True,
|
||||
}
|
||||
|
||||
call_command('pearson_make_tc_registration', username, course_id, **options)
|
||||
user = User.objects.get(username=username)
|
||||
registrations = get_testcenter_registration(user, course_id, exam_code)
|
||||
return registrations[0]
|
||||
|
||||
|
||||
def create_multiple_registrations(prefix='test'):
|
||||
username1 = '{}_multiple1'.format(prefix)
|
||||
create_tc_user(username1)
|
||||
create_tc_registration(username1)
|
||||
create_tc_registration(username1, course_id='org1/course2/term1')
|
||||
create_tc_registration(username1, exam_code='exam2')
|
||||
username2 = '{}_multiple2'.format(prefix)
|
||||
create_tc_user(username2)
|
||||
create_tc_registration(username2)
|
||||
username3 = '{}_multiple3'.format(prefix)
|
||||
create_tc_user(username3)
|
||||
create_tc_registration(username3, course_id='org1/course2/term1')
|
||||
username4 = '{}_multiple4'.format(prefix)
|
||||
create_tc_user(username4)
|
||||
create_tc_registration(username4, exam_code='exam2')
|
||||
|
||||
|
||||
def get_command_error_text(*args, **options):
|
||||
stderr_string = None
|
||||
old_stderr = sys.stderr
|
||||
sys.stderr = cStringIO.StringIO()
|
||||
try:
|
||||
call_command(*args, **options)
|
||||
except SystemExit, why1:
|
||||
# The goal here is to catch CommandError calls.
|
||||
# But these are actually translated into nice messages,
|
||||
# and sys.exit(1) is then called. For testing, we
|
||||
# want to catch what sys.exit throws, and get the
|
||||
# relevant text either from stdout or stderr.
|
||||
if (why1.message > 0):
|
||||
stderr_string = sys.stderr.getvalue()
|
||||
else:
|
||||
raise why1
|
||||
except Exception, why:
|
||||
raise why
|
||||
|
||||
finally:
|
||||
sys.stderr = old_stderr
|
||||
|
||||
if stderr_string is None:
|
||||
raise Exception("Expected call to {} to fail, but it succeeded!".format(args[0]))
|
||||
return stderr_string
|
||||
|
||||
|
||||
def get_error_string_for_management_call(*args, **options):
|
||||
stdout_string = None
|
||||
old_stdout = sys.stdout
|
||||
old_stderr = sys.stderr
|
||||
sys.stdout = cStringIO.StringIO()
|
||||
sys.stderr = cStringIO.StringIO()
|
||||
try:
|
||||
call_command(*args, **options)
|
||||
except SystemExit, why1:
|
||||
# The goal here is to catch CommandError calls.
|
||||
# But these are actually translated into nice messages,
|
||||
# and sys.exit(1) is then called. For testing, we
|
||||
# want to catch what sys.exit throws, and get the
|
||||
# relevant text either from stdout or stderr.
|
||||
if (why1.message == 1):
|
||||
stdout_string = sys.stdout.getvalue()
|
||||
stderr_string = sys.stderr.getvalue()
|
||||
else:
|
||||
raise why1
|
||||
except Exception, why:
|
||||
raise why
|
||||
|
||||
finally:
|
||||
sys.stdout = old_stdout
|
||||
sys.stderr = old_stderr
|
||||
|
||||
if stdout_string is None:
|
||||
raise Exception("Expected call to {} to fail, but it succeeded!".format(args[0]))
|
||||
return stdout_string, stderr_string
|
||||
|
||||
|
||||
def get_file_info(dirpath):
|
||||
filelist = os.listdir(dirpath)
|
||||
print 'Files found: {}'.format(filelist)
|
||||
numfiles = len(filelist)
|
||||
if numfiles == 1:
|
||||
filepath = os.path.join(dirpath, filelist[0])
|
||||
with open(filepath, 'r') as cddfile:
|
||||
filecontents = cddfile.readlines()
|
||||
numlines = len(filecontents)
|
||||
return filepath, numlines
|
||||
else:
|
||||
raise Exception("Expected to find a single file in {}, but found {}".format(dirpath, filelist))
|
||||
|
||||
|
||||
class PearsonTestCase(TestCase):
|
||||
'''
|
||||
Base class for tests running Pearson-related commands
|
||||
'''
|
||||
|
||||
def assertErrorContains(self, error_message, expected):
|
||||
self.assertTrue(error_message.find(expected) >= 0, 'error message "{}" did not contain "{}"'.format(error_message, expected))
|
||||
|
||||
def setUp(self):
|
||||
self.import_dir = mkdtemp(prefix="import")
|
||||
self.addCleanup(shutil.rmtree, self.import_dir)
|
||||
self.export_dir = mkdtemp(prefix="export")
|
||||
self.addCleanup(shutil.rmtree, self.export_dir)
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
# and clean up the database:
|
||||
# TestCenterUser.objects.all().delete()
|
||||
# TestCenterRegistration.objects.all().delete()
|
||||
|
||||
|
||||
class PearsonCommandTestCase(PearsonTestCase):
|
||||
|
||||
def test_missing_demographic_fields(self):
|
||||
# We won't bother to test all details of form validation here.
|
||||
# It is enough to show that it works here, but deal with test cases for the form
|
||||
# validation in the student tests, not these management tests.
|
||||
username = 'baduser'
|
||||
User.objects.create_user(username, '{}@edx.org'.format(username), 'fakepass')
|
||||
options = {}
|
||||
error_string = get_command_error_text('pearson_make_tc_user', username, **options)
|
||||
self.assertTrue(error_string.find('Field Form errors encountered:') >= 0)
|
||||
self.assertTrue(error_string.find('Field Form Error: city') >= 0)
|
||||
self.assertTrue(error_string.find('Field Form Error: first_name') >= 0)
|
||||
self.assertTrue(error_string.find('Field Form Error: last_name') >= 0)
|
||||
self.assertTrue(error_string.find('Field Form Error: country') >= 0)
|
||||
self.assertTrue(error_string.find('Field Form Error: phone_country_code') >= 0)
|
||||
self.assertTrue(error_string.find('Field Form Error: phone') >= 0)
|
||||
self.assertTrue(error_string.find('Field Form Error: address_1') >= 0)
|
||||
self.assertErrorContains(error_string, 'Field Form Error: address_1')
|
||||
|
||||
def test_create_good_testcenter_user(self):
|
||||
testcenter_user = create_tc_user("test_good_user")
|
||||
self.assertIsNotNone(testcenter_user)
|
||||
|
||||
def test_create_good_testcenter_registration(self):
|
||||
username = 'test_good_registration'
|
||||
create_tc_user(username)
|
||||
registration = create_tc_registration(username)
|
||||
self.assertIsNotNone(registration)
|
||||
|
||||
def test_cdd_missing_option(self):
|
||||
error_string = get_command_error_text('pearson_export_cdd', **{})
|
||||
self.assertErrorContains(error_string, 'Error: --destination or --dest-from-settings must be used')
|
||||
|
||||
def test_ead_missing_option(self):
|
||||
error_string = get_command_error_text('pearson_export_ead', **{})
|
||||
self.assertErrorContains(error_string, 'Error: --destination or --dest-from-settings must be used')
|
||||
|
||||
def test_export_single_cdd(self):
|
||||
# before we generate any tc_users, we expect there to be nothing to output:
|
||||
options = {'dest-from-settings': True}
|
||||
with self.settings(PEARSON={'LOCAL_EXPORT': self.export_dir}):
|
||||
call_command('pearson_export_cdd', **options)
|
||||
(filepath, numlines) = get_file_info(self.export_dir)
|
||||
self.assertEquals(numlines, 1, "Expect cdd file to have no non-header lines")
|
||||
os.remove(filepath)
|
||||
|
||||
# generating a tc_user should result in a line in the output
|
||||
username = 'test_single_cdd'
|
||||
create_tc_user(username)
|
||||
call_command('pearson_export_cdd', **options)
|
||||
(filepath, numlines) = get_file_info(self.export_dir)
|
||||
self.assertEquals(numlines, 2, "Expect cdd file to have one non-header line")
|
||||
os.remove(filepath)
|
||||
|
||||
# output after registration should not have any entries again.
|
||||
call_command('pearson_export_cdd', **options)
|
||||
(filepath, numlines) = get_file_info(self.export_dir)
|
||||
self.assertEquals(numlines, 1, "Expect cdd file to have no non-header lines")
|
||||
os.remove(filepath)
|
||||
|
||||
# if we modify the record, then it should be output again:
|
||||
user_options = {'first_name': 'NewTestFirst', }
|
||||
call_command('pearson_make_tc_user', username, **user_options)
|
||||
call_command('pearson_export_cdd', **options)
|
||||
(filepath, numlines) = get_file_info(self.export_dir)
|
||||
self.assertEquals(numlines, 2, "Expect cdd file to have one non-header line")
|
||||
os.remove(filepath)
|
||||
|
||||
def test_export_single_ead(self):
|
||||
# before we generate any registrations, we expect there to be nothing to output:
|
||||
options = {'dest-from-settings': True}
|
||||
with self.settings(PEARSON={'LOCAL_EXPORT': self.export_dir}):
|
||||
call_command('pearson_export_ead', **options)
|
||||
(filepath, numlines) = get_file_info(self.export_dir)
|
||||
self.assertEquals(numlines, 1, "Expect ead file to have no non-header lines")
|
||||
os.remove(filepath)
|
||||
|
||||
# generating a registration should result in a line in the output
|
||||
username = 'test_single_ead'
|
||||
create_tc_user(username)
|
||||
create_tc_registration(username)
|
||||
call_command('pearson_export_ead', **options)
|
||||
(filepath, numlines) = get_file_info(self.export_dir)
|
||||
self.assertEquals(numlines, 2, "Expect ead file to have one non-header line")
|
||||
os.remove(filepath)
|
||||
|
||||
# output after registration should not have any entries again.
|
||||
call_command('pearson_export_ead', **options)
|
||||
(filepath, numlines) = get_file_info(self.export_dir)
|
||||
self.assertEquals(numlines, 1, "Expect ead file to have no non-header lines")
|
||||
os.remove(filepath)
|
||||
|
||||
# if we modify the record, then it should be output again:
|
||||
create_tc_registration(username, accommodation_code='EQPMNT')
|
||||
call_command('pearson_export_ead', **options)
|
||||
(filepath, numlines) = get_file_info(self.export_dir)
|
||||
self.assertEquals(numlines, 2, "Expect ead file to have one non-header line")
|
||||
os.remove(filepath)
|
||||
|
||||
def test_export_multiple(self):
|
||||
create_multiple_registrations("export")
|
||||
with self.settings(PEARSON={'LOCAL_EXPORT': self.export_dir}):
|
||||
options = {'dest-from-settings': True}
|
||||
call_command('pearson_export_cdd', **options)
|
||||
(filepath, numlines) = get_file_info(self.export_dir)
|
||||
self.assertEquals(numlines, 5, "Expect cdd file to have four non-header lines: total was {}".format(numlines))
|
||||
os.remove(filepath)
|
||||
|
||||
call_command('pearson_export_ead', **options)
|
||||
(filepath, numlines) = get_file_info(self.export_dir)
|
||||
self.assertEquals(numlines, 7, "Expect ead file to have six non-header lines: total was {}".format(numlines))
|
||||
os.remove(filepath)
|
||||
|
||||
|
||||
# def test_bad_demographic_option(self):
|
||||
# username = 'nonuser'
|
||||
# output_string, stderrmsg = get_error_string_for_management_call('pearson_make_tc_user', username, **{'--garbage' : None })
|
||||
# print stderrmsg
|
||||
# self.assertErrorContains(stderrmsg, 'Unexpected option')
|
||||
#
|
||||
# def test_missing_demographic_user(self):
|
||||
# username = 'nonuser'
|
||||
# output_string, error_string = get_error_string_for_management_call('pearson_make_tc_user', username, **{})
|
||||
# self.assertErrorContains(error_string, 'User matching query does not exist')
|
||||
|
||||
# credentials for a test SFTP site:
|
||||
SFTP_HOSTNAME = 'ec2-23-20-150-101.compute-1.amazonaws.com'
|
||||
SFTP_USERNAME = 'pearsontest'
|
||||
SFTP_PASSWORD = 'password goes here'
|
||||
|
||||
S3_BUCKET = 'edx-pearson-archive'
|
||||
AWS_ACCESS_KEY_ID = 'put yours here'
|
||||
AWS_SECRET_ACCESS_KEY = 'put yours here'
|
||||
|
||||
|
||||
class PearsonTransferTestCase(PearsonTestCase):
|
||||
'''
|
||||
Class for tests running Pearson transfers
|
||||
'''
|
||||
|
||||
def test_transfer_config(self):
|
||||
stderrmsg = get_command_error_text('pearson_transfer', **{'mode': 'garbage'})
|
||||
self.assertErrorContains(stderrmsg, 'Error: No PEARSON entries')
|
||||
|
||||
stderrmsg = get_command_error_text('pearson_transfer')
|
||||
self.assertErrorContains(stderrmsg, 'Error: No PEARSON entries')
|
||||
|
||||
with self.settings(PEARSON={'LOCAL_EXPORT': self.export_dir,
|
||||
'LOCAL_IMPORT': self.import_dir}):
|
||||
stderrmsg = get_command_error_text('pearson_transfer')
|
||||
self.assertErrorContains(stderrmsg, 'Error: No entry in the PEARSON settings')
|
||||
|
||||
def test_transfer_export_missing_dest_dir(self):
|
||||
raise SkipTest()
|
||||
create_multiple_registrations('export_missing_dest')
|
||||
with self.settings(PEARSON={'LOCAL_EXPORT': self.export_dir,
|
||||
'SFTP_EXPORT': 'this/does/not/exist',
|
||||
'SFTP_HOSTNAME': SFTP_HOSTNAME,
|
||||
'SFTP_USERNAME': SFTP_USERNAME,
|
||||
'SFTP_PASSWORD': SFTP_PASSWORD,
|
||||
'S3_BUCKET': S3_BUCKET,
|
||||
},
|
||||
AWS_ACCESS_KEY_ID=AWS_ACCESS_KEY_ID,
|
||||
AWS_SECRET_ACCESS_KEY=AWS_SECRET_ACCESS_KEY):
|
||||
options = {'mode': 'export'}
|
||||
stderrmsg = get_command_error_text('pearson_transfer', **options)
|
||||
self.assertErrorContains(stderrmsg, 'Error: SFTP destination path does not exist')
|
||||
|
||||
def test_transfer_export(self):
|
||||
raise SkipTest()
|
||||
create_multiple_registrations("transfer_export")
|
||||
with self.settings(PEARSON={'LOCAL_EXPORT': self.export_dir,
|
||||
'SFTP_EXPORT': 'results/topvue',
|
||||
'SFTP_HOSTNAME': SFTP_HOSTNAME,
|
||||
'SFTP_USERNAME': SFTP_USERNAME,
|
||||
'SFTP_PASSWORD': SFTP_PASSWORD,
|
||||
'S3_BUCKET': S3_BUCKET,
|
||||
},
|
||||
AWS_ACCESS_KEY_ID=AWS_ACCESS_KEY_ID,
|
||||
AWS_SECRET_ACCESS_KEY=AWS_SECRET_ACCESS_KEY):
|
||||
options = {'mode': 'export'}
|
||||
# call_command('pearson_transfer', **options)
|
||||
# # confirm that the export directory is still empty:
|
||||
# self.assertEqual(len(os.listdir(self.export_dir)), 0, "expected export directory to be empty")
|
||||
|
||||
def test_transfer_import_missing_source_dir(self):
|
||||
raise SkipTest()
|
||||
create_multiple_registrations('import_missing_src')
|
||||
with self.settings(PEARSON={'LOCAL_IMPORT': self.import_dir,
|
||||
'SFTP_IMPORT': 'this/does/not/exist',
|
||||
'SFTP_HOSTNAME': SFTP_HOSTNAME,
|
||||
'SFTP_USERNAME': SFTP_USERNAME,
|
||||
'SFTP_PASSWORD': SFTP_PASSWORD,
|
||||
'S3_BUCKET': S3_BUCKET,
|
||||
},
|
||||
AWS_ACCESS_KEY_ID=AWS_ACCESS_KEY_ID,
|
||||
AWS_SECRET_ACCESS_KEY=AWS_SECRET_ACCESS_KEY):
|
||||
options = {'mode': 'import'}
|
||||
stderrmsg = get_command_error_text('pearson_transfer', **options)
|
||||
self.assertErrorContains(stderrmsg, 'Error: SFTP source path does not exist')
|
||||
|
||||
def test_transfer_import(self):
|
||||
raise SkipTest()
|
||||
create_multiple_registrations('import_missing_src')
|
||||
with self.settings(PEARSON={'LOCAL_IMPORT': self.import_dir,
|
||||
'SFTP_IMPORT': 'results',
|
||||
'SFTP_HOSTNAME': SFTP_HOSTNAME,
|
||||
'SFTP_USERNAME': SFTP_USERNAME,
|
||||
'SFTP_PASSWORD': SFTP_PASSWORD,
|
||||
'S3_BUCKET': S3_BUCKET,
|
||||
},
|
||||
AWS_ACCESS_KEY_ID=AWS_ACCESS_KEY_ID,
|
||||
AWS_SECRET_ACCESS_KEY=AWS_SECRET_ACCESS_KEY):
|
||||
options = {'mode': 'import'}
|
||||
call_command('pearson_transfer', **options)
|
||||
self.assertEqual(len(os.listdir(self.import_dir)), 0, "expected import directory to be empty")
|
||||
185
common/djangoapps/student/migrations/0029_remove_pearson.py
Normal file
185
common/djangoapps/student/migrations/0029_remove_pearson.py
Normal file
@@ -0,0 +1,185 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Deleting model 'TestCenterUser'
|
||||
db.delete_table('student_testcenteruser')
|
||||
|
||||
# Deleting model 'TestCenterRegistration'
|
||||
db.delete_table('student_testcenterregistration')
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Adding model 'TestCenterUser'
|
||||
db.create_table('student_testcenteruser', (
|
||||
('last_name', self.gf('django.db.models.fields.CharField')(max_length=50, db_index=True)),
|
||||
('suffix', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
|
||||
('confirmed_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)),
|
||||
('updated_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True, db_index=True)),
|
||||
('salutation', self.gf('django.db.models.fields.CharField')(max_length=50, blank=True)),
|
||||
('postal_code', self.gf('django.db.models.fields.CharField')(blank=True, max_length=16, db_index=True)),
|
||||
('processed_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)),
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('city', self.gf('django.db.models.fields.CharField')(max_length=32, db_index=True)),
|
||||
('first_name', self.gf('django.db.models.fields.CharField')(max_length=30, db_index=True)),
|
||||
('middle_name', self.gf('django.db.models.fields.CharField')(max_length=30, blank=True)),
|
||||
('phone_country_code', self.gf('django.db.models.fields.CharField')(max_length=3, db_index=True)),
|
||||
('upload_status', self.gf('django.db.models.fields.CharField')(blank=True, max_length=20, db_index=True)),
|
||||
('state', self.gf('django.db.models.fields.CharField')(blank=True, max_length=20, db_index=True)),
|
||||
('upload_error_message', self.gf('django.db.models.fields.CharField')(max_length=512, blank=True)),
|
||||
('company_name', self.gf('django.db.models.fields.CharField')(blank=True, max_length=50, db_index=True)),
|
||||
('candidate_id', self.gf('django.db.models.fields.IntegerField')(null=True, db_index=True)),
|
||||
('fax', self.gf('django.db.models.fields.CharField')(max_length=35, blank=True)),
|
||||
('user_updated_at', self.gf('django.db.models.fields.DateTimeField')(db_index=True)),
|
||||
('phone', self.gf('django.db.models.fields.CharField')(max_length=35)),
|
||||
('user', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['auth.User'], unique=True)),
|
||||
('uploaded_at', self.gf('django.db.models.fields.DateTimeField')(blank=True, null=True, db_index=True)),
|
||||
('extension', self.gf('django.db.models.fields.CharField')(blank=True, max_length=8, db_index=True)),
|
||||
('fax_country_code', self.gf('django.db.models.fields.CharField')(max_length=3, blank=True)),
|
||||
('country', self.gf('django.db.models.fields.CharField')(max_length=3, db_index=True)),
|
||||
('client_candidate_id', self.gf('django.db.models.fields.CharField')(max_length=50, unique=True, db_index=True)),
|
||||
('address_1', self.gf('django.db.models.fields.CharField')(max_length=40)),
|
||||
('address_2', self.gf('django.db.models.fields.CharField')(max_length=40, blank=True)),
|
||||
('address_3', self.gf('django.db.models.fields.CharField')(max_length=40, blank=True)),
|
||||
('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True, db_index=True)),
|
||||
))
|
||||
db.send_create_signal('student', ['TestCenterUser'])
|
||||
|
||||
# Adding model 'TestCenterRegistration'
|
||||
db.create_table('student_testcenterregistration', (
|
||||
('client_authorization_id', self.gf('django.db.models.fields.CharField')(max_length=20, unique=True, db_index=True)),
|
||||
('uploaded_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)),
|
||||
('user_updated_at', self.gf('django.db.models.fields.DateTimeField')(db_index=True)),
|
||||
('authorization_id', self.gf('django.db.models.fields.IntegerField')(null=True, db_index=True)),
|
||||
('upload_status', self.gf('django.db.models.fields.CharField')(blank=True, max_length=20, db_index=True)),
|
||||
('updated_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True, db_index=True)),
|
||||
('confirmed_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)),
|
||||
('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True, db_index=True)),
|
||||
('accommodation_request', self.gf('django.db.models.fields.CharField')(max_length=1024, blank=True)),
|
||||
('eligibility_appointment_date_first', self.gf('django.db.models.fields.DateField')(db_index=True)),
|
||||
('exam_series_code', self.gf('django.db.models.fields.CharField')(max_length=15, db_index=True)),
|
||||
('processed_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)),
|
||||
('upload_error_message', self.gf('django.db.models.fields.CharField')(max_length=512, blank=True)),
|
||||
('accommodation_code', self.gf('django.db.models.fields.CharField')(max_length=64, blank=True)),
|
||||
('course_id', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)),
|
||||
('testcenter_user', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['student.TestCenterUser'])),
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('eligibility_appointment_date_last', self.gf('django.db.models.fields.DateField')(db_index=True)),
|
||||
))
|
||||
db.send_create_signal('student', ['TestCenterRegistration'])
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'student.courseenrollment': {
|
||||
'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '100'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.courseenrollmentallowed': {
|
||||
'Meta': {'unique_together': "(('email', 'course_id'),)", 'object_name': 'CourseEnrollmentAllowed'},
|
||||
'auto_enroll': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||
},
|
||||
'student.pendingemailchange': {
|
||||
'Meta': {'object_name': 'PendingEmailChange'},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.pendingnamechange': {
|
||||
'Meta': {'object_name': 'PendingNameChange'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.registration': {
|
||||
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.userprofile': {
|
||||
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
|
||||
'allow_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
|
||||
'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
|
||||
'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}),
|
||||
'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
'student.userstanding': {
|
||||
'Meta': {'object_name': 'UserStanding'},
|
||||
'account_status': ('django.db.models.fields.CharField', [], {'max_length': '31', 'blank': 'True'}),
|
||||
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'standing_last_changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'standing'", 'unique': 'True', 'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.usertestgroup': {
|
||||
'Meta': {'object_name': 'UserTestGroup'},
|
||||
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
|
||||
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
@@ -11,7 +11,6 @@ file and check it in at the same time as your model changes. To do that,
|
||||
3. Add the migration file created in edx-platform/common/djangoapps/student/migrations/
|
||||
"""
|
||||
from datetime import datetime
|
||||
from random import randint
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
@@ -22,9 +21,7 @@ from django.contrib.auth.models import User
|
||||
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
import django.dispatch
|
||||
from django.forms import ModelForm, forms
|
||||
from django.dispatch import receiver, Signal
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
import lms.lib.comment_client as cc
|
||||
@@ -36,7 +33,7 @@ from track.views import server_track
|
||||
from eventtracking import tracker
|
||||
|
||||
|
||||
unenroll_done = django.dispatch.Signal(providing_args=["course_enrollment"])
|
||||
unenroll_done = Signal(providing_args=["course_enrollment"])
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
AUDIT_LOG = logging.getLogger("audit")
|
||||
@@ -144,480 +141,6 @@ class UserProfile(models.Model):
|
||||
def set_meta(self, js):
|
||||
self.meta = json.dumps(js)
|
||||
|
||||
TEST_CENTER_STATUS_ACCEPTED = "Accepted"
|
||||
TEST_CENTER_STATUS_ERROR = "Error"
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Also storing here the confirmation information received from Pearson (if any)
|
||||
as to the success or failure of the upload. (VCDC file)
|
||||
"""
|
||||
# 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 we assign our user for the Test Center.
|
||||
client_candidate_id = models.CharField(unique=True, 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, db_index=True)
|
||||
|
||||
# time at which edX sent the registration to the test center
|
||||
uploaded_at = models.DateTimeField(null=True, blank=True, db_index=True)
|
||||
|
||||
# confirmation back from the test center, as well as timestamps
|
||||
# on when they processed the request, and when we received
|
||||
# confirmation back.
|
||||
processed_at = models.DateTimeField(null=True, db_index=True)
|
||||
upload_status = models.CharField(max_length=20, blank=True, db_index=True) # 'Error' or 'Accepted'
|
||||
upload_error_message = models.CharField(max_length=512, blank=True)
|
||||
# Unique ID given to us for this User by the Testing Center. It's null when
|
||||
# we first create the User entry, and may be assigned by Pearson later.
|
||||
# (However, it may never be set if we are always initiating such candidate creation.)
|
||||
candidate_id = models.IntegerField(null=True, db_index=True)
|
||||
confirmed_at = models.DateTimeField(null=True, db_index=True)
|
||||
|
||||
@property
|
||||
def needs_uploading(self):
|
||||
return self.uploaded_at is None or self.uploaded_at < self.user_updated_at
|
||||
|
||||
@staticmethod
|
||||
def user_provided_fields():
|
||||
return ['first_name', 'middle_name', 'last_name', 'suffix', 'salutation',
|
||||
'address_1', 'address_2', 'address_3', 'city', 'state', 'postal_code', 'country',
|
||||
'phone', 'extension', 'phone_country_code', 'fax', 'fax_country_code', 'company_name']
|
||||
|
||||
@property
|
||||
def email(self):
|
||||
return self.user.email
|
||||
|
||||
def needs_update(self, fields):
|
||||
for fieldname in TestCenterUser.user_provided_fields():
|
||||
if fieldname in fields and getattr(self, fieldname) != fields[fieldname]:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _generate_edx_id(prefix):
|
||||
NUM_DIGITS = 12
|
||||
return u"{}{:012}".format(prefix, randint(1, 10 ** NUM_DIGITS - 1))
|
||||
|
||||
@staticmethod
|
||||
def _generate_candidate_id():
|
||||
return TestCenterUser._generate_edx_id("edX")
|
||||
|
||||
@classmethod
|
||||
def create(cls, user):
|
||||
testcenter_user = cls(user=user)
|
||||
# testcenter_user.candidate_id remains unset
|
||||
# assign an ID of our own:
|
||||
cand_id = cls._generate_candidate_id()
|
||||
while TestCenterUser.objects.filter(client_candidate_id=cand_id).exists():
|
||||
cand_id = cls._generate_candidate_id()
|
||||
testcenter_user.client_candidate_id = cand_id
|
||||
return testcenter_user
|
||||
|
||||
@property
|
||||
def is_accepted(self):
|
||||
return self.upload_status == TEST_CENTER_STATUS_ACCEPTED
|
||||
|
||||
@property
|
||||
def is_rejected(self):
|
||||
return self.upload_status == TEST_CENTER_STATUS_ERROR
|
||||
|
||||
@property
|
||||
def is_pending(self):
|
||||
return not self.is_accepted and not self.is_rejected
|
||||
|
||||
|
||||
class TestCenterUserForm(ModelForm):
|
||||
class Meta:
|
||||
model = TestCenterUser
|
||||
fields = ('first_name', 'middle_name', 'last_name', 'suffix', 'salutation',
|
||||
'address_1', 'address_2', 'address_3', 'city', 'state', 'postal_code', 'country',
|
||||
'phone', 'extension', 'phone_country_code', 'fax', 'fax_country_code', 'company_name')
|
||||
|
||||
def update_and_save(self):
|
||||
new_user = self.save(commit=False)
|
||||
# create additional values here:
|
||||
new_user.user_updated_at = datetime.now(UTC)
|
||||
new_user.upload_status = ''
|
||||
new_user.save()
|
||||
log.info("Updated demographic information for user's test center exam registration: username \"{}\" ".format(new_user.user.username))
|
||||
|
||||
# add validation:
|
||||
|
||||
def clean_country(self):
|
||||
code = self.cleaned_data['country']
|
||||
if code and (len(code) != 3 or not code.isalpha()):
|
||||
raise forms.ValidationError(u'Must be three characters (ISO 3166-1): e.g. USA, CAN, MNG')
|
||||
return code.upper()
|
||||
|
||||
def clean(self):
|
||||
def _can_encode_as_latin(fieldvalue):
|
||||
try:
|
||||
fieldvalue.encode('iso-8859-1')
|
||||
except UnicodeEncodeError:
|
||||
return False
|
||||
return True
|
||||
|
||||
cleaned_data = super(TestCenterUserForm, self).clean()
|
||||
|
||||
# check for interactions between fields:
|
||||
if 'country' in cleaned_data:
|
||||
country = cleaned_data.get('country')
|
||||
if country == 'USA' or country == 'CAN':
|
||||
if 'state' in cleaned_data and len(cleaned_data['state']) == 0:
|
||||
self._errors['state'] = self.error_class([u'Required if country is USA or CAN.'])
|
||||
del cleaned_data['state']
|
||||
|
||||
if 'postal_code' in cleaned_data and len(cleaned_data['postal_code']) == 0:
|
||||
self._errors['postal_code'] = self.error_class([u'Required if country is USA or CAN.'])
|
||||
del cleaned_data['postal_code']
|
||||
|
||||
if 'fax' in cleaned_data and len(cleaned_data['fax']) > 0 and 'fax_country_code' in cleaned_data and len(cleaned_data['fax_country_code']) == 0:
|
||||
self._errors['fax_country_code'] = self.error_class([u'Required if fax is specified.'])
|
||||
del cleaned_data['fax_country_code']
|
||||
|
||||
# check encoding for all fields:
|
||||
cleaned_data_fields = [fieldname for fieldname in cleaned_data]
|
||||
for fieldname in cleaned_data_fields:
|
||||
if not _can_encode_as_latin(cleaned_data[fieldname]):
|
||||
self._errors[fieldname] = self.error_class([u'Must only use characters in Latin-1 (iso-8859-1) encoding'])
|
||||
del cleaned_data[fieldname]
|
||||
|
||||
# Always return the full collection of cleaned data.
|
||||
return cleaned_data
|
||||
|
||||
# our own code to indicate that a request has been rejected.
|
||||
ACCOMMODATION_REJECTED_CODE = 'NONE'
|
||||
|
||||
ACCOMMODATION_CODES = (
|
||||
(ACCOMMODATION_REJECTED_CODE, 'No Accommodation Granted'),
|
||||
('EQPMNT', 'Equipment'),
|
||||
('ET12ET', 'Extra Time - 1/2 Exam Time'),
|
||||
('ET30MN', 'Extra Time - 30 Minutes'),
|
||||
('ETDBTM', 'Extra Time - Double Time'),
|
||||
('SEPRMM', 'Separate Room'),
|
||||
('SRREAD', 'Separate Room and Reader'),
|
||||
('SRRERC', 'Separate Room and Reader/Recorder'),
|
||||
('SRRECR', 'Separate Room and Recorder'),
|
||||
('SRSEAN', 'Separate Room and Service Animal'),
|
||||
('SRSGNR', 'Separate Room and Sign Language Interpreter'),
|
||||
)
|
||||
|
||||
ACCOMMODATION_CODE_DICT = {code: name for (code, name) in ACCOMMODATION_CODES}
|
||||
|
||||
|
||||
class TestCenterRegistration(models.Model):
|
||||
"""
|
||||
This is our representation of a user's registration 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. This is less of an issue
|
||||
than for the TestCenterUser.
|
||||
* Registrations are only created here when a user registers to take an exam in person.
|
||||
|
||||
The field names and lengths are modeled on the conventions and constraints
|
||||
of Pearson's data import system.
|
||||
"""
|
||||
# to find an exam registration, we key off of the user and course_id.
|
||||
# If multiple exams per course are possible, we would also need to add the
|
||||
# exam_series_code.
|
||||
testcenter_user = models.ForeignKey(TestCenterUser, default=None)
|
||||
course_id = models.CharField(max_length=128, db_index=True)
|
||||
|
||||
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.
|
||||
# The appointment dates, the exam count, and the accommodation codes can be updated,
|
||||
# but hopefully this won't happen often.
|
||||
user_updated_at = models.DateTimeField(db_index=True)
|
||||
# "client_authorization_id" is our unique identifier for the authorization.
|
||||
# This must be present for an update or delete to be sent to Pearson.
|
||||
client_authorization_id = models.CharField(max_length=20, unique=True, db_index=True)
|
||||
|
||||
# information about the test, from the course policy:
|
||||
exam_series_code = models.CharField(max_length=15, db_index=True)
|
||||
eligibility_appointment_date_first = models.DateField(db_index=True)
|
||||
eligibility_appointment_date_last = models.DateField(db_index=True)
|
||||
|
||||
# this is really a list of codes, using an '*' as a delimiter.
|
||||
# So it's not a choice list. We use the special value of ACCOMMODATION_REJECTED_CODE
|
||||
# to indicate the rejection of an accommodation request.
|
||||
accommodation_code = models.CharField(max_length=64, blank=True)
|
||||
|
||||
# store the original text of the accommodation request.
|
||||
accommodation_request = models.CharField(max_length=1024, blank=True, db_index=False)
|
||||
|
||||
# time at which edX sent the registration to the test center
|
||||
uploaded_at = models.DateTimeField(null=True, db_index=True)
|
||||
|
||||
# confirmation back from the test center, as well as timestamps
|
||||
# on when they processed the request, and when we received
|
||||
# confirmation back.
|
||||
processed_at = models.DateTimeField(null=True, db_index=True)
|
||||
upload_status = models.CharField(max_length=20, blank=True, db_index=True) # 'Error' or 'Accepted'
|
||||
upload_error_message = models.CharField(max_length=512, blank=True)
|
||||
# Unique ID given to us for this registration by the Testing Center. It's null when
|
||||
# we first create the registration entry, and may be assigned by Pearson later.
|
||||
# (However, it may never be set if we are always initiating such candidate creation.)
|
||||
authorization_id = models.IntegerField(null=True, db_index=True)
|
||||
confirmed_at = models.DateTimeField(null=True, db_index=True)
|
||||
|
||||
@property
|
||||
def candidate_id(self):
|
||||
return self.testcenter_user.candidate_id
|
||||
|
||||
@property
|
||||
def client_candidate_id(self):
|
||||
return self.testcenter_user.client_candidate_id
|
||||
|
||||
@property
|
||||
def authorization_transaction_type(self):
|
||||
if self.authorization_id is not None:
|
||||
return 'Update'
|
||||
elif self.uploaded_at is None:
|
||||
return 'Add'
|
||||
elif self.registration_is_rejected:
|
||||
# Assume that if the registration was rejected before,
|
||||
# it is more likely this is the (first) correction
|
||||
# than a second correction in flight before the first was
|
||||
# processed.
|
||||
return 'Add'
|
||||
else:
|
||||
# TODO: decide what to send when we have uploaded an initial version,
|
||||
# but have not received confirmation back from that upload. If the
|
||||
# registration here has been changed, then we don't know if this changed
|
||||
# registration should be submitted as an 'add' or an 'update'.
|
||||
#
|
||||
# If the first registration were lost or in error (e.g. bad code),
|
||||
# the second should be an "Add". If the first were processed successfully,
|
||||
# then the second should be an "Update". We just don't know....
|
||||
return 'Update'
|
||||
|
||||
@property
|
||||
def exam_authorization_count(self):
|
||||
# Someday this could go in the database (with a default value). But at present,
|
||||
# we do not expect anyone to be authorized to take an exam more than once.
|
||||
return 1
|
||||
|
||||
@property
|
||||
def needs_uploading(self):
|
||||
return self.uploaded_at is None or self.uploaded_at < self.user_updated_at
|
||||
|
||||
@classmethod
|
||||
def create(cls, testcenter_user, exam, accommodation_request):
|
||||
registration = cls(testcenter_user=testcenter_user)
|
||||
registration.course_id = exam.course_id
|
||||
registration.accommodation_request = accommodation_request.strip()
|
||||
registration.exam_series_code = exam.exam_series_code
|
||||
registration.eligibility_appointment_date_first = exam.first_eligible_appointment_date.strftime("%Y-%m-%d")
|
||||
registration.eligibility_appointment_date_last = exam.last_eligible_appointment_date.strftime("%Y-%m-%d")
|
||||
registration.client_authorization_id = cls._create_client_authorization_id()
|
||||
# accommodation_code remains blank for now, along with Pearson confirmation information
|
||||
return registration
|
||||
|
||||
@staticmethod
|
||||
def _generate_authorization_id():
|
||||
return TestCenterUser._generate_edx_id("edXexam")
|
||||
|
||||
@staticmethod
|
||||
def _create_client_authorization_id():
|
||||
"""
|
||||
Return a unique id for a registration, suitable for using as an authorization code
|
||||
for Pearson. It must fit within 20 characters.
|
||||
"""
|
||||
# generate a random value, and check to see if it already is in use here
|
||||
auth_id = TestCenterRegistration._generate_authorization_id()
|
||||
while TestCenterRegistration.objects.filter(client_authorization_id=auth_id).exists():
|
||||
auth_id = TestCenterRegistration._generate_authorization_id()
|
||||
return auth_id
|
||||
|
||||
# methods for providing registration status details on registration page:
|
||||
@property
|
||||
def demographics_is_accepted(self):
|
||||
return self.testcenter_user.is_accepted
|
||||
|
||||
@property
|
||||
def demographics_is_rejected(self):
|
||||
return self.testcenter_user.is_rejected
|
||||
|
||||
@property
|
||||
def demographics_is_pending(self):
|
||||
return self.testcenter_user.is_pending
|
||||
|
||||
@property
|
||||
def accommodation_is_accepted(self):
|
||||
return len(self.accommodation_request) > 0 and len(self.accommodation_code) > 0 and self.accommodation_code != ACCOMMODATION_REJECTED_CODE
|
||||
|
||||
@property
|
||||
def accommodation_is_rejected(self):
|
||||
return len(self.accommodation_request) > 0 and self.accommodation_code == ACCOMMODATION_REJECTED_CODE
|
||||
|
||||
@property
|
||||
def accommodation_is_pending(self):
|
||||
return len(self.accommodation_request) > 0 and len(self.accommodation_code) == 0
|
||||
|
||||
@property
|
||||
def accommodation_is_skipped(self):
|
||||
return len(self.accommodation_request) == 0
|
||||
|
||||
@property
|
||||
def registration_is_accepted(self):
|
||||
return self.upload_status == TEST_CENTER_STATUS_ACCEPTED
|
||||
|
||||
@property
|
||||
def registration_is_rejected(self):
|
||||
return self.upload_status == TEST_CENTER_STATUS_ERROR
|
||||
|
||||
@property
|
||||
def registration_is_pending(self):
|
||||
return not self.registration_is_accepted and not self.registration_is_rejected
|
||||
|
||||
# methods for providing registration status summary on dashboard page:
|
||||
@property
|
||||
def is_accepted(self):
|
||||
return self.registration_is_accepted and self.demographics_is_accepted
|
||||
|
||||
@property
|
||||
def is_rejected(self):
|
||||
return self.registration_is_rejected or self.demographics_is_rejected
|
||||
|
||||
@property
|
||||
def is_pending(self):
|
||||
return not self.is_accepted and not self.is_rejected
|
||||
|
||||
def get_accommodation_codes(self):
|
||||
return self.accommodation_code.split('*')
|
||||
|
||||
def get_accommodation_names(self):
|
||||
return [ACCOMMODATION_CODE_DICT.get(code, "Unknown code " + code) for code in self.get_accommodation_codes()]
|
||||
|
||||
@property
|
||||
def registration_signup_url(self):
|
||||
return settings.PEARSONVUE_SIGNINPAGE_URL
|
||||
|
||||
def demographics_status(self):
|
||||
if self.demographics_is_accepted:
|
||||
return "Accepted"
|
||||
elif self.demographics_is_rejected:
|
||||
return "Rejected"
|
||||
else:
|
||||
return "Pending"
|
||||
|
||||
def accommodation_status(self):
|
||||
if self.accommodation_is_skipped:
|
||||
return "Skipped"
|
||||
elif self.accommodation_is_accepted:
|
||||
return "Accepted"
|
||||
elif self.accommodation_is_rejected:
|
||||
return "Rejected"
|
||||
else:
|
||||
return "Pending"
|
||||
|
||||
def registration_status(self):
|
||||
if self.registration_is_accepted:
|
||||
return "Accepted"
|
||||
elif self.registration_is_rejected:
|
||||
return "Rejected"
|
||||
else:
|
||||
return "Pending"
|
||||
|
||||
|
||||
class TestCenterRegistrationForm(ModelForm):
|
||||
class Meta:
|
||||
model = TestCenterRegistration
|
||||
fields = ('accommodation_request', 'accommodation_code')
|
||||
|
||||
def clean_accommodation_request(self):
|
||||
code = self.cleaned_data['accommodation_request']
|
||||
if code and len(code) > 0:
|
||||
return code.strip()
|
||||
return code
|
||||
|
||||
def update_and_save(self):
|
||||
registration = self.save(commit=False)
|
||||
# create additional values here:
|
||||
registration.user_updated_at = datetime.now(UTC)
|
||||
registration.upload_status = ''
|
||||
registration.save()
|
||||
log.info("Updated registration information for user's test center exam registration: username \"{}\" course \"{}\", examcode \"{}\"".format(registration.testcenter_user.user.username, registration.course_id, registration.exam_series_code))
|
||||
|
||||
def clean_accommodation_code(self):
|
||||
code = self.cleaned_data['accommodation_code']
|
||||
if code:
|
||||
code = code.upper()
|
||||
codes = code.split('*')
|
||||
for codeval in codes:
|
||||
if codeval not in ACCOMMODATION_CODE_DICT:
|
||||
raise forms.ValidationError(u'Invalid accommodation code specified: "{}"'.format(codeval))
|
||||
return code
|
||||
|
||||
|
||||
def get_testcenter_registration(user, course_id, exam_series_code):
|
||||
try:
|
||||
tcu = TestCenterUser.objects.get(user=user)
|
||||
except TestCenterUser.DoesNotExist:
|
||||
return []
|
||||
return TestCenterRegistration.objects.filter(testcenter_user=tcu, course_id=course_id, exam_series_code=exam_series_code)
|
||||
|
||||
# nosetests thinks that anything with _test_ in the name is a test.
|
||||
# Correct this (https://nose.readthedocs.org/en/latest/finding_tests.html)
|
||||
get_testcenter_registration.__test__ = False
|
||||
|
||||
|
||||
def unique_id_for_user(user):
|
||||
"""
|
||||
|
||||
@@ -39,10 +39,9 @@ from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from student.models import (
|
||||
Registration, UserProfile, TestCenterUser, TestCenterUserForm,
|
||||
TestCenterRegistration, TestCenterRegistrationForm, PendingNameChange,
|
||||
Registration, UserProfile, PendingNameChange,
|
||||
PendingEmailChange, CourseEnrollment, unique_id_for_user,
|
||||
get_testcenter_registration, CourseEnrollmentAllowed, UserStanding,
|
||||
CourseEnrollmentAllowed, UserStanding,
|
||||
)
|
||||
from student.forms import PasswordResetFormNoActive
|
||||
|
||||
@@ -966,172 +965,6 @@ def create_account(request, post_override=None):
|
||||
return response
|
||||
|
||||
|
||||
def exam_registration_info(user, course):
|
||||
""" Returns a Registration object if the user is currently registered for a current
|
||||
exam of the course. Returns None if the user is not registered, or if there is no
|
||||
current exam for the course.
|
||||
"""
|
||||
exam_info = course.current_test_center_exam
|
||||
if exam_info is None:
|
||||
return None
|
||||
|
||||
exam_code = exam_info.exam_series_code
|
||||
registrations = get_testcenter_registration(user, course.id, exam_code)
|
||||
if registrations:
|
||||
registration = registrations[0]
|
||||
else:
|
||||
registration = None
|
||||
return registration
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def begin_exam_registration(request, course_id):
|
||||
""" Handles request to register the user for the current
|
||||
test center exam of the specified course. Called by form
|
||||
in dashboard.html.
|
||||
"""
|
||||
user = request.user
|
||||
|
||||
try:
|
||||
course = course_from_id(course_id)
|
||||
except ItemNotFoundError:
|
||||
log.error("User {0} enrolled in non-existent course {1}".format(user.username, course_id))
|
||||
raise Http404
|
||||
|
||||
# get the exam to be registered for:
|
||||
# (For now, we just assume there is one at most.)
|
||||
# if there is no exam now (because someone bookmarked this stupid page),
|
||||
# then return a 404:
|
||||
exam_info = course.current_test_center_exam
|
||||
if exam_info is None:
|
||||
raise Http404
|
||||
|
||||
# determine if the user is registered for this course:
|
||||
registration = exam_registration_info(user, course)
|
||||
|
||||
# we want to populate the registration page with the relevant information,
|
||||
# if it already exists. Create an empty object otherwise.
|
||||
try:
|
||||
testcenteruser = TestCenterUser.objects.get(user=user)
|
||||
except TestCenterUser.DoesNotExist:
|
||||
testcenteruser = TestCenterUser()
|
||||
testcenteruser.user = user
|
||||
|
||||
context = {'course': course,
|
||||
'user': user,
|
||||
'testcenteruser': testcenteruser,
|
||||
'registration': registration,
|
||||
'exam_info': exam_info,
|
||||
}
|
||||
|
||||
return render_to_response('test_center_register.html', context)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def create_exam_registration(request, post_override=None):
|
||||
"""
|
||||
JSON call to create a test center exam registration.
|
||||
Called by form in test_center_register.html
|
||||
"""
|
||||
post_vars = post_override if post_override else request.POST
|
||||
|
||||
# first determine if we need to create a new TestCenterUser, or if we are making any update
|
||||
# to an existing TestCenterUser.
|
||||
username = post_vars['username']
|
||||
user = User.objects.get(username=username)
|
||||
course_id = post_vars['course_id']
|
||||
course = course_from_id(course_id) # assume it will be found....
|
||||
|
||||
# make sure that any demographic data values received from the page have been stripped.
|
||||
# Whitespace is not an acceptable response for any of these values
|
||||
demographic_data = {}
|
||||
for fieldname in TestCenterUser.user_provided_fields():
|
||||
if fieldname in post_vars:
|
||||
demographic_data[fieldname] = (post_vars[fieldname]).strip()
|
||||
try:
|
||||
testcenter_user = TestCenterUser.objects.get(user=user)
|
||||
needs_updating = testcenter_user.needs_update(demographic_data)
|
||||
log.info("User {0} enrolled in course {1} {2}updating demographic info for exam registration".format(user.username, course_id, "" if needs_updating else "not "))
|
||||
except TestCenterUser.DoesNotExist:
|
||||
# do additional initialization here:
|
||||
testcenter_user = TestCenterUser.create(user)
|
||||
needs_updating = True
|
||||
log.info("User {0} enrolled in course {1} creating demographic info for exam registration".format(user.username, course_id))
|
||||
|
||||
# perform validation:
|
||||
if needs_updating:
|
||||
# first perform validation on the user information
|
||||
# using a Django Form.
|
||||
form = TestCenterUserForm(instance=testcenter_user, data=demographic_data)
|
||||
if form.is_valid():
|
||||
form.update_and_save()
|
||||
else:
|
||||
response_data = {'success': False}
|
||||
# return a list of errors...
|
||||
response_data['field_errors'] = form.errors
|
||||
response_data['non_field_errors'] = form.non_field_errors()
|
||||
return HttpResponse(json.dumps(response_data), mimetype="application/json")
|
||||
|
||||
# create and save the registration:
|
||||
needs_saving = False
|
||||
exam = course.current_test_center_exam
|
||||
exam_code = exam.exam_series_code
|
||||
registrations = get_testcenter_registration(user, course_id, exam_code)
|
||||
if registrations:
|
||||
registration = registrations[0]
|
||||
# NOTE: we do not bother to check here to see if the registration has changed,
|
||||
# because at the moment there is no way for a user to change anything about their
|
||||
# registration. They only provide an optional accommodation request once, and
|
||||
# cannot make changes to it thereafter.
|
||||
# It is possible that the exam_info content has been changed, such as the
|
||||
# scheduled exam dates, but those kinds of changes should not be handled through
|
||||
# this registration screen.
|
||||
|
||||
else:
|
||||
accommodation_request = post_vars.get('accommodation_request', '')
|
||||
registration = TestCenterRegistration.create(testcenter_user, exam, accommodation_request)
|
||||
needs_saving = True
|
||||
log.info("User {0} enrolled in course {1} creating new exam registration".format(user.username, course_id))
|
||||
|
||||
if needs_saving:
|
||||
# do validation of registration. (Mainly whether an accommodation request is too long.)
|
||||
form = TestCenterRegistrationForm(instance=registration, data=post_vars)
|
||||
if form.is_valid():
|
||||
form.update_and_save()
|
||||
else:
|
||||
response_data = {'success': False}
|
||||
# return a list of errors...
|
||||
response_data['field_errors'] = form.errors
|
||||
response_data['non_field_errors'] = form.non_field_errors()
|
||||
return HttpResponse(json.dumps(response_data), mimetype="application/json")
|
||||
|
||||
# only do the following if there is accommodation text to send,
|
||||
# and a destination to which to send it.
|
||||
# TODO: still need to create the accommodation email templates
|
||||
# if 'accommodation_request' in post_vars and 'TESTCENTER_ACCOMMODATION_REQUEST_EMAIL' in settings:
|
||||
# d = {'accommodation_request': post_vars['accommodation_request'] }
|
||||
#
|
||||
# # composes accommodation email
|
||||
# subject = render_to_string('emails/accommodation_email_subject.txt', d)
|
||||
# # Email subject *must not* contain newlines
|
||||
# subject = ''.join(subject.splitlines())
|
||||
# message = render_to_string('emails/accommodation_email.txt', d)
|
||||
#
|
||||
# try:
|
||||
# dest_addr = settings['TESTCENTER_ACCOMMODATION_REQUEST_EMAIL']
|
||||
# from_addr = user.email
|
||||
# send_mail(subject, message, from_addr, [dest_addr], fail_silently=False)
|
||||
# except:
|
||||
# log.exception(sys.exc_info())
|
||||
# response_data = {'success': False}
|
||||
# response_data['non_field_errors'] = [ 'Could not send accommodation e-mail.', ]
|
||||
# return HttpResponse(json.dumps(response_data), mimetype="application/json")
|
||||
|
||||
js = {'success': True}
|
||||
return HttpResponse(json.dumps(js), mimetype="application/json")
|
||||
|
||||
|
||||
def auto_auth(request):
|
||||
"""
|
||||
Automatically logs the user in with a generated random credentials
|
||||
|
||||
@@ -20,7 +20,6 @@ XMODULES = [
|
||||
"section = xmodule.backcompat_module:SemanticSectionDescriptor",
|
||||
"sequential = xmodule.seq_module:SequenceDescriptor",
|
||||
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
|
||||
"timelimit = xmodule.timelimit_module:TimeLimitDescriptor",
|
||||
"vertical = xmodule.vertical_module:VerticalDescriptor",
|
||||
"video = xmodule.video_module:VideoDescriptor",
|
||||
"videoalpha = xmodule.video_module:VideoDescriptor",
|
||||
|
||||
@@ -213,7 +213,6 @@ class CourseFields(object):
|
||||
discussion_blackouts = List(help="List of pairs of start/end dates for discussion blackouts", scope=Scope.settings)
|
||||
discussion_topics = Dict(help="Map of topics names to ids", scope=Scope.settings)
|
||||
discussion_sort_alpha = Boolean(scope=Scope.settings, default=False, help="Sort forum categories and subcategories alphabetically.")
|
||||
testcenter_info = Dict(help="Dictionary of Test Center info", scope=Scope.settings)
|
||||
announcement = Date(help="Date this course is announced", scope=Scope.settings)
|
||||
cohort_config = Dict(help="Dictionary defining cohort configuration", scope=Scope.settings)
|
||||
is_new = Boolean(help="Whether this course should be flagged as new", scope=Scope.settings)
|
||||
@@ -426,20 +425,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
if self.discussion_topics == {}:
|
||||
self.discussion_topics = {'General': {'id': self.location.html_id()}}
|
||||
|
||||
self.test_center_exams = []
|
||||
test_center_info = self.testcenter_info
|
||||
if test_center_info is not None:
|
||||
for exam_name in test_center_info:
|
||||
try:
|
||||
exam_info = test_center_info[exam_name]
|
||||
self.test_center_exams.append(self.TestCenterExam(self.id, exam_name, exam_info))
|
||||
except Exception as err:
|
||||
# If we can't parse the test center exam info, don't break
|
||||
# the rest of the courseware.
|
||||
msg = 'Error %s: Unable to load test-center exam info for exam "%s" of course "%s"' % (err, exam_name, self.id)
|
||||
log.error(msg)
|
||||
continue
|
||||
|
||||
# TODO check that this is still needed here and can't be by defaults.
|
||||
if not self.tabs:
|
||||
# When calling the various _tab methods, can omit the 'type':'blah' from the
|
||||
@@ -876,93 +861,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
|
||||
return True
|
||||
|
||||
class TestCenterExam(object):
|
||||
def __init__(self, course_id, exam_name, exam_info):
|
||||
self.course_id = course_id
|
||||
self.exam_name = exam_name
|
||||
self.exam_info = exam_info
|
||||
self.exam_series_code = exam_info.get('Exam_Series_Code') or exam_name
|
||||
self.display_name = exam_info.get('Exam_Display_Name') or self.exam_series_code
|
||||
self.first_eligible_appointment_date = self._try_parse_time('First_Eligible_Appointment_Date')
|
||||
if self.first_eligible_appointment_date is None:
|
||||
raise ValueError("First appointment date must be specified")
|
||||
# TODO: If defaulting the last appointment date, it should be the
|
||||
# *end* of the same day, not the same time. It's going to be used as the
|
||||
# end of the exam overall, so we don't want the exam to disappear too soon.
|
||||
# It's also used optionally as the registration end date, so time matters there too.
|
||||
self.last_eligible_appointment_date = self._try_parse_time('Last_Eligible_Appointment_Date') # or self.first_eligible_appointment_date
|
||||
if self.last_eligible_appointment_date is None:
|
||||
raise ValueError("Last appointment date must be specified")
|
||||
self.registration_start_date = (self._try_parse_time('Registration_Start_Date') or
|
||||
datetime.fromtimestamp(0, UTC()))
|
||||
self.registration_end_date = self._try_parse_time('Registration_End_Date') or self.last_eligible_appointment_date
|
||||
# do validation within the exam info:
|
||||
if self.registration_start_date > self.registration_end_date:
|
||||
raise ValueError("Registration start date must be before registration end date")
|
||||
if self.first_eligible_appointment_date > self.last_eligible_appointment_date:
|
||||
raise ValueError("First appointment date must be before last appointment date")
|
||||
if self.registration_end_date > self.last_eligible_appointment_date:
|
||||
raise ValueError("Registration end date must be before last appointment date")
|
||||
self.exam_url = exam_info.get('Exam_URL')
|
||||
|
||||
def _try_parse_time(self, key):
|
||||
"""
|
||||
Parse an optional metadata key containing a time: if present, complain
|
||||
if it doesn't parse.
|
||||
Return None if not present or invalid.
|
||||
"""
|
||||
if key in self.exam_info:
|
||||
try:
|
||||
return Date().from_json(self.exam_info[key])
|
||||
except ValueError as e:
|
||||
msg = "Exam {0} in course {1} loaded with a bad exam_info key '{2}': '{3}'".format(self.exam_name, self.course_id, self.exam_info[key], e)
|
||||
log.warning(msg)
|
||||
return None
|
||||
|
||||
def has_started(self):
|
||||
return datetime.now(UTC()) > self.first_eligible_appointment_date
|
||||
|
||||
def has_ended(self):
|
||||
return datetime.now(UTC()) > self.last_eligible_appointment_date
|
||||
|
||||
def has_started_registration(self):
|
||||
return datetime.now(UTC()) > self.registration_start_date
|
||||
|
||||
def has_ended_registration(self):
|
||||
return datetime.now(UTC()) > self.registration_end_date
|
||||
|
||||
def is_registering(self):
|
||||
now = datetime.now(UTC())
|
||||
return now >= self.registration_start_date and now <= self.registration_end_date
|
||||
|
||||
@property
|
||||
def first_eligible_appointment_date_text(self):
|
||||
return self.first_eligible_appointment_date.strftime("%b %d, %Y")
|
||||
|
||||
@property
|
||||
def last_eligible_appointment_date_text(self):
|
||||
return self.last_eligible_appointment_date.strftime("%b %d, %Y")
|
||||
|
||||
@property
|
||||
def registration_end_date_text(self):
|
||||
return date_utils.get_default_time_display(self.registration_end_date)
|
||||
|
||||
@property
|
||||
def current_test_center_exam(self):
|
||||
exams = [exam for exam in self.test_center_exams if exam.has_started_registration() and not exam.has_ended()]
|
||||
if len(exams) > 1:
|
||||
# TODO: output some kind of warning. This should already be
|
||||
# caught if we decide to do validation at load time.
|
||||
return exams[0]
|
||||
elif len(exams) == 1:
|
||||
return exams[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_test_center_exam(self, exam_series_code):
|
||||
exams = [exam for exam in self.test_center_exams if exam.exam_series_code == exam_series_code]
|
||||
return exams[0] if len(exams) == 1 else None
|
||||
|
||||
@property
|
||||
def number(self):
|
||||
return self.location.course
|
||||
|
||||
@@ -79,7 +79,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
|
||||
|
||||
# tags that really need unique names--they store (or should store) state.
|
||||
need_uniq_names = ('problem', 'sequential', 'video', 'course', 'chapter',
|
||||
'videosequence', 'poll_question', 'timelimit', 'vertical')
|
||||
'videosequence', 'poll_question', 'vertical')
|
||||
|
||||
attr = xml_data.attrib
|
||||
tag = xml_data.tag
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
import core
|
||||
import xmodule_asserts
|
||||
@@ -1,31 +0,0 @@
|
||||
"""
|
||||
View assertion functions for XModules
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from nose.tools import assert_equals, assert_not_equals # pylint: disable=no-name-in-module
|
||||
|
||||
from xmodule.timelimit_module import TimeLimitModule, TimeLimitDescriptor
|
||||
|
||||
from xmodule.tests.rendering.core import assert_student_view_valid_html, assert_student_view_invalid_html
|
||||
|
||||
|
||||
@assert_student_view_valid_html.register(TimeLimitModule)
|
||||
@assert_student_view_valid_html.register(TimeLimitDescriptor)
|
||||
def _(block, html):
|
||||
"""
|
||||
Assert that a TimeLimitModule renders student_view html correctly
|
||||
"""
|
||||
assert_not_equals(0, block.get_display_items())
|
||||
assert_student_view_valid_html(block.get_children()[0], html)
|
||||
|
||||
|
||||
@assert_student_view_invalid_html.register(TimeLimitModule)
|
||||
@assert_student_view_invalid_html.register(TimeLimitDescriptor)
|
||||
def _(block, html):
|
||||
"""
|
||||
Assert that a TimeLimitModule renders student_view html correctly
|
||||
"""
|
||||
assert_equals(0, len(block.get_display_items()))
|
||||
assert_equals(u"", html)
|
||||
@@ -1,136 +0,0 @@
|
||||
import logging
|
||||
|
||||
from lxml import etree
|
||||
from time import time
|
||||
|
||||
from xmodule.editing_module import XMLEditingDescriptor
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.progress import Progress
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xblock.fields import Float, String, Boolean, Scope
|
||||
from xblock.fragment import Fragment
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TimeLimitFields(object):
|
||||
has_children = True
|
||||
|
||||
beginning_at = Float(help="The time this timer was started", scope=Scope.user_state)
|
||||
ending_at = Float(help="The time this timer will end", scope=Scope.user_state)
|
||||
accomodation_code = String(help="A code indicating accommodations to be given the student", scope=Scope.user_state)
|
||||
time_expired_redirect_url = String(help="Url to redirect users to after the timelimit has expired", scope=Scope.settings)
|
||||
duration = Float(help="The length of this timer", scope=Scope.settings)
|
||||
suppress_toplevel_navigation = Boolean(help="Whether the toplevel navigation should be suppressed when viewing this module", scope=Scope.settings)
|
||||
|
||||
|
||||
class TimeLimitModule(TimeLimitFields, XModule):
|
||||
'''
|
||||
Wrapper module which imposes a time constraint for the completion of its child.
|
||||
'''
|
||||
|
||||
# For a timed activity, we are only interested here
|
||||
# in time-related accommodations, and these should be disjoint.
|
||||
# (For proctored exams, it is possible to have multiple accommodations
|
||||
# apply to an exam, so they require accommodating a multi-choice.)
|
||||
TIME_ACCOMMODATION_CODES = (('NONE', 'No Time Accommodation'),
|
||||
('ADDHALFTIME', 'Extra Time - 1 1/2 Time'),
|
||||
('ADD30MIN', 'Extra Time - 30 Minutes'),
|
||||
('DOUBLE', 'Extra Time - Double Time'),
|
||||
('TESTING', 'Extra Time -- Large amount for testing purposes')
|
||||
)
|
||||
|
||||
def _get_accommodated_duration(self, duration):
|
||||
'''
|
||||
Get duration for activity, as adjusted for accommodations.
|
||||
Input and output are expressed in seconds.
|
||||
'''
|
||||
if self.accommodation_code is None or self.accommodation_code == 'NONE':
|
||||
return duration
|
||||
elif self.accommodation_code == 'ADDHALFTIME':
|
||||
# TODO: determine what type to return
|
||||
return int(duration * 1.5)
|
||||
elif self.accommodation_code == 'ADD30MIN':
|
||||
return (duration + (30 * 60))
|
||||
elif self.accommodation_code == 'DOUBLE':
|
||||
return (duration * 2)
|
||||
elif self.accommodation_code == 'TESTING':
|
||||
# when testing, set timer to run for a week at a time.
|
||||
return 3600 * 24 * 7
|
||||
|
||||
@property
|
||||
def has_begun(self):
|
||||
return self.beginning_at is not None
|
||||
|
||||
@property
|
||||
def has_ended(self):
|
||||
if not self.ending_at:
|
||||
return False
|
||||
return self.ending_at < time()
|
||||
|
||||
def begin(self, duration):
|
||||
'''
|
||||
Sets the starting time and ending time for the activity,
|
||||
based on the duration provided (in seconds).
|
||||
'''
|
||||
self.beginning_at = time()
|
||||
modified_duration = self._get_accommodated_duration(duration)
|
||||
self.ending_at = self.beginning_at + modified_duration
|
||||
|
||||
def get_remaining_time_in_ms(self):
|
||||
return int((self.ending_at - time()) * 1000)
|
||||
|
||||
def student_view(self, context):
|
||||
# assumes there is one and only one child, so it only renders the first child
|
||||
children = self.get_display_items()
|
||||
if children:
|
||||
child = children[0]
|
||||
return child.render('student_view', context)
|
||||
else:
|
||||
return Fragment()
|
||||
|
||||
def get_progress(self):
|
||||
''' Return the total progress, adding total done and total available.
|
||||
(assumes that each submodule uses the same "units" for progress.)
|
||||
'''
|
||||
# TODO: Cache progress or children array?
|
||||
children = self.get_children()
|
||||
progresses = [child.get_progress() for child in children]
|
||||
progress = reduce(Progress.add_counts, progresses)
|
||||
return progress
|
||||
|
||||
def handle_ajax(self, _dispatch, _data):
|
||||
raise NotFoundError('Unexpected dispatch type')
|
||||
|
||||
def get_icon_class(self):
|
||||
children = self.get_children()
|
||||
if children:
|
||||
return children[0].get_icon_class()
|
||||
else:
|
||||
return "other"
|
||||
|
||||
class TimeLimitDescriptor(TimeLimitFields, XMLEditingDescriptor, XmlDescriptor):
|
||||
|
||||
module_class = TimeLimitModule
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
children = []
|
||||
for child in xml_object:
|
||||
try:
|
||||
children.append(system.process_xml(etree.tostring(child, encoding='unicode')).location.url())
|
||||
except Exception as e:
|
||||
log.exception("Unable to load child when parsing TimeLimit wrapper. Continuing...")
|
||||
if system.error_tracker is not None:
|
||||
system.error_tracker("ERROR: " + str(e))
|
||||
continue
|
||||
return {}, children
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
xml_object = etree.Element('timelimit')
|
||||
for child in self.get_children():
|
||||
xml_object.append(
|
||||
etree.fromstring(child.export_to_xml(resource_fs)))
|
||||
return xml_object
|
||||
@@ -3,7 +3,6 @@ import copy
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
from lxml import etree
|
||||
|
||||
from xblock.fields import Dict, Scope, ScopeIds
|
||||
@@ -133,15 +132,12 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
'ispublic', # if True, then course is listed for all users; see
|
||||
'xqa_key', # for xqaa server access
|
||||
'giturl', # url of git server for origin of file
|
||||
# information about testcenter exams is a dict (of dicts), not a string,
|
||||
# so it cannot be easily exportable as a course element's attribute.
|
||||
'testcenter_info',
|
||||
# VS[compat] Remove once unused.
|
||||
'name', 'slug')
|
||||
|
||||
metadata_to_strip = ('data_dir',
|
||||
'tabs', 'grading_policy', 'published_by', 'published_date',
|
||||
'discussion_blackouts', 'testcenter_info',
|
||||
'discussion_blackouts',
|
||||
# VS[compat] -- remove the below attrs once everything is in the CMS
|
||||
'course', 'org', 'url_name', 'filename',
|
||||
# Used for storing xml attributes between import and export, for roundtrips
|
||||
|
||||
@@ -3,21 +3,6 @@
|
||||
"graceperiod": "2 days 5 hours 59 minutes 59 seconds",
|
||||
"start": "2011-07-17T12:00",
|
||||
"display_name": "Toy Course",
|
||||
"testcenter_info": {
|
||||
"Midterm_Exam": {
|
||||
"Exam_Series_Code": "Midterm_Exam",
|
||||
"First_Eligible_Appointment_Date": "2012-11-09T00:00",
|
||||
"Last_Eligible_Appointment_Date": "2012-11-09T23:59"
|
||||
},
|
||||
"Final_Exam": {
|
||||
"Exam_Series_Code": "mit6002xfall12a",
|
||||
"Exam_Display_Name": "Final Exam",
|
||||
"First_Eligible_Appointment_Date": "2013-01-25T00:00",
|
||||
"Last_Eligible_Appointment_Date": "2013-01-25T23:59",
|
||||
"Registration_Start_Date": "2013-01-01T00:00",
|
||||
"Registration_End_Date": "2013-01-21T23:59"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chapter/Overview": {
|
||||
"display_name": "Overview"
|
||||
|
||||
@@ -19,7 +19,7 @@ All of our tables will be described below, first in summary form with field type
|
||||
.. list-table::
|
||||
:widths: 10 80
|
||||
:header-rows: 1
|
||||
|
||||
|
||||
* - Value
|
||||
- Meaning
|
||||
* - `int`
|
||||
@@ -36,13 +36,13 @@ All of our tables will be described below, first in summary form with field type
|
||||
- Date
|
||||
* - `datetime`
|
||||
- Datetime in UTC, precision in seconds.
|
||||
|
||||
|
||||
`Null`
|
||||
|
||||
.. list-table::
|
||||
:widths: 10 80
|
||||
:header-rows: 1
|
||||
|
||||
|
||||
* - Value
|
||||
- Meaning
|
||||
* - `YES`
|
||||
@@ -57,7 +57,7 @@ All of our tables will be described below, first in summary form with field type
|
||||
.. list-table::
|
||||
:widths: 10 80
|
||||
:header-rows: 1
|
||||
|
||||
|
||||
* - Value
|
||||
- Meaning
|
||||
* - `PRI`
|
||||
@@ -252,19 +252,19 @@ There is an important split in demographic data gathered for the students who si
|
||||
|
||||
`old_names`
|
||||
A list of the previous names this user had, and the timestamps at which they submitted a request to change those names. These name change request submissions used to require a staff member to approve it before the name change took effect. This is no longer the case, though we still record their previous names.
|
||||
|
||||
|
||||
Note that the value stored for each entry is the name they had, not the name they requested to get changed to. People often changed their names as the time for certificate generation approached, to replace nicknames with their actual names or correct spelling/punctuation errors.
|
||||
|
||||
|
||||
The timestamps are UTC, like all datetimes stored in our system.
|
||||
|
||||
|
||||
`old_emails`
|
||||
A list of previous emails this user had, with timestamps of when they changed them, in a format similar to `old_names`. There was never an approval process for this.
|
||||
|
||||
|
||||
The timestamps are UTC, like all datetimes stored in our system.
|
||||
|
||||
|
||||
`6002x_exit_response`
|
||||
Answers to a survey that was sent to students after the prototype 6.002x course in the Spring of 2012. The questions and number of questions were randomly selected to measure how much survey length affected response rate. Only students from this course have this field.
|
||||
|
||||
|
||||
|
||||
`courseware`
|
||||
------------
|
||||
@@ -277,7 +277,7 @@ There is an important split in demographic data gathered for the students who si
|
||||
.. list-table::
|
||||
:widths: 10 80
|
||||
:header-rows: 1
|
||||
|
||||
|
||||
* - Value
|
||||
- Meaning
|
||||
* - `NULL`
|
||||
@@ -306,10 +306,10 @@ There is an important split in demographic data gathered for the students who si
|
||||
.. list-table::
|
||||
:widths: 10 80
|
||||
:header-rows: 1
|
||||
|
||||
|
||||
* - Value
|
||||
- Meaning
|
||||
* - `NULL`
|
||||
* - `NULL`
|
||||
- This student signed up before this information was collected
|
||||
* - `''` (blank)
|
||||
- User did not specify level of education.
|
||||
@@ -335,7 +335,7 @@ There is an important split in demographic data gathered for the students who si
|
||||
- None
|
||||
* - `'other'`
|
||||
- Other
|
||||
|
||||
|
||||
`goals`
|
||||
-------
|
||||
Text field collected during student signup in response to the prompt, "Goals in signing up for edX". We only started collecting this information after the transition from MITx to edX, so prototype course students will have `NULL` for this field. Students who elected not to enter anything will have a blank string.
|
||||
@@ -382,7 +382,7 @@ Any piece of content in the courseware can store state and score in the `coursew
|
||||
|
||||
.. warning::
|
||||
**Modules might not be what you expect!**
|
||||
|
||||
|
||||
It's important to understand what "modules" are in the context of our system, as the terminology can be confusing. For the conventions of this table and many parts of our code, a "module" is a content piece that appears in the courseware. This can be nearly anything that appears when users are in the courseware tab: a video, a piece of HTML, a problem, etc. Modules can also be collections of other modules, such as sequences, verticals (modules stacked together on the same page), weeks, chapters, etc. In fact, the course itself is a top level module that contains all the other contents of the course as children. You can imagine the entire course as a tree with modules at every node.
|
||||
|
||||
Modules can store state, but whether and how they do so is up to the implemenation for that particular kind of module. When a user loads page, we look up all the modules they need to render in order to display it, and then we ask the database to look up state for those modules for that user. If there is corresponding entry for that user for a given module, we create a new row and set the state to an empty JSON dictionary.
|
||||
@@ -420,7 +420,7 @@ The `courseware_studentmodule` table holds all courseware state for a given user
|
||||
.. list-table::
|
||||
:widths: 10 80
|
||||
:header-rows: 0
|
||||
|
||||
|
||||
* - `chapter`
|
||||
- The top level categories for a course. Each of these is usually labeled as a Week in the courseware, but this is just convention.
|
||||
* - `combinedopenended`
|
||||
@@ -437,8 +437,6 @@ The `courseware_studentmodule` table holds all courseware state for a given user
|
||||
- Self assessment problems. An early test of the open ended grading system that is not in widespread use yet. Recently deprecated in favor of `combinedopenended`.
|
||||
* - `sequential`
|
||||
- A collection of videos, problems, and other materials, rendered as a horizontal icon bar in the courseware.
|
||||
* - `timelimit`
|
||||
- A special module that records the time you start working on a piece of courseware and enforces time limits, used for Pearson exams. This hasn't been completely generalized yet, so is not available for widespread use.
|
||||
* - `videosequence`
|
||||
- A collection of videos, exercise problems, and other materials, rendered as a horizontal icon bar in the courseware. Use is inconsistent, and some courses use a `sequential` instead.
|
||||
|
||||
@@ -451,20 +449,20 @@ The `courseware_studentmodule` table holds all courseware state for a given user
|
||||
.. list-table:: Breakdown of example `module_id`: `i4x://MITx/3.091x/problemset/Sample_Problems`
|
||||
:widths: 10 20 70
|
||||
:header-rows: 1
|
||||
|
||||
|
||||
* - Part
|
||||
- Example
|
||||
- Definition
|
||||
* - `i4x://`
|
||||
-
|
||||
-
|
||||
- Just a convention we ran with. We had plans for the domain `i4x.org` at one point.
|
||||
* - `org`
|
||||
- `MITx`
|
||||
- The organization part of the ID, indicating what organization created this piece of content.
|
||||
* - `course_num`
|
||||
* - `course_num`
|
||||
- `3.091x`
|
||||
- The course number this content was created for. Note that there is no run information here, so you can't know what runs of the course this content is being used for from the `module_id` alone; you have to look at the `courseware_studentmodule.course_id` field.
|
||||
* - `module_type`
|
||||
* - `module_type`
|
||||
- `problemset`
|
||||
- The module type, same value as what's in the `courseware_studentmodule.module_type` field.
|
||||
* - `module_name`
|
||||
@@ -501,33 +499,6 @@ The `courseware_studentmodule` table holds all courseware state for a given user
|
||||
`selfassessment`
|
||||
TODO: More details to come.
|
||||
|
||||
`timelimit`
|
||||
This very uncommon type was only used in one Pearson exam for one course, and the format may change significantly in the future. It is currently a JSON dictionary with fields:
|
||||
|
||||
.. list-table::
|
||||
:widths: 10 20 70
|
||||
:header-rows: 1
|
||||
|
||||
* - JSON field
|
||||
- Example
|
||||
- Definition
|
||||
* - `beginning_at`
|
||||
- `1360590255.488154`
|
||||
- UTC time as measured in seconds since UNIX epoch representing when the exam was started.
|
||||
* - `ending_at`
|
||||
- `1360596632.559758`
|
||||
- UTC time as measured in seconds since UNIX epoch representing the time the exam will close.
|
||||
* - `accomodation_codes`
|
||||
- `DOUBLE`
|
||||
- (optional) Sometimes students are given more time for accessibility reasons. Possible values are:
|
||||
|
||||
* `NONE`: no time accommodation
|
||||
* `ADDHALFTIME`: 1.5X normal time allowance
|
||||
* `ADD30MIN`: normal time allowance + 30 minutes
|
||||
* `DOUBLE`: 2X normal time allowance
|
||||
* `TESTING`: extra long period (for testing/debugging)
|
||||
|
||||
|
||||
`grade`
|
||||
-------
|
||||
Floating point value indicating the total unweighted grade for this problem that the student has scored. Basically how many responses they got right within the problem.
|
||||
@@ -608,13 +579,13 @@ The generatedcertificate table tracks certificate state for students who have be
|
||||
* `notpassing`
|
||||
* `restricted`
|
||||
* `error`
|
||||
|
||||
|
||||
After a course has been graded and certificates have been issued status will be one of:
|
||||
|
||||
|
||||
* `downloadable`
|
||||
* `notpassing`
|
||||
* `restricted`
|
||||
|
||||
|
||||
If the status is `downloadable` then the student passed the course and there will be a certificate available for download.
|
||||
|
||||
`download_url`
|
||||
|
||||
@@ -27,7 +27,6 @@ class StudentModule(models.Model):
|
||||
MODULE_TYPES = (('problem', 'problem'),
|
||||
('video', 'video'),
|
||||
('html', 'html'),
|
||||
('timelimit', 'timelimit'),
|
||||
)
|
||||
## These three are the key for the object
|
||||
module_type = models.CharField(max_length=32, choices=MODULE_TYPES, default='problem', db_index=True)
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
"""
|
||||
Tests of the TimeLimitModule
|
||||
|
||||
TODO: This should be a test in common/lib/xmodule. However,
|
||||
actually rendering HTML templates for XModules at this point requires
|
||||
Django (which is storing the templates), so the test can't run in isolation
|
||||
"""
|
||||
from xmodule.modulestore.tests.factories import ItemFactory
|
||||
from xmodule.tests.rendering.core import assert_student_view
|
||||
|
||||
from . import XModuleRenderingTestBase
|
||||
|
||||
|
||||
class TestTimeLimitModuleRendering(XModuleRenderingTestBase):
|
||||
"""
|
||||
Tests of TimeLimitModule html rendering
|
||||
"""
|
||||
def test_with_children(self):
|
||||
block = ItemFactory.create(category='timelimit')
|
||||
block.xmodule_runtime = self.new_module_runtime()
|
||||
ItemFactory.create(category='html', data='<html>This is just text</html>', parent=block)
|
||||
|
||||
assert_student_view(block, block.render('student_view'))
|
||||
|
||||
def test_without_children(self):
|
||||
block = ItemFactory.create(category='timelimit')
|
||||
block.xmodule_runtime = self.new_module_runtime()
|
||||
|
||||
assert_student_view(block, block.render('student_view'))
|
||||
@@ -172,71 +172,6 @@ def save_child_position(seq_module, child_name):
|
||||
seq_module.save()
|
||||
|
||||
|
||||
def check_for_active_timelimit_module(request, course_id, course):
|
||||
"""
|
||||
Looks for a timing module for the given user and course that is currently active.
|
||||
If found, returns a context dict with timer-related values to enable display of time remaining.
|
||||
"""
|
||||
context = {}
|
||||
|
||||
# TODO (cpennington): Once we can query the course structure, replace this with such a query
|
||||
timelimit_student_modules = StudentModule.objects.filter(student=request.user, course_id=course_id, module_type='timelimit')
|
||||
if timelimit_student_modules:
|
||||
for timelimit_student_module in timelimit_student_modules:
|
||||
# get the corresponding section_descriptor for the given StudentModel entry:
|
||||
module_state_key = timelimit_student_module.module_state_key
|
||||
timelimit_descriptor = modulestore().get_instance(course_id, Location(module_state_key))
|
||||
timelimit_module_cache = FieldDataCache.cache_for_descriptor_descendents(course.id, request.user,
|
||||
timelimit_descriptor, depth=None)
|
||||
timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor,
|
||||
timelimit_module_cache, course.id, position=None)
|
||||
if timelimit_module is not None and timelimit_module.category == 'timelimit' and \
|
||||
timelimit_module.has_begun and not timelimit_module.has_ended:
|
||||
location = timelimit_module.location
|
||||
# determine where to go when the timer expires:
|
||||
if timelimit_descriptor.time_expired_redirect_url is None:
|
||||
raise Http404("no time_expired_redirect_url specified at this location: {} ".format(timelimit_module.location))
|
||||
context['time_expired_redirect_url'] = timelimit_descriptor.time_expired_redirect_url
|
||||
# Fetch the remaining time relative to the end time as stored in the module when it was started.
|
||||
# This value should be in milliseconds.
|
||||
remaining_time = timelimit_module.get_remaining_time_in_ms()
|
||||
context['timer_expiration_duration'] = remaining_time
|
||||
context['suppress_toplevel_navigation'] = timelimit_descriptor.suppress_toplevel_navigation
|
||||
return_url = reverse('jump_to', kwargs={'course_id': course_id, 'location': location})
|
||||
context['timer_navigation_return_url'] = return_url
|
||||
return context
|
||||
|
||||
|
||||
def update_timelimit_module(user, course_id, field_data_cache, timelimit_descriptor, timelimit_module):
|
||||
"""
|
||||
Updates the state of the provided timing module, starting it if it hasn't begun.
|
||||
Returns dict with timer-related values to enable display of time remaining.
|
||||
Returns 'timer_expiration_duration' in dict if timer is still active, and not if timer has expired.
|
||||
"""
|
||||
context = {}
|
||||
# determine where to go when the exam ends:
|
||||
if timelimit_descriptor.time_expired_redirect_url is None:
|
||||
raise Http404("No time_expired_redirect_url specified at this location: {} ".format(timelimit_module.location))
|
||||
context['time_expired_redirect_url'] = timelimit_descriptor.time_expired_redirect_url
|
||||
|
||||
if not timelimit_module.has_ended:
|
||||
if not timelimit_module.has_begun:
|
||||
# user has not started the exam, so start it now.
|
||||
if timelimit_descriptor.duration is None:
|
||||
raise Http404("No duration specified at this location: {} ".format(timelimit_module.location))
|
||||
# The user may have an accommodation that has been granted to them.
|
||||
# This accommodation information should already be stored in the module's state.
|
||||
timelimit_module.begin(timelimit_descriptor.duration)
|
||||
|
||||
# the exam has been started, either because the student is returning to the
|
||||
# exam page, or because they have just visited it. Fetch the remaining time relative to the
|
||||
# end time as stored in the module when it was started.
|
||||
context['timer_expiration_duration'] = timelimit_module.get_remaining_time_in_ms()
|
||||
# also use the timed module to determine whether top-level navigation is visible:
|
||||
context['suppress_toplevel_navigation'] = timelimit_descriptor.suppress_toplevel_navigation
|
||||
return context
|
||||
|
||||
|
||||
def chat_settings(course, user):
|
||||
"""
|
||||
Returns a dict containing the settings required to connect to a
|
||||
@@ -390,22 +325,8 @@ def index(request, course_id, chapter=None, section=None,
|
||||
|
||||
# Save where we are in the chapter
|
||||
save_child_position(chapter_module, section)
|
||||
|
||||
# check here if this section *is* a timed module.
|
||||
if section_module.category == 'timelimit':
|
||||
timer_context = update_timelimit_module(user, course_id, section_field_data_cache,
|
||||
section_descriptor, section_module)
|
||||
if 'timer_expiration_duration' in timer_context:
|
||||
context.update(timer_context)
|
||||
else:
|
||||
# if there is no expiration defined, then we know the timer has expired:
|
||||
return HttpResponseRedirect(timer_context['time_expired_redirect_url'])
|
||||
else:
|
||||
# check here if this page is within a course that has an active timed module running. If so, then
|
||||
# add in the appropriate timer information to the rendering context:
|
||||
context.update(check_for_active_timelimit_module(request, course_id, course))
|
||||
|
||||
context['fragment'] = section_module.render('student_view')
|
||||
|
||||
else:
|
||||
# section is none, so display a message
|
||||
prev_section = get_current_child(chapter_module)
|
||||
|
||||
@@ -290,12 +290,6 @@ OPEN_ENDED_GRADING_INTERFACE = AUTH_TOKENS.get('OPEN_ENDED_GRADING_INTERFACE',
|
||||
EMAIL_HOST_USER = AUTH_TOKENS.get('EMAIL_HOST_USER', '') # django default is ''
|
||||
EMAIL_HOST_PASSWORD = AUTH_TOKENS.get('EMAIL_HOST_PASSWORD', '') # django default is ''
|
||||
|
||||
PEARSON_TEST_USER = "pearsontest"
|
||||
PEARSON_TEST_PASSWORD = AUTH_TOKENS.get("PEARSON_TEST_PASSWORD")
|
||||
|
||||
# Pearson hash for import/export
|
||||
PEARSON = AUTH_TOKENS.get("PEARSON")
|
||||
|
||||
# Datadog for events!
|
||||
DATADOG = AUTH_TOKENS.get("DATADOG", {})
|
||||
DATADOG.update(ENV_TOKENS.get("DATADOG", {}))
|
||||
|
||||
@@ -523,11 +523,6 @@ WIKI_USE_BOOTSTRAP_SELECT_WIDGET = False
|
||||
WIKI_LINK_LIVE_LOOKUPS = False
|
||||
WIKI_LINK_DEFAULT_LEVEL = 2
|
||||
|
||||
################################# Pearson TestCenter config ################
|
||||
|
||||
PEARSONVUE_SIGNINPAGE_URL = "https://www1.pearsonvue.com/testtaker/signin/SignInPage/EDX"
|
||||
# TESTCENTER_ACCOMMODATION_REQUEST_EMAIL = "exam-help@example.com"
|
||||
|
||||
##### Feedback submission mechanism #####
|
||||
FEEDBACK_SUBMISSION_EMAIL = None
|
||||
|
||||
|
||||
@@ -254,9 +254,6 @@ MITX_FEATURES['RESTRICT_ENROLL_BY_REG_METHOD'] = True
|
||||
|
||||
PIPELINE_SASS_ARGUMENTS = '--debug-info --require {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT)
|
||||
|
||||
########################## PEARSON TESTING ###########################
|
||||
MITX_FEATURES['ENABLE_PEARSON_LOGIN'] = False
|
||||
|
||||
########################## ANALYTICS TESTING ########################
|
||||
|
||||
ANALYTICS_SERVER_URL = "http://127.0.0.1:9000/"
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
@import 'multicourse/home';
|
||||
@import 'multicourse/dashboard';
|
||||
@import 'multicourse/account';
|
||||
@import 'multicourse/testcenter-register';
|
||||
@import 'multicourse/courses';
|
||||
@import 'multicourse/course_about';
|
||||
@import 'multicourse/jobs';
|
||||
|
||||
@@ -1,790 +0,0 @@
|
||||
// Pearson VUE Test Center Registration
|
||||
// =====
|
||||
|
||||
.testcenter-register {
|
||||
@include clearfix;
|
||||
padding: 60px 0px 120px;
|
||||
|
||||
// reset - horrible, but necessary
|
||||
p, a, h1, h2, h3, h4, h5, h6 {
|
||||
font-family: $sans-serif !important;
|
||||
}
|
||||
|
||||
// basic layout
|
||||
.introduction {
|
||||
width: flex-grid(12);
|
||||
}
|
||||
|
||||
.message-status-registration {
|
||||
width: flex-grid(12);
|
||||
}
|
||||
|
||||
.content, aside {
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-right: flex-gutter();
|
||||
width: flex-grid(8);
|
||||
float: left;
|
||||
}
|
||||
|
||||
aside {
|
||||
margin: 0;
|
||||
width: flex-grid(4);
|
||||
float: left;
|
||||
}
|
||||
|
||||
// introduction
|
||||
.introduction {
|
||||
|
||||
header {
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-family: $sans-serif;
|
||||
font-size: 16px;
|
||||
color: $lighter-base-font-color;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: $sans-serif;
|
||||
font-size: 34px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// content
|
||||
.content {
|
||||
background: rgb(255,255,255);
|
||||
}
|
||||
|
||||
// form
|
||||
.form-fields-primary, .form-fields-secondary {
|
||||
border-bottom: 1px solid rgba(0,0,0,0.25);
|
||||
box-shadow: 0 1px 2px 0 rgba(0,0,0, 0.1);
|
||||
}
|
||||
|
||||
form {
|
||||
border: 1px solid rgb(216, 223, 230);
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0,0,0, 0.2);
|
||||
|
||||
.instructions, .note {
|
||||
margin: 0;
|
||||
padding: ($baseline*1.5) ($baseline*1.5) 0 ($baseline*1.5);
|
||||
font-size: 14px;
|
||||
color: tint($base-font-color, 20%);
|
||||
|
||||
strong {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.title, .indicator {
|
||||
color: $base-font-color;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border-bottom: 1px solid rgba(216, 223, 230, 0.50);
|
||||
padding: ($baseline*1.5);
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
@include clearfix();
|
||||
padding: ($baseline*1.5);
|
||||
|
||||
button[type="submit"] {
|
||||
display: block;
|
||||
@include button(simple, $blue);
|
||||
@include box-sizing(border-box);
|
||||
border-radius: 3px;
|
||||
font: bold 15px/1.6rem $sans-serif;
|
||||
letter-spacing: 0;
|
||||
padding: ($baseline*0.75) $baseline;
|
||||
text-align: center;
|
||||
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.action-primary {
|
||||
float: left;
|
||||
width: flex-grid(5,8);
|
||||
margin-right: flex-gutter(2);
|
||||
}
|
||||
|
||||
.action-secondary {
|
||||
display: block;
|
||||
float: left;
|
||||
width: flex-grid(2,8);
|
||||
margin-top: $baseline;
|
||||
padding: ($baseline/4);
|
||||
font-size: 13px;
|
||||
text-align: right;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&.error {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.list-input {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
.field {
|
||||
border-bottom: 1px dotted rgba(216, 223, 230, 0.5);
|
||||
margin: 0 0 $baseline 0;
|
||||
padding: 0 0 $baseline 0;
|
||||
|
||||
&:last-child {
|
||||
border: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&.disabled, &.submitted {
|
||||
color: rgba(0,0,0,.25);
|
||||
|
||||
label {
|
||||
cursor: text;
|
||||
|
||||
&:after {
|
||||
margin-left: ($baseline/4);
|
||||
}
|
||||
}
|
||||
|
||||
textarea, input {
|
||||
background: rgb(255,255,255);
|
||||
color: rgba(0,0,0,.25);
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
label:after {
|
||||
color: rgba(0,0,0,.35);
|
||||
content: "(Disabled Currently)";
|
||||
}
|
||||
}
|
||||
|
||||
&.submitted {
|
||||
|
||||
label:after {
|
||||
content: "(Previously Submitted and Not Editable)";
|
||||
}
|
||||
|
||||
.value {
|
||||
border-radius: 3px;
|
||||
border: 1px solid #C8C8C8;
|
||||
padding: $baseline ($baseline*0.75);
|
||||
background: #FAFAFA;
|
||||
}
|
||||
}
|
||||
|
||||
&.error {
|
||||
|
||||
label {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
border-color: tint($red,50%);
|
||||
}
|
||||
}
|
||||
|
||||
&.required {
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
label:after {
|
||||
margin-left: ($baseline/4);
|
||||
content: "*";
|
||||
}
|
||||
}
|
||||
|
||||
label, input, textarea {
|
||||
display: block;
|
||||
font-family: $sans-serif;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
label {
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
@include transition(color 0.15s ease-in-out 0s);
|
||||
|
||||
&.is-focused {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: ($baseline/2);
|
||||
|
||||
&.long {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.short {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
textarea.long {
|
||||
height: ($baseline*5);
|
||||
}
|
||||
}
|
||||
|
||||
.field-group {
|
||||
@include clearfix();
|
||||
border-bottom: 1px dotted rgba(216, 223, 230, 0.5);
|
||||
margin: 0 0 $baseline 0;
|
||||
padding: 0 0 $baseline 0;
|
||||
|
||||
.field {
|
||||
display: block;
|
||||
float: left;
|
||||
border-bottom: none;
|
||||
margin: 0 $baseline ($baseline/2) 0;
|
||||
padding-bottom: 0;
|
||||
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.addresses {
|
||||
|
||||
.field {
|
||||
width: 45%;
|
||||
}
|
||||
}
|
||||
|
||||
&.postal-2 {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
|
||||
}
|
||||
|
||||
&.phoneinfo {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
|
||||
> .instructions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.field {
|
||||
opacity: 0.6;
|
||||
|
||||
.label, label {
|
||||
cursor: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// form - specifics
|
||||
.form-fields-secondary {
|
||||
display: none;
|
||||
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
|
||||
fieldset {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-fields-secondary-visibility {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: $baseline ($baseline*1.5) 0 ($baseline*1.5);
|
||||
font-size: 13px;
|
||||
|
||||
&.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// aside
|
||||
aside {
|
||||
padding-left: $baseline;
|
||||
|
||||
.message-status {
|
||||
border-radius: 3px;
|
||||
margin: 0 0 ($baseline*2) 0;
|
||||
border: 1px solid #ccc;
|
||||
padding: 0;
|
||||
background: tint($yellow,90%);
|
||||
|
||||
> * {
|
||||
padding: $baseline;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.label, .value {
|
||||
display: block;
|
||||
}
|
||||
|
||||
h3, h4, h5 {
|
||||
font-family: $sans-serif;
|
||||
}
|
||||
|
||||
h3 {
|
||||
border-bottom: 1px solid tint(rgb(0,0,0), 90%);
|
||||
padding-bottom: ($baseline*0.75);
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: ($baseline/4);
|
||||
}
|
||||
|
||||
.status-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: $baseline;
|
||||
|
||||
> .item {
|
||||
@include clearfix();
|
||||
margin: 0 0 ($baseline*0.75) 0;
|
||||
border-bottom: 1px solid tint(rgb(0,0,0), 95%);
|
||||
padding: 0 0 ($baseline/2) 0;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: ($baseline/4);
|
||||
position: relative;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: $baseline;
|
||||
margin-left: $baseline;
|
||||
content: "not started";
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
font-weight: normal;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.details, .item, .instructions {
|
||||
@include transition(opacity 0.10s ease-in-out 0s);
|
||||
font-size: 13px;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
&:before {
|
||||
border-radius: $baseline;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
display: block;
|
||||
float: left;
|
||||
width: ($baseline/2);
|
||||
height: ($baseline/2);
|
||||
margin: 0 ($baseline/2) 0 0;
|
||||
background: $dark-gray;
|
||||
content: "";
|
||||
}
|
||||
|
||||
// specific states
|
||||
&.status-processed {
|
||||
|
||||
&:before {
|
||||
background: green;
|
||||
}
|
||||
|
||||
.title:after {
|
||||
color: green;
|
||||
content: "processed";
|
||||
}
|
||||
|
||||
&.status-registration {
|
||||
.exam-link {
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.status-pending {
|
||||
|
||||
&:before {
|
||||
background: transparent;
|
||||
border: 1px dotted gray;
|
||||
}
|
||||
|
||||
.title:after {
|
||||
color: gray;
|
||||
content: "pending";
|
||||
}
|
||||
}
|
||||
|
||||
&.status-rejected {
|
||||
|
||||
&:before {
|
||||
background: $red;
|
||||
}
|
||||
|
||||
.title:after {
|
||||
color: red;
|
||||
content: "rejected";
|
||||
}
|
||||
|
||||
.call-link {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
&.status-initial {
|
||||
|
||||
&:before {
|
||||
background: transparent;
|
||||
border: 1px dotted gray;
|
||||
}
|
||||
|
||||
.title:after {
|
||||
color: gray;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
|
||||
.details, .item, .instructions {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sub menus
|
||||
.accommodations-list, .error-list {
|
||||
list-style: none;
|
||||
margin: ($baseline/2) 0;
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
|
||||
.item {
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// actions
|
||||
.contact-link {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.actions {
|
||||
box-shadow: inset 0 1px 1px 0px rgba(0,0,0,0.2);
|
||||
border-top: 1px solid tint(rgb(0,0,0), 90%);
|
||||
padding-top: ($baseline*0.75);
|
||||
background: tint($yellow,70%);
|
||||
font-size: 14px;
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.label, .value {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
|
||||
.value {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.message-copy {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.exam-button {
|
||||
@include button(simple, $pink);
|
||||
display: block;
|
||||
margin: ($baseline/2) 0 0 0;
|
||||
padding: ($baseline/2) $baseline;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover, &:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.registration-number {
|
||||
|
||||
.label {
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.registration-processed {
|
||||
|
||||
.message-copy {
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .details {
|
||||
border-bottom: 1px solid rgba(216, 223, 230, 0.5);
|
||||
margin: 0 0 $baseline 0;
|
||||
padding: 0 $baseline $baseline $baseline;
|
||||
font-family: $sans-serif;
|
||||
font-size: 14px;
|
||||
|
||||
&:last-child {
|
||||
border: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
font-family: $sans-serif;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.label, .value {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: rgba(0,0,0,.65);
|
||||
margin-right: ($baseline/2);
|
||||
}
|
||||
|
||||
.value {
|
||||
color: rgb(0,0,0);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.details-course {
|
||||
|
||||
}
|
||||
|
||||
.details-registration {
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// status messages
|
||||
.message {
|
||||
border-radius: 3px;
|
||||
display: none;
|
||||
margin: $baseline 0;
|
||||
padding: ($baseline/2) $baseline;
|
||||
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.message-copy {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
// registration status
|
||||
&.message-flash {
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
margin: 0 0 ($baseline*2) 0;
|
||||
border: 1px solid #ccc;
|
||||
padding-top: ($baseline*0.75);
|
||||
background: tint($yellow,70%);
|
||||
font-size: 14px;
|
||||
|
||||
.message-title, .message-copy {
|
||||
}
|
||||
|
||||
.message-title {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
}
|
||||
|
||||
.message-copy {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.contact-button {
|
||||
@include button(simple, $blue);
|
||||
}
|
||||
|
||||
.exam-button {
|
||||
@include button(simple, $pink);
|
||||
}
|
||||
|
||||
.button {
|
||||
position: absolute;
|
||||
top: ($baseline/4);
|
||||
right: $baseline;
|
||||
margin: ($baseline/2) 0 0 0;
|
||||
padding: ($baseline/2) $baseline;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0;
|
||||
|
||||
&:hover, &:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.message-action {
|
||||
|
||||
.message-title, .message-copy {
|
||||
width: 65%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// submission error
|
||||
&.submission-error {
|
||||
@include box-sizing(border-box);
|
||||
float: left;
|
||||
width: flex-grid(8,8);
|
||||
border: 1px solid tint($red,85%);
|
||||
background: tint($red,95%);
|
||||
font-size: 14px;
|
||||
|
||||
#submission-error-heading {
|
||||
margin-bottom: ($baseline/2);
|
||||
border-bottom: 1px solid tint($red, 85%);
|
||||
padding-bottom: ($baseline/2);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.field-name, .field-error {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.field-name {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
|
||||
.field-error {
|
||||
color: tint($red, 55%);
|
||||
}
|
||||
|
||||
p {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
margin-bottom: ($baseline/2);
|
||||
padding: 0;
|
||||
|
||||
span {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $red;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// submission success
|
||||
&.submission-saved {
|
||||
border: 1px solid tint($blue,85%);
|
||||
background: tint($blue,95%);
|
||||
|
||||
.message-copy {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
// specific - registration closed
|
||||
&.registration-closed {
|
||||
@include border-bottom-radius(0);
|
||||
margin-top: 0;
|
||||
border-bottom: 1px solid $light-gray;
|
||||
padding: $baseline;
|
||||
background: tint($light-gray,50%);
|
||||
|
||||
.message-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.message-copy {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-shown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// hidden
|
||||
.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,482 +0,0 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<%!
|
||||
from django.core.urlresolvers import reverse
|
||||
from courseware.courses import course_image_url, get_course_about_section
|
||||
from courseware.access import has_access
|
||||
from certificates.models import CertificateStatuses
|
||||
%>
|
||||
<%inherit file="main.html" />
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
<%block name="title"><title>${_('Pearson VUE Test Center Proctoring - Registration')}</title></%block>
|
||||
|
||||
<%block name="js_extra">
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
|
||||
// if form is disabled or registration is closed
|
||||
$('#testcenter_register_form.disabled').find('input, textarea, button').attr('disabled','disabled');
|
||||
|
||||
// toggling accommodations elements
|
||||
$('.form-fields-secondary-visibility').click(function(e){
|
||||
e.preventDefault();
|
||||
$(this).addClass("is-hidden");
|
||||
|
||||
$('.form-fields-secondary').addClass("is-shown");
|
||||
});
|
||||
|
||||
|
||||
$(document).on('ajax:success', '#testcenter_register_form', function(data, json, xhr) {
|
||||
if(json.success) {
|
||||
// when a form is successfully filled out, return back to the dashboard.
|
||||
location.href="${reverse('dashboard')}";
|
||||
} else {
|
||||
// This is performed by the following code that parses the errors returned as json by the
|
||||
// registration form validation.
|
||||
var field_errors = json.field_errors;
|
||||
var non_field_errors = json.non_field_errors;
|
||||
var fieldname;
|
||||
var field_errorlist;
|
||||
var field_error;
|
||||
var error_msg;
|
||||
var num_errors = 0;
|
||||
var error_html = '';
|
||||
// first get rid of any errors that are already present:
|
||||
$(".field.error", ".form-actions").removeClass('error');
|
||||
$("#submission-error-list").html(error_html);
|
||||
// start to display new errors:
|
||||
$(".form-actions").addClass("error");
|
||||
$(".submission-error").addClass("is-shown");
|
||||
for (fieldname in field_errors) {
|
||||
// to inform a user of what field the error occurred on, add a class of .error to the .field element.
|
||||
// for convenience, use the "id" attribute to identify the one matching the errant field's name.
|
||||
var field_id = "field-" + fieldname;
|
||||
var field_label = $("[id='"+field_id+"'] label").text();
|
||||
|
||||
$("[id='"+field_id+"']").addClass('error');
|
||||
|
||||
field_errorlist = field_errors[fieldname];
|
||||
for (i=0; i < field_errorlist.length; i+= 1) {
|
||||
field_error = field_errorlist[i];
|
||||
error_msg = '<span class="field-name">' + field_label + ':</span>' + '<span class="field-error">' + field_error + '</span>';
|
||||
error_html = error_html + '<li>' + '<a href="#field-' + fieldname + '">' + error_msg + '</a></li>';
|
||||
num_errors += 1;
|
||||
}
|
||||
}
|
||||
for (i=0; i < non_field_errors.length; i+= 1) {
|
||||
error_msg = non_field_errors[i];
|
||||
error_html = error_html + '<li class="to-be-determined">' + error_msg + '</li>';
|
||||
num_errors += 1;
|
||||
}
|
||||
if (num_errors == 1) {
|
||||
error_msg = 'was an error';
|
||||
} else {
|
||||
error_msg = 'were ' + num_errors + ' errors';
|
||||
}
|
||||
$('#submission-error-heading').text("We're sorry, but there " + error_msg + " in the information you provided below:")
|
||||
$("#submission-error-list").html(error_html);
|
||||
}
|
||||
});
|
||||
|
||||
$("form :input").focus(function() {
|
||||
$("label[for='" + this.id + "']").addClass("is-focused");
|
||||
}).blur(function() {
|
||||
$("label").removeClass("is-focused");
|
||||
});
|
||||
|
||||
})(this)
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<section class="testcenter-register container">
|
||||
|
||||
<section class="introduction">
|
||||
<header>
|
||||
<hgroup>
|
||||
<h2><a href="${reverse('dashboard')}">${get_course_about_section(course, 'university')} ${course.display_number_with_default | h} ${course.display_name_with_default | h}</a></h2>
|
||||
|
||||
% if registration:
|
||||
<h1>${_('Your Pearson VUE Proctored Exam Registration')}</h1>
|
||||
% else:
|
||||
<h1>${_('Register for a Pearson VUE Proctored Exam')}</h1>
|
||||
% endif
|
||||
</hgroup>
|
||||
</header>
|
||||
</section>
|
||||
|
||||
<%
|
||||
exam_help_href = "mailto:exam-help@edx.org?subject=Pearson VUE Exam - " + get_course_about_section(course, 'university') + " - " + course.number
|
||||
%>
|
||||
|
||||
% if registration:
|
||||
<!-- select one of four registration status banners to display -->
|
||||
% if registration.is_accepted:
|
||||
<section class="status message message-flash registration-processed message-action is-shown">
|
||||
<h3 class="message-title">${_('Your registration for the Pearson exam has been processed')}</h3>
|
||||
<p class="message-copy">${_("Your registration number is <strong>{reg_number}</strong>. "
|
||||
"(Write this down! You\'ll need it to schedule your exam.)").format(reg_number=registration.client_candidate_id)}</p>
|
||||
<a href="${registration.registration_signup_url}" class="button exam-button">${_('Schedule Pearson exam')}</a>
|
||||
</section>
|
||||
% endif
|
||||
|
||||
% if registration.demographics_is_rejected:
|
||||
<section class="status message message-flash demographics-rejected message-action is-shown">
|
||||
<h3 class="message-title">${_('Your demographic information contained an error and was rejected')}</h3>
|
||||
<p class="message-copy">${_('Please check the information you provided, and correct the errors noted below.')}
|
||||
</section>
|
||||
% endif
|
||||
|
||||
% if registration.registration_is_rejected:
|
||||
<section class="status message message-flash registration-rejected message-action is-shown">
|
||||
<h3 class="message-title">${_('Your registration for the Pearson exam has been rejected')}</h3>
|
||||
<p class="message-copy">${_('Please see your <strong>registration status</strong> details for more information.')}</p>
|
||||
</section>
|
||||
% endif
|
||||
|
||||
% if registration.is_pending:
|
||||
<section class="status message message-flash registration-pending is-shown">
|
||||
<h3 class="message-title">${_('Your registration for the Pearson exam is pending')}</h3>
|
||||
<p class="message-copy">${_('Once your information is processed, it will be forwarded to Pearson and you will be able to schedule an exam.')}</p>
|
||||
</section>
|
||||
% endif
|
||||
|
||||
% endif
|
||||
|
||||
<section class="content">
|
||||
<header>
|
||||
<h3 class="is-hidden">${_('Registration Form')}</h3>
|
||||
</header>
|
||||
% if exam_info.is_registering():
|
||||
<form id="testcenter_register_form" method="post" data-remote="true" action="/create_exam_registration">
|
||||
% else:
|
||||
<form id="testcenter_register_form" method="post" data-remote="true" action="/create_exam_registration" class="disabled">
|
||||
<!-- registration closed, so tell them they can't do anything: -->
|
||||
<div class="status message registration-closed is-shown">
|
||||
<h3 class="message-title">${_('Registration for this Pearson exam is closed')}</h3>
|
||||
<p class="message-copy">${_('Your previous information is available below, however you may not edit any of the information.')}
|
||||
</div>
|
||||
% endif
|
||||
|
||||
% if registration:
|
||||
<p class="instructions">
|
||||
${_('Please use the following form if you need to update your demographic information used in your Pearson VUE Proctored Exam. Required fields are noted by <strong class="indicator">bold text and an asterisk (*)')}</strong>.
|
||||
</p>
|
||||
% else:
|
||||
<p class="instructions">
|
||||
${_('Please provide the following demographic information to register for a Pearson VUE Proctored Exam. Required fields are noted by <strong class="indicator">bold text and an asterisk (*)')}</strong>.
|
||||
</p>
|
||||
% endif
|
||||
|
||||
<!-- include these as pass-throughs -->
|
||||
<input id="id_email" type="hidden" name="email" value="${user.email}" />
|
||||
<input id="id_username" type="hidden" name="username" value="${user.username}" />
|
||||
<input id="id_course_id" type="hidden" name="course_id" value="${course.id}" />
|
||||
<input id="id_exam_series_code" type="hidden" name="exam_series_code" value="${exam_info.exam_series_code}" />
|
||||
|
||||
<div class="form-fields-primary">
|
||||
<fieldset class="group group-form group-form-personalinformation">
|
||||
<legend class="is-hidden">${_('Personal Information')}</legend>
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field" id="field-salutation">
|
||||
<label for="salutation">${_('Salutation')}</label>
|
||||
<input class="short" id="salutation" type="text" name="salutation" value="${testcenteruser.salutation}" placeholder="${_('e.g. Mr., Ms., Mrs., Dr.')}" />
|
||||
</li>
|
||||
<li class="field required" id="field-first_name">
|
||||
<label for="first_name">${_('First Name')} </label>
|
||||
<input id="first_name" type="text" name="first_name" value="${testcenteruser.first_name}" placeholder="${_('e.g. Albert')}" />
|
||||
</li>
|
||||
<li class="field" id="field-middle_name">
|
||||
<label for="middle_name">${_("Middle Name")}</label>
|
||||
<input id="middle_name" type="text" name="middle_name" value="${testcenteruser.middle_name}" placeholder="" />
|
||||
</li>
|
||||
<li class="field required" id="field-last_name">
|
||||
<label for="last_name">${_('Last Name')}</label>
|
||||
<input id="last_name" type="text" name="last_name" value="${testcenteruser.last_name}" placeholder="${_('e.g. Einstein')}" />
|
||||
</li>
|
||||
<li class="field" id="field-suffix">
|
||||
<label for="suffix">${_('Suffix')}</label>
|
||||
<input class="short" id="suffix" type="text" name="suffix" value="${testcenteruser.suffix}" placeholder="${_('e.g. Jr., Sr. ')}" />
|
||||
</li>
|
||||
</ol>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="group group-form group-form-mailingaddress">
|
||||
<legend class="is-hidden">${_('Mailing Address')}</legend>
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field required" id="field-address_1">
|
||||
<label class="long" for="address_1">${_('Address Line #1')}</label>
|
||||
<input id="address_1" type="text" name="address_1" value="${testcenteruser.address_1}" placeholder="${_('e.g. 112 Mercer Street')}" />
|
||||
</li>
|
||||
<li class="field-group addresses">
|
||||
<div class="field" id="field-address_2">
|
||||
<label for="address_2">${_('Address Line #2')}</label>
|
||||
<input id="address_2" class="long" type="text" name="address_2" value="${testcenteruser.address_2}" placeholder="${_('e.g. Apartment 123')}" />
|
||||
</div>
|
||||
<div class="field" id="field-address_3">
|
||||
<label for="address_3">${_('Address Line #3')}</label>
|
||||
<input id="address_3" class="long" type="text" name="address_3" value="${testcenteruser.address_3}" placeholder="${_('e.g. Attention: Albert Einstein')}" />
|
||||
</div>
|
||||
</li>
|
||||
<li class="field required postal-1" id="field-city">
|
||||
<label for="city">${_('City')}</label>
|
||||
<input id="city" class="long" type="text" name="city" value="${testcenteruser.city}" placeholder="${_('e.g. Newark')}" />
|
||||
</li>
|
||||
<li class="field-group postal-2">
|
||||
<div class="field" id="field-state">
|
||||
<label for="state">${_('State/Province')}</label>
|
||||
<input id="state" class="short" type="text" name="state" value="${testcenteruser.state}" placeholder="${_('e.g. NJ')}" />
|
||||
</div>
|
||||
<div class="field" id="field-postal_code">
|
||||
<label for="postal_code">${_('Postal Code')}</label>
|
||||
<input id="postal_code" class="short" type="text" name="postal_code" value="${testcenteruser.postal_code}" placeholder="${_('e.g. 08540')}" />
|
||||
</div>
|
||||
<div class="field required" id="field-country">
|
||||
<label class="short" for="country">${_('Country Code')}</label>
|
||||
<input id="country" class="short" type="text" name="country" value="${testcenteruser.country}" placeholder="${_('e.g. USA')}" />
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="group group-form group-form-contactinformation">
|
||||
<legend class="is-hidden">${_('Contact & Other Information')}</legend>
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field-group phoneinfo">
|
||||
<div class="field required" id="field-phone">
|
||||
<label for="phone">${_('Phone Number')}</label>
|
||||
<input id="phone" type="tel" name="phone" value="${testcenteruser.phone}" />
|
||||
</div>
|
||||
<div class="field" id="field-extension">
|
||||
<label for="extension">${_('Extension')}</label>
|
||||
<input id="extension" class="short" type="tel" name="extension" value="${testcenteruser.extension}" />
|
||||
</div>
|
||||
<div class="field required" id="field-phone_country_code">
|
||||
<label for="phone_country_code">${_('Phone Country Code')}</label>
|
||||
<input id="phone_country_code" class="short" type="text" name="phone_country_code" value="${testcenteruser.phone_country_code}" />
|
||||
</div>
|
||||
</li>
|
||||
<li class="field-group faxinfo">
|
||||
<div class="field" id="field-fax">
|
||||
<label for="fax">${_('Fax Number')}</label>
|
||||
<input id="fax" type="tel" class="short" name="fax" value="${testcenteruser.fax}" />
|
||||
</div>
|
||||
<div class="field" id="field-fax_country_code">
|
||||
<label for="fax_country_code">${_('Fax Country Code')}</label>
|
||||
<input id="fax_country_code" class="short" type="text" name="fax_country_code" value="${testcenteruser.fax_country_code}" />
|
||||
</div>
|
||||
</li>
|
||||
<li class="field" id="field-company_name">
|
||||
<label for="company_name">${_('Company')}</label>
|
||||
<input id="company_name" type="text" name="company_name" value="${testcenteruser.company_name}" placeholder="${_('e.g. American Association of University Professors')}" />
|
||||
</li>
|
||||
</ol>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
% if registration:
|
||||
% if registration.accommodation_request and len(registration.accommodation_request) > 0:
|
||||
<div class="form-fields-secondary is-shown disabled" id="form-fields-secondary">
|
||||
% endif
|
||||
% else:
|
||||
<div class="form-fields-secondary" id="form-fields-secondary">
|
||||
% endif
|
||||
|
||||
% if registration:
|
||||
% if registration.accommodation_request and len(registration.accommodation_request) > 0:
|
||||
<p class="note">${_('<span class="title">Note</span>: Your previous accommodation request below needs to be reviewed in detail <strong>and will add a significant delay to your registration process</strong>.')}</p>
|
||||
% endif
|
||||
% else:
|
||||
<p class="note">${_('<span class="title">Note</span>: Accommodation requests are not part of your demographic information, <strong>and cannot be changed once submitted</strong>. Accommodation requests, which are reviewed on a case-by-case basis, <strong>will add significant delay to the registration process</strong>.')}</p>
|
||||
% endif
|
||||
|
||||
<fieldset class="group group-form group-form-optional">
|
||||
<legend class="is-hidden">${_('Optional Information')}</legend>
|
||||
|
||||
<ol class="list-input">
|
||||
% if registration:
|
||||
% if registration.accommodation_request and len(registration.accommodation_request) > 0:
|
||||
<li class="field submitted" id="field-accommodation_request">
|
||||
<label for="accommodation_request">${_('Accommodations Requested')}</label>
|
||||
<p class="value" id="accommodations">${registration.accommodation_request}</p>
|
||||
</li>
|
||||
% endif
|
||||
% else:
|
||||
<li class="field" id="field-accommodation_request">
|
||||
<label for="accommodation_request">${_('Accommodations Requested')}</label>
|
||||
<textarea class="long" id="accommodation_request" name="accommodation_request" value="" placeholder=""></textarea>
|
||||
</li>
|
||||
% endif
|
||||
</ol>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
% if registration:
|
||||
<button type="submit" id="submit" class="action action-primary action-update">${_('Update Demographics')}</button>
|
||||
<a href="${reverse('dashboard')}" class="action action-secondary action-cancel">${_('Cancel Update')}</a>
|
||||
% else:
|
||||
<button type="submit" id="submit" class="action action-primary action-register">${_('Register for Pearson VUE Test')}</button>
|
||||
<a href="${reverse('dashboard')}" class="action action-secondary action-cancel">${_('Cancel Registration')}</a>
|
||||
% endif
|
||||
|
||||
<div class="message message-status submission-error">
|
||||
<p id="submission-error-heading" class="message-copy"></p>
|
||||
<ul id="submission-error-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
% if registration:
|
||||
% if registration.accommodation_request and len(registration.accommodation_request) > 0:
|
||||
<a class="actions form-fields-secondary-visibility is-hidden" href="#form-fields-secondary">${_('Special (<abbr title="Americans with Disabilities Act">ADA</abbr>) Accommodations')}</a>
|
||||
% endif
|
||||
% else:
|
||||
<a class="actions form-fields-secondary-visibility" href="#form-fields-secondary">${_('Special (<abbr title="Americans with Disabilities Act">ADA</abbr>) Accommodations')}</a>
|
||||
% endif
|
||||
</section>
|
||||
|
||||
<aside>
|
||||
% if registration:
|
||||
|
||||
% if registration.is_accepted:
|
||||
<div class="message message-status registration-processed is-shown">
|
||||
% endif
|
||||
% if registration.is_rejected:
|
||||
<div class="message message-status registration-rejected is-shown">
|
||||
% endif
|
||||
% if registration.is_pending:
|
||||
<div class="message message-status registration-pending is-shown">
|
||||
% endif
|
||||
<h3>${_('Pearson Exam Registration Status')}</h3>
|
||||
|
||||
<ol class="status-list">
|
||||
<!-- first provide status of demographics -->
|
||||
% if registration.demographics_is_pending:
|
||||
<li class="item status status-pending status-demographics">
|
||||
<h4 class="title">${_('Demographic Information')}</h4>
|
||||
<p class="details">${_('The demographic information you most recently provided is pending. You may edit this information at any point before exam registration closes on <strong>{end_date}</strong>').format(end_date=exam_info.registration_end_date_text)}</p>
|
||||
</li>
|
||||
% endif
|
||||
% if registration.demographics_is_accepted:
|
||||
<li class="item status status-processed status-demographics">
|
||||
<h4 class="title">${_('Demographic Information')}</h4>
|
||||
<p class="details">${_('The demographic information you most recently provided has been processed. You may edit this information at any point before exam registration closes on <strong>{end_date}</strong>').format(end_date=exam_info.registration_end_date_text)}</p>
|
||||
</li>
|
||||
% endif
|
||||
% if registration.demographics_is_rejected:
|
||||
<li class="item status status-rejected status-demographics">
|
||||
<h4 class="title">${_('Demographic Information')}</h4>
|
||||
<p class="details">${_('The demographic information you most recently provided has been rejected by Pearson. You can correct and submit it again before the exam registration closes on <strong>{end_date}</strong>. The error message is:').format(end_date=exam_info.registration_end_date_text)}</p>
|
||||
|
||||
<ul class="error-list">
|
||||
<li class="item">${registration.testcenter_user.upload_error_message}.</li>
|
||||
</ul>
|
||||
|
||||
<p class="action">${_('If the error is not correctable by revising your demographic information, please {contact_link_start}contact edX at exam-help@edx.org{contact_link_end}.').format(contact_link_start='<a class="contact-link" href="{}"'.format(exam_help_href), contact_link_end='</a>')}</p>
|
||||
</li>
|
||||
% endif
|
||||
|
||||
<!-- then provide status of accommodations, if any -->
|
||||
% if registration.accommodation_is_pending:
|
||||
<li class="item status status-pending status-accommodations">
|
||||
<h4 class="title">${_('Accommodations Request')}</h4>
|
||||
<p class="details">${_('Your requested accommodations are pending. Within a few days, you should see confirmation here of granted accommodations.')}</p>
|
||||
</li>
|
||||
% endif
|
||||
% if registration.accommodation_is_accepted:
|
||||
<li class="item status status-processed status-accommodations">
|
||||
<h4 class="title">${_('Accommodations Request')}</h4>
|
||||
<p class="details">${_('Your requested accommodations have been reviewed and processed. You are allowed:')}</p>
|
||||
|
||||
<ul class="accommodations-list">
|
||||
% for accommodation_name in registration.get_accommodation_names():
|
||||
<li class="item">${accommodation_name}</li>
|
||||
% endfor
|
||||
</ul>
|
||||
</li>
|
||||
% endif
|
||||
% if registration.accommodation_is_rejected:
|
||||
<li class="item status status-processed status-accommodations">
|
||||
<h4 class="title">${_('Accommodations Request')}</h4>
|
||||
<p class="details">${_('Your requested accommodations have been reviewed and processed. You are allowed no accommodations.')}</p>
|
||||
|
||||
<p class="action">${_('Please {contact_link_start}contact {edX} at ${exam_help}{contact_link_end}.').format(contact_link_start='<a class="contact-link" href="{}">'.format(exam_help_href), contact_link_end='</a>', edX="edX", exam_help="exam-help@edx.org")}</p>
|
||||
</li>
|
||||
% endif
|
||||
|
||||
<!-- finally provide status of registration -->
|
||||
% if registration.registration_is_pending:
|
||||
<li class="item status status-pending status-registration">
|
||||
<h4 class="title">${_('Registration Request')}</h4>
|
||||
<p class="details">${_('Your exam registration is pending. Once your information is processed, it will be forwarded to Pearson and you will be able to schedule an exam.')}</p>
|
||||
</li>
|
||||
% endif
|
||||
% if registration.registration_is_accepted:
|
||||
<li class="item status status-processed status-registration">
|
||||
<h4 class="title">${_('Registration Request')}</h4>
|
||||
<p class="details">${_('Your exam registration has been processed and has been forwarded to Pearson. <strong>You are now able to {exam_link_start}schedule a Pearson exam{exam_link_end}</strong>.').format(exam_link_start='<a href="{}" class="exam-link">'.format(registration.registration_signup_url), exam_link_end='</a>')}</p>
|
||||
</li>
|
||||
% endif
|
||||
% if registration.registration_is_rejected:
|
||||
<li class="item status status-rejected status-registration">
|
||||
<h4 class="title">${_('Registration Request')}</h4>
|
||||
<p class="details">${_('Your exam registration has been rejected by Pearson. <strong>You currently cannot schedule an exam</strong>. The errors found include:')}</p>
|
||||
|
||||
<ul class="error-list">
|
||||
<li class="item">${registration.upload_error_message}</li>
|
||||
</ul>
|
||||
|
||||
<p class="action">${_('Please {contact_link_start}contact edX at exam-help@edx.org{contact_link_end}.').format(contact_link_start='<a class="contact-link" href="{}"'.format(exam_help_href), contact_link_end='</a>')}</p>
|
||||
</li>
|
||||
% endif
|
||||
</ol>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
% endif
|
||||
|
||||
<div class="details details-course">
|
||||
<h4>${_("About {university} {course_number}").format(university=get_course_about_section(course, 'university'), course_number=course.course.display_number_with_default) | h}</h4>
|
||||
<p>
|
||||
% if course.has_ended():
|
||||
<span class="label">${_('Course Completed:')}</span> <span class="value">${course.end_date_text}</span>
|
||||
% elif course.has_started():
|
||||
<span class="label">${_('Course Started:')}</span> <span class="value">${course.start_date_text}</span>
|
||||
% else: # hasn't started yet
|
||||
<span class="label">${_('Course Starts:')}</span> <span class="value">${course.start_date_text}</span>
|
||||
% endif
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="details details-registration">
|
||||
<h4>${_('Pearson VUE Test Details')}</h4>
|
||||
% if exam_info is not None:
|
||||
<ul>
|
||||
<li>
|
||||
<span class="label">${_('Exam Name:')}</span> <span class="value">${exam_info.display_name_with_default}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label">${_('First Eligible Appointment Date:')}</span> <span class="value">${exam_info.first_eligible_appointment_date_text}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label">${_('Last Eligible Appointment Date:')}</span> <span class="value">${exam_info.last_eligible_appointment_date_text}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label">${_('Registration Ends:')}</span> <span class="value">${exam_info.registration_end_date_text}</span>
|
||||
</li>
|
||||
</ul>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
<div class="details details-contact">
|
||||
<h4>${_('Questions')}</h4>
|
||||
<p>${_('If you have a specific question pertaining to your registration, you may {contact_link_start}contact edX at exam-help@edx.org{contact_link_end}.').format(contact_link_start='<a class="contact-link" href="{}"'.format(exam_help_href), contact_link_end='</a>')}</p>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
@@ -41,9 +41,6 @@ urlpatterns = ('', # nopep8
|
||||
url(r'^create_account$', 'student.views.create_account', name='create_account'),
|
||||
url(r'^activate/(?P<key>[^/]*)$', 'student.views.activate_account', name="activate"),
|
||||
|
||||
url(r'^begin_exam_registration/(?P<course_id>[^/]+/[^/]+/[^/]+)$', 'student.views.begin_exam_registration', name="begin_exam_registration"),
|
||||
url(r'^create_exam_registration$', 'student.views.create_exam_registration'),
|
||||
|
||||
url(r'^password_reset/$', 'student.views.password_reset', name='password_reset'),
|
||||
## Obsolete Django views for password resets
|
||||
## TODO: Replace with Mako-ized views
|
||||
@@ -402,9 +399,6 @@ if settings.MITX_FEATURES.get('AUTH_USE_OPENID_PROVIDER'):
|
||||
url(r'^openid/provider/xrds/$', 'external_auth.views.provider_xrds', name='openid-provider-xrds')
|
||||
)
|
||||
|
||||
if settings.MITX_FEATURES.get('ENABLE_PEARSON_LOGIN', False):
|
||||
urlpatterns += url(r'^testcenter/login$', 'external_auth.views.test_center_login'),
|
||||
|
||||
if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
|
||||
urlpatterns += (
|
||||
url(r'^migrate/modules$', 'lms_migration.migrate.manage_modulestores'),
|
||||
|
||||
Reference in New Issue
Block a user