Merge pull request #26928 from edx/ciduarte/hack

AA-671: Course Home MFE Progress Tab API cleanup
This commit is contained in:
Carla Duarte
2021-03-11 16:09:48 -05:00
committed by GitHub
5 changed files with 123 additions and 129 deletions

View File

@@ -5,20 +5,18 @@ from rest_framework import serializers
from rest_framework.reverse import reverse
class GradedTotalSerializer(serializers.Serializer):
earned = serializers.FloatField()
possible = serializers.FloatField()
class CourseGradeSerializer(serializers.Serializer):
percent = serializers.FloatField()
is_passing = serializers.BooleanField(source='passed')
class SubsectionSerializer(serializers.Serializer):
class SubsectionScoresSerializer(serializers.Serializer):
assignment_type = serializers.CharField(source='format')
display_name = serializers.CharField()
due = serializers.DateTimeField()
format = serializers.CharField()
graded = serializers.BooleanField()
graded_total = GradedTotalSerializer()
# TODO: override serializer
has_graded_assignment = serializers.BooleanField(source='graded')
num_points_earned = serializers.IntegerField(source='graded_total.earned')
num_points_possible = serializers.IntegerField(source='graded_total.possible')
percent_graded = serializers.FloatField()
problem_scores = serializers.SerializerMethodField()
show_correctness = serializers.CharField()
show_grades = serializers.SerializerMethodField()
url = serializers.SerializerMethodField()
@@ -28,64 +26,33 @@ class SubsectionSerializer(serializers.Serializer):
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):
class SectionScoresSerializer(serializers.Serializer):
"""
Serializer for chapters in coursewaresummary
Serializer for chapters in courseware_summary
"""
display_name = serializers.CharField()
subsections = SubsectionSerializer(source='sections', many=True)
subsections = SubsectionScoresSerializer(source='sections', many=True)
class GradingPolicySerializer(serializers.Serializer):
assignment_policies = serializers.SerializerMethodField()
grade_range = serializers.DictField(source='GRADE_CUTOFFS')
def get_assignment_policies(self, grading_policy):
return [{
'type': assignment_policy['type'],
'weight': assignment_policy['weight'],
} for assignment_policy in grading_policy['GRADER']]
class CertificateDataSerializer(serializers.Serializer):
cert_status = serializers.CharField()
cert_web_view_url = serializers.CharField()
download_url = serializers.CharField()
msg = serializers.CharField()
title = serializers.CharField()
class CreditRequirementSerializer(serializers.Serializer):
"""
Serializer for credit requirement objects
"""
display_name = serializers.CharField()
min_grade = serializers.SerializerMethodField()
status = serializers.CharField()
status_date = serializers.DateTimeField()
def get_min_grade(self, requirement):
if requirement['namespace'] == 'grade':
return requirement['criteria']['min_grade'] * 100
else:
return None
class CreditCourseRequirementsSerializer(serializers.Serializer):
"""
Serializer for credit_course_requirements
"""
dashboard_url = serializers.SerializerMethodField()
eligibility_status = serializers.CharField()
requirements = CreditRequirementSerializer(many=True)
def get_dashboard_url(self, _):
relative_path = reverse('dashboard')
request = self.context['request']
return request.build_absolute_uri(relative_path)
class VerificationDataSerializer(serializers.Serializer):
@@ -102,10 +69,10 @@ class ProgressTabSerializer(serializers.Serializer):
Serializer for progress tab
"""
certificate_data = CertificateDataSerializer()
credit_course_requirements = CreditCourseRequirementsSerializer()
credit_support_url = serializers.URLField()
courseware_summary = ChapterSerializer(many=True)
completion_summary = serializers.DictField()
course_grade = CourseGradeSerializer()
section_scores = SectionScoresSerializer(many=True)
enrollment_mode = serializers.CharField()
grading_policy = GradingPolicySerializer()
studio_url = serializers.CharField()
user_timezone = serializers.CharField()
verification_data = VerificationDataSerializer()

View File

@@ -33,12 +33,10 @@ class ProgressTabTestViews(BaseCourseHomeTests):
response = self.client.get(self.url)
assert response.status_code == 200
# Pulling out the courseware summary to check that the learner is able to see this info
assert response.data['courseware_summary'] is not None
for chapter in response.data['courseware_summary']:
assert response.data['section_scores'] is not None
for chapter in response.data['section_scores']:
assert chapter is not None
assert ('settings/grading/' + str(self.course.id)) in response.data['studio_url']
assert response.data['credit_support_url'] == CREDIT_SUPPORT_URL
assert response.data['verification_data'] is not None
assert response.data['verification_data']['status'] == 'none'
if enrollment_mode == CourseMode.VERIFIED:
@@ -57,23 +55,24 @@ class ProgressTabTestViews(BaseCourseHomeTests):
def test_get_unauthenticated_user(self):
self.client.logout()
response = self.client.get(self.url)
assert response.status_code == 403
assert response.status_code == 401
def test_get_unknown_course(self):
url = reverse('course-home-progress-tab', args=['course-v1:unknown+course+2T2020'])
response = self.client.get(url)
assert 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
assert self.client.get(self.url).data['user_timezone'] is None
# Now switch users and confirm we get a different result
self.update_masquerade(username=user.username)
assert self.client.get(self.url).data['user_timezone'] == 'Asia/Tokyo'
# TODO: (AA-212) implement masquerade
# 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
# assert self.client.get(self.url).data['user_timezone'] is None
#
# # Now switch users and confirm we get a different result
# self.update_masquerade(username=user.username)
# assert self.client.get(self.url).data['user_timezone'] == 'Asia/Tokyo'

View File

@@ -3,25 +3,24 @@ Progress Tab Views
"""
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.generics import RetrieveAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
import lms.djangoapps.course_blocks.api as course_blocks_api
from xmodule.modulestore.django import modulestore
from common.djangoapps.student.models import CourseEnrollment
from lms.djangoapps.course_api.blocks.transformers.blocks_api import BlocksAPITransformer
from lms.djangoapps.course_home_api.progress.v1.serializers import ProgressTabSerializer
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_with_access, get_studio_url
from lms.djangoapps.courseware.courses import get_course_blocks_completion_summary, get_course_with_access, get_studio_url
from lms.djangoapps.courseware.masquerade import setup_masquerade
from lms.djangoapps.courseware.views.views import credit_course_requirements, get_cert_data
from lms.djangoapps.courseware.views.views import get_cert_data
from lms.djangoapps.grades.api import CourseGradeFactory
from lms.djangoapps.verify_student.services import IDVerificationService
from openedx.core.djangoapps.content.block_structure.transformers import BlockStructureTransformers
CREDIT_SUPPORT_URL = 'https://support.edx.org/hc/en-us/sections/115004154688-Purchasing-Academic-Credit'
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
class ProgressTabView(RetrieveAPIView):
@@ -39,54 +38,58 @@ class ProgressTabView(RetrieveAPIView):
Body consists of the following fields:
certificate_data: Object containing information about the user's certificate status
cert_status: (str) the status of a user's certificate (full list of statuses are defined in
lms/djangoapps/certificates/models.py)
cert_web_view_url: (str) the url to view the certificate
download_url: (str) the url to download the certificate
is_downloadable: (bool) true if the status is downloadable and the download url is not None
is_requestable: (bool) true if status is requesting and request_cert_url is not None
msg: (str) message for the certificate status
title: (str) title of the certificate status
credit_course_requirements: An object containing the following fields
dashboard_url: (str) the url to the user's dashboard
eligibility_status: (str) the user's eligibility to receive a course credit
requirements: object containing the following fields
display_name: (str) the name of the requirement that should be displayed
namespace: (str) the type that the requirement is
min_grade: (float) the percentage formatted minimum grade needed for this requirement
status: (str) the status of the requirement
status_date: (str) the date the status was set
credit_support_url: (str) the url to the support docs for purchasing a credit
courseware_summary: List of serialized Chapters. each Chapter has the following fields:
completion_summary: Object containing unit completion counts with the following fields:
complete_count: (float) number of complete units
incomplete_count: (float) number of incomplete units
locked_count: (float) number of units where contains_gated_content is True
course_grade: Object containing the following fields:
percent: (float) the user's total graded percent in the course
is_passing: (bool) whether the user's grade is above the passing grade cutoff
graded_course_blocks: 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:
assignment_type: (str) the format, if any, of the Subsection (Homework, Exam, etc)
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
has_graded_assignment: (bool) whether or not the Subsection is a graded assignment
num_points_earned: (int) the amount of points the user has earned for the given subsection
num_points_possible: (int) the total amount of points possible for the given subsection
percent_graded: (float) the percentage of total points the user has received a grade for in a given subsection
show_correctness: (str) a str representing whether to show the problem/practice scores based on due date
('always', 'never', 'past_due', values defined in
common/lib/xmodule/xmodule/modulestore/inheritance.py)
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', ...)
grading_policy:
assignment_policies: List of serialized assignment grading policy objects, each has the following fields:
type: (str) the assignment type
weight: (float) the percent weight the given assigment type has on the overall grade
grade_range: an object containing the grade range cutoffs. The exact keys in the object can vary, but they
range from just 'Pass', to a combination of 'A', 'B', 'C', and 'D'. If a letter grade is present,
'Pass' is not included.
studio_url: (str) a str of the link to the grading in studio for the course
user_timezone: (str) The user's preferred timezone
verification_data: an object containing
link: (str) the link to either start or retry verification
status: (str) the status of the verification
status_date: (str) the date time string of when the verification status was set
**Returns**
* 200 on success with above fields.
* 302 if the user is not enrolled.
* 403 if the user is not authenticated.
* 401 if the user is not authenticated.
* 404 if the course is not available or cannot be seen.
"""
authentication_classes = (
JwtAuthentication,
BearerAuthenticationAllowInactiveUser,
SessionAuthenticationAllowInactiveUser,
)
permission_classes = (IsAuthenticated,)
serializer_class = ProgressTabSerializer
@@ -106,20 +109,14 @@ 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),
]
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=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()
course_grade = CourseGradeFactory().read(request.user, course)\
descriptor = modulestore().get_course(course_key)
grading_policy = descriptor.grading_policy
verification_status = IDVerificationService.user_status(request.user)
verification_link = None
@@ -135,12 +132,12 @@ class ProgressTabView(RetrieveAPIView):
data = {
'certificate_data': get_cert_data(request.user, course, enrollment_mode, course_grade),
'courseware_summary': courseware_summary,
'credit_course_requirements': credit_course_requirements(course_key, request.user),
'credit_support_url': CREDIT_SUPPORT_URL,
'completion_summary': get_course_blocks_completion_summary(course_key, request.user),
'course_grade': course_grade,
'section_scores': course_grade.chapter_grades.values(),
'enrollment_mode': enrollment_mode,
'grading_policy': grading_policy,
'studio_url': get_studio_url(course, 'settings/grading'),
'user_timezone': user_timezone,
'verification_data': verification_data,
}
context = self.get_serializer_context()

View File

@@ -519,6 +519,41 @@ def get_course_assignment_date_blocks(course, user, request, num_return=None,
return date_blocks
@request_cached()
def get_course_blocks_completion_summary(course_key, user):
"""
Returns an object with the number of complete units, incomplete units, and units that contain gated content
for the given course. The complete and incomplete counts only reflect units that are able to be completed by
the given user. If a unit contains gated content, it is not counted towards the incomplete count.
The object contains fields: complete_count, incomplete_count, locked_count
"""
if not user.id:
return []
store = modulestore()
course_usage_key = store.make_course_usage_key(course_key)
block_data = get_course_blocks(user, course_usage_key, allow_start_dates_in_future=True, include_completion=True)
complete_count, incomplete_count, locked_count = 0, 0, 0
for section_key in block_data.get_children(course_usage_key): # pylint: disable=too-many-nested-blocks
for subsection_key in block_data.get_children(section_key):
for unit_key in block_data.get_children(subsection_key):
complete = block_data.get_xblock_field(unit_key, 'complete', False)
contains_gated_content = block_data.get_xblock_field(unit_key, 'contains_gated_content', False)
if contains_gated_content:
locked_count += 1
elif complete:
complete_count += 1
else:
incomplete_count += 1
return {
'complete_count': complete_count,
'incomplete_count': incomplete_count,
'locked_count': locked_count
}
@request_cached()
def get_course_assignments(course_key, user, include_access=False): # lint-amnesty, pylint: disable=too-many-statements
"""

View File

@@ -137,10 +137,6 @@ class CourseApiTestViews(BaseCoursewareTests):
assert not response.data['user_has_passing_grade']
if enrollment_mode == 'audit':
# This message comes from AUDIT_PASSING_CERT_DATA in lms/djangoapps/courseware/views/views.py
expected_audit_message = ('You are enrolled in the audit track for this course. '
'The audit track does not include a certificate.')
assert response.data['certificate_data']['msg'] == expected_audit_message
assert response.data['verify_identity_url'] is None
assert response.data['verification_status'] == 'none' # lint-amnesty, pylint: disable=literal-comparison
assert response.data['linkedin_add_to_profile_url'] is None