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:
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
@@ -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')),
|
||||
]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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't forget, you have 2 weeks left to upgrade to a Verified Certificate.'],
|
||||
['2017-07-04 10:00:00', 'Don't forget, you have 1 day left to upgrade to a Verified Certificate.'],
|
||||
['2017-07-05 08:00:00', 'Don't forget, you have 1 hour left to upgrade to a Verified Certificate.'],
|
||||
['2017-07-05 08:55:00', 'Don'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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
14
lms/urls.py
14
lms/urls.py
@@ -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 += [
|
||||
|
||||
Reference in New Issue
Block a user