diff --git a/cms/djangoapps/contentstore/tests/test_utils.py b/cms/djangoapps/contentstore/tests/test_utils.py index c5e679ccb3..cb2d9b29cf 100644 --- a/cms/djangoapps/contentstore/tests/test_utils.py +++ b/cms/djangoapps/contentstore/tests/test_utils.py @@ -535,9 +535,9 @@ class GetUserPartitionInfoTest(ModuleStoreTestCase): ), UserPartition( id=1, - name="Verification user partition", - scheme=UserPartition.get_scheme("verification"), - description="Verification user partition", + name="Completely random user partition", + scheme=UserPartition.get_scheme("random"), + description="Random user partition", groups=[ Group(id=0, name="Group C"), ], @@ -562,9 +562,9 @@ class GetUserPartitionInfoTest(ModuleStoreTestCase): ), UserPartition( id=1, - name="Verification user partition", - scheme=UserPartition.get_scheme("verification"), - description="Verification user partition", + name="Completely random user partition", + scheme=UserPartition.get_scheme("random"), + description="Random user partition", groups=[ Group(id=0, name="Group C"), ], @@ -574,7 +574,7 @@ class GetUserPartitionInfoTest(ModuleStoreTestCase): # Expect that the partition with no groups is excluded from the results partitions = self._get_partition_info() self.assertEqual(len(partitions), 1) - self.assertEqual(partitions[0]["scheme"], "verification") + self.assertEqual(partitions[0]["scheme"], "random") def _set_partitions(self, partitions): """Set the user partitions of the course descriptor. """ diff --git a/cms/djangoapps/contentstore/views/tests/test_item.py b/cms/djangoapps/contentstore/views/tests/test_item.py index 93637793b9..0d923de16c 100644 --- a/cms/djangoapps/contentstore/views/tests/test_item.py +++ b/cms/djangoapps/contentstore/views/tests/test_item.py @@ -342,9 +342,9 @@ class GetItemTest(ItemTest): self.course.user_partitions = [ UserPartition( id=0, - name="Verification user partition", - scheme=UserPartition.get_scheme("verification"), - description="Verification user partition", + name="Random user partition", + scheme=UserPartition.get_scheme("random"), + description="Random user partition", groups=[ Group(id=0, name="Group A"), Group(id=1, name="Group B"), @@ -364,8 +364,8 @@ class GetItemTest(ItemTest): self.assertEqual(result["user_partitions"], [ { "id": 0, - "name": "Verification user partition", - "scheme": "verification", + "name": "Random user partition", + "scheme": "random", "groups": [ { "id": 0, diff --git a/cms/lib/xblock/test/test_authoring_mixin.py b/cms/lib/xblock/test/test_authoring_mixin.py index f9eb62ff5c..26c41dc480 100644 --- a/cms/lib/xblock/test/test_authoring_mixin.py +++ b/cms/lib/xblock/test/test_authoring_mixin.py @@ -56,26 +56,6 @@ class AuthoringMixinTestCase(ModuleStoreTestCase): self.course.user_partitions = [self.content_partition] self.store.update_item(self.course, self.user.id) - def create_verification_user_partitions(self, checkpoint_names): - """ - Create user partitions for verification checkpoints. - """ - scheme = UserPartition.get_scheme("verification") - self.course.user_partitions = [ - UserPartition( - id=0, - name=checkpoint_name, - description="Verification checkpoint", - scheme=scheme, - groups=[ - Group(scheme.ALLOW, "Completed verification at {}".format(checkpoint_name)), - Group(scheme.DENY, "Did not complete verification at {}".format(checkpoint_name)), - ], - ) - for checkpoint_name in checkpoint_names - ] - self.store.update_item(self.course, self.user.id) - def set_staff_only(self, item_location): """Make an item visible to staff only.""" item = self.store.get_item(item_location) @@ -149,14 +129,3 @@ class AuthoringMixinTestCase(ModuleStoreTestCase): 'Content group no longer exists.' ] ) - - def test_html_verification_checkpoints(self): - self.create_verification_user_partitions(["Midterm A", "Midterm B"]) - self.verify_visibility_view_contains( - self.video_location, - [ - "Verification Checkpoint", - "Midterm A", - "Midterm B", - ] - ) diff --git a/cms/templates/visibility_editor.html b/cms/templates/visibility_editor.html index 1344901a2c..73ae87a8ad 100644 --- a/cms/templates/visibility_editor.html +++ b/cms/templates/visibility_editor.html @@ -1,12 +1,10 @@ <% from django.utils.translation import ugettext as _ -from openedx.core.djangoapps.credit.partition_schemes import VerificationPartitionScheme from contentstore.utils import ancestor_has_staff_lock, get_visibility_partition_info partition_info = get_visibility_partition_info(xblock) user_partitions = partition_info["user_partitions"] cohort_partitions = partition_info["cohort_partitions"] -verification_partitions = partition_info["verification_partitions"] has_selected_groups = partition_info["has_selected_groups"] selected_verified_partition_id = partition_info["selected_verified_partition_id"] @@ -92,42 +90,6 @@ is_staff_locked = ancestor_has_staff_lock(xblock) % endfor % endfor - ## Allow only one verification checkpoint to be selected at a time. - % if verification_partitions: -
-
${_('Verification Checkpoint')}
-
- - - - - - - -
- ${_("Learners who require verification must pass the selected checkpoint to see the content in this component. Learners who do not require verification see this content by default.")} -
-
-
- % endif diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index d541be7dca..0cef0cbfd2 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -48,7 +48,7 @@ from lms.djangoapps.grades.signals.signals import SCORE_PUBLISHED from lms.djangoapps.lms_xblock.field_data import LmsFieldData from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig from lms.djangoapps.lms_xblock.runtime import LmsModuleSystem -from lms.djangoapps.verify_student.services import VerificationService, ReverificationService +from lms.djangoapps.verify_student.services import VerificationService from openedx.core.djangoapps.bookmarks.services import BookmarksService from openedx.core.djangoapps.crawlers.models import CrawlersConfig from openedx.core.djangoapps.credit.services import CreditService @@ -679,7 +679,6 @@ def get_module_system_for_user(user, student_data, # TODO # pylint: disable=to 'field-data': field_data, 'user': DjangoXBlockUserService(user, user_is_staff=user_is_staff), 'verification': VerificationService(), - 'reverification': ReverificationService(), 'proctoring': ProctoringService(), 'milestones': milestones_helpers.get_service(), 'credit': CreditService(), diff --git a/lms/djangoapps/grades/tests/test_tasks.py b/lms/djangoapps/grades/tests/test_tasks.py index bb1c99b6fa..51ade280b3 100644 --- a/lms/djangoapps/grades/tests/test_tasks.py +++ b/lms/djangoapps/grades/tests/test_tasks.py @@ -145,8 +145,8 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase): @ddt.data( (ModuleStoreEnum.Type.mongo, 1, 24, True), (ModuleStoreEnum.Type.mongo, 1, 21, False), - (ModuleStoreEnum.Type.split, 3, 23, True), - (ModuleStoreEnum.Type.split, 3, 20, False), + (ModuleStoreEnum.Type.split, 3, 24, True), + (ModuleStoreEnum.Type.split, 3, 21, False), ) @ddt.unpack def test_query_counts(self, default_store, num_mongo_calls, num_sql_calls, create_multiple_subsections): @@ -159,7 +159,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase): @ddt.data( (ModuleStoreEnum.Type.mongo, 1, 24), - (ModuleStoreEnum.Type.split, 3, 23), + (ModuleStoreEnum.Type.split, 3, 24), ) @ddt.unpack def test_query_counts_dont_change_with_more_content(self, default_store, num_mongo_calls, num_sql_calls): diff --git a/lms/djangoapps/verify_student/admin.py b/lms/djangoapps/verify_student/admin.py index 72b4756213..a454712e0d 100644 --- a/lms/djangoapps/verify_student/admin.py +++ b/lms/djangoapps/verify_student/admin.py @@ -6,12 +6,7 @@ Admin site configurations for verify_student. from config_models.admin import ConfigurationModelAdmin from ratelimitbackend import admin -from lms.djangoapps.verify_student.models import ( - IcrvStatusEmailsConfiguration, - SkippedReverification, - SoftwareSecurePhotoVerification, - VerificationStatus, -) +from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification @admin.register(SoftwareSecurePhotoVerification) @@ -22,42 +17,3 @@ class SoftwareSecurePhotoVerificationAdmin(admin.ModelAdmin): list_display = ('id', 'user', 'status', 'receipt_id', 'submitted_at', 'updated_at',) raw_id_fields = ('user', 'reviewing_user', 'copy_id_photo_from',) search_fields = ('receipt_id', 'user__username',) - - -@admin.register(VerificationStatus) -class VerificationStatusAdmin(admin.ModelAdmin): - """ - Admin for the VerificationStatus table. - """ - list_display = ('timestamp', 'user', 'status', 'checkpoint') - readonly_fields = () - search_fields = ('checkpoint__checkpoint_location', 'user__username') - raw_id_fields = ('user',) - - def get_readonly_fields(self, request, obj=None): - """When editing an existing record, all fields should be read-only. - - VerificationStatus records should be immutable; to change the user's - status, create a new record with the updated status and a more - recent timestamp. - - """ - if obj: - return self.readonly_fields + ('status', 'checkpoint', 'user', 'response', 'error') - return self.readonly_fields - - -@admin.register(SkippedReverification) -class SkippedReverificationAdmin(admin.ModelAdmin): - """Admin for the SkippedReverification table. """ - list_display = ('created_at', 'user', 'course_id', 'checkpoint') - raw_id_fields = ('user',) - readonly_fields = ('user', 'course_id') - search_fields = ('user__username', 'course_id', 'checkpoint__checkpoint_location') - - def has_add_permission(self, request): - """Skipped verifications can't be created in Django admin. """ - return False - - -admin.site.register(IcrvStatusEmailsConfiguration, ConfigurationModelAdmin) diff --git a/lms/djangoapps/verify_student/models.py b/lms/djangoapps/verify_student/models.py index db4325cf2d..02e953389f 100644 --- a/lms/djangoapps/verify_student/models.py +++ b/lms/djangoapps/verify_student/models.py @@ -18,17 +18,14 @@ from email.utils import formatdate import pytz import requests import uuid -from lazy import lazy -from opaque_keys.edx.keys import UsageKey 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.core.cache import cache from django.core.files.base import ContentFile from django.dispatch import receiver -from django.db import models, transaction +from django.db import models from django.utils.functional import cached_property from django.utils.translation import ugettext as _, ugettext_lazy @@ -42,10 +39,9 @@ from lms.djangoapps.verify_student.ssencrypt import ( random_aes_key, encrypt_and_encode, generate_signed_message, rsa_encrypt ) -from xmodule.modulestore.django import modulestore -from xmodule.modulestore.exceptions import ItemNotFoundError from openedx.core.djangoapps.xmodule_django.models import CourseKeyField from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers +from openedx.core.djangolib.model_mixins import DeprecatedModelMixin log = logging.getLogger(__name__) @@ -1103,12 +1099,9 @@ def invalidate_deadline_caches(sender, **kwargs): # pylint: disable=unused-argu cache.delete(VerificationDeadline.ALL_DEADLINES_CACHE_KEY) -class VerificationCheckpoint(models.Model): - """Represents a point at which a user is asked to re-verify his/her - identity. - - Each checkpoint is uniquely identified by a - (course_id, checkpoint_location) tuple. +class VerificationCheckpoint(DeprecatedModelMixin, models.Model): # pylint: disable=model-missing-unicode + """ + DEPRECATED - do not use. To be removed in a future Open edX release (Hawthorn). """ course_id = CourseKeyField(max_length=255, db_index=True) checkpoint_location = models.CharField(max_length=255) @@ -1118,86 +1111,10 @@ class VerificationCheckpoint(models.Model): app_label = "verify_student" unique_together = ('course_id', 'checkpoint_location') - def __unicode__(self): - """ - Unicode representation of the checkpoint. - """ - return u"{checkpoint} in {course}".format( - checkpoint=self.checkpoint_name, - course=self.course_id - ) - @lazy - def checkpoint_name(self): - """Lazy method for getting checkpoint name of reverification block. - - Return location of the checkpoint if no related assessment found in - database. - """ - checkpoint_key = UsageKey.from_string(self.checkpoint_location) - try: - checkpoint_name = modulestore().get_item(checkpoint_key).related_assessment - except ItemNotFoundError: - log.warning( - u"Verification checkpoint block with location '%s' and course id '%s' " - u"not found in database.", self.checkpoint_location, unicode(self.course_id) - ) - checkpoint_name = self.checkpoint_location - - return checkpoint_name - - def add_verification_attempt(self, verification_attempt): - """Add the verification attempt in M2M relation of photo_verification. - - Arguments: - verification_attempt(object): SoftwareSecurePhotoVerification object - - Returns: - None - """ - self.photo_verification.add(verification_attempt) # pylint: disable=no-member - - def get_user_latest_status(self, user_id): - """Get the status of the latest checkpoint attempt of the given user. - - Args: - user_id(str): Id of user - - Returns: - VerificationStatus object if found any else None - """ - try: - return self.checkpoint_status.filter(user_id=user_id).latest() - except ObjectDoesNotExist: - return None - - @classmethod - def get_or_create_verification_checkpoint(cls, course_id, checkpoint_location): - """ - Get or create the verification checkpoint for given 'course_id' and - checkpoint name. - - Arguments: - course_id (CourseKey): CourseKey - checkpoint_location (str): Verification checkpoint location - - Raises: - IntegrityError if create fails due to concurrent create. - - Returns: - VerificationCheckpoint object if exists otherwise None - """ - with transaction.atomic(): - checkpoint, __ = cls.objects.get_or_create(course_id=course_id, checkpoint_location=checkpoint_location) - return checkpoint - - -class VerificationStatus(models.Model): - """This model is an append-only table that represents user status changes - during the verification process. - - A verification status represents a user’s progress through the verification - process for a particular checkpoint. +class VerificationStatus(DeprecatedModelMixin, models.Model): # pylint: disable=model-missing-unicode + """ + DEPRECATED - do not use. To be removed in a future Open edX release (Hawthorn). """ SUBMITTED_STATUS = "submitted" APPROVED_STATUS = "approved" @@ -1224,172 +1141,24 @@ class VerificationStatus(models.Model): verbose_name = "Verification Status" verbose_name_plural = "Verification Statuses" - @classmethod - def add_verification_status(cls, checkpoint, user, status): - """Create new verification status object. - Arguments: - checkpoint(VerificationCheckpoint): VerificationCheckpoint object - user(User): user object - status(str): Status from VERIFICATION_STATUS_CHOICES - - Returns: - None - """ - cls.objects.create(checkpoint=checkpoint, user=user, status=status) - - @classmethod - def add_status_from_checkpoints(cls, checkpoints, user, status): - """Create new verification status objects for a user against the given - checkpoints. - - Arguments: - checkpoints(list): list of VerificationCheckpoint objects - user(User): user object - status(str): Status from VERIFICATION_STATUS_CHOICES - - Returns: - None - """ - for checkpoint in checkpoints: - cls.objects.create(checkpoint=checkpoint, user=user, status=status) - - @classmethod - def get_user_status_at_checkpoint(cls, user, course_key, location): - """ - Get the user's latest status at the checkpoint. - - Arguments: - user (User): The user whose status we are retrieving. - course_key (CourseKey): The identifier for the course. - location (UsageKey): The location of the checkpoint in the course. - - Returns: - unicode or None - - """ - try: - return cls.objects.filter( - user=user, - checkpoint__course_id=course_key, - checkpoint__checkpoint_location=unicode(location), - ).latest().status - except cls.DoesNotExist: - return None - - @classmethod - def get_user_attempts(cls, user_id, course_key, checkpoint_location): - """ - Get re-verification attempts against a user for a given 'checkpoint' - and 'course_id'. - - Arguments: - user_id (str): User Id string - course_key (str): A CourseKey of a course - checkpoint_location (str): Verification checkpoint location - - Returns: - Count of re-verification attempts - """ - - return cls.objects.filter( - user_id=user_id, - checkpoint__course_id=course_key, - checkpoint__checkpoint_location=checkpoint_location, - status=cls.SUBMITTED_STATUS - ).count() - - @classmethod - def get_location_id(cls, photo_verification): - """Get the location ID of reverification XBlock. - - Args: - photo_verification(object): SoftwareSecurePhotoVerification object - - Return: - Location Id of XBlock if any else empty string - """ - try: - verification_status = cls.objects.filter(checkpoint__photo_verification=photo_verification).latest() - return verification_status.checkpoint.checkpoint_location - except cls.DoesNotExist: - return "" - - @classmethod - def get_all_checkpoints(cls, user_id, course_key): - """Return dict of all the checkpoints with their status. - Args: - user_id(int): Id of user. - course_key(unicode): Unicode of course key - - Returns: - dict: {checkpoint:status} - """ - all_checks_points = cls.objects.filter( - user_id=user_id, checkpoint__course_id=course_key - ) - check_points = {} - for check in all_checks_points: - check_points[check.checkpoint.checkpoint_location] = check.status - - return check_points - - @classmethod - def cache_key_name(cls, user_id, course_key): - """Return the name of the key to use to cache the current configuration - Args: - user_id(int): Id of user. - course_key(unicode): Unicode of course key - - Returns: - Unicode cache key - """ - return u"verification.{}.{}".format(user_id, unicode(course_key)) - - -@receiver(models.signals.post_save, sender=VerificationStatus) -@receiver(models.signals.post_delete, sender=VerificationStatus) -def invalidate_verification_status_cache(sender, instance, **kwargs): # pylint: disable=unused-argument, invalid-name - """Invalidate the cache of VerificationStatus model. """ - - cache_key = VerificationStatus.cache_key_name( - instance.user.id, - unicode(instance.checkpoint.course_id) - ) - cache.delete(cache_key) - - -# DEPRECATED: this feature has been permanently enabled. -# Once the application code has been updated in production, -# this table can be safely deleted. -class InCourseReverificationConfiguration(ConfigurationModel): - """Configure in-course re-verification. - - Enable or disable in-course re-verification feature. - When this flag is disabled, the "in-course re-verification" feature - will be disabled. - - When the flag is enabled, the "in-course re-verification" feature - will be enabled. +class InCourseReverificationConfiguration(DeprecatedModelMixin, ConfigurationModel): # pylint: disable=model-missing-unicode + """ + DEPRECATED - do not use. To be removed in a future Open edX release (Hawthorn). """ pass -class IcrvStatusEmailsConfiguration(ConfigurationModel): - """Toggle in-course reverification (ICRV) status emails - - Disabled by default. When disabled, ICRV status emails will not be sent. - When enabled, ICRV status emails are sent. +class IcrvStatusEmailsConfiguration(DeprecatedModelMixin, ConfigurationModel): # pylint: disable=model-missing-unicode + """ + DEPRECATED - do not use. To be removed in a future Open edX release (Hawthorn). """ pass -class SkippedReverification(models.Model): - """Model for tracking skipped Reverification of a user against a specific - course. - - If a user skipped a Reverification checkpoint for a specific course then in - future that user cannot see the reverification link. +class SkippedReverification(DeprecatedModelMixin, models.Model): # pylint: disable=model-missing-unicode + """ + DEPRECATED - do not use. To be removed in a future Open edX release (Hawthorn). """ user = models.ForeignKey(User) course_id = CourseKeyField(max_length=255, db_index=True) @@ -1399,57 +1168,3 @@ class SkippedReverification(models.Model): class Meta(object): app_label = "verify_student" unique_together = (('user', 'course_id'),) - - @classmethod - @transaction.atomic - def add_skipped_reverification_attempt(cls, checkpoint, user_id, course_id): - """Create skipped reverification object. - - Arguments: - checkpoint(VerificationCheckpoint): VerificationCheckpoint object - user_id(str): User Id of currently logged in user - course_id(CourseKey): CourseKey - - Returns: - None - """ - cls.objects.create(checkpoint=checkpoint, user_id=user_id, course_id=course_id) - - @classmethod - def check_user_skipped_reverification_exists(cls, user_id, course_id): - """Check existence of a user's skipped re-verification attempt for a - specific course. - - Arguments: - user_id(str): user id - course_id(CourseKey): CourseKey - - Returns: - Boolean - """ - has_skipped = cls.objects.filter(user_id=user_id, course_id=course_id).exists() - return has_skipped - - @classmethod - def cache_key_name(cls, user_id, course_key): - """Return the name of the key to use to cache the current configuration - Arguments: - user(User): user object - course_key(CourseKey): CourseKey - - Returns: - string: cache key name - """ - return u"skipped_reverification.{}.{}".format(user_id, unicode(course_key)) - - -@receiver(models.signals.post_save, sender=SkippedReverification) -@receiver(models.signals.post_delete, sender=SkippedReverification) -def invalidate_skipped_verification_cache(sender, instance, **kwargs): # pylint: disable=unused-argument, invalid-name - """Invalidate the cache of skipped verification model. """ - - cache_key = SkippedReverification.cache_key_name( - instance.user.id, - unicode(instance.course_id) - ) - cache.delete(cache_key) diff --git a/lms/djangoapps/verify_student/services.py b/lms/djangoapps/verify_student/services.py index 8cdb8bb5a0..680ab4f8a4 100644 --- a/lms/djangoapps/verify_student/services.py +++ b/lms/djangoapps/verify_student/services.py @@ -11,7 +11,6 @@ from django.db import IntegrityError from opaque_keys.edx.keys import CourseKey from student.models import User, CourseEnrollment -from lms.djangoapps.verify_student.models import VerificationCheckpoint, VerificationStatus, SkippedReverification from .models import SoftwareSecurePhotoVerification @@ -47,124 +46,3 @@ class VerificationService(object): Returns the URL for a user to verify themselves. """ return reverse('verify_student_reverify') - - -class ReverificationService(object): - """ - Reverification XBlock service - """ - - SKIPPED_STATUS = "skipped" - NON_VERIFIED_TRACK = "not-verified" - - def get_status(self, user_id, course_id, related_assessment_location): - """Get verification attempt status against a user for a given - 'checkpoint' and 'course_id'. - - Args: - user_id (str): User Id string - course_id (str): A string of course id - related_assessment_location (str): Location of Reverification XBlock - - Returns: str or None - """ - user = User.objects.get(id=user_id) - course_key = CourseKey.from_string(course_id) - - if not CourseEnrollment.is_enrolled_as_verified(user, course_key): - return self.NON_VERIFIED_TRACK - elif SkippedReverification.check_user_skipped_reverification_exists(user_id, course_key): - return self.SKIPPED_STATUS - - try: - checkpoint_status = VerificationStatus.objects.filter( - user_id=user_id, - checkpoint__course_id=course_key, - checkpoint__checkpoint_location=related_assessment_location - ).latest() - return checkpoint_status.status - except ObjectDoesNotExist: - return None - - def start_verification(self, course_id, related_assessment_location): - """Create re-verification link against a verification checkpoint. - - Args: - course_id(str): A string of course id - related_assessment_location(str): Location of Reverification XBlock - - Returns: - Re-verification link - """ - course_key = CourseKey.from_string(course_id) - - # Get-or-create the verification checkpoint - VerificationCheckpoint.get_or_create_verification_checkpoint(course_key, related_assessment_location) - - re_verification_link = reverse( - 'verify_student_incourse_reverify', - args=( - unicode(course_key), - unicode(related_assessment_location) - ) - ) - return re_verification_link - - def skip_verification(self, user_id, course_id, related_assessment_location): - """Add skipped verification attempt entry for a user against a given - 'checkpoint'. - - Args: - user_id(str): User Id string - course_id(str): A string of course_id - related_assessment_location(str): Location of Reverification XBlock - - Returns: - None - """ - course_key = CourseKey.from_string(course_id) - checkpoint = VerificationCheckpoint.objects.get( - course_id=course_key, - checkpoint_location=related_assessment_location - ) - user = User.objects.get(id=user_id) - - # user can skip a reverification attempt only if that user has not already - # skipped an attempt - try: - SkippedReverification.add_skipped_reverification_attempt(checkpoint, user_id, course_key) - except IntegrityError: - log.exception("Skipped attempt already exists for user %s: with course %s:", user_id, unicode(course_id)) - return - - try: - # Avoid circular import - from openedx.core.djangoapps.credit.api import set_credit_requirement_status - - # As a user skips the reverification it declines to fulfill the requirement so - # requirement sets to declined. - set_credit_requirement_status( - user, - course_key, - 'reverification', - checkpoint.checkpoint_location, - status='declined' - ) - - except Exception as err: # pylint: disable=broad-except - log.error("Unable to add credit requirement status for user with id %d: %s", user_id, err) - - def get_attempts(self, user_id, course_id, related_assessment_location): - """Get re-verification attempts against a user for a given 'checkpoint' - and 'course_id'. - - Args: - user_id(str): User Id string - course_id(str): A string of course id - related_assessment_location(str): Location of Reverification XBlock - - Returns: - Number of re-verification attempts of a user - """ - course_key = CourseKey.from_string(course_id) - return VerificationStatus.get_user_attempts(user_id, course_key, related_assessment_location) diff --git a/lms/djangoapps/verify_student/tests/test_models.py b/lms/djangoapps/verify_student/tests/test_models.py index a7f0350097..b49e31f8f6 100644 --- a/lms/djangoapps/verify_student/tests/test_models.py +++ b/lms/djangoapps/verify_student/tests/test_models.py @@ -5,7 +5,6 @@ import json import boto import ddt from django.conf import settings -from django.db import IntegrityError from freezegun import freeze_time import mock from mock import patch @@ -24,9 +23,7 @@ from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from lms.djangoapps.verify_student.models import ( SoftwareSecurePhotoVerification, - VerificationException, VerificationCheckpoint, - VerificationStatus, SkippedReverification, - VerificationDeadline + VerificationException, VerificationDeadline ) @@ -522,308 +519,6 @@ class TestPhotoVerification(MockS3Mixin, ModuleStoreTestCase): self.assertEqual(fourth_result, first_result) -@ddt.ddt -class VerificationCheckpointTest(ModuleStoreTestCase): - """Tests for the VerificationCheckpoint model. """ - - def setUp(self): - super(VerificationCheckpointTest, self).setUp() - self.user = UserFactory.create() - self.course = CourseFactory.create() - self.checkpoint_midterm = u'i4x://{org}/{course}/edx-reverification-block/midterm_uuid'.format( - org=self.course.id.org, course=self.course.id.course - ) - self.checkpoint_final = u'i4x://{org}/{course}/edx-reverification-block/final_uuid'.format( - org=self.course.id.org, course=self.course.id.course - ) - - @ddt.data('midterm', 'final') - def test_get_or_create_verification_checkpoint(self, checkpoint): - """ - Test that a reverification checkpoint is created properly. - """ - checkpoint_location = u'i4x://{org}/{course}/edx-reverification-block/{checkpoint}'.format( - org=self.course.id.org, course=self.course.id.course, checkpoint=checkpoint - ) - # create the 'VerificationCheckpoint' checkpoint - verification_checkpoint = VerificationCheckpoint.objects.create( - course_id=self.course.id, - checkpoint_location=checkpoint_location - ) - self.assertEqual( - VerificationCheckpoint.get_or_create_verification_checkpoint(self.course.id, checkpoint_location), - verification_checkpoint - ) - - def test_get_or_create_verification_checkpoint_for_not_existing_values(self): - # Retrieving a checkpoint that doesn't yet exist will create it - location = u'i4x://edX/DemoX/edx-reverification-block/invalid_location' - checkpoint = VerificationCheckpoint.get_or_create_verification_checkpoint(self.course.id, location) - - self.assertIsNot(checkpoint, None) - self.assertEqual(checkpoint.course_id, self.course.id) - self.assertEqual(checkpoint.checkpoint_location, location) - - def test_get_or_create_integrity_error(self): - # Create the checkpoint - VerificationCheckpoint.objects.create( - course_id=self.course.id, - checkpoint_location=self.checkpoint_midterm, - ) - - # Simulate that the get-or-create operation raises an IntegrityError. - # This can happen when two processes both try to get-or-create at the same time - # when the database is set to REPEATABLE READ. - # To avoid IntegrityError situations when calling this method, set the view to - # use a READ COMMITTED transaction instead. - with patch.object(VerificationCheckpoint.objects, "get_or_create") as mock_get_or_create: - mock_get_or_create.side_effect = IntegrityError - with self.assertRaises(IntegrityError): - _ = VerificationCheckpoint.get_or_create_verification_checkpoint( - self.course.id, - self.checkpoint_midterm - ) - - def test_unique_together_constraint(self): - """ - Test the unique together constraint. - """ - # create the VerificationCheckpoint checkpoint - VerificationCheckpoint.objects.create(course_id=self.course.id, checkpoint_location=self.checkpoint_midterm) - - # test creating the VerificationCheckpoint checkpoint with same course - # id and checkpoint name - with self.assertRaises(IntegrityError): - VerificationCheckpoint.objects.create(course_id=self.course.id, checkpoint_location=self.checkpoint_midterm) - - def test_add_verification_attempt_software_secure(self): - """ - Test adding Software Secure photo verification attempts for the - reverification checkpoints. - """ - # adding two check points. - first_checkpoint = VerificationCheckpoint.objects.create( - course_id=self.course.id, checkpoint_location=self.checkpoint_midterm - ) - second_checkpoint = VerificationCheckpoint.objects.create( - course_id=self.course.id, checkpoint_location=self.checkpoint_final - ) - - # make an attempt for the 'first_checkpoint' - first_checkpoint.add_verification_attempt(SoftwareSecurePhotoVerification.objects.create(user=self.user)) - self.assertEqual(first_checkpoint.photo_verification.count(), 1) - - # make another attempt for the 'first_checkpoint' - first_checkpoint.add_verification_attempt(SoftwareSecurePhotoVerification.objects.create(user=self.user)) - self.assertEqual(first_checkpoint.photo_verification.count(), 2) - - # make new attempt for the 'second_checkpoint' - attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) - second_checkpoint.add_verification_attempt(attempt) - self.assertEqual(second_checkpoint.photo_verification.count(), 1) - - # remove the attempt from 'second_checkpoint' - second_checkpoint.photo_verification.remove(attempt) - self.assertEqual(second_checkpoint.photo_verification.count(), 0) - - -@ddt.ddt -class VerificationStatusTest(ModuleStoreTestCase): - """ Tests for the VerificationStatus model. """ - - def setUp(self): - super(VerificationStatusTest, self).setUp() - self.user = UserFactory.create() - self.course = CourseFactory.create() - - self.first_checkpoint_location = u'i4x://{org}/{course}/edx-reverification-block/first_checkpoint_uuid'.format( - org=self.course.id.org, course=self.course.id.course - ) - self.first_checkpoint = VerificationCheckpoint.objects.create( - course_id=self.course.id, - checkpoint_location=self.first_checkpoint_location - ) - - self.second_checkpoint_location = u'i4x://{org}/{course}/edx-reverification-block/second_checkpoint_uuid'.\ - format(org=self.course.id.org, course=self.course.id.course) - self.second_checkpoint = VerificationCheckpoint.objects.create( - course_id=self.course.id, - checkpoint_location=self.second_checkpoint_location - ) - - @ddt.data('submitted', "approved", "denied", "error") - def test_add_verification_status(self, status): - """ Adding verification status using the class method. """ - - # adding verification status - VerificationStatus.add_verification_status( - checkpoint=self.first_checkpoint, - user=self.user, - status=status - ) - - # test the status from database - result = VerificationStatus.objects.filter(checkpoint=self.first_checkpoint)[0] - self.assertEqual(result.status, status) - self.assertEqual(result.user, self.user) - - @ddt.data("approved", "denied", "error") - def test_add_status_from_checkpoints(self, status): - """Test verification status for reverification checkpoints after - submitting software secure photo verification. - """ - - # add initial verification status for checkpoints - initial_status = "submitted" - VerificationStatus.add_verification_status( - checkpoint=self.first_checkpoint, - user=self.user, - status=initial_status - ) - VerificationStatus.add_verification_status( - checkpoint=self.second_checkpoint, - user=self.user, - status=initial_status - ) - - # now add verification status for multiple checkpoint points - VerificationStatus.add_status_from_checkpoints( - checkpoints=[self.first_checkpoint, self.second_checkpoint], user=self.user, status=status - ) - - # test that verification status entries with new status have been added - # for both checkpoints - result = VerificationStatus.objects.filter(user=self.user, checkpoint=self.first_checkpoint) - self.assertEqual(len(result), len(self.first_checkpoint.checkpoint_status.all())) - self.assertEqual( - list(result.values_list('checkpoint__checkpoint_location', flat=True)), - list(self.first_checkpoint.checkpoint_status.values_list('checkpoint__checkpoint_location', flat=True)) - ) - - result = VerificationStatus.objects.filter(user=self.user, checkpoint=self.second_checkpoint) - self.assertEqual(len(result), len(self.second_checkpoint.checkpoint_status.all())) - self.assertEqual( - list(result.values_list('checkpoint__checkpoint_location', flat=True)), - list(self.second_checkpoint.checkpoint_status.values_list('checkpoint__checkpoint_location', flat=True)) - ) - - def test_get_location_id(self): - """ - Getting location id for a specific checkpoint. - """ - - # creating software secure attempt against checkpoint - self.first_checkpoint.add_verification_attempt(SoftwareSecurePhotoVerification.objects.create(user=self.user)) - - # add initial verification status for checkpoint - VerificationStatus.add_verification_status( - checkpoint=self.first_checkpoint, - user=self.user, - status='submitted', - ) - attempt = SoftwareSecurePhotoVerification.objects.filter(user=self.user) - - self.assertIsNotNone(VerificationStatus.get_location_id(attempt)) - self.assertEqual(VerificationStatus.get_location_id(None), '') - - def test_get_user_attempts(self): - """ - Test adding verification status. - """ - VerificationStatus.add_verification_status( - checkpoint=self.first_checkpoint, - user=self.user, - status='submitted' - ) - - actual_attempts = VerificationStatus.get_user_attempts( - self.user.id, - self.course.id, - self.first_checkpoint_location - ) - self.assertEqual(actual_attempts, 1) - - -class SkippedReverificationTest(ModuleStoreTestCase): - """ - Tests for the SkippedReverification model. - """ - - def setUp(self): - super(SkippedReverificationTest, self).setUp() - self.user = UserFactory.create() - self.course = CourseFactory.create() - dummy_checkpoint_location = u'i4x://edX/DemoX/edx-reverification-block/midterm_uuid' - self.checkpoint = VerificationCheckpoint.objects.create( - course_id=self.course.id, - checkpoint_location=dummy_checkpoint_location - ) - - def test_add_skipped_attempts(self): - """ - Test 'add_skipped_reverification_attempt' method. - """ - - # add verification status - SkippedReverification.add_skipped_reverification_attempt( - checkpoint=self.checkpoint, user_id=self.user.id, course_id=unicode(self.course.id) - ) - - # test the status of skipped reverification from database - result = SkippedReverification.objects.filter(course_id=self.course.id)[0] - self.assertEqual(result.checkpoint, self.checkpoint) - self.assertEqual(result.user, self.user) - self.assertEqual(result.course_id, self.course.id) - - def test_unique_constraint(self): - """Test that adding skipped re-verification with same user and course - id will raise 'IntegrityError' exception. - """ - # add verification object - SkippedReverification.add_skipped_reverification_attempt( - checkpoint=self.checkpoint, user_id=self.user.id, course_id=unicode(self.course.id) - ) - with self.assertRaises(IntegrityError): - SkippedReverification.add_skipped_reverification_attempt( - checkpoint=self.checkpoint, user_id=self.user.id, course_id=unicode(self.course.id) - ) - - # create skipped attempt for different user - user2 = UserFactory.create() - SkippedReverification.add_skipped_reverification_attempt( - checkpoint=self.checkpoint, user_id=user2.id, course_id=unicode(self.course.id) - ) - - # test the status of skipped reverification from database - result = SkippedReverification.objects.filter(user=user2)[0] - self.assertEqual(result.checkpoint, self.checkpoint) - self.assertEqual(result.user, user2) - self.assertEqual(result.course_id, self.course.id) - - def test_check_user_skipped_reverification_exists(self): - """ - Test the 'check_user_skipped_reverification_exists' method's response. - """ - # add verification status - SkippedReverification.add_skipped_reverification_attempt( - checkpoint=self.checkpoint, user_id=self.user.id, course_id=unicode(self.course.id) - ) - self.assertTrue( - SkippedReverification.check_user_skipped_reverification_exists( - user_id=self.user.id, - course_id=self.course.id - ) - ) - - user2 = UserFactory.create() - self.assertFalse( - SkippedReverification.check_user_skipped_reverification_exists( - user_id=user2.id, - course_id=self.course.id - ) - ) - - class VerificationDeadlineTest(CacheIsolationTestCase): """ Tests for the VerificationDeadline model. diff --git a/lms/djangoapps/verify_student/tests/test_services.py b/lms/djangoapps/verify_student/tests/test_services.py deleted file mode 100644 index 59c4016146..0000000000 --- a/lms/djangoapps/verify_student/tests/test_services.py +++ /dev/null @@ -1,194 +0,0 @@ -""" -Tests of re-verification service. -""" - -import ddt - -from opaque_keys.edx.keys import CourseKey - -from course_modes.models import CourseMode -from course_modes.tests.factories import CourseModeFactory -from student.models import CourseEnrollment -from student.tests.factories import UserFactory -from lms.djangoapps.verify_student.models import VerificationCheckpoint, VerificationStatus, SkippedReverification -from lms.djangoapps.verify_student.services import ReverificationService - -from openedx.core.djangoapps.credit.api import get_credit_requirement_status, set_credit_requirements -from openedx.core.djangoapps.credit.models import CreditCourse -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory - - -@ddt.ddt -class TestReverificationService(ModuleStoreTestCase): - """ - Tests for the re-verification service. - """ - - def setUp(self): - super(TestReverificationService, self).setUp() - - self.user = UserFactory.create(username="rusty", password="test") - self.course = CourseFactory.create(org='Robot', number='999', display_name='Test Course') - self.course_id = self.course.id - CourseModeFactory.create( - mode_slug="verified", - course_id=self.course_id, - min_price=100, - ) - self.course_key = CourseKey.from_string(unicode(self.course_id)) - - self.item = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') - self.final_checkpoint_location = u'i4x://{org}/{course}/edx-reverification-block/final_uuid'.format( - org=self.course_id.org, course=self.course_id.course - ) - - # Enroll in a verified mode - self.enrollment = CourseEnrollment.enroll(self.user, self.course_id, mode=CourseMode.VERIFIED) - - @ddt.data('final', 'midterm') - def test_start_verification(self, checkpoint_name): - """Test the 'start_verification' service method. - - Check that if a reverification checkpoint exists for a specific course - then 'start_verification' method returns that checkpoint otherwise it - creates that checkpoint. - """ - reverification_service = ReverificationService() - checkpoint_location = u'i4x://{org}/{course}/edx-reverification-block/{checkpoint}'.format( - org=self.course_id.org, course=self.course_id.course, checkpoint=checkpoint_name - ) - expected_url = ( - '/verify_student/reverify' - '/{course_key}' - '/{checkpoint_location}/' - ).format(course_key=unicode(self.course_id), checkpoint_location=checkpoint_location) - - self.assertEqual( - reverification_service.start_verification(unicode(self.course_id), checkpoint_location), - expected_url - ) - - def test_get_status(self): - """Test the verification statuses of a user for a given 'checkpoint' - and 'course_id'. - """ - reverification_service = ReverificationService() - self.assertIsNone( - reverification_service.get_status(self.user.id, unicode(self.course_id), self.final_checkpoint_location) - ) - - checkpoint_obj = VerificationCheckpoint.objects.create( - course_id=unicode(self.course_id), - checkpoint_location=self.final_checkpoint_location - ) - VerificationStatus.objects.create(checkpoint=checkpoint_obj, user=self.user, status='submitted') - self.assertEqual( - reverification_service.get_status(self.user.id, unicode(self.course_id), self.final_checkpoint_location), - 'submitted' - ) - - VerificationStatus.objects.create(checkpoint=checkpoint_obj, user=self.user, status='approved') - self.assertEqual( - reverification_service.get_status(self.user.id, unicode(self.course_id), self.final_checkpoint_location), - 'approved' - ) - - def test_skip_verification(self): - """ - Test adding skip attempt of a user for a reverification checkpoint. - """ - reverification_service = ReverificationService() - VerificationCheckpoint.objects.create( - course_id=unicode(self.course_id), - checkpoint_location=self.final_checkpoint_location - ) - - reverification_service.skip_verification(self.user.id, unicode(self.course_id), self.final_checkpoint_location) - self.assertEqual( - SkippedReverification.objects.filter(user=self.user, course_id=self.course_id).count(), - 1 - ) - - # now test that a user can have only one entry for a skipped - # reverification for a course - reverification_service.skip_verification(self.user.id, unicode(self.course_id), self.final_checkpoint_location) - self.assertEqual( - SkippedReverification.objects.filter(user=self.user, course_id=self.course_id).count(), - 1 - ) - - # testing service for skipped attempt. - self.assertEqual( - reverification_service.get_status(self.user.id, unicode(self.course_id), self.final_checkpoint_location), - 'skipped' - ) - - @ddt.data( - *CourseMode.CREDIT_ELIGIBLE_MODES - ) - def test_declined_verification_on_skip(self, mode): - """Test that status with value 'declined' is added in credit - requirement status model when a user skip's an ICRV. - """ - reverification_service = ReverificationService() - checkpoint = VerificationCheckpoint.objects.create( - course_id=unicode(self.course_id), - checkpoint_location=self.final_checkpoint_location - ) - # Create credit course and set credit requirements. - CreditCourse.objects.create(course_key=self.course_key, enabled=True) - self.enrollment.update_enrollment(mode=mode) - - set_credit_requirements( - self.course_key, - [ - { - "namespace": "reverification", - "name": checkpoint.checkpoint_location, - "display_name": "Assessment 1", - "criteria": {}, - } - ] - ) - - reverification_service.skip_verification(self.user.id, unicode(self.course_id), self.final_checkpoint_location) - requirement_status = get_credit_requirement_status( - self.course_key, self.user.username, 'reverification', checkpoint.checkpoint_location - ) - self.assertEqual(SkippedReverification.objects.filter(user=self.user, course_id=self.course_id).count(), 1) - self.assertEqual(len(requirement_status), 1) - self.assertEqual(requirement_status[0].get('name'), checkpoint.checkpoint_location) - self.assertEqual(requirement_status[0].get('status'), 'declined') - - def test_get_attempts(self): - """Check verification attempts count against a user for a given - 'checkpoint' and 'course_id'. - """ - reverification_service = ReverificationService() - course_id = unicode(self.course_id) - self.assertEqual( - reverification_service.get_attempts(self.user.id, course_id, self.final_checkpoint_location), - 0 - ) - - # now create a checkpoint and add user's entry against it then test - # that the 'get_attempts' service method returns correct count - checkpoint_obj = VerificationCheckpoint.objects.create( - course_id=course_id, - checkpoint_location=self.final_checkpoint_location - ) - VerificationStatus.objects.create(checkpoint=checkpoint_obj, user=self.user, status='submitted') - self.assertEqual( - reverification_service.get_attempts(self.user.id, course_id, self.final_checkpoint_location), - 1 - ) - - def test_not_in_verified_track(self): - # No longer enrolled in a verified track - self.enrollment.update_enrollment(mode=CourseMode.HONOR) - - # Should be marked as "skipped" (opted out) - service = ReverificationService() - status = service.get_status(self.user.id, unicode(self.course_id), self.final_checkpoint_location) - self.assertEqual(status, service.NON_VERIFIED_TRACK) diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py index c22e49ccf5..e545ecda03 100644 --- a/lms/djangoapps/verify_student/tests/test_views.py +++ b/lms/djangoapps/verify_student/tests/test_views.py @@ -16,7 +16,7 @@ import boto import moto import pytz from bs4 import BeautifulSoup -from mock import patch, Mock, ANY +from mock import patch, Mock import requests from django.conf import settings @@ -25,15 +25,12 @@ from django.core import mail from django.test import TestCase from django.test.client import Client, RequestFactory from django.test.utils import override_settings -from django.utils import timezone from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locator import CourseLocator -from opaque_keys.edx.keys import UsageKey from course_modes.models import CourseMode from course_modes.tests.factories import CourseModeFactory -from courseware.url_helpers import get_redirect_url from common.test.utils import XssTestMixin from commerce.models import CommerceConfiguration from commerce.tests import TEST_PAYMENT_DATA, TEST_API_URL, TEST_PUBLIC_URL_ROOT @@ -43,22 +40,17 @@ from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_t from shoppingcart.models import Order, CertificateItem from student.tests.factories import UserFactory, CourseEnrollmentFactory from student.models import CourseEnrollment -from util.date_utils import get_default_time_display from util.testing import UrlResetMixin from lms.djangoapps.verify_student.views import ( checkout_with_ecommerce_service, render_to_response, PayAndVerifyView, - _compose_message_reverification_email ) from lms.djangoapps.verify_student.models import ( VerificationDeadline, SoftwareSecurePhotoVerification, - VerificationCheckpoint, VerificationStatus, - IcrvStatusEmailsConfiguration, ) from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory +from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.django import modulestore from xmodule.modulestore import ModuleStoreEnum -from xmodule.modulestore.tests.factories import check_mongo_calls def mock_render_to_response(*args, **kwargs): @@ -1840,159 +1832,6 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase): ) self.assertIn('Result Unknown not understood', response.content) - @mock.patch( - 'lms.djangoapps.verify_student.ssencrypt.has_valid_signature', - mock.Mock(side_effect=mocked_has_valid_signature) - ) - def test_in_course_reverify_disabled(self): - """ - Test for verification passed. - """ - data = { - "EdX-ID": self.receipt_id, - "Result": "PASS", - "Reason": "", - "MessageType": "You have been verified." - } - json_data = json.dumps(data) - response = self.client.post( - reverse('verify_student_results_callback'), data=json_data, - content_type='application/json', - HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', - HTTP_DATE='testdate' - ) - attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=self.receipt_id) - self.assertEqual(attempt.status, u'approved') - self.assertEquals(response.content, 'OK!') - # Verify that photo submission confirmation email was sent - self.assertEqual(len(mail.outbox), 0) - user_status = VerificationStatus.objects.filter(user=self.user).count() - self.assertEqual(user_status, 0) - - @mock.patch( - 'lms.djangoapps.verify_student.ssencrypt.has_valid_signature', - mock.Mock(side_effect=mocked_has_valid_signature) - ) - def test_pass_in_course_reverify_result(self): - """ - Test for verification passed. - """ - # Verify that ICRV status email was sent when config is enabled - IcrvStatusEmailsConfiguration.objects.create(enabled=True) - self.create_reverification_xblock() - - data = { - "EdX-ID": self.receipt_id, - "Result": "PASS", - "Reason": "", - "MessageType": "You have been verified." - } - - json_data = json.dumps(data) - - response = self.client.post( - reverse('verify_student_results_callback'), data=json_data, - content_type='application/json', - HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', - HTTP_DATE='testdate' - ) - attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=self.receipt_id) - - self.assertEqual(attempt.status, u'approved') - self.assertEquals(response.content, 'OK!') - self.assertEqual(len(mail.outbox), 1) - self.assertEqual("Re-verification Status", mail.outbox[0].subject) - - @mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature)) - def test_icrv_status_email_with_disable_config(self): - """ - Verify that photo re-verification status email was not sent when config is disable - """ - IcrvStatusEmailsConfiguration.objects.create(enabled=False) - - self.create_reverification_xblock() - - data = { - "EdX-ID": self.receipt_id, - "Result": "PASS", - "Reason": "", - "MessageType": "You have been verified." - } - - json_data = json.dumps(data) - - response = self.client.post( - reverse('verify_student_results_callback'), data=json_data, - content_type='application/json', - HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', - HTTP_DATE='testdate' - ) - attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=self.receipt_id) - - self.assertEqual(attempt.status, u'approved') - self.assertEquals(response.content, 'OK!') - self.assertEqual(len(mail.outbox), 0) - - @mock.patch('lms.djangoapps.verify_student.views._send_email') - @mock.patch( - 'lms.djangoapps.verify_student.ssencrypt.has_valid_signature', - mock.Mock(side_effect=mocked_has_valid_signature) - ) - def test_reverification_on_callback(self, mock_send_email): - """ - Test software secure callback flow for re-verification. - """ - IcrvStatusEmailsConfiguration.objects.create(enabled=True) - - # Create the 'edx-reverification-block' in course tree - self.create_reverification_xblock() - - # create dummy data for software secure photo verification result callback - data = { - "EdX-ID": self.receipt_id, - "Result": "PASS", - "Reason": "", - "MessageType": "You have been verified." - } - json_data = json.dumps(data) - response = self.client.post( - reverse('verify_student_results_callback'), - data=json_data, - content_type='application/json', - HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', - HTTP_DATE='testdate' - ) - self.assertEqual(response.content, 'OK!') - - # now check that '_send_email' method is called on result callback - # with required parameters - subject = "Re-verification Status" - mock_send_email.assert_called_once_with(self.user.id, subject, ANY) - - def create_reverification_xblock(self): - """ - Create the reverification XBlock. - """ - # Create the 'edx-reverification-block' in course tree - section = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') - subsection = ItemFactory.create(parent=section, category='sequential', display_name='Test Subsection') - vertical = ItemFactory.create(parent=subsection, category='vertical', display_name='Test Unit') - reverification = ItemFactory.create( - parent=vertical, - category='edx-reverification-block', - display_name='Test Verification Block' - ) - - # Create checkpoint - checkpoint = VerificationCheckpoint(course_id=self.course_id, checkpoint_location=reverification.location) - checkpoint.save() - - # Add a re-verification attempt - checkpoint.add_verification_attempt(self.attempt) - - # Add a re-verification attempt status for the user - VerificationStatus.add_verification_status(checkpoint, self.user, "submitted") - @attr(shard=2) class TestReverifyView(TestCase): @@ -2104,495 +1943,3 @@ class TestReverifyView(TestCase): """ response = self._get_reverify_page() self.assertContains(response, "reverify-blocked") - - -@attr(shard=2) -class TestInCourseReverifyView(ModuleStoreTestCase): - """ - Tests for the incourse reverification views. - """ - IMAGE_DATA = "abcd,1234" - - def build_course(self): - """ - Build up a course tree with a Reverificaiton xBlock. - """ - self.course_key = SlashSeparatedCourseKey("Robot", "999", "Test_Course") - self.course = CourseFactory.create(org='Robot', number='999', display_name='Test Course') - - # Create the course modes - for mode in ('audit', 'honor', 'verified'): - min_price = 0 if mode in ["honor", "audit"] else 1 - CourseModeFactory.create(mode_slug=mode, course_id=self.course_key, min_price=min_price) - - # Create the 'edx-reverification-block' in course tree - section = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') - subsection = ItemFactory.create(parent=section, category='sequential', display_name='Test Subsection') - vertical = ItemFactory.create(parent=subsection, category='vertical', display_name='Test Unit') - self.reverification = ItemFactory.create( - parent=vertical, - category='edx-reverification-block', - display_name='Test Verification Block' - ) - self.section_location = section.location - self.subsection_location = subsection.location - self.vertical_location = vertical.location - self.reverification_location = unicode(self.reverification.location) - self.reverification_assessment = self.reverification.related_assessment - - def setUp(self): - super(TestInCourseReverifyView, self).setUp() - - self.build_course() - - self.user = UserFactory.create(username="rusty", password="test") - self.client.login(username="rusty", password="test") - - # Enroll the user in the default mode (honor) to emulate - CourseEnrollment.enroll(self.user, self.course_key, mode="verified") - - # mocking and patching for bi events - analytics_patcher = patch('lms.djangoapps.verify_student.views.analytics') - self.mock_tracker = analytics_patcher.start() - self.addCleanup(analytics_patcher.stop) - - @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) - def test_incourse_reverify_invalid_checkpoint_get(self): - # Retrieve a checkpoint that doesn't yet exist - response = self.client.get(self._get_url(self.course_key, "invalid_checkpoint")) - self.assertEqual(response.status_code, 404) - - @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) - def test_incourse_reverify_initial_redirect_get(self): - self._create_checkpoint() - response = self.client.get(self._get_url(self.course_key, self.reverification_location)) - - url = reverse('verify_student_verify_now', kwargs={"course_id": unicode(self.course_key)}) - url += u"?{params}".format(params=urllib.urlencode({"checkpoint": self.reverification_location})) - self.assertRedirects(response, url) - - @override_settings(LMS_SEGMENT_KEY="foobar") - @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) - def test_incourse_reverify_get(self): - """ - Test incourse reverification. - """ - self._create_checkpoint() - self._create_initial_verification() - - response = self.client.get(self._get_url(self.course_key, self.reverification_location)) - self.assertEquals(response.status_code, 200) - - # verify that Google Analytics event fires after successfully - # submitting the photo verification - self.mock_tracker.track.assert_called_once_with( # pylint: disable=no-member - self.user.id, - 'edx.bi.reverify.started', - { - 'category': "verification", - 'label': unicode(self.course_key), - 'checkpoint': self.reverification_assessment - }, - - context={ - 'ip': '127.0.0.1', - 'Google Analytics': - {'clientId': None} - } - ) - self.mock_tracker.reset_mock() - - @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) - def test_checkpoint_post(self): - """Verify that POST requests including an invalid checkpoint location - results in a 400 response. - """ - response = self._submit_photos(self.course_key, self.reverification_location, self.IMAGE_DATA) - self.assertEquals(response.status_code, 400) - - @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) - def test_incourse_reverify_id_required_if_no_initial_verification(self): - self._create_checkpoint() - - # Since the user has no initial verification and we're not sending the ID photo, - # we should expect a 400 bad request - response = self._submit_photos(self.course_key, self.reverification_location, self.IMAGE_DATA) - self.assertEqual(response.status_code, 400) - - @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) - def test_incourse_reverify_index_error_post(self): - self._create_checkpoint() - self._create_initial_verification() - - response = self._submit_photos(self.course_key, self.reverification_location, "") - self.assertEqual(response.status_code, 400) - - @override_settings(LMS_SEGMENT_KEY="foobar") - @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) - def test_incourse_reverify_post(self): - self._create_checkpoint() - self._create_initial_verification() - - response = self._submit_photos(self.course_key, self.reverification_location, self.IMAGE_DATA) - self.assertEqual(response.status_code, 200) - - # Check that the checkpoint status has been updated - status = VerificationStatus.get_user_status_at_checkpoint( - self.user, self.course_key, self.reverification_location - ) - self.assertEqual(status, "submitted") - - # Test that Google Analytics event fires after successfully submitting - # photo verification - self.mock_tracker.track.assert_called_once_with( # pylint: disable=no-member - self.user.id, - 'edx.bi.reverify.submitted', - { - 'category': "verification", - 'label': unicode(self.course_key), - 'checkpoint': self.reverification_assessment - }, - context={ - 'ip': '127.0.0.1', - 'Google Analytics': - {'clientId': None} - } - ) - self.mock_tracker.reset_mock() - - def _create_checkpoint(self): - """ - Helper method for creating a reverification checkpoint. - """ - checkpoint = VerificationCheckpoint(course_id=self.course_key, checkpoint_location=self.reverification_location) - checkpoint.save() - - def _create_initial_verification(self): - """ - Helper method for initial verification. - """ - attempt = SoftwareSecurePhotoVerification(user=self.user, photo_id_key="dummy_photo_id_key") - attempt.mark_ready() - attempt.save() - attempt.submit() - - def _get_url(self, course_key, checkpoint_location): - """ - Construct the reverification url. - - Arguments: - course_key (unicode): The ID of the course - checkpoint_location (str): Location of verification checkpoint - - Returns: - url - """ - return reverse( - 'verify_student_incourse_reverify', - kwargs={ - "course_id": unicode(course_key), - "usage_id": checkpoint_location - } - ) - - def _submit_photos(self, course_key, checkpoint_location, face_image_data): - """ Submit photos for verification. """ - url = reverse("verify_student_submit_photos") - data = { - "course_key": unicode(course_key), - "checkpoint": checkpoint_location, - "face_image": face_image_data, - } - return self.client.post(url, data) - - -@attr(shard=2) -class TestEmailMessageWithCustomICRVBlock(ModuleStoreTestCase): - """ - Test email sending on re-verification - """ - - def build_course(self): - """ - Build up a course tree with a Reverificaiton xBlock. - """ - self.course_key = SlashSeparatedCourseKey("Robot", "999", "Test_Course") - self.course = CourseFactory.create(org='Robot', number='999', display_name='Test Course') - self.due_date = datetime.now(pytz.UTC) + timedelta(days=20) - self.allowed_attempts = 1 - - # Create the course modes - for mode in ('audit', 'honor', 'verified'): - min_price = 0 if mode in ["honor", "audit"] else 1 - CourseModeFactory.create(mode_slug=mode, course_id=self.course_key, min_price=min_price) - - # Create the 'edx-reverification-block' in course tree - section = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') - subsection = ItemFactory.create(parent=section, category='sequential', display_name='Test Subsection') - vertical = ItemFactory.create(parent=subsection, category='vertical', display_name='Test Unit') - - self.reverification = ItemFactory.create( - parent=vertical, - category='edx-reverification-block', - display_name='Test Verification Block', - metadata={'attempts': self.allowed_attempts, 'due': self.due_date} - ) - - self.section_location = section.location - self.subsection_location = subsection.location - self.vertical_location = vertical.location - self.reverification_location = unicode(self.reverification.location) - self.assessment = self.reverification.related_assessment - - self.re_verification_link = reverse( - 'verify_student_incourse_reverify', - args=( - unicode(self.course_key), - self.reverification_location - ) - ) - - def setUp(self): - """ - Setup method for testing photo verification email messages. - """ - super(TestEmailMessageWithCustomICRVBlock, self).setUp() - self.build_course() - self.check_point = VerificationCheckpoint.objects.create( - course_id=self.course.id, checkpoint_location=self.reverification_location - ) - self.check_point.add_verification_attempt(SoftwareSecurePhotoVerification.objects.create(user=self.user)) - - VerificationStatus.add_verification_status( - checkpoint=self.check_point, - user=self.user, - status='submitted' - ) - self.attempt = SoftwareSecurePhotoVerification.objects.filter(user=self.user) - location_id = VerificationStatus.get_location_id(self.attempt) - usage_key = UsageKey.from_string(location_id) - redirect_url = get_redirect_url(self.course_key, usage_key.replace(course_key=self.course_key)) - self.request = RequestFactory().get('/url') - self.course_link = self.request.build_absolute_uri(redirect_url) - - def test_approved_email_message(self): - - subject, body = _compose_message_reverification_email( - self.course.id, self.user.id, self.reverification_location, "approved", self.request - ) - - self.assertIn( - "We have successfully verified your identity for the {assessment} " - "assessment in the {course_name} course.".format( - assessment=self.assessment, - course_name=self.course.display_name_with_default_escaped - ), - body - ) - - self.check_courseware_link_exists(body) - self.assertIn("Re-verification Status", subject) - - def test_denied_email_message_with_valid_due_date_and_attempts_allowed(self): - - subject, body = _compose_message_reverification_email( - self.course.id, self.user.id, self.reverification_location, "denied", self.request - ) - - self.assertIn( - "We could not verify your identity for the {assessment} assessment " - "in the {course_name} course. You have used " - "{used_attempts} out of {allowed_attempts} attempts to " - "verify your identity".format( - course_name=self.course.display_name_with_default_escaped, - assessment=self.assessment, - used_attempts=1, - allowed_attempts=self.allowed_attempts + 1 - ), - body - ) - - self.assertIn( - "You must verify your identity before the assessment " - "closes on {due_date}".format( - due_date=get_default_time_display(self.due_date) - ), - body - ) - reverify_link = self.request.build_absolute_uri(self.re_verification_link) - self.assertIn( - "To try to verify your identity again, select the following link:", - body - ) - - self.assertIn(reverify_link, body) - self.assertIn("Re-verification Status", subject) - - def test_denied_email_message_with_due_date_and_no_attempts(self): - """ Denied email message if due date is still open but user has no - attempts available. - """ - - VerificationStatus.add_verification_status( - checkpoint=self.check_point, - user=self.user, - status='submitted' - ) - - __, body = _compose_message_reverification_email( - self.course.id, self.user.id, self.reverification_location, "denied", self.request - ) - - self.assertIn( - "We could not verify your identity for the {assessment} assessment " - "in the {course_name} course. You have used " - "{used_attempts} out of {allowed_attempts} attempts to " - "verify your identity, and verification is no longer " - "possible".format( - course_name=self.course.display_name_with_default_escaped, - assessment=self.assessment, - used_attempts=2, - allowed_attempts=self.allowed_attempts + 1 - ), - body - ) - - self.check_courseware_link_exists(body) - - def test_denied_email_message_with_close_verification_dates(self): - # Due date given and expired - return_value = datetime.now(tz=pytz.UTC) + timedelta(days=22) - with patch.object(timezone, 'now', return_value=return_value): - __, body = _compose_message_reverification_email( - self.course.id, self.user.id, self.reverification_location, "denied", self.request - ) - - self.assertIn( - "We could not verify your identity for the {assessment} assessment " - "in the {course_name} course. You have used " - "{used_attempts} out of {allowed_attempts} attempts to " - "verify your identity, and verification is no longer " - "possible".format( - course_name=self.course.display_name_with_default_escaped, - assessment=self.assessment, - used_attempts=1, - allowed_attempts=self.allowed_attempts + 1 - ), - body - ) - - def test_check_num_queries(self): - # Get the re-verification block to check the call made - with check_mongo_calls(1): - ver_block = modulestore().get_item(self.reverification.location) - - # Expect that the verification block is fetched - self.assertIsNotNone(ver_block) - - def check_courseware_link_exists(self, body): - """Checking courseware url and signature information of EDX""" - self.assertIn( - "To go to the courseware, select the following link:", - body - ) - self.assertIn( - "{course_link}".format( - course_link=self.course_link - ), - body - ) - - self.assertIn("Thanks,", body) - self.assertIn( - u"The {platform_name} team".format( - platform_name=settings.PLATFORM_NAME - ), - body - ) - - -@attr(shard=2) -class TestEmailMessageWithDefaultICRVBlock(ModuleStoreTestCase): - """ - Test for In-course Re-verification - """ - - def build_course(self): - """ - Build up a course tree with a Reverificaiton xBlock. - """ - self.course_key = SlashSeparatedCourseKey("Robot", "999", "Test_Course") - self.course = CourseFactory.create(org='Robot', number='999', display_name='Test Course') - - # Create the course modes - for mode in ('audit', 'honor', 'verified'): - min_price = 0 if mode in ["honor", "audit"] else 1 - CourseModeFactory.create(mode_slug=mode, course_id=self.course_key, min_price=min_price) - - # Create the 'edx-reverification-block' in course tree - section = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') - subsection = ItemFactory.create(parent=section, category='sequential', display_name='Test Subsection') - vertical = ItemFactory.create(parent=subsection, category='vertical', display_name='Test Unit') - - self.reverification = ItemFactory.create( - parent=vertical, - category='edx-reverification-block', - display_name='Test Verification Block' - ) - - self.section_location = section.location - self.subsection_location = subsection.location - self.vertical_location = vertical.location - self.reverification_location = unicode(self.reverification.location) - self.assessment = self.reverification.related_assessment - - self.re_verification_link = reverse( - 'verify_student_incourse_reverify', - args=( - unicode(self.course_key), - self.reverification_location - ) - ) - - def setUp(self): - super(TestEmailMessageWithDefaultICRVBlock, self).setUp() - - self.build_course() - self.check_point = VerificationCheckpoint.objects.create( - course_id=self.course.id, checkpoint_location=self.reverification_location - ) - self.check_point.add_verification_attempt(SoftwareSecurePhotoVerification.objects.create(user=self.user)) - self.attempt = SoftwareSecurePhotoVerification.objects.filter(user=self.user) - self.request = RequestFactory().get('/url') - - def test_denied_email_message_with_no_attempt_allowed(self): - - VerificationStatus.add_verification_status( - checkpoint=self.check_point, - user=self.user, - status='submitted' - ) - - __, body = _compose_message_reverification_email( - self.course.id, self.user.id, self.reverification_location, "denied", self.request - ) - - self.assertIn( - "We could not verify your identity for the {assessment} assessment " - "in the {course_name} course. You have used " - "{used_attempts} out of {allowed_attempts} attempts to " - "verify your identity, and verification is no longer " - "possible".format( - course_name=self.course.display_name_with_default_escaped, - assessment=self.assessment, - used_attempts=1, - allowed_attempts=1 - ), - body - ) - - def test_error_on_compose_email(self): - resp = _compose_message_reverification_email( - self.course.id, self.user.id, self.reverification_location, "denied", True - ) - self.assertIsNone(resp) diff --git a/lms/djangoapps/verify_student/urls.py b/lms/djangoapps/verify_student/urls.py index e93417dc26..bee7f25573 100644 --- a/lms/djangoapps/verify_student/urls.py +++ b/lms/djangoapps/verify_student/urls.py @@ -105,18 +105,6 @@ urlpatterns = patterns( views.ReverifyView.as_view(), name="verify_student_reverify" ), - - # Endpoint for in-course reverification - # Users are sent to this end-point from within courseware - # to re-verify their identities by re-submitting face photos. - url( - r'^reverify/{course_id}/{usage_id}/$'.format( - course_id=settings.COURSE_ID_PATTERN, - usage_id=settings.USAGE_ID_PATTERN - ), - views.InCourseReverifyView.as_view(), - name="verify_student_incourse_reverify" - ), ) # Fake response page for incourse reverification ( software secure ) diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 2366c3ebfe..8ffd364067 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -6,7 +6,6 @@ import datetime import decimal import json import logging -import urllib from pytz import UTC from ipware.ip import get_ip @@ -16,9 +15,7 @@ from django.core.mail import send_mail from django.core.urlresolvers import reverse from django.db import transaction from django.http import HttpResponse, HttpResponseBadRequest, Http404 -from django.contrib.auth.models import User from django.shortcuts import redirect -from django.utils import timezone from django.utils.decorators import method_decorator from django.utils.translation import ugettext as _, ugettext_lazy from django.views.decorators.csrf import csrf_exempt @@ -28,7 +25,7 @@ from django.views.generic.base import View import analytics from eventtracking import tracker from opaque_keys import InvalidKeyError -from opaque_keys.edx.keys import CourseKey, UsageKey +from opaque_keys.edx.keys import CourseKey from commerce.utils import EcommerceService from course_modes.models import CourseMode @@ -40,7 +37,6 @@ from openedx.core.djangoapps.commerce.utils import ecommerce_api_client from openedx.core.djangoapps.user_api.accounts import NAME_MIN_LENGTH from openedx.core.djangoapps.user_api.accounts.api import update_account_settings from openedx.core.djangoapps.user_api.errors import UserNotFound, AccountValidationError -from openedx.core.djangoapps.credit.api import set_credit_requirement_status from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.lib.log_utils import audit_log from student.models import CourseEnrollment @@ -52,13 +48,9 @@ from lms.djangoapps.verify_student.ssencrypt import has_valid_signature from lms.djangoapps.verify_student.models import ( VerificationDeadline, SoftwareSecurePhotoVerification, - VerificationCheckpoint, - VerificationStatus, - IcrvStatusEmailsConfiguration, ) from lms.djangoapps.verify_student.image import decode_image_data, InvalidImageData from util.json_request import JsonResponse -from util.date_utils import get_default_time_display from util.db import outer_atomic from xmodule.modulestore.django import modulestore from django.contrib.staticfiles.storage import staticfiles_storage @@ -856,9 +848,7 @@ class SubmitPhotosView(View): """ # If the user already has an initial verification attempt, we can re-use the photo ID - # the user submitted with the initial attempt. This is useful for the in-course reverification - # case in which users submit only the face photo and have it matched against their ID photos - # submitted with the initial verification. + # the user submitted with the initial attempt. initial_verification = SoftwareSecurePhotoVerification.get_initial_verification(request.user) # Validate the POST parameters @@ -889,35 +879,9 @@ class SubmitPhotosView(View): # Submit the attempt attempt = self._submit_attempt(request.user, face_image, photo_id_image, initial_verification) - # If this attempt was submitted at a checkpoint, then associate - # the attempt with the checkpoint. - submitted_at_checkpoint = "checkpoint" in params and "course_key" in params - if submitted_at_checkpoint: - checkpoint = self._associate_attempt_with_checkpoint( - request.user, attempt, - params["course_key"], - params["checkpoint"] - ) - - # If the submission came from an in-course checkpoint - if initial_verification is not None and submitted_at_checkpoint: - self._fire_event(request.user, "edx.bi.reverify.submitted", { - "category": "verification", - "label": unicode(params["course_key"]), - "checkpoint": checkpoint.checkpoint_name, - }) - - # Send a URL that the client can redirect to in order - # to return to the checkpoint in the courseware. - redirect_url = get_redirect_url(params["course_key"], params["checkpoint"]) - return JsonResponse({"url": redirect_url}) - - # Otherwise, the submission came from an initial verification flow. - else: - self._fire_event(request.user, "edx.bi.verify.submitted", {"category": "verification"}) - self._send_confirmation_email(request.user) - redirect_url = None - return JsonResponse({}) + self._fire_event(request.user, "edx.bi.verify.submitted", {"category": "verification"}) + self._send_confirmation_email(request.user) + return JsonResponse({}) def _validate_parameters(self, request, has_initial_verification): """ @@ -938,7 +902,6 @@ class SubmitPhotosView(View): "face_image", "photo_id_image", "course_key", - "checkpoint", "full_name" ] if param_name in request.POST @@ -974,14 +937,6 @@ class SubmitPhotosView(View): except InvalidKeyError: return None, HttpResponseBadRequest(_("Invalid course key")) - if "checkpoint" in params: - try: - params["checkpoint"] = UsageKey.from_string(params["checkpoint"]).replace( - course_key=params["course_key"] - ) - except InvalidKeyError: - return None, HttpResponseBadRequest(_("Invalid checkpoint location")) - return params, None def _update_full_name(self, user, full_name): @@ -1070,24 +1025,6 @@ class SubmitPhotosView(View): return attempt - def _associate_attempt_with_checkpoint(self, user, attempt, course_key, usage_id): - """ - Associate the verification attempt with a checkpoint within a course. - - Arguments: - user (User): The user making the attempt. - attempt (SoftwareSecurePhotoVerification): The verification attempt. - course_key (CourseKey): The identifier for the course. - usage_key (UsageKey): The location of the checkpoint within the course. - - Returns: - VerificationCheckpoint - """ - checkpoint = VerificationCheckpoint.get_or_create_verification_checkpoint(course_key, usage_id) - checkpoint.add_verification_attempt(attempt) - VerificationStatus.add_verification_status(checkpoint, user, "submitted") - return checkpoint - def _send_confirmation_email(self, user): """ Send an email confirming that the user submitted photos @@ -1134,125 +1071,6 @@ class SubmitPhotosView(View): analytics.track(user.id, event_name, parameters, context=context) -def _compose_message_reverification_email( - course_key, user_id, related_assessment_location, status, request -): # pylint: disable=invalid-name - """ - Compose subject and message for photo reverification email. - - Args: - course_key(CourseKey): CourseKey object - user_id(str): User Id - related_assessment_location(str): Location of reverification XBlock - photo_verification(QuerySet): Queryset of SoftwareSecure objects - status(str): Approval status - is_secure(Bool): Is running on secure protocol or not - - Returns: - None if any error occurred else Tuple of subject and message strings - """ - try: - usage_key = UsageKey.from_string(related_assessment_location) - reverification_block = modulestore().get_item(usage_key) - - course = modulestore().get_course(course_key) - redirect_url = get_redirect_url(course_key, usage_key.replace(course_key=course_key)) - - subject = "Re-verification Status" - context = { - "status": status, - "course_name": course.display_name_with_default_escaped, - "assessment": reverification_block.related_assessment - } - - # Allowed attempts is 1 if not set on verification block - allowed_attempts = reverification_block.attempts + 1 - used_attempts = VerificationStatus.get_user_attempts(user_id, course_key, related_assessment_location) - left_attempts = allowed_attempts - used_attempts - is_attempt_allowed = left_attempts > 0 - verification_open = True - if reverification_block.due: - verification_open = timezone.now() <= reverification_block.due - - context["left_attempts"] = left_attempts - context["is_attempt_allowed"] = is_attempt_allowed - context["verification_open"] = verification_open - context["due_date"] = get_default_time_display(reverification_block.due) - - context['platform_name'] = configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME) - context["used_attempts"] = used_attempts - context["allowed_attempts"] = allowed_attempts - context["support_link"] = configuration_helpers.get_value('email_from_address', settings.CONTACT_EMAIL) - - re_verification_link = reverse( - 'verify_student_incourse_reverify', - args=( - unicode(course_key), - related_assessment_location - ) - ) - - context["course_link"] = request.build_absolute_uri(redirect_url) - context["reverify_link"] = request.build_absolute_uri(re_verification_link) - - message = render_to_string('emails/reverification_processed.txt', context) - log.info( - "Sending email to User_Id=%s. Attempts left for this user are %s. " - "Allowed attempts %s. " - "Due Date %s", - str(user_id), left_attempts, allowed_attempts, str(reverification_block.due) - ) - return subject, message - # Catch all exception to avoid raising back to view - except: # pylint: disable=bare-except - log.exception("The email for re-verification sending failed for user_id %s", user_id) - - -def _send_email(user_id, subject, message): - """ Send email to given user - - Args: - user_id(str): User Id - subject(str): Subject lines of emails - message(str): Email message body - - Returns: - None - """ - from_address = configuration_helpers.get_value( - 'email_from_address', - settings.DEFAULT_FROM_EMAIL - ) - user = User.objects.get(id=user_id) - user.email_user(subject, message, from_address) - - -def _set_user_requirement_status(attempt, namespace, status, reason=None): - """Sets the status of a credit requirement for the user, - based on a verification checkpoint. - """ - checkpoint = None - try: - checkpoint = VerificationCheckpoint.objects.get(photo_verification=attempt) - except VerificationCheckpoint.DoesNotExist: - log.error("Unable to find checkpoint for user with id %d", attempt.user.id) - - if checkpoint is not None: - try: - set_credit_requirement_status( - attempt.user, - checkpoint.course_id, - namespace, - checkpoint.checkpoint_location, - status=status, - reason=reason, - ) - except Exception: # pylint: disable=broad-except - # Catch exception if unable to add credit requirement - # status for user - log.error("Unable to add Credit requirement status for user with id %d", attempt.user.id) - - @require_POST @csrf_exempt # SS does its own message signing, and their API won't have a cookie value def results_callback(request): @@ -1310,15 +1128,11 @@ def results_callback(request): log.debug("Approving verification for %s", receipt_id) attempt.approve() status = "approved" - _set_user_requirement_status(attempt, 'reverification', 'satisfied') elif result == "FAIL": log.debug("Denying verification for %s", receipt_id) attempt.deny(json.dumps(reason), error_code=error_code) status = "denied" - _set_user_requirement_status( - attempt, 'reverification', 'failed', json.dumps(reason) - ) elif result == "SYSTEM FAIL": log.debug("System failure for %s -- resetting to must_retry", receipt_id) attempt.system_error(json.dumps(reason), error_code=error_code) @@ -1330,22 +1144,6 @@ def results_callback(request): "Result {} not understood. Known results: PASS, FAIL, SYSTEM FAIL".format(result) ) - checkpoints = VerificationCheckpoint.objects.filter(photo_verification=attempt).all() - VerificationStatus.add_status_from_checkpoints(checkpoints=checkpoints, user=attempt.user, status=status) - - # Trigger ICRV email only if ICRV status emails config is enabled - icrv_status_emails = IcrvStatusEmailsConfiguration.current() - if icrv_status_emails.enabled and checkpoints: - user_id = attempt.user.id - course_key = checkpoints[0].course_id - related_assessment_location = checkpoints[0].checkpoint_location - - subject, message = _compose_message_reverification_email( - course_key, user_id, related_assessment_location, status, request - ) - - _send_email(user_id, subject, message) - return HttpResponse("OK!") @@ -1398,130 +1196,3 @@ class ReverifyView(View): "status": status } return render_to_response("verify_student/reverify_not_allowed.html", context) - - -class InCourseReverifyView(View): - """ - The in-course reverification view. - - In-course reverification occurs while a student is taking a course. - At points in the course, students are prompted to submit face photos, - which are matched against the ID photos the user submitted during their - initial verification. - - Students are prompted to enter this flow from an "In Course Reverification" - XBlock (courseware component) that course authors add to the course. - See https://github.com/edx/edx-reverification-block for more details. - - """ - @method_decorator(login_required) - def get(self, request, course_id, usage_id): - """Display the view for face photo submission. - - Args: - request(HttpRequest): HttpRequest object - course_id(str): A string of course id - usage_id(str): Location of Reverification XBlock in courseware - - Returns: - HttpResponse - """ - user = request.user - course_key = CourseKey.from_string(course_id) - course = modulestore().get_course(course_key) - if course is None: - log.error(u"Could not find course '%s' for in-course reverification.", course_key) - raise Http404 - - try: - checkpoint = VerificationCheckpoint.objects.get(course_id=course_key, checkpoint_location=usage_id) - except VerificationCheckpoint.DoesNotExist: - log.error( - u"No verification checkpoint exists for the " - u"course '%s' and checkpoint location '%s'.", - course_key, usage_id - ) - raise Http404 - - initial_verification = SoftwareSecurePhotoVerification.get_initial_verification(user) - if not initial_verification: - return self._redirect_to_initial_verification(user, course_key, usage_id) - - # emit the reverification event - self._track_reverification_events('edx.bi.reverify.started', user.id, course_id, checkpoint.checkpoint_name) - - context = { - 'course_key': unicode(course_key), - 'course_name': course.display_name_with_default_escaped, - 'checkpoint_name': checkpoint.checkpoint_name, - 'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), - 'usage_id': usage_id, - 'capture_sound': staticfiles_storage.url("audio/camera_capture.wav"), - } - return render_to_response("verify_student/incourse_reverify.html", context) - - def _track_reverification_events(self, event_name, user_id, course_id, checkpoint): - """Track re-verification events for a user against a reverification - checkpoint of a course. - - Arguments: - event_name (str): Name of event being tracked - user_id (str): The ID of the user - course_id (unicode): ID associated with the course - checkpoint (str): Checkpoint name - - Returns: - None - """ - log.info( - u"In-course reverification: event %s occurred for user '%s' in course '%s' at checkpoint '%s'", - event_name, user_id, course_id, checkpoint - ) - - if settings.LMS_SEGMENT_KEY: - tracking_context = tracker.get_tracker().resolve_context() - analytics.track( - user_id, - event_name, - { - 'category': "verification", - 'label': unicode(course_id), - 'checkpoint': checkpoint - }, - context={ - 'ip': tracking_context.get('ip'), - 'Google Analytics': { - 'clientId': tracking_context.get('client_id') - } - } - ) - - def _redirect_to_initial_verification(self, user, course_key, checkpoint): - """ - Redirect because the user does not have an initial verification. - - We will redirect the user to the initial verification flow, - passing the identifier for this checkpoint. When the user - submits a verification attempt, it will count for *both* - the initial and checkpoint verification. - - Arguments: - user (User): The user who made the request. - course_key (CourseKey): The identifier for the course for which - the user is attempting to re-verify. - checkpoint (string): Location of the checkpoint in the courseware. - - Returns: - HttpResponse - - """ - log.info( - u"User %s does not have an initial verification, so " - u"he/she will be redirected to the \"verify later\" flow " - u"for the course %s.", - user.id, course_key - ) - base_url = reverse('verify_student_verify_now', kwargs={'course_id': unicode(course_key)}) - params = urllib.urlencode({"checkpoint": checkpoint}) - full_url = u"{base}?{params}".format(base=base_url, params=params) - return redirect(full_url) diff --git a/openedx/core/djangoapps/credit/api/eligibility.py b/openedx/core/djangoapps/credit/api/eligibility.py index 278b741bd6..6024b27e4e 100644 --- a/openedx/core/djangoapps/credit/api/eligibility.py +++ b/openedx/core/djangoapps/credit/api/eligibility.py @@ -47,12 +47,6 @@ def set_credit_requirements(course_key, requirements): >>> set_credit_requirements( "course-v1-edX-DemoX-1T2015", [ - { - "namespace": "reverification", - "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", - "display_name": "Assessment 1", - "criteria": {}, - }, { "namespace": "proctored_exam", "name": "i4x://edX/DemoX/proctoring-block/final_uuid", @@ -106,12 +100,6 @@ def get_credit_requirements(course_key, namespace=None): { requirements = [ - { - "namespace": "reverification", - "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", - "display_name": "Assessment 1", - "criteria": {}, - }, { "namespace": "proctored_exam", "name": "i4x://edX/DemoX/proctoring-block/final_uuid", @@ -216,17 +204,6 @@ def set_credit_requirement_status(user, course_key, req_namespace, req_name, sta Keyword Arguments: status (str): Status of the requirement (either "satisfied" or "failed") reason (dict): Reason of the status - - Example: - >>> set_credit_requirement_status( - "staff", - CourseKey.from_string("course-v1-edX-DemoX-1T2015"), - "reverification", - "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", - status="satisfied", - reason={} - ) - """ # Check whether user has credit eligible enrollment. enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(user, course_key) @@ -317,14 +294,6 @@ def remove_credit_requirement_status(username, course_key, req_namespace, req_na req_name (str): Name of the requirement (e.g. "grade" or the location of the ICRV XBlock) - Example: - >>> remove_credit_requirement_status( - "staff", - CourseKey.from_string("course-v1-edX-DemoX-1T2015"), - "reverification", - "i4x://edX/DemoX/edx-reverification-block/assessment_uuid". - ) - """ # Find the requirement we're trying to remove @@ -364,16 +333,6 @@ def get_credit_requirement_status(course_key, username, namespace=None, name=Non >>> get_credit_requirement_status("course-v1-edX-DemoX-1T2015", "john") [ - { - "namespace": "reverification", - "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", - "display_name": "In Course Reverification", - "criteria": {}, - "reason": {}, - "status": "failed", - "status_date": "2015-06-26 07:49:13", - "order": 0, - }, { "namespace": "proctored_exam", "name": "i4x://edX/DemoX/proctoring-block/final_uuid", diff --git a/openedx/core/djangoapps/credit/partition_schemes.py b/openedx/core/djangoapps/credit/partition_schemes.py deleted file mode 100644 index db364a1011..0000000000 --- a/openedx/core/djangoapps/credit/partition_schemes.py +++ /dev/null @@ -1,136 +0,0 @@ -""" -Partition scheme for in-course reverification. - -This is responsible for placing users into one of two groups, -ALLOW or DENY, for a partition associated with a particular -in-course reverification checkpoint. - -NOTE: This really should be defined in the verify_student app, -which owns the verification and reverification process. -It isn't defined there now because (a) we need access to this in both Studio -and the LMS, but verify_student is specific to the LMS, and -(b) in-course reverification checkpoints currently have messaging that's -specific to credit requirements. - -""" -import logging - -from django.core.cache import cache - -from lms.djangoapps.verify_student.models import SkippedReverification, VerificationStatus -from student.models import CourseEnrollment -from xmodule.partitions.partitions import NoSuchUserPartitionGroupError - - -log = logging.getLogger(__name__) - - -class VerificationPartitionScheme(object): - """ - Assign users to groups for a particular verification checkpoint. - - Users in the ALLOW group can see gated content; - users in the DENY group cannot. - """ - - DENY = 0 - ALLOW = 1 - - @classmethod - def get_group_for_user(cls, course_key, user, user_partition, **kwargs): # pylint: disable=unused-argument - """ - Return the user's group depending their enrollment and verification - status. - - Args: - course_key (CourseKey): CourseKey - user (User): user object - user_partition (UserPartition): The user partition object. - - Returns: - string of allowed access group - """ - checkpoint = user_partition.parameters['location'] - - # Retrieve all information we need to determine the user's group - # as a multi-get from the cache. - is_verified, has_skipped, has_completed = _get_user_statuses(user, course_key, checkpoint) - - # Decide whether the user should have access to content gated by this checkpoint. - # Intuitively, we allow access if the user doesn't need to do anything at the checkpoint, - # either because the user is in a non-verified track or the user has already submitted. - # - # Note that we do NOT wait the user's reverification attempt to be approved, - # since this can take some time and the user might miss an assignment deadline. - partition_group = cls.DENY - if not is_verified or has_skipped or has_completed: - partition_group = cls.ALLOW - - # Return matching user partition group if it exists - try: - return user_partition.get_group(partition_group) - except NoSuchUserPartitionGroupError: - log.error( - ( - u"Could not find group with ID %s for verified partition " - "with ID %s in course %s. The user will not be assigned a group." - ), - partition_group, - user_partition.id, - course_key - ) - return None - - -def _get_user_statuses(user, course_key, checkpoint): - """ - Retrieve all the information we need to determine the user's group. - - This will retrieve the information as a multi-get from the cache. - - Args: - user (User): User object - course_key (CourseKey): Identifier for the course. - checkpoint (unicode): Location of the checkpoint in the course (serialized usage key) - - Returns: - tuple of booleans of the form (is_verified, has_skipped, has_completed) - - """ - enrollment_cache_key = CourseEnrollment.cache_key_name(user.id, unicode(course_key)) - has_skipped_cache_key = SkippedReverification.cache_key_name(user.id, unicode(course_key)) - verification_status_cache_key = VerificationStatus.cache_key_name(user.id, unicode(course_key)) - - # Try a multi-get from the cache - cache_values = cache.get_many([ - enrollment_cache_key, - has_skipped_cache_key, - verification_status_cache_key - ]) - - # Retrieve whether the user is enrolled in a verified mode. - is_verified = cache_values.get(enrollment_cache_key) - if is_verified is None: - is_verified = CourseEnrollment.is_enrolled_as_verified(user, course_key) - cache.set(enrollment_cache_key, is_verified) - - # Retrieve whether the user has skipped any checkpoints in this course - has_skipped = cache_values.get(has_skipped_cache_key) - if has_skipped is None: - has_skipped = SkippedReverification.check_user_skipped_reverification_exists(user, course_key) - cache.set(has_skipped_cache_key, has_skipped) - - # Retrieve the user's verification status for each checkpoint in the course. - verification_statuses = cache_values.get(verification_status_cache_key) - if verification_statuses is None: - verification_statuses = VerificationStatus.get_all_checkpoints(user.id, course_key) - cache.set(verification_status_cache_key, verification_statuses) - - # Check whether the user has completed this checkpoint - # "Completion" here means *any* submission, regardless of its status - # since we want to show the user the content if they've submitted - # photos. - checkpoint = verification_statuses.get(checkpoint) - has_completed_check = bool(checkpoint) - - return (is_verified, has_skipped, has_completed_check) diff --git a/openedx/core/djangoapps/credit/signals.py b/openedx/core/djangoapps/credit/signals.py index c154adc05e..982ff87be3 100644 --- a/openedx/core/djangoapps/credit/signals.py +++ b/openedx/core/djangoapps/credit/signals.py @@ -9,7 +9,6 @@ from django.utils import timezone from opaque_keys.edx.keys import CourseKey from xmodule.modulestore.django import SignalHandler -from openedx.core.djangoapps.credit.verification_access import update_verification_partitions from openedx.core.djangoapps.signals.signals import COURSE_GRADE_CHANGED log = logging.getLogger(__name__) @@ -33,25 +32,6 @@ def on_course_publish(course_key): log.info(u'Added task to update credit requirements for course "%s" to the task queue', course_key) -@receiver(SignalHandler.pre_publish) -def on_pre_publish(sender, course_key, **kwargs): # pylint: disable=unused-argument - """ - Create user partitions for verification checkpoints. - - This is a pre-publish step since we need to write to the course descriptor. - """ - from openedx.core.djangoapps.credit import api - if api.is_credit_course(course_key): - # For now, we are tagging content with in-course-reverification access groups - # only in credit courses on publish. In the long run, this is not where we want to put this. - # This really should be a transformation on the course structure performed as a pre-processing - # step by the LMS, and the transformation should be owned by the verify_student app. - # Since none of that infrastructure currently exists, we're doing it this way instead. - log.info(u"Starting to update in-course reverification access rules") - update_verification_partitions(course_key) - log.info(u"Finished updating in-course reverification access rules") - - @receiver(COURSE_GRADE_CHANGED) def listen_for_grade_calculation(sender, user, course_grade, course_key, deadline, **kwargs): # pylint: disable=unused-argument """Receive 'MIN_GRADE_REQUIREMENT_STATUS' signal and update minimum grade requirement status. diff --git a/openedx/core/djangoapps/credit/tasks.py b/openedx/core/djangoapps/credit/tasks.py index a963e4798e..8125235bcf 100644 --- a/openedx/core/djangoapps/credit/tasks.py +++ b/openedx/core/djangoapps/credit/tasks.py @@ -18,12 +18,6 @@ from xmodule.modulestore.exceptions import ItemNotFoundError LOGGER = get_task_logger(__name__) -# XBlocks that can be added as credit requirements -CREDIT_REQUIREMENT_XBLOCK_CATEGORIES = [ - "edx-reverification-block", -] - - # pylint: disable=not-callable @task(default_retry_delay=settings.CREDIT_TASK_DEFAULT_RETRY_DELAY, max_retries=settings.CREDIT_TASK_MAX_RETRIES) def update_credit_course_requirements(course_id): # pylint: disable=invalid-name @@ -67,18 +61,14 @@ def _get_course_credit_requirements(course_key): List of credit requirements (dictionaries) """ - credit_xblock_requirements = _get_credit_course_requirement_xblocks(course_key) min_grade_requirement = _get_min_grade_requirement(course_key) proctored_exams_requirements = _get_proctoring_requirements(course_key) - block_requirements = credit_xblock_requirements + proctored_exams_requirements - # sort credit requirements list based on start date and put all the - # requirements with no start date at the end of requirement list. - sorted_block_requirements = sorted( - block_requirements, key=lambda x: (x['start_date'] is None, x['start_date'], x['display_name']) + sorted_exam_requirements = sorted( + proctored_exams_requirements, key=lambda x: (x['start_date'] is None, x['start_date'], x['display_name']) ) credit_requirements = ( - min_grade_requirement + sorted_block_requirements + min_grade_requirement + sorted_exam_requirements ) return credit_requirements @@ -112,76 +102,6 @@ def _get_min_grade_requirement(course_key): return [] -def _get_credit_course_requirement_xblocks(course_key): # pylint: disable=invalid-name - """Generate a course structure dictionary for the specified course. - - Args: - course_key (CourseKey): Identifier for the course. - - Returns: - The list of credit requirements xblocks dicts - - """ - requirements = [] - - # Retrieve all XBlocks from the course that we know to be credit requirements. - # For performance reasons, we look these up by their "category" to avoid - # loading and searching the entire course tree. - for category in CREDIT_REQUIREMENT_XBLOCK_CATEGORIES: - requirements.extend([ - { - "namespace": block.get_credit_requirement_namespace(), - "name": block.get_credit_requirement_name(), - "display_name": block.get_credit_requirement_display_name(), - 'start_date': block.start, - "criteria": {}, - } - for block in _get_xblocks(course_key, category) - if _is_credit_requirement(block) - ]) - - return requirements - - -def _get_xblocks(course_key, category): - """ - Retrieve all XBlocks in the course for a particular category. - - Returns only XBlocks that are published and haven't been deleted. - """ - xblocks = get_course_blocks(course_key, category) - - return xblocks - - -def _is_credit_requirement(xblock): - """ - Check if the given XBlock is a credit requirement. - - Args: - xblock(XBlock): The given XBlock object - - Returns: - True if XBlock is a credit requirement else False - - """ - required_methods = [ - "get_credit_requirement_namespace", - "get_credit_requirement_name", - "get_credit_requirement_display_name" - ] - - for method_name in required_methods: - if not callable(getattr(xblock, method_name, None)): - LOGGER.error( - "XBlock %s is marked as a credit requirement but does not " - "implement %s", unicode(xblock), method_name - ) - return False - - return True - - def _get_proctoring_requirements(course_key): """ Will return list of requirements regarding any exams that have been diff --git a/openedx/core/djangoapps/credit/tests/test_api.py b/openedx/core/djangoapps/credit/tests/test_api.py index 95c3fe51a2..b3c44d3c32 100644 --- a/openedx/core/djangoapps/credit/tests/test_api.py +++ b/openedx/core/djangoapps/credit/tests/test_api.py @@ -287,7 +287,7 @@ class CreditRequirementApiTests(CreditApiTestBase): # Set initial requirements requirements = [ { - "namespace": "reverification", + "namespace": "grade", "name": "midterm", "display_name": "Midterm", "criteria": {}, @@ -328,8 +328,8 @@ class CreditRequirementApiTests(CreditApiTestBase): requirements = [ { - "namespace": "reverification", - "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", + "namespace": "grade", + "name": "other_grade", "display_name": "Assessment 1", "criteria": {}, } @@ -457,8 +457,8 @@ class CreditRequirementApiTests(CreditApiTestBase): }, }, { - "namespace": "reverification", - "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", + "namespace": "grade", + "name": "other_grade", "display_name": "Assessment 1", "criteria": {}, } @@ -499,15 +499,15 @@ class CreditRequirementApiTests(CreditApiTestBase): # Set the requirement to "declined" and check that it's actually set api.set_credit_requirement_status( self.user, self.course_key, - "reverification", - "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", + "grade", + "other_grade", status="declined" ) req_status = api.get_credit_requirement_status( self.course_key, username, - namespace="reverification", - name="i4x://edX/DemoX/edx-reverification-block/assessment_uuid" + namespace="grade", + name="other_grade" ) self.assertEqual(req_status[0]["status"], "declined") @@ -528,8 +528,8 @@ class CreditRequirementApiTests(CreditApiTestBase): }, }, { - "namespace": "reverification", - "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", + "namespace": "grade", + "name": "other_grade", "display_name": "Assessment 1", "criteria": {}, } @@ -600,8 +600,8 @@ class CreditRequirementApiTests(CreditApiTestBase): }, }, { - "namespace": "reverification", - "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", + "namespace": "grade", + "name": "other_grade", "display_name": "Assessment 1", "criteria": {}, } @@ -727,8 +727,8 @@ class CreditRequirementApiTests(CreditApiTestBase): }, }, { - "namespace": "reverification", - "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", + "namespace": "grade", + "name": "other_grade", "display_name": "Assessment 1", "criteria": {}, } @@ -790,8 +790,8 @@ class CreditRequirementApiTests(CreditApiTestBase): }, }, { - "namespace": "reverification", - "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", + "namespace": "grade", + "name": "other_grade", "display_name": "Assessment 1", "criteria": {}, } diff --git a/openedx/core/djangoapps/credit/tests/test_models.py b/openedx/core/djangoapps/credit/tests/test_models.py index 45e3968b00..f85158927e 100644 --- a/openedx/core/djangoapps/credit/tests/test_models.py +++ b/openedx/core/djangoapps/credit/tests/test_models.py @@ -61,9 +61,9 @@ class CreditEligibilityModelTests(TestCase): self.assertEqual(created, True) requirement = { - "namespace": "reverification", - "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", - "display_name": "Assessment 1", + "namespace": "new_grade", + "name": "new_grade", + "display_name": "New Grade", "criteria": {}, } credit_req, created = CreditRequirement.add_or_update_course_requirement(credit_course, requirement, 1) diff --git a/openedx/core/djangoapps/credit/tests/test_partition.py b/openedx/core/djangoapps/credit/tests/test_partition.py deleted file mode 100644 index 1fff58795e..0000000000 --- a/openedx/core/djangoapps/credit/tests/test_partition.py +++ /dev/null @@ -1,182 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Tests for In-Course Reverification Access Control Partition scheme -""" - -import ddt -from nose.plugins.attrib import attr - -from lms.djangoapps.verify_student.models import ( - VerificationCheckpoint, - VerificationStatus, - SkippedReverification, -) -from openedx.core.djangoapps.credit.partition_schemes import VerificationPartitionScheme -from openedx.core.djangolib.testing.utils import skip_unless_lms -from student.models import CourseEnrollment -from student.tests.factories import UserFactory -from xmodule.partitions.partitions import UserPartition, Group -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory - - -@attr(shard=2) -@ddt.ddt -@skip_unless_lms -class ReverificationPartitionTest(ModuleStoreTestCase): - """Tests for the Reverification Partition Scheme. """ - - SUBMITTED = "submitted" - APPROVED = "approved" - DENIED = "denied" - ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] - - def setUp(self): - super(ReverificationPartitionTest, self).setUp() - - # creating course, checkpoint location and user partition mock object. - self.course = CourseFactory.create() - self.checkpoint_location = u'i4x://{org}/{course}/edx-reverification-block/first_uuid'.format( - org=self.course.id.org, course=self.course.id.course - ) - - scheme = UserPartition.get_scheme("verification") - self.user_partition = UserPartition( - id=0, - name=u"Verification Checkpoint", - description=u"Verification Checkpoint", - scheme=scheme, - parameters={"location": self.checkpoint_location}, - groups=[ - Group(scheme.ALLOW, "Allow access to content"), - Group(scheme.DENY, "Deny access to content"), - ] - ) - - self.first_checkpoint = VerificationCheckpoint.objects.create( - course_id=self.course.id, - checkpoint_location=self.checkpoint_location - ) - - def create_user_and_enroll(self, enrollment_type): - """Create and enroll users with provided enrollment type.""" - - user = UserFactory.create() - CourseEnrollment.objects.create( - user=user, - course_id=self.course.id, - mode=enrollment_type, - is_active=True - ) - return user - - def add_verification_status(self, user, status): - """Adding the verification status for a user.""" - - VerificationStatus.add_status_from_checkpoints( - checkpoints=[self.first_checkpoint], - user=user, - status=status - ) - - @ddt.data( - ("verified", SUBMITTED, VerificationPartitionScheme.ALLOW), - ("verified", APPROVED, VerificationPartitionScheme.ALLOW), - ("verified", DENIED, VerificationPartitionScheme.ALLOW), - ("verified", None, VerificationPartitionScheme.DENY), - ("honor", None, VerificationPartitionScheme.ALLOW), - ) - @ddt.unpack - def test_get_group_for_user(self, enrollment_type, verification_status, expected_group): - # creating user and enroll them. - user = self.create_user_and_enroll(enrollment_type) - if verification_status: - self.add_verification_status(user, verification_status) - - self._assert_group_assignment(user, expected_group) - - def test_get_group_for_user_with_skipped(self): - # Check that a user is in verified allow group if that user has skipped - # any ICRV block. - user = self.create_user_and_enroll('verified') - - SkippedReverification.add_skipped_reverification_attempt( - checkpoint=self.first_checkpoint, - user_id=user.id, - course_id=self.course.id - ) - - self._assert_group_assignment(user, VerificationPartitionScheme.ALLOW) - - def test_cache_with_skipped_icrv(self): - # Check that a user is in verified allow group if that user has skipped - # any ICRV block. - user = self.create_user_and_enroll('verified') - SkippedReverification.add_skipped_reverification_attempt( - checkpoint=self.first_checkpoint, - user_id=user.id, - course_id=self.course.id - ) - # this will warm the cache. - with self.assertNumQueries(3): - self._assert_group_assignment(user, VerificationPartitionScheme.ALLOW) - - # no db queries this time. - with self.assertNumQueries(0): - self._assert_group_assignment(user, VerificationPartitionScheme.ALLOW) - - def test_cache_with_submitted_status(self): - # Check that a user is in verified allow group if that user has approved status at - # any ICRV block. - user = self.create_user_and_enroll('verified') - self.add_verification_status(user, VerificationStatus.APPROVED_STATUS) - # this will warm the cache. - with self.assertNumQueries(4): - self._assert_group_assignment(user, VerificationPartitionScheme.ALLOW) - - # no db queries this time. - with self.assertNumQueries(0): - self._assert_group_assignment(user, VerificationPartitionScheme.ALLOW) - - def test_cache_with_denied_status(self): - # Check that a user is in verified allow group if that user has denied at - # any ICRV block. - user = self.create_user_and_enroll('verified') - self.add_verification_status(user, VerificationStatus.DENIED_STATUS) - - # this will warm the cache. - with self.assertNumQueries(4): - self._assert_group_assignment(user, VerificationPartitionScheme.ALLOW) - - # no db queries this time. - with self.assertNumQueries(0): - self._assert_group_assignment(user, VerificationPartitionScheme.ALLOW) - - def test_cache_with_honor(self): - # Check that a user is in honor mode. - # any ICRV block. - user = self.create_user_and_enroll('honor') - # this will warm the cache. - with self.assertNumQueries(3): - self._assert_group_assignment(user, VerificationPartitionScheme.ALLOW) - - # no db queries this time. - with self.assertNumQueries(0): - self._assert_group_assignment(user, VerificationPartitionScheme.ALLOW) - - def test_cache_with_verified_deny_group(self): - # Check that a user is in verified mode. But not perform any action - - user = self.create_user_and_enroll('verified') - # this will warm the cache. - with self.assertNumQueries(3): - self._assert_group_assignment(user, VerificationPartitionScheme.DENY) - - # no db queries this time. - with self.assertNumQueries(0): - self._assert_group_assignment(user, VerificationPartitionScheme.DENY) - - def _assert_group_assignment(self, user, expected_group_id): - """Check that the user was assigned to a group. """ - actual_group = VerificationPartitionScheme.get_group_for_user(self.course.id, user, self.user_partition) - self.assertEqual(actual_group.id, expected_group_id) diff --git a/openedx/core/djangoapps/credit/tests/test_tasks.py b/openedx/core/djangoapps/credit/tests/test_tasks.py index a2d3ee7808..13f1bf710c 100644 --- a/openedx/core/djangoapps/credit/tests/test_tasks.py +++ b/openedx/core/djangoapps/credit/tests/test_tasks.py @@ -4,16 +4,14 @@ Tests for credit course tasks. import mock from nose.plugins.attrib import attr -from datetime import datetime, timedelta +from datetime import datetime -from pytz import UTC from openedx.core.djangoapps.credit.api import get_credit_requirements from openedx.core.djangoapps.credit.exceptions import InvalidCreditRequirements from openedx.core.djangoapps.credit.models import CreditCourse from openedx.core.djangoapps.credit.signals import on_course_publish -from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls_range +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from edx_proctoring.api import create_exam @@ -34,25 +32,6 @@ class TestTaskExecution(ModuleStoreTestCase): """ raise InvalidCreditRequirements - def add_icrv_xblock(self, related_assessment_name=None, start_date=None): - """ Create the 'edx-reverification-block' in course tree """ - block = ItemFactory.create( - parent=self.vertical, - category='edx-reverification-block', - ) - - if related_assessment_name is not None: - block.related_assessment = related_assessment_name - - block.start = start_date - - self.store.update_item(block, ModuleStoreEnum.UserID.test) - - with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, self.course.id): - self.store.publish(block.location, ModuleStoreEnum.UserID.test) - - return block - def setUp(self): super(TestTaskExecution, self).setUp() @@ -86,19 +65,6 @@ class TestTaskExecution(ModuleStoreTestCase): requirements = get_credit_requirements(self.course.id) self.assertEqual(len(requirements), 1) - def test_task_adding_icrv_requirements(self): - """Make sure that the receiver correctly fires off the task when - invoked by signal. - """ - self.add_credit_course(self.course.id) - self.add_icrv_xblock() - requirements = get_credit_requirements(self.course.id) - self.assertEqual(len(requirements), 0) - on_course_publish(self.course.id) - - requirements = get_credit_requirements(self.course.id) - self.assertEqual(len(requirements), 2) - def test_proctored_exam_requirements(self): """ Make sure that proctored exams are being registered as requirements @@ -202,71 +168,6 @@ class TestTaskExecution(ModuleStoreTestCase): if requirement['namespace'] == 'proctored_exam' ]) - def test_query_counts(self): - self.add_credit_course(self.course.id) - self.add_icrv_xblock() - - with check_mongo_calls_range(max_finds=11): - on_course_publish(self.course.id) - - def test_remove_icrv_requirement(self): - self.add_credit_course(self.course.id) - self.add_icrv_xblock() - on_course_publish(self.course.id) - - # There should be one ICRV requirement - requirements = get_credit_requirements(self.course.id, namespace="reverification") - self.assertEqual(len(requirements), 1) - - # Delete the parent section containing the ICRV block - with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, self.course.id): - self.store.delete_item(self.subsection.location, ModuleStoreEnum.UserID.test) - - # Check that the ICRV block is no longer visible in the requirements - on_course_publish(self.course.id) - requirements = get_credit_requirements(self.course.id, namespace="reverification") - self.assertEqual(len(requirements), 0) - - def test_icrv_requirement_ordering(self): - self.add_credit_course(self.course.id) - - # Create multiple ICRV blocks - start = datetime.now(UTC) - self.add_icrv_xblock(related_assessment_name="Midterm A", start_date=start) - - start = start - timedelta(days=1) - self.add_icrv_xblock(related_assessment_name="Midterm B", start_date=start) - - # Primary sort is based on start date - on_course_publish(self.course.id) - requirements = get_credit_requirements(self.course.id, namespace="reverification") - self.assertEqual(len(requirements), 2) - self.assertEqual(requirements[0]["display_name"], "Midterm B") - self.assertEqual(requirements[1]["display_name"], "Midterm A") - - # Add two additional ICRV blocks that have no start date - # and the same name. - start = datetime.now(UTC) - first_block = self.add_icrv_xblock(related_assessment_name="Midterm Start Date") - - start = start + timedelta(days=1) - second_block = self.add_icrv_xblock(related_assessment_name="Midterm Start Date") - - on_course_publish(self.course.id) - requirements = get_credit_requirements(self.course.id, namespace="reverification") - self.assertEqual(len(requirements), 4) - # Since we are now primarily sorting on start_date and display_name if - # start_date is present otherwise we are just sorting on display_name. - self.assertEqual(requirements[0]["display_name"], "Midterm B") - self.assertEqual(requirements[1]["display_name"], "Midterm A") - self.assertEqual(requirements[2]["display_name"], "Midterm Start Date") - self.assertEqual(requirements[3]["display_name"], "Midterm Start Date") - - # Since the last two requirements have the same display name, - # we need to also check that their internal names (locations) are the same. - self.assertEqual(requirements[2]["name"], first_block.get_credit_requirement_name()) - self.assertEqual(requirements[3]["name"], second_block.get_credit_requirement_name()) - @mock.patch( 'openedx.core.djangoapps.credit.tasks.set_credit_requirements', mock.Mock( @@ -315,24 +216,15 @@ class TestTaskExecution(ModuleStoreTestCase): self.assertEqual(requirements[1]['display_name'], 'A Proctored Exam') self.assertEqual(requirements[1]['criteria'], {}) - # Create multiple ICRV blocks - start = datetime.now(UTC) - self.add_icrv_xblock(related_assessment_name="Midterm A", start_date=start) - - start = start - timedelta(days=1) - self.add_icrv_xblock(related_assessment_name="Midterm B", start_date=start) - # Primary sort is based on start date on_course_publish(self.course.id) requirements = get_credit_requirements(self.course.id) # grade requirement is added on publish of the requirements - self.assertEqual(len(requirements), 4) + self.assertEqual(len(requirements), 2) # check requirements are added in the desired order # 1st Minimum grade then the blocks with start date than other blocks self.assertEqual(requirements[0]["display_name"], "Minimum Grade") self.assertEqual(requirements[1]["display_name"], "A Proctored Exam") - self.assertEqual(requirements[2]["display_name"], "Midterm B") - self.assertEqual(requirements[3]["display_name"], "Midterm A") def add_credit_course(self, course_key): """Add the course as a credit. diff --git a/openedx/core/djangoapps/credit/tests/test_verification_access.py b/openedx/core/djangoapps/credit/tests/test_verification_access.py deleted file mode 100644 index 763d2fed2d..0000000000 --- a/openedx/core/djangoapps/credit/tests/test_verification_access.py +++ /dev/null @@ -1,275 +0,0 @@ -""" -Tests for in-course reverification user partition creation. - -This should really belong to the verify_student app, -but we can't move it there because it's in the LMS and we're -currently applying these rules on publish from Studio. - -In the future, this functionality should be a course transformation -defined in the verify_student app, and these tests should be moved -into verify_student. - -""" - -from mock import patch -from nose.plugins.attrib import attr - -from django.conf import settings - -from openedx.core.djangoapps.credit.models import CreditCourse -from openedx.core.djangoapps.credit.partition_schemes import VerificationPartitionScheme -from openedx.core.djangoapps.credit.verification_access import update_verification_partitions -from openedx.core.djangoapps.credit.signals import on_pre_publish -from xmodule.modulestore import ModuleStoreEnum -from xmodule.modulestore.django import SignalHandler -from xmodule.modulestore.exceptions import ItemNotFoundError -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, TEST_DATA_SPLIT_MODULESTORE -from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls_range -from xmodule.partitions.partitions import Group, UserPartition - - -@attr(shard=2) -class CreateVerificationPartitionTest(ModuleStoreTestCase): - """ - Tests for applying verification access rules. - """ - - # Run the tests in split modulestore - # While verification access will work in old-Mongo, it's not something - # we're committed to supporting, since this feature is meant for use - # in new courses. - MODULESTORE = TEST_DATA_SPLIT_MODULESTORE - - @patch.dict(settings.FEATURES, {"ENABLE_COURSEWARE_INDEX": False}) - def setUp(self): - super(CreateVerificationPartitionTest, self).setUp() - - # Disconnect the signal receiver -- we'll invoke the update code ourselves - SignalHandler.pre_publish.disconnect(receiver=on_pre_publish) - self.addCleanup(SignalHandler.pre_publish.connect, receiver=on_pre_publish) - - # Create a dummy course with a single verification checkpoint - # Because we need to check "exam" content surrounding the ICRV checkpoint, - # we need to create a fairly large course structure, with multiple sections, - # subsections, verticals, units, and items. - self.course = CourseFactory() - self.sections = [ - ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section A'), - ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section B'), - ] - self.subsections = [ - ItemFactory.create(parent=self.sections[0], category='sequential', display_name='Test Subsection A 1'), - ItemFactory.create(parent=self.sections[0], category='sequential', display_name='Test Subsection A 2'), - ItemFactory.create(parent=self.sections[1], category='sequential', display_name='Test Subsection B 1'), - ItemFactory.create(parent=self.sections[1], category='sequential', display_name='Test Subsection B 2'), - ] - self.verticals = [ - ItemFactory.create(parent=self.subsections[0], category='vertical', display_name='Test Unit A 1 a'), - ItemFactory.create(parent=self.subsections[0], category='vertical', display_name='Test Unit A 1 b'), - ItemFactory.create(parent=self.subsections[1], category='vertical', display_name='Test Unit A 2 a'), - ItemFactory.create(parent=self.subsections[1], category='vertical', display_name='Test Unit A 2 b'), - ItemFactory.create(parent=self.subsections[2], category='vertical', display_name='Test Unit B 1 a'), - ItemFactory.create(parent=self.subsections[2], category='vertical', display_name='Test Unit B 1 b'), - ItemFactory.create(parent=self.subsections[3], category='vertical', display_name='Test Unit B 2 a'), - ItemFactory.create(parent=self.subsections[3], category='vertical', display_name='Test Unit B 2 b'), - ] - self.icrv = ItemFactory.create(parent=self.verticals[0], category='edx-reverification-block') - self.sibling_problem = ItemFactory.create(parent=self.verticals[0], category='problem') - - def test_creates_user_partitions(self): - self._update_partitions() - - # Check that a new user partition was created for the ICRV block - self.assertEqual(len(self.course.user_partitions), 1) - partition = self.course.user_partitions[0] - self.assertEqual(partition.scheme.name, "verification") - self.assertEqual(partition.parameters["location"], unicode(self.icrv.location)) - - # Check that the groups for the partition were created correctly - self.assertEqual(len(partition.groups), 2) - self.assertItemsEqual( - [g.id for g in partition.groups], - [ - VerificationPartitionScheme.ALLOW, - VerificationPartitionScheme.DENY, - ] - ) - - @patch.dict(settings.FEATURES, {"ENABLE_COURSEWARE_INDEX": False}) - def test_removes_deleted_user_partitions(self): - self._update_partitions() - - # Delete the reverification block, then update the partitions - self.store.delete_item( - self.icrv.location, - ModuleStoreEnum.UserID.test, - revision=ModuleStoreEnum.RevisionOption.published_only - ) - self._update_partitions() - - # Check that the user partition was marked as inactive - self.assertEqual(len(self.course.user_partitions), 1) - partition = self.course.user_partitions[0] - self.assertFalse(partition.active) - self.assertEqual(partition.scheme.name, "verification") - - @patch.dict(settings.FEATURES, {"ENABLE_COURSEWARE_INDEX": False}) - def test_preserves_partition_id_for_verified_partitions(self): - self._update_partitions() - partition_id = self.course.user_partitions[0].id - self._update_partitions() - new_partition_id = self.course.user_partitions[0].id - self.assertEqual(partition_id, new_partition_id) - - @patch.dict(settings.FEATURES, {"ENABLE_COURSEWARE_INDEX": False}) - def test_preserves_existing_user_partitions(self): - # Add other, non-verified partition to the course - self.course.user_partitions = [ - UserPartition( - id=0, - name='Cohort user partition', - scheme=UserPartition.get_scheme('cohort'), - description='Cohorted user partition', - groups=[ - Group(id=0, name="Group A"), - Group(id=1, name="Group B"), - ], - ), - UserPartition( - id=1, - name='Random user partition', - scheme=UserPartition.get_scheme('random'), - description='Random user partition', - groups=[ - Group(id=0, name="Group A"), - Group(id=1, name="Group B"), - ], - ), - ] - self.course = self.store.update_item(self.course, ModuleStoreEnum.UserID.test) - - # Update the verification partitions. - # The existing partitions should still be available - self._update_partitions() - partition_ids = [p.id for p in self.course.user_partitions] - self.assertEqual(len(partition_ids), 3) - self.assertIn(0, partition_ids) - self.assertIn(1, partition_ids) - - def test_multiple_reverification_blocks(self): - # Add an additional ICRV block in another section - other_icrv = ItemFactory.create(parent=self.verticals[3], category='edx-reverification-block') - self._update_partitions() - - # Expect that both ICRV blocks have corresponding partitions - self.assertEqual(len(self.course.user_partitions), 2) - partition_locations = [p.parameters.get("location") for p in self.course.user_partitions] - self.assertIn(unicode(self.icrv.location), partition_locations) - self.assertIn(unicode(other_icrv.location), partition_locations) - - # Delete the first ICRV block and update partitions - icrv_location = self.icrv.location - self.store.delete_item( - self.icrv.location, - ModuleStoreEnum.UserID.test, - revision=ModuleStoreEnum.RevisionOption.published_only - ) - self._update_partitions() - - # Expect that the correct partition is marked as inactive - self.assertEqual(len(self.course.user_partitions), 2) - partitions_by_loc = { - p.parameters["location"]: p - for p in self.course.user_partitions - } - self.assertFalse(partitions_by_loc[unicode(icrv_location)].active) - self.assertTrue(partitions_by_loc[unicode(other_icrv.location)].active) - - def test_query_counts_with_no_reverification_blocks(self): - # Delete the ICRV block, so the number of ICRV blocks is zero - self.store.delete_item( - self.icrv.location, - ModuleStoreEnum.UserID.test, - revision=ModuleStoreEnum.RevisionOption.published_only - ) - - # 2 calls: get the course (definitions + structures) - # 2 calls: look up ICRV blocks in the course (definitions + structures) - with check_mongo_calls_range(max_finds=4, max_sends=2): - self._update_partitions(reload_items=False) - - def test_query_counts_with_one_reverification_block(self): - # One ICRV block created in the setup method - # Additional call to load the ICRV block - with check_mongo_calls_range(max_finds=5, max_sends=3): - self._update_partitions(reload_items=False) - - def test_query_counts_with_multiple_reverification_blocks(self): - # Total of two ICRV blocks (one created in setup method) - # Additional call to load each ICRV block - ItemFactory.create(parent=self.verticals[3], category='edx-reverification-block') - with check_mongo_calls_range(max_finds=6, max_sends=3): - self._update_partitions(reload_items=False) - - def _update_partitions(self, reload_items=True): - """Update user partitions in the course descriptor, then reload the content. """ - update_verification_partitions(self.course.id) # pylint: disable=no-member - - # Reload each component so we can see the changes - if reload_items: - self.course = self.store.get_course(self.course.id) # pylint: disable=no-member - self.sections = [self._reload_item(section.location) for section in self.sections] - self.subsections = [self._reload_item(subsection.location) for subsection in self.subsections] - self.verticals = [self._reload_item(vertical.location) for vertical in self.verticals] - self.icrv = self._reload_item(self.icrv.location) - self.sibling_problem = self._reload_item(self.sibling_problem.location) - - def _reload_item(self, location): - """Safely reload an item from the moduelstore. """ - try: - return self.store.get_item(location) - except ItemNotFoundError: - return None - - -@attr(shard=2) -class WriteOnPublishTest(ModuleStoreTestCase): - """ - Verify that updates to the course descriptor's - user partitions are written automatically on publish. - """ - MODULESTORE = TEST_DATA_SPLIT_MODULESTORE - - ENABLED_SIGNALS = ['course_published', 'pre_publish'] - - @patch.dict(settings.FEATURES, {"ENABLE_COURSEWARE_INDEX": False}) - def setUp(self): - super(WriteOnPublishTest, self).setUp() - - # Create a dummy course with an ICRV block - self.course = CourseFactory() - self.section = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') - self.subsection = ItemFactory.create(parent=self.section, category='sequential', display_name='Test Subsection') - self.vertical = ItemFactory.create(parent=self.subsection, category='vertical', display_name='Test Unit') - self.icrv = ItemFactory.create(parent=self.vertical, category='edx-reverification-block') - - # Mark the course as credit - CreditCourse.objects.create(course_key=self.course.id, enabled=True) # pylint: disable=no-member - - @patch.dict(settings.FEATURES, {"ENABLE_COURSEWARE_INDEX": False}) - def test_can_write_on_publish_signal(self): - # Sanity check -- initially user partitions should be empty - self.assertEqual(self.course.user_partitions, []) - - # Make and publish a change to a block, which should trigger the publish signal - with self.store.bulk_operations(self.course.id): # pylint: disable=no-member - self.icrv.display_name = "Updated display name" - self.store.update_item(self.icrv, ModuleStoreEnum.UserID.test) - self.store.publish(self.icrv.location, ModuleStoreEnum.UserID.test) - - # Within the test, the course pre-publish signal should have fired synchronously - # Since the course is marked as credit, the in-course verification partitions - # should have been created. - # We need to verify that these changes were actually persisted to the modulestore. - retrieved_course = self.store.get_course(self.course.id) # pylint: disable=no-member - self.assertEqual(len(retrieved_course.user_partitions), 1) diff --git a/openedx/core/djangoapps/credit/verification_access.py b/openedx/core/djangoapps/credit/verification_access.py deleted file mode 100644 index 5a5c625718..0000000000 --- a/openedx/core/djangoapps/credit/verification_access.py +++ /dev/null @@ -1,187 +0,0 @@ -""" -Create in-course reverification access groups in a course. - -We model the rules as a set of user partitions, one for each -verification checkpoint in a course. - -For example, suppose that a course has two verification checkpoints, -one at midterm A and one at the midterm B. - -Then the user partitions would look like this: - -Midterm A: |-- ALLOW --|-- DENY --| -Midterm B: |-- ALLOW --|-- DENY --| - -where the groups are defined as: - -* ALLOW: The user has access to content gated by the checkpoint. -* DENY: The user does not have access to content gated by the checkpoint. - -""" -import logging - -from util.db import generate_int_id -from openedx.core.djangoapps.credit.utils import get_course_blocks -from xmodule.modulestore.django import modulestore -from xmodule.modulestore import ModuleStoreEnum -from xmodule.partitions.partitions import Group, UserPartition - - -log = logging.getLogger(__name__) - - -VERIFICATION_SCHEME_NAME = "verification" -VERIFICATION_BLOCK_CATEGORY = "edx-reverification-block" - - -def update_verification_partitions(course_key): - """ - Create a user partition for each verification checkpoint in the course. - - This will modify the published version of the course descriptor. - It ensures that any in-course reverification XBlocks in the course - have an associated user partition. Other user partitions (e.g. cohorts) - will be preserved. Partitions associated with deleted reverification checkpoints - will be marked as inactive and will not be used to restrict access. - - Arguments: - course_key (CourseKey): identifier for the course. - - Returns: - None - """ - # Batch all the queries we're about to do and suppress - # the "publish" signal to avoid an infinite call loop. - with modulestore().bulk_operations(course_key, emit_signals=False): - - # Retrieve all in-course reverification blocks in the course - icrv_blocks = get_course_blocks(course_key, VERIFICATION_BLOCK_CATEGORY) - - # Update the verification definitions in the course descriptor - # This will also clean out old verification partitions if checkpoints - # have been deleted. - _set_verification_partitions(course_key, icrv_blocks) - - -def _unique_partition_id(course): - """Return a unique user partition ID for the course. """ - # Exclude all previously used IDs, even for partitions that have been disabled - # (e.g. if the course author deleted an in-course reverifification block but - # there are courseware components that reference the disabled partition). - used_ids = set(p.id for p in course.user_partitions) - return generate_int_id(used_ids=used_ids) - - -def _other_partitions(verified_partitions, exclude_partitions, course_key): - """ - Retrieve all partitions NOT associated with the current set of ICRV blocks. - - Any partition associated with a deleted ICRV block will be marked as inactive - so its access rules will no longer be enforced. - - Arguments: - all_partitions (list of UserPartition): All verified partitions defined in the course. - exclude_partitions (list of UserPartition): Partitions to exclude (e.g. the ICRV partitions already added) - course_key (CourseKey): Identifier for the course (used for logging). - - Returns: list of `UserPartition`s - - """ - results = [] - partition_by_id = { - p.id: p for p in verified_partitions - } - other_partition_ids = set(p.id for p in verified_partitions) - set(p.id for p in exclude_partitions) - - for pid in other_partition_ids: - partition = partition_by_id[pid] - results.append( - UserPartition( - id=partition.id, - name=partition.name, - description=partition.description, - scheme=partition.scheme, - parameters=partition.parameters, - groups=partition.groups, - active=False, - ) - ) - log.info( - ( - "Disabled partition %s in course %s because the " - "associated in-course-reverification checkpoint does not exist." - ), - partition.id, course_key - ) - - return results - - -def _set_verification_partitions(course_key, icrv_blocks): - """ - Create or update user partitions in the course. - - Ensures that each ICRV block in the course has an associated user partition - with the groups ALLOW and DENY. - - Arguments: - course_key (CourseKey): Identifier for the course. - icrv_blocks (list of XBlock): In-course reverification blocks, e.g. reverification checkpoints. - - Returns: - list of UserPartition - """ - scheme = UserPartition.get_scheme(VERIFICATION_SCHEME_NAME) - if scheme is None: - log.error("Could not retrieve user partition scheme with ID %s", VERIFICATION_SCHEME_NAME) - return [] - - course = modulestore().get_course(course_key) - if course is None: - log.error("Could not find course %s", course_key) - return [] - - verified_partitions = course.get_user_partitions_for_scheme(scheme) - partition_id_for_location = { - p.parameters["location"]: p.id - for p in verified_partitions - if "location" in p.parameters - } - - partitions = [] - for block in icrv_blocks: - partition = UserPartition( - id=partition_id_for_location.get( - unicode(block.location), - _unique_partition_id(course) - ), - name=block.related_assessment, - description=u"Verification checkpoint at {}".format(block.related_assessment), - scheme=scheme, - parameters={"location": unicode(block.location)}, - groups=[ - Group(scheme.ALLOW, "Completed verification at {}".format(block.related_assessment)), - Group(scheme.DENY, "Did not complete verification at {}".format(block.related_assessment)), - ] - ) - partitions.append(partition) - - log.info( - ( - "Configured partition %s for course %s using a verified partition scheme " - "for the in-course-reverification checkpoint at location %s" - ), - partition.id, - course_key, - partition.parameters["location"] - ) - - # Preserve existing, non-verified partitions from the course - # Mark partitions for deleted in-course reverification as disabled. - partitions += _other_partitions(verified_partitions, partitions, course_key) - course.set_user_partitions_for_scheme(partitions, scheme) - modulestore().update_item(course, ModuleStoreEnum.UserID.system) - - log.info("Saved updated partitions for the course %s", course_key) - - return partitions diff --git a/openedx/core/djangolib/model_mixins.py b/openedx/core/djangolib/model_mixins.py new file mode 100644 index 0000000000..f2433475f5 --- /dev/null +++ b/openedx/core/djangolib/model_mixins.py @@ -0,0 +1,14 @@ +""" +Custom Django Model mixins. +""" + + +class DeprecatedModelMixin(object): + """ + Used to make a class unusable in practice, but leave database tables intact. + """ + def __init__(self, *args, **kwargs): # pylint: disable=unused-argument + """ + Override to kill usage of this model. + """ + raise TypeError("This model has been deprecated and should not be used.") diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index c5e94c2f21..3e3266d1f0 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -86,7 +86,6 @@ git+https://github.com/solashirai/crowdsourcehinter.git@518605f0a95190949fe77bd3 git+https://github.com/edx/edx-milestones.git@v0.1.10#egg=edx-milestones==0.1.10 git+https://github.com/edx/xblock-utils.git@v1.0.3#egg=xblock-utils==1.0.3 -e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive --e git+https://github.com/edx/edx-reverification-block.git@0.0.5#egg=edx-reverification-block==0.0.5 git+https://github.com/edx/edx-user-state-client.git@1.0.1#egg=edx-user-state-client==1.0.1 git+https://github.com/edx/xblock-lti-consumer.git@v1.1.2#egg=lti_consumer-xblock==1.1.2 git+https://github.com/edx/edx-proctoring.git@0.18.0#egg=edx-proctoring==0.18.0 diff --git a/setup.py b/setup.py index 08f2637360..981ac7530e 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,6 @@ setup( "openedx.user_partition_scheme": [ "random = openedx.core.djangoapps.user_api.partition_schemes:RandomUserPartitionScheme", "cohort = openedx.core.djangoapps.course_groups.partition_scheme:CohortPartitionScheme", - "verification = openedx.core.djangoapps.credit.partition_schemes:VerificationPartitionScheme", ], "openedx.block_structure_transformer": [ "library_content = lms.djangoapps.course_blocks.transformers.library_content:ContentLibraryTransformer",