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:
Michael Terry
2020-06-22 17:40:08 -04:00
parent cf5f9482e3
commit 3030efec78
40 changed files with 318 additions and 447 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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