Add TestCenterExam class to course module, and plumb through.

This commit is contained in:
Brian Wilson
2013-01-06 17:57:13 -05:00
parent ea8a56da0a
commit e57656e24f
5 changed files with 204 additions and 95 deletions

View File

@@ -41,6 +41,7 @@ import json
import logging
import uuid
from random import randint
from time import strftime
from django.conf import settings
@@ -236,6 +237,15 @@ class TestCenterUser(models.Model):
testcenter_user.client_candidate_id = cand_id
return testcenter_user
def is_accepted(self):
return self.upload_status == 'Accepted'
def is_rejected(self):
return self.upload_status == 'Error'
def is_pending(self):
return self.upload_status == ''
class TestCenterUserForm(ModelForm):
class Meta:
model = TestCenterUser
@@ -373,15 +383,15 @@ class TestCenterRegistration(models.Model):
@property
def client_candidate_id(self):
return self.testcenter_user.client_candidate_id
@staticmethod
def create(testcenter_user, course_id, exam_info, accommodation_request):
def create(testcenter_user, exam, accommodation_request):
registration = TestCenterRegistration(testcenter_user = testcenter_user)
registration.course_id = course_id
registration.course_id = exam.course_id
registration.accommodation_request = accommodation_request
registration.exam_series_code = exam_info.get('Exam_Series_Code')
registration.eligibility_appointment_date_first = exam_info.get('First_Eligible_Appointment_Date')
registration.eligibility_appointment_date_last = exam_info.get('Last_Eligible_Appointment_Date')
registration.exam_series_code = exam.exam_series_code # .get('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)
# accommodation_code remains blank for now, along with Pearson confirmation
registration.client_authorization_id = registration._create_client_authorization_id()
return registration
@@ -404,16 +414,16 @@ class TestCenterRegistration(models.Model):
return auth_id
def is_accepted(self):
return self.upload_status == 'Accepted'
return self.upload_status == 'Accepted' and self.testcenter_user.is_accepted()
def is_rejected(self):
return self.upload_status == 'Error'
return self.upload_status == 'Error' or self.testcenter_user.is_rejected()
def is_pending_accommodation(self):
return len(self.accommodation_request) > 0 and self.accommodation_code == ''
def is_pending_acknowledgement(self):
return self.upload_status == '' and not self.is_pending_accommodation()
return (self.upload_status == '' or self.testcenter_user.is_pending()) and not self.is_pending_accommodation()
class TestCenterRegistrationForm(ModelForm):
class Meta:
@@ -430,15 +440,12 @@ class TestCenterRegistrationForm(ModelForm):
def get_testcenter_registrations_for_user_and_course(user, course_id, exam_series_code=None):
def get_testcenter_registration(user, course_id, exam_series_code):
try:
tcu = TestCenterUser.objects.get(user=user)
except TestCenterUser.DoesNotExist:
return []
if exam_series_code is None:
return TestCenterRegistration.objects.filter(testcenter_user=tcu, course_id=course_id)
else:
return TestCenterRegistration.objects.filter(testcenter_user=tcu, course_id=course_id, exam_series_code=exam_series_code)
return TestCenterRegistration.objects.filter(testcenter_user=tcu, course_id=course_id, exam_series_code=exam_series_code)
def unique_id_for_user(user):
"""

View File

@@ -31,7 +31,7 @@ from student.models import (Registration, UserProfile, TestCenterUser, TestCente
TestCenterRegistration, TestCenterRegistrationForm,
PendingNameChange, PendingEmailChange,
CourseEnrollment, unique_id_for_user,
get_testcenter_registrations_for_user_and_course)
get_testcenter_registration)
from certificates.models import CertificateStatuses, certificate_status_for_student
@@ -237,6 +237,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)
@@ -247,6 +249,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)
@@ -589,32 +592,45 @@ 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 len(registrations) > 0:
registration = registrations[0]
else:
registration = None
return registration
@login_required
@ensure_csrf_cookie
def begin_test_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.)
# TODO: this should be an object, including the course_id and the
# exam info for a particular exam from the course.
exam_info = course.testcenter_info
exam_info = course.current_test_center_exam
# figure out if the user is already registered for this exam:
# (Again, for now we assume that any registration that exists is for this exam.)
registrations = get_testcenter_registrations_for_user_and_course(user, course_id)
if len(registrations) > 0:
registration = registrations[0]
else:
registration = None
log.info("User {0} enrolled in course {1} calls for test registration page".format(user.username, course_id))
# 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.
@@ -636,12 +652,9 @@ def begin_test_registration(request, course_id):
@ensure_csrf_cookie
def create_test_registration(request, post_override=None):
'''
JSON call to create test registration.
Used by form in test_center_register.html, which is called from
into dashboard.html
JSON call to create a test center exam registration.
Called by form in test_center_register.html
'''
# js = {'success': False}
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
@@ -660,7 +673,6 @@ def create_test_registration(request, post_override=None):
testcenter_user = TestCenterUser.create(user)
needs_updating = True
# perform validation:
if needs_updating:
log.info("User {0} enrolled in course {1} updating demographic info for test registration".format(user.username, course_id))
@@ -678,20 +690,19 @@ def create_test_registration(request, post_override=None):
# create and save the registration:
needs_saving = False
exam_info = course.testcenter_info
registrations = get_testcenter_registrations_for_user_and_course(user, course_id)
# In future, this should check the exam series code of the registrations, if there
# were multiple.
exam = course.current_test_center_exam
exam_code = exam.exam_series_code
registrations = get_testcenter_registration(user, course_id, exam_code)
if len(registrations) > 0:
registration = registrations[0]
# check to see if registration changed. Should check appointment dates too...
# TODO: check to see if registration changed. Should check appointment dates too...
# And later should check changes in accommodation_code.
# But at the moment, we don't expect anything to cause this to change
# right now.
# because of the registration form.
else:
accommodation_request = post_vars.get('accommodation_request','')
registration = TestCenterRegistration.create(testcenter_user, course_id, exam_info, accommodation_request)
registration = TestCenterRegistration.create(testcenter_user, exam, accommodation_request)
needs_saving = True
if needs_saving:
@@ -732,8 +743,6 @@ def create_test_registration(request, post_override=None):
# TODO: enable appropriate stat
# statsd.increment("common.student.account_created")
log.info("User {0} enrolled in course {1} returning from enter/update demographic info for test registration".format(user.username, course_id))
js = {'success': True}
return HttpResponse(json.dumps(js), mimetype="application/json")

View File

@@ -96,6 +96,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:
@@ -315,26 +330,89 @@ class CourseDescriptor(SequenceDescriptor):
Returns None if no url specified.
"""
return self.metadata.get('end_of_course_survey_url')
@property
def testcenter_info(self):
"""
Pull from policy.
TODO: decide if we expect this entry to be a single test, or if multiple tests are possible
per course.
For now we expect this entry to be a single test.
Returns None if no testcenter info specified, or if no exam is included.
"""
info = self.metadata.get('testcenter_info')
if info is None or len(info) == 0:
return None;
else:
return info.values()[0]
class TestCenterExam:
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