Skip to content
Snippets Groups Projects
Unverified Commit 4e6bff88 authored by Carla Duarte's avatar Carla Duarte Committed by GitHub
Browse files

Merge pull request #24819 from edx/ciduarte/AA-125

AA-125: Add Course Goals to MFE API
parents 3d70cfcb 6392722e
No related branches found
No related tags found
No related merge requests found
......@@ -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:
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment