Commit ea8a56da by Brian Wilson

add id generation and validation

parent e32dfcf0
......@@ -8,29 +8,15 @@ from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'TestCenterUser.upload_status'
db.add_column('student_testcenteruser', 'upload_status',
self.gf('django.db.models.fields.CharField')(default='', max_length=20, blank=True),
keep_default=False)
# Adding field 'TestCenterUser.uploaded_at'
db.add_column('student_testcenteruser', 'uploaded_at',
self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=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 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'], unique=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)),
......@@ -42,8 +28,35 @@ class Migration(SchemaMigration):
))
db.send_create_signal('student', ['TestCenterRegistration'])
# 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.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.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 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')
......@@ -126,17 +139,17 @@ class Migration(SchemaMigration):
'accommodation_code': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'accommodation_request': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
'client_authorization_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}),
'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'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'}),
'testcenter_user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['student.TestCenterUser']", 'unique': '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', [], {'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': {
......@@ -146,9 +159,8 @@ class Migration(SchemaMigration):
'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', [], {'max_length': '50', 'db_index': 'True'}),
'company_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', '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'}),
'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'}),
......@@ -166,7 +178,8 @@ class Migration(SchemaMigration):
'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', [], {'max_length': '20', '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'})
},
......@@ -194,4 +207,4 @@ class Migration(SchemaMigration):
}
}
complete_apps = ['student']
complete_apps = ['student']
\ No newline at end of file
......@@ -40,6 +40,7 @@ import hashlib
import json
import logging
import uuid
from random import randint
from django.conf import settings
......@@ -47,10 +48,12 @@ 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
from django.forms import ModelForm, forms
import comment_client as cc
from django_comment_client.models import Role
from feedparser import binascii
import os
log = logging.getLogger(__name__)
......@@ -160,7 +163,7 @@ class TestCenterUser(models.Model):
candidate_id = models.IntegerField(null=True, db_index=True)
# 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(unique=True, max_length=50, db_index=True)
# Name
first_name = models.CharField(max_length=30, db_index=True)
......@@ -191,10 +194,10 @@ 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)
# Confirmation
upload_status = models.CharField(max_length=20, blank=True) # 'Error' or 'Accepted'
upload_status = models.CharField(max_length=20, blank=True, db_index=True) # 'Error' or 'Accepted'
uploaded_at = models.DateTimeField(null=True, blank=True, db_index=True)
upload_error_message = models.CharField(max_length=512, blank=True)
......@@ -217,26 +220,21 @@ class TestCenterUser(models.Model):
return False
# def update(self, dict):
# # leave user and client_candidate_id as before
# self.user_updated_at = datetime.now()
# for fieldname in TestCenterUser.user_provided_fields():
# self.__setattr__(fieldname, dict[fieldname])
# @staticmethod
# def create(user, dict):
# testcenter_user = TestCenterUser(user=user)
# testcenter_user.update(dict)
# # testcenter_user.candidate_id remains unset
# # TODO: assign an ID of our own:
# testcenter_user.client_candidate_id = 'edx' + unique_id_for_user(user) # some unique value
@staticmethod
def _generate_candidate_id():
NUM_DIGITS = 12
return u"edX%0d" % randint(1, 10**NUM_DIGITS-1) # binascii.hexlify(os.urandom(8))
@staticmethod
def create(user):
testcenter_user = TestCenterUser(user=user)
# testcenter_user.candidate_id remains unset
# assign an ID of our own:
testcenter_user.client_candidate_id = 'edx' + unique_id_for_user(user) # some unique value
cand_id = TestCenterUser._generate_candidate_id()
while TestCenterUser.objects.filter(client_candidate_id=cand_id).exists():
cand_id = TestCenterUser._generate_candidate_id()
testcenter_user.client_candidate_id = cand_id
return testcenter_user
class TestCenterUserForm(ModelForm):
class Meta:
......@@ -251,6 +249,60 @@ class TestCenterUserForm(ModelForm):
new_user.user_updated_at = datetime.now()
new_user.save()
# add validation:
@staticmethod
def can_encode_as_latin(fieldvalue):
try:
fieldvalue.encode('iso-8859-1')
except UnicodeEncodeError:
return False
return True
def check_country_code(self, fieldname):
code = self.cleaned_data[fieldname]
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_country(self):
return self.check_country_code('country')
def clean_phone_country_code(self):
return self.check_country_code('phone_country_code')
def clean_fax_country_code(self):
return self.check_country_code('fax_country_code')
def clean(self):
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 TestCenterUserForm.can_encode_as_latin(cleaned_data[fieldname]):
self._errors[fieldname] = self.error_class([u'Must only use characters in Latin-1 encoding'])
del cleaned_data[fieldname]
# Always return the full collection of cleaned data.
return cleaned_data
ACCOMODATION_CODES = (
......@@ -282,7 +334,7 @@ class TestCenterRegistration(models.Model):
# 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, default=None)
course_id = models.CharField(max_length=128, db_index=True)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
......@@ -295,7 +347,7 @@ class TestCenterRegistration(models.Model):
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)
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)
......@@ -311,7 +363,7 @@ class TestCenterRegistration(models.Model):
# Confirmation
upload_status = models.CharField(max_length=20, blank=True) # 'Error' or 'Accepted'
uploaded_at = models.DateTimeField(null=True, blank=True, db_index=True)
uploaded_at = models.DateTimeField(null=True, db_index=True)
upload_error_message = models.CharField(max_length=512, blank=True)
@property
......@@ -331,25 +383,26 @@ class TestCenterRegistration(models.Model):
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.now()
#registration.client_authorization_id = registration._create_client_authorization_id()
registration.client_authorization_id = registration._create_client_authorization_id()
return registration
@staticmethod
def _generate_authorization_id():
NUM_DIGITS = 12
return u"edX%0d" % randint(1, 10**NUM_DIGITS-1) # binascii.hexlify(os.urandom(8))
def _create_client_authorization_id(self):
"""
Return a unique id for a registration, suitable for inserting into
e.g. personalized survey links.
Return a unique id for a registration, suitable for using as an authorization code
for Pearson. It must fit within 20 characters.
"""
# include the secret key as a salt, and to make the ids unique across
# different LMS installs. Then add in (user, course, exam), which should
# be unique.
h = hashlib.md5()
h.update(settings.SECRET_KEY)
h.update(str(self.testcenter_user.user.id))
h.update(str(self.course_id))
h.update(str(self.exam_series_code))
return h.hexdigest()
# 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
def is_accepted(self):
return self.upload_status == 'Accepted'
......@@ -362,7 +415,21 @@ class TestCenterRegistration(models.Model):
def is_pending_acknowledgement(self):
return self.upload_status == '' and not self.is_pending_accommodation()
class TestCenterRegistrationForm(ModelForm):
class Meta:
model = TestCenterRegistration
fields = ( 'accommodation_request', )
def update_and_save(self):
registration = self.save(commit=False)
# create additional values here:
registration.user_updated_at = datetime.now()
registration.save()
# TODO: add validation code for values added to accommodation_code field.
def get_testcenter_registrations_for_user_and_course(user, course_id, exam_series_code=None):
try:
tcu = TestCenterUser.objects.get(user=user)
......
......@@ -18,18 +18,17 @@ from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
from django.core.context_processors import csrf
from django.core.mail import send_mail
from django.core.urlresolvers import reverse
from django.core.validators import validate_email, validate_slug, ValidationError
from django.db import IntegrityError
from django.http import HttpResponse, HttpResponseForbidden, Http404,\
HttpResponseRedirect
from django.http import HttpResponse, HttpResponseForbidden, Http404
from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_response, render_to_string
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, TestCenterUser, TestCenterUserForm, TestCenterRegistration,
from student.models import (Registration, UserProfile, TestCenterUser, TestCenterUserForm,
TestCenterRegistration, TestCenterRegistrationForm,
PendingNameChange, PendingEmailChange,
CourseEnrollment, unique_id_for_user,
get_testcenter_registrations_for_user_and_course)
......@@ -248,8 +247,6 @@ def dashboard(request):
'show_courseware_links_for' : show_courseware_links_for,
'cert_statuses': cert_statuses,
'news': top_news,
# No longer needed here...move to begin_registration
# 'testcenteruser': testcenteruser,
}
return render_to_response('dashboard.html', context)
......@@ -657,11 +654,12 @@ def create_test_registration(request, post_override=None):
try:
testcenter_user = TestCenterUser.objects.get(user=user)
needs_updating = testcenter_user.needs_update(post_vars)
except TestCenterUser.DoesNotExist:
# do additional initialization here:
testcenter_user = TestCenterUser.create(user)
needs_updating = True
needs_updating = testcenter_user.needs_update(post_vars)
# perform validation:
if needs_updating:
......@@ -692,20 +690,28 @@ def create_test_registration(request, post_override=None):
# right now.
else:
accommodation_request = post_vars.get('accommodations','')
accommodation_request = post_vars.get('accommodation_request','')
registration = TestCenterRegistration.create(testcenter_user, course_id, exam_info, accommodation_request)
needs_saving = True
# TODO: add validation of registration. (Mainly whether an accommodation request is too long.)
if needs_saving:
registration.save()
# 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 'accommodations' in post_vars and settings.MITX_FEATURES.get('ACCOMMODATION_EMAIL'):
d = {'accommodations': post_vars['accommodations'] }
if 'accommodation_request' in post_vars and settings.MITX_FEATURES.get('ACCOMMODATION_EMAIL'):
d = {'accommodation_request': post_vars['accommodation_request'] }
# composes accommodation email
subject = render_to_string('emails/accommodation_email_subject.txt', d)
......
......@@ -247,7 +247,7 @@
<div class="message message-status is-shown exam-schedule">
<!-- TODO: pull Pearson destination out into a Setting -->
<a href="https://www1.pearsonvue.com/testtaker/signin/SignInPage/EDX" class="exam-button">Schedule Pearson exam</a>
<p class="exam-registration-number"><a href="${testcenter_register_target}" id="exam_register_link">Registration</a> number: <strong>edx00015879548</strong></p>
<p class="exam-registration-number"><a href="${testcenter_register_target}" id="exam_register_link">Registration</a> number: <strong>${registrations[0].client_authorization_id}</strong></p>
<p class="message-copy">Write this down! You’ll need it to schedule your exam.</p>
</div>
% endif
......
......@@ -68,17 +68,6 @@
$("label").removeClass("is-focused");
});
$(document).delegate('#testcenter_register_form', 'ajax:success', function(data, json, xhr) {
if(json.success) {
location.href="${reverse('dashboard')}";
} else {
if($('#testcenter_register_error').length == 0) {
$('#testcenter_register_form').prepend('<div id="testcenter_register_error" class="modal-form-error"></div>');
}
$('#testcenter_register_error').text(json.field_errors).stop().css("display", "block");
}
});
})(this)
</script>
</%block>
......@@ -262,9 +251,9 @@
</li>
% endif
% else:
<li class="field">
<li data-field="accommodation_request" class="field">
<label for="accommodations">Accommodations Requested</label>
<textarea class="long" id="accommodations" name="accommodations" value="" placeholder=""></textarea>
<textarea class="long" id="accommodations" name="accommodation_request" value="" placeholder=""></textarea>
</li>
% endif
</ol>
......@@ -290,7 +279,7 @@
<% regstatus = "Registration approved by Pearson" %>
<div class="message message-status registration-accepted is-shown">
<p class="registration-status"><span class="label">Registration Status: </span><span class="value">${regstatus}</span></p>
<p class="registration-number"><span class="label">Registration number: </span> <span class="value">edx00015879548</span></p>
<p class="registration-number"><span class="label">Registration number: </span> <span class="value">${registration.client_authorization_id}</span></p>
<p class="message-copy">Write this down! You’ll need it to schedule your exam.</p>
<a href="https://www1.pearsonvue.com/testtaker/signin/SignInPage/EDX" class="button exam-button">Schedule Pearson exam</a>
</div>
......
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