Merge pull request #26928 from edx/ciduarte/hack
AA-671: Course Home MFE Progress Tab API cleanup
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user