Merge pull request #1246 from MITx/feature/brian/pearson-reg
Feature/brian/pearson reg
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
import csv
|
||||
import uuid
|
||||
from collections import defaultdict, OrderedDict
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from os.path import isdir
|
||||
from optparse import make_option
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from student.models import TestCenterUser
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
CSV_TO_MODEL_FIELDS = OrderedDict([
|
||||
# Skipping optional field CandidateID
|
||||
("ClientCandidateID", "client_candidate_id"),
|
||||
("FirstName", "first_name"),
|
||||
("LastName", "last_name"),
|
||||
@@ -34,9 +37,17 @@ class Command(BaseCommand):
|
||||
("LastUpdate", "user_updated_at"), # in UTC, so same as what we store
|
||||
])
|
||||
|
||||
args = '<output_file>'
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option(
|
||||
'--dump_all',
|
||||
action='store_true',
|
||||
dest='dump_all',
|
||||
),
|
||||
)
|
||||
|
||||
args = '<output_file_or_dir>'
|
||||
help = """
|
||||
Export user information from TestCenterUser model into a tab delimited
|
||||
Export user demographic information from TestCenterUser model into a tab delimited
|
||||
text file with a format that Pearson expects.
|
||||
"""
|
||||
def handle(self, *args, **kwargs):
|
||||
@@ -44,9 +55,33 @@ class Command(BaseCommand):
|
||||
print Command.help
|
||||
return
|
||||
|
||||
self.reset_sample_data()
|
||||
# update time should use UTC in order to be comparable to the user_updated_at
|
||||
# field
|
||||
uploaded_at = datetime.utcnow()
|
||||
|
||||
with open(args[0], "wb") as outfile:
|
||||
# if specified destination is an existing directory, then
|
||||
# create a filename for it automatically. If it doesn't exist,
|
||||
# or exists as a file, then we will just write to it.
|
||||
# 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.
|
||||
dest = args[0]
|
||||
if isdir(dest):
|
||||
destfile = os.path.join(dest, uploaded_at.strftime("cdd-%Y%m%d-%H%M%S.dat"))
|
||||
else:
|
||||
destfile = dest
|
||||
|
||||
# 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 = kwargs['dump_all']
|
||||
|
||||
with open(destfile, "wb") as outfile:
|
||||
writer = csv.DictWriter(outfile,
|
||||
Command.CSV_TO_MODEL_FIELDS,
|
||||
delimiter="\t",
|
||||
@@ -54,103 +89,14 @@ class Command(BaseCommand):
|
||||
extrasaction='ignore')
|
||||
writer.writeheader()
|
||||
for tcu in TestCenterUser.objects.order_by('id'):
|
||||
record = dict((csv_field, getattr(tcu, model_field))
|
||||
for csv_field, model_field
|
||||
in Command.CSV_TO_MODEL_FIELDS.items())
|
||||
record["LastUpdate"] = record["LastUpdate"].strftime("%Y/%m/%d %H:%M:%S")
|
||||
writer.writerow(record)
|
||||
if dump_all or tcu.needs_uploading:
|
||||
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()
|
||||
|
||||
def reset_sample_data(self):
|
||||
def make_sample(**kwargs):
|
||||
data = dict((model_field, kwargs.get(model_field, ""))
|
||||
for model_field in Command.CSV_TO_MODEL_FIELDS.values())
|
||||
return TestCenterUser(**data)
|
||||
|
||||
def generate_id():
|
||||
return "edX{:012}".format(uuid.uuid4().int % (10**12))
|
||||
|
||||
# TestCenterUser.objects.all().delete()
|
||||
|
||||
samples = [
|
||||
make_sample(
|
||||
client_candidate_id=generate_id(),
|
||||
first_name="Jack",
|
||||
last_name="Doe",
|
||||
middle_name="C",
|
||||
address_1="11 Cambridge Center",
|
||||
address_2="Suite 101",
|
||||
city="Cambridge",
|
||||
state="MA",
|
||||
postal_code="02140",
|
||||
country="USA",
|
||||
phone="(617)555-5555",
|
||||
phone_country_code="1",
|
||||
user_updated_at=datetime.utcnow()
|
||||
),
|
||||
make_sample(
|
||||
client_candidate_id=generate_id(),
|
||||
first_name="Clyde",
|
||||
last_name="Smith",
|
||||
middle_name="J",
|
||||
suffix="Jr.",
|
||||
salutation="Mr.",
|
||||
address_1="1 Penny Lane",
|
||||
city="Honolulu",
|
||||
state="HI",
|
||||
postal_code="96792",
|
||||
country="USA",
|
||||
phone="555-555-5555",
|
||||
phone_country_code="1",
|
||||
user_updated_at=datetime.utcnow()
|
||||
),
|
||||
make_sample(
|
||||
client_candidate_id=generate_id(),
|
||||
first_name="Patty",
|
||||
last_name="Lee",
|
||||
salutation="Dr.",
|
||||
address_1="P.O. Box 555",
|
||||
city="Honolulu",
|
||||
state="HI",
|
||||
postal_code="96792",
|
||||
country="USA",
|
||||
phone="808-555-5555",
|
||||
phone_country_code="1",
|
||||
user_updated_at=datetime.utcnow()
|
||||
),
|
||||
make_sample(
|
||||
client_candidate_id=generate_id(),
|
||||
first_name="Jimmy",
|
||||
last_name="James",
|
||||
address_1="2020 Palmer Blvd.",
|
||||
city="Springfield",
|
||||
state="MA",
|
||||
postal_code="96792",
|
||||
country="USA",
|
||||
phone="917-555-5555",
|
||||
phone_country_code="1",
|
||||
extension="2039",
|
||||
fax="917-555-5556",
|
||||
fax_country_code="1",
|
||||
company_name="ACME Traps",
|
||||
user_updated_at=datetime.utcnow()
|
||||
),
|
||||
make_sample(
|
||||
client_candidate_id=generate_id(),
|
||||
first_name="Yeong-Un",
|
||||
last_name="Seo",
|
||||
address_1="Duryu, Lotte 101",
|
||||
address_2="Apt 55",
|
||||
city="Daegu",
|
||||
country="KOR",
|
||||
phone="917-555-5555",
|
||||
phone_country_code="011",
|
||||
user_updated_at=datetime.utcnow()
|
||||
),
|
||||
|
||||
]
|
||||
|
||||
for tcu in samples:
|
||||
tcu.save()
|
||||
|
||||
|
||||
|
||||
@@ -1,150 +1,93 @@
|
||||
import csv
|
||||
import uuid
|
||||
from collections import defaultdict, OrderedDict
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from os.path import isdir, join
|
||||
from optparse import make_option
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from student.models import TestCenterUser
|
||||
|
||||
def generate_id():
|
||||
return "{:012}".format(uuid.uuid4().int % (10**12))
|
||||
from student.models import TestCenterRegistration
|
||||
|
||||
class Command(BaseCommand):
|
||||
args = '<output_file>'
|
||||
|
||||
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
|
||||
])
|
||||
|
||||
args = '<output_file_or_dir>'
|
||||
help = """
|
||||
Export user information from TestCenterUser model into a tab delimited
|
||||
Export user registration information from TestCenterRegistration model into a tab delimited
|
||||
text file with a format that Pearson expects.
|
||||
"""
|
||||
FIELDS = [
|
||||
'AuthorizationTransactionType',
|
||||
'AuthorizationID',
|
||||
'ClientAuthorizationID',
|
||||
'ClientCandidateID',
|
||||
'ExamAuthorizationCount',
|
||||
'ExamSeriesCode',
|
||||
'EligibilityApptDateFirst',
|
||||
'EligibilityApptDateLast',
|
||||
'LastUpdate',
|
||||
]
|
||||
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option(
|
||||
'--dump_all',
|
||||
action='store_true',
|
||||
dest='dump_all',
|
||||
),
|
||||
make_option(
|
||||
'--force_add',
|
||||
action='store_true',
|
||||
dest='force_add',
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
if len(args) < 1:
|
||||
print Command.help
|
||||
return
|
||||
|
||||
# self.reset_sample_data()
|
||||
# update time should use UTC in order to be comparable to the user_updated_at
|
||||
# field
|
||||
uploaded_at = datetime.utcnow()
|
||||
|
||||
with open(args[0], "wb") as outfile:
|
||||
# if specified destination is an existing directory, then
|
||||
# create a filename for it automatically. If it doesn't exist,
|
||||
# or exists as a file, then we will just write to it.
|
||||
# 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.
|
||||
dest = args[0]
|
||||
if isdir(dest):
|
||||
destfile = join(dest, uploaded_at.strftime("ead-%Y%m%d-%H%M%S.dat"))
|
||||
else:
|
||||
destfile = dest
|
||||
|
||||
dump_all = kwargs['dump_all']
|
||||
|
||||
with open(destfile, "wb") as outfile:
|
||||
writer = csv.DictWriter(outfile,
|
||||
Command.FIELDS,
|
||||
Command.CSV_TO_MODEL_FIELDS,
|
||||
delimiter="\t",
|
||||
quoting=csv.QUOTE_MINIMAL,
|
||||
extrasaction='ignore')
|
||||
writer.writeheader()
|
||||
for tcu in TestCenterUser.objects.order_by('id')[:5]:
|
||||
record = defaultdict(
|
||||
lambda: "",
|
||||
AuthorizationTransactionType="Add",
|
||||
ClientAuthorizationID=generate_id(),
|
||||
ClientCandidateID=tcu.client_candidate_id,
|
||||
ExamAuthorizationCount="1",
|
||||
ExamSeriesCode="6002x001",
|
||||
EligibilityApptDateFirst="2012/12/15",
|
||||
EligibilityApptDateLast="2012/12/30",
|
||||
LastUpdate=datetime.utcnow().strftime("%Y/%m/%d %H:%M:%S")
|
||||
)
|
||||
writer.writerow(record)
|
||||
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 kwargs['force_add']:
|
||||
record['AuthorizationTransactionType'] = 'Add'
|
||||
|
||||
writer.writerow(record)
|
||||
tcr.uploaded_at = uploaded_at
|
||||
tcr.save()
|
||||
|
||||
|
||||
def reset_sample_data(self):
|
||||
def make_sample(**kwargs):
|
||||
data = dict((model_field, kwargs.get(model_field, ""))
|
||||
for model_field in Command.CSV_TO_MODEL_FIELDS.values())
|
||||
return TestCenterUser(**data)
|
||||
|
||||
# TestCenterUser.objects.all().delete()
|
||||
|
||||
samples = [
|
||||
make_sample(
|
||||
client_candidate_id=generate_id(),
|
||||
first_name="Jack",
|
||||
last_name="Doe",
|
||||
middle_name="C",
|
||||
address_1="11 Cambridge Center",
|
||||
address_2="Suite 101",
|
||||
city="Cambridge",
|
||||
state="MA",
|
||||
postal_code="02140",
|
||||
country="USA",
|
||||
phone="(617)555-5555",
|
||||
phone_country_code="1",
|
||||
user_updated_at=datetime.utcnow()
|
||||
),
|
||||
make_sample(
|
||||
client_candidate_id=generate_id(),
|
||||
first_name="Clyde",
|
||||
last_name="Smith",
|
||||
middle_name="J",
|
||||
suffix="Jr.",
|
||||
salutation="Mr.",
|
||||
address_1="1 Penny Lane",
|
||||
city="Honolulu",
|
||||
state="HI",
|
||||
postal_code="96792",
|
||||
country="USA",
|
||||
phone="555-555-5555",
|
||||
phone_country_code="1",
|
||||
user_updated_at=datetime.utcnow()
|
||||
),
|
||||
make_sample(
|
||||
client_candidate_id=generate_id(),
|
||||
first_name="Patty",
|
||||
last_name="Lee",
|
||||
salutation="Dr.",
|
||||
address_1="P.O. Box 555",
|
||||
city="Honolulu",
|
||||
state="HI",
|
||||
postal_code="96792",
|
||||
country="USA",
|
||||
phone="808-555-5555",
|
||||
phone_country_code="1",
|
||||
user_updated_at=datetime.utcnow()
|
||||
),
|
||||
make_sample(
|
||||
client_candidate_id=generate_id(),
|
||||
first_name="Jimmy",
|
||||
last_name="James",
|
||||
address_1="2020 Palmer Blvd.",
|
||||
city="Springfield",
|
||||
state="MA",
|
||||
postal_code="96792",
|
||||
country="USA",
|
||||
phone="917-555-5555",
|
||||
phone_country_code="1",
|
||||
extension="2039",
|
||||
fax="917-555-5556",
|
||||
fax_country_code="1",
|
||||
company_name="ACME Traps",
|
||||
user_updated_at=datetime.utcnow()
|
||||
),
|
||||
make_sample(
|
||||
client_candidate_id=generate_id(),
|
||||
first_name="Yeong-Un",
|
||||
last_name="Seo",
|
||||
address_1="Duryu, Lotte 101",
|
||||
address_2="Apt 55",
|
||||
city="Daegu",
|
||||
country="KOR",
|
||||
phone="917-555-5555",
|
||||
phone_country_code="011",
|
||||
user_updated_at=datetime.utcnow()
|
||||
),
|
||||
|
||||
]
|
||||
|
||||
for tcu in samples:
|
||||
tcu.save()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
from optparse import make_option
|
||||
from time import strftime
|
||||
|
||||
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.'
|
||||
),
|
||||
)
|
||||
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))
|
||||
|
||||
# check to see if a course_id was specified, and use information from that:
|
||||
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:
|
||||
# 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'] = strftime("%Y-%m-%d", exam.first_eligible_appointment_date)
|
||||
our_options['eligibility_appointment_date_last'] = strftime("%Y-%m-%d", exam.last_eligible_appointment_date)
|
||||
|
||||
if exam is None:
|
||||
raise CommandError("Exam for course_id {%s} 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:
|
||||
print "Field Form Error: %s" % fielderror
|
||||
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,35 +1,53 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from optparse import make_option
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from student.models import TestCenterUser
|
||||
from student.models import TestCenterUser, TestCenterUserForm
|
||||
|
||||
class Command(BaseCommand):
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option(
|
||||
'--client_candidate_id',
|
||||
action='store',
|
||||
dest='client_candidate_id',
|
||||
help='ID we assign a user to identify them to Pearson'
|
||||
),
|
||||
# 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',
|
||||
@@ -58,15 +76,56 @@ class Command(BaseCommand):
|
||||
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 a TestCenterUser entry for a given Student"
|
||||
help = "Create or modify a TestCenterUser entry for a given Student"
|
||||
|
||||
@staticmethod
|
||||
def is_valid_option(option_name):
|
||||
@@ -79,7 +138,52 @@ class Command(BaseCommand):
|
||||
print username
|
||||
|
||||
our_options = dict((k, v) for k, v in options.items()
|
||||
if Command.is_valid_option(k))
|
||||
if Command.is_valid_option(k) and v is not None)
|
||||
student = User.objects.get(username=username)
|
||||
student.test_center_user = TestCenterUser(**our_options)
|
||||
student.test_center_user.save()
|
||||
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:
|
||||
if (len(form.errors) > 0):
|
||||
print "Field Form errors encountered:"
|
||||
for fielderror in form.errors:
|
||||
print "Field Form Error: %s" % fielderror
|
||||
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 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."
|
||||
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
# -*- 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):
|
||||
# Adding model 'TestCenterRegistration'
|
||||
db.create_table('student_testcenterregistration', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('testcenter_user', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['student.TestCenterUser'])),
|
||||
('course_id', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)),
|
||||
('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)),
|
||||
('updated_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
|
||||
('user_updated_at', self.gf('django.db.models.fields.DateTimeField')(db_index=True)),
|
||||
('client_authorization_id', self.gf('django.db.models.fields.CharField')(unique=True, max_length=20, db_index=True)),
|
||||
('exam_series_code', self.gf('django.db.models.fields.CharField')(max_length=15, db_index=True)),
|
||||
('eligibility_appointment_date_first', self.gf('django.db.models.fields.DateField')(db_index=True)),
|
||||
('eligibility_appointment_date_last', self.gf('django.db.models.fields.DateField')(db_index=True)),
|
||||
('accommodation_code', self.gf('django.db.models.fields.CharField')(max_length=64, blank=True)),
|
||||
('accommodation_request', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=1024, blank=True)),
|
||||
('uploaded_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)),
|
||||
('processed_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)),
|
||||
('upload_status', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=20, blank=True)),
|
||||
('upload_error_message', self.gf('django.db.models.fields.CharField')(max_length=512, blank=True)),
|
||||
('authorization_id', self.gf('django.db.models.fields.IntegerField')(null=True, db_index=True)),
|
||||
('confirmed_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)),
|
||||
))
|
||||
db.send_create_signal('student', ['TestCenterRegistration'])
|
||||
|
||||
# Adding field 'TestCenterUser.uploaded_at'
|
||||
db.add_column('student_testcenteruser', 'uploaded_at',
|
||||
self.gf('django.db.models.fields.DateTimeField')(db_index=True, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'TestCenterUser.processed_at'
|
||||
db.add_column('student_testcenteruser', 'processed_at',
|
||||
self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'TestCenterUser.upload_status'
|
||||
db.add_column('student_testcenteruser', 'upload_status',
|
||||
self.gf('django.db.models.fields.CharField')(db_index=True, default='', max_length=20, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'TestCenterUser.upload_error_message'
|
||||
db.add_column('student_testcenteruser', 'upload_error_message',
|
||||
self.gf('django.db.models.fields.CharField')(default='', max_length=512, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'TestCenterUser.confirmed_at'
|
||||
db.add_column('student_testcenteruser', 'confirmed_at',
|
||||
self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding index on 'TestCenterUser', fields ['company_name']
|
||||
db.create_index('student_testcenteruser', ['company_name'])
|
||||
|
||||
# Adding unique constraint on 'TestCenterUser', fields ['client_candidate_id']
|
||||
db.create_unique('student_testcenteruser', ['client_candidate_id'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Removing unique constraint on 'TestCenterUser', fields ['client_candidate_id']
|
||||
db.delete_unique('student_testcenteruser', ['client_candidate_id'])
|
||||
|
||||
# Removing index on 'TestCenterUser', fields ['company_name']
|
||||
db.delete_index('student_testcenteruser', ['company_name'])
|
||||
|
||||
# Deleting model 'TestCenterRegistration'
|
||||
db.delete_table('student_testcenterregistration')
|
||||
|
||||
# Deleting field 'TestCenterUser.uploaded_at'
|
||||
db.delete_column('student_testcenteruser', 'uploaded_at')
|
||||
|
||||
# Deleting field 'TestCenterUser.processed_at'
|
||||
db.delete_column('student_testcenteruser', 'processed_at')
|
||||
|
||||
# Deleting field 'TestCenterUser.upload_status'
|
||||
db.delete_column('student_testcenteruser', 'upload_status')
|
||||
|
||||
# Deleting field 'TestCenterUser.upload_error_message'
|
||||
db.delete_column('student_testcenteruser', 'upload_error_message')
|
||||
|
||||
# Deleting field 'TestCenterUser.confirmed_at'
|
||||
db.delete_column('student_testcenteruser', 'confirmed_at')
|
||||
|
||||
|
||||
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': {'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'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.courseenrollmentallowed': {
|
||||
'Meta': {'unique_together': "(('email', 'course_id'),)", 'object_name': 'CourseEnrollmentAllowed'},
|
||||
'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.testcenterregistration': {
|
||||
'Meta': {'object_name': 'TestCenterRegistration'},
|
||||
'accommodation_code': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
|
||||
'accommodation_request': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '1024', 'blank': 'True'}),
|
||||
'authorization_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'client_authorization_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}),
|
||||
'confirmed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
|
||||
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'eligibility_appointment_date_first': ('django.db.models.fields.DateField', [], {'db_index': 'True'}),
|
||||
'eligibility_appointment_date_last': ('django.db.models.fields.DateField', [], {'db_index': 'True'}),
|
||||
'exam_series_code': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'processed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'testcenter_user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['student.TestCenterUser']"}),
|
||||
'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'upload_error_message': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}),
|
||||
'upload_status': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}),
|
||||
'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'})
|
||||
},
|
||||
'student.testcenteruser': {
|
||||
'Meta': {'object_name': 'TestCenterUser'},
|
||||
'address_1': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
|
||||
'address_2': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}),
|
||||
'address_3': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}),
|
||||
'candidate_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'city': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
|
||||
'client_candidate_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'}),
|
||||
'company_name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '50', 'blank': 'True'}),
|
||||
'confirmed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'country': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}),
|
||||
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'extension': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '8', 'blank': 'True'}),
|
||||
'fax': ('django.db.models.fields.CharField', [], {'max_length': '35', 'blank': 'True'}),
|
||||
'fax_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
|
||||
'middle_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'phone': ('django.db.models.fields.CharField', [], {'max_length': '35'}),
|
||||
'phone_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}),
|
||||
'postal_code': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'blank': 'True'}),
|
||||
'processed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'salutation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
|
||||
'state': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}),
|
||||
'suffix': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'upload_error_message': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}),
|
||||
'upload_status': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}),
|
||||
'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'unique': 'True'}),
|
||||
'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'})
|
||||
},
|
||||
'student.userprofile': {
|
||||
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
|
||||
'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.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']
|
||||
@@ -40,6 +40,8 @@ import hashlib
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from random import randint
|
||||
from time import strftime
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
@@ -47,10 +49,10 @@ from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.forms import ModelForm, forms
|
||||
|
||||
import comment_client as cc
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -125,6 +127,9 @@ 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:
|
||||
@@ -140,6 +145,9 @@ class TestCenterUser(models.Model):
|
||||
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)
|
||||
@@ -150,12 +158,8 @@ class TestCenterUser(models.Model):
|
||||
# updated_at, this will not get incremented when we do a batch data import.
|
||||
user_updated_at = models.DateTimeField(db_index=True)
|
||||
|
||||
# Unique ID given to us for this User by the Testing Center. It's null when
|
||||
# we first create the User entry, and is assigned by Pearson later.
|
||||
candidate_id = models.IntegerField(null=True, db_index=True)
|
||||
|
||||
# Unique ID we assign our user for a the Test Center.
|
||||
client_candidate_id = models.CharField(max_length=50, db_index=True)
|
||||
# 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)
|
||||
@@ -186,18 +190,369 @@ class TestCenterUser(models.Model):
|
||||
fax_country_code = models.CharField(max_length=3, blank=True)
|
||||
|
||||
# Company
|
||||
company_name = models.CharField(max_length=50, blank=True)
|
||||
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.utcnow()
|
||||
new_user.save()
|
||||
log.info("Updated demographic information for user's test center exam registration: username \"{}\" ".format(new_user.username))
|
||||
|
||||
# add validation:
|
||||
|
||||
def clean_country(self):
|
||||
code = self.cleaned_data['country']
|
||||
if code and len(code) != 3:
|
||||
raise forms.ValidationError(u'Must be three characters (ISO 3166-1): e.g. USA, CAN, MNG')
|
||||
return code
|
||||
|
||||
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=True)
|
||||
|
||||
# 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'
|
||||
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):
|
||||
# TODO: figure out if this should really go in the database (with a default value).
|
||||
return 1
|
||||
|
||||
@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 = strftime("%Y-%m-%d", exam.first_eligible_appointment_date)
|
||||
registration.eligibility_appointment_date_last = strftime("%Y-%m-%d", exam.last_eligible_appointment_date)
|
||||
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
|
||||
|
||||
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.utcnow()
|
||||
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))
|
||||
|
||||
# TODO: add validation code for values added to accommodation_code field.
|
||||
|
||||
|
||||
|
||||
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)
|
||||
|
||||
def unique_id_for_user(user):
|
||||
"""
|
||||
Return a unique id for a user, suitable for inserting into
|
||||
e.g. personalized survey links.
|
||||
"""
|
||||
# include the secret key as a salt, and to make the ids unique accross
|
||||
# include the secret key as a salt, and to make the ids unique across
|
||||
# different LMS installs.
|
||||
h = hashlib.md5()
|
||||
h.update(settings.SECRET_KEY)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import datetime
|
||||
import feedparser
|
||||
import itertools
|
||||
#import itertools
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import string
|
||||
import sys
|
||||
import time
|
||||
#import time
|
||||
import urllib
|
||||
import uuid
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import logout, authenticate, login
|
||||
from django.contrib.auth.forms import PasswordResetForm
|
||||
@@ -26,18 +27,19 @@ from bs4 import BeautifulSoup
|
||||
from django.core.cache import cache
|
||||
|
||||
from django_future.csrf import ensure_csrf_cookie, csrf_exempt
|
||||
from student.models import (Registration, UserProfile,
|
||||
from student.models import (Registration, UserProfile, TestCenterUser, TestCenterUserForm,
|
||||
TestCenterRegistration, TestCenterRegistrationForm,
|
||||
PendingNameChange, PendingEmailChange,
|
||||
CourseEnrollment, unique_id_for_user)
|
||||
CourseEnrollment, unique_id_for_user,
|
||||
get_testcenter_registration)
|
||||
|
||||
from certificates.models import CertificateStatuses, certificate_status_for_student
|
||||
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
from datetime import date
|
||||
#from datetime import date
|
||||
from collections import namedtuple
|
||||
|
||||
from courseware.courses import get_courses
|
||||
@@ -209,7 +211,7 @@ def _cert_info(user, course, cert_status):
|
||||
def dashboard(request):
|
||||
user = request.user
|
||||
enrollments = CourseEnrollment.objects.filter(user=user)
|
||||
|
||||
|
||||
# Build our courses list for the user, but ignore any courses that no longer
|
||||
# exist (because the course IDs have changed). Still, we don't delete those
|
||||
# enrollments, because it could have been a data push snafu.
|
||||
@@ -239,6 +241,8 @@ def dashboard(request):
|
||||
|
||||
cert_statuses = { course.id: cert_info(request.user, course) for course in courses}
|
||||
|
||||
exam_registrations = { course.id: exam_registration_info(request.user, course) for course in courses}
|
||||
|
||||
# Get the 3 most recent news
|
||||
top_news = _get_news(top=3)
|
||||
|
||||
@@ -249,6 +253,7 @@ def dashboard(request):
|
||||
'show_courseware_links_for' : show_courseware_links_for,
|
||||
'cert_statuses': cert_statuses,
|
||||
'news': top_news,
|
||||
'exam_registrations': exam_registrations,
|
||||
}
|
||||
|
||||
return render_to_response('dashboard.html', context)
|
||||
@@ -300,7 +305,7 @@ def change_enrollment(request):
|
||||
try:
|
||||
course = course_from_id(course_id)
|
||||
except ItemNotFoundError:
|
||||
log.warning("User {0} tried to enroll in non-existant course {1}"
|
||||
log.warning("User {0} tried to enroll in non-existent course {1}"
|
||||
.format(user.username, enrollment.course_id))
|
||||
return {'success': False, 'error': 'The course requested does not exist.'}
|
||||
|
||||
@@ -466,8 +471,9 @@ def _do_create_account(post_vars):
|
||||
try:
|
||||
profile.year_of_birth = int(post_vars['year_of_birth'])
|
||||
except (ValueError, KeyError):
|
||||
profile.year_of_birth = None # If they give us garbage, just ignore it instead
|
||||
# of asking them to put an integer.
|
||||
# If they give us garbage, just ignore it instead
|
||||
# of asking them to put an integer.
|
||||
profile.year_of_birth = None
|
||||
try:
|
||||
profile.save()
|
||||
except Exception:
|
||||
@@ -599,6 +605,162 @@ def create_account(request, post_override=None):
|
||||
js = {'success': True}
|
||||
return HttpResponse(json.dumps(js), mimetype="application/json")
|
||||
|
||||
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:
|
||||
# TODO: do more than just log!! The rest will fail, so we should fail right now.
|
||||
log.error("User {0} enrolled in non-existent course {1}"
|
||||
.format(user.username, course_id))
|
||||
|
||||
# get the exam to be registered for:
|
||||
# (For now, we just assume there is one at most.)
|
||||
exam_info = course.current_test_center_exam
|
||||
|
||||
# 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....
|
||||
|
||||
try:
|
||||
testcenter_user = TestCenterUser.objects.get(user=user)
|
||||
needs_updating = testcenter_user.needs_update(post_vars)
|
||||
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=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")
|
||||
|
||||
# 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 get_random_post_override():
|
||||
"""
|
||||
@@ -654,7 +816,7 @@ def password_reset(request):
|
||||
|
||||
# By default, Django doesn't allow Users with is_active = False to reset their passwords,
|
||||
# but this bites people who signed up a long time ago, never activated, and forgot their
|
||||
# password. So for their sake, we'll auto-activate a user for whome password_reset is called.
|
||||
# password. So for their sake, we'll auto-activate a user for whom password_reset is called.
|
||||
try:
|
||||
user = User.objects.get(email=request.POST['email'])
|
||||
user.is_active = True
|
||||
|
||||
@@ -97,6 +97,21 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
# disable the syllabus content for courses that do not provide a syllabus
|
||||
self.syllabus_present = self.system.resources_fs.exists(path('syllabus'))
|
||||
|
||||
self.test_center_exams = []
|
||||
test_center_info = self.metadata.get('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
|
||||
|
||||
|
||||
def set_grading_policy(self, policy_str):
|
||||
"""Parse the policy specified in policy_str, and save it"""
|
||||
try:
|
||||
@@ -362,6 +377,88 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
"""
|
||||
return self.metadata.get('end_of_course_survey_url')
|
||||
|
||||
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 time.gmtime(0)
|
||||
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")
|
||||
|
||||
|
||||
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 parse_time(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 time.gmtime() > self.first_eligible_appointment_date
|
||||
|
||||
def has_ended(self):
|
||||
return time.gmtime() > self.last_eligible_appointment_date
|
||||
|
||||
def has_started_registration(self):
|
||||
return time.gmtime() > self.registration_start_date
|
||||
|
||||
def has_ended_registration(self):
|
||||
return time.gmtime() > self.registration_end_date
|
||||
|
||||
def is_registering(self):
|
||||
now = time.gmtime()
|
||||
return now >= self.registration_start_date and now <= self.registration_end_date
|
||||
|
||||
@property
|
||||
def first_eligible_appointment_date_text(self):
|
||||
return time.strftime("%b %d, %Y", self.first_eligible_appointment_date)
|
||||
|
||||
@property
|
||||
def last_eligible_appointment_date_text(self):
|
||||
return time.strftime("%b %d, %Y", self.last_eligible_appointment_date)
|
||||
|
||||
@property
|
||||
def registration_end_date_text(self):
|
||||
return time.strftime("%b %d, %Y", 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
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self.display_name
|
||||
|
||||
@@ -124,3 +124,7 @@ class RoundTripTestCase(unittest.TestCase):
|
||||
def test_graphicslidertool_roundtrip(self):
|
||||
#Test graphicslidertool xmodule to see if it exports correctly
|
||||
self.check_export_roundtrip(DATA_DIR,"graphic_slider_tool")
|
||||
|
||||
def test_exam_registration_roundtrip(self):
|
||||
# Test exam_registration xmodule to see if it exports correctly
|
||||
self.check_export_roundtrip(DATA_DIR,"test_exam_registration")
|
||||
|
||||
@@ -94,12 +94,18 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
'start', 'due', 'graded', 'display_name', 'url_name', 'hide_from_toc',
|
||||
'ispublic', # if True, then course is listed for all users; see
|
||||
'xqa_key', # for xqaa server access
|
||||
# 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',
|
||||
# VS[compat] -- remove the below attrs once everything is in the CMS
|
||||
'course', 'org', 'url_name', 'filename')
|
||||
# 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 the below attrs once everything is in the CMS
|
||||
'course', 'org', 'url_name', 'filename')
|
||||
|
||||
# A dictionary mapping xml attribute names AttrMaps that describe how
|
||||
# to import and export them
|
||||
|
||||
2
common/test/data/test_exam_registration/README.md
Normal file
2
common/test/data/test_exam_registration/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
Simple course with test center exam information included in policy.json.
|
||||
|
||||
1
common/test/data/test_exam_registration/course.xml
Symbolic link
1
common/test/data/test_exam_registration/course.xml
Symbolic link
@@ -0,0 +1 @@
|
||||
roots/2012_Fall.xml
|
||||
15
common/test/data/test_exam_registration/course/2012_Fall.xml
Normal file
15
common/test/data/test_exam_registration/course/2012_Fall.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<course>
|
||||
<chapter url_name="Overview">
|
||||
<videosequence url_name="Toy_Videos">
|
||||
<html url_name="toylab"/>
|
||||
<video url_name="Video_Resources" youtube="1.0:1bK-WdDi6Qw"/>
|
||||
</videosequence>
|
||||
<video url_name="Welcome" youtube="1.0:p2Q6BrNhdh8"/>
|
||||
</chapter>
|
||||
<chapter url_name="Ch2">
|
||||
<html url_name="test_html">
|
||||
<h2>Welcome</h2>
|
||||
</html>
|
||||
</chapter>
|
||||
|
||||
</course>
|
||||
3
common/test/data/test_exam_registration/html/toylab.html
Normal file
3
common/test/data/test_exam_registration/html/toylab.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<b>Lab 2A: Superposition Experiment</b>
|
||||
|
||||
<p>Isn't the toy course great?</p>
|
||||
1
common/test/data/test_exam_registration/html/toylab.xml
Normal file
1
common/test/data/test_exam_registration/html/toylab.xml
Normal file
@@ -0,0 +1 @@
|
||||
<html filename="toylab.html"/>
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"course/2012_Fall": {
|
||||
"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"
|
||||
},
|
||||
"chapter/Ch2": {
|
||||
"display_name": "Chapter 2",
|
||||
"start": "2015-07-17T12:00"
|
||||
},
|
||||
"videosequence/Toy_Videos": {
|
||||
"display_name": "Toy Videos",
|
||||
"format": "Lecture Sequence"
|
||||
},
|
||||
"html/toylab": {
|
||||
"display_name": "Toy lab"
|
||||
},
|
||||
"video/Video_Resources": {
|
||||
"display_name": "Video Resources"
|
||||
},
|
||||
"video/Welcome": {
|
||||
"display_name": "Welcome"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<course org="edX" course="test_start_date" url_name="2012_Fall"/>
|
||||
@@ -340,6 +340,11 @@ STAFF_GRADING_INTERFACE = {
|
||||
# Used for testing, debugging
|
||||
MOCK_STAFF_GRADING = False
|
||||
|
||||
################################# Pearson TestCenter config ################
|
||||
|
||||
PEARSONVUE_SIGNINPAGE_URL = "https://www1.pearsonvue.com/testtaker/signin/SignInPage/EDX"
|
||||
# TESTCENTER_ACCOMMODATION_REQUEST_EMAIL = "exam-help@edx.org"
|
||||
|
||||
################################# Peer grading config #####################
|
||||
|
||||
#By setting up the default settings with an incorrect user name and password,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
@import 'multicourse/home';
|
||||
@import 'multicourse/dashboard';
|
||||
@import 'multicourse/testcenter-register';
|
||||
@import 'multicourse/courses';
|
||||
@import 'multicourse/course_about';
|
||||
@import 'multicourse/jobs';
|
||||
|
||||
@@ -267,13 +267,12 @@
|
||||
}
|
||||
|
||||
.my-course {
|
||||
@include border-radius(3px);
|
||||
@include box-shadow(0 1px 8px 0 rgba(0,0,0, 0.1), inset 0 -1px 0 0 rgba(255,255,255, 0.8), inset 0 1px 0 0 rgba(255,255,255, 0.8));
|
||||
clear: both;
|
||||
@include clearfix;
|
||||
height: 120px;
|
||||
margin-right: flex-gutter();
|
||||
margin-bottom: 10px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 50px;
|
||||
padding-bottom: 50px;
|
||||
border-bottom: 1px solid $light-gray;
|
||||
position: relative;
|
||||
width: flex-grid(12);
|
||||
z-index: 20;
|
||||
@@ -283,13 +282,7 @@
|
||||
margin-bottom: none;
|
||||
}
|
||||
|
||||
.cover {
|
||||
background: rgb(225,225,225);
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
border: 1px solid rgb(120,120,120);
|
||||
@include border-left-radius(3px);
|
||||
@include box-shadow(inset 0 0 0 1px rgba(255,255,255, 0.6), 1px 0 0 0 rgba(255,255,255, 0.8));
|
||||
.cover {
|
||||
@include box-sizing(border-box);
|
||||
float: left;
|
||||
height: 100%;
|
||||
@@ -299,100 +292,51 @@
|
||||
position: relative;
|
||||
@include transition(all, 0.15s, linear);
|
||||
width: 200px;
|
||||
height: 120px;
|
||||
|
||||
.shade {
|
||||
@include background-image(linear-gradient(-90deg, rgba(255,255,255, 0.3) 0%,
|
||||
rgba(0,0,0, 0.3) 100%));
|
||||
bottom: 0px;
|
||||
content: "";
|
||||
display: block;
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
z-index: 50;
|
||||
top: 0px;
|
||||
@include transition(all, 0.15s, linear);
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
img {
|
||||
width: 100%;
|
||||
font-size: 70px;
|
||||
line-height: 110px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: rgba(0, 0, 0, .7);
|
||||
opacity: 0;
|
||||
@include transition(all, 0.15s, linear);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.shade {
|
||||
background: rgba(255,255,255, 0.3);
|
||||
@include background-image(linear-gradient(-90deg, rgba(255,255,255, 0.3) 0%,
|
||||
rgba(0,0,0, 0.3) 100%));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
background: rgb(250,250,250);
|
||||
@include background-image(linear-gradient(-90deg, rgb(253,253,253), rgb(240,240,240)));
|
||||
@include box-sizing(border-box);
|
||||
border: 1px solid rgb(190,190,190);
|
||||
border-left: none;
|
||||
@include border-right-radius(3px);
|
||||
left: 201px;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
padding: 0px 10px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
z-index: 2;
|
||||
@include clearfix;
|
||||
padding: 0 10px 0 230px;
|
||||
|
||||
> hgroup {
|
||||
@include clearfix;
|
||||
border-bottom: 1px solid rgb(210,210,210);
|
||||
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6));
|
||||
padding: 12px 0px;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
|
||||
.university {
|
||||
background: rgba(255,255,255, 1);
|
||||
border: 1px solid rgb(180,180,180);
|
||||
@include border-radius(3px);
|
||||
@include box-shadow(inset 0 0 3px 0 rgba(0,0,0, 0.2), 0 1px 0 0 rgba(255,255,255, 0.6));
|
||||
color: $lighter-base-font-color;
|
||||
display: block;
|
||||
font-style: italic;
|
||||
font-family: $sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
@include inline-block;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 0;
|
||||
padding: 5px 10px;
|
||||
float: left;
|
||||
font-weight: 400;
|
||||
margin: 0 0 6px;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
.date-block {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
font-family: $sans-serif;
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
color: $lighter-base-font-color;
|
||||
}
|
||||
|
||||
h3 a {
|
||||
display: block;
|
||||
margin-bottom: 0px;
|
||||
overflow: hidden;
|
||||
padding-top: 2px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 10px;
|
||||
font-family: $sans-serif;
|
||||
font-size: 34px;
|
||||
line-height: 42px;
|
||||
font-weight: 300;
|
||||
|
||||
a {
|
||||
color: $base-font-color;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 1px rgba(255,255,255, 0.6);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -430,71 +374,56 @@
|
||||
}
|
||||
|
||||
.enter-course {
|
||||
@include button(shiny, $blue);
|
||||
@include button(simple, $blue);
|
||||
@include box-sizing(border-box);
|
||||
@include border-radius(3px);
|
||||
display: block;
|
||||
float: left;
|
||||
font: normal 1rem/1.6rem $sans-serif;
|
||||
letter-spacing: 1px;
|
||||
padding: 6px 0px;
|
||||
text-transform: uppercase;
|
||||
font: normal 15px/1.6rem $sans-serif;
|
||||
letter-spacing: 0;
|
||||
padding: 6px 32px 7px;
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
width: flex-grid(4);
|
||||
}
|
||||
}
|
||||
|
||||
> a:hover {
|
||||
.cover {
|
||||
.shade {
|
||||
background: rgba(255,255,255, 0.1);
|
||||
@include background-image(linear-gradient(-90deg, rgba(255,255,255, 0.3) 0%,
|
||||
rgba(0,0,0, 0.3) 100%));
|
||||
&.archived {
|
||||
@include button(simple, #eee);
|
||||
font: normal 15px/1.6rem $sans-serif;
|
||||
padding: 6px 32px 7px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
background: darken(rgb(250,250,250), 5%);
|
||||
@include background-image(linear-gradient(-90deg, darken(rgb(253,253,253), 3%), darken(rgb(240,240,240), 5%)));
|
||||
border-color: darken(rgb(190,190,190), 10%);
|
||||
|
||||
.course-status {
|
||||
background: darken($yellow, 3%);
|
||||
border-color: darken(rgb(200,200,200), 3%);
|
||||
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6));
|
||||
}
|
||||
|
||||
.course-status-completed {
|
||||
background: #888;
|
||||
color: #fff;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-status {
|
||||
@include clearfix;
|
||||
@include border-radius(3px);
|
||||
@include box-shadow(0 1px 4px 0 rgba(0,0,0, 0.1), inset 0 -1px 0 0 rgba(255,255,255, 0.8), inset 0 1px 0 0 rgba(255,255,255, 0.8));
|
||||
display: none;
|
||||
position: relative;
|
||||
top: -15px;
|
||||
z-index: 10;
|
||||
margin: 0 0 20px 0;
|
||||
margin: 20px 0 10px;
|
||||
padding: 15px 20px;
|
||||
font-family: "Open Sans", Verdana, Geneva, sans-serif;
|
||||
background: #fffcf0;
|
||||
font-family: $sans-serif;
|
||||
background: tint($yellow,70%);
|
||||
border: 1px solid #ccc;
|
||||
|
||||
.message-copy {
|
||||
font-family: $sans-serif;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
|
||||
a {
|
||||
font-family: $sans-serif;
|
||||
}
|
||||
|
||||
.grade-value {
|
||||
font-size: 1.4rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
@@ -502,19 +431,18 @@
|
||||
.actions {
|
||||
@include clearfix;
|
||||
list-style: none;
|
||||
margin: 15px 0 0 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
.action {
|
||||
float: left;
|
||||
margin:0 15px 10px 0;
|
||||
margin: 0 15px 0 0;
|
||||
|
||||
.btn, .cta {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn {
|
||||
@include button(shiny, $blue);
|
||||
@include box-sizing(border-box);
|
||||
@include border-radius(3px);
|
||||
float: left;
|
||||
@@ -524,7 +452,6 @@
|
||||
text-align: center;
|
||||
|
||||
&.disabled {
|
||||
@include button(shiny, #eee);
|
||||
cursor: default !important;
|
||||
|
||||
&:hover {
|
||||
@@ -539,7 +466,6 @@
|
||||
}
|
||||
|
||||
.cta {
|
||||
@include button(shiny, #666);
|
||||
float: left;
|
||||
font: normal 0.8rem/1.2rem $sans-serif;
|
||||
letter-spacing: 1px;
|
||||
@@ -549,6 +475,52 @@
|
||||
}
|
||||
}
|
||||
|
||||
.exam-registration-number {
|
||||
font-family: $sans-serif;
|
||||
font-size: 18px;
|
||||
|
||||
a {
|
||||
font-family: $sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
&.exam-register {
|
||||
|
||||
.message-copy {
|
||||
margin-top: 5px;
|
||||
width: 55%;
|
||||
}
|
||||
}
|
||||
|
||||
&.exam-schedule {
|
||||
.exam-button {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.exam-button {
|
||||
@include button(simple, $pink);
|
||||
margin-top: 0;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.contact-button {
|
||||
@include button(simple, $pink);
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
padding: 9px 18px 10px;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
@@ -577,17 +549,16 @@
|
||||
|
||||
a.unenroll {
|
||||
float: right;
|
||||
display: block;
|
||||
font-style: italic;
|
||||
color: #a0a0a0;
|
||||
text-decoration: underline;
|
||||
font-size: .8em;
|
||||
@include inline-block;
|
||||
margin-bottom: 40px;
|
||||
margin-top: 32px;
|
||||
|
||||
&:hover {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
794
lms/static/sass/multicourse/_testcenter-register.scss
Normal file
794
lms/static/sass/multicourse/_testcenter-register.scss
Normal file
@@ -0,0 +1,794 @@
|
||||
// ==========
|
||||
|
||||
$baseline: 20px;
|
||||
$yellow: rgb(255, 235, 169);
|
||||
$red: rgb(178, 6, 16);
|
||||
|
||||
// ==========
|
||||
|
||||
.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);
|
||||
@include box-shadow(0 1px 2px 0 rgba(0,0,0, 0.1));
|
||||
}
|
||||
|
||||
form {
|
||||
border: 1px solid rgb(216, 223, 230);
|
||||
@include border-radius(3px);
|
||||
@include 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);
|
||||
@include 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 {
|
||||
@include 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);
|
||||
|
||||
&.is-focused {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
padding: $baseline ($baseline*.75);
|
||||
|
||||
&.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 {
|
||||
@include 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);
|
||||
font-size: 13px;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
&:before {
|
||||
@include 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 {
|
||||
|
||||
.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 {
|
||||
@include 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 {
|
||||
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 {
|
||||
@include 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 {
|
||||
@include 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 {
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,8 @@ label {
|
||||
textarea,
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"] {
|
||||
input[type="password"],
|
||||
input[type="tel"] {
|
||||
background: rgb(250,250,250);
|
||||
border: 1px solid rgb(200,200,200);
|
||||
@include border-radius(3px);
|
||||
|
||||
@@ -198,87 +198,132 @@
|
||||
course_target = reverse('about_course', args=[course.id])
|
||||
%>
|
||||
|
||||
<a href="${course_target}">
|
||||
<section class="cover" style="background-image: url('${course_image_url(course)}')">
|
||||
<div class="shade"></div>
|
||||
<div class="arrow">❯</div>
|
||||
</section>
|
||||
|
||||
<section class="info">
|
||||
<hgroup>
|
||||
<h2 class="university">${get_course_about_section(course, 'university')}</h2>
|
||||
<h3>${course.number} ${course.title}</h3>
|
||||
</hgroup>
|
||||
<section class="course-status course-status-completed">
|
||||
<p>
|
||||
|
||||
<a href="${course_target}" class="cover">
|
||||
<img src="${course_image_url(course)}" />
|
||||
</a>
|
||||
|
||||
<section class="info">
|
||||
<hgroup>
|
||||
<p class="date-block">
|
||||
% if course.has_ended():
|
||||
Course Completed - <span>${course.end_date_text}</span>
|
||||
Course Completed - ${course.end_date_text}
|
||||
% elif course.has_started():
|
||||
Course Started - <span>${course.start_date_text}</span>
|
||||
Course Started - ${course.start_date_text}
|
||||
% else: # hasn't started yet
|
||||
Course Starts - <span>${course.start_date_text}</span>
|
||||
Course Starts - ${course.start_date_text}
|
||||
% endif
|
||||
</p>
|
||||
</section>
|
||||
% if course.id in show_courseware_links_for:
|
||||
<p class="enter-course">View Courseware</p>
|
||||
% endif
|
||||
</section>
|
||||
</a>
|
||||
<h2 class="university">${get_course_about_section(course, 'university')}</h2>
|
||||
<h3><a href="${course_target}">${course.number} ${course.title}</a></h3>
|
||||
</hgroup>
|
||||
|
||||
<%
|
||||
testcenter_exam_info = course.current_test_center_exam
|
||||
registration = exam_registrations.get(course.id)
|
||||
testcenter_register_target = reverse('begin_exam_registration', args=[course.id])
|
||||
%>
|
||||
% if testcenter_exam_info is not None:
|
||||
|
||||
% if registration is None and testcenter_exam_info.is_registering():
|
||||
<div class="message message-status is-shown exam-register">
|
||||
<a href="${testcenter_register_target}" class="button exam-button" id="exam_register_button">Register for Pearson exam</a>
|
||||
<p class="message-copy">Registration for the Pearson exam is now open and will close on <strong>${testcenter_exam_info.registration_end_date_text}</strong></p>
|
||||
</div>
|
||||
% endif
|
||||
<!-- display a registration for a current exam, even if the registration period is over -->
|
||||
% if registration is not None:
|
||||
% if registration.is_accepted:
|
||||
<div class="message message-status is-shown exam-schedule">
|
||||
<a href="${registration.registration_signup_url}" class="button exam-button">Schedule Pearson exam</a>
|
||||
<p class="exam-registration-number"><a href="${testcenter_register_target}" id="exam_register_link">Registration</a> number: <strong>${registration.client_candidate_id}</strong></p>
|
||||
<p class="message-copy">Write this down! You’ll need it to schedule your exam.</p>
|
||||
</div>
|
||||
% endif
|
||||
% if registration.is_rejected:
|
||||
<div class="message message-status is-shown exam-schedule">
|
||||
<p class="message-copy">Your
|
||||
<a href="${testcenter_register_target}" id="exam_register_link">registration for the Pearson exam</a>
|
||||
has been rejected. Please check the information you provided, and try to correct any demographic errors. Otherwise
|
||||
contact edX for further help.</p>
|
||||
<a href="mailto:exam-help@edx.org?subject=Pearson VUE Exam - ${get_course_about_section(course, 'university')} ${course.number}" class="button contact-button">Contact exam-help@edx.org</a>
|
||||
</div>
|
||||
% endif
|
||||
% if not registration.is_accepted and not registration.is_rejected:
|
||||
<div class="message message-status is-shown">
|
||||
<p class="message-copy">Your
|
||||
<a href="${testcenter_register_target}" id="exam_register_link">registration for the Pearson exam</a>
|
||||
is pending. Within a few days, you should see a confirmation number here, which can be used to schedule your exam.</p>
|
||||
</div>
|
||||
% endif
|
||||
% endif
|
||||
|
||||
% endif
|
||||
|
||||
<%
|
||||
cert_status = cert_statuses.get(course.id)
|
||||
%>
|
||||
% if course.has_ended() and cert_status:
|
||||
<%
|
||||
if cert_status['status'] == 'generating':
|
||||
status_css_class = 'course-status-certrendering'
|
||||
elif cert_status['status'] == 'ready':
|
||||
status_css_class = 'course-status-certavailable'
|
||||
elif cert_status['status'] == 'notpassing':
|
||||
status_css_class = 'course-status-certnotavailable'
|
||||
else:
|
||||
status_css_class = 'course-status-processing'
|
||||
%>
|
||||
<div class="message message-status ${status_css_class} is-shown">
|
||||
|
||||
% if cert_status['status'] == 'processing':
|
||||
<p class="message-copy">Final course details are being wrapped up at
|
||||
this time. Your final standing will be available shortly.</p>
|
||||
% elif cert_status['status'] in ('generating', 'ready', 'notpassing'):
|
||||
<p class="message-copy">Your final grade:
|
||||
<span class="grade-value">${"{0:.0f}%".format(float(cert_status['grade'])*100)}</span>.
|
||||
% if cert_status['status'] == 'notpassing':
|
||||
Grade required for a certificate: <span class="grade-value">
|
||||
${"{0:.0f}%".format(float(course.lowest_passing_grade)*100)}</span>.
|
||||
% endif
|
||||
</p>
|
||||
% endif
|
||||
|
||||
% if cert_status['show_disabled_download_button'] or cert_status['show_download_url'] or cert_status['show_survey_button']:
|
||||
<ul class="actions">
|
||||
% if cert_status['show_disabled_download_button']:
|
||||
<li class="action"><span class="disabled">
|
||||
Your Certificate is Generating</span></li>
|
||||
% elif cert_status['show_download_url']:
|
||||
<li class="action">
|
||||
<a class="btn" href="${cert_status['download_url']}"
|
||||
title="This link will open/download a PDF document">
|
||||
Download Your PDF Certificate</a></li>
|
||||
% endif
|
||||
|
||||
% if cert_status['show_survey_button']:
|
||||
<li class="action"><a class="cta" href="${cert_status['survey_url']}">
|
||||
Complete our course feedback survey</a></li>
|
||||
% endif
|
||||
</ul>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
% endif
|
||||
|
||||
% if course.id in show_courseware_links_for:
|
||||
% if course.has_ended():
|
||||
<a href="${course_target}" class="enter-course archived">View Archived Course</a>
|
||||
% else:
|
||||
<a href="${course_target}" class="enter-course">View Course</a>
|
||||
% endif
|
||||
% endif
|
||||
<a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}">Unregister</a>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
<%
|
||||
cert_status = cert_statuses.get(course.id)
|
||||
%>
|
||||
% if course.has_ended() and cert_status:
|
||||
<%
|
||||
if cert_status['status'] == 'generating':
|
||||
status_css_class = 'course-status-certrendering'
|
||||
elif cert_status['status'] == 'ready':
|
||||
status_css_class = 'course-status-certavailable'
|
||||
elif cert_status['status'] == 'notpassing':
|
||||
status_css_class = 'course-status-certnotavailable'
|
||||
else:
|
||||
status_css_class = 'course-status-processing'
|
||||
%>
|
||||
<div class="message message-status ${status_css_class} is-shown">
|
||||
|
||||
% if cert_status['status'] == 'processing':
|
||||
<p class="message-copy">Final course details are being wrapped up at
|
||||
this time. Your final standing will be available shortly.</p>
|
||||
% elif cert_status['status'] in ('generating', 'ready', 'notpassing'):
|
||||
<p class="message-copy">Your final grade:
|
||||
<span class="grade-value">${"{0:.0f}%".format(float(cert_status['grade'])*100)}</span>.
|
||||
% if cert_status['status'] == 'notpassing':
|
||||
Grade required for a certificate: <span class="grade-value">
|
||||
${"{0:.0f}%".format(float(course.lowest_passing_grade)*100)}</span>.
|
||||
% endif
|
||||
</p>
|
||||
% endif
|
||||
|
||||
% if cert_status['show_disabled_download_button'] or cert_status['show_download_url'] or cert_status['show_survey_button']:
|
||||
<ul class="actions">
|
||||
% if cert_status['show_disabled_download_button']:
|
||||
<li class="action"><span class="btn disabled" href="">
|
||||
Your Certificate is Generating</span></li>
|
||||
% elif cert_status['show_download_url']:
|
||||
<li class="action">
|
||||
<a class="btn" href="${cert_status['download_url']}"
|
||||
title="This link will open/download a PDF document">
|
||||
Download Your PDF Certificate</a></li>
|
||||
% endif
|
||||
|
||||
% if cert_status['show_survey_button']:
|
||||
<li class="action"><a class="cta" href="${cert_status['survey_url']}">
|
||||
Complete our course feedback survey</a></li>
|
||||
% endif
|
||||
</ul>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
% endif
|
||||
|
||||
<a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}">Unregister</a>
|
||||
|
||||
|
||||
% endfor
|
||||
% else:
|
||||
|
||||
480
lms/templates/test_center_register.html
Normal file
480
lms/templates/test_center_register.html
Normal file
@@ -0,0 +1,480 @@
|
||||
<%!
|
||||
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.number} ${course.title}</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>${registration.client_candidate_id}</strong>. (Write this down! You’ll need it to schedule your exam.)</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 registration status 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}" placeholder="e.g. 1-55-555-5555" />
|
||||
</div>
|
||||
<div class="field" id="field-extension">
|
||||
<label for="extension">Extension</label>
|
||||
<input id="extension" class="short" type="tel" name="extension" value="${testcenteruser.extension}" placeholder="e.g. 555" />
|
||||
</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}" placeholder="e.g. 1, 44, 976" />
|
||||
</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}" placeholder="e.g. 1-55-555-5555" />
|
||||
</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}" placeholder="e.g. 1, 44, 976" />
|
||||
</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>${exam_info.registration_end_date_text}</strong></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>${exam_info.registration_end_date_text}</strong></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>${exam_info.registration_end_date_text}</strong>.
|
||||
The error message is:</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 <a class="contact-link" href="${exam_help_href}">contact edX at exam-help@edx.org</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 <a class="contact-link" href="${exam_help_href}">contact edX at exam-help@edx.org</a> if you have any questions.</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 <a href="${registration.registration_signup_url}" class="exam-link">schedule a Pearson exam</a></strong>.</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 <a class="contact-link" href="${exam_help_href}">contact edX at exam-help@edx.org</a>.</p>
|
||||
</li>
|
||||
% endif
|
||||
</ol>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
% endif
|
||||
|
||||
<div class="details details-course">
|
||||
<h4>About ${get_course_about_section(course, 'university')} ${course.number}</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}</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 End Date:</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 <a class="contact-link" href="${exam_help_href}">exam-help@edx.org</a>.</p>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
@@ -45,6 +45,9 @@ urlpatterns = ('',
|
||||
url(r'^create_account$', 'student.views.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
|
||||
|
||||
Reference in New Issue
Block a user