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)
This commit is contained in:
Michael Terry
2022-03-15 09:32:14 -04:00
parent 584f400ca8
commit ce5f1bb343
86 changed files with 194 additions and 5747 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": <course-key>, "goal_key": "<goal-key>", "user": "<username>"}
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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(
'<a href="{learner_profile_url}">{learner_profile_name}</a>'
).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(
'<div class="message-actions">'
'<a id="certificate_upsell" class="btn btn-upgrade"'
'data-creative="original_message" data-position="course_message"'
'href="{upgrade_url}">{upgrade_label}</a>'
'</div>'
).format(
upgrade_url=self.link,
upgrade_label=Text(_('Upgrade ({upgrade_price})')).format(upgrade_price=upgrade_price),
)
),
title=Text(upgrade_message)
)
class VerificationDeadlineDate(DateSummary):
"""

View File

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

View File

@@ -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, '<section class="about-sidebar-html">')
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)

View File

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

View File

@@ -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 = [
'<h3 class="hd hd-6 handouts-header">Upcoming Dates</h3>',
'<div class="date-summary',
'<p class="hd hd-6 date localized-datetime"',
'data-timezone="None"'
]
url = reverse(url_name, args=(course.id,))
response = self.client.get(url, follow=True)
for html in html_elements:
self.assertContains(response, html)
@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_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)
set_user_preference(user, 'time_zone', 'America/Los_Angeles')
url = reverse(url_name, args=(course.id,))
response = self.client.get(url, follow=True)
html_elements = [
'<h3 class="hd hd-6 handouts-header">Upcoming Dates</h3>',
'<div class="date-summary',
'<p class="hd hd-6 date localized-datetime"',
'data-timezone="America/Los_Angeles"'
]
for html in html_elements:
self.assertContains(response, html)
## Tests Course Start Date
def test_course_start_date(self):
course = create_course_run()
@@ -478,46 +416,6 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
block = CourseStartDate(course, user)
assert block.date == course.start
@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_start_date_render(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)
url = reverse(url_name, args=(course.id,))
response = self.client.get(url, follow=True)
html_elements = [
'data-datetime="2015-01-03 00:00:00+00:00"'
]
for html in html_elements:
self.assertContains(response, html)
@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_start_date_render_time_zone(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)
set_user_preference(user, 'time_zone', 'America/Los_Angeles')
url = reverse(url_name, args=(course.id,))
response = self.client.get(url, follow=True)
html_elements = [
'data-datetime="2015-01-03 00:00:00+00:00"',
'data-timezone="America/Los_Angeles"'
]
for html in html_elements:
self.assertContains(response, html)
## Tests Course End Date Block
def test_course_end_date_for_certificate_eligible_mode(self):
course = create_course_run(days_till_start=-1)
@@ -723,160 +621,6 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
block = VerificationDeadlineDate(course, user)
assert block.relative_datestring == expected_date_string
@ddt.data(
'info',
'openedx.course_experience.course_home',
)
@override_waffle_flag(DISABLE_UNIFIED_COURSE_TAB_FLAG, active=False)
@override_waffle_flag(RELATIVE_DATES_FLAG, active=True)
def test_dates_tab_link_render(self, url_name):
""" The dates tab link should only show for enrolled or staff users """
course = create_course_run()
html_elements = [
'class="dates-tab-link"',
'View all course dates</a>',
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 <span class="date localized-datetime" data-format="shortDate"'],
['2017-06-17 09:00:00', 'in 2 weeks on <span class="date localized-datetime" data-format="shortDate"'],
['2017-06-30 10:00:00', 'in 1 day at <span class="date localized-datetime" data-format="shortTime"'],
['2017-07-01 08:00:00', 'in 1 hour at <span class="date localized-datetime" data-format="shortTime"'],
['2017-07-01 08:55:00', 'in 5 minutes at <span class="date localized-datetime" data-format="shortTime"'],
['2017-07-01 09:00:00', None],
['2017-08-01 09:00:00', None],
)
@ddt.unpack
def test_start_date_alert(self, current_time, expected_message_html):
"""
Verify that course start date alerts are registered.
"""
with freeze_time(current_time):
block = CourseStartDate(self.course, self.request.user)
block.register_alerts(self.request, self.course)
messages = list(CourseHomeMessages.user_messages(self.request))
if expected_message_html:
assert len(messages) == 1
assert expected_message_html in messages[0].message_html
else:
assert len(messages) == 0
@ddt.data(
['2017-06-30 09:00:00', None],
['2017-07-01 09:00:00', 'in 2 weeks on <span class="date localized-datetime" data-format="shortDate"'],
['2017-07-14 10:00:00', 'in 1 day at <span class="date localized-datetime" data-format="shortTime"'],
['2017-07-15 08:00:00', 'in 1 hour at <span class="date localized-datetime" data-format="shortTime"'],
['2017-07-15 08:55:00', 'in 5 minutes at <span class="date localized-datetime" data-format="shortTime"'],
['2017-07-15 09:00:00', None],
['2017-08-15 09:00:00', None],
)
@ddt.unpack
def test_end_date_alert(self, current_time, expected_message_html):
"""
Verify that course end date alerts are registered.
"""
with freeze_time(current_time):
block = CourseEndDate(self.course, self.request.user)
block.register_alerts(self.request, self.course)
messages = list(CourseHomeMessages.user_messages(self.request))
if expected_message_html:
assert len(messages) == 1
assert expected_message_html in messages[0].message_html
else:
assert len(messages) == 0
@ddt.data(
['2017-06-20 09:00:00', None],
['2017-06-21 09:00:00', 'Don&#39;t forget, you have 2 weeks left to upgrade to a Verified Certificate.'],
['2017-07-04 10:00:00', 'Don&#39;t forget, you have 1 day left to upgrade to a Verified Certificate.'],
['2017-07-05 08:00:00', 'Don&#39;t forget, you have 1 hour left to upgrade to a Verified Certificate.'],
['2017-07-05 08:55:00', 'Don&#39;t forget, you have 5 minutes left to upgrade to a Verified Certificate.'],
['2017-07-05 09:00:00', None],
['2017-08-05 09:00:00', None],
)
@ddt.unpack
@override_waffle_flag(UPGRADE_DEADLINE_MESSAGE, active=True)
def test_verified_upgrade_deadline_alert(self, current_time, expected_message_html):
"""
Verify the verified upgrade deadline alerts.
"""
with freeze_time(current_time):
block = VerifiedUpgradeDeadlineDate(self.course, self.request.user)
block.register_alerts(self.request, self.course)
messages = list(CourseHomeMessages.user_messages(self.request))
if expected_message_html:
assert len(messages) == 1
assert expected_message_html in messages[0].message_html
else:
assert len(messages) == 0
@ddt.data(
['2017-07-15 08:00:00', None],
['2017-07-15 09:00:00', 'If you have earned a certificate, you will be able to access it 1 week from now.'],
['2017-07-21 09:00:00', 'If you have earned a certificate, you will be able to access it 1 day from now.'],
['2017-07-22 08:00:00', 'If you have earned a certificate, you will be able to access it 1 hour from now.'],
['2017-07-22 09:00:00', None],
['2017-07-23 09:00:00', None],
)
@ddt.unpack
@waffle.testutils.override_switch('certificates.auto_certificate_generation', True)
def test_certificate_availability_alert(self, current_time, expected_message_html):
"""
Verify the verified upgrade deadline alerts.
"""
with freeze_time(current_time):
block = CertificateAvailableDate(self.course, self.request.user)
block.register_alerts(self.request, self.course)
messages = list(CourseHomeMessages.user_messages(self.request))
if expected_message_html:
assert len(messages) == 1
assert expected_message_html in messages[0].message_html
else:
assert len(messages) == 0
@ddt.ddt
class TestScheduleOverrides(SharedModuleStoreTestCase):

View File

@@ -126,7 +126,7 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
assert ('course-navigation' in response.content.decode('utf-8')) == accordion
self.assertTabInactive('progress', response)
self.assertTabActive('home', response)
self.assertTabActive('courseware', response)
response = self.client.get(reverse('courseware_section', kwargs={
'course_id': str(self.course.id),
@@ -135,7 +135,7 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
}))
self.assertTabActive('progress', response)
self.assertTabInactive('home', response)
self.assertTabInactive('courseware', response)
@override_settings(SESSION_INACTIVITY_TIMEOUT_IN_SECONDS=1)
def test_inactive_session_timeout(self):

View File

@@ -79,7 +79,6 @@ from lms.djangoapps.certificates.tests.factories import (
)
from lms.djangoapps.commerce.models import CommerceConfiguration
from lms.djangoapps.commerce.utils import EcommerceService
from lms.djangoapps.course_home_api.toggles import COURSE_HOME_USE_LEGACY_FRONTEND
from lms.djangoapps.courseware import access_utils
from lms.djangoapps.courseware.access_utils import check_course_open_for_learner
from lms.djangoapps.courseware.model_data import FieldDataCache, set_score
@@ -144,13 +143,6 @@ def _set_preview_mfe_flag(active: bool):
return override_waffle_flag(COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW, active=active)
def _set_course_home_mfe_flag(activate_mfe: bool):
"""
A decorator/contextmanager to force the courseware home MFE flag on or off.
"""
return override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=(not activate_mfe))
@ddt.ddt
class TestJumpTo(ModuleStoreTestCase):
"""
@@ -591,32 +583,17 @@ class ViewsTestCase(BaseViewsTestCase):
self.assertNotContains(response, 'Problem 1')
self.assertNotContains(response, 'Problem 2')
@ddt.data(False, True)
def test_mfe_link_from_about_page(self, activate_mfe):
def test_mfe_link_from_about_page(self):
"""
Verify course about page links to the MFE when enabled.
Verify course about page links to the MFE.
"""
with self.store.default_store(ModuleStoreEnum.Type.split):
course = CourseFactory.create()
CourseEnrollment.enroll(self.user, course.id)
assert self.client.login(username=self.user.username, password=TEST_PASSWORD)
legacy_url = reverse(
'openedx.course_experience.course_home',
kwargs={
'course_id': str(course.id),
}
)
mfe_url = get_learning_mfe_home_url(course_key=course.id, url_fragment='home')
with _set_course_home_mfe_flag(activate_mfe):
response = self.client.get(reverse('about_course', args=[str(course.id)]))
if activate_mfe:
self.assertContains(response, mfe_url)
self.assertNotContains(response, legacy_url)
else:
self.assertNotContains(response, mfe_url)
self.assertContains(response, legacy_url)
response = self.client.get(reverse('about_course', args=[str(course.id)]))
self.assertContains(response, get_learning_mfe_home_url(course_key=course.id, url_fragment='home'))
def _create_url_for_enroll_staff(self):
"""

View File

@@ -40,7 +40,7 @@ from openedx.core.djangolib.markup import HTML, Text
from openedx.features.course_experience import (
COURSE_ENABLE_UNENROLLED_ACCESS_FLAG,
DISABLE_COURSE_OUTLINE_PAGE_FLAG,
default_course_url_name
default_course_url
)
from openedx.features.course_experience.views.course_sock import CourseSockFragmentView
from openedx.features.course_experience.url_helpers import make_learning_mfe_courseware_url
@@ -417,8 +417,7 @@ class CoursewareIndex(View):
Also returns the table of contents for the courseware.
"""
course_url_name = default_course_url_name(self.course.id)
course_url = reverse(course_url_name, kwargs={'course_id': str(self.course.id)})
course_url = default_course_url(self.course.id)
show_search = (
settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH') or
(settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH_FOR_COURSE_STAFF') and self.is_staff)

View File

@@ -69,7 +69,7 @@ from lms.djangoapps.certificates import api as certs_api
from lms.djangoapps.certificates.data import CertificateStatuses
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_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 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.courses import (
@@ -124,7 +124,7 @@ from openedx.core.djangoapps.zendesk_proxy.utils import create_zendesk_ticket
from openedx.core.djangolib.markup import HTML, Text
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_name
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.url_helpers import (
ExperienceOption,
@@ -488,7 +488,7 @@ def course_info(request, 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(reverse(course_home_url_name(course_key), args=[course_id]))
return redirect(course_home_url(course_key))
with modulestore().bulk_operations(course_key):
course = get_course_with_access(request.user, 'load', course_key)
@@ -922,7 +922,7 @@ def course_about(request, course_id):
# If user needs to be redirected to course home then redirect
if _course_home_redirect_enabled():
return redirect(reverse(course_home_url_name(course_key), args=[str(course_key)]))
return redirect(course_home_url(course_key))
with modulestore().bulk_operations(course_key):
permission = get_permission_for_course_about()
@@ -935,10 +935,7 @@ def course_about(request, course_id):
studio_url = get_studio_url(course, 'settings/details')
if request.user.has_perm(VIEW_COURSE_HOME, course):
if course_home_legacy_is_active(course.id):
course_target = reverse(course_home_url_name(course.id), args=[str(course.id)])
else:
course_target = get_learning_mfe_home_url(course_key=course.id, url_fragment='home')
course_target = course_home_url(course.id)
else:
course_target = reverse('about_course', args=[str(course.id)])
@@ -1489,7 +1486,7 @@ def course_survey(request, course_id):
course_key = CourseKey.from_string(course_id)
course = get_course_with_access(request.user, 'load', course_key, check_survey_complete=False)
redirect_url = reverse(course_home_url_name(course.id), args=[course_id])
redirect_url = course_home_url(course_key)
# if there is no Survey associated with this course,
# then redirect to the course instead
@@ -1629,9 +1626,6 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True):
Returns an HttpResponse with HTML content for the xBlock with the given usage_key.
The returned HTML is a chromeless rendering of the xBlock (excluding content of the containing courseware).
"""
from lms.urls import RESET_COURSE_DEADLINES_NAME
from openedx.features.course_experience.urls import COURSE_HOME_VIEW_NAME
usage_key = UsageKey.from_string(usage_key_string)
usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key))
@@ -1738,12 +1732,11 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True):
'missed_deadlines': missed_deadlines,
'missed_gated_content': missed_gated_content,
'has_ended': course.has_ended(),
'web_app_course_url': reverse(COURSE_HOME_VIEW_NAME, args=[course.id]),
'web_app_course_url': get_learning_mfe_home_url(course_key=course.id, url_fragment='home'),
'on_courseware_page': True,
'verified_upgrade_link': verified_upgrade_deadline_link(request.user, course=course),
'is_learning_mfe': is_learning_mfe,
'is_mobile_app': is_mobile_app,
'reset_deadlines_url': reverse(RESET_COURSE_DEADLINES_NAME),
'render_course_wide_assets': True,
**optimization_flags,

View File

@@ -2098,7 +2098,7 @@ class ProgramCourseEnrollmentOverviewGetTests(
def test_course_run_url(self):
self.log_in()
course_run_url = f'http://testserver/courses/{str(self.course_id)}/course/'
course_run_url = f'http://learning-mfe/course/{str(self.course_id)}/home'
response_status_code, response_course_runs = self.get_status_and_course_runs()
assert status.HTTP_200_OK == response_status_code

View File

@@ -3,13 +3,12 @@ Serializers for use in the support app.
"""
import json
from django.urls import reverse
from rest_framework import serializers
from common.djangoapps.student.models import CourseEnrollment, ManualEnrollmentAudit
from lms.djangoapps.program_enrollments.models import ProgramCourseEnrollment, ProgramEnrollment
from openedx.core.djangoapps.catalog.utils import get_programs_by_uuids
from openedx.features.course_experience import default_course_url_name
from openedx.features.course_experience import default_course_url
DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S'
# pylint: disable=abstract-method
@@ -47,8 +46,7 @@ class ProgramCourseEnrollmentSerializer(serializers.Serializer):
model = ProgramCourseEnrollment
def get_course_url(self, obj):
course_url_name = default_course_url_name(obj.course_key)
return reverse(course_url_name, kwargs={'course_id': obj.course_key})
return default_course_url(obj.course_key)
class ProgramEnrollmentSerializer(serializers.Serializer):

View File

@@ -1036,8 +1036,6 @@ SOFTWARE_SECURE_REQUEST_RETRY_DELAY = 60 * 60
SOFTWARE_SECURE_RETRY_MAX_ATTEMPTS = 6
RETRY_CALENDAR_SYNC_EMAIL_MAX_ATTEMPTS = 5
# Deadline message configurations
COURSE_MESSAGE_ALERT_DURATION_IN_DAYS = 14
MARKETING_EMAILS_OPT_IN = False
@@ -3186,7 +3184,6 @@ INSTALLED_APPS = [
'openedx.features.calendar_sync',
'openedx.features.course_bookmarks',
'openedx.features.course_experience',
'openedx.features.course_search',
'openedx.features.enterprise_support.apps.EnterpriseSupportConfig',
'openedx.features.learner_profile',
'openedx.features.course_duration_limits',

View File

@@ -25,11 +25,6 @@ from openedx.features.course_experience import course_home_page_title, DISABLE_C
completion_aggregator_url = settings.COMPLETION_AGGREGATOR_URL if settings.FEATURES.get("SHOW_PROGRESS_BAR", False) else ""
%>
% if display_reset_dates_banner:
<script type="text/javascript">
$('.reset-deadlines-banner').css('display', 'flex');
</script>
% endif
<%def name="course_name()">
<% return _("{course_number} Courseware").format(course_number=course.display_number_with_default) %>
</%def>

View File

@@ -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
>
<article class="course${mode_class}" aria-labelledby="course-title-${enrollment.course_id}" id="course-card-${course_card_index}">
<% 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) %>
<section class="details" aria-labelledby="details-heading-${enrollment.course_id}">
<h2 class="hd hd-2 sr" id="details-heading-${enrollment.course_id}">${_('Course details')}</h2>
<div class="wrapper-course-image" aria-hidden="true">

View File

@@ -1,108 +0,0 @@
## mako
<%page expression_filter="h"/>
<%!
from django.utils.translation import ugettext as _
from lms.djangoapps.courseware.date_summary import CourseAssignmentDate
from common.djangoapps.course_modes.models import CourseMode
%>
<%
additional_styling_class = 'on-mobile' if is_mobile_app else 'has-button'
%>
<%def name="reset_dates_banner()">
<div class="banner-cta ${additional_styling_class}">
<div class="banner-cta-text">
% if is_mobile_app:
${_('It looks like you missed some important deadlines based on our suggested schedule. ')}
${_('To keep yourself on track, you can update this schedule and shift the past due assignments into the future by visiting ')}
<a class="mobile-dates-link" href="${web_app_course_url}">edx.org</a>.
${_(" Don't worry—you won't lose any of the progress you've made when you shift your due dates.")}
% else:
<strong>${_('It looks like you missed some important deadlines based on our suggested schedule.')}</strong>
${_("To keep yourself on track, you can update this schedule and shift the past due assignments into the future. Don't worry—you won't lose any of the progress you've made when you shift your due dates.")}
% endif
</div>
% if not is_mobile_app:
<div class="banner-cta-button">
<form method="post" action="${reset_deadlines_url}">
<input type="hidden" id="csrf_token" name="csrfmiddlewaretoken" value="${csrf_token}">
<input type="hidden" name="course_id" value="${course.id}">
<button class="btn btn-outline-primary">${_("Shift due dates")}</button>
</form>
</div>
% endif
</div>
</%def>
<%def name="upgrade_to_reset_banner()">
<div class="banner-cta ${additional_styling_class}">
<div class="banner-cta-text">
% if is_mobile_app:
<strong>${_('You are auditing this course,')}</strong>
${_(' which means that you are unable to participate in graded assignments.')}
${_(' It looks like you missed some important deadlines based on our suggested schedule. Graded assignments and schedule adjustment are available to Verified Track learners.')}
% else:
<strong>${_('You are auditing this course,')}</strong>
${_(' which means that you are unable to participate in graded assignments.')}
${_(' It looks like you missed some important deadlines based on our suggested schedule. To complete graded assignments as part of this course and shift the past due assignments into the future, you can upgrade today.')}
% endif
</div>
% if not is_mobile_app:
<div class="banner-cta-button">
<a class="personalized_learner_schedules_button" href="${verified_upgrade_link}">
<button type="button">
${_('Upgrade to shift due dates')}
</button>
</a>
</div>
% endif
</div>
</%def>
<%def name="upgrade_to_complete_graded_banner()">
<div class="banner-cta ${additional_styling_class}">
<div class="banner-cta-text">
% if is_mobile_app:
<strong>${_('You are auditing this course,')}</strong>
${_(' which means that you are unable to participate in graded assignments.')}
${_('Graded assignments are available to Verified Track learners.')}
% else:
<strong>${_('You are auditing this course,')}</strong>
${_(' which means that you are unable to participate in graded assignments.')}
${_(' To complete graded assignments as part of this course, you can upgrade today.')}
% endif
</div>
% if not is_mobile_app:
<div class="banner-cta-button">
<a class="personalized_learner_schedules_button" href="${verified_upgrade_link}">
<button type="button">
${_('Upgrade now')}
</button>
</a>
</div>
% endif
</div>
</%def>
% if not has_ended:
% if on_dates_tab and not missed_deadlines:
%if getattr(course, 'self_paced', False):
<div class="banner-cta">
<div class="banner-cta-text">
<strong>${_("We've built a suggested schedule to help you stay on track.")}</strong>
${_("But don't worry—it's flexible so you can learn at your own pace. If you happen to fall behind on our suggested dates, you'll be able to adjust them to keep yourself on track.")}
</div>
</div>
% endif
% if content_type_gating_enabled:
${upgrade_to_complete_graded_banner()}
% endif
% elif missed_deadlines:
% if missed_gated_content:
${upgrade_to_reset_banner()}
% else:
${reset_dates_banner()}
% endif
% endif
% endif

View File

@@ -667,14 +667,6 @@ urlpatterns += [
include('openedx.features.calendar_sync.urls'),
),
# Course search
re_path(
r'^courses/{}/search/'.format(
settings.COURSE_ID_PATTERN,
),
include('openedx.features.course_search.urls'),
),
# Learner profile
path(
'u/',
@@ -809,12 +801,6 @@ if configuration_helpers.get_value('ENABLE_BULK_ENROLLMENT_VIEW', settings.FEATU
path('api/bulk_enroll/v1/', include('lms.djangoapps.bulk_enroll.urls')),
]
# Course goals
urlpatterns += [
path('api/course_goals/', include(('lms.djangoapps.course_goals.urls', 'lms.djangoapps.course_goals'),
namespace='course_goals_api')),
]
# Embargo
if settings.FEATURES.get('EMBARGO'):
urlpatterns += [