Commit ee990806 by Brian Wilson

add additional fields to testcenter user and update test center registration.

parent f472ac60
...@@ -141,6 +141,9 @@ class TestCenterUser(models.Model): ...@@ -141,6 +141,9 @@ class TestCenterUser(models.Model):
The field names and lengths are modeled on the conventions and constraints The field names and lengths are modeled on the conventions and constraints
of Pearson's data import system, including oddities such as suffix having of Pearson's data import system, including oddities such as suffix having
a limit of 255 while last_name only gets 50. 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... # Our own record keeping...
user = models.ForeignKey(User, unique=True, default=None) user = models.ForeignKey(User, unique=True, default=None)
...@@ -155,7 +158,7 @@ class TestCenterUser(models.Model): ...@@ -155,7 +158,7 @@ class TestCenterUser(models.Model):
# we first create the User entry, and is assigned by Pearson later. # we first create the User entry, and is assigned by Pearson later.
candidate_id = models.IntegerField(null=True, db_index=True) candidate_id = models.IntegerField(null=True, db_index=True)
# Unique ID we assign our user for a the Test Center. # Unique ID we assign our user for the Test Center.
client_candidate_id = models.CharField(max_length=50, db_index=True) client_candidate_id = models.CharField(max_length=50, db_index=True)
# Name # Name
...@@ -189,6 +192,11 @@ class TestCenterUser(models.Model): ...@@ -189,6 +192,11 @@ class TestCenterUser(models.Model):
# Company # Company
company_name = models.CharField(max_length=50, blank=True) company_name = models.CharField(max_length=50, blank=True)
# Confirmation
upload_status = models.CharField(max_length=20, blank=True) # 'Error' or 'Accepted'
confirmed_at = models.DateTimeField(null=True, db_index=True)
upload_error_message = models.CharField(max_length=512, blank=True)
@staticmethod @staticmethod
def user_provided_fields(): def user_provided_fields():
return [ 'first_name', 'middle_name', 'last_name', 'suffix', 'salutation', return [ 'first_name', 'middle_name', 'last_name', 'suffix', 'salutation',
...@@ -212,17 +220,38 @@ class TestCenterRegistration(models.Model): ...@@ -212,17 +220,38 @@ class TestCenterRegistration(models.Model):
The field names and lengths are modeled on the conventions and constraints The field names and lengths are modeled on the conventions and constraints
of Pearson's data import system. of Pearson's data import system.
""" """
# TODO: Check the spec to find out lengths specified by Pearson # 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, unique=True, default=None) testcenter_user = models.ForeignKey(TestCenterUser, unique=True, default=None)
course_id = models.CharField(max_length=128, db_index=True)
created_at = models.DateTimeField(auto_now_add=True, db_index=True) created_at = models.DateTimeField(auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True, db_index=True) updated_at = models.DateTimeField(auto_now=True, db_index=True)
course_id = models.CharField(max_length=128, 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 the client's 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)
# TODO: this should be an enumeration:
accommodation_code = models.CharField(max_length=64, blank=True)
# store the original text of the accommodation request. # store the original text of the accommodation request.
accommodation_request = models.CharField(max_length=1024, blank=True) accommodation_request = models.CharField(max_length=1024, blank=True)
# TODO: this should be an enumeration:
accommodation_code = models.CharField(max_length=64, blank=True) # Confirmation
upload_status = models.CharField(max_length=20, blank=True) # 'Error' or 'Accepted'
confirmed_at = models.DateTimeField(db_index=True)
upload_error_message = models.CharField(max_length=512, blank=True)
@property @property
def candidate_id(self): def candidate_id(self):
...@@ -232,6 +261,15 @@ class TestCenterRegistration(models.Model): ...@@ -232,6 +261,15 @@ class TestCenterRegistration(models.Model):
def client_candidate_id(self): def client_candidate_id(self):
return self.testcenter_user.client_candidate_id return self.testcenter_user.client_candidate_id
def get_testcenter_registrations_for_user_and_course(user, course_id):
try:
tcu = TestCenterUser.objects.get(user=user)
except User.DoesNotExist:
return []
return TestCenterRegistration.objects.filter(testcenter_user=tcu, course_id=course_id)
def unique_id_for_user(user): def unique_id_for_user(user):
""" """
Return a unique id for a user, suitable for inserting into Return a unique id for a user, suitable for inserting into
......
import datetime import datetime
import feedparser import feedparser
import itertools #import itertools
import json import json
import logging import logging
import random import random
import string import string
import sys import sys
import time #import time
import urllib import urllib
import uuid import uuid
...@@ -19,7 +19,8 @@ from django.core.context_processors import csrf ...@@ -19,7 +19,8 @@ from django.core.context_processors import csrf
from django.core.mail import send_mail from django.core.mail import send_mail
from django.core.validators import validate_email, validate_slug, ValidationError from django.core.validators import validate_email, validate_slug, ValidationError
from django.db import IntegrityError from django.db import IntegrityError
from django.http import HttpResponse, HttpResponseForbidden, Http404 from django.http import HttpResponse, HttpResponseForbidden, Http404,\
HttpResponseRedirect
from django.shortcuts import redirect from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
...@@ -37,13 +38,14 @@ from xmodule.modulestore.exceptions import ItemNotFoundError ...@@ -37,13 +38,14 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from datetime import date #from datetime import date
from collections import namedtuple from collections import namedtuple
from courseware.courses import get_courses_by_university from courseware.courses import get_courses_by_university
from courseware.access import has_access from courseware.access import has_access
from statsd import statsd from statsd import statsd
from django.contrib.localflavor.ie.ie_counties import IE_COUNTY_CHOICES
log = logging.getLogger("mitx.student") log = logging.getLogger("mitx.student")
Article = namedtuple('Article', 'title url author image deck publication publish_date') Article = namedtuple('Article', 'title url author image deck publication publish_date')
...@@ -640,9 +642,11 @@ def _do_create_or_update_test_center_user(post_vars): ...@@ -640,9 +642,11 @@ def _do_create_or_update_test_center_user(post_vars):
# first determine if we need to create a new TestCenterUser, or if we are making any update # first determine if we need to create a new TestCenterUser, or if we are making any update
# to an existing TestCenterUser. # to an existing TestCenterUser.
username=post_vars['username'] username = post_vars['username']
user = User.objects.get(username=username) user = User.objects.get(username=username)
course_id = post_vars['course_id']
course = (course_from_id(course_id)) # assume it will be found....
needs_saving = False needs_saving = False
try: try:
testcenter_user = TestCenterUser.objects.get(user=user) testcenter_user = TestCenterUser.objects.get(user=user)
...@@ -651,9 +655,8 @@ def _do_create_or_update_test_center_user(post_vars): ...@@ -651,9 +655,8 @@ def _do_create_or_update_test_center_user(post_vars):
for fieldname in TestCenterUser.user_provided_fields()]) for fieldname in TestCenterUser.user_provided_fields()])
if needs_updating: if needs_updating:
# TODO: what do we set a timestamp to, in order to get now()? # leave user and client_candidate_id as before
testcenter_user.user_updated_at = datetime.datetime.now() testcenter_user.user_updated_at = datetime.datetime.now()
# Now do the update:
for fieldname in TestCenterUser.user_provided_fields(): for fieldname in TestCenterUser.user_provided_fields():
testcenter_user.__setattr__(fieldname, post_vars[fieldname]) testcenter_user.__setattr__(fieldname, post_vars[fieldname])
needs_saving = True needs_saving = True
...@@ -661,7 +664,6 @@ def _do_create_or_update_test_center_user(post_vars): ...@@ -661,7 +664,6 @@ def _do_create_or_update_test_center_user(post_vars):
except TestCenterUser.DoesNotExist: except TestCenterUser.DoesNotExist:
# did not find the TestCenterUser, so create a new one # did not find the TestCenterUser, so create a new one
testcenter_user = TestCenterUser(user=user) testcenter_user = TestCenterUser(user=user)
# testcenter_user.user = user
for fieldname in TestCenterUser.user_provided_fields(): for fieldname in TestCenterUser.user_provided_fields():
testcenter_user.__setattr__(fieldname, post_vars[fieldname]) testcenter_user.__setattr__(fieldname, post_vars[fieldname])
# testcenter_user.candidate_id remains unset # testcenter_user.candidate_id remains unset
...@@ -670,31 +672,43 @@ def _do_create_or_update_test_center_user(post_vars): ...@@ -670,31 +672,43 @@ def _do_create_or_update_test_center_user(post_vars):
needs_saving = True needs_saving = True
# additional validation occurs at save time, so handle exceptions # additional validation occurs at save time, so handle exceptions
# TODO: Rearrange so that if part of the process fails, the whole process fails.
# Right now, we can have e.g. no registration e-mail sent out and a zombie account
if needs_saving: if needs_saving:
try: try:
testcenter_user.save() testcenter_user.save()
except IntegrityError: except IntegrityError, ie:
js = {'success': False} message = ie
# TODO: Figure out the cause of the integrity error context = {'course': course,
if len(User.objects.filter(username=post_vars['username'])) > 0: 'user': user,
js['value'] = "An account with this username already exists." 'message': message,
js['field'] = 'username' 'testcenteruser': testcenter_user,
return HttpResponse(json.dumps(js)) }
return render_to_response('test_center_register.html', context)
if len(User.objects.filter(email=post_vars['email'])) > 0:
js['value'] = "An account with this e-mail already exists."
js['field'] = 'email'
return HttpResponse(json.dumps(js))
raise
# create and save the registration:
registration = TestCenterRegistration(testcenter_user = testcenter_user) registration = TestCenterRegistration(testcenter_user = testcenter_user)
# registration.register(user)
registration.course_id = post_vars['course_id'] registration.course_id = post_vars['course_id']
registration.accommodation_request = post_vars['accommodations'] registration.accommodation_request = post_vars['accommodations']
exam_info = course.testcenter_info
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')
# accommodation_code remains blank for now, along with Pearson confirmation
registration.user_updated_at = datetime.datetime.now()
# "client_authorization_id" is the client's unique identifier for the authorization.
# This must be present for an update or delete to be sent to Pearson.
registration.client_authorization_id = "1"
try:
registration.save()
except IntegrityError, ie:
message = ie
context = {'course': course,
'user': user,
'message': message,
'testcenteruser': testcenter_user,
}
return render_to_response('test_center_register.html', context)
return (user, testcenter_user, registration) return (user, testcenter_user, registration)
...@@ -759,11 +773,12 @@ def create_test_registration(request, post_override=None): ...@@ -759,11 +773,12 @@ def create_test_registration(request, post_override=None):
js['value'] = 'Could not send accommodation e-mail.' js['value'] = 'Could not send accommodation e-mail.'
return HttpResponse(json.dumps(js)) return HttpResponse(json.dumps(js))
# TODO: enable appropriate stat
# statsd.increment("common.student.account_created") # statsd.increment("common.student.account_created")
js = {'success': True} # js = {'success': True}
return HttpResponse(json.dumps(js), mimetype="application/json") # return HttpResponse(json.dumps(js), mimetype="application/json")
return HttpResponseRedirect('/dashboard')
def get_random_post_override(): def get_random_post_override():
""" """
......
...@@ -315,7 +315,7 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -315,7 +315,7 @@ class CourseDescriptor(SequenceDescriptor):
Returns None if no url specified. Returns None if no url specified.
""" """
return self.metadata.get('end_of_course_survey_url') return self.metadata.get('end_of_course_survey_url')
@property @property
def testcenter_info(self): def testcenter_info(self):
""" """
...@@ -324,10 +324,17 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -324,10 +324,17 @@ class CourseDescriptor(SequenceDescriptor):
TODO: decide if we expect this entry to be a single test, or if multiple tests are possible TODO: decide if we expect this entry to be a single test, or if multiple tests are possible
per course. per course.
Returns None if no testcenter info specified. For now we expect this entry to be a single test.
Returns None if no testcenter info specified, or if no exam is included.
""" """
return self.metadata.get('testcenter_info') info = self.metadata.get('testcenter_info')
if info is None or len(info) == 0:
return None;
else:
return info.values()[0]
@property @property
def title(self): def title(self):
return self.display_name return self.display_name
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
from courseware.courses import course_image_url, get_course_about_section from courseware.courses import course_image_url, get_course_about_section
from courseware.access import has_access from courseware.access import has_access
from certificates.models import CertificateStatuses from certificates.models import CertificateStatuses
from student.models import get_testcenter_registrations_for_user_and_course
%> %>
<%inherit file="main.html" /> <%inherit file="main.html" />
...@@ -222,20 +223,26 @@ ...@@ -222,20 +223,26 @@
<!-- TODO: need to add logic to select which of the following to display. Like certs? --> <!-- TODO: need to add logic to select which of the following to display. Like certs? -->
<% <%
testcenter_info = course.testcenter_info testcenter_info = course.testcenter_info
testcenter_register_target = reverse('begin_test_registration', args=[course.id])
%> %>
% if testcenter_info is not None: % if testcenter_info is not None:
<% <!-- see if there is already a registration object
testcenter_register_target = reverse('begin_test_registration', args=[course.id]) TODO: need to add logic for when registration can begin. -->
%> <%
registrations = get_testcenter_registrations_for_user_and_course(user, course.id)
%>
% if len(registrations) == 0:
<div class="message message-status is-shown exam-register"> <div class="message message-status is-shown exam-register">
<!-- <a href="#testcenter-register-modal" rel="leanModal" class="exam-button" data-course-id="${course.id}" data-course-number="${course.number}" id="exam_register_button">Register for Pearson exam</a> <!-- <a href="#testcenter-register-modal" rel="leanModal" class="exam-button" data-course-id="${course.id}" data-course-number="${course.number}" id="exam_register_button">Register for Pearson exam</a>
--> <a href="${testcenter_register_target}" class="exam-button" id="exam_register_button">Register for Pearson exam</a> --> <a href="${testcenter_register_target}" class="exam-button" id="exam_register_button">Register for Pearson exam</a>
<p class="message-copy">Registration for the Pearson exam is now open.</p> <p class="message-copy">Registration for the Pearson exam is now open.</p>
</div> </div>
% else:
<div class="message message-status is-shown"> <div class="message message-status is-shown">
<p class="message-copy">Your registration for the Pearson exam is pending. Within a few days, you should receive a confirmation number, which can be used to schedule your exam.</p> <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> </div>
<div class="message message-status is-shown exam-schedule"> <div class="message message-status is-shown exam-schedule">
...@@ -244,6 +251,7 @@ ...@@ -244,6 +251,7 @@
<p class="exam-registration-number">Registration number: <strong>edx00015879548</strong></p> <p class="exam-registration-number">Registration number: <strong>edx00015879548</strong></p>
<p class="message-copy">Write this down! You’ll need it to schedule your exam.</p> <p class="message-copy">Write this down! You’ll need it to schedule your exam.</p>
</div> </div>
% endif
% endif % endif
......
...@@ -71,12 +71,9 @@ ...@@ -71,12 +71,9 @@
<!-- TODO: need to add logic to select which of the following to display. Like certs? --> <!-- TODO: need to add logic to select which of the following to display. Like certs? -->
<% <%
testcenter_info = course.testcenter_info exam_info = course.testcenter_info
%> %>
% if testcenter_info is not None: % if exam_info is not None:
<%
exam_info = testcenter_info.get('Final_Exam')
%>
<p>Exam Series Code: ${exam_info.get('Exam_Series_Code')}</p> <p>Exam Series Code: ${exam_info.get('Exam_Series_Code')}</p>
<p>First Eligible Appointment Date: ${exam_info.get('First_Eligible_Appointment_Date')}</p> <p>First Eligible Appointment Date: ${exam_info.get('First_Eligible_Appointment_Date')}</p>
<p>Last Eligible Appointment Date: ${exam_info.get('Last_Eligible_Appointment_Date')}</p> <p>Last Eligible Appointment Date: ${exam_info.get('Last_Eligible_Appointment_Date')}</p>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment