From ce5f1bb343b766db7e8a8ba7f23eaadb4e0ca86f Mon Sep 17 00:00:00 2001 From: Michael Terry Date: Tue, 15 Mar 2022 09:32:14 -0400 Subject: [PATCH] feat!: drop legacy course home view and related code This was the "outline tab" view of the course. Preceded by the course info view, succeeded by the MFE outline tab. In addition to the course home view itself, this drops related features: - Legacy version of Course Goals (MFE has a newer implementation) - Course home in-course search (MFE has no search) The old course info view and course about views survive for now. This also drops a few now-unused feature toggles: - course_experience.latest_update - course_experience.show_upgrade_msg_on_course_home - course_experience.upgrade_deadline_message - course_home.course_home_use_legacy_frontend With this change, just the progress and courseware tabs are still supported in legacy form, if you opt-in with waffle flags. The outline and dates tabs are offered only by the MFE. AA-798 (This is identical to previous commit be5c1a6, just reintroduced now that the e2e tests have been fixed) --- .../course_modes/tests/test_views.py | 6 +- common/djangoapps/course_modes/views.py | 3 +- common/static/common/js/utils/clamp-html.js | 57 -- .../common/js/utils/clamp-html.test.jsx | 38 - lms/djangoapps/course_api/api.py | 4 +- lms/djangoapps/course_goals/api.py | 79 +- lms/djangoapps/course_goals/handlers.py | 1 - lms/djangoapps/course_goals/models.py | 16 +- .../course_goals/tests/test_views.py | 114 --- lms/djangoapps/course_goals/urls.py | 16 - lms/djangoapps/course_goals/views.py | 104 -- .../outline/tests/test_goals.py | 4 - .../outline/tests/test_view.py | 8 - .../course_home_api/outline/views.py | 10 - lms/djangoapps/course_home_api/toggles.py | 19 +- lms/djangoapps/courseware/date_summary.py | 134 +-- lms/djangoapps/courseware/tabs.py | 15 +- lms/djangoapps/courseware/tests/test_about.py | 56 +- .../courseware/tests/test_course_survey.py | 28 +- .../courseware/tests/test_date_summary.py | 258 +---- .../courseware/tests/test_navigation.py | 4 +- lms/djangoapps/courseware/tests/test_views.py | 31 +- lms/djangoapps/courseware/views/index.py | 5 +- lms/djangoapps/courseware/views/views.py | 21 +- .../rest_api/v1/tests/test_views.py | 2 +- lms/djangoapps/support/serializers.py | 6 +- lms/envs/common.py | 3 - lms/templates/courseware/courseware.html | 5 - .../dashboard/_dashboard_course_listing.html | 6 +- lms/templates/dates_banner.html | 108 -- lms/urls.py | 14 - .../embargo/tests/test_middleware.py | 8 +- .../core/djangoapps/schedules/resolvers.py | 9 +- .../schedules/tests/test_resolvers.py | 4 +- .../djangoapps/user_authn/views/auto_auth.py | 6 +- .../user_authn/views/tests/test_auto_auth.py | 8 +- .../calendar_sync/views/calendar_sync.py | 4 +- .../views/course_bookmarks.py | 5 +- .../tests/test_course_expiration.py | 52 +- .../features/course_experience/__init__.py | 76 +- .../fixtures/course-home-fragment.html | 109 --- .../fixtures/course-outline-fragment.html | 131 --- .../fixtures/enrollment-button.html | 1 - .../fixtures/latest-update-fragment.html | 10 - .../fixtures/welcome-message-fragment.html | 32 - .../images/home_message_author.png | Bin 4831 -> 0 bytes .../course_experience/js/CourseGoals.js | 50 - .../static/course_experience/js/CourseHome.js | 160 --- .../course_experience/js/CourseOutline.js | 112 --- .../static/course_experience/js/Enrollment.js | 45 - .../course_experience/js/LatestUpdate.js | 15 - .../course_experience/js/WelcomeMessage.js | 65 -- .../js/spec/CourseHome_spec.js | 70 -- .../js/spec/CourseOutline_spec.js | 113 --- .../js/spec/Currency_spec.js | 7 + .../js/spec/Enrollment_spec.js | 48 - .../js/spec/LatestUpdate_spec.js | 38 - .../js/spec/WelcomeMessage_spec.js | 107 -- .../course-home-fragment.html | 236 ----- .../course-messages-fragment.html | 40 - .../course-outline-fragment.html | 180 ---- .../latest-update-fragment.html | 27 - .../welcome-message-fragment.html | 41 - .../tests/views/test_course_dates.py | 48 - .../tests/views/test_course_home.py | 926 +----------------- .../tests/views/test_course_outline.py | 767 --------------- .../tests/views/test_course_sock.py | 19 +- .../tests/views/test_masquerade.py | 25 +- .../tests/views/test_welcome_message.py | 80 -- openedx/features/course_experience/urls.py | 40 +- openedx/features/course_experience/utils.py | 17 - .../course_experience/views/course_dates.py | 36 - .../course_experience/views/course_home.py | 242 +---- .../views/course_home_messages.py | 232 ----- .../course_experience/views/course_outline.py | 165 ---- .../course_experience/views/course_updates.py | 7 +- .../course_experience/views/latest_update.py | 50 - .../views/welcome_message.py | 67 -- openedx/features/course_search/__init__.py | 0 .../course_search/course-search-fragment.html | 73 -- openedx/features/course_search/urls.py | 15 - .../features/course_search/views/__init__.py | 0 .../course_search/views/course_search.py | 68 -- .../enterprise_support/tests/test_utils.py | 36 +- openedx/features/enterprise_support/utils.py | 38 +- webpack.common.config.js | 6 - 86 files changed, 194 insertions(+), 5747 deletions(-) delete mode 100644 common/static/common/js/utils/clamp-html.js delete mode 100644 common/static/common/js/utils/clamp-html.test.jsx delete mode 100644 lms/djangoapps/course_goals/tests/test_views.py delete mode 100644 lms/djangoapps/course_goals/urls.py delete mode 100644 lms/djangoapps/course_goals/views.py delete mode 100644 lms/templates/dates_banner.html delete mode 100644 openedx/features/course_experience/static/course_experience/fixtures/course-home-fragment.html delete mode 100644 openedx/features/course_experience/static/course_experience/fixtures/course-outline-fragment.html delete mode 100644 openedx/features/course_experience/static/course_experience/fixtures/enrollment-button.html delete mode 100644 openedx/features/course_experience/static/course_experience/fixtures/latest-update-fragment.html delete mode 100644 openedx/features/course_experience/static/course_experience/fixtures/welcome-message-fragment.html delete mode 100644 openedx/features/course_experience/static/course_experience/images/home_message_author.png delete mode 100644 openedx/features/course_experience/static/course_experience/js/CourseGoals.js delete mode 100644 openedx/features/course_experience/static/course_experience/js/CourseHome.js delete mode 100644 openedx/features/course_experience/static/course_experience/js/CourseOutline.js delete mode 100644 openedx/features/course_experience/static/course_experience/js/Enrollment.js delete mode 100644 openedx/features/course_experience/static/course_experience/js/LatestUpdate.js delete mode 100644 openedx/features/course_experience/static/course_experience/js/WelcomeMessage.js delete mode 100644 openedx/features/course_experience/static/course_experience/js/spec/CourseHome_spec.js delete mode 100644 openedx/features/course_experience/static/course_experience/js/spec/CourseOutline_spec.js delete mode 100644 openedx/features/course_experience/static/course_experience/js/spec/Enrollment_spec.js delete mode 100644 openedx/features/course_experience/static/course_experience/js/spec/LatestUpdate_spec.js delete mode 100644 openedx/features/course_experience/static/course_experience/js/spec/WelcomeMessage_spec.js delete mode 100644 openedx/features/course_experience/templates/course_experience/course-home-fragment.html delete mode 100644 openedx/features/course_experience/templates/course_experience/course-messages-fragment.html delete mode 100644 openedx/features/course_experience/templates/course_experience/course-outline-fragment.html delete mode 100644 openedx/features/course_experience/templates/course_experience/latest-update-fragment.html delete mode 100644 openedx/features/course_experience/templates/course_experience/welcome-message-fragment.html delete mode 100644 openedx/features/course_experience/tests/views/test_course_dates.py delete mode 100644 openedx/features/course_experience/tests/views/test_course_outline.py delete mode 100644 openedx/features/course_experience/tests/views/test_welcome_message.py delete mode 100644 openedx/features/course_experience/views/course_home_messages.py delete mode 100644 openedx/features/course_experience/views/course_outline.py delete mode 100644 openedx/features/course_experience/views/latest_update.py delete mode 100644 openedx/features/course_experience/views/welcome_message.py delete mode 100644 openedx/features/course_search/__init__.py delete mode 100644 openedx/features/course_search/templates/course_search/course-search-fragment.html delete mode 100644 openedx/features/course_search/urls.py delete mode 100644 openedx/features/course_search/views/__init__.py delete mode 100644 openedx/features/course_search/views/course_search.py diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py index 5d035f506e..44e2a49b1b 100644 --- a/common/djangoapps/course_modes/tests/test_views.py +++ b/common/djangoapps/course_modes/tests/test_views.py @@ -104,10 +104,8 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest # Check whether we were correctly redirected if redirect: if has_started: - self.assertRedirects( - response, reverse('openedx.course_experience.course_home', kwargs={'course_id': course.id}), - target_status_code=302, # for follow-on redirection to MFE (ideally we'd just be sent there first) - ) + mfe_url = f'http://learning-mfe/course/{course.id}/home' + self.assertRedirects(response, mfe_url, fetch_redirect_response=False) else: self.assertRedirects(response, reverse('dashboard')) else: diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 4762c89e07..7958a480ed 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -40,6 +40,7 @@ from openedx.core.djangoapps.enrollments.permissions import ENROLL_IN_COURSE from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.features.course_duration_limits.models import CourseDurationLimitConfig from openedx.features.course_duration_limits.access import get_user_course_duration, get_user_course_expiration_date +from openedx.features.course_experience import course_home_url from openedx.features.enterprise_support.api import enterprise_customer_for_request from common.djangoapps.student.models import CourseEnrollment from common.djangoapps.util.db import outer_atomic @@ -413,7 +414,7 @@ class ChooseModeView(View): 302 to the course if possible or the dashboard if not. """ if course.has_started() or user.is_staff: - return redirect(reverse('openedx.course_experience.course_home', kwargs={'course_id': course_key})) + return redirect(course_home_url(course_key)) else: return redirect(reverse('dashboard')) diff --git a/common/static/common/js/utils/clamp-html.js b/common/static/common/js/utils/clamp-html.js deleted file mode 100644 index 4d123e669b..0000000000 --- a/common/static/common/js/utils/clamp-html.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Used to ellipsize a section of arbitrary HTML after a specified number of words. - * - * Note: this will modify the DOM structure of root in place. - * To keep the original around, you may want to save the result of cloneNode(true) before calling this method. - * - * Known bug: This method will ignore any special whitespace in the source and simply output single spaces. - * Which means that   will not be respected. This is not considered worth solving at time of writing. - * - * Returns how many words remain (or a negative number if the content got clamped) - */ -function clampHtmlByWords(root, wordsLeft) { - 'use strict'; - - if (root.nodeName === 'SCRIPT' || root.nodeName === 'STYLE') { - return wordsLeft; // early exit and ignore - } - - var remaining = wordsLeft; - var nodes = Array.from(root.childNodes ? root.childNodes : []); - var words, chopped; - - // First, cut short any text in our node, as necessary - if (root.nodeName === '#text' && root.data) { - // split on words, ignoring any resulting empty strings - words = root.data.split(/\s+/).filter(Boolean); - if (remaining < 0) { - root.data = ''; // eslint-disable-line no-param-reassign - } else if (remaining > words.length) { - remaining -= words.length; - } else { - // OK, let's add an ellipsis and cut some of root.data - chopped = words.slice(0, remaining).join(' ') + '…'; - // But be careful to get any preceding space too - if (root.data.match(/^\s/)) { - chopped = ' ' + chopped; - } - root.data = chopped; // eslint-disable-line no-param-reassign - remaining = -1; - } - } - - // Now do the same for any child nodes - nodes.forEach(function(node) { - if (remaining < 0) { - root.removeChild(node); - } else { - remaining = clampHtmlByWords(node, remaining); - } - }); - - return remaining; -} - -module.exports = { - clampHtmlByWords: clampHtmlByWords -}; diff --git a/common/static/common/js/utils/clamp-html.test.jsx b/common/static/common/js/utils/clamp-html.test.jsx deleted file mode 100644 index 0f08678446..0000000000 --- a/common/static/common/js/utils/clamp-html.test.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { clampHtmlByWords } from './clamp-html'; - -let container; -const scriptTag = ''; -const styleTag = ''; - -beforeEach(() => { - container = document.createElement("div"); - document.body.appendChild(container); -}); - -afterEach(() => { - document.body.removeChild(container); - container = null; -}); - -describe('ClampHtml', () => { - test.each([ - ['', 0, ''], - ['a b', 0, '…'], - ['a b', 1, 'a…'], - ['a b c', 2, 'a b…'], - ['a aa ab b', 2, 'a aa…'], - ['a aa ab ac', 2, 'a aa…'], - ['a aa aaa', 2, 'a aa…'], - ['a aa aaa ab', 3, 'a aa aaa…'], - ['a aa ab b c', 4, 'a aa ab b…'], - [scriptTag + 'a b c', 2, scriptTag + 'a b…'], - [styleTag + 'a b c', 2, styleTag + 'a b…'], - [scriptTag + styleTag + 'a b c', 2, scriptTag + styleTag + 'a b…'], - ])('clamps by words: %s, %i', (input, wordsLeft, expected) => { - const div = ReactDOM.render(
, container); - clampHtmlByWords(div, wordsLeft); - expect(div.innerHTML).toEqual(expected); - }); -}); diff --git a/lms/djangoapps/course_api/api.py b/lms/djangoapps/course_api/api.py index 4b5eeb942f..726e49d095 100644 --- a/lms/djangoapps/course_api/api.py +++ b/lms/djangoapps/course_api/api.py @@ -25,6 +25,7 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOvervi from openedx.core.djangoapps.content.learning_sequences.api import get_course_outline from openedx.core.djangoapps.content.learning_sequences.data import CourseOutlineData from openedx.core.lib.api.view_utils import LazySequence +from openedx.features.course_experience import course_home_url from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order @@ -285,8 +286,7 @@ def get_course_run_url(request, course_id): Returns: (string): the URL to the course run associated with course_id """ - course_run_url = reverse('openedx.course_experience.course_home', args=[course_id]) - return request.build_absolute_uri(course_run_url) + return request.build_absolute_uri(course_home_url(course_id)) def get_course_members(course_key): diff --git a/lms/djangoapps/course_goals/api.py b/lms/djangoapps/course_goals/api.py index 414eda0ba7..079dfc46b7 100644 --- a/lms/djangoapps/course_goals/api.py +++ b/lms/djangoapps/course_goals/api.py @@ -2,31 +2,9 @@ Course Goals Python API """ - from opaque_keys.edx.keys import CourseKey -from rest_framework.reverse import reverse -from common.djangoapps.course_modes.models import CourseMode -from lms.djangoapps.course_goals.models import CourseGoal, GOAL_KEY_CHOICES -from openedx.features.course_experience import ENABLE_COURSE_GOALS - - -def add_course_goal_deprecated(user, course_id, goal_key): - """ - Add a new course goal for the provided user and course. If the goal - already exists, simply update and save the goal. - This method is for the deprecated version of course goals and will be removed as soon - as the newer number of days version of course goals is fully implemented. - - Arguments: - user: The user that is setting the goal - course_id (string): The id for the course the goal refers to - goal_key (string): The goal key for the new goal. - """ - course_key = CourseKey.from_string(str(course_id)) - CourseGoal.objects.update_or_create( - user=user, course_key=course_key, defaults={'goal_key': goal_key} - ) +from lms.djangoapps.course_goals.models import CourseGoal def add_course_goal(user, course_id, subscribed_to_reminders, days_per_week=None): @@ -61,58 +39,3 @@ def get_course_goal(user, course_key): return None return CourseGoal.objects.filter(user=user, course_key=course_key).first() - - -def get_goal_api_url(request): - """ - Returns the endpoint for accessing REST API. - """ - return reverse('course_goals_api:v0:course_goal-list', request=request) - - -def has_course_goal_permission(request, course_id, user_access): - """ - Returns whether the user can access the course goal functionality. - - Only authenticated users that are enrolled in a verifiable course - can use this feature. - """ - course_key = CourseKey.from_string(course_id) - has_verified_mode = CourseMode.has_verified_mode(CourseMode.modes_for_course_dict(course_key)) - return user_access['is_enrolled'] and has_verified_mode and ENABLE_COURSE_GOALS.is_enabled(course_key) - - -def get_course_goal_options(): - """ - Returns the valid options for goal keys, mapped to their translated - strings, as defined by theCourseGoal model. - """ - return dict(GOAL_KEY_CHOICES) - - -def get_course_goal_text(goal_key): - """ - Returns the translated string for the given goal key - """ - goal_options = get_course_goal_options() - return goal_options[goal_key] - - -def valid_course_goals_ordered(include_unsure=False): - """ - Returns a list of the valid options for goal keys ordered by the level of commitment. - Each option is represented as a tuple, with (goal_key, goal_string). - - This list does not return the unsure option by default since it does not have a relevant commitment level. - """ - goal_options = get_course_goal_options() - - ordered_goal_options = [] - ordered_goal_options.append((GOAL_KEY_CHOICES.certify, goal_options[GOAL_KEY_CHOICES.certify])) - ordered_goal_options.append((GOAL_KEY_CHOICES.complete, goal_options[GOAL_KEY_CHOICES.complete])) - ordered_goal_options.append((GOAL_KEY_CHOICES.explore, goal_options[GOAL_KEY_CHOICES.explore])) - - if include_unsure: - ordered_goal_options.append((GOAL_KEY_CHOICES.unsure, goal_options[GOAL_KEY_CHOICES.unsure])) - - return ordered_goal_options diff --git a/lms/djangoapps/course_goals/handlers.py b/lms/djangoapps/course_goals/handlers.py index 661d3220f5..0c25746958 100644 --- a/lms/djangoapps/course_goals/handlers.py +++ b/lms/djangoapps/course_goals/handlers.py @@ -17,7 +17,6 @@ def emit_course_goal_event(sender, instance, **kwargs): # lint-amnesty, pylint: name = 'edx.course.goal.added' if kwargs.get('created', False) else 'edx.course.goal.updated' properties = { 'courserun_key': str(instance.course_key), - 'goal_key': instance.goal_key, 'days_per_week': instance.days_per_week, 'subscribed_to_reminders': instance.subscribed_to_reminders, } diff --git a/lms/djangoapps/course_goals/models.py b/lms/djangoapps/course_goals/models.py index f2dfc8e4ef..73d69d6bd6 100644 --- a/lms/djangoapps/course_goals/models.py +++ b/lms/djangoapps/course_goals/models.py @@ -8,7 +8,6 @@ from datetime import datetime, timedelta from django.contrib.auth import get_user_model from django.db import models -from django.utils.translation import gettext_lazy as _ from edx_django_utils.cache import TieredCache from model_utils import Choices from model_utils.models import TimeStampedModel @@ -20,12 +19,11 @@ from lms.djangoapps.courseware.context_processor import get_user_timezone_or_las from openedx.core.lib.mobile_utils import is_request_from_mobile_app from openedx.features.course_experience import ENABLE_COURSE_GOALS -# Each goal is represented by a goal key and a string description. -GOAL_KEY_CHOICES = Choices( - ('certify', _('Earn a certificate')), - ('complete', _('Complete the course')), - ('explore', _('Explore the course')), - ('unsure', _('Not sure yet')), +_GOAL_KEY_CHOICES = Choices( + ('certify', 'Earn a certificate'), + ('complete', 'Complete the course'), + ('explore', 'Explore the course'), + ('unsure', 'Not sure yet'), ) User = get_user_model() @@ -57,7 +55,9 @@ class CourseGoal(models.Model): unsubscribe_token = models.UUIDField(null=True, blank=True, unique=True, editable=False, default=uuid.uuid4, help_text='Used to validate unsubscribe requests without requiring a login') - goal_key = models.CharField(max_length=100, choices=GOAL_KEY_CHOICES, default=GOAL_KEY_CHOICES.unsure) + # Deprecated and unused - replaced by days_per_week and its subscription-based approach to goals + goal_key = models.CharField(max_length=100, choices=_GOAL_KEY_CHOICES, default=_GOAL_KEY_CHOICES.unsure) + history = HistoricalRecords() def __str__(self): diff --git a/lms/djangoapps/course_goals/tests/test_views.py b/lms/djangoapps/course_goals/tests/test_views.py deleted file mode 100644 index a5fd30c6d2..0000000000 --- a/lms/djangoapps/course_goals/tests/test_views.py +++ /dev/null @@ -1,114 +0,0 @@ -""" -Unit tests for course_goals.views methods. -""" - - -from unittest import mock - -from django.test.utils import override_settings -from django.urls import reverse -from rest_framework.test import APIClient - -from common.djangoapps.student.models import CourseEnrollment -from common.djangoapps.student.tests.factories import UserFactory -from lms.djangoapps.course_goals.models import CourseGoal -from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order - -EVENT_NAME_ADDED = 'edx.course.goal.added' -EVENT_NAME_UPDATED = 'edx.course.goal.updated' - - -class TestCourseGoalsAPI(SharedModuleStoreTestCase): - """ - Testing the Course Goals API. - """ - - def setUp(self): - # Create a course with a verified track - super().setUp() - self.course = CourseFactory.create(emit_signals=True) - - self.user = UserFactory.create(username='john', email='lennon@thebeatles.com', password='password') - CourseEnrollment.enroll(self.user, self.course.id) - - self.client = APIClient(enforce_csrf_checks=True) - self.client.login(username=self.user.username, password=self.user.password) - self.client.force_authenticate(user=self.user) - - self.apiUrl = reverse('course_goals_api:v0:course_goal-list') - - @mock.patch('lms.djangoapps.course_goals.handlers.segment.track') - @override_settings(LMS_SEGMENT_KEY="foobar") - def test_add_valid_goal(self, segment_call): - """ Ensures a correctly formatted post succeeds.""" - response = self.post_course_goal(valid=True, goal_key='certify') - segment_call.assert_called_once_with(self.user.id, EVENT_NAME_ADDED, { - 'courserun_key': str(self.course.id), - 'goal_key': 'certify', - 'days_per_week': 0, - 'subscribed_to_reminders': False, - }) - assert response.status_code == 201 - - current_goals = CourseGoal.objects.filter(user=self.user, course_key=self.course.id) - assert len(current_goals) == 1 - assert current_goals[0].goal_key == 'certify' - - def test_add_invalid_goal(self): - """ Ensures an incorrectly formatted post does not succeed. """ - response = self.post_course_goal(valid=False) - assert response.status_code == 400 - assert len(CourseGoal.objects.filter(user=self.user, course_key=self.course.id)) == 0 - - def test_add_without_goal_key(self): - """ Ensures if no goal key provided, post does not succeed. """ - response = self.post_course_goal(goal_key=None) - assert len(CourseGoal.objects.filter(user=self.user, course_key=self.course.id)) == 0 - self.assertContains( - response=response, - text='Please provide a valid goal key from following options.', - status_code=400 - ) - - @mock.patch('lms.djangoapps.course_goals.handlers.segment.track') - @override_settings(LMS_SEGMENT_KEY="foobar") - def test_update_goal(self, segment_call): - """ Ensures that repeated course goal post events do not create new instances of the goal. """ - self.post_course_goal(valid=True, goal_key='explore') - self.post_course_goal(valid=True, goal_key='certify') - self.post_course_goal(valid=True, goal_key='unsure') - - segment_call.assert_any_call(self.user.id, EVENT_NAME_ADDED, { - 'courserun_key': str(self.course.id), 'goal_key': 'explore', - 'days_per_week': 0, - 'subscribed_to_reminders': False, - }) - segment_call.assert_any_call(self.user.id, EVENT_NAME_UPDATED, { - 'courserun_key': str(self.course.id), 'goal_key': 'certify', - 'days_per_week': 0, - 'subscribed_to_reminders': False, - }) - segment_call.assert_any_call(self.user.id, EVENT_NAME_UPDATED, { - 'courserun_key': str(self.course.id), 'goal_key': 'unsure', - 'days_per_week': 0, - 'subscribed_to_reminders': False, - }) - current_goals = CourseGoal.objects.filter(user=self.user, course_key=self.course.id) - assert len(current_goals) == 1 - assert current_goals[0].goal_key == 'unsure' - - def post_course_goal(self, valid=True, goal_key='certify'): - """ - Sends a post request to set a course goal and returns the response. - """ - goal_key = goal_key if valid else 'invalid' - post_data = { - 'course_key': self.course.id, - 'user': self.user.username, - } - if goal_key: - post_data['goal_key'] = goal_key - - response = self.client.post(self.apiUrl, post_data) - return response diff --git a/lms/djangoapps/course_goals/urls.py b/lms/djangoapps/course_goals/urls.py deleted file mode 100644 index fd55934ef6..0000000000 --- a/lms/djangoapps/course_goals/urls.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Course Goals URLs -""" - - -from django.urls import include, path -from rest_framework import routers - -from .views import CourseGoalViewSet - -router = routers.DefaultRouter() -router.register(r'course_goals', CourseGoalViewSet, basename='course_goal') - -urlpatterns = [ - path('v0/', include((router.urls, "api"), namespace='v0')), -] diff --git a/lms/djangoapps/course_goals/views.py b/lms/djangoapps/course_goals/views.py deleted file mode 100644 index a2992a64c5..0000000000 --- a/lms/djangoapps/course_goals/views.py +++ /dev/null @@ -1,104 +0,0 @@ -""" -Course Goals Views - includes REST API -""" - - -from django.contrib.auth import get_user_model -from django.http import JsonResponse -from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication -from opaque_keys.edx.keys import CourseKey -from rest_framework import permissions, serializers, status, viewsets -from rest_framework.authentication import SessionAuthentication -from rest_framework.response import Response - -from lms.djangoapps.course_goals.api import get_course_goal_options -from lms.djangoapps.course_goals.models import GOAL_KEY_CHOICES, CourseGoal -from openedx.core.lib.api.permissions import IsStaffOrOwner - -User = get_user_model() - - -class CourseGoalSerializer(serializers.ModelSerializer): - """ - Serializes CourseGoal models. - """ - user = serializers.SlugRelatedField(slug_field='username', queryset=User.objects.all()) - - class Meta: - model = CourseGoal - fields = ('user', 'course_key', 'goal_key') - - -class CourseGoalViewSet(viewsets.ModelViewSet): - """ - API calls to create and update a course goal. - - Validates incoming data to ensure that course_key maps to an actual - course and that the goal_key is a valid option. - - **Use Case** - * Create a new goal for a user. - * Update an existing goal for a user - - **Example Requests** - POST /api/course_goals/v0/course_goals/ - Request data: {"course_key": , "goal_key": "", "user": ""} - - Returns Http400 response if the course_key does not map to a known - course or if the goal_key does not map to a valid goal key. - """ - authentication_classes = (JwtAuthentication, SessionAuthentication,) - permission_classes = (permissions.IsAuthenticated, IsStaffOrOwner,) - queryset = CourseGoal.objects.all() - serializer_class = CourseGoalSerializer - - # Another version of this endpoint exists in ../course_home_api/outline/views.py - # This version is used by the legacy frontend and is deprecated - def create(self, post_data): # lint-amnesty, pylint: disable=arguments-differ - """ Create a new goal if one does not exist, otherwise update the existing goal. """ - # Ensure goal_key is valid - goal_options = get_course_goal_options() - goal_key = post_data.data.get('goal_key') - if not goal_key: - return Response( - 'Please provide a valid goal key from following options. (options= {goal_options}).'.format( - goal_options=goal_options, - ), - status=status.HTTP_400_BAD_REQUEST, - ) - elif goal_key not in goal_options: - return Response( - 'Provided goal key, {goal_key}, is not a valid goal key (options= {goal_options}).'.format( - goal_key=goal_key, - goal_options=goal_options, - ), - status=status.HTTP_400_BAD_REQUEST, - ) - - # Ensure course key is valid - course_key = CourseKey.from_string(post_data.data['course_key']) - if not course_key: - return Response( - 'Provided course_key ({course_key}) does not map to a course.'.format( - course_key=course_key - ), - status=status.HTTP_400_BAD_REQUEST, - ) - - user = post_data.user - goal = CourseGoal.objects.filter(user=user.id, course_key=course_key).first() - if goal: - goal.goal_key = goal_key - goal.save(update_fields=['goal_key']) - else: - CourseGoal.objects.create( - user=user, - course_key=course_key, - goal_key=goal_key, - ) - data = { - 'goal_key': str(goal_key), - 'goal_text': str(goal_options[goal_key]), - 'is_unsure': goal_key == GOAL_KEY_CHOICES.unsure, - } - return JsonResponse(data, content_type="application/json", status=(200 if goal else 201)) # lint-amnesty, pylint: disable=redundant-content-type-for-json-response diff --git a/lms/djangoapps/course_home_api/outline/tests/test_goals.py b/lms/djangoapps/course_home_api/outline/tests/test_goals.py index aa7a97585f..699531c93d 100644 --- a/lms/djangoapps/course_home_api/outline/tests/test_goals.py +++ b/lms/djangoapps/course_home_api/outline/tests/test_goals.py @@ -72,7 +72,6 @@ class TestCourseGoalsAPI(SharedModuleStoreTestCase): 'courserun_key': str(self.course.id), 'days_per_week': 1, 'subscribed_to_reminders': True, - 'goal_key': 'unsure', }) current_goals = CourseGoal.objects.filter(user=self.user, course_key=self.course.id) @@ -89,7 +88,6 @@ class TestCourseGoalsAPI(SharedModuleStoreTestCase): 'courserun_key': str(self.course.id), 'days_per_week': 1, 'subscribed_to_reminders': True, - 'goal_key': 'unsure', }) self.save_course_goal(3, True) @@ -97,7 +95,6 @@ class TestCourseGoalsAPI(SharedModuleStoreTestCase): 'courserun_key': str(self.course.id), 'days_per_week': 3, 'subscribed_to_reminders': True, - 'goal_key': 'unsure', }) self.save_course_goal(5, False) @@ -105,7 +102,6 @@ class TestCourseGoalsAPI(SharedModuleStoreTestCase): 'courserun_key': str(self.course.id), 'days_per_week': 5, 'subscribed_to_reminders': False, - 'goal_key': 'unsure', }) current_goals = CourseGoal.objects.filter(user=self.user, course_key=self.course.id) diff --git a/lms/djangoapps/course_home_api/outline/tests/test_view.py b/lms/djangoapps/course_home_api/outline/tests/test_view.py index 2714f3ece5..ecbf803095 100644 --- a/lms/djangoapps/course_home_api/outline/tests/test_view.py +++ b/lms/djangoapps/course_home_api/outline/tests/test_view.py @@ -19,7 +19,6 @@ from common.djangoapps.student.models import CourseEnrollment from common.djangoapps.student.roles import CourseInstructorRole from common.djangoapps.student.tests.factories import UserFactory from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests -from lms.djangoapps.course_home_api.toggles import COURSE_HOME_USE_LEGACY_FRONTEND from openedx.core.djangoapps.content.learning_sequences.api import replace_course_outline from openedx.core.djangoapps.content.learning_sequences.data import CourseOutlineData, CourseVisibility from openedx.core.djangoapps.course_date_signals.utils import MIN_DURATION @@ -148,13 +147,6 @@ class OutlineTabTestViews(BaseCourseHomeTests): response = self.client.get(url) assert response.status_code == 404 - @override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True) - @ddt.data(CourseMode.AUDIT, CourseMode.VERIFIED) - def test_legacy_view_enabled(self, enrollment_mode): - CourseEnrollment.enroll(self.user, self.course.id, enrollment_mode) - response = self.client.get(self.url) - assert response.status_code == 404 - @ddt.data(True, False) def test_welcome_message(self, welcome_message_is_dismissed): CourseEnrollment.enroll(self.user, self.course.id) diff --git a/lms/djangoapps/course_home_api/outline/views.py b/lms/djangoapps/course_home_api/outline/views.py index 863f9c07dc..69460f13cb 100644 --- a/lms/djangoapps/course_home_api/outline/views.py +++ b/lms/djangoapps/course_home_api/outline/views.py @@ -6,7 +6,6 @@ from datetime import datetime, timezone from completion.exceptions import UnavailableCompletionData # lint-amnesty, pylint: disable=wrong-import-order from completion.utilities import get_key_to_last_completed_block # lint-amnesty, pylint: disable=wrong-import-order from django.conf import settings # lint-amnesty, pylint: disable=wrong-import-order -from django.http.response import Http404 # lint-amnesty, pylint: disable=wrong-import-order from django.shortcuts import get_object_or_404 # lint-amnesty, pylint: disable=wrong-import-order from django.urls import reverse # lint-amnesty, pylint: disable=wrong-import-order from django.utils.translation import gettext as _ # lint-amnesty, pylint: disable=wrong-import-order @@ -29,9 +28,6 @@ from lms.djangoapps.course_goals.api import ( ) from lms.djangoapps.course_goals.models import CourseGoal from lms.djangoapps.course_home_api.outline.serializers import OutlineTabSerializer -from lms.djangoapps.course_home_api.toggles import ( - course_home_legacy_is_active, -) from lms.djangoapps.courseware.access import has_access from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs from lms.djangoapps.courseware.courses import get_course_date_blocks, get_course_info_section, get_course_with_access @@ -54,7 +50,6 @@ from openedx.features.course_experience.url_helpers import get_learning_mfe_home from openedx.features.course_experience.utils import get_course_outline_block_tree, get_start_block from openedx.features.discounts.utils import generate_offer_data from xmodule.course_module import COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order class UnableToDismissWelcomeMessage(APIException): @@ -166,10 +161,6 @@ class OutlineTabView(RetrieveAPIView): def get(self, request, *args, **kwargs): # pylint: disable=too-many-statements course_key_string = kwargs.get('course_key_string') course_key = CourseKey.from_string(course_key_string) - course_usage_key = modulestore().make_course_usage_key(course_key) # pylint: disable=unused-variable - - if course_home_legacy_is_active(course_key): - raise Http404 # Enable NR tracing for this view based on course monitoring_utils.set_custom_attribute('course_id', course_key_string) @@ -384,7 +375,6 @@ def dismiss_welcome_message(request): # pylint: disable=missing-function-docstr @permission_classes((IsAuthenticated,)) def save_course_goal(request): # pylint: disable=missing-function-docstring course_id = request.data.get('course_id') - goal_key = request.data.get('goal_key') days_per_week = request.data.get('days_per_week') subscribed_to_reminders = request.data.get('subscribed_to_reminders') diff --git a/lms/djangoapps/course_home_api/toggles.py b/lms/djangoapps/course_home_api/toggles.py index 8c8b8ad215..bc47acf30b 100644 --- a/lms/djangoapps/course_home_api/toggles.py +++ b/lms/djangoapps/course_home_api/toggles.py @@ -11,29 +11,12 @@ WAFFLE_FLAG_NAMESPACE = LegacyWaffleFlagNamespace(name='course_home') COURSE_HOME_MICROFRONTEND_PROGRESS_TAB = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'course_home_mfe_progress_tab', # lint-amnesty, pylint: disable=toggle-missing-annotation __name__) -# .. toggle_name: course_home.course_home_use_legacy_frontend -# .. toggle_implementation: CourseWaffleFlag -# .. toggle_default: False -# .. toggle_description: This flag enables the use of the legacy view of course home as the default course frontend. -# .. Learning microfrontend (frontend-app-learning) is now an opt-out view, where if this flag is -# .. enabled the default changes from the learning microfrontend to legacy. -# .. toggle_warnings: None -# .. toggle_use_cases: temporary -# .. toggle_creation_date: 2021-06-11 -# .. toggle_target_removal_date: 2022-05-15 -# .. toggle_tickets: https://openedx.atlassian.net/browse/AA-797 -COURSE_HOME_USE_LEGACY_FRONTEND = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'course_home_use_legacy_frontend', __name__) - - -def course_home_legacy_is_active(course_key): - return COURSE_HOME_USE_LEGACY_FRONTEND.is_enabled(course_key) or course_key.deprecated - def course_home_mfe_progress_tab_is_active(course_key): # Avoiding a circular dependency from .models import DisableProgressPageStackedConfig return ( - (not course_home_legacy_is_active(course_key)) and + not course_key.deprecated and COURSE_HOME_MICROFRONTEND_PROGRESS_TAB.is_enabled(course_key) and not DisableProgressPageStackedConfig.current(course_key=course_key).disabled ) diff --git a/lms/djangoapps/courseware/date_summary.py b/lms/djangoapps/courseware/date_summary.py index d9d20018f0..4123da5f38 100644 --- a/lms/djangoapps/courseware/date_summary.py +++ b/lms/djangoapps/courseware/date_summary.py @@ -10,8 +10,6 @@ import datetime import crum from babel.dates import format_timedelta from django.conf import settings -from django.urls import reverse -from django.utils.formats import date_format from django.utils.functional import cached_property from django.utils.translation import get_language, to_locale from django.utils.translation import gettext as _ @@ -19,14 +17,14 @@ from django.utils.translation import gettext_lazy from lazy import lazy from pytz import utc -from common.djangoapps.course_modes.models import CourseMode, get_cosmetic_verified_display_price +from common.djangoapps.course_modes.models import CourseMode from lms.djangoapps.certificates.api import get_active_web_certificate, can_show_certificate_available_date_field from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link, can_show_verified_upgrade from lms.djangoapps.verify_student.models import VerificationDeadline from lms.djangoapps.verify_student.services import IDVerificationService -from openedx.core.djangolib.markup import HTML, Text +from openedx.core.djangolib.markup import HTML from openedx.features.course_duration_limits.access import get_user_course_expiration_date -from openedx.features.course_experience import RELATIVE_DATES_FLAG, UPGRADE_DEADLINE_MESSAGE, CourseHomeMessages +from openedx.features.course_experience import RELATIVE_DATES_FLAG from common.djangoapps.student.models import CourseEnrollment from .context_processor import user_timezone_locale_prefs @@ -79,12 +77,6 @@ class DateSummary: """Extra detail to display as a tooltip.""" return None - def register_alerts(self, request, course): - """ - Registers any relevant course alerts given the current request. - """ - pass # lint-amnesty, pylint: disable=unnecessary-pass - @property def date(self): """This summary's date.""" @@ -280,35 +272,6 @@ class CourseStartDate(DateSummary): return gettext_lazy('Enrollment Date') return gettext_lazy('Course starts') - def register_alerts(self, request, course): - """ - Registers an alert if the course has not started yet. - """ - is_enrolled = CourseEnrollment.get_enrollment(request.user, course.id) - if not course.start or not is_enrolled: - return - days_until_start = (course.start - self.current_time).days - if course.start > self.current_time: - if days_until_start > 0: - CourseHomeMessages.register_info_message( - request, - Text(_( - "Don't forget to add a calendar reminder!" - )), - title=Text(_("Course starts in {time_remaining_string} on {course_start_date}.")).format( - time_remaining_string=self.time_remaining_string, - course_start_date=self.long_date_html, - ) - ) - else: - CourseHomeMessages.register_info_message( - request, - Text(_("Course starts in {time_remaining_string} at {course_start_time}.")).format( - time_remaining_string=self.time_remaining_string, - course_start_time=self.short_time_html, - ) - ) - class CourseEndDate(DateSummary): """ @@ -361,34 +324,6 @@ class CourseEndDate(DateSummary): def date_type(self): return 'course-end-date' - def register_alerts(self, request, course): - """ - Registers an alert if the end date is approaching. - """ - is_enrolled = CourseEnrollment.get_enrollment(request.user, course.id) - if not course.start or not course.end or self.current_time < course.start or not is_enrolled: - return - days_until_end = (course.end - self.current_time).days - if course.end > self.current_time and days_until_end <= settings.COURSE_MESSAGE_ALERT_DURATION_IN_DAYS: - if days_until_end > 0: - CourseHomeMessages.register_info_message( - request, - Text(self.description), - title=Text(_('This course is ending in {time_remaining_string} on {course_end_date}.')).format( - time_remaining_string=self.time_remaining_string, - course_end_date=self.long_date_html, - ) - ) - else: - CourseHomeMessages.register_info_message( - request, - Text(self.description), - title=Text(_('This course is ending in {time_remaining_string} at {course_end_time}.')).format( - time_remaining_string=self.time_remaining_string, - course_end_time=self.short_time_html, - ) - ) - class CourseAssignmentDate(DateSummary): """ @@ -512,31 +447,6 @@ class CertificateAvailableDate(DateSummary): ) if mode.slug != CourseMode.AUDIT ) - def register_alerts(self, request, course): - """ - Registers an alert close to the certificate delivery date. - """ - is_enrolled = CourseEnrollment.get_enrollment(request.user, course.id) - if not is_enrolled or not self.is_enabled or (course.end and course.end > self.current_time): - return - if self.date > self.current_time: - CourseHomeMessages.register_info_message( - request, - Text(_( - 'If you have earned a certificate, you will be able to access it {time_remaining_string}' - ' from now. You will also be able to view your certificates on your {learner_profile_link}.' - )).format( - time_remaining_string=self.time_remaining_string, - learner_profile_link=HTML( - '{learner_profile_name}' - ).format( - learner_profile_url=reverse('learner_profile', kwargs={'username': request.user.username}), - learner_profile_name=_('Learner Profile'), - ), - ), - title=Text(_('We are working on generating course certificates.')) - ) - class VerifiedUpgradeDeadlineDate(DateSummary): """ @@ -608,44 +518,6 @@ class VerifiedUpgradeDeadlineDate(DateSummary): # according to their locale. return _('by {date}') - def register_alerts(self, request, course): - """ - Registers an alert if the verification deadline is approaching. - """ - upgrade_price = get_cosmetic_verified_display_price(course) - if not UPGRADE_DEADLINE_MESSAGE.is_enabled(course.id) or not self.is_enabled or not upgrade_price: - return - days_left_to_upgrade = (self.date - self.current_time).days - if self.date > self.current_time and days_left_to_upgrade <= settings.COURSE_MESSAGE_ALERT_DURATION_IN_DAYS: - upgrade_message = _( - "Don't forget, you have {time_remaining_string} left to upgrade to a Verified Certificate." - ).format(time_remaining_string=self.time_remaining_string) - if self._dynamic_deadline() is not None: - upgrade_message = _( - "Don't forget to upgrade to a verified certificate by {localized_date}." - ).format(localized_date=date_format(self.date)) - CourseHomeMessages.register_info_message( - request, - Text(_( - 'In order to qualify for a certificate, you must meet all course grading ' - 'requirements, upgrade before the course deadline, and successfully verify ' - 'your identity on {platform_name} if you have not done so already.{button_panel}' - )).format( - platform_name=settings.PLATFORM_NAME, - button_panel=HTML( - '
' - '{upgrade_label}' - '
' - ).format( - upgrade_url=self.link, - upgrade_label=Text(_('Upgrade ({upgrade_price})')).format(upgrade_price=upgrade_price), - ) - ), - title=Text(upgrade_message) - ) - class VerificationDeadlineDate(DateSummary): """ diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index 911819e855..7f774faf62 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -7,15 +7,15 @@ perform some LMS-specific tab display gymnastics for the Entrance Exams feature from django.conf import settings from django.utils.translation import gettext as _ from django.utils.translation import gettext_noop +from xmodule.tabs import CourseTab, CourseTabList, key_checker 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_legacy_is_active, course_home_mfe_progress_tab_is_active +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_name +from openedx.features.course_experience import DISABLE_UNIFIED_COURSE_TAB_FLAG, default_course_url from openedx.features.course_experience.url_helpers import get_learning_mfe_home_url from common.djangoapps.student.models import CourseEnrollment -from xmodule.tabs import CourseTab, CourseTabList, course_reverse_func_from_name_func, key_checker # lint-amnesty, pylint: disable=wrong-import-order class EnrolledTab(CourseTab): @@ -41,13 +41,8 @@ class CoursewareTab(EnrolledTab): supports_preview_menu = True def __init__(self, tab_dict): - def link_func(course, reverse_func): - if course_home_legacy_is_active(course.id): - reverse_name_func = lambda course: default_course_url_name(course.id) - url_func = course_reverse_func_from_name_func(reverse_name_func) - return url_func(course, reverse_func) - else: - return get_learning_mfe_home_url(course_key=course.id, url_fragment='home') + def link_func(course, _reverse_func): + return default_course_url(course.id) tab_dict['link_func'] = link_func super().__init__(tab_dict) diff --git a/lms/djangoapps/courseware/tests/test_about.py b/lms/djangoapps/courseware/tests/test_about.py index 4b863fbc8e..244a5fd2b3 100644 --- a/lms/djangoapps/courseware/tests/test_about.py +++ b/lms/djangoapps/courseware/tests/test_about.py @@ -9,7 +9,6 @@ from unittest import mock from unittest.mock import patch import ddt import pytz -from ccx_keys.locator import CCXLocator from django.conf import settings from django.test.utils import override_settings from django.urls import reverse @@ -29,13 +28,11 @@ from xmodule.modulestore.tests.utils import TEST_DATA_DIR from xmodule.modulestore.xml_importer import import_course_from_xml from common.djangoapps.course_modes.models import CourseMode -from lms.djangoapps.ccx.tests.factories import CcxFactory from openedx.core.djangoapps.models.course_details import CourseDetails -from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG +from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, course_home_url from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML from openedx.features.course_experience.waffle import WAFFLE_NAMESPACE as COURSE_EXPERIENCE_WAFFLE_NAMESPACE -from lms.djangoapps.course_home_api.toggles import COURSE_HOME_USE_LEGACY_FRONTEND -from common.djangoapps.student.tests.factories import AdminFactory, CourseEnrollmentAllowedFactory, UserFactory +from common.djangoapps.student.tests.factories import CourseEnrollmentAllowedFactory, UserFactory from common.djangoapps.track.tests import EventTrackingTestCase from common.djangoapps.util.milestones_helpers import get_prerequisite_courses_display, set_prerequisite_courses @@ -47,7 +44,6 @@ SHIB_ERROR_STR = "The currently logged-in user account does not have permission @ddt.ddt -@override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True) class AboutTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase, EventTrackingTestCase, MilestonesTestCaseMixin): """ Tests about xblock. @@ -124,13 +120,7 @@ class AboutTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase, EventTra self.setup_user() url = reverse('about_course', args=[str(self.course.id)]) resp = self.client.get(url) - # should be redirected - assert resp.status_code == 302 - # follow this time, and check we're redirected to the course home page - resp = self.client.get(url, follow=True) - target_url = resp.redirect_chain[-1][0] - course_home_url = reverse('openedx.course_experience.course_home', args=[str(self.course.id)]) - assert target_url.endswith(course_home_url) + self.assertRedirects(resp, course_home_url(self.course.id), fetch_redirect_response=False) @patch.dict(settings.FEATURES, {'ENABLE_COURSE_HOME_REDIRECT': False}) @patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True}) @@ -229,7 +219,6 @@ class AboutTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase, EventTra self.assertContains(resp, "Enroll Now") -@override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True) class AboutTestCaseXML(LoginEnrollmentTestCase, ModuleStoreTestCase): """ Tests for the course about page @@ -273,7 +262,6 @@ class AboutTestCaseXML(LoginEnrollmentTestCase, ModuleStoreTestCase): self.assertContains(resp, self.xml_data) -@override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True) class AboutWithCappedEnrollmentsTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase): """ This test case will check the About page when a course has a capped enrollment @@ -316,7 +304,6 @@ class AboutWithCappedEnrollmentsTestCase(LoginEnrollmentTestCase, SharedModuleSt self.assertNotContains(resp, REG_STR) -@override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True) class AboutWithInvitationOnly(SharedModuleStoreTestCase): """ This test case will check the About page when a course is invitation only. @@ -356,7 +343,6 @@ class AboutWithInvitationOnly(SharedModuleStoreTestCase): self.assertContains(resp, REG_STR) -@override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True) class AboutWithClosedEnrollment(ModuleStoreTestCase): """ This test case will check the About page for a course that has enrollment start/end @@ -393,7 +379,6 @@ class AboutWithClosedEnrollment(ModuleStoreTestCase): @ddt.ddt -@override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True) class AboutSidebarHTMLTestCase(SharedModuleStoreTestCase): """ This test case will check the About page for the content in the HTML sidebar. @@ -433,38 +418,3 @@ class AboutSidebarHTMLTestCase(SharedModuleStoreTestCase): self.assertContains(resp, itemfactory_data) else: self.assertNotContains(resp, '
') - - -class CourseAboutTestCaseCCX(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") - - @override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True) - def test_redirect_to_dashboard_unenrolled_ccx(self): - """ - Assert that when unenrolled user tries to access CCX do not allow the user to 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('openedx.course_experience.course_home', args=[ccx_locator]) - response = self.client.get(url) - expected = reverse('dashboard') - self.assertRedirects(response, expected, status_code=302, target_status_code=200) diff --git a/lms/djangoapps/courseware/tests/test_course_survey.py b/lms/djangoapps/courseware/tests/test_course_survey.py index 5cfae9047c..d729add1b6 100644 --- a/lms/djangoapps/courseware/tests/test_course_survey.py +++ b/lms/djangoapps/courseware/tests/test_course_survey.py @@ -5,20 +5,19 @@ Python tests for the Survey workflows from collections import OrderedDict from copy import deepcopy +from urllib.parse import quote from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.urls import reverse -from edx_toggles.toggles.testutils import override_waffle_flag from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory from common.test.utils import XssTestMixin -from lms.djangoapps.course_home_api.toggles import COURSE_HOME_USE_LEGACY_FRONTEND from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase from lms.djangoapps.survey.models import SurveyAnswer, SurveyForm +from openedx.features.course_experience import course_home_url -@override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True) class SurveyViewsTests(LoginEnrollmentTestCase, SharedModuleStoreTestCase, XssTestMixin): """ All tests for the views.py file @@ -78,7 +77,7 @@ class SurveyViewsTests(LoginEnrollmentTestCase, SharedModuleStoreTestCase, XssTe """ Helper method to assert that all known redirect points do redirect as expected """ - for view_name in ['courseware', 'openedx.course_experience.course_home', 'progress']: + for view_name in ['courseware', 'progress']: resp = self.client.get( reverse( view_name, @@ -95,7 +94,7 @@ class SurveyViewsTests(LoginEnrollmentTestCase, SharedModuleStoreTestCase, XssTe Helper method to asswer that all known conditionally redirect points do not redirect as expected """ - for view_name in ['courseware', 'openedx.course_experience.course_home', 'progress']: + for view_name in ['courseware', 'progress']: resp = self.client.get( reverse( view_name, @@ -119,17 +118,20 @@ class SurveyViewsTests(LoginEnrollmentTestCase, SharedModuleStoreTestCase, XssTe def test_anonymous_user_visiting_course_with_survey(self): """ - Verifies that anonymous user going to the courseware home with an unanswered survey is not - redirected to survey and home page renders without server error. + Verifies that anonymous user going to the course with an unanswered survey is not + redirected to survey. """ self.logout() resp = self.client.get( reverse( - 'openedx.course_experience.course_home', + 'courseware', kwargs={'course_id': str(self.course.id)} ) ) - assert resp.status_code == 200 + self.assertRedirects( + resp, + f'/login?next=/courses/{quote(str(self.course.id))}/courseware' + ) def test_visiting_course_with_existing_answers(self): """ @@ -206,10 +208,10 @@ class SurveyViewsTests(LoginEnrollmentTestCase, SharedModuleStoreTestCase, XssTe kwargs={'course_id': str(self.course_with_bogus_survey.id)} ) ) - course_home_path = 'openedx.course_experience.course_home' self.assertRedirects( resp, - reverse(course_home_path, kwargs={'course_id': str(self.course_with_bogus_survey.id)}) + course_home_url(self.course_with_bogus_survey.id), + fetch_redirect_response=False, ) def test_visiting_survey_with_no_course_survey(self): @@ -223,10 +225,10 @@ class SurveyViewsTests(LoginEnrollmentTestCase, SharedModuleStoreTestCase, XssTe kwargs={'course_id': str(self.course_without_survey.id)} ) ) - course_home_path = 'openedx.course_experience.course_home' self.assertRedirects( resp, - reverse(course_home_path, kwargs={'course_id': str(self.course_without_survey.id)}) + course_home_url(self.course_without_survey.id), + fetch_redirect_response=False, ) def test_survey_xss(self): diff --git a/lms/djangoapps/courseware/tests/test_date_summary.py b/lms/djangoapps/courseware/tests/test_date_summary.py index 3e914168fd..741a02bea4 100644 --- a/lms/djangoapps/courseware/tests/test_date_summary.py +++ b/lms/djangoapps/courseware/tests/test_date_summary.py @@ -9,7 +9,6 @@ import crum import ddt import waffle # lint-amnesty, pylint: disable=invalid-django-waffle-import from django.conf import settings -from django.contrib.messages.middleware import MessageMiddleware from django.test import RequestFactory from django.urls import reverse from edx_toggles.toggles.testutils import override_waffle_flag @@ -22,7 +21,6 @@ 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 lms.djangoapps.commerce.models import CommerceConfiguration -from lms.djangoapps.course_home_api.toggles import COURSE_HOME_USE_LEGACY_FRONTEND from lms.djangoapps.courseware.courses import get_course_date_blocks from lms.djangoapps.courseware.date_summary import ( CertificateAvailableDate, @@ -45,14 +43,8 @@ from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVer 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.core.djangoapps.user_api.preferences.api import set_user_preference from openedx.features.course_duration_limits.models import CourseDurationLimitConfig -from openedx.features.course_experience import ( - DISABLE_UNIFIED_COURSE_TAB_FLAG, - RELATIVE_DATES_FLAG, - UPGRADE_DEADLINE_MESSAGE, - CourseHomeMessages -) +from openedx.features.course_experience import RELATIVE_DATES_FLAG from common.djangoapps.student.tests.factories import TEST_PASSWORD, CourseEnrollmentFactory, UserFactory @@ -82,13 +74,6 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): response = self.client.get(url) self.assertNotContains(response, 'date-summary', status_code=302) - @override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True) - def test_course_home_logged_out(self): - course = create_course_run() - url = reverse('openedx.course_experience.course_home', args=(course.id,)) - response = self.client.get(url) - assert 200 == response.status_code - # 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.""" @@ -424,53 +409,6 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): assert block.date == datetime.now(utc) assert block.title == 'current_datetime' - @ddt.data( - 'info', - 'openedx.course_experience.course_home', - ) - @override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True) - @override_waffle_flag(DISABLE_UNIFIED_COURSE_TAB_FLAG, active=False) - def test_todays_date_no_timezone(self, url_name): - with freeze_time('2015-01-02'): - course = create_course_run() - user = create_user() - self.client.login(username=user.username, password=TEST_PASSWORD) - - html_elements = [ - '

Upcoming Dates

', - '
Upcoming Dates', - '
', - f'/course/{course.id}/dates', - ] - url = reverse(url_name, args=(course.id,)) - - def assert_html_elements(assert_function, user): - self.client.login(username=user.username, password=TEST_PASSWORD) - response = self.client.get(url, follow=True) - if user.is_staff: - for html in html_elements: - assert_function(response, html) - else: - assert 404 == response.status_code - self.client.logout() - - with freeze_time('2015-01-02'): - unenrolled_user = create_user() - assert_html_elements(self.assertNotContains, unenrolled_user) - - staff_user = create_user() - staff_user.is_staff = True - staff_user.save() - assert_html_elements(self.assertContains, staff_user) - - enrolled_user = create_user() - CourseEnrollmentFactory(course_id=course.id, user=enrolled_user, mode=CourseMode.VERIFIED) - assert_html_elements(self.assertContains, enrolled_user) - - -@ddt.ddt -class TestDateAlerts(SharedModuleStoreTestCase): - """ - Unit tests for date alerts. - """ - def setUp(self): - super().setUp() - with freeze_time('2017-07-01 09:00:00'): - self.course = create_course_run(days_till_start=0) - self.course.certificate_available_date = self.course.start + timedelta(days=21) - enable_course_certificates(self.course) - self.enrollment = CourseEnrollmentFactory(course_id=self.course.id, mode=CourseMode.AUDIT) - self.request = RequestFactory().request() - self.request.session = {} - self.request.user = self.enrollment.user - MessageMiddleware().process_request(self.request) - - @ddt.data( - ['2017-01-01 09:00:00', 'in 6 months on -% if display_reset_dates_banner: - -% endif <%def name="course_name()"> <% return _("{course_number} Courseware").format(course_number=course.display_number_with_default) %> diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html index 9d0cd188d2..b80ec5d77f 100644 --- a/lms/templates/dashboard/_dashboard_course_listing.html +++ b/lms/templates/dashboard/_dashboard_course_listing.html @@ -12,12 +12,10 @@ from django.urls import reverse from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.course_modes.helpers import enrollment_mode_display from common.djangoapps.student.helpers import user_has_passing_grade_in_course -from lms.djangoapps.course_home_api.toggles import course_home_legacy_is_active from lms.djangoapps.verify_student.services import IDVerificationService from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string from openedx.core.djangolib.markup import HTML, Text -from openedx.features.course_experience import course_home_url_name -from openedx.features.course_experience.url_helpers import get_learning_mfe_home_url +from openedx.features.course_experience import course_home_url from common.djangoapps.student.helpers import ( VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, @@ -82,7 +80,7 @@ from lms.djangoapps.experiments.utils import UPSELL_TRACKING_FLAG % endif >
- <% course_target = reverse(course_home_url_name(course_overview.id), args=[str(course_overview.id)]) if course_home_legacy_is_active(course_overview.id) else get_learning_mfe_home_url(course_key=course_overview.id, url_fragment="home") %> + <% course_target = course_home_url(course_overview.id) %>

${_('Course details')}