AA-177: Add masquerading for course home MFE
- Looks at masquerading config for dates, outline, metadata, and celebration APIs in course_home_api / courseware_api. - Consolidates and cleans up places we check whether masquerading gives us full access to a course.
This commit is contained in:
@@ -2,10 +2,14 @@
|
||||
Helper functions used by both content_type_gating and course_duration_limits.
|
||||
"""
|
||||
|
||||
|
||||
import logging
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from openedx.core.djangoapps.config_model_utils.utils import is_in_holdback
|
||||
from student.models import CourseEnrollment
|
||||
from student.role_helpers import has_staff_roles
|
||||
from xmodule.partitions.partitions import Group
|
||||
|
||||
# Studio generates partition IDs starting at 100. There is already a manually generated
|
||||
@@ -21,16 +25,22 @@ FULL_ACCESS = Group(CONTENT_TYPE_GATE_GROUP_IDS['full_access'], 'Full-access Use
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def correct_modes_for_fbe(course_key, enrollment=None, user=None):
|
||||
def correct_modes_for_fbe(course_key=None, enrollment=None, user=None, course=None):
|
||||
"""
|
||||
If CONTENT_TYPE_GATING is enabled use the following logic to determine whether
|
||||
enabled_for_enrollment should be false
|
||||
"""
|
||||
if course_key is None:
|
||||
if course_key is None and course is None:
|
||||
return True
|
||||
|
||||
modes = CourseMode.modes_for_course(course_key, include_expired=True, only_selectable=False)
|
||||
# Separate these two calls to help with cache hits (most modes_for_course callers pass in a positional course key)
|
||||
if course:
|
||||
modes = CourseMode.modes_for_course(course=course, include_expired=True, only_selectable=False)
|
||||
else:
|
||||
modes = CourseMode.modes_for_course(course_key, include_expired=True, only_selectable=False)
|
||||
|
||||
modes_dict = {mode.slug: mode for mode in modes}
|
||||
course_key = course_key or course.id
|
||||
|
||||
# If there is no audit mode or no verified mode, FBE will not be enabled
|
||||
if (CourseMode.AUDIT not in modes_dict) or (CourseMode.VERIFIED not in modes_dict):
|
||||
@@ -57,3 +67,63 @@ def correct_modes_for_fbe(course_key, enrollment=None, user=None):
|
||||
if mode_slug != CourseMode.AUDIT:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def has_full_access_role_in_masquerade(user, course_key):
|
||||
"""
|
||||
The roles of the masquerade user are used to determine whether the content gate displays.
|
||||
|
||||
Returns:
|
||||
True if we are masquerading as a full-access generic user
|
||||
False if we are masquerading as a non-full-access generic user
|
||||
None if we are not masquerading or masquerading as a specific student that should go through normal checks
|
||||
"""
|
||||
# The masquerade module imports from us, so avoid a circular dependency here
|
||||
from lms.djangoapps.courseware.masquerade import (
|
||||
get_course_masquerade, is_masquerading_as_full_access, is_masquerading_as_non_audit_enrollment,
|
||||
is_masquerading_as_specific_student, is_masquerading_as_staff,
|
||||
)
|
||||
|
||||
course_masquerade = get_course_masquerade(user, course_key)
|
||||
if not course_masquerade or is_masquerading_as_specific_student(user, course_key):
|
||||
return None
|
||||
return (is_masquerading_as_staff(user, course_key) or
|
||||
is_masquerading_as_full_access(user, course_key, course_masquerade) or
|
||||
is_masquerading_as_non_audit_enrollment(user, course_key, course_masquerade))
|
||||
|
||||
|
||||
def enrollment_date_for_fbe(user, course_key=None, course=None):
|
||||
"""
|
||||
Gets the enrollment date for the given user and course, if FBE is enabled.
|
||||
|
||||
One of course_key or course must be provided.
|
||||
|
||||
Returns:
|
||||
None if FBE is disabled for either this user or course
|
||||
The enrollment creation date if an enrollment exists
|
||||
now() if no enrollment.
|
||||
"""
|
||||
if user is None or (course_key is None and course is None):
|
||||
raise ValueError('Both user and either course_key or course must be specified if no enrollment is provided')
|
||||
|
||||
course_key = course_key or course.id
|
||||
|
||||
full_access_masquerade = has_full_access_role_in_masquerade(user, course_key)
|
||||
if full_access_masquerade:
|
||||
return None
|
||||
elif full_access_masquerade is False:
|
||||
user = None # we are masquerading as a generic user, not a specific one -- avoid all user checks below
|
||||
|
||||
if user and user.id and has_staff_roles(user, course_key):
|
||||
return None
|
||||
|
||||
enrollment = user and CourseEnrollment.get_enrollment(user, course_key, ['fbeenrollmentexclusion'])
|
||||
|
||||
if is_in_holdback(enrollment):
|
||||
return None
|
||||
|
||||
if not correct_modes_for_fbe(enrollment=enrollment, user=user, course_key=course_key, course=course):
|
||||
return None
|
||||
|
||||
# If the user isn't enrolled, act as if the user enrolled today
|
||||
return enrollment.created if enrollment else timezone.now()
|
||||
|
||||
@@ -2,28 +2,14 @@
|
||||
Content Type Gating Configuration Models
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from lms.djangoapps.courseware.masquerade import (
|
||||
get_course_masquerade,
|
||||
get_masquerading_user_group,
|
||||
is_masquerading_as_specific_student
|
||||
)
|
||||
from openedx.core.djangoapps.config_model_utils.models import StackedConfigurationModel
|
||||
from openedx.core.djangoapps.config_model_utils.utils import is_in_holdback
|
||||
from openedx.features.content_type_gating.helpers import FULL_ACCESS, LIMITED_ACCESS, correct_modes_for_fbe
|
||||
from student.models import CourseEnrollment
|
||||
from student.role_helpers import has_staff_roles
|
||||
from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID
|
||||
from openedx.features.content_type_gating.helpers import correct_modes_for_fbe, enrollment_date_for_fbe
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
@@ -57,36 +43,7 @@ class ContentTypeGatingConfig(StackedConfigurationModel):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def has_full_access_role_in_masquerade(cls, user, course_key, course_masquerade, student_masquerade,
|
||||
user_partition):
|
||||
"""
|
||||
The roles of the masquerade user are used to determine whether the content gate displays.
|
||||
The gate will not appear if the masquerade user has any of the following roles:
|
||||
Staff, Instructor, Beta Tester, Forum Community TA, Forum Group Moderator, Forum Moderator, Forum Administrator
|
||||
"""
|
||||
if student_masquerade:
|
||||
# If a request is masquerading as a specific user, the user variable will represent the correct user.
|
||||
if user and user.id and has_staff_roles(user, course_key):
|
||||
return True
|
||||
elif user_partition:
|
||||
# If the current user is masquerading as a generic student in a specific group,
|
||||
# then return the value based on that group.
|
||||
masquerade_group = get_masquerading_user_group(course_key, user, user_partition)
|
||||
if masquerade_group is None:
|
||||
audit_mode_id = settings.COURSE_ENROLLMENT_MODES.get(CourseMode.AUDIT, {}).get('id')
|
||||
# We are checking the user partition id here because currently content
|
||||
# cannot have both the enrollment track partition and content gating partition
|
||||
# configured simultaneously. We may change this in the future and allow
|
||||
# configuring both partitions on content and selecting both partitions in masquerade.
|
||||
if course_masquerade.user_partition_id == ENROLLMENT_TRACK_PARTITION_ID:
|
||||
return course_masquerade.group_id != audit_mode_id
|
||||
elif masquerade_group is FULL_ACCESS:
|
||||
return True
|
||||
elif masquerade_group is LIMITED_ACCESS:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def enabled_for_enrollment(cls, user=None, course_key=None, user_partition=None):
|
||||
def enabled_for_enrollment(cls, user=None, course_key=None):
|
||||
"""
|
||||
Return whether Content Type Gating is enabled for this enrollment.
|
||||
|
||||
@@ -99,41 +56,9 @@ class ContentTypeGatingConfig(StackedConfigurationModel):
|
||||
user: The user being queried.
|
||||
course_key: The CourseKey of the course being queried.
|
||||
"""
|
||||
if user is None or course_key is None:
|
||||
raise ValueError('Both user and course_key must be specified if no enrollment is provided')
|
||||
|
||||
enrollment = CourseEnrollment.get_enrollment(user, course_key, ['fbeenrollmentexclusion'])
|
||||
|
||||
if user is None and enrollment is not None:
|
||||
user = enrollment.user
|
||||
|
||||
course_masquerade = get_course_masquerade(user, course_key)
|
||||
no_masquerade = course_masquerade is None
|
||||
student_masquerade = is_masquerading_as_specific_student(user, course_key)
|
||||
user_variable_represents_correct_user = (no_masquerade or student_masquerade)
|
||||
|
||||
if course_masquerade:
|
||||
if cls.has_full_access_role_in_masquerade(user, course_key, course_masquerade, student_masquerade,
|
||||
user_partition):
|
||||
return False
|
||||
# When a request is not in a masquerade state the user variable represents the correct user.
|
||||
elif user and user.id and has_staff_roles(user, course_key):
|
||||
target_datetime = enrollment_date_for_fbe(user, course_key=course_key)
|
||||
if not target_datetime:
|
||||
return False
|
||||
|
||||
# check if user is in holdback
|
||||
if user_variable_represents_correct_user and is_in_holdback(user, enrollment):
|
||||
return False
|
||||
|
||||
if not correct_modes_for_fbe(course_key, enrollment, user):
|
||||
return False
|
||||
|
||||
# enrollment might be None if the user isn't enrolled. In that case,
|
||||
# return enablement as if the user enrolled today
|
||||
# Also, ignore enrollment creation date if the user is masquerading.
|
||||
if enrollment is None or course_masquerade:
|
||||
target_datetime = timezone.now()
|
||||
else:
|
||||
target_datetime = enrollment.created
|
||||
current_config = cls.current(course_key=course_key)
|
||||
return current_config.enabled_as_of_datetime(target_datetime=target_datetime)
|
||||
|
||||
|
||||
@@ -148,8 +148,7 @@ class ContentTypeGatingPartitionScheme(object):
|
||||
"""
|
||||
Returns the Group for the specified user.
|
||||
"""
|
||||
if not ContentTypeGatingConfig.enabled_for_enrollment(user=user, course_key=course_key,
|
||||
user_partition=user_partition):
|
||||
if not ContentTypeGatingConfig.enabled_for_enrollment(user=user, course_key=course_key):
|
||||
return FULL_ACCESS
|
||||
else:
|
||||
return LIMITED_ACCESS
|
||||
|
||||
@@ -18,12 +18,8 @@ from django.contrib.auth.models import User
|
||||
from mock import patch, Mock
|
||||
from pyquery import PyQuery as pq
|
||||
|
||||
from six.moves.html_parser import HTMLParser
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from course_api.blocks.api import get_blocks
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from experiments.models import ExperimentData, ExperimentKeyValue
|
||||
from lms.djangoapps.courseware.module_render import load_single_xblock
|
||||
from lms.djangoapps.courseware.tests.factories import (
|
||||
BetaTesterFactory,
|
||||
@@ -33,6 +29,7 @@ from lms.djangoapps.courseware.tests.factories import (
|
||||
OrgStaffFactory,
|
||||
StaffFactory
|
||||
)
|
||||
from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin
|
||||
from lms.djangoapps.discussion.django_comment_client.tests.factories import RoleFactory
|
||||
from openedx.core.djangoapps.django_comment_common.models import (
|
||||
FORUM_ROLE_ADMINISTRATOR,
|
||||
@@ -166,7 +163,7 @@ def _assert_block_is_empty(block, user_id, course, request_factory):
|
||||
@override_settings(FIELD_OVERRIDE_PROVIDERS=(
|
||||
'openedx.features.content_type_gating.field_override.ContentTypeGatingFieldOverride',
|
||||
))
|
||||
class TestProblemTypeAccess(SharedModuleStoreTestCase):
|
||||
class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin):
|
||||
|
||||
PROBLEM_TYPES = ['problem', 'openassessment', 'drag-and-drop-v2', 'done', 'edx_sga']
|
||||
# 'html' is a component that just displays html, in these tests it is used to test that users who do not have access
|
||||
@@ -690,29 +687,6 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
|
||||
else:
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def update_masquerade(self, role='student', group_id=None, username=None, user_partition_id=None):
|
||||
"""
|
||||
Toggle masquerade state.
|
||||
"""
|
||||
masquerade_url = reverse(
|
||||
'masquerade_update',
|
||||
kwargs={
|
||||
'course_key_string': six.text_type(self.course.id),
|
||||
}
|
||||
)
|
||||
response = self.client.post(
|
||||
masquerade_url,
|
||||
json.dumps({
|
||||
'role': role,
|
||||
'group_id': group_id,
|
||||
'user_name': username,
|
||||
'user_partition_id': user_partition_id,
|
||||
}),
|
||||
'application/json'
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
return response
|
||||
|
||||
@ddt.data(
|
||||
InstructorFactory,
|
||||
StaffFactory,
|
||||
@@ -740,6 +714,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
|
||||
user = role_factory.create()
|
||||
else:
|
||||
user = role_factory.create(course_key=self.course.id)
|
||||
CourseEnrollment.enroll(user, self.course.id)
|
||||
self.update_masquerade(username=user.username)
|
||||
|
||||
block = self.blocks_dict['problem']
|
||||
|
||||
@@ -107,7 +107,7 @@ def check_course_expired(user, course):
|
||||
if get_course_masquerade(user, course.id):
|
||||
return ACCESS_GRANTED
|
||||
|
||||
if not CourseDurationLimitConfig.enabled_for_enrollment(user=user, course_key=course.id):
|
||||
if not CourseDurationLimitConfig.enabled_for_enrollment(user, course):
|
||||
return ACCESS_GRANTED
|
||||
|
||||
expiration_date = get_user_course_expiration_date(user, course)
|
||||
@@ -127,7 +127,7 @@ def generate_course_expired_message(user, course):
|
||||
"""
|
||||
Generate the message for the user course expiration date if it exists.
|
||||
"""
|
||||
if not CourseDurationLimitConfig.enabled_for_enrollment(user=user, course_key=course.id):
|
||||
if not CourseDurationLimitConfig.enabled_for_enrollment(user, course):
|
||||
return
|
||||
|
||||
expiration_date = get_user_course_expiration_date(user, course)
|
||||
|
||||
@@ -2,32 +2,14 @@
|
||||
Course Duration Limit Configuration Models
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from lms.djangoapps.courseware.masquerade import (
|
||||
get_course_masquerade,
|
||||
get_masquerade_role,
|
||||
is_masquerading_as_specific_student
|
||||
)
|
||||
from openedx.core.djangoapps.config_model_utils.models import StackedConfigurationModel
|
||||
from openedx.core.djangoapps.config_model_utils.utils import is_in_holdback
|
||||
from openedx.features.content_type_gating.helpers import (
|
||||
CONTENT_GATING_PARTITION_ID,
|
||||
CONTENT_TYPE_GATE_GROUP_IDS,
|
||||
correct_modes_for_fbe
|
||||
)
|
||||
from student.models import CourseEnrollment
|
||||
from student.role_helpers import has_staff_roles
|
||||
from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID
|
||||
from openedx.features.content_type_gating.helpers import correct_modes_for_fbe, enrollment_date_for_fbe
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
@@ -52,35 +34,7 @@ class CourseDurationLimitConfig(StackedConfigurationModel):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def has_full_access_role_in_masquerade(cls, user, course_key, course_masquerade):
|
||||
"""
|
||||
When masquerading, the course duration limits will never trigger the course to expire, redirecting the user.
|
||||
The roles of the masquerade user are still used to determine whether the course duration limit banner displays.
|
||||
Another banner also displays if the course is expired for the masquerade user.
|
||||
Both banners will appear if the masquerade user does not have any of the following roles:
|
||||
Staff, Instructor, Beta Tester, Forum Community TA, Forum Group Moderator, Forum Moderator, Forum Administrator
|
||||
"""
|
||||
masquerade_role = get_masquerade_role(user, course_key)
|
||||
verified_mode_id = settings.COURSE_ENROLLMENT_MODES.get(CourseMode.VERIFIED, {}).get('id')
|
||||
# Masquerading users can select the the role of a verified users without selecting a specific user
|
||||
is_verified = (course_masquerade.user_partition_id == ENROLLMENT_TRACK_PARTITION_ID
|
||||
and course_masquerade.group_id == verified_mode_id)
|
||||
# Masquerading users can select the role of staff without selecting a specific user
|
||||
is_staff = masquerade_role == 'staff'
|
||||
# Masquerading users can select other full access roles for which content type gating is disabled
|
||||
is_full_access = (course_masquerade.user_partition_id == CONTENT_GATING_PARTITION_ID
|
||||
and course_masquerade.group_id == CONTENT_TYPE_GATE_GROUP_IDS['full_access'])
|
||||
# When masquerading as a specific user, we can check that user's staff roles as we would with a normal user
|
||||
is_staff_role = False
|
||||
if course_masquerade.user_name:
|
||||
is_staff_role = has_staff_roles(user, course_key)
|
||||
|
||||
if is_verified or is_full_access or is_staff or is_staff_role:
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def enabled_for_enrollment(cls, user=None, course_key=None):
|
||||
def enabled_for_enrollment(cls, user, course):
|
||||
"""
|
||||
Return whether Course Duration Limits are enabled for this enrollment.
|
||||
|
||||
@@ -89,54 +43,15 @@ class CourseDurationLimitConfig(StackedConfigurationModel):
|
||||
such as the org, site, or globally), and if the configuration is specified to be
|
||||
``enabled_as_of`` before the enrollment was created.
|
||||
|
||||
Only one of enrollment and (user, course_key) may be specified at a time.
|
||||
|
||||
Arguments:
|
||||
enrollment: The enrollment being queried.
|
||||
user: The user being queried.
|
||||
course_key: The CourseKey of the course being queried.
|
||||
course: The CourseOverview object being queried.
|
||||
"""
|
||||
|
||||
if user is None or course_key is None:
|
||||
raise ValueError('Both user and course_key must be specified if no enrollment is provided')
|
||||
|
||||
enrollment = CourseEnrollment.get_enrollment(user, course_key, ['fbeenrollmentexclusion'])
|
||||
|
||||
if user is None and enrollment is not None:
|
||||
user = enrollment.user
|
||||
|
||||
if user and user.id:
|
||||
course_masquerade = get_course_masquerade(user, course_key)
|
||||
if course_masquerade:
|
||||
if cls.has_full_access_role_in_masquerade(user, course_key, course_masquerade):
|
||||
return False
|
||||
elif has_staff_roles(user, course_key):
|
||||
return False
|
||||
|
||||
is_masquerading = get_course_masquerade(user, course_key)
|
||||
no_masquerade = is_masquerading is None
|
||||
student_masquerade = is_masquerading_as_specific_student(user, course_key)
|
||||
|
||||
# check if user is in holdback
|
||||
if (no_masquerade or student_masquerade) and is_in_holdback(user, enrollment):
|
||||
target_datetime = enrollment_date_for_fbe(user, course=course)
|
||||
if not target_datetime:
|
||||
return False
|
||||
|
||||
not_student_masquerade = is_masquerading and not student_masquerade
|
||||
|
||||
# enrollment might be None if the user isn't enrolled. In that case,
|
||||
# return enablement as if the user enrolled today
|
||||
# When masquerading as a user group rather than a specific learner,
|
||||
# course duration limits will be on if they are on for the course.
|
||||
# When masquerading as a specific learner, course duration limits
|
||||
# will be on if they are currently on for the learner.
|
||||
if enrollment is None or not_student_masquerade:
|
||||
# we bypass enabled_for_course here and use enabled_as_of_datetime directly
|
||||
# because the correct_modes_for_fbe for FBE check contained in enabled_for_course
|
||||
# is redundant with checks done upstream of this code
|
||||
target_datetime = timezone.now()
|
||||
else:
|
||||
target_datetime = enrollment.created
|
||||
current_config = cls.current(course_key=course_key)
|
||||
current_config = cls.current(course_key=course.id)
|
||||
return current_config.enabled_as_of_datetime(target_datetime=target_datetime)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
Contains tests to verify correctness of course expiration functionality
|
||||
"""
|
||||
|
||||
|
||||
import json
|
||||
from datetime import timedelta
|
||||
|
||||
import ddt
|
||||
@@ -14,7 +12,6 @@ from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from experiments.models import ExperimentData
|
||||
from lms.djangoapps.courseware.tests.factories import (
|
||||
BetaTesterFactory,
|
||||
GlobalStaffFactory,
|
||||
@@ -23,6 +20,7 @@ from lms.djangoapps.courseware.tests.factories import (
|
||||
OrgStaffFactory,
|
||||
StaffFactory
|
||||
)
|
||||
from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin
|
||||
from lms.djangoapps.discussion.django_comment_client.tests.factories import RoleFactory
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.course_date_signals.utils import MAX_DURATION, MIN_DURATION
|
||||
@@ -46,7 +44,7 @@ from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CourseExpirationTestCase(ModuleStoreTestCase):
|
||||
class CourseExpirationTestCase(ModuleStoreTestCase, MasqueradeMixin):
|
||||
"""Tests to verify the get_user_course_expiration_date function is working correctly"""
|
||||
def setUp(self):
|
||||
super(CourseExpirationTestCase, self).setUp()
|
||||
@@ -57,7 +55,8 @@ class CourseExpirationTestCase(ModuleStoreTestCase):
|
||||
self.THREE_YEARS_AGO = now() - timedelta(days=(365 * 3))
|
||||
|
||||
# Make this a verified course so we can test expiration date
|
||||
add_course_mode(self.course, upgrade_deadline_expired=False)
|
||||
add_course_mode(self.course, mode_slug=CourseMode.AUDIT)
|
||||
add_course_mode(self.course)
|
||||
|
||||
def tearDown(self):
|
||||
CourseEnrollment.unenroll(self.user, self.course.id)
|
||||
@@ -223,29 +222,6 @@ class CourseExpirationTestCase(ModuleStoreTestCase):
|
||||
else:
|
||||
self.assertNotContains(response, banner_text)
|
||||
|
||||
def update_masquerade(self, role='student', group_id=None, username=None, user_partition_id=None):
|
||||
"""
|
||||
Toggle masquerade state.
|
||||
"""
|
||||
masquerade_url = reverse(
|
||||
'masquerade_update',
|
||||
kwargs={
|
||||
'course_key_string': six.text_type(self.course.id),
|
||||
}
|
||||
)
|
||||
response = self.client.post(
|
||||
masquerade_url,
|
||||
json.dumps({
|
||||
'role': role,
|
||||
'group_id': group_id,
|
||||
'user_name': username,
|
||||
'user_partition_id': user_partition_id,
|
||||
}),
|
||||
'application/json'
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
return response
|
||||
|
||||
@mock.patch("openedx.core.djangoapps.course_date_signals.utils.get_course_run_details")
|
||||
def test_masquerade_in_holdback(self, mock_get_course_run_details):
|
||||
mock_get_course_run_details.return_value = {'weeks_to_complete': 12}
|
||||
|
||||
@@ -74,13 +74,10 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
|
||||
user = self.user
|
||||
course_key = self.course_overview.id
|
||||
|
||||
query_count = 6
|
||||
query_count = 7
|
||||
|
||||
with self.assertNumQueries(query_count):
|
||||
enabled = CourseDurationLimitConfig.enabled_for_enrollment(
|
||||
user=user,
|
||||
course_key=course_key,
|
||||
)
|
||||
enabled = CourseDurationLimitConfig.enabled_for_enrollment(user, self.course_overview)
|
||||
self.assertEqual(not enrolled_before_enabled, enabled)
|
||||
|
||||
def test_enabled_for_enrollment_failure(self):
|
||||
|
||||
@@ -219,7 +219,7 @@ class TestCourseHomePage(CourseHomePageTestCase):
|
||||
|
||||
# Fetch the view and verify the query counts
|
||||
# TODO: decrease query count as part of REVO-28
|
||||
with self.assertNumQueries(74, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with self.assertNumQueries(72, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with check_mongo_calls(4):
|
||||
url = course_home_url(self.course)
|
||||
self.client.get(url)
|
||||
@@ -252,7 +252,8 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
|
||||
super(TestCourseHomePageAccess, self).setUp()
|
||||
|
||||
# Make this a verified course so that an upgrade message might be shown
|
||||
add_course_mode(self.course, upgrade_deadline_expired=False)
|
||||
add_course_mode(self.course, mode_slug=CourseMode.AUDIT)
|
||||
add_course_mode(self.course)
|
||||
|
||||
# Add a welcome message
|
||||
create_course_update(self.course, self.staff_user, TEST_WELCOME_MESSAGE)
|
||||
@@ -709,6 +710,7 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
|
||||
# Verify that unenrolled users visiting a course with a Master's track
|
||||
# that is not the only track are shown an enroll call to action message
|
||||
add_course_mode(self.course, CourseMode.MASTERS, 'Master\'s Mode', upgrade_deadline_expired=False)
|
||||
remove_course_mode(self.course, CourseMode.AUDIT)
|
||||
|
||||
self.create_user_for_course(self.course, CourseUserType.UNENROLLED)
|
||||
url = course_home_url(self.course)
|
||||
|
||||
@@ -26,10 +26,11 @@ from waffle.testutils import override_switch
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from lms.djangoapps.courseware.tests.factories import StaffFactory
|
||||
from lms.urls import RESET_COURSE_DEADLINES_NAME
|
||||
from gating import api as lms_gating_api
|
||||
from lms.djangoapps.courseware.tests.factories import StaffFactory
|
||||
from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin
|
||||
from lms.djangoapps.course_api.blocks.transformers.milestones import MilestonesAndSpecialExamsTransformer
|
||||
from lms.urls import RESET_COURSE_DEADLINES_NAME
|
||||
from openedx.core.djangoapps.schedules.models import Schedule
|
||||
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
|
||||
from openedx.core.djangoapps.course_date_signals.models import SelfPacedRelativeDatesConfig
|
||||
@@ -55,7 +56,7 @@ GATING_NAMESPACE_QUALIFIER = '.gating'
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestCourseOutlinePage(SharedModuleStoreTestCase):
|
||||
class TestCourseOutlinePage(SharedModuleStoreTestCase, MasqueradeMixin):
|
||||
"""
|
||||
Test the course outline view.
|
||||
"""
|
||||
@@ -241,8 +242,6 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
|
||||
enrollment = CourseEnrollment.objects.get(course_id=course.id)
|
||||
enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=30)
|
||||
enrollment.schedule.save()
|
||||
post_dict = {'reset_deadlines_redirect_url_id_dict': json.dumps({'course_id': str(course.id)})}
|
||||
course = self.courses[0]
|
||||
|
||||
student_schedule = CourseEnrollment.objects.get(course_id=course.id, user=self.user).schedule
|
||||
student_schedule.start_date = timezone.now() - datetime.timedelta(days=30)
|
||||
@@ -255,19 +254,7 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
|
||||
)
|
||||
|
||||
self.client.login(username=staff.username, password=TEST_PASSWORD)
|
||||
masquerade_url = reverse(
|
||||
'masquerade_update',
|
||||
kwargs={
|
||||
'course_key_string': six.text_type(course.id),
|
||||
}
|
||||
)
|
||||
response = self.client.post(
|
||||
masquerade_url,
|
||||
json.dumps({"role": 'student', "group_id": None, "user_name": self.user.username}),
|
||||
"application/json"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
self.update_masquerade(course=course, username=self.user.username)
|
||||
|
||||
post_dict = {'reset_deadlines_redirect_url_id_dict': json.dumps({'course_id': str(course.id)})}
|
||||
self.client.post(reverse(RESET_COURSE_DEADLINES_NAME), post_dict)
|
||||
@@ -292,19 +279,7 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
|
||||
)
|
||||
|
||||
self.client.login(username=staff.username, password=TEST_PASSWORD)
|
||||
masquerade_url = reverse(
|
||||
'masquerade_update',
|
||||
kwargs={
|
||||
'course_key_string': six.text_type(course.id),
|
||||
}
|
||||
)
|
||||
response = self.client.post(
|
||||
masquerade_url,
|
||||
json.dumps({"role": 'student', "group_id": None, "user_name": None}),
|
||||
"application/json"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
self.update_masquerade(course=course)
|
||||
|
||||
post_dict = {'reset_deadlines_redirect_url_id_dict': json.dumps({'course_id': str(course.id)})}
|
||||
self.client.post(reverse(RESET_COURSE_DEADLINES_NAME), post_dict)
|
||||
@@ -772,28 +747,10 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleT
|
||||
self.assertEqual(DEFAULT_COMPLETION_TRACKING_START, view._completion_data_collection_start())
|
||||
|
||||
|
||||
class TestCourseOutlinePreview(SharedModuleStoreTestCase):
|
||||
class TestCourseOutlinePreview(SharedModuleStoreTestCase, MasqueradeMixin):
|
||||
"""
|
||||
Unit tests for staff preview of the course outline.
|
||||
"""
|
||||
def update_masquerade(self, course, role, group_id=None, user_name=None):
|
||||
"""
|
||||
Toggle masquerade state.
|
||||
"""
|
||||
masquerade_url = reverse(
|
||||
'masquerade_update',
|
||||
kwargs={
|
||||
'course_key_string': six.text_type(course.id),
|
||||
}
|
||||
)
|
||||
response = self.client.post(
|
||||
masquerade_url,
|
||||
json.dumps({'role': role, 'group_id': group_id, 'user_name': user_name}),
|
||||
'application/json'
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
return response
|
||||
|
||||
def test_preview(self):
|
||||
"""
|
||||
Verify the behavior of preview for the course outline.
|
||||
@@ -830,7 +787,7 @@ class TestCourseOutlinePreview(SharedModuleStoreTestCase):
|
||||
self.assertContains(response, 'Future Chapter')
|
||||
|
||||
# Verify that staff masquerading as a learner see the future chapter.
|
||||
self.update_masquerade(course, role='student')
|
||||
self.update_masquerade(course=course, role='student')
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'Future Chapter')
|
||||
|
||||
@@ -134,7 +134,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase):
|
||||
|
||||
# Fetch the view and verify that the query counts haven't changed
|
||||
# TODO: decrease query count as part of REVO-28
|
||||
with self.assertNumQueries(50, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with self.assertNumQueries(48, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with check_mongo_calls(4):
|
||||
url = course_updates_url(self.course)
|
||||
self.client.get(url)
|
||||
|
||||
@@ -2,13 +2,7 @@
|
||||
Tests for masquerading functionality on course_experience
|
||||
"""
|
||||
|
||||
|
||||
import json
|
||||
|
||||
import six
|
||||
from django.urls import reverse
|
||||
|
||||
from lms.djangoapps.commerce.models import CommerceConfiguration
|
||||
from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
|
||||
from openedx.features.course_experience import DISPLAY_COURSE_SOCK_FLAG, SHOW_UPGRADE_MSG_ON_COURSE_HOME
|
||||
from student.roles import CourseStaffRole
|
||||
@@ -19,14 +13,14 @@ from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID
|
||||
from xmodule.partitions.partitions_service import PartitionService
|
||||
|
||||
from .helpers import add_course_mode
|
||||
from .test_course_home import TEST_UPDATE_MESSAGE, course_home_url
|
||||
from .test_course_home import course_home_url
|
||||
from .test_course_sock import TEST_VERIFICATION_SOCK_LOCATOR
|
||||
|
||||
TEST_PASSWORD = 'test'
|
||||
UPGRADE_MESSAGE_CONTAINER = 'section-upgrade'
|
||||
|
||||
|
||||
class MasqueradeTestBase(SharedModuleStoreTestCase):
|
||||
class MasqueradeTestBase(SharedModuleStoreTestCase, MasqueradeMixin):
|
||||
"""
|
||||
Base test class for masquerading functionality on course_experience
|
||||
"""
|
||||
@@ -66,29 +60,6 @@ class MasqueradeTestBase(SharedModuleStoreTestCase):
|
||||
return group.id
|
||||
return None
|
||||
|
||||
def update_masquerade(self, role, course, username=None, group_id=None):
|
||||
"""
|
||||
Toggle masquerade state.
|
||||
"""
|
||||
masquerade_url = reverse(
|
||||
'masquerade_update',
|
||||
kwargs={
|
||||
'course_key_string': six.text_type(course.id),
|
||||
}
|
||||
)
|
||||
response = self.client.post(
|
||||
masquerade_url,
|
||||
json.dumps({
|
||||
"role": role,
|
||||
"group_id": group_id,
|
||||
"user_name": username,
|
||||
"user_partition_id": ENROLLMENT_TRACK_PARTITION_ID
|
||||
}),
|
||||
"application/json"
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
return response
|
||||
|
||||
|
||||
class TestVerifiedUpgradesWithMasquerade(MasqueradeTestBase):
|
||||
"""
|
||||
@@ -99,7 +70,7 @@ class TestVerifiedUpgradesWithMasquerade(MasqueradeTestBase):
|
||||
@override_waffle_flag(SHOW_UPGRADE_MSG_ON_COURSE_HOME, active=True)
|
||||
def test_masquerade_as_student(self):
|
||||
# Elevate the staff user to be student
|
||||
self.update_masquerade(role='student', course=self.verified_course)
|
||||
self.update_masquerade(course=self.verified_course, user_partition_id=ENROLLMENT_TRACK_PARTITION_ID)
|
||||
response = self.client.get(course_home_url(self.verified_course))
|
||||
self.assertContains(response, TEST_VERIFICATION_SOCK_LOCATOR, html=False)
|
||||
self.assertContains(response, UPGRADE_MESSAGE_CONTAINER, html=False)
|
||||
@@ -110,7 +81,8 @@ class TestVerifiedUpgradesWithMasquerade(MasqueradeTestBase):
|
||||
self.verified_course.id,
|
||||
'Verified Certificate'
|
||||
)
|
||||
self.update_masquerade(role='student', course=self.verified_course, group_id=user_group_id)
|
||||
self.update_masquerade(course=self.verified_course, group_id=user_group_id,
|
||||
user_partition_id=ENROLLMENT_TRACK_PARTITION_ID)
|
||||
response = self.client.get(course_home_url(self.verified_course))
|
||||
self.assertNotContains(response, TEST_VERIFICATION_SOCK_LOCATOR, html=False)
|
||||
self.assertNotContains(response, UPGRADE_MESSAGE_CONTAINER, html=False)
|
||||
@@ -121,7 +93,8 @@ class TestVerifiedUpgradesWithMasquerade(MasqueradeTestBase):
|
||||
self.masters_course.id,
|
||||
'Masters'
|
||||
)
|
||||
self.update_masquerade(role='student', course=self.masters_course, group_id=user_group_id)
|
||||
self.update_masquerade(course=self.masters_course, group_id=user_group_id,
|
||||
user_partition_id=ENROLLMENT_TRACK_PARTITION_ID)
|
||||
response = self.client.get(course_home_url(self.masters_course))
|
||||
self.assertNotContains(response, TEST_VERIFICATION_SOCK_LOCATOR, html=False)
|
||||
self.assertNotContains(response, UPGRADE_MESSAGE_CONTAINER, html=False)
|
||||
|
||||
Reference in New Issue
Block a user