From 6f8ecf336813003d47548159f7b6175c91860f0e Mon Sep 17 00:00:00 2001 From: Daphne Li-Chen Date: Thu, 2 Jul 2020 09:53:51 -0400 Subject: [PATCH 1/3] AA-204: passing correct section information to frontend to complete outline portion of tab --- .../progress/v1/serializers.py | 51 ++++++++++++++++++- .../course_home_api/progress/v1/views.py | 18 +++++-- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/course_home_api/progress/v1/serializers.py b/lms/djangoapps/course_home_api/progress/v1/serializers.py index 2565bd5ccf..303939179b 100644 --- a/lms/djangoapps/course_home_api/progress/v1/serializers.py +++ b/lms/djangoapps/course_home_api/progress/v1/serializers.py @@ -1,9 +1,56 @@ """ Progress Tab Serializers """ - from rest_framework import serializers from lms.djangoapps.course_home_api.outline.v1.serializers import CourseBlockSerializer +from rest_framework.reverse import reverse + + +class GradedTotalSerializer(serializers.Serializer): + earned = serializers.FloatField() + first_attempted = serializers.CharField() + graded = serializers.BooleanField() + possible = serializers.FloatField() + + +class SubsectionSerializer(serializers.Serializer): + display_name = serializers.CharField() + due = serializers.DateTimeField() + format = serializers.CharField() + graded = serializers.BooleanField() + graded_total = GradedTotalSerializer() + # TODO: override serializer + percent_graded = serializers.FloatField() + problem_scores = serializers.SerializerMethodField() + show_correctness = serializers.CharField() + show_grades = serializers.SerializerMethodField() + url = serializers.SerializerMethodField() + + def get_url(self, subsection): + relative_path = reverse('jump_to', args=[self.context['course_key'], subsection.location]) + request = self.context['request'] + return request.build_absolute_uri(relative_path) + + def get_problem_scores(self, subsection): + problem_scores = [ + { + 'earned': score.earned, + 'possible': score.possible, + } + for score in subsection.problem_scores.values() + ] + return problem_scores + + def get_show_grades(self, subsection): + return subsection.show_grades(self.context['staff_access']) + + +class ChapterSerializer(serializers.Serializer): + """ + Serializer for chapters in coursewaresummary + """ + display_name = serializers.CharField() + subsections = SubsectionSerializer(source='sections', many=True) class ProgressTabSerializer(serializers.Serializer): @@ -11,4 +58,6 @@ class ProgressTabSerializer(serializers.Serializer): Serializer for progress tab """ course_blocks = CourseBlockSerializer() + courseware_summary = ChapterSerializer(many=True) enrollment_mode = serializers.CharField() + user_timezone = serializers.CharField() diff --git a/lms/djangoapps/course_home_api/progress/v1/views.py b/lms/djangoapps/course_home_api/progress/v1/views.py index 5cae6e5e4e..64d3bb9640 100644 --- a/lms/djangoapps/course_home_api/progress/v1/views.py +++ b/lms/djangoapps/course_home_api/progress/v1/views.py @@ -13,6 +13,7 @@ from lms.djangoapps.course_home_api.progress.v1.serializers import ProgressTabSe from student.models import CourseEnrollment, UserTestGroup from lms.djangoapps.course_api.blocks.transformers.blocks_api import BlocksAPITransformer +from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs from lms.djangoapps.courseware.courses import get_course_with_access from lms.djangoapps.courseware.masquerade import setup_masquerade from lms.djangoapps.courseware.access import has_access @@ -20,6 +21,7 @@ from xmodule.modulestore.django import modulestore from lms.djangoapps.course_blocks.api import get_course_blocks import lms.djangoapps.course_blocks.api as course_blocks_api +from lms.djangoapps.grades.api import CourseGradeFactory from openedx.core.djangoapps.content.block_structure.transformers import BlockStructureTransformers @@ -82,21 +84,31 @@ class ProgressTabView(RetrieveAPIView): reset_masquerade_data=True ) + user_timezone_locale = user_timezone_locale_prefs(request) + user_timezone = user_timezone_locale['user_timezone'] + transformers = BlockStructureTransformers() transformers += course_blocks_api.get_course_block_access_transformers(request.user) transformers += [ BlocksAPITransformer(None, None, depth=3), ] - get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) + course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) course_blocks = get_course_blocks(request.user, course_usage_key, transformers, include_completion=True) enrollment_mode, _ = CourseEnrollment.enrollment_mode_for_user(request.user, course_key) + course_grade = CourseGradeFactory().read(request.user, course) + courseware_summary = course_grade.chapter_grades.values() + data = { 'course_blocks': course_blocks, 'enrollment_mode': enrollment_mode, + 'courseware_summary': courseware_summary, + 'user_timezone': user_timezone, } - - serializer = self.get_serializer(data) + context = self.get_serializer_context() + context['staff_access'] = bool(has_access(request.user, 'staff', course)) + context['course_key'] = course_key + serializer = self.get_serializer_class()(data, context=context) return Response(serializer.data) From af1a7ca2d8796c85b9b7bcafd1eadd1e0bbdcb07 Mon Sep 17 00:00:00 2001 From: Daphne Li-Chen Date: Thu, 23 Jul 2020 16:37:29 -0400 Subject: [PATCH 2/3] AA-204: adding tests --- .../progress/v1/tests/__init__.py | 0 .../progress/v1/tests/test_views.py | 68 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 lms/djangoapps/course_home_api/progress/v1/tests/__init__.py create mode 100644 lms/djangoapps/course_home_api/progress/v1/tests/test_views.py diff --git a/lms/djangoapps/course_home_api/progress/v1/tests/__init__.py b/lms/djangoapps/course_home_api/progress/v1/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/course_home_api/progress/v1/tests/test_views.py b/lms/djangoapps/course_home_api/progress/v1/tests/test_views.py new file mode 100644 index 0000000000..a2d051fccd --- /dev/null +++ b/lms/djangoapps/course_home_api/progress/v1/tests/test_views.py @@ -0,0 +1,68 @@ +""" +Tests for Progress Tab API in the Course Home API +""" + +from datetime import datetime +import ddt + +from django.urls import reverse + +from course_modes.models import CourseMode +from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests +from lms.djangoapps.course_home_api.toggles import COURSE_HOME_MICROFRONTEND +from openedx.features.content_type_gating.models import ContentTypeGatingConfig +from openedx.core.djangoapps.user_api.preferences.api import set_user_preference +from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag +from student.models import CourseEnrollment +from student.tests.factories import UserFactory + +@override_waffle_flag(COURSE_HOME_MICROFRONTEND, active=True) +@ddt.ddt +class ProgressTabTestViews(BaseCourseHomeTests): + """ + Tests for the Progress Tab API + """ + def setUp(self): + super().setUp() + self.url = reverse('course-home-progress-tab', args=[self.course.id]) + ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2017, 1, 1)) + + @ddt.data(CourseMode.AUDIT, CourseMode.VERIFIED) + def test_get_authenticated_enrolled_user(self, enrollment_mode): + CourseEnrollment.enroll(self.user, self.course.id, enrollment_mode) + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + + # Pulling out the date blocks to check learner has access. + self.assertNotEqual(response.data['courseware_summary'], None) + for chapter in response.data['courseware_summary']: + self.assertNotEqual(chapter, None) + + def test_get_authenticated_user_not_enrolled(self): + response = self.client.get(self.url) + # expecting a redirect + self.assertEqual(response.status_code, 302) + + def test_get_unauthenticated_user(self): + self.client.logout() + response = self.client.get(self.url) + self.assertEqual(response.status_code, 403) + + def test_get_unknown_course(self): + url = reverse('course-home-progress-tab', args=['course-v1:unknown+course+2T2020']) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + def test_masquerade(self): + user = UserFactory() + set_user_preference(user, 'time_zone', 'Asia/Tokyo') + CourseEnrollment.enroll(user, self.course.id) + + self.switch_to_staff() # needed for masquerade + + # Sanity check on our normal user + self.assertEqual(self.client.get(self.url).data['user_timezone'], None) + + # Now switch users and confirm we get a different result + self.update_masquerade(username=user.username) + self.assertEqual(self.client.get(self.url).data['user_timezone'], 'Asia/Tokyo') From 1d2dee8e25aaaf675593c548a61764dd08dbf228 Mon Sep 17 00:00:00 2001 From: Daphne Li-Chen Date: Mon, 27 Jul 2020 14:28:02 -0400 Subject: [PATCH 3/3] AA-204: fixed up documentation and tests --- .../progress/v1/serializers.py | 2 -- .../progress/v1/tests/test_views.py | 14 +++++----- .../course_home_api/progress/v1/views.py | 26 ++++++++++++++----- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/lms/djangoapps/course_home_api/progress/v1/serializers.py b/lms/djangoapps/course_home_api/progress/v1/serializers.py index 303939179b..0260d8c4b8 100644 --- a/lms/djangoapps/course_home_api/progress/v1/serializers.py +++ b/lms/djangoapps/course_home_api/progress/v1/serializers.py @@ -8,8 +8,6 @@ from rest_framework.reverse import reverse class GradedTotalSerializer(serializers.Serializer): earned = serializers.FloatField() - first_attempted = serializers.CharField() - graded = serializers.BooleanField() possible = serializers.FloatField() diff --git a/lms/djangoapps/course_home_api/progress/v1/tests/test_views.py b/lms/djangoapps/course_home_api/progress/v1/tests/test_views.py index a2d051fccd..fa8bc87048 100644 --- a/lms/djangoapps/course_home_api/progress/v1/tests/test_views.py +++ b/lms/djangoapps/course_home_api/progress/v1/tests/test_views.py @@ -2,7 +2,6 @@ Tests for Progress Tab API in the Course Home API """ -from datetime import datetime import ddt from django.urls import reverse @@ -10,12 +9,12 @@ from django.urls import reverse from course_modes.models import CourseMode from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests from lms.djangoapps.course_home_api.toggles import COURSE_HOME_MICROFRONTEND -from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.core.djangoapps.user_api.preferences.api import set_user_preference from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag from student.models import CourseEnrollment from student.tests.factories import UserFactory + @override_waffle_flag(COURSE_HOME_MICROFRONTEND, active=True) @ddt.ddt class ProgressTabTestViews(BaseCourseHomeTests): @@ -24,8 +23,7 @@ class ProgressTabTestViews(BaseCourseHomeTests): """ def setUp(self): super().setUp() - self.url = reverse('course-home-progress-tab', args=[self.course.id]) - ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2017, 1, 1)) + self.url = reverse('course-home-progress-tab', args=[self.course.id]) @ddt.data(CourseMode.AUDIT, CourseMode.VERIFIED) def test_get_authenticated_enrolled_user(self, enrollment_mode): @@ -33,10 +31,10 @@ class ProgressTabTestViews(BaseCourseHomeTests): response = self.client.get(self.url) self.assertEqual(response.status_code, 200) - # Pulling out the date blocks to check learner has access. - self.assertNotEqual(response.data['courseware_summary'], None) + # Pulling out the courseware summary to check that the learner is able to see this info + self.assertIsNotNone(response.data['courseware_summary']) for chapter in response.data['courseware_summary']: - self.assertNotEqual(chapter, None) + self.assertIsNotNone(chapter) def test_get_authenticated_user_not_enrolled(self): response = self.client.get(self.url) @@ -61,7 +59,7 @@ class ProgressTabTestViews(BaseCourseHomeTests): self.switch_to_staff() # needed for masquerade # Sanity check on our normal user - self.assertEqual(self.client.get(self.url).data['user_timezone'], None) + self.assertIsNone(self.client.get(self.url).data['user_timezone']) # Now switch users and confirm we get a different result self.update_masquerade(username=user.username) diff --git a/lms/djangoapps/course_home_api/progress/v1/views.py b/lms/djangoapps/course_home_api/progress/v1/views.py index 64d3bb9640..4d90533bc9 100644 --- a/lms/djangoapps/course_home_api/progress/v1/views.py +++ b/lms/djangoapps/course_home_api/progress/v1/views.py @@ -11,7 +11,7 @@ from opaque_keys.edx.keys import CourseKey from lms.djangoapps.course_home_api.progress.v1.serializers import ProgressTabSerializer -from student.models import CourseEnrollment, UserTestGroup +from student.models import CourseEnrollment from lms.djangoapps.course_api.blocks.transformers.blocks_api import BlocksAPITransformer from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs from lms.djangoapps.courseware.courses import get_course_with_access @@ -39,10 +39,6 @@ class ProgressTabView(RetrieveAPIView): Body consists of the following fields: - user: Serialized User object. The serialization has the following fields: - username: (str) The username of the user - email: (str) the email of the user - is_staff: (bool) boolean indicating whether the user has staff permisions or not course_blocks: blocks: List of serialized Course Block objects. Each serialization has the following fields: id: (str) The usage ID of the block. @@ -55,11 +51,29 @@ class ProgressTabView(RetrieveAPIView): xBlock on the web LMS. children: (list) If the block has child blocks, a list of IDs of the child blocks. + courseware_summary: List of serialized Chapters. each Chapter has the following fields: + display_name: (str) a str of what the name of the Chapter is for displaying on the site + subsections: List of serialized Subsections, each has the following fields: + display_name: (str) a str of what the name of the Subsection is for displaying on the site + due: (str) a DateTime string for when the Subsection is due + format: (str) the format, if any, of the Subsection (Homework, Exam, etc) + graded: (bool) whether or not the Subsection is graded + graded_total: an object containing the following fields + earned: (float) the amount of points the user earned + possible: (float) the amount of points the user could have earned + percent_graded: (float) the percentage of the points the user received for the subsection + show_correctness: (str) a str representing whether to show the problem/practice scores based on due date + show_grades: (bool) a bool for whether to show grades based on the access the user has + url: (str) the absolute path url to the Subsection enrollment_mode: (str) a str representing the enrollment the user has ('audit', 'verified', ...) + user_timezone: (str) The user's preferred timezone + + **Returns** * 200 on success with above fields. + * 302 if the user is not enrolled. * 403 if the user is not authenticated. * 404 if the course is not available or cannot be seen. """ @@ -102,8 +116,8 @@ class ProgressTabView(RetrieveAPIView): data = { 'course_blocks': course_blocks, - 'enrollment_mode': enrollment_mode, 'courseware_summary': courseware_summary, + 'enrollment_mode': enrollment_mode, 'user_timezone': user_timezone, } context = self.get_serializer_context()