diff --git a/lms/djangoapps/course_goals/api.py b/lms/djangoapps/course_goals/api.py
index 6c8fe3c9f8..608e9cf8eb 100644
--- a/lms/djangoapps/course_goals/api.py
+++ b/lms/djangoapps/course_goals/api.py
@@ -88,12 +88,20 @@ def get_course_goal_options():
return {goal_key: goal_text for goal_key, goal_text in models.GOAL_KEY_CHOICES}
-def valid_course_goals_ordered():
+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 since it does not have a relevant commitment level.
+ 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()
@@ -102,4 +110,7 @@ def valid_course_goals_ordered():
ordered_goal_options.append((models.GOAL_KEY_CHOICES.complete, goal_options[models.GOAL_KEY_CHOICES.complete]))
ordered_goal_options.append((models.GOAL_KEY_CHOICES.explore, goal_options[models.GOAL_KEY_CHOICES.explore]))
+ if include_unsure:
+ ordered_goal_options.append((models.GOAL_KEY_CHOICES.unsure, goal_options[models.GOAL_KEY_CHOICES.unsure]))
+
return ordered_goal_options
diff --git a/lms/djangoapps/course_goals/views.py b/lms/djangoapps/course_goals/views.py
index 70c4c70bef..c98ff66aad 100644
--- a/lms/djangoapps/course_goals/views.py
+++ b/lms/djangoapps/course_goals/views.py
@@ -3,7 +3,6 @@ Course Goals Views - includes REST API
"""
-from django.conf import settings
from django.contrib.auth import get_user_model
from django.db.models.signals import post_save
from django.dispatch import receiver
@@ -58,6 +57,7 @@ class CourseGoalViewSet(viewsets.ModelViewSet):
queryset = CourseGoal.objects.all()
serializer_class = CourseGoalSerializer
+ # Another version of this endpoint exists in ../course_home_api/outline/v1/views.py
def create(self, post_data):
""" Create a new goal if one does not exist, otherwise update the existing goal. """
# Ensure goal_key is valid
diff --git a/lms/djangoapps/course_home_api/outline/v1/serializers.py b/lms/djangoapps/course_home_api/outline/v1/serializers.py
index 78c3c04ceb..a730a3211a 100644
--- a/lms/djangoapps/course_home_api/outline/v1/serializers.py
+++ b/lms/djangoapps/course_home_api/outline/v1/serializers.py
@@ -7,21 +7,6 @@ from lms.djangoapps.course_home_api.dates.v1.serializers import DateSummarySeria
from rest_framework.reverse import reverse
-class CourseToolSerializer(serializers.Serializer):
- """
- Serializer for Course Tool Objects
- """
- analytics_id = serializers.CharField()
- title = serializers.CharField()
- url = serializers.SerializerMethodField()
-
- def get_url(self, tool):
- course_key = self.context.get('course_key')
- url = tool.url(course_key)
- request = self.context.get('request')
- return request.build_absolute_uri(url)
-
-
class CourseBlockSerializer(serializers.Serializer):
"""
Serializer for Course Block Objects
@@ -45,6 +30,29 @@ class CourseBlockSerializer(serializers.Serializer):
}
+class CourseGoalSerializer(serializers.Serializer):
+ """
+ Serializer for Course Goal data
+ """
+ goal_options = serializers.ListField()
+ selected_goal = serializers.DictField()
+
+
+class CourseToolSerializer(serializers.Serializer):
+ """
+ Serializer for Course Tool Objects
+ """
+ analytics_id = serializers.CharField()
+ title = serializers.CharField()
+ url = serializers.SerializerMethodField()
+
+ def get_url(self, tool):
+ course_key = self.context.get('course_key')
+ url = tool.url(course_key)
+ request = self.context.get('request')
+ return request.build_absolute_uri(url)
+
+
class DatesWidgetSerializer(serializers.Serializer):
"""
Serializer for Dates Widget data
@@ -63,6 +71,9 @@ class EnrollAlertSerializer(serializers.Serializer):
class ResumeCourseSerializer(serializers.Serializer):
+ """
+ Serializer for resume course data
+ """
has_visited_course = serializers.BooleanField()
url = serializers.URLField()
@@ -73,6 +84,7 @@ class OutlineTabSerializer(serializers.Serializer):
"""
course_blocks = CourseBlockSerializer()
course_expired_html = serializers.CharField()
+ course_goals = CourseGoalSerializer()
course_tools = CourseToolSerializer(many=True)
dates_widget = DatesWidgetSerializer()
enroll_alert = EnrollAlertSerializer()
diff --git a/lms/djangoapps/course_home_api/outline/v1/tests/test_views.py b/lms/djangoapps/course_home_api/outline/v1/tests/test_views.py
index c41bcdb3d7..fff38d645a 100644
--- a/lms/djangoapps/course_home_api/outline/v1/tests/test_views.py
+++ b/lms/djangoapps/course_home_api/outline/v1/tests/test_views.py
@@ -11,7 +11,7 @@ from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests
from lms.djangoapps.course_home_api.toggles import COURSE_HOME_MICROFRONTEND, COURSE_HOME_MICROFRONTEND_OUTLINE_TAB
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
from openedx.core.djangoapps.user_api.tests.factories import UserCourseTagFactory
-from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG
+from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, ENABLE_COURSE_GOALS
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
from xmodule.course_module import COURSE_VISIBILITY_PUBLIC
@@ -27,6 +27,7 @@ class OutlineTabTestViews(BaseCourseHomeTests):
super().setUp()
self.url = reverse('course-home-outline-tab', args=[self.course.id])
+ @ENABLE_COURSE_GOALS.override(active=True)
@COURSE_HOME_MICROFRONTEND.override(active=True)
@COURSE_HOME_MICROFRONTEND_OUTLINE_TAB.override(active=True)
@ddt.data(CourseMode.AUDIT, CourseMode.VERIFIED)
@@ -35,6 +36,16 @@ class OutlineTabTestViews(BaseCourseHomeTests):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
+ course_goals = response.data.get('course_goals')
+ goal_options = course_goals['goal_options']
+ if enrollment_mode == CourseMode.VERIFIED:
+ self.assertEqual(goal_options, [])
+ else:
+ self.assertGreater(len(goal_options), 0)
+
+ selected_goal = course_goals['selected_goal']
+ self.assertIsNone(selected_goal)
+
course_tools = response.data.get('course_tools')
self.assertTrue(course_tools)
self.assertEqual(course_tools[0]['analytics_id'], 'edx.bookmarks')
@@ -56,6 +67,9 @@ class OutlineTabTestViews(BaseCourseHomeTests):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
+ course_goals = response.data.get('course_goals')
+ self.assertEqual(course_goals['goal_options'], [])
+
course_tools = response.data.get('course_tools')
self.assertEqual(len(course_tools), 0)
@@ -170,3 +184,24 @@ class OutlineTabTestViews(BaseCourseHomeTests):
html = '
Course expired HTML
'
gen_html.return_value = html
self.assertEqual(self.client.get(self.url).data['course_expired_html'], html)
+
+ @ENABLE_COURSE_GOALS.override(active=True)
+ @COURSE_HOME_MICROFRONTEND.override(active=True)
+ @COURSE_HOME_MICROFRONTEND_OUTLINE_TAB.override(active=True)
+ def test_post_course_goal(self):
+ CourseEnrollment.enroll(self.user, self.course.id, CourseMode.AUDIT)
+
+ post_data = {
+ 'course_id': self.course.id,
+ 'goal_key': 'certify'
+ }
+ post_course_goal_response = self.client.post(reverse('course-home-save-course-goal'), post_data)
+ self.assertEqual(post_course_goal_response.status_code, 200)
+
+ response = self.client.get(self.url)
+ self.assertEqual(response.status_code, 200)
+
+ course_goals = response.data.get('course_goals')
+ selected_goal = course_goals['selected_goal']
+ self.assertIsNotNone(selected_goal)
+ self.assertEqual(selected_goal['key'], 'certify')
diff --git a/lms/djangoapps/course_home_api/outline/v1/views.py b/lms/djangoapps/course_home_api/outline/v1/views.py
index 64e56750b8..75c016264c 100644
--- a/lms/djangoapps/course_home_api/outline/v1/views.py
+++ b/lms/djangoapps/course_home_api/outline/v1/views.py
@@ -7,6 +7,7 @@ from django.urls import reverse
from django.utils.translation import gettext as _
from edx_django_utils import monitoring as monitoring_utils
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
+from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
from opaque_keys.edx.keys import CourseKey
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from rest_framework.exceptions import APIException, ParseError
@@ -19,8 +20,11 @@ from completion.utilities import get_key_to_last_completed_block
from course_modes.models import CourseMode
from lms.djangoapps.course_api.blocks.transformers.blocks_api import BlocksAPITransformer
from lms.djangoapps.course_blocks.api import get_course_block_access_transformers, get_course_blocks
+from lms.djangoapps.course_goals.api import (add_course_goal, get_course_goal, get_course_goal_text,
+ has_course_goal_permission, valid_course_goals_ordered)
from lms.djangoapps.course_home_api.outline.v1.serializers import OutlineTabSerializer
-from lms.djangoapps.course_home_api.toggles import course_home_mfe_dates_tab_is_active, course_home_mfe_outline_tab_is_active
+from lms.djangoapps.course_home_api.toggles import (course_home_mfe_dates_tab_is_active,
+ course_home_mfe_outline_tab_is_active)
from lms.djangoapps.course_home_api.utils import get_microfrontend_url
from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs
@@ -47,6 +51,12 @@ class UnableToDismissWelcomeMessage(APIException):
default_code = 'unable_to_dismiss_welcome_message'
+class UnableToSaveCourseGoal(APIException):
+ status_code = 400
+ default_detail = 'Unable to save course goal'
+ default_code = 'unable_to_save_course_goal'
+
+
class OutlineTabView(RetrieveAPIView):
"""
**Use Cases**
@@ -73,6 +83,11 @@ class OutlineTabView(RetrieveAPIView):
xBlock on the web LMS.
children: (list) If the block has child blocks, a list of IDs of
the child blocks.
+ course_goals:
+ goal_options: (list) A list of goals where each goal is represented as a tuple (goal_key, goal_string)
+ selected_goal:
+ key: (str) The unique id given to the user's selected goal.
+ text: (str) The display text for the user's selected goal.
course_tools: List of serialized Course Tool objects. Each serialization has the following fields:
analytics_id: (str) The unique id given to the tool.
title: (str) The display title of the tool.
@@ -209,9 +224,32 @@ class OutlineTabView(RetrieveAPIView):
'user_timezone': user_timezone,
}
+ # Only show the set course goal message for enrolled, unverified
+ # users in a course that allows for verified statuses.
+ is_already_verified = CourseEnrollment.is_enrolled_as_verified(request.user, course_key)
+ if (not is_already_verified and
+ has_course_goal_permission(request, course_key_string, {'is_enrolled': is_enrolled})):
+ course_goals = {
+ 'goal_options': valid_course_goals_ordered(include_unsure=True),
+ 'selected_goal': None
+ }
+
+ selected_goal = get_course_goal(request.user, course_key)
+ if selected_goal:
+ course_goals['selected_goal'] = {
+ 'key': selected_goal.goal_key,
+ 'text': get_course_goal_text(selected_goal.goal_key),
+ }
+ else:
+ course_goals = {
+ 'goal_options': [],
+ 'selected_goal': None
+ }
+
data = {
'course_blocks': course_blocks,
'course_expired_html': course_expired_html,
+ 'course_goals': course_goals,
'course_tools': course_tools,
'dates_widget': dates_widget,
'enroll_alert': enroll_alert,
@@ -233,7 +271,7 @@ class OutlineTabView(RetrieveAPIView):
def dismiss_welcome_message(request):
course_id = request.data.get('course_id', None)
- # If body doesnt contain 'course_id', return 400 to client.
+ # If body doesn't contain 'course_id', return 400 to client.
if not course_id:
raise ParseError(_("'course_id' is required."))
@@ -247,3 +285,29 @@ def dismiss_welcome_message(request):
return Response({'message': _('Welcome message successfully dismissed.')})
except Exception:
raise UnableToDismissWelcomeMessage
+
+
+# Another version of this endpoint exists in ../course_goals/views.py
+@api_view(['POST'])
+@authentication_classes((JwtAuthentication, SessionAuthenticationAllowInactiveUser,))
+@permission_classes((IsAuthenticated,))
+def save_course_goal(request):
+ course_id = request.data.get('course_id', None)
+ goal_key = request.data.get('goal_key', None)
+
+ # If body doesn't contain 'course_id', return 400 to client.
+ if not course_id:
+ raise ParseError(_("'course_id' is required."))
+
+ # If body doesn't contain 'goal', return 400 to client.
+ if not goal_key:
+ raise ParseError(_("'goal_key' is required."))
+
+ try:
+ add_course_goal(request.user, course_id, goal_key)
+ return Response({
+ 'header': _('Your course goal has been successfully set.'),
+ 'message': _('Course goal updated successfully.'),
+ })
+ except Exception:
+ raise UnableToSaveCourseGoal
diff --git a/lms/djangoapps/course_home_api/urls.py b/lms/djangoapps/course_home_api/urls.py
index 371e17e8ec..83bf406a64 100644
--- a/lms/djangoapps/course_home_api/urls.py
+++ b/lms/djangoapps/course_home_api/urls.py
@@ -8,7 +8,7 @@ from django.urls import re_path
from lms.djangoapps.course_home_api.dates.v1.views import DatesTabView
from lms.djangoapps.course_home_api.course_metadata.v1.views import CourseHomeMetadataView
-from lms.djangoapps.course_home_api.outline.v1.views import OutlineTabView, dismiss_welcome_message
+from lms.djangoapps.course_home_api.outline.v1.views import OutlineTabView, dismiss_welcome_message, save_course_goal
from lms.djangoapps.course_home_api.progress.v1.views import ProgressTabView
urlpatterns = []
@@ -48,6 +48,14 @@ urlpatterns += [
),
]
+urlpatterns += [
+ re_path(
+ r'v1/save_course_goal',
+ save_course_goal,
+ name='course-home-save-course-goal'
+ ),
+]
+
# Progress Tab URLs
urlpatterns += [
re_path(
diff --git a/openedx/features/course_experience/api/v1/views.py b/openedx/features/course_experience/api/v1/views.py
index 62249dedca..676f2e2d5a 100644
--- a/openedx/features/course_experience/api/v1/views.py
+++ b/openedx/features/course_experience/api/v1/views.py
@@ -57,9 +57,9 @@ def reset_course_deadlines(request):
return Response({
'body': format_html('{}', body_link, _('View all dates')),
- 'header': format_html(
- '{}
', _('Your due dates have been successfully shifted to help you stay on track.')
- ),
+ 'header': _('Your due dates have been successfully shifted to help you stay on track.'),
+ 'link': body_link,
+ 'link_text': _('View all dates'),
'message': _('Deadlines successfully reset.'),
})
except Exception: