Commit 5c26acc1 by Eric Fischer

Re-kill ICRV block

This reverts commit 1224e341. I've also added
NotImplementedPartitionScheme, which allows deprecated partition types to have
a valid entry point despite being unusable.

TNL-6675
parent bd2025d6
......@@ -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"),
],
......@@ -572,9 +572,9 @@ class GetUserPartitionInfoTest(ModuleStoreTestCase):
])
# Expect that the partition with no groups is excluded from the results
partitions = self._get_partition_info(schemes=["cohort", "verification"])
partitions = self._get_partition_info(schemes=["cohort", "random"])
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. """
......
......@@ -47,7 +47,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
......@@ -680,7 +680,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(),
......
......@@ -156,8 +156,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
@ddt.data(
(ModuleStoreEnum.Type.mongo, 1, 28, True),
(ModuleStoreEnum.Type.mongo, 1, 24, False),
(ModuleStoreEnum.Type.split, 3, 27, True),
(ModuleStoreEnum.Type.split, 3, 23, False),
(ModuleStoreEnum.Type.split, 3, 28, True),
(ModuleStoreEnum.Type.split, 3, 24, False),
)
@ddt.unpack
def test_query_counts(self, default_store, num_mongo_calls, num_sql_calls, create_multiple_subsections):
......@@ -170,7 +170,7 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
@ddt.data(
(ModuleStoreEnum.Type.mongo, 1, 28),
(ModuleStoreEnum.Type.split, 3, 27),
(ModuleStoreEnum.Type.split, 3, 28),
)
@ddt.unpack
def test_query_counts_dont_change_with_more_content(self, default_store, num_mongo_calls, num_sql_calls):
......@@ -216,7 +216,7 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
@ddt.data(
(ModuleStoreEnum.Type.mongo, 1, 11),
(ModuleStoreEnum.Type.split, 3, 10),
(ModuleStoreEnum.Type.split, 3, 11),
)
@ddt.unpack
def test_persistent_grades_not_enabled_on_course(self, default_store, num_mongo_queries, num_sql_queries):
......@@ -231,7 +231,7 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
@ddt.data(
(ModuleStoreEnum.Type.mongo, 1, 25),
(ModuleStoreEnum.Type.split, 3, 24),
(ModuleStoreEnum.Type.split, 3, 25),
)
@ddt.unpack
def test_persistent_grades_enabled_on_course(self, default_store, num_mongo_queries, num_sql_queries):
......
......@@ -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)
......@@ -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)
"""
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)
......@@ -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 )
......
......@@ -48,12 +48,6 @@ def set_credit_requirements(course_key, 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",
"display_name": "Final Exam",
......@@ -107,12 +101,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",
"display_name": "Final Exam",
......@@ -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
......@@ -365,16 +334,6 @@ def get_credit_requirement_status(course_key, username, namespace=None, name=Non
[
{
"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",
"display_name": "Proctored Mid Term Exam",
......
"""
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)
......@@ -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.
......
......@@ -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
......
......@@ -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": {},
}
......@@ -453,8 +453,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": {},
}
......@@ -516,15 +516,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")
......@@ -571,8 +571,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": {},
}
......@@ -643,8 +643,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": {},
}
......@@ -770,8 +770,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": {},
}
......@@ -833,8 +833,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": {},
}
......
......@@ -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)
......
# -*- 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)
......@@ -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(
......@@ -290,7 +191,7 @@ class TestTaskExecution(ModuleStoreTestCase):
def test_credit_requirement_blocks_ordering(self):
"""
Test ordering of the proctoring and ICRV blocks are in proper order.
Test ordering of proctoring blocks.
"""
self.add_credit_course(self.course.id)
......@@ -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.
......
"""
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 = [p for p in course.user_partitions if p.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
......@@ -10,6 +10,20 @@ from xmodule.partitions.partitions import UserPartitionError, NoSuchUserPartitio
log = logging.getLogger(__name__)
class NotImplementedPartitionScheme(object):
"""
This "scheme" allows previously-defined schemes to be purged, while giving existing
course data definitions a safe entry point to load.
"""
@classmethod
def get_group_for_user(cls, course_key, user, user_partition, assign=True, track_function=None):
"""
Dummy method, will fail hard if anyone tries to use this scheme.
"""
raise NotImplementedError()
class RandomUserPartitionScheme(object):
"""
This scheme randomly assigns users into the partition's groups.
......
"""
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.")
......@@ -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.4#egg=xblock-utils==1.0.4
-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
......
......@@ -41,7 +41,7 @@ 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",
"verification = openedx.core.djangoapps.user_api.partition_schemes:NotImplementedPartitionScheme",
"enrollment_track = openedx.core.djangoapps.verified_track_content.partition_scheme:EnrollmentTrackPartitionScheme",
],
"openedx.block_structure_transformer": [
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment