Commit 341875bb by Jay Zoldak

Remove code related to Pearson Testing Centers

parent f94565d6
...@@ -441,7 +441,6 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -441,7 +441,6 @@ class CourseMetadataEditingTest(CourseTestCase):
def test_update_from_json(self): def test_update_from_json(self):
test_model = CourseMetadata.update_from_json(self.course, { test_model = CourseMetadata.update_from_json(self.course, {
"advertised_start": "start A", "advertised_start": "start A",
"testcenter_info": {"c": "test"},
"days_early_for_beta": 2 "days_early_for_beta": 2
}) })
self.update_check(test_model) self.update_check(test_model)
...@@ -464,8 +463,6 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -464,8 +463,6 @@ class CourseMetadataEditingTest(CourseTestCase):
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value") 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.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.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.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") 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 ...@@ -21,7 +21,7 @@ from django.core.exceptions import ValidationError
if settings.MITX_FEATURES.get('AUTH_USE_CAS'): if settings.MITX_FEATURES.get('AUTH_USE_CAS'):
from django_cas.views import login as django_cas_login 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.http import HttpResponse, HttpResponseRedirect, HttpRequest, HttpResponseForbidden
from django.utils.http import urlquote, is_safe_url from django.utils.http import urlquote, is_safe_url
...@@ -880,146 +880,7 @@ def provider_xrds(request): ...@@ -880,146 +880,7 @@ def provider_xrds(request):
return response return response
#-------------------
# Pearson
#-------------------
def course_from_id(course_id): def course_from_id(course_id):
"""Return the CourseDescriptor corresponding to this course_id""" """Return the CourseDescriptor corresponding to this course_id"""
course_loc = CourseDescriptor.id_to_location(course_id) course_loc = CourseDescriptor.id_to_location(course_id)
return modulestore().get_instance(course_id, course_loc) 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)
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)
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()
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()
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)
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."
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."
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()
...@@ -39,10 +39,9 @@ from mitxmako.shortcuts import render_to_response, render_to_string ...@@ -39,10 +39,9 @@ from mitxmako.shortcuts import render_to_response, render_to_string
from course_modes.models import CourseMode from course_modes.models import CourseMode
from student.models import ( from student.models import (
Registration, UserProfile, TestCenterUser, TestCenterUserForm, Registration, UserProfile, PendingNameChange,
TestCenterRegistration, TestCenterRegistrationForm, PendingNameChange,
PendingEmailChange, CourseEnrollment, unique_id_for_user, PendingEmailChange, CourseEnrollment, unique_id_for_user,
get_testcenter_registration, CourseEnrollmentAllowed, UserStanding, CourseEnrollmentAllowed, UserStanding,
) )
from student.forms import PasswordResetFormNoActive from student.forms import PasswordResetFormNoActive
...@@ -966,172 +965,6 @@ def create_account(request, post_override=None): ...@@ -966,172 +965,6 @@ def create_account(request, post_override=None):
return response 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): def auto_auth(request):
""" """
Automatically logs the user in with a generated random credentials Automatically logs the user in with a generated random credentials
......
...@@ -20,7 +20,6 @@ XMODULES = [ ...@@ -20,7 +20,6 @@ XMODULES = [
"section = xmodule.backcompat_module:SemanticSectionDescriptor", "section = xmodule.backcompat_module:SemanticSectionDescriptor",
"sequential = xmodule.seq_module:SequenceDescriptor", "sequential = xmodule.seq_module:SequenceDescriptor",
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor", "slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"timelimit = xmodule.timelimit_module:TimeLimitDescriptor",
"vertical = xmodule.vertical_module:VerticalDescriptor", "vertical = xmodule.vertical_module:VerticalDescriptor",
"video = xmodule.video_module:VideoDescriptor", "video = xmodule.video_module:VideoDescriptor",
"videoalpha = xmodule.video_module:VideoDescriptor", "videoalpha = xmodule.video_module:VideoDescriptor",
......
...@@ -213,7 +213,6 @@ class CourseFields(object): ...@@ -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_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_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.") 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) announcement = Date(help="Date this course is announced", scope=Scope.settings)
cohort_config = Dict(help="Dictionary defining cohort configuration", 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) is_new = Boolean(help="Whether this course should be flagged as new", scope=Scope.settings)
...@@ -426,20 +425,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -426,20 +425,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
if self.discussion_topics == {}: if self.discussion_topics == {}:
self.discussion_topics = {'General': {'id': self.location.html_id()}} 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. # TODO check that this is still needed here and can't be by defaults.
if not self.tabs: if not self.tabs:
# When calling the various _tab methods, can omit the 'type':'blah' from the # When calling the various _tab methods, can omit the 'type':'blah' from the
...@@ -876,93 +861,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -876,93 +861,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
return True 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 @property
def number(self): def number(self):
return self.location.course return self.location.course
......
...@@ -79,7 +79,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -79,7 +79,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
# tags that really need unique names--they store (or should store) state. # tags that really need unique names--they store (or should store) state.
need_uniq_names = ('problem', 'sequential', 'video', 'course', 'chapter', need_uniq_names = ('problem', 'sequential', 'video', 'course', 'chapter',
'videosequence', 'poll_question', 'timelimit', 'vertical') 'videosequence', 'poll_question', 'vertical')
attr = xml_data.attrib attr = xml_data.attrib
tag = xml_data.tag tag = xml_data.tag
......
import core import core
import xmodule_asserts
\ No newline at end of file
"""
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)
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 ...@@ -3,7 +3,6 @@ import copy
import logging import logging
import os import os
import sys import sys
from collections import namedtuple
from lxml import etree from lxml import etree
from xblock.fields import Dict, Scope, ScopeIds from xblock.fields import Dict, Scope, ScopeIds
...@@ -133,15 +132,12 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -133,15 +132,12 @@ class XmlDescriptor(XModuleDescriptor):
'ispublic', # if True, then course is listed for all users; see 'ispublic', # if True, then course is listed for all users; see
'xqa_key', # for xqaa server access 'xqa_key', # for xqaa server access
'giturl', # url of git server for origin of file '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. # VS[compat] Remove once unused.
'name', 'slug') 'name', 'slug')
metadata_to_strip = ('data_dir', metadata_to_strip = ('data_dir',
'tabs', 'grading_policy', 'published_by', 'published_date', '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 # VS[compat] -- remove the below attrs once everything is in the CMS
'course', 'org', 'url_name', 'filename', 'course', 'org', 'url_name', 'filename',
# Used for storing xml attributes between import and export, for roundtrips # Used for storing xml attributes between import and export, for roundtrips
......
...@@ -3,21 +3,6 @@ ...@@ -3,21 +3,6 @@
"graceperiod": "2 days 5 hours 59 minutes 59 seconds", "graceperiod": "2 days 5 hours 59 minutes 59 seconds",
"start": "2011-07-17T12:00", "start": "2011-07-17T12:00",
"display_name": "Toy Course", "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": { "chapter/Overview": {
"display_name": "Overview" "display_name": "Overview"
......
...@@ -19,7 +19,7 @@ All of our tables will be described below, first in summary form with field type ...@@ -19,7 +19,7 @@ All of our tables will be described below, first in summary form with field type
.. list-table:: .. list-table::
:widths: 10 80 :widths: 10 80
:header-rows: 1 :header-rows: 1
* - Value * - Value
- Meaning - Meaning
* - `int` * - `int`
...@@ -36,13 +36,13 @@ All of our tables will be described below, first in summary form with field type ...@@ -36,13 +36,13 @@ All of our tables will be described below, first in summary form with field type
- Date - Date
* - `datetime` * - `datetime`
- Datetime in UTC, precision in seconds. - Datetime in UTC, precision in seconds.
`Null` `Null`
.. list-table:: .. list-table::
:widths: 10 80 :widths: 10 80
:header-rows: 1 :header-rows: 1
* - Value * - Value
- Meaning - Meaning
* - `YES` * - `YES`
...@@ -57,7 +57,7 @@ All of our tables will be described below, first in summary form with field type ...@@ -57,7 +57,7 @@ All of our tables will be described below, first in summary form with field type
.. list-table:: .. list-table::
:widths: 10 80 :widths: 10 80
:header-rows: 1 :header-rows: 1
* - Value * - Value
- Meaning - Meaning
* - `PRI` * - `PRI`
...@@ -252,19 +252,19 @@ There is an important split in demographic data gathered for the students who si ...@@ -252,19 +252,19 @@ There is an important split in demographic data gathered for the students who si
`old_names` `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. 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. 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. The timestamps are UTC, like all datetimes stored in our system.
`old_emails` `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. 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. The timestamps are UTC, like all datetimes stored in our system.
`6002x_exit_response` `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. 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` `courseware`
------------ ------------
...@@ -277,7 +277,7 @@ There is an important split in demographic data gathered for the students who si ...@@ -277,7 +277,7 @@ There is an important split in demographic data gathered for the students who si
.. list-table:: .. list-table::
:widths: 10 80 :widths: 10 80
:header-rows: 1 :header-rows: 1
* - Value * - Value
- Meaning - Meaning
* - `NULL` * - `NULL`
...@@ -306,10 +306,10 @@ There is an important split in demographic data gathered for the students who si ...@@ -306,10 +306,10 @@ There is an important split in demographic data gathered for the students who si
.. list-table:: .. list-table::
:widths: 10 80 :widths: 10 80
:header-rows: 1 :header-rows: 1
* - Value * - Value
- Meaning - Meaning
* - `NULL` * - `NULL`
- This student signed up before this information was collected - This student signed up before this information was collected
* - `''` (blank) * - `''` (blank)
- User did not specify level of education. - 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 ...@@ -335,7 +335,7 @@ There is an important split in demographic data gathered for the students who si
- None - None
* - `'other'` * - `'other'`
- Other - Other
`goals` `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. 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 ...@@ -382,7 +382,7 @@ Any piece of content in the courseware can store state and score in the `coursew
.. warning:: .. warning::
**Modules might not be what you expect!** **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. 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. 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 ...@@ -420,7 +420,7 @@ The `courseware_studentmodule` table holds all courseware state for a given user
.. list-table:: .. list-table::
:widths: 10 80 :widths: 10 80
:header-rows: 0 :header-rows: 0
* - `chapter` * - `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. - 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` * - `combinedopenended`
...@@ -437,8 +437,6 @@ The `courseware_studentmodule` table holds all courseware state for a given user ...@@ -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`. - 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` * - `sequential`
- A collection of videos, problems, and other materials, rendered as a horizontal icon bar in the courseware. - 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` * - `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. - 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 ...@@ -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` .. list-table:: Breakdown of example `module_id`: `i4x://MITx/3.091x/problemset/Sample_Problems`
:widths: 10 20 70 :widths: 10 20 70
:header-rows: 1 :header-rows: 1
* - Part * - Part
- Example - Example
- Definition - Definition
* - `i4x://` * - `i4x://`
- -
- Just a convention we ran with. We had plans for the domain `i4x.org` at one point. - Just a convention we ran with. We had plans for the domain `i4x.org` at one point.
* - `org` * - `org`
- `MITx` - `MITx`
- The organization part of the ID, indicating what organization created this piece of content. - The organization part of the ID, indicating what organization created this piece of content.
* - `course_num` * - `course_num`
- `3.091x` - `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. - 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` - `problemset`
- The module type, same value as what's in the `courseware_studentmodule.module_type` field. - The module type, same value as what's in the `courseware_studentmodule.module_type` field.
* - `module_name` * - `module_name`
...@@ -501,33 +499,6 @@ The `courseware_studentmodule` table holds all courseware state for a given user ...@@ -501,33 +499,6 @@ The `courseware_studentmodule` table holds all courseware state for a given user
`selfassessment` `selfassessment`
TODO: More details to come. 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` `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. 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 ...@@ -608,13 +579,13 @@ The generatedcertificate table tracks certificate state for students who have be
* `notpassing` * `notpassing`
* `restricted` * `restricted`
* `error` * `error`
After a course has been graded and certificates have been issued status will be one of: After a course has been graded and certificates have been issued status will be one of:
* `downloadable` * `downloadable`
* `notpassing` * `notpassing`
* `restricted` * `restricted`
If the status is `downloadable` then the student passed the course and there will be a certificate available for download. If the status is `downloadable` then the student passed the course and there will be a certificate available for download.
`download_url` `download_url`
......
...@@ -27,7 +27,6 @@ class StudentModule(models.Model): ...@@ -27,7 +27,6 @@ class StudentModule(models.Model):
MODULE_TYPES = (('problem', 'problem'), MODULE_TYPES = (('problem', 'problem'),
('video', 'video'), ('video', 'video'),
('html', 'html'), ('html', 'html'),
('timelimit', 'timelimit'),
) )
## These three are the key for the object ## These three are the key for the object
module_type = models.CharField(max_length=32, choices=MODULE_TYPES, default='problem', db_index=True) module_type = models.CharField(max_length=32, choices=MODULE_TYPES, default='problem', db_index=True)
......
"""
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): ...@@ -172,71 +172,6 @@ def save_child_position(seq_module, child_name):
seq_module.save() 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): def chat_settings(course, user):
""" """
Returns a dict containing the settings required to connect to a Returns a dict containing the settings required to connect to a
...@@ -390,22 +325,8 @@ def index(request, course_id, chapter=None, section=None, ...@@ -390,22 +325,8 @@ def index(request, course_id, chapter=None, section=None,
# Save where we are in the chapter # Save where we are in the chapter
save_child_position(chapter_module, section) 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') context['fragment'] = section_module.render('student_view')
else: else:
# section is none, so display a message # section is none, so display a message
prev_section = get_current_child(chapter_module) prev_section = get_current_child(chapter_module)
......
...@@ -290,12 +290,6 @@ OPEN_ENDED_GRADING_INTERFACE = AUTH_TOKENS.get('OPEN_ENDED_GRADING_INTERFACE', ...@@ -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_USER = AUTH_TOKENS.get('EMAIL_HOST_USER', '') # django default is ''
EMAIL_HOST_PASSWORD = AUTH_TOKENS.get('EMAIL_HOST_PASSWORD', '') # 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 for events!
DATADOG = AUTH_TOKENS.get("DATADOG", {}) DATADOG = AUTH_TOKENS.get("DATADOG", {})
DATADOG.update(ENV_TOKENS.get("DATADOG", {})) DATADOG.update(ENV_TOKENS.get("DATADOG", {}))
......
...@@ -523,11 +523,6 @@ WIKI_USE_BOOTSTRAP_SELECT_WIDGET = False ...@@ -523,11 +523,6 @@ WIKI_USE_BOOTSTRAP_SELECT_WIDGET = False
WIKI_LINK_LIVE_LOOKUPS = False WIKI_LINK_LIVE_LOOKUPS = False
WIKI_LINK_DEFAULT_LEVEL = 2 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 mechanism #####
FEEDBACK_SUBMISSION_EMAIL = None FEEDBACK_SUBMISSION_EMAIL = None
......
...@@ -254,9 +254,6 @@ MITX_FEATURES['RESTRICT_ENROLL_BY_REG_METHOD'] = True ...@@ -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) 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 TESTING ########################
ANALYTICS_SERVER_URL = "http://127.0.0.1:9000/" ANALYTICS_SERVER_URL = "http://127.0.0.1:9000/"
......
...@@ -41,9 +41,6 @@ urlpatterns = ('', # nopep8 ...@@ -41,9 +41,6 @@ urlpatterns = ('', # nopep8
url(r'^create_account$', 'student.views.create_account', name='create_account'), url(r'^create_account$', 'student.views.create_account', name='create_account'),
url(r'^activate/(?P<key>[^/]*)$', 'student.views.activate_account', name="activate"), 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'), url(r'^password_reset/$', 'student.views.password_reset', name='password_reset'),
## Obsolete Django views for password resets ## Obsolete Django views for password resets
## TODO: Replace with Mako-ized views ## TODO: Replace with Mako-ized views
...@@ -402,9 +399,6 @@ if settings.MITX_FEATURES.get('AUTH_USE_OPENID_PROVIDER'): ...@@ -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') 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'): if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
urlpatterns += ( urlpatterns += (
url(r'^migrate/modules$', 'lms_migration.migrate.manage_modulestores'), url(r'^migrate/modules$', 'lms_migration.migrate.manage_modulestores'),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment