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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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)