Merge pull request #14588 from edx/efischer/kill_stuff
Kill edx-reverification-block
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
@@ -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": {},
|
||||
}
|
||||
@@ -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": {},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
14
openedx/core/djangolib/model_mixins.py
Normal file
14
openedx/core/djangolib/model_mixins.py
Normal file
@@ -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.")
|
||||
Reference in New Issue
Block a user