From da94aca522d45348dcc61d48e452487ae05c4750 Mon Sep 17 00:00:00 2001 From: zubair-arbi Date: Wed, 29 Apr 2015 16:12:03 +0500 Subject: [PATCH] add location_id field for the Reverification XBlock in VerificationStatus model ECOM-1477 --- ...dd_field_verificationstatus_location_id.py | 122 ++++++++++++++++++ lms/djangoapps/verify_student/models.py | 50 ++++--- .../verify_student/tests/test_models.py | 59 +++++++-- lms/djangoapps/verify_student/views.py | 2 +- 4 files changed, 203 insertions(+), 30 deletions(-) create mode 100644 lms/djangoapps/verify_student/migrations/0007_auto__add_field_verificationstatus_location_id.py diff --git a/lms/djangoapps/verify_student/migrations/0007_auto__add_field_verificationstatus_location_id.py b/lms/djangoapps/verify_student/migrations/0007_auto__add_field_verificationstatus_location_id.py new file mode 100644 index 0000000000..e2a14fcaeb --- /dev/null +++ b/lms/djangoapps/verify_student/migrations/0007_auto__add_field_verificationstatus_location_id.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'VerificationStatus.location_id' + db.add_column('verify_student_verificationstatus', 'location_id', + self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'VerificationStatus.location_id' + db.delete_column('verify_student_verificationstatus', 'location_id') + + + 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'}) + }, + 'reverification.midcoursereverificationwindow': { + 'Meta': {'object_name': 'MidcourseReverificationWindow'}, + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'start_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}) + }, + 'verify_student.incoursereverificationconfiguration': { + 'Meta': {'object_name': 'InCourseReverificationConfiguration'}, + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'verify_student.skippedreverification': { + 'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'SkippedReverification'}, + 'checkpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'skipped_checkpoint'", 'to': "orm['verify_student.VerificationCheckpoint']"}), + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'verify_student.softwaresecurephotoverification': { + 'Meta': {'ordering': "['-created_at']", 'object_name': 'SoftwareSecurePhotoVerification'}, + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'display': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'error_code': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'error_msg': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'face_image_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'photo_id_image_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}), + 'photo_id_key': ('django.db.models.fields.TextField', [], {'max_length': '1024'}), + 'receipt_id': ('django.db.models.fields.CharField', [], {'default': "'9997c000-3299-4097-a2cf-9ab35f9efdb5'", 'max_length': '255', 'db_index': 'True'}), + 'reviewing_service': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'reviewing_user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'photo_verifications_reviewed'", 'null': 'True', 'to': "orm['auth.User']"}), + 'status': ('model_utils.fields.StatusField', [], {'default': "'created'", 'max_length': '100', u'no_check_for_status': 'True'}), + 'status_changed': ('model_utils.fields.MonitorField', [], {'default': 'datetime.datetime.now', u'monitor': "u'status'"}), + 'submitted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'window': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reverification.MidcourseReverificationWindow']", 'null': 'True'}) + }, + 'verify_student.verificationcheckpoint': { + 'Meta': {'unique_together': "(('course_id', 'checkpoint_name'),)", 'object_name': 'VerificationCheckpoint'}, + 'checkpoint_name': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'photo_verification': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['verify_student.SoftwareSecurePhotoVerification']", 'symmetrical': 'False'}) + }, + 'verify_student.verificationstatus': { + 'Meta': {'object_name': 'VerificationStatus'}, + 'checkpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'checkpoint_status'", 'to': "orm['verify_student.VerificationCheckpoint']"}), + 'error': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + } + } + + complete_apps = ['verify_student'] \ No newline at end of file diff --git a/lms/djangoapps/verify_student/models.py b/lms/djangoapps/verify_student/models.py index e7f7137e72..66ea5d9e8a 100644 --- a/lms/djangoapps/verify_student/models.py +++ b/lms/djangoapps/verify_student/models.py @@ -13,33 +13,31 @@ from email.utils import formatdate import functools import json import logging +import pytz +import requests import uuid +from django.conf import settings +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist +from django.core.urlresolvers import reverse +from django.db import models +from django.utils.translation import ugettext as _, ugettext_lazy + from boto.s3.connection import S3Connection from boto.s3.key import Key -from django.core.exceptions import ObjectDoesNotExist -import pytz -import requests - -from django.conf import settings -from django.core.urlresolvers import reverse -from django.db import models -from django.contrib.auth.models import User -from django.utils.translation import ugettext as _ +from config_models.models import ConfigurationModel from model_utils.models import StatusModel from model_utils import Choices - +from reverification.models import MidcourseReverificationWindow from verify_student.ssencrypt import ( random_aes_key, encrypt_and_encode, generate_signed_message, rsa_encrypt ) - -from reverification.models import MidcourseReverificationWindow - from xmodule_django.models import CourseKeyField -log = logging.getLogger(__name__) -from config_models.models import ConfigurationModel + +log = logging.getLogger(__name__) def generateUUID(): # pylint: disable=invalid-name @@ -1018,21 +1016,30 @@ class VerificationStatus(models.Model): response = models.TextField(null=True, blank=True) error = models.TextField(null=True, blank=True) + # This field is used to save location of Reverification module in courseware + location_id = models.CharField( + null=True, + blank=True, + max_length=255, + help_text=ugettext_lazy("Usage id of Reverification XBlock.") + ) + class Meta(object): # pylint: disable=missing-docstring get_latest_by = "timestamp" @classmethod - def add_verification_status(cls, checkpoint, user, status): + def add_verification_status(cls, checkpoint, user, status, location_id=''): """ Create new verification status object Arguments: checkpoint(VerificationCheckpoint): VerificationCheckpoint object user(User): user object status(str): String representing the status from VERIFICATION_STATUS_CHOICES + location_id(str): Usage key of Reverification XBlock Returns: None """ - cls.objects.create(checkpoint=checkpoint, user=user, status=status) + cls.objects.create(checkpoint=checkpoint, user=user, status=status, location_id=location_id) @classmethod def add_status_from_checkpoints(cls, checkpoints, user, status): @@ -1046,7 +1053,14 @@ class VerificationStatus(models.Model): None """ for checkpoint in checkpoints: - cls.objects.create(checkpoint=checkpoint, user=user, status=status) + # get 'location_id' from last entry (if it exists) and add it in + # new entry + try: + location_id = cls.objects.filter(checkpoint=checkpoint).latest().location_id + except cls.DoesNotExist: + location_id = '' + + cls.objects.create(checkpoint=checkpoint, user=user, status=status, location_id=location_id) class InCourseReverificationConfiguration(ConfigurationModel): diff --git a/lms/djangoapps/verify_student/tests/test_models.py b/lms/djangoapps/verify_student/tests/test_models.py index 795f66ce4c..4e66ba20ff 100644 --- a/lms/djangoapps/verify_student/tests/test_models.py +++ b/lms/djangoapps/verify_student/tests/test_models.py @@ -675,7 +675,7 @@ class VerificationCheckpointTest(ModuleStoreTestCase): @ddt.ddt class VerificationStatusTest(ModuleStoreTestCase): - """Tests for the VerificationStatus model. """ + """ Tests for the VerificationStatus model. """ def setUp(self): super(VerificationStatusTest, self).setUp() @@ -683,33 +683,70 @@ class VerificationStatusTest(ModuleStoreTestCase): self.course = CourseFactory.create() self.check_point1 = VerificationCheckpoint.objects.create(course_id=self.course.id, checkpoint_name="midterm") self.check_point2 = VerificationCheckpoint.objects.create(course_id=self.course.id, checkpoint_name="final") + self.dummy_reverification_item_id_1 = 'i4x://{}/{}/edx-reverification-block/related_assessment_1'.format( + self.course.location.org, + self.course.location.course + ) + self.dummy_reverification_item_id_2 = 'i4x://{}/{}/edx-reverification-block/related_assessment_2'.format( + self.course.location.org, + self.course.location.course + ) @ddt.data('submitted', "approved", "denied", "error") def test_add_verification_status(self, status): - """adding verfication status using the class method.""" + """ Adding verification status using the class method. """ # adding verification status - VerificationStatus.add_verification_status(checkpoint=self.check_point1, user=self.user, status=status) + VerificationStatus.add_verification_status( + checkpoint=self.check_point1, + user=self.user, + status=status, + location_id=self.dummy_reverification_item_id_1 + ) # getting the status from db result = VerificationStatus.objects.filter(checkpoint=self.check_point1)[0] self.assertEqual(result.status, status) self.assertEqual(result.user, self.user) - @ddt.data('submitted', "approved", "denied", "error") + @ddt.data("approved", "denied", "error") def test_add_status_from_checkpoints(self, status): - """adding verfication status for checkpoints list.""" + """ Adding verification status for checkpoints list after submitting sspv. """ - # adding verification status with multiple points + # add initial verification status for checkpoints + initial_status = "submitted" + VerificationStatus.add_verification_status( + checkpoint=self.check_point1, + user=self.user, + status=initial_status, + location_id=self.dummy_reverification_item_id_1 + ) + VerificationStatus.add_verification_status( + checkpoint=self.check_point2, + user=self.user, + status=initial_status, + location_id=self.dummy_reverification_item_id_2 + ) + + # now add verification status for multiple checkpoint points VerificationStatus.add_status_from_checkpoints( checkpoints=[self.check_point1, self.check_point2], user=self.user, status=status ) - # getting the status from db. - result = VerificationStatus.objects.filter(user=self.user) - self.assertEqual(len(result), len([self.check_point1.checkpoint_name, self.check_point2.checkpoint_name])) - self.assertEqual(result[0].checkpoint.checkpoint_name, self.check_point1.checkpoint_name) - self.assertEqual(result[1].checkpoint.checkpoint_name, self.check_point2.checkpoint_name) + # test that verification status entries with new status have been added + # for both checkpoints and all entries have related 'location_id'. + result = VerificationStatus.objects.filter(user=self.user, checkpoint=self.check_point1) + self.assertEqual(len(result), len(self.check_point1.checkpoint_status.all())) + self.assertEqual( + list(result.values_list('location_id', flat=True)), + list(self.check_point1.checkpoint_status.all().values_list('location_id', flat=True)) + ) + result = VerificationStatus.objects.filter(user=self.user, checkpoint=self.check_point2) + self.assertEqual(len(result), len(self.check_point2.checkpoint_status.all())) + self.assertEqual( + list(result.values_list('location_id', flat=True)), + list(self.check_point2.checkpoint_status.all().values_list('location_id', flat=True)) + ) class SkippedReverificationTest(ModuleStoreTestCase): diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 60c10ac692..5d52b0c45c 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -1244,7 +1244,7 @@ class InCourseReverifyView(View): request.user, request.POST['face_image'], init_verification.photo_id_key ) checkpoint.add_verification_attempt(attempt) - VerificationStatus.add_verification_status(checkpoint, user, "submitted") + VerificationStatus.add_verification_status(checkpoint, user, "submitted", usage_id) # emit the reverification event self._track_reverification_events(