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:
@@ -336,17 +336,16 @@ class CourseMode(models.Model):
|
||||
@classmethod
|
||||
@request_cached(CACHE_NAMESPACE)
|
||||
def modes_for_course(
|
||||
cls, course_id=None, include_expired=False, only_selectable=True, course=None, exclude_credit=True
|
||||
cls, course_id=None, include_expired=False, only_selectable=True, course=None,
|
||||
):
|
||||
"""
|
||||
Returns a list of the non-expired modes for a given course id
|
||||
|
||||
If no modes have been set in the table, returns the default mode
|
||||
|
||||
Arguments:
|
||||
Keyword Arguments:
|
||||
course_id (CourseKey): Search for course modes for this course.
|
||||
|
||||
Keyword Arguments:
|
||||
include_expired (bool): If True, expired course modes will be included
|
||||
in the returned JSON data. If False, these modes will be omitted.
|
||||
|
||||
@@ -370,10 +369,10 @@ class CourseMode(models.Model):
|
||||
course_id = course.id
|
||||
course = None
|
||||
|
||||
if course_id is not None:
|
||||
found_course_modes = cls.objects.filter(course_id=course_id)
|
||||
if course is not None:
|
||||
found_course_modes = course.modes.all()
|
||||
else:
|
||||
found_course_modes = course.modes
|
||||
found_course_modes = cls.objects.filter(course_id=course_id)
|
||||
|
||||
# Filter out expired course modes if include_expired is not set
|
||||
if not include_expired:
|
||||
@@ -386,10 +385,7 @@ class CourseMode(models.Model):
|
||||
# we exclude them from the list if we're only looking for selectable modes
|
||||
# (e.g. on the track selection page or in the payment/verification flows).
|
||||
if only_selectable:
|
||||
if course is not None and hasattr(course, 'selectable_modes'):
|
||||
found_course_modes = course.selectable_modes
|
||||
elif exclude_credit:
|
||||
found_course_modes = found_course_modes.exclude(mode_slug__in=cls.CREDIT_MODES)
|
||||
found_course_modes = found_course_modes.exclude(mode_slug__in=cls.CREDIT_MODES)
|
||||
|
||||
modes = ([mode.to_tuple() for mode in found_course_modes])
|
||||
if not modes:
|
||||
|
||||
@@ -176,10 +176,7 @@ class ChooseModeView(View):
|
||||
user=request.user,
|
||||
course_key=course_key
|
||||
),
|
||||
"course_duration_limit_enabled": CourseDurationLimitConfig.enabled_for_enrollment(
|
||||
user=request.user,
|
||||
course_key=course_key
|
||||
),
|
||||
"course_duration_limit_enabled": CourseDurationLimitConfig.enabled_for_enrollment(request.user, course),
|
||||
}
|
||||
context.update(
|
||||
get_experiment_user_metadata_context(
|
||||
|
||||
@@ -778,7 +778,8 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
|
||||
self.override_waffle_switch(True)
|
||||
|
||||
course = CourseFactory.create(start=self.THREE_YEARS_AGO)
|
||||
add_course_mode(course, upgrade_deadline_expired=False)
|
||||
add_course_mode(course, mode_slug=CourseMode.AUDIT)
|
||||
add_course_mode(course)
|
||||
enrollment = CourseEnrollmentFactory.create(
|
||||
user=self.user,
|
||||
course_id=course.id
|
||||
|
||||
@@ -244,7 +244,7 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
|
||||
__test__ = True
|
||||
|
||||
# TODO: decrease query count as part of REVO-28
|
||||
QUERY_COUNT = 33
|
||||
QUERY_COUNT = 31
|
||||
TEST_DATA = {
|
||||
# (providers, course_width, enable_ccx, view_as_ccx): (
|
||||
# # of sql queries to default,
|
||||
@@ -273,7 +273,7 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
|
||||
__test__ = True
|
||||
|
||||
# TODO: decrease query count as part of REVO-28
|
||||
QUERY_COUNT = 33
|
||||
QUERY_COUNT = 31
|
||||
|
||||
TEST_DATA = {
|
||||
('no_overrides', 1, True, False): (QUERY_COUNT, 3),
|
||||
|
||||
@@ -395,7 +395,7 @@ class CourseListSearchViewTest(CourseApiTestViewMixin, ModuleStoreTestCase, Sear
|
||||
self.setup_user(self.audit_user)
|
||||
|
||||
# These query counts were found empirically
|
||||
query_counts = [62, 45, 45, 45, 45, 45, 45, 45, 45, 45, 15]
|
||||
query_counts = [53, 45, 45, 45, 45, 45, 45, 45, 45, 45, 15]
|
||||
ordered_course_ids = sorted([str(cid) for cid in (course_ids + [c.id for c in self.courses])])
|
||||
|
||||
self.clear_caches()
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
General view for the Course Home that contains metadata every page needs.
|
||||
"""
|
||||
|
||||
|
||||
from rest_framework.generics import RetrieveAPIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
@@ -10,6 +9,7 @@ from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from student.models import CourseEnrollment
|
||||
from lms.djangoapps.courseware.access import has_access
|
||||
from lms.djangoapps.courseware.masquerade import setup_masquerade
|
||||
from lms.djangoapps.courseware.tabs import get_course_tab_list
|
||||
from lms.djangoapps.course_api.api import course_detail
|
||||
from lms.djangoapps.course_home_api.course_metadata.v1.serializers import CourseHomeMetadataSerializer
|
||||
@@ -52,6 +52,14 @@ class CourseHomeMetadataView(RetrieveAPIView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
course_key_string = kwargs.get('course_key_string')
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
|
||||
_, request.user = setup_masquerade(
|
||||
request,
|
||||
course_key,
|
||||
staff_access=has_access(request.user, 'staff', course_key),
|
||||
reset_masquerade_data=True,
|
||||
)
|
||||
|
||||
course = course_detail(request, request.user.username, course_key)
|
||||
user_is_enrolled = CourseEnrollment.is_enrolled(request.user, course_key_string)
|
||||
|
||||
|
||||
@@ -66,3 +66,13 @@ class DatesTabTestViews(BaseCourseHomeTests):
|
||||
self.assertContains(response, 'missed_gated_content')
|
||||
self.assertContains(response, 'content_type_gating_enabled')
|
||||
self.assertContains(response, 'verified_upgrade_link')
|
||||
|
||||
@COURSE_HOME_MICROFRONTEND.override(active=True)
|
||||
@COURSE_HOME_MICROFRONTEND_DATES_TAB.override(active=True)
|
||||
def test_masquerade(self):
|
||||
self.upgrade_to_staff()
|
||||
CourseEnrollment.enroll(self.user, self.course.id, 'audit')
|
||||
self.assertTrue(self.client.get(self.url).data.get('learner_is_full_access'))
|
||||
|
||||
self.update_masquerade(role='student')
|
||||
self.assertFalse(self.client.get(self.url).data.get('learner_is_full_access'))
|
||||
|
||||
@@ -10,9 +10,11 @@ from rest_framework.response import Response
|
||||
from edx_django_utils import monitoring as monitoring_utils
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from lms.djangoapps.courseware.access import has_access
|
||||
from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs
|
||||
from lms.djangoapps.courseware.courses import get_course_date_blocks, get_course_with_access
|
||||
from lms.djangoapps.courseware.date_summary import TodaysDate, verified_upgrade_deadline_link
|
||||
from lms.djangoapps.courseware.masquerade import setup_masquerade
|
||||
from lms.djangoapps.course_home_api.dates.v1.serializers import DatesTabSerializer
|
||||
from lms.djangoapps.course_home_api.toggles import course_home_mfe_dates_tab_is_active
|
||||
from openedx.features.course_experience.utils import dates_banner_should_display
|
||||
@@ -70,6 +72,14 @@ class DatesTabView(RetrieveAPIView):
|
||||
monitoring_utils.set_custom_metric('is_staff', request.user.is_staff)
|
||||
|
||||
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=False)
|
||||
|
||||
_, request.user = setup_masquerade(
|
||||
request,
|
||||
course_key,
|
||||
staff_access=has_access(request.user, 'staff', course_key),
|
||||
reset_masquerade_data=True,
|
||||
)
|
||||
|
||||
blocks = get_course_date_blocks(course, request.user, request, include_access=True, include_past_dates=True)
|
||||
missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, request.user)
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@ Tests for Outline Tab API in the Course Home API
|
||||
"""
|
||||
|
||||
import ddt
|
||||
from course_modes.models import CourseMode
|
||||
from django.urls import reverse
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests
|
||||
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -40,7 +42,6 @@ class OutlineTabTestViews(BaseCourseHomeTests):
|
||||
def test_get_authenticated_user_not_enrolled(self):
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(response.data.get('learner_is_verified'))
|
||||
|
||||
course_tools = response.data.get('course_tools')
|
||||
self.assertEqual(len(course_tools), 0)
|
||||
@@ -56,4 +57,18 @@ class OutlineTabTestViews(BaseCourseHomeTests):
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_masquerade(self):
|
||||
user = UserFactory()
|
||||
set_user_preference(user, 'time_zone', 'Asia/Tokyo')
|
||||
CourseEnrollment.enroll(user, self.course.id)
|
||||
|
||||
self.upgrade_to_staff() # needed for masquerade
|
||||
|
||||
# Sanity check on our normal user
|
||||
self.assertEqual(self.client.get(self.url).data['dates_widget']['user_timezone'], None)
|
||||
|
||||
# Now switch users and confirm we get a different result
|
||||
self.update_masquerade(username=user.username)
|
||||
self.assertEqual(self.client.get(self.url).data['dates_widget']['user_timezone'], 'Asia/Tokyo')
|
||||
|
||||
# TODO: write test_get_unknown_course when more data is pulled into the Outline Tab API
|
||||
|
||||
@@ -8,15 +8,17 @@ from rest_framework.response import Response
|
||||
|
||||
from edx_django_utils import monitoring as monitoring_utils
|
||||
from django.urls import reverse
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from lms.djangoapps.course_api.blocks.transformers.blocks_api import BlocksAPITransformer
|
||||
from lms.djangoapps.course_home_api.outline.v1.serializers import OutlineTabSerializer
|
||||
|
||||
from lms.djangoapps.course_home_api.toggles import course_home_mfe_dates_tab_is_active
|
||||
from lms.djangoapps.courseware.access import has_access
|
||||
from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs
|
||||
from lms.djangoapps.courseware.courses import get_course_date_blocks, get_course_with_access
|
||||
from lms.djangoapps.courseware.date_summary import TodaysDate
|
||||
from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs
|
||||
from lms.djangoapps.courseware.masquerade import setup_masquerade
|
||||
from lms.djangoapps.course_home_api.utils import get_microfrontend_url
|
||||
from openedx.core.djangoapps.content.block_structure.transformers import BlockStructureTransformers
|
||||
from openedx.features.course_experience.course_tools import CourseToolsPluginManager
|
||||
@@ -78,9 +80,16 @@ class OutlineTabView(RetrieveAPIView):
|
||||
monitoring_utils.set_custom_metric('user_id', request.user.id)
|
||||
monitoring_utils.set_custom_metric('is_staff', request.user.is_staff)
|
||||
|
||||
course_tools = CourseToolsPluginManager.get_enabled_course_tools(request, course_key)
|
||||
|
||||
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=False)
|
||||
|
||||
_, request.user = setup_masquerade(
|
||||
request,
|
||||
course_key,
|
||||
staff_access=has_access(request.user, 'staff', course_key),
|
||||
reset_masquerade_data=True,
|
||||
)
|
||||
|
||||
course_tools = CourseToolsPluginManager.get_enabled_course_tools(request, course_key)
|
||||
date_blocks = get_course_date_blocks(course, request.user, request, num_assignments=1)
|
||||
|
||||
# User locale settings
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
Base classes or util functions for use in Course Home API tests
|
||||
"""
|
||||
|
||||
|
||||
import unittest
|
||||
|
||||
from datetime import datetime
|
||||
@@ -10,6 +9,7 @@ from django.conf import settings
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin
|
||||
from lms.djangoapps.verify_student.models import VerificationDeadline
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from student.tests.factories import UserFactory
|
||||
@@ -19,7 +19,7 @@ from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class BaseCourseHomeTests(SharedModuleStoreTestCase):
|
||||
class BaseCourseHomeTests(SharedModuleStoreTestCase, MasqueradeMixin):
|
||||
"""
|
||||
Base class for Course Home API tests.
|
||||
|
||||
@@ -66,3 +66,7 @@ class BaseCourseHomeTests(SharedModuleStoreTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.client.login(username=self.user.username, password='foo')
|
||||
|
||||
def upgrade_to_staff(self):
|
||||
self.user.is_staff = True
|
||||
self.user.save()
|
||||
|
||||
@@ -609,11 +609,7 @@ def get_courses(user, org=None, filter_=None):
|
||||
org=org,
|
||||
filter_=filter_,
|
||||
).prefetch_related(
|
||||
Prefetch(
|
||||
'modes',
|
||||
queryset=CourseMode.objects.exclude(mode_slug__in=CourseMode.CREDIT_MODES),
|
||||
to_attr='selectable_modes',
|
||||
),
|
||||
'modes',
|
||||
).select_related(
|
||||
'image_set'
|
||||
)
|
||||
|
||||
@@ -434,7 +434,7 @@ class CourseExpiredDate(DateSummary):
|
||||
|
||||
@property
|
||||
def date(self):
|
||||
if not CourseDurationLimitConfig.enabled_for_enrollment(user=self.user, course_key=self.course_id):
|
||||
if not CourseDurationLimitConfig.enabled_for_enrollment(self.user, self.course):
|
||||
return
|
||||
return get_user_course_expiration_date(self.user, self.course)
|
||||
|
||||
|
||||
@@ -329,6 +329,52 @@ class CourseAccessTestMixin(TestCase):
|
||||
self.assertFalse(has_access(user, action, CourseOverview.get_from_id(course.id)))
|
||||
|
||||
|
||||
class MasqueradeMixin:
|
||||
"""
|
||||
Adds masquerade utilities for your TestCase.
|
||||
|
||||
Your test case class must have self.client. And can optionally have self.course if you don't want
|
||||
to pass in the course parameter below.
|
||||
"""
|
||||
|
||||
def update_masquerade(self, course=None, role='student', group_id=None, username=None, user_partition_id=None):
|
||||
"""
|
||||
Installs a masquerade for the specified user and course, to enable
|
||||
the user to masquerade as belonging to the specific partition/group
|
||||
combination.
|
||||
|
||||
Arguments:
|
||||
course (object): a course or None for self.course
|
||||
user_partition_id (int): the integer partition id, referring to partitions already
|
||||
configured in the course.
|
||||
group_id (int); the integer group id, within the specified partition.
|
||||
username (str): user to masquerade as
|
||||
role (str): role to masquerade as
|
||||
|
||||
Returns: the response object for the AJAX call to update the user's masquerade.
|
||||
"""
|
||||
course = course or self.course
|
||||
masquerade_url = reverse(
|
||||
'masquerade_update',
|
||||
kwargs={
|
||||
'course_key_string': str(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)
|
||||
self.assertTrue(response.json()['success'], response.json().get('error'))
|
||||
return response
|
||||
|
||||
|
||||
def masquerade_as_group_member(user, course, partition_id, group_id):
|
||||
"""
|
||||
Installs a masquerade for the specified user and course, to enable
|
||||
|
||||
@@ -844,18 +844,18 @@ class CourseOverviewAccessTestCase(ModuleStoreTestCase):
|
||||
if user_attr_name == 'user_staff' and action == 'see_exists':
|
||||
# always checks staff role, and if the course has started, check the duration configuration
|
||||
if course_attr_name == 'course_started':
|
||||
num_queries = 3
|
||||
num_queries = 2
|
||||
else:
|
||||
num_queries = 1
|
||||
elif user_attr_name == 'user_normal' and action == 'see_exists':
|
||||
if course_attr_name == 'course_started':
|
||||
num_queries = 6
|
||||
num_queries = 4
|
||||
else:
|
||||
# checks staff role and enrollment data
|
||||
num_queries = 2
|
||||
elif user_attr_name == 'user_anonymous' and action == 'see_exists':
|
||||
if course_attr_name == 'course_started':
|
||||
num_queries = 3
|
||||
num_queries = 1
|
||||
else:
|
||||
num_queries = 0
|
||||
else:
|
||||
|
||||
@@ -431,8 +431,8 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest
|
||||
|
||||
def test_num_queries_instructor_paced(self):
|
||||
# TODO: decrease query count as part of REVO-28
|
||||
self.fetch_course_info_with_queries(self.instructor_paced_course, 44, 3)
|
||||
self.fetch_course_info_with_queries(self.instructor_paced_course, 42, 3)
|
||||
|
||||
def test_num_queries_self_paced(self):
|
||||
# TODO: decrease query count as part of REVO-28
|
||||
self.fetch_course_info_with_queries(self.self_paced_course, 44, 3)
|
||||
self.fetch_course_info_with_queries(self.self_paced_course, 42, 3)
|
||||
|
||||
@@ -22,7 +22,7 @@ from lms.djangoapps.courseware.masquerade import (
|
||||
CourseMasquerade, MasqueradingKeyValueStore, get_masquerading_user_group,
|
||||
)
|
||||
from lms.djangoapps.courseware.tests.factories import StaffFactory
|
||||
from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase, masquerade_as_group_member
|
||||
from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase, MasqueradeMixin, masquerade_as_group_member
|
||||
from lms.djangoapps.courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin
|
||||
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
|
||||
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
@@ -37,7 +37,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from xmodule.partitions.partitions import Group, UserPartition
|
||||
|
||||
|
||||
class MasqueradeTestCase(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
class MasqueradeTestCase(SharedModuleStoreTestCase, LoginEnrollmentTestCase, MasqueradeMixin):
|
||||
"""
|
||||
Base class for masquerade tests that sets up a test course and enrolls a user in the course.
|
||||
"""
|
||||
@@ -205,24 +205,6 @@ class StaffMasqueradeTestCase(MasqueradeTestCase):
|
||||
"""
|
||||
return StaffFactory(course_key=self.course.id)
|
||||
|
||||
def update_masquerade(self, role, group_id=None, user_name=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": user_name}),
|
||||
"application/json"
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
return response
|
||||
|
||||
|
||||
class TestStaffMasqueradeAsStudent(StaffMasqueradeTestCase):
|
||||
"""
|
||||
@@ -334,7 +316,7 @@ class TestStaffMasqueradeAsSpecificStudent(StaffMasqueradeTestCase, ProblemSubmi
|
||||
|
||||
# Masquerade as the student,enable the self paced configuration, and check we can see the info page.
|
||||
SelfPacedConfiguration(enable_course_home_improvements=True).save()
|
||||
self.update_masquerade(role='student', user_name=self.student_user.username)
|
||||
self.update_masquerade(role='student', username=self.student_user.username)
|
||||
response = self.get_course_info_page()
|
||||
self.assertContains(response, "OOGIE BLOOGIE")
|
||||
|
||||
@@ -363,7 +345,7 @@ class TestStaffMasqueradeAsSpecificStudent(StaffMasqueradeTestCase, ProblemSubmi
|
||||
self.assertEqual(self.get_progress_detail(), u'0/2')
|
||||
|
||||
# Masquerade as the student, and check we can see the student state.
|
||||
self.update_masquerade(role='student', user_name=student.username)
|
||||
self.update_masquerade(role='student', username=student.username)
|
||||
self.assertEqual(self.get_progress_detail(), u'2/2')
|
||||
|
||||
# Temporarily override the student state.
|
||||
@@ -402,7 +384,7 @@ class TestStaffMasqueradeAsSpecificStudent(StaffMasqueradeTestCase, ProblemSubmi
|
||||
|
||||
# Set student language preference and set masquerade to view same page the student.
|
||||
set_user_preference(self.student_user, preference_key=LANGUAGE_KEY, preference_value='es-419')
|
||||
self.update_masquerade(role='student', user_name=self.student_user.username)
|
||||
self.update_masquerade(role='student', username=self.student_user.username)
|
||||
|
||||
# Reload the page and check we have expected language preference in system and in cookies.
|
||||
self.get_courseware_page()
|
||||
@@ -423,7 +405,7 @@ class TestStaffMasqueradeAsSpecificStudent(StaffMasqueradeTestCase, ProblemSubmi
|
||||
self.assertIn("OOGIE BLOOGIE", content)
|
||||
|
||||
# Masquerade as the student, and check we can see the info page.
|
||||
self.update_masquerade(role='student', user_name=self.student_user.username)
|
||||
self.update_masquerade(role='student', username=self.student_user.username)
|
||||
content = self.get_course_info_page().content.decode('utf-8')
|
||||
self.assertIn("OOGIE BLOOGIE", content)
|
||||
|
||||
@@ -446,7 +428,7 @@ class TestStaffMasqueradeAsSpecificStudent(StaffMasqueradeTestCase, ProblemSubmi
|
||||
self.assertIn("1 of 2 possible points", staff_progress)
|
||||
|
||||
# Should now see the student's scores
|
||||
self.update_masquerade(role='student', user_name=self.student_user.username)
|
||||
self.update_masquerade(role='student', username=self.student_user.username)
|
||||
masquerade_progress = self.get_progress_page().content.decode('utf-8')
|
||||
self.assertNotIn("1 of 2 possible points", masquerade_progress)
|
||||
self.assertIn("2 of 2 possible points", masquerade_progress)
|
||||
|
||||
@@ -268,8 +268,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
|
||||
NUM_PROBLEMS = 20
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.mongo, 10, 167),
|
||||
(ModuleStoreEnum.Type.split, 4, 165),
|
||||
(ModuleStoreEnum.Type.mongo, 10, 170),
|
||||
(ModuleStoreEnum.Type.split, 4, 166),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count):
|
||||
@@ -1434,8 +1434,8 @@ class ProgressPageTests(ProgressPageBaseTests):
|
||||
self.assertContains(resp, u"Download Your Certificate")
|
||||
|
||||
@ddt.data(
|
||||
(True, 54),
|
||||
(False, 53),
|
||||
(True, 52),
|
||||
(False, 51),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_progress_queries_paced_courses(self, self_paced, query_count):
|
||||
@@ -1448,8 +1448,8 @@ class ProgressPageTests(ProgressPageBaseTests):
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS': False})
|
||||
@ddt.data(
|
||||
(False, 62, 40),
|
||||
(True, 53, 35)
|
||||
(False, 60, 41),
|
||||
(True, 51, 36)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_progress_queries(self, enable_waffle, initial, subsequent):
|
||||
@@ -1628,7 +1628,8 @@ class ProgressPageTests(ProgressPageBaseTests):
|
||||
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
|
||||
user = UserFactory.create()
|
||||
self.assertTrue(self.client.login(username=user.username, password='test'))
|
||||
add_course_mode(self.course, upgrade_deadline_expired=False)
|
||||
add_course_mode(self.course, mode_slug=CourseMode.AUDIT)
|
||||
add_course_mode(self.course)
|
||||
CourseEnrollmentFactory(user=user, course_id=self.course.id, mode=course_mode)
|
||||
|
||||
response = self._get_progress_page()
|
||||
@@ -2807,7 +2808,8 @@ class TestIndexViewWithCourseDurationLimits(ModuleStoreTestCase):
|
||||
"""
|
||||
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
|
||||
self.assertTrue(self.client.login(username=self.user.username, password='test'))
|
||||
add_course_mode(self.course, upgrade_deadline_expired=False)
|
||||
add_course_mode(self.course, mode_slug=CourseMode.AUDIT)
|
||||
add_course_mode(self.course)
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
'courseware_section',
|
||||
|
||||
@@ -405,8 +405,8 @@ class ViewsQueryCountTestCase(
|
||||
return inner
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.mongo, 3, 4, 40),
|
||||
(ModuleStoreEnum.Type.split, 3, 13, 40),
|
||||
(ModuleStoreEnum.Type.mongo, 3, 4, 38),
|
||||
(ModuleStoreEnum.Type.split, 3, 13, 38),
|
||||
)
|
||||
@ddt.unpack
|
||||
@count_queries
|
||||
@@ -414,8 +414,8 @@ class ViewsQueryCountTestCase(
|
||||
self.create_thread_helper(mock_request)
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.mongo, 3, 3, 36),
|
||||
(ModuleStoreEnum.Type.split, 3, 10, 36),
|
||||
(ModuleStoreEnum.Type.mongo, 3, 3, 34),
|
||||
(ModuleStoreEnum.Type.split, 3, 10, 34),
|
||||
)
|
||||
@ddt.unpack
|
||||
@count_queries
|
||||
|
||||
@@ -463,18 +463,18 @@ class SingleThreadQueryCountTestCase(ForumsEnableMixin, ModuleStoreTestCase):
|
||||
# course is outside the context manager that is verifying the number of queries,
|
||||
# and with split mongo, that method ends up querying disabled_xblocks (which is then
|
||||
# cached and hence not queried as part of call_single_thread).
|
||||
(ModuleStoreEnum.Type.mongo, False, 1, 5, 2, 23, 7),
|
||||
(ModuleStoreEnum.Type.mongo, False, 50, 5, 2, 23, 7),
|
||||
(ModuleStoreEnum.Type.mongo, False, 1, 5, 2, 21, 7),
|
||||
(ModuleStoreEnum.Type.mongo, False, 50, 5, 2, 21, 7),
|
||||
# split mongo: 3 queries, regardless of thread response size.
|
||||
(ModuleStoreEnum.Type.split, False, 1, 3, 3, 23, 7),
|
||||
(ModuleStoreEnum.Type.split, False, 50, 3, 3, 23, 7),
|
||||
(ModuleStoreEnum.Type.split, False, 1, 3, 3, 21, 8),
|
||||
(ModuleStoreEnum.Type.split, False, 50, 3, 3, 21, 8),
|
||||
|
||||
# Enabling Enterprise integration should have no effect on the number of mongo queries made.
|
||||
(ModuleStoreEnum.Type.mongo, True, 1, 5, 2, 23, 7),
|
||||
(ModuleStoreEnum.Type.mongo, True, 50, 5, 2, 23, 7),
|
||||
(ModuleStoreEnum.Type.mongo, True, 1, 5, 2, 21, 7),
|
||||
(ModuleStoreEnum.Type.mongo, True, 50, 5, 2, 21, 7),
|
||||
# split mongo: 3 queries, regardless of thread response size.
|
||||
(ModuleStoreEnum.Type.split, True, 1, 3, 3, 23, 7),
|
||||
(ModuleStoreEnum.Type.split, True, 50, 3, 3, 23, 7),
|
||||
(ModuleStoreEnum.Type.split, True, 1, 3, 3, 21, 8),
|
||||
(ModuleStoreEnum.Type.split, True, 50, 3, 3, 21, 8),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_number_of_mongo_queries(
|
||||
|
||||
@@ -348,7 +348,7 @@ def get_audit_access_expiration(user, course):
|
||||
"""
|
||||
Return the expiration date and course duration for the user's audit access to this course.
|
||||
"""
|
||||
if not CourseDurationLimitConfig.enabled_for_enrollment(user=user, course_key=course.id):
|
||||
if not CourseDurationLimitConfig.enabled_for_enrollment(user, course):
|
||||
return None, None
|
||||
|
||||
return get_user_course_expiration_date(user, course), get_user_course_duration(user, course)
|
||||
|
||||
@@ -96,7 +96,7 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Returns expiration date for a course audit expiration, if any or null
|
||||
"""
|
||||
if not CourseDurationLimitConfig.enabled_for_enrollment(user=model.user, course_key=model.course.id):
|
||||
if not CourseDurationLimitConfig.enabled_for_enrollment(model.user, model.course):
|
||||
return None
|
||||
|
||||
return get_user_course_expiration_date(model.user, model.course)
|
||||
|
||||
@@ -290,7 +290,8 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
|
||||
course = CourseFactory.create(start=self.LAST_WEEK, mobile_available=True)
|
||||
self.enroll(course.id)
|
||||
|
||||
add_course_mode(course, upgrade_deadline_expired=False)
|
||||
add_course_mode(course, mode_slug=CourseMode.AUDIT)
|
||||
add_course_mode(course)
|
||||
|
||||
def _get_enrollment_data(self, api_version, expired):
|
||||
self.login()
|
||||
|
||||
@@ -391,7 +391,7 @@ class SupportViewEnrollmentsTests(SharedModuleStoreTestCase, SupportViewTestCase
|
||||
|
||||
def _assert_generated_modes(self, response):
|
||||
"""Dry method to generate course modes dict and test with response data."""
|
||||
modes = CourseMode.modes_for_course(self.course.id, include_expired=True, exclude_credit=False)
|
||||
modes = CourseMode.modes_for_course(self.course.id, include_expired=True, only_selectable=False)
|
||||
modes_data = []
|
||||
for mode in modes:
|
||||
expiry = mode.expiration_datetime.strftime('%Y-%m-%dT%H:%M:%SZ') if mode.expiration_datetime else None
|
||||
|
||||
@@ -188,7 +188,7 @@ class EnrollmentSupportListView(GenericAPIView):
|
||||
course_modes = CourseMode.modes_for_course(
|
||||
course_key,
|
||||
include_expired=True,
|
||||
exclude_credit=False
|
||||
only_selectable=False,
|
||||
)
|
||||
return [
|
||||
ModeSerializer(mode).data
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
from student.models import FBEEnrollmentExclusion
|
||||
|
||||
|
||||
def is_in_holdback(user, enrollment):
|
||||
def is_in_holdback(enrollment):
|
||||
"""
|
||||
Return true if given user is in holdback expermiment
|
||||
"""
|
||||
|
||||
@@ -11,6 +11,7 @@ from django.conf import settings
|
||||
|
||||
from lms.djangoapps.courseware.access_utils import ACCESS_DENIED, ACCESS_GRANTED
|
||||
from lms.djangoapps.courseware.tabs import ExternalLinkCourseTab
|
||||
from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin
|
||||
from student.models import CourseEnrollment, CourseEnrollmentCelebration
|
||||
from student.tests.factories import CourseEnrollmentCelebrationFactory, UserFactory
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -159,7 +160,7 @@ class ResumeApiTestViews(BaseCoursewareTests, CompletionWaffleTestMixin):
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CelebrationApiTestViews(BaseCoursewareTests):
|
||||
class CelebrationApiTestViews(BaseCoursewareTests, MasqueradeMixin):
|
||||
"""
|
||||
Tests for the celebration API
|
||||
"""
|
||||
@@ -207,3 +208,20 @@ class CelebrationApiTestViews(BaseCoursewareTests):
|
||||
response = self.client.post('/api/courseware/celebration/course-v1:does+not+exist',
|
||||
{'first_section': True}, content_type='application/json')
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_masquerade(self):
|
||||
self.user.is_staff = True
|
||||
self.user.save()
|
||||
|
||||
user = UserFactory()
|
||||
CourseEnrollment.enroll(user, self.course.id, 'verified')
|
||||
|
||||
response = self.client.post(self.url, {'first_section': True}, content_type='application/json')
|
||||
assert response.status_code == 201
|
||||
|
||||
self.update_masquerade(username=user.username)
|
||||
response = self.client.post(self.url, {'first_section': False}, content_type='application/json')
|
||||
assert response.status_code == 202
|
||||
|
||||
celebration = CourseEnrollmentCelebration.objects.first()
|
||||
assert celebration.celebrate_first_section # make sure it didn't change during masquerade attempt
|
||||
|
||||
@@ -21,12 +21,6 @@ from edxnotes.helpers import is_feature_enabled
|
||||
from lms.djangoapps.course_api.api import course_detail
|
||||
from lms.djangoapps.courseware.access import has_access
|
||||
from lms.djangoapps.courseware.courses import check_course_access
|
||||
from lms.djangoapps.courseware.masquerade import is_masquerading
|
||||
from lms.djangoapps.courseware.masquerade import is_masquerading_as_audit_enrollment
|
||||
from lms.djangoapps.courseware.masquerade import is_masquerading_as_full_access
|
||||
from lms.djangoapps.courseware.masquerade import is_masquerading_as_limited_access
|
||||
from lms.djangoapps.courseware.masquerade import is_masquerading_as_non_audit_enrollment
|
||||
from lms.djangoapps.courseware.masquerade import is_masquerading_as_staff
|
||||
from lms.djangoapps.courseware.masquerade import setup_masquerade
|
||||
from lms.djangoapps.courseware.module_render import get_module_by_usage_id
|
||||
from lms.djangoapps.courseware.tabs import get_course_tab_list
|
||||
@@ -96,27 +90,10 @@ class CoursewareMeta:
|
||||
|
||||
@property
|
||||
def content_type_gating_enabled(self):
|
||||
course_key = self.course_key
|
||||
user = self.effective_user
|
||||
is_enabled = None
|
||||
course_masquerade = self.course_masquerade
|
||||
if is_masquerading(user, course_key, course_masquerade):
|
||||
if is_masquerading_as_staff(user, course_key):
|
||||
is_enabled = False
|
||||
elif is_masquerading_as_full_access(user, course_key, course_masquerade):
|
||||
is_enabled = False
|
||||
elif is_masquerading_as_non_audit_enrollment(user, course_key, course_masquerade):
|
||||
is_enabled = False
|
||||
elif is_masquerading_as_audit_enrollment(user, course_key, course_masquerade):
|
||||
is_enabled = ContentTypeGatingConfig.enabled_for_course(course_key)
|
||||
elif is_masquerading_as_limited_access(user, course_key, course_masquerade):
|
||||
is_enabled = ContentTypeGatingConfig.enabled_for_course(course_key)
|
||||
if is_enabled is None:
|
||||
is_enabled = ContentTypeGatingConfig.enabled_for_enrollment(
|
||||
user=user,
|
||||
course_key=course_key,
|
||||
)
|
||||
return is_enabled
|
||||
return ContentTypeGatingConfig.enabled_for_enrollment(
|
||||
user=self.effective_user,
|
||||
course_key=self.course_key,
|
||||
)
|
||||
|
||||
@property
|
||||
def can_show_upgrade_sock(self):
|
||||
@@ -396,7 +373,7 @@ class Celebration(DeveloperErrorViewMixin, APIView):
|
||||
|
||||
**Returns**
|
||||
|
||||
* 200 or 201 on success with above fields.
|
||||
* 200 or 201 or 202 on success with above fields.
|
||||
* 400 if an invalid parameter was sent.
|
||||
* 404 if the course is not available or cannot be seen.
|
||||
"""
|
||||
@@ -414,6 +391,16 @@ class Celebration(DeveloperErrorViewMixin, APIView):
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
|
||||
# Check if we're masquerading as someone else. If so, we should just ignore this request.
|
||||
_, user = setup_masquerade(
|
||||
request,
|
||||
course_key,
|
||||
staff_access=has_access(request.user, 'staff', course_key),
|
||||
reset_masquerade_data=True,
|
||||
)
|
||||
if user != request.user:
|
||||
return Response(status=202) # "Accepted"
|
||||
|
||||
data = dict(request.data)
|
||||
first_section = data.pop('first_section', None)
|
||||
if data:
|
||||
|
||||
@@ -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