Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -281,15 +281,20 @@ def get_user_course_outline(course_key: CourseKey,
@function_trace('learning_sequences.api.get_user_course_outline_details')
def get_user_course_outline_details(course_key: CourseKey,
user: types.User,
at_time: datetime) -> UserCourseOutlineDetailsData:
at_time: datetime,
preview_verified_content: bool = False) -> UserCourseOutlineDetailsData:
"""
Get an outline with supplementary data like scheduling information.

See the definition of UserCourseOutlineDetailsData for details about the
data returned.

preview_verified_content can be set to True to have the function also return info about content
that the learner can see but not access due to enrollment track partitions removing verified-only
content from an audit learner's outline
"""
user_course_outline, processors = _get_user_course_outline_and_processors(
course_key, user, at_time
course_key, user, at_time, preview_verified_content=preview_verified_content
)
with function_trace('learning_sequences.api.get_user_course_outline_details.schedule'):
schedule_processor = processors['schedule']
Expand Down Expand Up @@ -348,7 +353,6 @@ def _get_user_course_outline_and_processors(course_key: CourseKey, # lint-amnes
usage_keys_to_remove = set()
inaccessible_sequences = set()
preview_usage_keys = set()

for name, processor_cls in processor_classes:
# Future optimization: This should be parallelizable (don't rely on a
# particular ordering).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,24 @@
Where possible, seed data using public API methods (e.g. replace_course_outline
from this app, edx-when's set_dates_for_course).
"""
import attr
from contextlib import contextmanager
from datetime import datetime, timezone

import ddt
from unittest.mock import patch
from edx_when.api import set_dates_for_course
from opaque_keys.edx.keys import CourseKey
from rest_framework.test import APITestCase, APIClient

from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole
from common.djangoapps.student.tests.factories import UserFactory
from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms

from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID
from ..api import replace_course_outline
from ..api.tests.test_data import generate_sections
from ..data import CourseOutlineData, CourseVisibility
Expand Down Expand Up @@ -296,6 +301,179 @@ def test_target_user_takes_precedence(self):
assert result.data['username'] == 'student2'


@ddt.ddt
@skip_unless_lms
class CourseOutlineViewAuditPreviewTest(CacheIsolationTestCase, APITestCase):
"""
Tests for the CourseOutlineView with regards to audit preview feature
"""
@classmethod
def setUpTestData(cls):
cls.course_key = CourseKey.from_string("course-v1:edX+Audit+Preview")
cls.course_url = outline_url(cls.course_key)
start_date = datetime(2020, 5, 20, tzinfo=timezone.utc)

set_dates_for_course(
cls.course_key,
[
(
cls.course_key.make_usage_key('course', 'course'),
{'start': start_date}
)
]
)

# Create a course with three sections with 2, 2, and 3 subsections respectively
outline = CourseOutlineData(
course_key=cls.course_key,
title="Views Test Course!",
published_at=start_date,
published_version="5ebece4b69dd593d82fe2020",
entrance_exam_id=None,
days_early_for_beta=None,
sections=generate_sections(cls.course_key, [2, 2, 3]),
self_paced=False,
course_visibility=CourseVisibility.PUBLIC
)

# Restrict the first section to verified only
verified_section = attr.evolve(
outline.sections[0],
user_partition_groups={
ENROLLMENT_TRACK_PARTITION_ID: [2]
}
)
outline.sections[0] = verified_section

# Restrict the first subsection of the second section to verified only
verified_sequence = attr.evolve(
outline.sections[1].sequences[0],
user_partition_groups={
ENROLLMENT_TRACK_PARTITION_ID: [2]
}
)
outline.sections[1].sequences[0] = verified_sequence

replace_course_outline(outline)
cls.outline = outline

CourseMode.objects.create(
course_id=cls.course_key,
mode_slug=CourseMode.AUDIT,
mode_display_name='Audit',
)
CourseMode.objects.create(
course_id=cls.course_key,
mode_slug=CourseMode.VERIFIED,
mode_display_name='Verified',
min_price=50
)

cls.verified_student = UserFactory.create(
username='verified_student', email='verified_student@example.com', is_staff=False, password='student_pass'
)
cls.audit_student = UserFactory.create(
username='audit_student', email='audit_student@example.com', is_staff=False, password='student_pass'
)
CourseEnrollment.enroll(cls.audit_student, cls.course_key, CourseMode.AUDIT)
CourseEnrollment.enroll(cls.verified_student, cls.course_key, CourseMode.VERIFIED)

def setUp(self):
super().setUp()
self.client = APIClient()

@contextmanager
def set_preview_enabled(self, enabled):
"""
Helper contextmanager to make mocking more legible
"""
with patch(
'openedx.core.djangoapps.content.learning_sequences.views.learner_can_preview_verified_content',
return_value=enabled
):
yield

@ddt.unpack
@ddt.data(
(
'verified_student',
False,
{
'ch_1': {
'seq_1_0': (True, False),
'seq_1_1': (True, False),
},
'ch_2': {
'seq_2_0': (True, False),
'seq_2_1': (True, False),
},
'ch_3': {
'seq_3_0': (True, False),
'seq_3_1': (True, False),
'seq_3_2': (True, False),
}
}
),
(
'audit_student',
True,
{
'ch_1': {
'seq_1_0': (True, True),
'seq_1_1': (True, True),
},
'ch_2': {
'seq_2_0': (True, True),
'seq_2_1': (True, False),
},
'ch_3': {
'seq_3_0': (True, False),
'seq_3_1': (True, False),
'seq_3_2': (True, False),
}
}
),
(
'audit_student',
False,
{
'ch_2': {
'seq_2_1': (True, False),
},
'ch_3': {
'seq_3_0': (True, False),
'seq_3_1': (True, False),
'seq_3_2': (True, False),
}
}
),
)
def test_audit_preview(self, username, preview_enabled, expected_outline):
"""
Call the endpoint with the given username and the given enablement of the
audit preview. The outline returned should match what we expect
"""
self.client.login(username=username, password='student_pass')
with self.set_preview_enabled(preview_enabled):
result = self.client.get(outline_url(self.course_key))
outline = result.data['outline']
block_template = 'block-v1:edX+Audit+Preview+type@{block_type}+block@{block_id}'
self.assertEqual(len(outline['sections']), len(expected_outline))
actual_sections = {section['id']: section for section in outline['sections']}

for section_id, sequences in expected_outline.items():
actual_section = actual_sections.get(block_template.format(block_type="chapter", block_id=section_id))
self.assertIsNotNone(actual_section, section_id)
self.assertEqual(len(actual_section['sequence_ids']), len(sequences), section_id)
for sequence_id, access in sequences.items():
actual_sequence_id = block_template.format(block_type="sequential", block_id=sequence_id)
self.assertIn(actual_sequence_id, actual_section['sequence_ids'], sequence_id)
actual_sequence = outline['sequences'].get(actual_sequence_id)
self.assertIsNotNone(actual_sequence, sequence_id)
self.assertEqual(access[0], actual_sequence['accessible'], sequence_id)
self.assertEqual(access[1], actual_sequence['previewable'], sequence_id)


def outline_url(course_key):
"""Helper: Course outline URL for a given course key."""
return f'/api/learning_sequences/v1/course_outline/{course_key}'
14 changes: 11 additions & 3 deletions openedx/core/djangoapps/content/learning_sequences/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.courseware.masquerade import setup_masquerade
from lms.djangoapps.course_home_api.toggles import learner_can_preview_verified_content

from openedx.core import types
from openedx.core.lib.api.view_utils import validate_course_key

Expand Down Expand Up @@ -95,13 +97,16 @@ def to_representation(self, user_course_outline_details): # lint-amnesty, pylin
schedule.sequences.get(seq_usage_key),
exam_information.sequences.get(seq_usage_key, {}),
user_course_outline.accessible_sequences,
user_course_outline.previewable_sequences,
)
for seq_usage_key, sequence in user_course_outline.sequences.items()
},
},
}

def _sequence_repr(self, sequence, sequence_schedule, sequence_exam, accessible_sequences):
def _sequence_repr(
self, sequence, sequence_schedule, sequence_exam, accessible_sequences, previewable_sequences
):
"""Representation of a Sequence."""
if sequence_schedule is None:
schedule_item_dict = {'start': None, 'effective_start': None, 'due': None}
Expand All @@ -117,6 +122,7 @@ def _sequence_repr(self, sequence, sequence_schedule, sequence_exam, accessible_
"id": str(sequence.usage_key),
"title": sequence.title,
"accessible": sequence.usage_key in accessible_sequences,
"previewable": sequence.usage_key in previewable_sequences,
"inaccessible_after_due": sequence.inaccessible_after_due,
**schedule_item_dict,
}
Expand Down Expand Up @@ -165,10 +171,12 @@ def get(self, request, course_key_str, format=None): # lint-amnesty, pylint: di

# Get target user (and override request user for the benefit of any waffle checks)
request.user = self._determine_user(request, course_key)

preview_verified_content = learner_can_preview_verified_content(course_key, request.user)
try:
# Grab the user's outline and send our response...
user_course_outline_details = get_user_course_outline_details(course_key, request.user, at_time)
user_course_outline_details = get_user_course_outline_details(
course_key, request.user, at_time, preview_verified_content
)
except CourseOutlineData.DoesNotExist as does_not_exist_err:
if not request.user.id:
# Outline is private or doesn't exist. But don't leak whether a course exists or not to anonymous
Expand Down
Loading