From ea8a56da0ae33db6a60982d1d37f00a551c8c8a5 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Fri, 4 Jan 2013 18:45:46 -0500 Subject: [PATCH] add id generation and validation --- ...022_add_more_fields_to_test_center_user.py | 59 +++++--- common/djangoapps/student/models.py | 137 +++++++++++++----- common/djangoapps/student/views.py | 32 ++-- lms/templates/dashboard.html | 2 +- lms/templates/test_center_register.html | 17 +-- 5 files changed, 161 insertions(+), 86 deletions(-) diff --git a/common/djangoapps/student/migrations/0022_add_more_fields_to_test_center_user.py b/common/djangoapps/student/migrations/0022_add_more_fields_to_test_center_user.py index 1bffec2213..25d20f9e0d 100644 --- a/common/djangoapps/student/migrations/0022_add_more_fields_to_test_center_user.py +++ b/common/djangoapps/student/migrations/0022_add_more_fields_to_test_center_user.py @@ -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 diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 9254dff551..84bb4f9b3a 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -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) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index de3dcde553..61a0d59c18 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -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) diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 384ffec0ac..5db8a4cd2a 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -247,7 +247,7 @@
Schedule Pearson exam -

Registration number: edx00015879548

+

Registration number: ${registrations[0].client_authorization_id}

Write this down! You’ll need it to schedule your exam.

% endif diff --git a/lms/templates/test_center_register.html b/lms/templates/test_center_register.html index da5f9f1450..7b80ebc46b 100644 --- a/lms/templates/test_center_register.html +++ b/lms/templates/test_center_register.html @@ -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(''); - } - $('#testcenter_register_error').text(json.field_errors).stop().css("display", "block"); - } - }); - })(this) @@ -262,9 +251,9 @@ % endif % else: -
  • +
  • - +
  • % endif @@ -290,7 +279,7 @@ <% regstatus = "Registration approved by Pearson" %>

    Registration Status: ${regstatus}

    -

    Registration number: edx00015879548

    +

    Registration number: ${registration.client_authorization_id}

    Write this down! You’ll need it to schedule your exam.

    Schedule Pearson exam