Merge pull request #14588 from edx/efischer/kill_stuff

Kill edx-reverification-block
This commit is contained in:
Eric Fischer
2017-03-06 09:29:36 -05:00
committed by GitHub
27 changed files with 82 additions and 3113 deletions

View File

@@ -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",

View File

@@ -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)

View File

@@ -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.

View File

@@ -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

View File

@@ -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": {},
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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.

View File

@@ -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)

View File

@@ -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

View 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.")