refactor: Remove legacy course info page & related code
This commit is contained in:
2
.github/workflows/pylint-checks.yml
vendored
2
.github/workflows/pylint-checks.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
- module-name: openedx-1
|
||||
path: "openedx/core/types/ openedx/core/djangoapps/ace_common/ openedx/core/djangoapps/agreements/ openedx/core/djangoapps/api_admin/ openedx/core/djangoapps/auth_exchange/ openedx/core/djangoapps/bookmarks/ openedx/core/djangoapps/cache_toolbox/ openedx/core/djangoapps/catalog/ openedx/core/djangoapps/ccxcon/ openedx/core/djangoapps/commerce/ openedx/core/djangoapps/common_initialization/ openedx/core/djangoapps/common_views/ openedx/core/djangoapps/config_model_utils/ openedx/core/djangoapps/content/ openedx/core/djangoapps/content_libraries/ openedx/core/djangoapps/contentserver/ openedx/core/djangoapps/cookie_metadata/ openedx/core/djangoapps/cors_csrf/ openedx/core/djangoapps/course_apps/ openedx/core/djangoapps/course_date_signals/ openedx/core/djangoapps/course_groups/ openedx/core/djangoapps/courseware_api/ openedx/core/djangoapps/crawlers/ openedx/core/djangoapps/credentials/ openedx/core/djangoapps/credit/ openedx/core/djangoapps/dark_lang/ openedx/core/djangoapps/debug/ openedx/core/djangoapps/demographics/ openedx/core/djangoapps/discussions/ openedx/core/djangoapps/django_comment_common/ openedx/core/djangoapps/embargo/ openedx/core/djangoapps/enrollments/ openedx/core/djangoapps/external_user_ids/ openedx/core/djangoapps/zendesk_proxy/ openedx/core/djangolib/ openedx/core/lib/ openedx/core/tests/ openedx/core/djangoapps/course_live/"
|
||||
- module-name: openedx-2
|
||||
path: "openedx/core/djangoapps/geoinfo/ openedx/core/djangoapps/header_control/ openedx/core/djangoapps/heartbeat/ openedx/core/djangoapps/lang_pref/ openedx/core/djangoapps/models/ openedx/core/djangoapps/monkey_patch/ openedx/core/djangoapps/oauth_dispatch/ openedx/core/djangoapps/olx_rest_api/ openedx/core/djangoapps/password_policy/ openedx/core/djangoapps/plugin_api/ openedx/core/djangoapps/plugins/ openedx/core/djangoapps/profile_images/ openedx/core/djangoapps/programs/ openedx/core/djangoapps/safe_sessions/ openedx/core/djangoapps/schedules/ openedx/core/djangoapps/self_paced/ openedx/core/djangoapps/service_status/ openedx/core/djangoapps/session_inactivity_timeout/ openedx/core/djangoapps/signals/ openedx/core/djangoapps/site_configuration/ openedx/core/djangoapps/system_wide_roles/ openedx/core/djangoapps/theming/ openedx/core/djangoapps/user_api/ openedx/core/djangoapps/user_authn/ openedx/core/djangoapps/util/ openedx/core/djangoapps/verified_track_content/ openedx/core/djangoapps/video_config/ openedx/core/djangoapps/video_pipeline/ openedx/core/djangoapps/waffle_utils/ openedx/core/djangoapps/xblock/ openedx/core/djangoapps/xmodule_django/ openedx/core/tests/ openedx/features/ openedx/testing/ openedx/tests/ openedx/core/djangoapps/learner_pathway/"
|
||||
path: "openedx/core/djangoapps/geoinfo/ openedx/core/djangoapps/header_control/ openedx/core/djangoapps/heartbeat/ openedx/core/djangoapps/lang_pref/ openedx/core/djangoapps/models/ openedx/core/djangoapps/monkey_patch/ openedx/core/djangoapps/oauth_dispatch/ openedx/core/djangoapps/olx_rest_api/ openedx/core/djangoapps/password_policy/ openedx/core/djangoapps/plugin_api/ openedx/core/djangoapps/plugins/ openedx/core/djangoapps/profile_images/ openedx/core/djangoapps/programs/ openedx/core/djangoapps/safe_sessions/ openedx/core/djangoapps/schedules/ openedx/core/djangoapps/service_status/ openedx/core/djangoapps/session_inactivity_timeout/ openedx/core/djangoapps/signals/ openedx/core/djangoapps/site_configuration/ openedx/core/djangoapps/system_wide_roles/ openedx/core/djangoapps/theming/ openedx/core/djangoapps/user_api/ openedx/core/djangoapps/user_authn/ openedx/core/djangoapps/util/ openedx/core/djangoapps/verified_track_content/ openedx/core/djangoapps/video_config/ openedx/core/djangoapps/video_pipeline/ openedx/core/djangoapps/waffle_utils/ openedx/core/djangoapps/xblock/ openedx/core/djangoapps/xmodule_django/ openedx/core/tests/ openedx/features/ openedx/testing/ openedx/tests/ openedx/core/djangoapps/learner_pathway/"
|
||||
- module-name: common
|
||||
path: "common"
|
||||
- module-name: cms
|
||||
|
||||
2
.github/workflows/unit-test-shards.json
vendored
2
.github/workflows/unit-test-shards.json
vendored
@@ -132,7 +132,6 @@
|
||||
"openedx/core/djangoapps/programs/",
|
||||
"openedx/core/djangoapps/safe_sessions/",
|
||||
"openedx/core/djangoapps/schedules/",
|
||||
"openedx/core/djangoapps/self_paced/",
|
||||
"openedx/core/djangoapps/service_status/",
|
||||
"openedx/core/djangoapps/session_inactivity_timeout/",
|
||||
"openedx/core/djangoapps/signals/",
|
||||
@@ -212,7 +211,6 @@
|
||||
"openedx/core/djangoapps/programs/",
|
||||
"openedx/core/djangoapps/safe_sessions/",
|
||||
"openedx/core/djangoapps/schedules/",
|
||||
"openedx/core/djangoapps/self_paced/",
|
||||
"openedx/core/djangoapps/service_status/",
|
||||
"openedx/core/djangoapps/session_inactivity_timeout/",
|
||||
"openedx/core/djangoapps/signals/",
|
||||
|
||||
@@ -33,7 +33,7 @@ def print_course(course):
|
||||
|
||||
|
||||
# course.tabs looks like this
|
||||
# [{u'type': u'courseware'}, {u'type': u'course_info', u'name': u'Course Info'}, {u'type': u'textbooks'},
|
||||
# [{u'type': u'courseware'}, {u'type': u'textbooks'},
|
||||
# {u'type': u'discussion', u'name': u'Discussion'}, {u'type': u'wiki', u'name': u'Wiki'},
|
||||
# {u'type': u'progress', u'name': u'Progress'}]
|
||||
|
||||
@@ -53,7 +53,7 @@ command again, adding --insert or --delete to edit the list.
|
||||
|
||||
course_help = '--course <id> required, e.g. Stanford/CS99/2013_spring'
|
||||
delete_help = '--delete <tab-number>'
|
||||
insert_help = '--insert <tab-number> <type> <name>, e.g. 4 "course_info" "Course Info"'
|
||||
insert_help = '--insert <tab-number> <type> <name>, e.g. 4 "discussion" "Discussion"'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--course',
|
||||
|
||||
@@ -40,13 +40,13 @@ class BackfillCourseTabsTest(ModuleStoreTestCase):
|
||||
course = CourseFactory()
|
||||
course.tabs = [tab for tab in course.tabs if tab.type != 'dates']
|
||||
self.update_course(course, ModuleStoreEnum.UserID.test)
|
||||
assert len(course.tabs) == 6
|
||||
assert len(course.tabs) == 5
|
||||
assert 'dates' not in {tab.type for tab in course.tabs}
|
||||
|
||||
call_command('backfill_course_tabs')
|
||||
|
||||
course = self.store.get_course(course.id)
|
||||
assert len(course.tabs) == 7
|
||||
assert len(course.tabs) == 6
|
||||
assert 'dates' in {tab.type for tab in course.tabs}
|
||||
mock_logger.info.assert_any_call(f'Updating tabs for {course.id}.')
|
||||
mock_logger.info.assert_any_call(f'Successfully updated tabs for {course.id}.')
|
||||
@@ -66,16 +66,16 @@ class BackfillCourseTabsTest(ModuleStoreTestCase):
|
||||
CourseFactory()
|
||||
CourseFactory()
|
||||
course = CourseFactory()
|
||||
course.tabs = [tab for tab in course.tabs if tab.type in ('course_info', 'courseware')]
|
||||
course.tabs = [tab for tab in course.tabs if tab.type == 'courseware']
|
||||
self.update_course(course, ModuleStoreEnum.UserID.test)
|
||||
assert len(course.tabs) == 2
|
||||
assert len(course.tabs) == 1
|
||||
assert 'dates' not in {tab.type for tab in course.tabs}
|
||||
assert 'progress' not in {tab.type for tab in course.tabs}
|
||||
|
||||
call_command('backfill_course_tabs')
|
||||
|
||||
course = self.store.get_course(course.id)
|
||||
assert len(course.tabs) == 7
|
||||
assert len(course.tabs) == 6
|
||||
assert 'dates' in {tab.type for tab in course.tabs}
|
||||
assert 'progress' in {tab.type for tab in course.tabs}
|
||||
mock_logger.info.assert_any_call('4 courses read from modulestore. Processing 0 to 4.')
|
||||
@@ -99,8 +99,8 @@ class BackfillCourseTabsTest(ModuleStoreTestCase):
|
||||
course_2 = CourseFactory()
|
||||
course_2.tabs = [tab for tab in course_2.tabs if tab.type != 'progress']
|
||||
self.update_course(course_2, ModuleStoreEnum.UserID.test)
|
||||
assert len(course_1.tabs) == 6
|
||||
assert len(course_2.tabs) == 6
|
||||
assert len(course_1.tabs) == 5
|
||||
assert len(course_2.tabs) == 5
|
||||
assert 'dates' not in {tab.type for tab in course_1.tabs}
|
||||
assert 'progress' not in {tab.type for tab in course_2.tabs}
|
||||
|
||||
@@ -108,8 +108,8 @@ class BackfillCourseTabsTest(ModuleStoreTestCase):
|
||||
|
||||
course_1 = self.store.get_course(course_1.id)
|
||||
course_2 = self.store.get_course(course_2.id)
|
||||
assert len(course_1.tabs) == 7
|
||||
assert len(course_2.tabs) == 7
|
||||
assert len(course_1.tabs) == 6
|
||||
assert len(course_2.tabs) == 6
|
||||
assert 'dates' in {tab.type for tab in course_1.tabs}
|
||||
assert 'progress' in {tab.type for tab in course_2.tabs}
|
||||
mock_logger.info.assert_any_call('2 courses read from modulestore. Processing 0 to 2.')
|
||||
@@ -168,13 +168,13 @@ class BackfillCourseTabsTest(ModuleStoreTestCase):
|
||||
def test_arguments_batching(self, start, count, expected_tabs_modified):
|
||||
courses = CourseFactory.create_batch(4)
|
||||
for course in courses:
|
||||
course.tabs = [tab for tab in course.tabs if tab.type in ('course_info', 'courseware')]
|
||||
course.tabs = [tab for tab in course.tabs if tab.type == 'courseware']
|
||||
course = self.update_course(course, ModuleStoreEnum.UserID.test)
|
||||
assert len(course.tabs) == 2
|
||||
assert len(course.tabs) == 1
|
||||
|
||||
BackfillCourseTabsConfig.objects.create(enabled=True, start_index=start, count=count)
|
||||
call_command('backfill_course_tabs')
|
||||
|
||||
for i, course in enumerate(courses):
|
||||
course = self.store.get_course(course.id)
|
||||
assert len(course.tabs) == (7 if expected_tabs_modified[i] else 2), f'Wrong tabs for course index {i}'
|
||||
assert len(course.tabs) == (6 if expected_tabs_modified[i] else 1), f'Wrong tabs for course index {i}'
|
||||
|
||||
@@ -173,7 +173,7 @@ class ContentStoreImportTest(ModuleStoreTestCase):
|
||||
def test_tab_name_imports_correctly(self):
|
||||
_module_store, _content_store, course = self.load_test_import_course()
|
||||
print(f"course tabs = {course.tabs}")
|
||||
self.assertEqual(course.tabs[2]['name'], 'Syllabus')
|
||||
self.assertEqual(course.tabs[1]['name'], 'Syllabus')
|
||||
|
||||
def test_import_performance_mongo(self):
|
||||
store = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo)
|
||||
|
||||
@@ -220,8 +220,8 @@ def get_tab_by_locator(tab_list: List[CourseTab], tab_location: Union[str, Usage
|
||||
|
||||
def validate_args(num, tab_type):
|
||||
"Throws for the disallowed cases."
|
||||
if num <= 1:
|
||||
raise ValueError('Tabs 1 and 2 cannot be edited')
|
||||
if num < 1:
|
||||
raise ValueError('Tab 1 cannot be edited')
|
||||
if tab_type == 'static_tab':
|
||||
raise ValueError('Tabs of type static_tab cannot be edited here (use Studio)')
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ class TabsPageTests(CourseTestCase):
|
||||
def test_reorder_tabs_invalid_tab(self):
|
||||
"""Test re-ordering of tabs with invalid tab"""
|
||||
|
||||
invalid_tab_ids = ['courseware', 'info', 'invalid_tab_id']
|
||||
invalid_tab_ids = ['courseware', 'invalid_tab_id']
|
||||
|
||||
# post the request
|
||||
resp = self.client.ajax_post(
|
||||
@@ -189,16 +189,14 @@ class PrimitiveTabEdit(ModuleStoreTestCase):
|
||||
course = CourseFactory.create()
|
||||
with self.assertRaises(ValueError):
|
||||
tabs.primitive_delete(course, 0)
|
||||
with self.assertRaises(ValueError):
|
||||
tabs.primitive_delete(course, 1)
|
||||
with self.assertRaises(IndexError):
|
||||
tabs.primitive_delete(course, 7)
|
||||
tabs.primitive_delete(course, 6)
|
||||
|
||||
assert course.tabs[2] != {'type': 'dates', 'name': 'Dates'}
|
||||
tabs.primitive_delete(course, 2)
|
||||
assert course.tabs[1] != {'type': 'dates', 'name': 'Dates'}
|
||||
tabs.primitive_delete(course, 1)
|
||||
assert {'type': 'progress'} not in course.tabs
|
||||
# Check that dates has shifted up
|
||||
assert course.tabs[2] == {'type': 'dates', 'name': 'Dates'}
|
||||
assert course.tabs[1] == {'type': 'dates', 'name': 'Dates'}
|
||||
|
||||
def test_insert(self):
|
||||
"""Test primitive tab insertion."""
|
||||
|
||||
@@ -1661,9 +1661,6 @@ INSTALLED_APPS = [
|
||||
# edx-milestones service
|
||||
'milestones',
|
||||
|
||||
# Self-paced course configuration
|
||||
'openedx.core.djangoapps.self_paced',
|
||||
|
||||
# Coursegraph
|
||||
'cms.djangoapps.coursegraph.apps.CoursegraphConfig',
|
||||
|
||||
|
||||
@@ -167,11 +167,6 @@ def calculate(request):
|
||||
return HttpResponse(json.dumps({'result': str(result)})) # lint-amnesty, pylint: disable=http-response-with-json-dumps
|
||||
|
||||
|
||||
def info(request):
|
||||
""" Info page (link from main header) """
|
||||
return render_to_response("info.html", {})
|
||||
|
||||
|
||||
def add_p3p_header(view_func):
|
||||
"""
|
||||
This decorator should only be used with views which may be displayed through the iframe.
|
||||
|
||||
@@ -532,14 +532,6 @@ class CourseFields: # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
scope=Scope.settings
|
||||
)
|
||||
has_children = True
|
||||
info_sidebar_name = String(
|
||||
display_name=_("Course Home Sidebar Name"),
|
||||
help=_(
|
||||
"Enter the heading that you want students to see above your course handouts on the Course Home page. "
|
||||
"Your course handouts appear in the right panel of the page."
|
||||
),
|
||||
deprecated=True,
|
||||
scope=Scope.settings, default=_('Course Handouts'))
|
||||
show_timezone = Boolean(
|
||||
help=_(
|
||||
"True if timezones should be shown on dates in the course. "
|
||||
|
||||
@@ -97,7 +97,6 @@ class SplitModuleTest(unittest.TestCase):
|
||||
"fields": {
|
||||
"tabs": [
|
||||
CourseTab.load('courseware'),
|
||||
CourseTab.load('course_info'),
|
||||
CourseTab.load('discussion'),
|
||||
CourseTab.load('wiki'),
|
||||
],
|
||||
@@ -147,7 +146,6 @@ class SplitModuleTest(unittest.TestCase):
|
||||
"end": _date_field.from_json("2013-04-13T04:30"),
|
||||
"tabs": [
|
||||
CourseTab.load('courseware'),
|
||||
CourseTab.load('course_info'),
|
||||
CourseTab.load('discussion'),
|
||||
CourseTab.load('wiki'),
|
||||
CourseTab.load(
|
||||
@@ -320,7 +318,6 @@ class SplitModuleTest(unittest.TestCase):
|
||||
"fields": {
|
||||
"tabs": [
|
||||
CourseTab.load('courseware'),
|
||||
CourseTab.load('course_info'),
|
||||
CourseTab.load('discussion'),
|
||||
CourseTab.load('wiki'),
|
||||
],
|
||||
@@ -417,7 +414,6 @@ class SplitModuleTest(unittest.TestCase):
|
||||
"fields": {
|
||||
"tabs": [
|
||||
CourseTab.load('courseware'),
|
||||
CourseTab.load('course_info'),
|
||||
CourseTab.load('discussion'),
|
||||
CourseTab.load('wiki'),
|
||||
],
|
||||
@@ -581,7 +577,7 @@ class SplitModuleCourseTests(SplitModuleTest):
|
||||
course = self.findByIdInResult(courses, "head12345")
|
||||
assert course.location.org == 'testx'
|
||||
assert course.category == 'course', 'wrong category'
|
||||
assert len(course.tabs) == 6, 'wrong number of tabs'
|
||||
assert len(course.tabs) == 5, 'wrong number of tabs'
|
||||
assert course.display_name == 'The Ancient Greek Hero', 'wrong display name'
|
||||
assert course.advertised_start == 'Fall 2013', 'advertised_start'
|
||||
assert len(course.children) == 4, 'children'
|
||||
@@ -635,7 +631,7 @@ class SplitModuleCourseTests(SplitModuleTest):
|
||||
assert course.location.course_key.org == 'testx'
|
||||
assert course.location.course_key.course == 'wonderful'
|
||||
assert course.category == 'course', 'wrong category'
|
||||
assert len(course.tabs) == 4, 'wrong number of tabs'
|
||||
assert len(course.tabs) == 3, 'wrong number of tabs'
|
||||
assert course.display_name == 'The most wonderful course', course.display_name
|
||||
assert course.advertised_start is None
|
||||
assert len(course.children) == 0, 'children'
|
||||
@@ -665,7 +661,7 @@ class SplitModuleCourseTests(SplitModuleTest):
|
||||
assert course.location.course_key.org is None
|
||||
assert course.location.version_guid == head_course.previous_version
|
||||
assert course.category == 'course'
|
||||
assert len(course.tabs) == 6
|
||||
assert len(course.tabs) == 5
|
||||
assert course.display_name == 'The Ancient Greek Hero'
|
||||
assert course.graceperiod == datetime.timedelta(hours=2)
|
||||
assert course.advertised_start is None
|
||||
@@ -681,7 +677,7 @@ class SplitModuleCourseTests(SplitModuleTest):
|
||||
assert course.location.course_key.course == 'GreekHero'
|
||||
assert course.location.course_key.run == 'run'
|
||||
assert course.category == 'course'
|
||||
assert len(course.tabs) == 6
|
||||
assert len(course.tabs) == 5
|
||||
assert course.display_name == 'The Ancient Greek Hero'
|
||||
assert course.advertised_start == 'Fall 2013'
|
||||
assert len(course.children) == 4
|
||||
@@ -933,7 +929,7 @@ class SplitModuleItemTests(SplitModuleTest):
|
||||
assert block.location.org == 'testx'
|
||||
assert block.location.course == 'GreekHero'
|
||||
assert block.location.run == 'run'
|
||||
assert len(block.tabs) == 6, 'wrong number of tabs'
|
||||
assert len(block.tabs) == 5, 'wrong number of tabs'
|
||||
assert block.display_name == 'The Ancient Greek Hero'
|
||||
assert block.advertised_start == 'Fall 2013'
|
||||
assert len(block.children) == 4
|
||||
|
||||
@@ -382,7 +382,6 @@ class CourseTabList(List):
|
||||
within the course.
|
||||
"""
|
||||
course_tabs = [
|
||||
CourseTab.load('course_info'),
|
||||
CourseTab.load('courseware')
|
||||
]
|
||||
|
||||
@@ -481,14 +480,18 @@ class CourseTabList(List):
|
||||
@classmethod
|
||||
def upgrade_tabs(cls, tabs):
|
||||
"""
|
||||
Reverse and Rename Courseware to Course and Course Info to Home Tabs.
|
||||
Remove course_info tab, and rename courseware tab to Course if needed.
|
||||
"""
|
||||
if tabs and len(tabs) > 1:
|
||||
# Reverse them so that course_info is first, and rename courseware to Course
|
||||
if tabs[0].get('type') == 'courseware' and tabs[1].get('type') == 'course_info':
|
||||
tabs[0], tabs[1] = tabs[1], tabs[0]
|
||||
tabs[0]['name'] = _('Home')
|
||||
tabs[1]['name'] = _('Course')
|
||||
|
||||
# NOTE: this check used for legacy courses containing the course_info tab. course_info
|
||||
# should be removed according to https://github.com/openedx/public-engineering/issues/56.
|
||||
if tabs[0].get('type') == 'course_info':
|
||||
tabs.pop(0)
|
||||
return tabs
|
||||
|
||||
@classmethod
|
||||
@@ -499,22 +502,15 @@ class CourseTabList(List):
|
||||
|
||||
Specific rules checked:
|
||||
- if no tabs specified, that's fine
|
||||
- if tabs specified, first two must have type 'courseware' and 'course_info', in that order.
|
||||
- if tabs specified, first must have type 'courseware'.
|
||||
|
||||
"""
|
||||
if tabs is None or len(tabs) == 0:
|
||||
return
|
||||
|
||||
if len(tabs) < 2:
|
||||
raise InvalidTabsException(f"Expected at least two tabs. tabs: '{tabs}'")
|
||||
|
||||
if tabs[0].get('type') != 'course_info':
|
||||
if tabs[0].get('type') != 'courseware':
|
||||
raise InvalidTabsException(
|
||||
f"Expected first tab to have type 'course_info'. tabs: '{tabs}'")
|
||||
|
||||
if tabs[1].get('type') != 'courseware':
|
||||
raise InvalidTabsException(
|
||||
f"Expected second tab to have type 'courseware'. tabs: '{tabs}'")
|
||||
f"Expected first tab to have type 'courseware'. tabs: '{tabs}'")
|
||||
|
||||
# the following tabs should appear only once
|
||||
# TODO: don't import openedx capabilities from common
|
||||
|
||||
117
common/lib/xmodule/xmodule/tests/test_tabs.py
Normal file
117
common/lib/xmodule/xmodule/tests/test_tabs.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
Tests for CourseTabsListTestCase.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
import ddt
|
||||
|
||||
from xmodule.tabs import CourseTabList, InvalidTabsException
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CourseTabsListTestCase(TestCase):
|
||||
"""
|
||||
Class containing CourseTabsListTestCase tests.
|
||||
"""
|
||||
|
||||
@ddt.data(
|
||||
[
|
||||
[],
|
||||
[]
|
||||
],
|
||||
[
|
||||
[
|
||||
{'type': 'courseware', 'course_staff_only': False, 'name': 'Courseware'},
|
||||
{'type': 'course_info', 'course_staff_only': False, 'name': 'Course Info'},
|
||||
{'type': 'discussion', 'course_staff_only': False, 'name': 'Discussion'},
|
||||
{'type': 'wiki', 'course_staff_only': False, 'name': 'Wiki'},
|
||||
{'type': 'textbooks', 'course_staff_only': False, 'name': 'Textbooks'},
|
||||
{'type': 'progress', 'course_staff_only': False, 'name': 'Progress'}
|
||||
],
|
||||
[
|
||||
{'type': 'courseware', 'course_staff_only': False, 'name': 'Course'},
|
||||
{'type': 'discussion', 'course_staff_only': False, 'name': 'Discussion'},
|
||||
{'type': 'wiki', 'course_staff_only': False, 'name': 'Wiki'},
|
||||
{'type': 'textbooks', 'course_staff_only': False, 'name': 'Textbooks'},
|
||||
{'type': 'progress', 'course_staff_only': False, 'name': 'Progress'}
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
{'type': 'course_info', 'course_staff_only': False, 'name': 'Home'},
|
||||
{'type': 'courseware', 'course_staff_only': False, 'name': 'Course'},
|
||||
{'type': 'discussion', 'course_staff_only': False, 'name': 'Discussion'},
|
||||
{'type': 'wiki', 'course_staff_only': False, 'name': 'Wiki'},
|
||||
{'type': 'textbooks', 'course_staff_only': False, 'name': 'Textbooks'},
|
||||
{'type': 'progress', 'course_staff_only': False, 'name': 'Progress'}
|
||||
],
|
||||
[
|
||||
{'type': 'courseware', 'course_staff_only': False, 'name': 'Course'},
|
||||
{'type': 'discussion', 'course_staff_only': False, 'name': 'Discussion'},
|
||||
{'type': 'wiki', 'course_staff_only': False, 'name': 'Wiki'},
|
||||
{'type': 'textbooks', 'course_staff_only': False, 'name': 'Textbooks'},
|
||||
{'type': 'progress', 'course_staff_only': False, 'name': 'Progress'}
|
||||
],
|
||||
]
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_upgrade_tabs(self, tabs, expected_result):
|
||||
CourseTabList.upgrade_tabs(tabs)
|
||||
self.assertEqual(tabs, expected_result)
|
||||
|
||||
@ddt.data(
|
||||
[
|
||||
[],
|
||||
True
|
||||
],
|
||||
[
|
||||
[
|
||||
{'type': 'courseware', 'course_staff_only': False, 'name': 'Course'},
|
||||
],
|
||||
True
|
||||
],
|
||||
[
|
||||
[
|
||||
{'type': 'course_info', 'course_staff_only': False, 'name': 'Home'},
|
||||
],
|
||||
False
|
||||
],
|
||||
[
|
||||
[
|
||||
{'type': 'course_info', 'course_staff_only': False, 'name': 'Home'},
|
||||
{'type': 'courseware', 'course_staff_only': False, 'name': 'Course'},
|
||||
{'type': 'discussion', 'course_staff_only': False, 'name': 'Discussion'},
|
||||
{'type': 'wiki', 'course_staff_only': False, 'name': 'Wiki'},
|
||||
{'type': 'textbooks', 'course_staff_only': False, 'name': 'Textbooks'},
|
||||
{'type': 'progress', 'course_staff_only': False, 'name': 'Progress'}
|
||||
],
|
||||
False
|
||||
],
|
||||
[
|
||||
[
|
||||
{'type': 'courseware', 'course_staff_only': False, 'name': 'Course'},
|
||||
{'type': 'courseware', 'course_staff_only': False, 'name': 'Course'},
|
||||
{'type': 'discussion', 'course_staff_only': False, 'name': 'Discussion'},
|
||||
{'type': 'wiki', 'course_staff_only': False, 'name': 'Wiki'},
|
||||
{'type': 'textbooks', 'course_staff_only': False, 'name': 'Textbooks'},
|
||||
{'type': 'progress', 'course_staff_only': False, 'name': 'Progress'}
|
||||
],
|
||||
False
|
||||
],
|
||||
[
|
||||
[
|
||||
{'type': 'courseware', 'course_staff_only': False, 'name': 'Course'},
|
||||
{'type': 'discussion', 'course_staff_only': False, 'name': 'Discussion'},
|
||||
{'type': 'wiki', 'course_staff_only': False, 'name': 'Wiki'},
|
||||
{'type': 'textbooks', 'course_staff_only': False, 'name': 'Textbooks'},
|
||||
{'type': 'progress', 'course_staff_only': False, 'name': 'Progress'}
|
||||
],
|
||||
True
|
||||
]
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_validate_tabs(self, tabs, expected_success):
|
||||
if not expected_success:
|
||||
with self.assertRaises(InvalidTabsException):
|
||||
CourseTabList.validate_tabs(tabs)
|
||||
else:
|
||||
CourseTabList.validate_tabs(tabs)
|
||||
@@ -27,10 +27,6 @@
|
||||
"minimum_grade_credit": 0.8,
|
||||
"start": "2030-01-01T00:00:00Z",
|
||||
"tabs": [
|
||||
{
|
||||
"name": "Home",
|
||||
"type": "course_info"
|
||||
},
|
||||
{
|
||||
"name": "Course",
|
||||
"type": "courseware"
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
# Include template which does not exist in the theme.
|
||||
<%include file="/courseware/error-message.html" />
|
||||
# Include template which is overriden in the theme.
|
||||
<%include file="/courseware/info.html" />
|
||||
<%include file="/courseware/progress.html" />
|
||||
# Include custom template which only exists in the theme.
|
||||
<%include file="/courseware/test-theme-custom.html" />
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
<%page expression_filter="h"/>
|
||||
<p>This overrides the courseware/info.html template.</p>
|
||||
@@ -0,0 +1,2 @@
|
||||
<%page expression_filter="h"/>
|
||||
<p>This overrides the courseware/progress.html template.</p>
|
||||
@@ -13,7 +13,7 @@ from lms.djangoapps.courseware.access import has_access
|
||||
from lms.djangoapps.courseware.entrance_exams import user_can_skip_entrance_exam
|
||||
from lms.djangoapps.course_home_api.toggles import course_home_mfe_progress_tab_is_active
|
||||
from openedx.core.lib.course_tabs import CourseTabPluginManager
|
||||
from openedx.features.course_experience import DISABLE_UNIFIED_COURSE_TAB_FLAG, default_course_url
|
||||
from openedx.features.course_experience import default_course_url
|
||||
from openedx.features.course_experience.url_helpers import get_learning_mfe_home_url
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
|
||||
@@ -50,28 +50,8 @@ class CoursewareTab(EnrolledTab):
|
||||
@classmethod
|
||||
def is_enabled(cls, course, user=None):
|
||||
"""
|
||||
Returns true if this tab is enabled.
|
||||
Courseware tabs are viewable to everyone, even anonymous users.
|
||||
"""
|
||||
if DISABLE_UNIFIED_COURSE_TAB_FLAG.is_enabled(course.id):
|
||||
return super().is_enabled(course, user)
|
||||
# If this is the unified course tab then it is always enabled
|
||||
return True
|
||||
|
||||
|
||||
class CourseInfoTab(CourseTab):
|
||||
"""
|
||||
The course info view.
|
||||
"""
|
||||
type = 'course_info'
|
||||
title = gettext_noop('Home')
|
||||
priority = 10
|
||||
view_name = 'info'
|
||||
tab_id = 'info'
|
||||
is_movable = False
|
||||
is_default = False
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls, course, user=None):
|
||||
return True
|
||||
|
||||
|
||||
@@ -355,9 +335,6 @@ def get_course_tab_list(user, course):
|
||||
continue
|
||||
tab.name = _("Entrance Exam")
|
||||
tab.title = _("Entrance Exam")
|
||||
# TODO: LEARNER-611 - once the course_info tab is removed, remove this code
|
||||
if not DISABLE_UNIFIED_COURSE_TAB_FLAG.is_enabled(course.id) and tab.type == 'course_info':
|
||||
continue
|
||||
if tab.type == 'static_tab' and tab.course_staff_only and \
|
||||
not bool(user and has_access(user, 'staff', course, course.id)):
|
||||
continue
|
||||
|
||||
@@ -1,417 +0,0 @@
|
||||
"""
|
||||
Test the course_info xblock
|
||||
"""
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from unittest import mock
|
||||
import ddt
|
||||
from ccx_keys.locator import CCXLocator
|
||||
from django.conf import settings
|
||||
from django.http import QueryDict
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
from pyquery import PyQuery as pq
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
|
||||
from xmodule.modulestore.tests.utils import TEST_DATA_DIR
|
||||
from xmodule.modulestore.xml_importer import import_course_from_xml
|
||||
|
||||
from lms.djangoapps.ccx.tests.factories import CcxFactory
|
||||
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration_context
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
|
||||
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
|
||||
from openedx.features.course_experience import DISABLE_UNIFIED_COURSE_TAB_FLAG
|
||||
from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.tests.factories import AdminFactory
|
||||
from common.djangoapps.util.date_utils import strftime_localized
|
||||
|
||||
from .helpers import LoginEnrollmentTestCase
|
||||
|
||||
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES
|
||||
|
||||
|
||||
@override_waffle_flag(DISABLE_UNIFIED_COURSE_TAB_FLAG, active=True)
|
||||
class CourseInfoTestCase(EnterpriseTestConsentRequired, LoginEnrollmentTestCase, SharedModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the Course Info page
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.course = CourseFactory.create()
|
||||
cls.page = ItemFactory.create(
|
||||
category="course_info", parent_location=cls.course.location,
|
||||
data="OOGIE BLOOGIE", display_name="updates"
|
||||
)
|
||||
|
||||
def test_logged_in_unenrolled(self):
|
||||
self.setup_user()
|
||||
url = reverse('info', args=[str(self.course.id)])
|
||||
resp = self.client.get(url)
|
||||
self.assertContains(resp, "OOGIE BLOOGIE")
|
||||
self.assertContains(resp, "You are not currently enrolled in this course")
|
||||
|
||||
def test_logged_in_enrolled(self):
|
||||
self.enroll(self.course)
|
||||
url = reverse('info', args=[str(self.course.id)])
|
||||
resp = self.client.get(url)
|
||||
assert b'You are not currently enrolled in this course' not in resp.content
|
||||
|
||||
# TODO: LEARNER-611: If this is only tested under Course Info, does this need to move?
|
||||
@mock.patch('openedx.features.enterprise_support.api.enterprise_customer_for_request')
|
||||
def test_redirection_missing_enterprise_consent(self, mock_enterprise_customer_for_request):
|
||||
"""
|
||||
Verify that users viewing the course info who are enrolled, but have not provided
|
||||
data sharing consent, are first redirected to a consent page, and then, once they've
|
||||
provided consent, are able to view the course info.
|
||||
"""
|
||||
# ENT-924: Temporary solution to replace sensitive SSO usernames.
|
||||
mock_enterprise_customer_for_request.return_value = None
|
||||
|
||||
self.setup_user()
|
||||
self.enroll(self.course)
|
||||
|
||||
url = reverse('info', args=[str(self.course.id)])
|
||||
|
||||
self.verify_consent_required(self.client, url) # lint-amnesty, pylint: disable=no-value-for-parameter
|
||||
|
||||
def test_anonymous_user(self):
|
||||
url = reverse('info', args=[str(self.course.id)])
|
||||
resp = self.client.get(url)
|
||||
assert resp.status_code == 200
|
||||
assert b'OOGIE BLOOGIE' not in resp.content
|
||||
|
||||
def test_logged_in_not_enrolled(self):
|
||||
self.setup_user()
|
||||
url = reverse('info', args=[str(self.course.id)])
|
||||
self.client.get(url)
|
||||
|
||||
# Check whether the user has been enrolled in the course.
|
||||
# There was a bug in which users would be automatically enrolled
|
||||
# with is_active=False (same as if they enrolled and immediately unenrolled).
|
||||
# This verifies that the user doesn't have *any* enrollment record.
|
||||
enrollment_exists = CourseEnrollment.objects.filter(
|
||||
user=self.user, course_id=self.course.id
|
||||
).exists()
|
||||
assert not enrollment_exists
|
||||
|
||||
@mock.patch.dict(settings.FEATURES, {'DISABLE_START_DATES': False})
|
||||
def test_non_live_course(self):
|
||||
"""Ensure that a user accessing a non-live course sees a redirect to
|
||||
the student dashboard, not a 404.
|
||||
"""
|
||||
self.setup_user()
|
||||
self.enroll(self.course)
|
||||
url = reverse('info', args=[str(self.course.id)])
|
||||
response = self.client.get(url)
|
||||
start_date = strftime_localized(self.course.start, 'SHORT_DATE')
|
||||
expected_params = QueryDict(mutable=True)
|
||||
expected_params['notlive'] = start_date
|
||||
expected_url = '{url}?{params}'.format(
|
||||
url=reverse('dashboard'),
|
||||
params=expected_params.urlencode()
|
||||
)
|
||||
self.assertRedirects(response, expected_url)
|
||||
|
||||
@mock.patch.dict(settings.FEATURES, {'DISABLE_START_DATES': False})
|
||||
@mock.patch("common.djangoapps.util.date_utils.strftime_localized")
|
||||
def test_non_live_course_other_language(self, mock_strftime_localized):
|
||||
"""Ensure that a user accessing a non-live course sees a redirect to
|
||||
the student dashboard, not a 404, even if the localized date is unicode
|
||||
"""
|
||||
self.setup_user()
|
||||
self.enroll(self.course)
|
||||
fake_unicode_start_time = "üñîçø∂é_ßtå®t_tîµé"
|
||||
mock_strftime_localized.return_value = fake_unicode_start_time
|
||||
|
||||
url = reverse('info', args=[str(self.course.id)])
|
||||
response = self.client.get(url)
|
||||
expected_params = QueryDict(mutable=True)
|
||||
expected_params['notlive'] = fake_unicode_start_time
|
||||
expected_url = '{url}?{params}'.format(
|
||||
url=reverse('dashboard'),
|
||||
params=expected_params.urlencode()
|
||||
)
|
||||
self.assertRedirects(response, expected_url)
|
||||
|
||||
def test_nonexistent_course(self):
|
||||
self.setup_user()
|
||||
url = reverse('info', args=['not/a/course'])
|
||||
response = self.client.get(url)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
@override_waffle_flag(DISABLE_UNIFIED_COURSE_TAB_FLAG, active=True)
|
||||
class CourseInfoLastAccessedTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
"""
|
||||
Tests of the CourseInfo last accessed link.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.course = CourseFactory.create()
|
||||
self.page = ItemFactory.create(
|
||||
category="course_info", parent_location=self.course.location,
|
||||
data="OOGIE BLOOGIE", display_name="updates"
|
||||
)
|
||||
|
||||
def test_last_accessed_courseware_not_shown(self):
|
||||
"""
|
||||
Test that the last accessed courseware link is not shown if there
|
||||
is no course content.
|
||||
"""
|
||||
SelfPacedConfiguration(enable_course_home_improvements=True).save()
|
||||
url = reverse('info', args=(str(self.course.id),))
|
||||
response = self.client.get(url)
|
||||
content = pq(response.content)
|
||||
assert content('.page-header-secondary a').length == 0
|
||||
|
||||
def get_resume_course_url(self, course_info_url):
|
||||
"""
|
||||
Retrieves course info page and returns the resume course url
|
||||
or None if the button doesn't exist.
|
||||
"""
|
||||
info_page_response = self.client.get(course_info_url)
|
||||
content = pq(info_page_response.content)
|
||||
return content('.page-header-secondary .last-accessed-link').attr('href')
|
||||
|
||||
def test_resume_course_visibility(self):
|
||||
SelfPacedConfiguration(enable_course_home_improvements=True).save()
|
||||
chapter = ItemFactory.create(
|
||||
category="chapter", parent_location=self.course.location
|
||||
)
|
||||
section = ItemFactory.create(
|
||||
category='sequential', parent_location=chapter.location
|
||||
)
|
||||
section_url = reverse(
|
||||
'courseware_section',
|
||||
kwargs={
|
||||
'section': section.url_name,
|
||||
'chapter': chapter.url_name,
|
||||
'course_id': self.course.id
|
||||
}
|
||||
)
|
||||
self.client.get(section_url)
|
||||
info_url = reverse('info', args=(str(self.course.id),))
|
||||
|
||||
# Assuring a non-authenticated user cannot see the resume course button.
|
||||
resume_course_url = self.get_resume_course_url(info_url)
|
||||
assert resume_course_url is None
|
||||
|
||||
# Assuring an unenrolled user cannot see the resume course button.
|
||||
self.setup_user()
|
||||
resume_course_url = self.get_resume_course_url(info_url)
|
||||
assert resume_course_url is None
|
||||
|
||||
# Assuring an enrolled user can see the resume course button.
|
||||
self.enroll(self.course)
|
||||
resume_course_url = self.get_resume_course_url(info_url)
|
||||
assert resume_course_url == section_url
|
||||
|
||||
|
||||
@override_waffle_flag(DISABLE_UNIFIED_COURSE_TAB_FLAG, active=True)
|
||||
@ddt.ddt
|
||||
class CourseInfoTitleTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
"""
|
||||
Tests of the CourseInfo page title site configuration options.
|
||||
"""
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.course = CourseFactory.create(
|
||||
org="HogwartZ",
|
||||
number="Potions_3",
|
||||
display_organization="HogwartsX",
|
||||
display_coursenumber="Potions101",
|
||||
display_name="Introduction to Potions"
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
# Default site configuration shows course number, org, and display name as subtitle.
|
||||
({},
|
||||
"Welcome to HogwartsX's Potions101!", "Introduction to Potions"),
|
||||
|
||||
# Show org in title
|
||||
(dict(COURSE_HOMEPAGE_INVERT_TITLE=False,
|
||||
COURSE_HOMEPAGE_SHOW_SUBTITLE=True,
|
||||
COURSE_HOMEPAGE_SHOW_ORG=True),
|
||||
"Welcome to HogwartsX's Potions101!", "Introduction to Potions"),
|
||||
|
||||
# Don't show org in title
|
||||
(dict(COURSE_HOMEPAGE_INVERT_TITLE=False,
|
||||
COURSE_HOMEPAGE_SHOW_SUBTITLE=True,
|
||||
COURSE_HOMEPAGE_SHOW_ORG=False),
|
||||
"Welcome to Potions101!", "Introduction to Potions"),
|
||||
|
||||
# Hide subtitle and org
|
||||
(dict(COURSE_HOMEPAGE_INVERT_TITLE=False,
|
||||
COURSE_HOMEPAGE_SHOW_SUBTITLE=False,
|
||||
COURSE_HOMEPAGE_SHOW_ORG=False),
|
||||
"Welcome to Potions101!", None),
|
||||
|
||||
# Show display name as title, hide subtitle and org.
|
||||
(dict(COURSE_HOMEPAGE_INVERT_TITLE=True,
|
||||
COURSE_HOMEPAGE_SHOW_SUBTITLE=False,
|
||||
COURSE_HOMEPAGE_SHOW_ORG=False),
|
||||
"Welcome to Introduction to Potions!", None),
|
||||
|
||||
# Show display name as title with org, hide subtitle.
|
||||
(dict(COURSE_HOMEPAGE_INVERT_TITLE=True,
|
||||
COURSE_HOMEPAGE_SHOW_SUBTITLE=False,
|
||||
COURSE_HOMEPAGE_SHOW_ORG=True),
|
||||
"Welcome to HogwartsX's Introduction to Potions!", None),
|
||||
|
||||
# Show display name as title, hide org, and show course number as subtitle.
|
||||
(dict(COURSE_HOMEPAGE_INVERT_TITLE=True,
|
||||
COURSE_HOMEPAGE_SHOW_SUBTITLE=True,
|
||||
COURSE_HOMEPAGE_SHOW_ORG=False),
|
||||
"Welcome to Introduction to Potions!", 'Potions101'),
|
||||
|
||||
# Show display name as title with org, and show course number as subtitle.
|
||||
(dict(COURSE_HOMEPAGE_INVERT_TITLE=True,
|
||||
COURSE_HOMEPAGE_SHOW_SUBTITLE=True,
|
||||
COURSE_HOMEPAGE_SHOW_ORG=True),
|
||||
"Welcome to HogwartsX's Introduction to Potions!", 'Potions101'),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_info_title(self, site_config, expected_title, expected_subtitle):
|
||||
"""
|
||||
Test the info page on a course with all the multiple display options
|
||||
depeding on the current site configuration
|
||||
"""
|
||||
url = reverse('info', args=(str(self.course.id),))
|
||||
with with_site_configuration_context(configuration=site_config):
|
||||
response = self.client.get(url)
|
||||
|
||||
content = pq(response.content)
|
||||
|
||||
assert expected_title == content('.page-title').contents()[0].strip()
|
||||
|
||||
if expected_subtitle is None:
|
||||
assert not content('.page-subtitle')
|
||||
else:
|
||||
assert expected_subtitle == content('.page-subtitle').contents()[0].strip()
|
||||
|
||||
|
||||
@override_waffle_flag(DISABLE_UNIFIED_COURSE_TAB_FLAG, active=True)
|
||||
class CourseInfoTestCaseCCX(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"""
|
||||
Test for unenrolled student tries to access ccx.
|
||||
Note: Only CCX coach can enroll a student in CCX. In sum self-registration not allowed.
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.course = CourseFactory.create()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# Create ccx coach account
|
||||
self.coach = coach = AdminFactory.create(password="test")
|
||||
self.client.login(username=coach.username, password="test")
|
||||
|
||||
def test_redirect_to_dashboard_unenrolled_ccx(self):
|
||||
"""
|
||||
Assert that when unenroll student tries to access ccx do not allow them self-register.
|
||||
Redirect them to their student dashboard
|
||||
"""
|
||||
# create ccx
|
||||
ccx = CcxFactory(course_id=self.course.id, coach=self.coach)
|
||||
ccx_locator = CCXLocator.from_course_locator(self.course.id, str(ccx.id))
|
||||
|
||||
self.setup_user()
|
||||
url = reverse('info', args=[ccx_locator])
|
||||
response = self.client.get(url)
|
||||
expected = reverse('dashboard')
|
||||
self.assertRedirects(response, expected, status_code=302, target_status_code=200)
|
||||
|
||||
|
||||
@override_waffle_flag(DISABLE_UNIFIED_COURSE_TAB_FLAG, active=True)
|
||||
class CourseInfoTestCaseXML(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the Course Info page for an XML course
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up the tests
|
||||
"""
|
||||
super().setUp()
|
||||
|
||||
# The following test course (which lives at common/test/data/2014)
|
||||
# is closed; we're testing that a course info page still appears when
|
||||
# the course is already closed
|
||||
self.xml_course_key = self.store.make_course_key('edX', 'detached_pages', '2014')
|
||||
import_course_from_xml(
|
||||
self.store,
|
||||
self.user.id,
|
||||
TEST_DATA_DIR,
|
||||
source_dirs=['2014'],
|
||||
static_content_store=None,
|
||||
target_id=self.xml_course_key,
|
||||
raise_on_failure=True,
|
||||
create_if_not_present=True,
|
||||
)
|
||||
|
||||
# this text appears in that course's course info page
|
||||
# common/test/data/2014/info/updates.html
|
||||
self.xml_data = "course info 463139"
|
||||
|
||||
@mock.patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
|
||||
def test_logged_in_xml(self):
|
||||
self.setup_user()
|
||||
url = reverse('info', args=[str(self.xml_course_key)])
|
||||
resp = self.client.get(url)
|
||||
self.assertContains(resp, self.xml_data)
|
||||
|
||||
@mock.patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
|
||||
def test_anonymous_user_xml(self):
|
||||
url = reverse('info', args=[str(self.xml_course_key)])
|
||||
resp = self.client.get(url)
|
||||
self.assertNotContains(resp, self.xml_data)
|
||||
|
||||
|
||||
@override_settings(FEATURES=dict(settings.FEATURES, EMBARGO=False))
|
||||
@override_waffle_flag(DISABLE_UNIFIED_COURSE_TAB_FLAG, active=True)
|
||||
class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the info page of self-paced courses.
|
||||
"""
|
||||
ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache']
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.instructor_paced_course = CourseFactory.create(self_paced=False)
|
||||
cls.self_paced_course = CourseFactory.create(self_paced=True)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
|
||||
|
||||
self.setup_user()
|
||||
|
||||
def fetch_course_info_with_queries(self, course, sql_queries, mongo_queries):
|
||||
"""
|
||||
Fetch the given course's info page, asserting the number of SQL
|
||||
and Mongo queries.
|
||||
"""
|
||||
url = reverse('info', args=[str(course.id)])
|
||||
with self.assertNumQueries(sql_queries, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
|
||||
with check_mongo_calls(mongo_queries):
|
||||
with mock.patch("openedx.core.djangoapps.theming.helpers.get_current_site", return_value=None):
|
||||
resp = self.client.get(url)
|
||||
assert resp.status_code == 200
|
||||
|
||||
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, 41, 2)
|
||||
|
||||
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, 41, 2)
|
||||
@@ -9,7 +9,6 @@ import crum
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from django.test import RequestFactory
|
||||
from django.urls import reverse
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag, override_waffle_switch
|
||||
from freezegun import freeze_time
|
||||
from pytz import utc
|
||||
@@ -19,7 +18,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
|
||||
from common.djangoapps.student.tests.factories import TEST_PASSWORD, CourseEnrollmentFactory, UserFactory
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from lms.djangoapps.certificates.config import AUTO_CERTIFICATE_GENERATION
|
||||
from lms.djangoapps.commerce.models import CommerceConfiguration
|
||||
from lms.djangoapps.courseware.courses import get_course_date_blocks
|
||||
@@ -43,7 +42,6 @@ from lms.djangoapps.verify_student.services import IDVerificationService
|
||||
from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
|
||||
from openedx.features.course_experience import RELATIVE_DATES_FLAG
|
||||
|
||||
@@ -51,9 +49,6 @@ from openedx.features.course_experience import RELATIVE_DATES_FLAG
|
||||
@ddt.ddt
|
||||
class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
"""Tests for course date summary blocks."""
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
SelfPacedConfiguration.objects.create(enable_course_home_improvements=True)
|
||||
|
||||
def make_request(self, user):
|
||||
""" Creates a request """
|
||||
@@ -63,17 +58,6 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
crum.set_current_request(request)
|
||||
return request
|
||||
|
||||
def test_course_info_feature_flag(self):
|
||||
SelfPacedConfiguration(enable_course_home_improvements=False).save()
|
||||
course = create_course_run()
|
||||
user = create_user()
|
||||
CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED)
|
||||
|
||||
self.client.login(username=user.username, password=TEST_PASSWORD)
|
||||
url = reverse('info', args=(course.id,))
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, 'date-summary', status_code=302)
|
||||
|
||||
# Tests for which blocks are enabled
|
||||
def assert_block_types(self, course, user, expected_blocks):
|
||||
"""Assert that the enabled block types for this course are as expected."""
|
||||
|
||||
@@ -13,7 +13,6 @@ from operator import itemgetter # lint-amnesty, pylint: disable=wrong-import-or
|
||||
from django.conf import settings
|
||||
from django.test import TestCase, RequestFactory
|
||||
from django.urls import reverse
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
from pytz import UTC
|
||||
from xblock.runtime import DictKeyValueStore
|
||||
|
||||
@@ -31,9 +30,7 @@ from lms.djangoapps.courseware.tests.helpers import (
|
||||
)
|
||||
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
|
||||
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference, set_user_preference
|
||||
from openedx.features.course_experience import DISABLE_UNIFIED_COURSE_TAB_FLAG
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.tests.factories import StaffFactory
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
@@ -108,18 +105,6 @@ class MasqueradeTestCase(SharedModuleStoreTestCase, LoginEnrollmentTestCase, Mas
|
||||
)
|
||||
return self.client.get(url)
|
||||
|
||||
def get_course_info_page(self):
|
||||
"""
|
||||
Returns the server response for course info page.
|
||||
"""
|
||||
url = reverse(
|
||||
'info',
|
||||
kwargs={
|
||||
'course_id': str(self.course.id),
|
||||
}
|
||||
)
|
||||
return self.client.get(url)
|
||||
|
||||
def get_progress_page(self):
|
||||
"""
|
||||
Returns the server response for progress page.
|
||||
@@ -351,27 +336,6 @@ class TestStaffMasqueradeAsSpecificStudent(StaffMasqueradeTestCase, ProblemSubmi
|
||||
assert get_user_preference(user, LANGUAGE_KEY) == expected_language_code
|
||||
assert self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value == expected_language_code
|
||||
|
||||
@override_waffle_flag(DISABLE_UNIFIED_COURSE_TAB_FLAG, active=True)
|
||||
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
|
||||
def test_masquerade_as_specific_user_on_self_paced(self):
|
||||
"""
|
||||
Test masquerading as a specific user for course info page when self paced configuration
|
||||
"enable_course_home_improvements" flag is set
|
||||
|
||||
Login as a staff user and visit course info page.
|
||||
set masquerade to view same page as a specific student and revisit the course info page.
|
||||
"""
|
||||
# Log in as staff, and check we can see the info page.
|
||||
self.login_staff()
|
||||
response = self.get_course_info_page()
|
||||
self.assertContains(response, "OOGIE BLOOGIE")
|
||||
|
||||
# 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', username=self.student_user.username)
|
||||
response = self.get_course_info_page()
|
||||
self.assertContains(response, "OOGIE BLOOGIE")
|
||||
|
||||
@ddt.data(
|
||||
'john', # Non-unicode username
|
||||
'fôô@bar', # Unicode username with @, which is what the ENABLE_UNICODE_USERNAME feature allows
|
||||
@@ -442,25 +406,6 @@ class TestStaffMasqueradeAsSpecificStudent(StaffMasqueradeTestCase, ProblemSubmi
|
||||
self.get_courseware_page()
|
||||
self.assertExpectedLanguageInPreference(self.test_user, english_language_code)
|
||||
|
||||
@override_waffle_flag(DISABLE_UNIFIED_COURSE_TAB_FLAG, active=True)
|
||||
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
|
||||
def test_masquerade_as_specific_student_course_info(self):
|
||||
"""
|
||||
Test masquerading as a specific user for course info page.
|
||||
|
||||
We login with login_staff and check course info page content if it's working and then we
|
||||
set masquerade to view same page as a specific student and test if it's working or not.
|
||||
"""
|
||||
# Log in as staff, and check we can see the info page.
|
||||
self.login_staff()
|
||||
content = self.get_course_info_page().content.decode('utf-8')
|
||||
assert 'OOGIE BLOOGIE' in content
|
||||
|
||||
# Masquerade as the student, and check we can see the info page.
|
||||
self.update_masquerade(role='student', username=self.student_user.username)
|
||||
content = self.get_course_info_page().content.decode('utf-8')
|
||||
assert 'OOGIE BLOOGIE' in content
|
||||
|
||||
def test_masquerade_as_specific_student_progress(self):
|
||||
"""
|
||||
Test masquerading as a specific user for progress page.
|
||||
|
||||
@@ -10,9 +10,7 @@ from django.http import Http404
|
||||
from django.urls import reverse
|
||||
from milestones.tests.utils import MilestonesTestCaseMixin
|
||||
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
from lms.djangoapps.courseware.tabs import (
|
||||
CourseInfoTab,
|
||||
CoursewareTab,
|
||||
DatesTab,
|
||||
ExternalDiscussionCourseTab,
|
||||
@@ -24,7 +22,6 @@ from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from lms.djangoapps.courseware.views.views import StaticCourseTabView, get_static_tab_fragment
|
||||
from openedx.core.djangolib.testing.utils import get_mock_request
|
||||
from openedx.core.lib.courses import get_course_by_id
|
||||
from openedx.features.course_experience import DISABLE_UNIFIED_COURSE_TAB_FLAG
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.tests.factories import InstructorFactory
|
||||
from common.djangoapps.student.tests.factories import StaffFactory
|
||||
@@ -496,20 +493,16 @@ class TabListTestCase(TabTestCase):
|
||||
|
||||
# invalid tabs
|
||||
self.invalid_tabs = [
|
||||
# less than 2 tabs
|
||||
[{'type': CoursewareTab.type}],
|
||||
# missing course_info
|
||||
[{'type': CoursewareTab.type}, {'type': 'discussion', 'name': 'fake_name'}],
|
||||
# missing courseware
|
||||
[{'type': 'unknown_type'}],
|
||||
# incorrect order
|
||||
[{'type': 'discussion', 'name': 'fake_name'},
|
||||
{'type': CourseInfoTab.type, 'name': 'fake_name'}, {'type': CoursewareTab.type}],
|
||||
{'type': CoursewareTab.type}],
|
||||
]
|
||||
|
||||
# tab types that should appear only once
|
||||
unique_tab_types = [
|
||||
CoursewareTab.type,
|
||||
CourseInfoTab.type,
|
||||
'textbooks',
|
||||
'pdf_textbooks',
|
||||
'html_textbooks',
|
||||
@@ -518,7 +511,6 @@ class TabListTestCase(TabTestCase):
|
||||
for unique_tab_type in unique_tab_types:
|
||||
self.invalid_tabs.append([
|
||||
{'type': CoursewareTab.type},
|
||||
{'type': CourseInfoTab.type, 'name': 'fake_name'},
|
||||
# add the unique tab multiple times
|
||||
{'type': unique_tab_type},
|
||||
{'type': unique_tab_type},
|
||||
@@ -532,7 +524,6 @@ class TabListTestCase(TabTestCase):
|
||||
# all valid tabs
|
||||
[
|
||||
{'type': CoursewareTab.type},
|
||||
{'type': CourseInfoTab.type, 'name': 'fake_name'},
|
||||
{'type': DatesTab.type}, # Add this even though we filter it out, for testing purposes
|
||||
{'type': 'discussion', 'name': 'fake_name'},
|
||||
{'type': ExternalLinkCourseTab.type, 'name': 'fake_name', 'link': 'fake_link'},
|
||||
@@ -547,7 +538,6 @@ class TabListTestCase(TabTestCase):
|
||||
# with external discussion
|
||||
[
|
||||
{'type': CoursewareTab.type},
|
||||
{'type': CourseInfoTab.type, 'name': 'fake_name'},
|
||||
{'type': ExternalDiscussionCourseTab.type, 'name': 'fake_name', 'link': 'fake_link'}
|
||||
],
|
||||
]
|
||||
@@ -575,8 +565,7 @@ class ValidateTabsTestCase(TabListTestCase):
|
||||
"""
|
||||
tab_list = xmodule_tabs.CourseTabList()
|
||||
assert len(tab_list.from_json([{'type': CoursewareTab.type},
|
||||
{'type': CourseInfoTab.type, 'name': 'fake_name'},
|
||||
{'type': 'no_such_type'}])) == 2
|
||||
{'type': 'no_such_type'}])) == 1
|
||||
|
||||
|
||||
class CourseTabListTestCase(TabListTestCase):
|
||||
@@ -746,33 +735,6 @@ class StaticTabTestCase(TabTestCase):
|
||||
self.check_get_and_set_method_for_key(tab, 'url_slug')
|
||||
|
||||
|
||||
class CourseInfoTabTestCase(TabTestCase):
|
||||
"""Test cases for the course info tab."""
|
||||
def setUp(self): # lint-amnesty, pylint: disable=super-method-not-called
|
||||
self.user = self.create_mock_user()
|
||||
self.addCleanup(set_current_request, None)
|
||||
|
||||
@override_waffle_flag(DISABLE_UNIFIED_COURSE_TAB_FLAG, active=True)
|
||||
def test_default_tab(self):
|
||||
# Verify that the course info tab is the first tab
|
||||
tabs = get_course_tab_list(self.user, self.course)
|
||||
assert tabs[0].type == 'course_info'
|
||||
|
||||
@override_waffle_flag(DISABLE_UNIFIED_COURSE_TAB_FLAG, active=False)
|
||||
def test_default_tab_for_new_course_experience(self):
|
||||
# Verify that the unified course experience hides the course info tab
|
||||
tabs = get_course_tab_list(self.user, self.course)
|
||||
assert tabs[0].type == 'courseware'
|
||||
|
||||
# TODO: LEARNER-611 - remove once course_info is removed.
|
||||
@override_waffle_flag(DISABLE_UNIFIED_COURSE_TAB_FLAG, active=False)
|
||||
def test_default_tab_for_displayable(self):
|
||||
tabs = xmodule_tabs.CourseTabList.iterate_displayable(self.course, self.user)
|
||||
for i, tab in enumerate(tabs):
|
||||
if i == 0:
|
||||
assert tab.type == 'course_info'
|
||||
|
||||
|
||||
class DiscussionLinkTestCase(TabTestCase):
|
||||
"""Test cases for discussion link tab."""
|
||||
|
||||
|
||||
@@ -91,7 +91,6 @@ from openedx.features.content_type_gating.models import ContentTypeGatingConfig
|
||||
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
|
||||
from openedx.features.course_experience import (
|
||||
DISABLE_COURSE_OUTLINE_PAGE_FLAG,
|
||||
DISABLE_UNIFIED_COURSE_TAB_FLAG,
|
||||
)
|
||||
from openedx.features.course_experience.tests.views.helpers import add_course_mode
|
||||
from openedx.features.course_experience.url_helpers import (
|
||||
@@ -1022,19 +1021,6 @@ class ViewsTestCase(BaseViewsTestCase):
|
||||
response = self.client.get(url)
|
||||
self.assertRedirects(response, reverse('signin_user') + '?next=' + url)
|
||||
|
||||
@override_waffle_flag(DISABLE_UNIFIED_COURSE_TAB_FLAG, active=True)
|
||||
def test_bypass_course_info(self):
|
||||
course_id = str(self.course_key)
|
||||
|
||||
response = self.client.get(reverse('info', args=[course_id]))
|
||||
assert response.status_code == 200
|
||||
|
||||
response = self.client.get(reverse('info', args=[course_id]), HTTP_REFERER=reverse('dashboard'))
|
||||
assert response.status_code == 200
|
||||
|
||||
response = self.client.get(reverse('info', args=[course_id]), HTTP_REFERER='foo')
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
# Patching 'lms.djangoapps.courseware.views.views.get_programs' would be ideal,
|
||||
# but for some unknown reason that patch doesn't seem to be applied.
|
||||
|
||||
@@ -67,7 +67,7 @@ from lms.djangoapps.commerce.utils import EcommerceService
|
||||
from lms.djangoapps.course_goals.models import UserActivity
|
||||
from lms.djangoapps.course_home_api.toggles import course_home_mfe_progress_tab_is_active
|
||||
from lms.djangoapps.courseware.access import has_access, has_ccx_coach_role
|
||||
from lms.djangoapps.courseware.access_utils import check_course_open_for_learner, check_public_access
|
||||
from lms.djangoapps.courseware.access_utils import check_public_access
|
||||
from lms.djangoapps.courseware.courses import (
|
||||
can_self_enroll_in_course,
|
||||
course_open_for_self_enrollment,
|
||||
@@ -75,7 +75,6 @@ from lms.djangoapps.courseware.courses import (
|
||||
get_course_overview_with_access,
|
||||
get_course_with_access,
|
||||
get_courses,
|
||||
get_current_child,
|
||||
get_permission_for_course_about,
|
||||
get_studio_url,
|
||||
sort_by_announcement,
|
||||
@@ -113,7 +112,6 @@ from openedx.core.djangoapps.enrollments.permissions import ENROLL_IN_COURSE
|
||||
from openedx.core.djangoapps.models.course_details import CourseDetails
|
||||
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
|
||||
from openedx.core.djangoapps.programs.utils import ProgramMarketingDataExtender
|
||||
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.util.user_messages import PageLevelMessages
|
||||
from openedx.core.djangoapps.zendesk_proxy.utils import create_zendesk_ticket
|
||||
@@ -121,19 +119,16 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
from openedx.core.lib.courses import get_course_by_id
|
||||
from openedx.core.lib.mobile_utils import is_request_from_mobile_app
|
||||
from openedx.features.course_duration_limits.access import generate_course_expired_fragment
|
||||
from openedx.features.course_experience import DISABLE_UNIFIED_COURSE_TAB_FLAG, course_home_url
|
||||
from openedx.features.course_experience.course_tools import CourseToolsPluginManager
|
||||
from openedx.features.course_experience import course_home_url
|
||||
from openedx.features.course_experience.url_helpers import (
|
||||
get_courseware_url,
|
||||
get_learning_mfe_home_url,
|
||||
is_request_from_learning_mfe
|
||||
)
|
||||
from openedx.features.course_experience.utils import dates_banner_should_display
|
||||
from openedx.features.course_experience.views.course_dates import CourseDatesFragmentView
|
||||
from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML
|
||||
from openedx.features.enterprise_support.api import data_sharing_consent_required
|
||||
|
||||
from ..entrance_exams import user_can_skip_entrance_exam
|
||||
from ..module_render import get_module, get_module_by_usage_id, get_module_for_descriptor
|
||||
from ..tabs import _get_dynamic_tabs
|
||||
from ..toggles import COURSEWARE_OPTIMIZED_RENDER_XBLOCK
|
||||
@@ -436,149 +431,6 @@ def jump_to(request, course_id, location):
|
||||
return redirect(redirect_url)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@ensure_valid_course_key
|
||||
@data_sharing_consent_required
|
||||
def course_info(request, course_id):
|
||||
"""
|
||||
Display the course's info.html, or 404 if there is no such course.
|
||||
Assumes the course_id is in a valid format.
|
||||
"""
|
||||
# TODO: LEARNER-611: This can be deleted with Course Info removal. The new
|
||||
# Course Home is using its own processing of last accessed.
|
||||
def get_last_accessed_courseware(course, request, user):
|
||||
"""
|
||||
Returns the courseware module URL that the user last accessed, or None if it cannot be found.
|
||||
"""
|
||||
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
|
||||
course.id, request.user, course, depth=2
|
||||
)
|
||||
course_module = get_module_for_descriptor(
|
||||
user,
|
||||
request,
|
||||
course,
|
||||
field_data_cache,
|
||||
course.id,
|
||||
course=course,
|
||||
will_recheck_access=True,
|
||||
)
|
||||
chapter_module = get_current_child(course_module)
|
||||
if chapter_module is not None:
|
||||
section_module = get_current_child(chapter_module)
|
||||
if section_module is not None:
|
||||
url = reverse('courseware_section', kwargs={
|
||||
'course_id': str(course.id),
|
||||
'chapter': chapter_module.url_name,
|
||||
'section': section_module.url_name
|
||||
})
|
||||
return url
|
||||
return None
|
||||
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
|
||||
# If the unified course experience is enabled, redirect to the "Course" tab
|
||||
if not DISABLE_UNIFIED_COURSE_TAB_FLAG.is_enabled(course_key):
|
||||
return redirect(course_home_url(course_key))
|
||||
|
||||
with modulestore().bulk_operations(course_key):
|
||||
course = get_course_with_access(request.user, 'load', course_key)
|
||||
|
||||
can_masquerade = request.user.has_perm(MASQUERADE_AS_STUDENT, course)
|
||||
masquerade, user = setup_masquerade(request, course_key, can_masquerade, reset_masquerade_data=True)
|
||||
|
||||
# LEARNER-612: CCX redirect handled by new Course Home (DONE)
|
||||
# LEARNER-1697: Transition banner messages to new Course Home (DONE)
|
||||
# if user is not enrolled in a course then app will show enroll/get register link inside course info page.
|
||||
user_is_enrolled = CourseEnrollment.is_enrolled(user, course.id)
|
||||
show_enroll_banner = request.user.is_authenticated and not user_is_enrolled
|
||||
|
||||
# If the user is not enrolled but this is a course that does not support
|
||||
# direct enrollment then redirect them to the dashboard.
|
||||
if not user_is_enrolled and not can_self_enroll_in_course(course_key):
|
||||
return redirect(reverse('dashboard'))
|
||||
|
||||
# LEARNER-170: Entrance exam is handled by new Course Outline. (DONE)
|
||||
# If the user needs to take an entrance exam to access this course, then we'll need
|
||||
# to send them to that specific course module before allowing them into other areas
|
||||
if not user_can_skip_entrance_exam(user, course):
|
||||
return redirect(reverse('courseware', args=[str(course.id)]))
|
||||
|
||||
# Construct the dates fragment
|
||||
dates_fragment = None
|
||||
|
||||
if request.user.is_authenticated:
|
||||
# TODO: LEARNER-611: Remove enable_course_home_improvements
|
||||
if SelfPacedConfiguration.current().enable_course_home_improvements:
|
||||
# Shared code with the new Course Home (DONE)
|
||||
dates_fragment = CourseDatesFragmentView().render_to_fragment(request, course_id=course_id)
|
||||
|
||||
# Shared code with the new Course Home (DONE)
|
||||
# Get the course tools enabled for this user and course
|
||||
course_tools = CourseToolsPluginManager.get_enabled_course_tools(request, course_key)
|
||||
|
||||
course_homepage_invert_title =\
|
||||
configuration_helpers.get_value(
|
||||
'COURSE_HOMEPAGE_INVERT_TITLE',
|
||||
False
|
||||
)
|
||||
|
||||
course_homepage_show_subtitle =\
|
||||
configuration_helpers.get_value(
|
||||
'COURSE_HOMEPAGE_SHOW_SUBTITLE',
|
||||
True
|
||||
)
|
||||
|
||||
course_homepage_show_org =\
|
||||
configuration_helpers.get_value('COURSE_HOMEPAGE_SHOW_ORG', True)
|
||||
|
||||
course_title = course.display_number_with_default
|
||||
course_subtitle = course.display_name_with_default
|
||||
if course_homepage_invert_title:
|
||||
course_title = course.display_name_with_default
|
||||
course_subtitle = course.display_number_with_default
|
||||
|
||||
context = {
|
||||
'request': request,
|
||||
'masquerade_user': user,
|
||||
'course_id': str(course_key),
|
||||
'url_to_enroll': CourseTabView.url_to_enroll(course_key),
|
||||
'cache': None,
|
||||
'course': course,
|
||||
'course_title': course_title,
|
||||
'course_subtitle': course_subtitle,
|
||||
'show_subtitle': course_homepage_show_subtitle,
|
||||
'show_org': course_homepage_show_org,
|
||||
'can_masquerade': can_masquerade,
|
||||
'masquerade': masquerade,
|
||||
'supports_preview_menu': True,
|
||||
'studio_url': get_studio_url(course, 'course_info'),
|
||||
'show_enroll_banner': show_enroll_banner,
|
||||
'user_is_enrolled': user_is_enrolled,
|
||||
'dates_fragment': dates_fragment,
|
||||
'course_tools': course_tools,
|
||||
}
|
||||
context.update(
|
||||
get_experiment_user_metadata_context(
|
||||
course,
|
||||
user,
|
||||
)
|
||||
)
|
||||
|
||||
# Get the URL of the user's last position in order to display the 'where you were last' message
|
||||
context['resume_course_url'] = None
|
||||
# TODO: LEARNER-611: Remove enable_course_home_improvements
|
||||
if SelfPacedConfiguration.current().enable_course_home_improvements:
|
||||
context['resume_course_url'] = get_last_accessed_courseware(course, request, user)
|
||||
|
||||
if not check_course_open_for_learner(user, course):
|
||||
# Disable student view button if user is staff and
|
||||
# course is not yet visible to students.
|
||||
context['disable_student_access'] = True
|
||||
context['supports_preview_menu'] = False
|
||||
|
||||
return render_to_response('courseware/info.html', context)
|
||||
|
||||
|
||||
class StaticCourseTabView(EdxFragmentView):
|
||||
"""
|
||||
View that displays a static course tab with a given name.
|
||||
|
||||
@@ -505,8 +505,7 @@ FEATURES = {
|
||||
# .. toggle_implementation: DjangoSetting
|
||||
# .. toggle_default: True
|
||||
# .. toggle_description: When enabled, along with the ENABLE_MKTG_SITE feature toggle, users who attempt to access a
|
||||
# course "about" page will be redirected to the course home url. This url might be the course "info" page or the
|
||||
# unified course tab (when the DISABLE_UNIFIED_COURSE_TAB_FLAG waffle is not enabled).
|
||||
# course "about" page will be redirected to the course home url.
|
||||
# .. toggle_use_cases: open_edx
|
||||
# .. toggle_creation_date: 2019-01-15
|
||||
# .. toggle_tickets: https://github.com/edx/edx-platform/pull/19604
|
||||
@@ -3148,9 +3147,6 @@ INSTALLED_APPS = [
|
||||
# Catalog integration
|
||||
'openedx.core.djangoapps.catalog',
|
||||
|
||||
# Self-paced course configuration
|
||||
'openedx.core.djangoapps.self_paced',
|
||||
|
||||
'sorl.thumbnail',
|
||||
|
||||
# edx-milestones service
|
||||
|
||||
@@ -1,386 +0,0 @@
|
||||
//// Notifications
|
||||
// Upgrade
|
||||
|
||||
$notification-highlight-border-color: $uxpl-green-base !default;
|
||||
$notification-background: rgb(255, 255, 255) !default;
|
||||
|
||||
.home {
|
||||
@include clearfix();
|
||||
|
||||
max-width: map-get($container-max-widths, xl);
|
||||
margin: 0 auto;
|
||||
padding: $baseline $baseline ($baseline/2) $baseline;
|
||||
|
||||
.page-header-main {
|
||||
display: inline-block;
|
||||
width: flex-grid(8, 12);
|
||||
margin: 0;
|
||||
|
||||
.page-title {
|
||||
margin-bottom: 5px;
|
||||
color: $dark-gray1;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
color: $dark-gray1;
|
||||
font-size: 14px;
|
||||
text-transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header-secondary {
|
||||
@include float(right);
|
||||
|
||||
display: inline-block;
|
||||
margin: ($baseline/2);
|
||||
padding: ($baseline/2) ($baseline*0.75);
|
||||
background-color: $blue;
|
||||
border-radius: 2px;
|
||||
|
||||
.last-accessed-link {
|
||||
@extend %t-title6;
|
||||
|
||||
color: $very-light-text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.info-wrapper {
|
||||
background-color: $homepage-background;
|
||||
|
||||
section.updates {
|
||||
@extend .content;
|
||||
|
||||
@include padding-left($baseline);
|
||||
|
||||
line-height: lh();
|
||||
width: 100%;
|
||||
display: block;
|
||||
|
||||
> p {
|
||||
margin-bottom: lh();
|
||||
}
|
||||
|
||||
> ol,
|
||||
section,
|
||||
div {
|
||||
list-style: none;
|
||||
margin-bottom: lh();
|
||||
padding-left: 0;
|
||||
|
||||
.updates-article {
|
||||
border-radius: 3px;
|
||||
background-color: $white;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
border: 1px solid $gray-l3;
|
||||
}
|
||||
}
|
||||
|
||||
.show-older-updates {
|
||||
@extend %btn-pl-white-base;
|
||||
|
||||
padding: ($baseline/2);
|
||||
|
||||
@include font-size(14);
|
||||
|
||||
width: 100%;
|
||||
display: block;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: unset;
|
||||
color: $m-blue-d3;
|
||||
border: 1px solid black;
|
||||
}
|
||||
}
|
||||
|
||||
> li,
|
||||
article {
|
||||
@extend .clearfix;
|
||||
|
||||
padding: $baseline;
|
||||
list-style-type: none;
|
||||
margin-bottom: lh(1.5);
|
||||
background-color: $white;
|
||||
|
||||
ol,
|
||||
ul {
|
||||
ol,
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
}
|
||||
|
||||
.date {
|
||||
@extend %t-title9;
|
||||
|
||||
margin-bottom: ($baseline/4);
|
||||
text-transform: none;
|
||||
background: url('#{$static-path}/images/calendar-icon.png') 0 center no-repeat;
|
||||
|
||||
@include padding-left($baseline);
|
||||
@include float(left);
|
||||
}
|
||||
|
||||
|
||||
.toggle-visibility-button {
|
||||
@extend %t-title9;
|
||||
|
||||
@include float(right);
|
||||
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
color: $blue;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.toggle-visibility-element {
|
||||
content: '';
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
section.update-description {
|
||||
section {
|
||||
&.primary {
|
||||
border: 1px solid #ddd;
|
||||
background: $gray-l6;
|
||||
padding: 20px;
|
||||
|
||||
p {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.author {
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
margin: lh(1.5) 0 lh(0.5);
|
||||
}
|
||||
|
||||
> ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
> ol {
|
||||
list-style: decimal outside none;
|
||||
padding: 0 0 0 1em;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: lh(0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.handouts {
|
||||
padding: 20px 30px;
|
||||
margin: 0;
|
||||
|
||||
@extend .sidebar;
|
||||
|
||||
background: rgba(0, 0, 0, 0);
|
||||
box-shadow: none;
|
||||
font-size: 14px;
|
||||
|
||||
a {
|
||||
color: $link-color;
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
|
||||
span {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: -1px;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.handouts-header {
|
||||
@include text-align(left);
|
||||
|
||||
@extend %t-strong;
|
||||
@extend %t-title6;
|
||||
|
||||
margin-bottom: 0;
|
||||
padding: 12px 26px 10px 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
ol {
|
||||
margin-bottom: 14px;
|
||||
|
||||
li {
|
||||
@include text-align(left);
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: 0;
|
||||
color: $link-color;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&.expandable,
|
||||
&.collapsable {
|
||||
margin: 0 16px 14px;
|
||||
|
||||
@include transition(all 0.2s linear 0s);
|
||||
|
||||
h4 {
|
||||
color: $link-color;
|
||||
font-size: 1em;
|
||||
font-weight: normal;
|
||||
padding-left: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
&.collapsable {
|
||||
background: $white;
|
||||
border-radius: 3px;
|
||||
padding: 14px 0;
|
||||
box-shadow: 0 0 1px 1px $shadow-l1, 0 1px 3px rgba(0, 0, 0, 0.25);
|
||||
|
||||
h4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&.multiple {
|
||||
a {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
background: none;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
border-bottom: 0;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
font-size: 0.9em;
|
||||
margin: 0;
|
||||
padding: 15px 30px;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.hitarea {
|
||||
background-image: url('#{$static-path}/images/treeview-default.gif') no-repeat;
|
||||
display: block;
|
||||
height: 100%;
|
||||
margin-left: 0;
|
||||
max-height: 20px;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
opacity: 0.6;
|
||||
filter: alpha(opacity=60);
|
||||
|
||||
+ h4 {
|
||||
@extend a:hover;
|
||||
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&.expandable-hitarea {
|
||||
background-position: -72px 0;
|
||||
}
|
||||
|
||||
&.collapsable-hitarea {
|
||||
background-position: -55px -23px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
border-bottom: 0;
|
||||
box-shadow: none;
|
||||
color: #888;
|
||||
font-size: 1em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
letter-spacing: 0;
|
||||
margin: 0;
|
||||
text-transform: none;
|
||||
|
||||
a {
|
||||
padding-right: 8px;
|
||||
|
||||
&::before {
|
||||
color: $gray-l3;
|
||||
content: "•";
|
||||
display: inline-block;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
&::before {
|
||||
content: "";
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
background: transparent !important;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
<%page expression_filter="h"/>
|
||||
<%inherit file="../main.html" />
|
||||
<%def name="online_help_token()"><% return "courseinfo" %></%def>
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
<%!
|
||||
from datetime import datetime
|
||||
from pytz import timezone, utc
|
||||
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from lms.djangoapps.courseware.courses import get_course_info_section
|
||||
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
%>
|
||||
|
||||
<%block name="pagetitle">${_("{course_number} Course Info").format(course_number=course.display_number_with_default)}</%block>
|
||||
|
||||
<%block name="headextra">
|
||||
<%static:css group='style-course-vendor'/>
|
||||
<%static:css group='style-course'/>
|
||||
</%block>
|
||||
|
||||
% if show_enroll_banner:
|
||||
<div class="wrapper-msg urgency-low" id="failed-verification-banner">
|
||||
<div class="msg msg-reverify is-dismissable">
|
||||
<div class="msg-content">
|
||||
<h2 class="title">${_("You are not enrolled yet")}</h2>
|
||||
<div class="copy">
|
||||
<p class='enroll-message'>
|
||||
${Text(_("You are not currently enrolled in this course. {link_start}Enroll now!{link_end}")).format(
|
||||
link_start=HTML("<a href={}>").format(url_to_enroll),
|
||||
link_end=HTML("</a>")
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<%include file="/courseware/course_navigation.html" args="active_page='info'" />
|
||||
|
||||
<%static:require_module_async module_name="js/courseware/toggle_element_visibility" class_name="ToggleElementVisibility">
|
||||
ToggleElementVisibility();
|
||||
</%static:require_module_async>
|
||||
<%static:require_module_async module_name="js/courseware/course_info_events" class_name="CourseInfoEvents">
|
||||
CourseInfoEvents();
|
||||
</%static:require_module_async>
|
||||
|
||||
<%block name="bodyclass">view-in-course view-course-info ${course.css_class or ''}</%block>
|
||||
|
||||
<main id="main" aria-label="Content" tabindex="-1">
|
||||
<div class="container"
|
||||
% if getattr(course, 'language'):
|
||||
lang="${course.language}"
|
||||
% endif
|
||||
>
|
||||
<div class="home">
|
||||
<div class="page-header-main">
|
||||
<h2 class="hd hd-2 page-title">
|
||||
% if show_org:
|
||||
${_("Welcome to {org}'s {course_title}!").format(org=course.display_org_with_default, course_title=course_title)}
|
||||
% else:
|
||||
${_("Welcome to {course_title}!").format(course_title=course_title)}
|
||||
% endif
|
||||
% if show_subtitle:
|
||||
<div class="page-subtitle">${course_subtitle}</div>
|
||||
% endif
|
||||
</h2>
|
||||
</div>
|
||||
% if resume_course_url and user_is_enrolled:
|
||||
<div class="page-header-secondary">
|
||||
<a href="${resume_course_url}" class="last-accessed-link">${_("Resume Course")}</a>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
<div class="info-wrapper">
|
||||
% if user.is_authenticated:
|
||||
<section class="updates">
|
||||
% if studio_url is not None and masquerade and masquerade.role == 'staff':
|
||||
<div class="wrap-instructor-info studio-view">
|
||||
<a class="instructor-info-action" href="${studio_url}">
|
||||
${_("View Updates in Studio")}
|
||||
</a>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<h3 class="hd hd-3">${_("Course Updates and News")}</h3>
|
||||
${HTML(get_course_info_section(request, masquerade_user, course, 'updates'))}
|
||||
|
||||
</section>
|
||||
<section aria-label="${_('Handout Navigation')}" class="handouts">
|
||||
% if course_tools:
|
||||
<h3 class="hd hd-3 handouts-header">${_("Course Tools")}</h3>
|
||||
% for course_tool in course_tools:
|
||||
<a href="${course_tool.url(course.id)}">
|
||||
<span class="icon ${course_tool.icon_classes()}" aria-hidden="true"></span>
|
||||
${course_tool.title()}
|
||||
</a>
|
||||
% endfor
|
||||
% endif
|
||||
% if SelfPacedConfiguration.current().enable_course_home_improvements:
|
||||
${HTML(dates_fragment.body_html())}
|
||||
% endif
|
||||
<h3 class="hd hd-3 handouts-header">${_(course.info_sidebar_name)}</h3>
|
||||
${HTML(get_course_info_section(request, masquerade_user, course, 'handouts'))}
|
||||
</section>
|
||||
% else:
|
||||
<section class="updates">
|
||||
<h3 class="hd hd-3 handouts-header">${_("Course Updates and News")}</h3>
|
||||
${HTML(get_course_info_section(request, masquerade_user, course, 'guest_updates'))}
|
||||
</section>
|
||||
<section aria-label="${_('Handout Navigation')}" class="handouts">
|
||||
<h3 class="hd hd-3 handouts-header">${_("Course Handouts")}</h3>
|
||||
${HTML(get_course_info_section(request, masquerade_user, course, 'guest_handouts'))}
|
||||
</section>
|
||||
% endif <!-- if course authenticated -->
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory">
|
||||
DateUtilFactory.transform(iterationKey=".localized-datetime");
|
||||
</%static:require_module_async>
|
||||
11
lms/urls.py
11
lms/urls.py
@@ -47,7 +47,6 @@ from openedx.core.djangoapps.password_policy import compliance as password_polic
|
||||
from openedx.core.djangoapps.password_policy.forms import PasswordPolicyAwareAdminAuthForm
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType
|
||||
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
|
||||
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.user_authn.views.login import redirect_to_lms_login
|
||||
from openedx.features.enterprise_support.api import enterprise_enabled
|
||||
@@ -379,16 +378,9 @@ urlpatterns += [
|
||||
r'^courses/{}/$'.format(
|
||||
settings.COURSE_ID_PATTERN,
|
||||
),
|
||||
courseware_views.course_info,
|
||||
courseware_views.course_about,
|
||||
name='course_root',
|
||||
),
|
||||
re_path(
|
||||
r'^courses/{}/info$'.format(
|
||||
settings.COURSE_ID_PATTERN,
|
||||
),
|
||||
courseware_views.course_info,
|
||||
name='info',
|
||||
),
|
||||
# TODO arjun remove when custom tabs in place, see courseware/courses.py
|
||||
re_path(
|
||||
r'^courses/{}/syllabus$'.format(
|
||||
@@ -892,7 +884,6 @@ if settings.FEATURES.get('ENABLE_LTI_PROVIDER'):
|
||||
]
|
||||
|
||||
urlpatterns += [
|
||||
path('config/self_paced', ConfigurationModelCurrentAPIView.as_view(model=SelfPacedConfiguration)),
|
||||
path('config/programs', ConfigurationModelCurrentAPIView.as_view(model=ProgramsApiConfig)),
|
||||
path('config/catalog', ConfigurationModelCurrentAPIView.as_view(model=CatalogIntegration)),
|
||||
path('config/forums', ConfigurationModelCurrentAPIView.as_view(model=ForumsConfig)),
|
||||
|
||||
@@ -62,7 +62,7 @@ class CourseOverviewTestCase(CatalogIntegrationMixin, ModuleStoreTestCase, Cache
|
||||
None: None,
|
||||
}
|
||||
|
||||
COURSE_OVERVIEW_TABS = {'courseware', 'info', 'textbooks', 'discussion', 'wiki', 'progress', 'dates'}
|
||||
COURSE_OVERVIEW_TABS = {'courseware', 'textbooks', 'discussion', 'wiki', 'progress', 'dates'}
|
||||
|
||||
ENABLED_SIGNALS = ['course_deleted', 'course_published']
|
||||
|
||||
|
||||
@@ -236,17 +236,6 @@ Configuration Flags
|
||||
Configuring Schedule Creation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Self-paced Configuration
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Schedules will only be created for a course if it is self-paced. A
|
||||
course can be configured to be self-paced by going to
|
||||
``<studio_url>/admin/self_paced/selfpacedconfiguration/`` and adding an
|
||||
enabled self paced config. Then, go to Studio settings for the course
|
||||
and change the Course Pacing value to “Self-Paced”. Note that the Course
|
||||
Start Date has to be set to sometime in the future in order to change
|
||||
the Course Pacing.
|
||||
|
||||
Configuring Upgrade Deadline on Schedule
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -252,7 +252,7 @@ class TestCourseNextSectionUpdateResolver(SchedulesResolverTestMixin, ModuleStor
|
||||
def test_schedule_context(self):
|
||||
resolver = self.create_resolver()
|
||||
# using this to make sure the select_related stays intact
|
||||
with self.assertNumQueries(41):
|
||||
with self.assertNumQueries(38):
|
||||
sc = resolver.get_schedules()
|
||||
schedules = list(sc)
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
"""
|
||||
Admin site bindings for self-paced courses.
|
||||
"""
|
||||
|
||||
|
||||
from config_models.admin import ConfigurationModelAdmin
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import SelfPacedConfiguration
|
||||
|
||||
admin.site.register(SelfPacedConfiguration, ConfigurationModelAdmin)
|
||||
@@ -1,27 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SelfPacedConfiguration',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
|
||||
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
|
||||
('enable_course_home_improvements', models.BooleanField(default=False, verbose_name='Enable course home page improvements.')),
|
||||
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-change_date',),
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,21 +0,0 @@
|
||||
"""
|
||||
Configuration for self-paced courses.
|
||||
"""
|
||||
|
||||
|
||||
from config_models.models import ConfigurationModel
|
||||
from django.db.models import BooleanField
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class SelfPacedConfiguration(ConfigurationModel):
|
||||
"""
|
||||
Configuration for self-paced courses.
|
||||
|
||||
.. no_pii:
|
||||
"""
|
||||
|
||||
enable_course_home_improvements = BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Enable course home page improvements.")
|
||||
)
|
||||
@@ -124,8 +124,8 @@ class TestComprehensiveThemeLMS(TestCase):
|
||||
courses_url = reverse('courses')
|
||||
resp = self.client.get(courses_url)
|
||||
assert resp.status_code == 200
|
||||
# The courses.html template includes the info.html file, which is overriden in the theme.
|
||||
self.assertContains(resp, "This overrides the courseware/info.html template.")
|
||||
# The courses.html template includes the progress.html file, which is overriden in the theme.
|
||||
self.assertContains(resp, "This overrides the courseware/progress.html template.")
|
||||
|
||||
@with_comprehensive_theme("test-theme")
|
||||
def test_include_custom_template(self):
|
||||
|
||||
@@ -16,11 +16,6 @@ DISABLE_COURSE_OUTLINE_PAGE_FLAG = CourseWaffleFlag( # lint-amnesty, pylint: di
|
||||
f'{WAFFLE_FLAG_NAMESPACE}.disable_course_outline_page', __name__
|
||||
)
|
||||
|
||||
# Waffle flag to enable a single unified "Course" tab.
|
||||
DISABLE_UNIFIED_COURSE_TAB_FLAG = CourseWaffleFlag( # lint-amnesty, pylint: disable=toggle-missing-annotation
|
||||
f'{WAFFLE_FLAG_NAMESPACE}.disable_unified_course_tab', __name__
|
||||
)
|
||||
|
||||
# Waffle flag to enable the sock on the footer of the home and courseware pages.
|
||||
DISPLAY_COURSE_SOCK_FLAG = CourseWaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.display_course_sock', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation
|
||||
|
||||
@@ -100,8 +95,4 @@ def course_home_url(course_key):
|
||||
course_key (CourseKey): The course key for which the home url is being requested.
|
||||
"""
|
||||
from .url_helpers import get_learning_mfe_home_url
|
||||
|
||||
if DISABLE_UNIFIED_COURSE_TAB_FLAG.is_enabled(course_key):
|
||||
return reverse('info', args=[str(course_key)])
|
||||
|
||||
return get_learning_mfe_home_url(course_key, url_fragment='home')
|
||||
|
||||
@@ -11,7 +11,6 @@ from django.utils.translation import gettext as _
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from openedx.core.lib.courses import get_course_by_id
|
||||
|
||||
from . import DISABLE_UNIFIED_COURSE_TAB_FLAG
|
||||
from .course_tools import CourseTool
|
||||
from .views.course_updates import CourseUpdatesFragmentView
|
||||
|
||||
@@ -46,8 +45,6 @@ class CourseUpdatesTool(CourseTool):
|
||||
"""
|
||||
Returns True if the user should be shown course updates for this course.
|
||||
"""
|
||||
if DISABLE_UNIFIED_COURSE_TAB_FLAG.is_enabled(course_key):
|
||||
return False
|
||||
if not CourseEnrollment.is_enrolled(request.user, course_key):
|
||||
return False
|
||||
course = get_course_by_id(course_key)
|
||||
|
||||
@@ -704,9 +704,12 @@ class TestEnterpriseApi(EnterpriseServiceMockMixin, CacheIsolationTestCase):
|
||||
)
|
||||
|
||||
course_id = 'course-v1:edX+DemoX+Demo_Course'
|
||||
return_to = None if is_return_to_null else 'info'
|
||||
return_to = None if is_return_to_null else 'courseware'
|
||||
|
||||
expected_path = request_mock.path if is_return_to_null else '/courses/course-v1:edX+DemoX+Demo_Course/info'
|
||||
if is_return_to_null:
|
||||
expected_path = request_mock.path
|
||||
else:
|
||||
expected_path = '/courses/course-v1:edX+DemoX+Demo_Course/courseware'
|
||||
expected_url_args = {
|
||||
'course_id': ['course-v1:edX+DemoX+Demo_Course'],
|
||||
'failure_url': ['http://localhost:8000/dashboard?consent_failed=course-v1%3AedX%2BDemoX%2BDemo_Course'],
|
||||
|
||||
1
setup.py
1
setup.py
@@ -20,7 +20,6 @@ setup(
|
||||
"openedx.course_tab": [
|
||||
"ccx = lms.djangoapps.ccx.plugins:CcxCourseTab",
|
||||
"courseware = lms.djangoapps.courseware.tabs:CoursewareTab",
|
||||
"course_info = lms.djangoapps.courseware.tabs:CourseInfoTab",
|
||||
"dates = lms.djangoapps.courseware.tabs:DatesTab",
|
||||
"discussion = lms.djangoapps.discussion.plugins:DiscussionTab",
|
||||
"edxnotes = lms.djangoapps.edxnotes.plugins:EdxNotesTab",
|
||||
|
||||
Reference in New Issue
Block a user