Merge pull request #24819 from edx/ciduarte/AA-125
AA-125: Add Course Goals to MFE API
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = '<div>Course expired HTML</div>'
|
||||
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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -57,9 +57,9 @@ def reset_course_deadlines(request):
|
||||
|
||||
return Response({
|
||||
'body': format_html('<a href="{}">{}</a>', body_link, _('View all dates')),
|
||||
'header': format_html(
|
||||
'<div>{}</div>', _('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:
|
||||
|
||||
Reference in New Issue
Block a user