From ff184fec7ecf41e02c611f14b8dbc32de7539065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Fernanda=20Magallanes=20Z?= Date: Thu, 21 Apr 2022 20:21:52 -0400 Subject: [PATCH 01/11] fix: correct relative dates flag --- openedx/features/course_experience/__init__.py | 2 +- .../templates/course_experience/course-outline-fragment.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openedx/features/course_experience/__init__.py b/openedx/features/course_experience/__init__.py index 1bbca546aa2..d12d32dc6a9 100644 --- a/openedx/features/course_experience/__init__.py +++ b/openedx/features/course_experience/__init__.py @@ -78,7 +78,7 @@ # .. toggle_warnings: To set a relative due date for self-paced courses, the weeks_to_complete field for a course run # needs to be set. Currently it can be set through the publisher app. # .. toggle_tickets: https://openedx.atlassian.net/browse/AA-27 -RELATIVE_DATES_FLAG = ExperimentWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'relative_dates', __name__, experiment_id=17) +RELATIVE_DATES_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'relative_dates') # .. toggle_name: course_experience.calendar_sync # .. toggle_implementation: CourseWaffleFlag diff --git a/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html b/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html index 6b7e30d76b2..7d2ce5b1581 100644 --- a/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +++ b/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html @@ -21,7 +21,7 @@ <% course_sections = blocks.get('children') self_paced = context.get('self_paced', False) -relative_dates_flag_is_enabled = RELATIVE_DATES_FLAG.is_enabled(str(course_key)) +relative_dates_flag_is_enabled = RELATIVE_DATES_FLAG.is_enabled(course_key) is_course_staff = bool(user and course and has_access(user, 'staff', course, course.id)) dates_banner_displayed = False %> From 83c23c2144a5c1ef2080f7eb5b1c5dc93b91c8e5 Mon Sep 17 00:00:00 2001 From: Ivo Branco Date: Wed, 16 Jun 2021 16:19:54 +0000 Subject: [PATCH 02/11] GN-561 Error when viewing a course Anonymous when course dates tab --- lms/djangoapps/courseware/courses.py | 9 +++++++++ lms/djangoapps/courseware/tabs.py | 5 +++++ .../features/course_experience/views/course_home.py | 11 ++++++++--- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 9f9bfefbdc3..7174c2e8ee3 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -477,7 +477,16 @@ def get_course_date_blocks(course, user, request=None, include_access=False, VerificationDeadlineDate, VerifiedUpgradeDeadlineDate, ] + if not course.self_paced and certs_api.get_active_web_certificate(course): + default_block_classes.insert(0, CertificateAvailableDate) + blocks.extend([cls(course, user) for cls in default_block_classes]) + if RELATIVE_DATES_FLAG.is_enabled(course.id) and user and user.is_authenticated: + blocks.append(CourseExpiredDate(course, user)) + blocks.extend(get_course_assignment_date_blocks( + course, user, request, num_return=num_assignments, + include_access=include_access, include_past_dates=include_past_dates, + )) blocks = filter(lambda b: b.is_allowed and b.date and (include_past_dates or b.is_enabled), blocks) return sorted(blocks, key=date_block_key_fn) diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index 0a8cc2da5c2..b8e7e4d99aa 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -342,6 +342,11 @@ def link_func(course, reverse_func): tab_dict['link_func'] = link_func super().__init__(tab_dict) + @classmethod + def is_enabled(cls, course, user=None): + """Returns true if this tab is enabled.""" + return RELATIVE_DATES_FLAG.is_enabled(course.id) and super(DatesTab, cls).is_enabled(course, user=user) + def get_course_tab_list(user, course): """ diff --git a/openedx/features/course_experience/views/course_home.py b/openedx/features/course_experience/views/course_home.py index a4d031f3320..c01f57e1f55 100644 --- a/openedx/features/course_experience/views/course_home.py +++ b/openedx/features/course_experience/views/course_home.py @@ -120,9 +120,6 @@ def render_to_fragment(self, request, course_id=None, **kwargs): # lint-amnesty course_key = CourseKey.from_string(course_id) course = get_course_with_access(request.user, 'load', course_key) - # Render the course dates as a fragment - dates_fragment = CourseDatesFragmentView().render_to_fragment(request, course_id=course_id, **kwargs) - # Render the full content to enrolled users, as well as to course and global staff. # Unenrolled users who are not course or global staff are given only a subset. enrollment = CourseEnrollment.get_enrollment(request.user, course_key) @@ -145,6 +142,7 @@ def render_to_fragment(self, request, course_id=None, **kwargs): # lint-amnesty has_visited_course = None resume_course_url = None handouts_html = None + dates_fragment = None course_overview = CourseOverview.get_from_id(course.id) if user_access['is_enrolled'] or user_access['is_staff']: @@ -172,6 +170,13 @@ def render_to_fragment(self, request, course_id=None, **kwargs): # lint-amnesty request.user, course_overview ) + + next_up_banner_fragment = NextUpBannerFragmentView().render_to_fragment( + assignment_title=resume_course_title, resume_course_url=resume_course_url, assignment_duration='10 min' + ) + + dates_fragment = CourseDatesFragmentView().render_to_fragment(request, course_id=course_id, **kwargs) + elif allow_public_outline or allow_public: outline_fragment = CourseOutlineFragmentView().render_to_fragment( request, course_id=course_id, user_is_enrolled=False, **kwargs From 8bb3cccbb32718d7871f0428e6cc644370c675ea Mon Sep 17 00:00:00 2001 From: henrry pulgarin Date: Mon, 2 May 2022 22:07:45 -0500 Subject: [PATCH 03/11] fix: missing modules, unused variables --- lms/djangoapps/courseware/courses.py | 1 + lms/djangoapps/courseware/tabs.py | 1 + .../course_experience/views/course_home.py | 13 ++++++--- .../course_experience/views/next_up_banner.py | 27 +++++++++++++++++++ 4 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 openedx/features/course_experience/views/next_up_banner.py diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 7174c2e8ee3..e621dd67f67 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -18,6 +18,7 @@ from fs.errors import ResourceNotFound from opaque_keys.edx.keys import UsageKey from path import Path as path +from lms.djangoapps.certificates import api as certs_api from openedx.core.lib.cache_utils import request_cached diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index b8e7e4d99aa..44b27faf7d9 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -14,6 +14,7 @@ from openedx.core.lib.course_tabs import CourseTabPluginManager from openedx.features.course_experience import DISABLE_UNIFIED_COURSE_TAB_FLAG, default_course_url_name from openedx.features.course_experience.url_helpers import get_learning_mfe_home_url +from openedx.features.course_experience import RELATIVE_DATES_FLAG from common.djangoapps.student.models import CourseEnrollment from xmodule.tabs import CourseTab, CourseTabList, course_reverse_func_from_name_func, key_checker diff --git a/openedx/features/course_experience/views/course_home.py b/openedx/features/course_experience/views/course_home.py index c01f57e1f55..170805cd48d 100644 --- a/openedx/features/course_experience/views/course_home.py +++ b/openedx/features/course_experience/views/course_home.py @@ -14,6 +14,7 @@ from opaque_keys.edx.keys import CourseKey from web_fragments.fragment import Fragment + from lms.djangoapps.course_home_api.toggles import course_home_mfe_outline_tab_is_active from lms.djangoapps.courseware.access import has_access from lms.djangoapps.courseware.courses import can_self_enroll_in_course, get_course_info_section, get_course_with_access @@ -39,6 +40,7 @@ from common.djangoapps.util.views import ensure_valid_course_key from xmodule.course_module import COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE +from .next_up_banner import NextUpBannerFragmentView from .. import ( COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, LATEST_UPDATE_FLAG, @@ -87,10 +89,11 @@ def _get_resume_course_info(self, request, course_id): """ Returns information relevant to resume course functionality. - Returns a tuple: (has_visited_course, resume_course_url) + Returns a tuple: (has_visited_course, resume_course_url, resume_course_title) has_visited_course: True if the user has ever completed a block, False otherwise. resume_course_url: The URL of the 'resume course' block if the user has completed a block, otherwise the URL of the first block to start the course. + resume_course_title: The display_name of the resume course block, otherwise the display_name of course root """ course_outline_root_block = get_course_outline_block_tree(request, course_id, request.user) @@ -98,11 +101,13 @@ def _get_resume_course_info(self, request, course_id): has_visited_course = bool(resume_block) if resume_block: resume_course_url = resume_block['lms_web_url'] + resume_course_title = resume_block['display_name'] else: start_block = get_start_block(course_outline_root_block) if course_outline_root_block else None resume_course_url = start_block['lms_web_url'] if start_block else None + resume_course_title = course_outline_root_block['display_name'] if course_outline_root_block else None - return has_visited_course, resume_course_url + return has_visited_course, resume_course_url, resume_course_title def _get_course_handouts(self, request, course): """ @@ -138,6 +143,7 @@ def render_to_fragment(self, request, course_id=None, **kwargs): # lint-amnesty update_message_fragment = None course_sock_fragment = None offer_banner_fragment = None + next_up_banner_fragment = None course_expiration_fragment = None has_visited_course = None resume_course_url = None @@ -160,7 +166,7 @@ def render_to_fragment(self, request, course_id=None, **kwargs): # lint-amnesty course_sock_fragment = CourseSockFragmentView().render_to_fragment( request, course=course, **kwargs ) - has_visited_course, resume_course_url = self._get_resume_course_info(request, course_id) + has_visited_course, resume_course_url, resume_course_title = self._get_resume_course_info(request, course_id) # pylint: disable=line-too-long handouts_html = self._get_course_handouts(request, course) offer_banner_fragment = get_first_purchase_offer_banner_fragment( request.user, @@ -237,6 +243,7 @@ def render_to_fragment(self, request, course_id=None, **kwargs): # lint-amnesty 'course_home_message_fragment': course_home_message_fragment, 'offer_banner_fragment': offer_banner_fragment, 'course_expiration_fragment': course_expiration_fragment, + 'next_up_banner_fragment': next_up_banner_fragment, 'has_visited_course': has_visited_course, 'resume_course_url': resume_course_url, 'course_tools': course_tools, diff --git a/openedx/features/course_experience/views/next_up_banner.py b/openedx/features/course_experience/views/next_up_banner.py new file mode 100644 index 00000000000..d1067581415 --- /dev/null +++ b/openedx/features/course_experience/views/next_up_banner.py @@ -0,0 +1,27 @@ +""" +View logic for handling course messages. +""" + +from django.template.loader import render_to_string +from web_fragments.fragment import Fragment + +from openedx.core.djangoapps.plugin_api.views import EdxFragmentView + + +class NextUpBannerFragmentView(EdxFragmentView): + """ + A fragment that displays an up next banner with a call to action to resume the course. + """ + + # pylint: disable=arguments-differ + def render_to_fragment(self, assignment_title, resume_course_url, assignment_duration='10 mins'): + """ + Renders an up next banner fragment with the provided assignment title, duration, and a link to the URL. + """ + context = { + 'assignment_title': assignment_title, + 'resume_course_url': resume_course_url, + 'assignment_duration': assignment_duration, + } + html = render_to_string('course_experience/next-up-banner-fragment.html', context) + return Fragment(html) From b2c6e8daaf98486b8b24ce9e6dcef828b214f800 Mon Sep 17 00:00:00 2001 From: henrry pulgarin Date: Tue, 3 May 2022 15:56:29 -0500 Subject: [PATCH 04/11] fix: pytest issues --- openedx/core/djangoapps/courseware_api/tests/test_views.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openedx/core/djangoapps/courseware_api/tests/test_views.py b/openedx/core/djangoapps/courseware_api/tests/test_views.py index 0a96cd7bd3e..704422cfb95 100644 --- a/openedx/core/djangoapps/courseware_api/tests/test_views.py +++ b/openedx/core/djangoapps/courseware_api/tests/test_views.py @@ -36,10 +36,8 @@ from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import ItemFactory, ToyCourseFactory - User = get_user_model() - @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') class BaseCoursewareTests(SharedModuleStoreTestCase): """ @@ -138,7 +136,7 @@ def test_course_metadata(self, logged_in, enrollment_mode, enable_anonymous, is_ enrollment = response.data['enrollment'] assert enrollment_mode == enrollment['mode'] assert enrollment['is_active'] - assert len(response.data['tabs']) == 6 + assert len(response.data['tabs']) == 5 found = False for tab in response.data['tabs']: if tab['type'] == 'external_link': From 03cc532ab6352c22f1dcc5afdc999383670426d8 Mon Sep 17 00:00:00 2001 From: henrry pulgarin Date: Wed, 4 May 2022 12:02:02 -0500 Subject: [PATCH 05/11] fix: adding missing file --- .../templates/course_experience/next-up-banner-fragment.html | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 openedx/features/course_experience/templates/course_experience/next-up-banner-fragment.html diff --git a/openedx/features/course_experience/templates/course_experience/next-up-banner-fragment.html b/openedx/features/course_experience/templates/course_experience/next-up-banner-fragment.html new file mode 100644 index 00000000000..e69de29bb2d From 379a4b0e58b14224cca06c3629ab3d83cd641f7d Mon Sep 17 00:00:00 2001 From: henrry pulgarin Date: Wed, 4 May 2022 14:12:55 -0500 Subject: [PATCH 06/11] fix: quality and pytest issues --- openedx/core/djangoapps/courseware_api/tests/test_views.py | 1 + openedx/core/djangoapps/schedules/tests/test_resolvers.py | 2 +- openedx/features/course_experience/views/course_home.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openedx/core/djangoapps/courseware_api/tests/test_views.py b/openedx/core/djangoapps/courseware_api/tests/test_views.py index 704422cfb95..86c9dd4c162 100644 --- a/openedx/core/djangoapps/courseware_api/tests/test_views.py +++ b/openedx/core/djangoapps/courseware_api/tests/test_views.py @@ -38,6 +38,7 @@ User = get_user_model() + @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') class BaseCoursewareTests(SharedModuleStoreTestCase): """ diff --git a/openedx/core/djangoapps/schedules/tests/test_resolvers.py b/openedx/core/djangoapps/schedules/tests/test_resolvers.py index b8cf1274cff..858315a3a0d 100644 --- a/openedx/core/djangoapps/schedules/tests/test_resolvers.py +++ b/openedx/core/djangoapps/schedules/tests/test_resolvers.py @@ -251,7 +251,7 @@ def create_resolver(self, user_start_date_offset=8): def test_schedule_context(self): resolver = self.create_resolver() # using this to make sure the select_related stays intact - with self.assertNumQueries(15): + with self.assertNumQueries(26): sc = resolver.get_schedules() schedules = list(sc) diff --git a/openedx/features/course_experience/views/course_home.py b/openedx/features/course_experience/views/course_home.py index 170805cd48d..58a6d4f1ab9 100644 --- a/openedx/features/course_experience/views/course_home.py +++ b/openedx/features/course_experience/views/course_home.py @@ -166,7 +166,7 @@ def render_to_fragment(self, request, course_id=None, **kwargs): # lint-amnesty course_sock_fragment = CourseSockFragmentView().render_to_fragment( request, course=course, **kwargs ) - has_visited_course, resume_course_url, resume_course_title = self._get_resume_course_info(request, course_id) # pylint: disable=line-too-long + has_visited_course, resume_course_url, resume_course_title = self._get_resume_course_info(request, course_id) #pylint: disable=line-too-long handouts_html = self._get_course_handouts(request, course) offer_banner_fragment = get_first_purchase_offer_banner_fragment( request.user, From 7224009c192703b86a268568e216e60130ba9b96 Mon Sep 17 00:00:00 2001 From: henrry pulgarin Date: Thu, 5 May 2022 08:37:40 -0500 Subject: [PATCH 07/11] fix: empty file --- .../course_experience/next-up-banner-fragment.html | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openedx/features/course_experience/templates/course_experience/next-up-banner-fragment.html b/openedx/features/course_experience/templates/course_experience/next-up-banner-fragment.html index e69de29bb2d..63762d52330 100644 --- a/openedx/features/course_experience/templates/course_experience/next-up-banner-fragment.html +++ b/openedx/features/course_experience/templates/course_experience/next-up-banner-fragment.html @@ -0,0 +1,13 @@ +## mako + +<%page expression_filter="h"/> + +<%! +from django.utils.translation import ugettext as _ +%> + +
+ ${_("Get started on what's next: ")} + ${assignment_title} + (${assignment_duration}) +
\ No newline at end of file From 15ed677654cb4498e057f8deffd8d3b5d4ff875e Mon Sep 17 00:00:00 2001 From: henrry pulgarin Date: Wed, 11 May 2022 11:04:38 -0500 Subject: [PATCH 08/11] fix: WaffleFlags overwrite --- lms/djangoapps/experiments/testutils.py | 9 +++++++++ openedx/features/calendar_sync/tests/test_plugins.py | 4 ++-- .../tests/views/test_course_outline.py | 10 +++++----- .../features/course_experience/views/course_home.py | 2 +- .../show_answer/tests/test_show_answer_override.py | 6 +++--- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/lms/djangoapps/experiments/testutils.py b/lms/djangoapps/experiments/testutils.py index 1a14fd35c85..24dc7f00d9e 100644 --- a/lms/djangoapps/experiments/testutils.py +++ b/lms/djangoapps/experiments/testutils.py @@ -15,3 +15,12 @@ def override_experiment_waffle_flag(flag, active=True, bucket=1): with override_waffle_flag(flag, active): with patch.object(flag, "get_bucket", return_value=bucket): yield + + +@contextmanager +def override__coursewaffleflag_flag(flag, active=True): + """ + Override the base waffle flag + """ + with override_waffle_flag(flag, active): + yield diff --git a/openedx/features/calendar_sync/tests/test_plugins.py b/openedx/features/calendar_sync/tests/test_plugins.py index 37313e4fa9f..7b4c3fc8990 100644 --- a/openedx/features/calendar_sync/tests/test_plugins.py +++ b/openedx/features/calendar_sync/tests/test_plugins.py @@ -8,7 +8,7 @@ from django.test import RequestFactory from edx_toggles.toggles.testutils import override_waffle_flag -from lms.djangoapps.experiments.testutils import override_experiment_waffle_flag +from lms.djangoapps.experiments.testutils import override__coursewaffleflag_flag from openedx.features.calendar_sync.plugins import CalendarSyncToggleTool from openedx.features.course_experience import CALENDAR_SYNC_FLAG, RELATIVE_DATES_FLAG from xmodule.modulestore.tests.django_utils import CourseUserType, SharedModuleStoreTestCase @@ -35,7 +35,7 @@ def setUpClass(cls): ) @ddt.unpack @override_waffle_flag(CALENDAR_SYNC_FLAG, active=True) - @override_experiment_waffle_flag(RELATIVE_DATES_FLAG, active=True) + @override__coursewaffleflag_flag(RELATIVE_DATES_FLAG, active=True) def test_calendar_sync_toggle_tool_is_enabled(self, user_type, should_be_enabled): request = RequestFactory().request() request.user = self.create_user_for_course(self.course, user_type) diff --git a/openedx/features/course_experience/tests/views/test_course_outline.py b/openedx/features/course_experience/tests/views/test_course_outline.py index 2368d20f9bf..113cccb0c2f 100644 --- a/openedx/features/course_experience/tests/views/test_course_outline.py +++ b/openedx/features/course_experience/tests/views/test_course_outline.py @@ -30,7 +30,7 @@ from lms.djangoapps.gating import api as lms_gating_api from lms.djangoapps.courseware.tests.factories import StaffFactory from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin -from lms.djangoapps.experiments.testutils import override_experiment_waffle_flag +from lms.djangoapps.experiments.testutils import override__coursewaffleflag_flag from lms.urls import RESET_COURSE_DEADLINES_NAME from openedx.core.djangoapps.course_date_signals.models import SelfPacedRelativeDatesConfig from openedx.core.djangoapps.schedules.models import Schedule @@ -138,7 +138,7 @@ def setUp(self): super(TestCourseOutlinePage, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments self.client.login(username=self.user.username, password=TEST_PASSWORD) - @override_experiment_waffle_flag(RELATIVE_DATES_FLAG, active=True) + @override__coursewaffleflag_flag(RELATIVE_DATES_FLAG, active=True) def test_outline_details(self): for course in self.courses: @@ -193,7 +193,7 @@ def test_num_graded_problems(self): self.assertRegex(content, sequential2.display_name + r'\s*\(1 Question\)\s*') self.assertRegex(content, sequential3.display_name + r'\s*\(2 Questions\)\s*') - @override_experiment_waffle_flag(RELATIVE_DATES_FLAG, active=True) + @override__coursewaffleflag_flag(RELATIVE_DATES_FLAG, active=True) @ddt.data( ([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.AUDIT, False, True), ([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.VERIFIED, False, True), @@ -232,7 +232,7 @@ def test_reset_course_deadlines_banner_shows_for_self_paced_course( else: self.assertNotContains(response, '