Files
edx-platform/lms/djangoapps/course_home_api/progress/views.py
Michael Terry a162140492 fix: have progress MFE API recalculate visible course grade
Previously, the course grade returned would be the stored grade,
which is calculated for all content, not just the visible grades.
(Some grades are not yet released to the learner.)

This fix recalculates the overall grade before sending to the MFE,
so that it doesn't have to recompute it itself (without all the
particular logic that the platform uses when grading).

AA-1217
2022-03-11 15:11:05 -05:00

271 lines
15 KiB
Python

"""
Progress Tab Views
"""
from django.contrib.auth import get_user_model
from django.http.response import Http404
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
from xmodule.modulestore.django import modulestore
from common.djangoapps.student.models import CourseEnrollment
from lms.djangoapps.course_home_api.progress.serializers import ProgressTabSerializer
from lms.djangoapps.course_home_api.toggles import course_home_mfe_progress_tab_is_active
from lms.djangoapps.courseware.access import has_access, has_ccx_coach_role
from lms.djangoapps.course_blocks.api import get_course_blocks
from lms.djangoapps.course_blocks.transformers import start_date
from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException
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.grades.api import CourseGradeFactory
from lms.djangoapps.verify_student.services import IDVerificationService
from openedx.core.djangoapps.content.block_structure.transformers import BlockStructureTransformers
from openedx.core.djangoapps.content.block_structure.api import get_block_structure_manager
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
from openedx.features.content_type_gating.block_transformers import ContentTypeGateTransformer
from openedx.features.course_duration_limits.access import get_access_expiration_data
from openedx.features.enterprise_support.utils import get_enterprise_learner_generic_name
User = get_user_model()
class ProgressTabView(RetrieveAPIView):
"""
**Use Cases**
Request details for the Progress Tab
**Example Requests**
GET api/course_home/v1/progress/{course_key}
GET api/course_home/v1/progress/{course_key}/{student_id}/
**Response Values**
Body consists of the following fields:
access_expiration: An object detailing when access to this course will expire
expiration_date: (str) When the access expires, in ISO 8601 notation
masquerading_expired_course: (bool) Whether this course is expired for the masqueraded user
upgrade_deadline: (str) Last chance to upgrade, in ISO 8601 notation (or None if can't upgrade anymore)
upgrade_url: (str) Upgrade link (or None if can't upgrade anymore)
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
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:
is_passing: (bool) whether the user's grade is above the passing grade cutoff
letter_grade: (str) the user's letter grade based on the set grade range.
If user is passing, value may be 'A', 'B', 'C', 'D', 'Pass', otherwise none
percent: (float) the user's total graded percent in the course
credit_course_requirements: Object containing credit course requirements with the following fields:
eligibility_status: (str) Indicates if the user is eligible to receive credit. Value may be
"eligible", "not_eligible", or "partial_eligible"
requirements: List of requirements that must be fulfilled to be eligible to receive credit. See
`get_credit_requirement_status` for details on the fields
end: (date) end date of the course
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:
num_droppable: (int) the number of lowest scored assignments that will not be counted towards the final
grade
short_label: (str) the abbreviated name given to the assignment type
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.
has_scheduled_content: (bool) boolean on if the course has content scheduled with a release date in the future
section_scores: 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)
block_key: (str) the key of the given subsection block
display_name: (str) a str of what the name of the Subsection is for displaying on the site
has_graded_assignment: (bool) whether or not the Subsection is a graded assignment
learner_has_access: (bool) whether the learner has access to the subsection (could be FBE gated)
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
override: Optional object if grade has been overridden by proctor or grading change
system: (str) either GRADING or PROCTORING
reason: (str) a comment on the grading override
percent_graded: (float) the percentage of total points the user has received a grade for in a given
subsection
problem_scores: List of objects that represent individual problem scores with the following fields:
earned: (float) number of earned points
possible: (float) number of possible points
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 or None) the absolute path url to the Subsection or None if the Subsection is no longer
accessible to the learner due to a hide_after_due course team setting
studio_url: (str) a str of the link to the grading in studio for the course
user_has_passing_grade: (bool) boolean on if the user has a passing grade in the course
username: (str) username of the student whose progress information is being displayed.
verification_data: an object containing
link: (str) the link to either start or retry ID verification
status: (str) the status of the ID verification
status_date: (str) the date time string of when the ID verification status was set
**Returns**
* 200 on success with above fields.
* 401 if the user is not authenticated or not enrolled.
* 404 if the course is not available or cannot be seen.
"""
authentication_classes = (
JwtAuthentication,
BearerAuthenticationAllowInactiveUser,
SessionAuthenticationAllowInactiveUser,
)
permission_classes = (IsAuthenticated,)
serializer_class = ProgressTabSerializer
def _get_student_user(self, request, course_key, student_id, is_staff):
"""Gets the student User object, either from coaching, masquerading, or normal actual request"""
if student_id:
try:
student_id = int(student_id)
except ValueError as e:
raise Http404 from e
if student_id is None or student_id == request.user.id:
_, student = setup_masquerade(
request,
course_key,
staff_access=is_staff,
reset_masquerade_data=True
)
return student
# When a student_id is passed in, we display the progress page for the user
# with the provided user id, rather than the requesting user
try:
coach_access = has_ccx_coach_role(request.user, course_key)
except CCXLocatorValidationException:
coach_access = False
has_access_on_students_profiles = is_staff or coach_access
# Requesting access to a different student's profile
if not has_access_on_students_profiles:
raise Http404
try:
return User.objects.get(id=student_id)
except User.DoesNotExist as exc:
raise Http404 from exc
def get(self, request, *args, **kwargs):
course_key_string = kwargs.get('course_key_string')
course_key = CourseKey.from_string(course_key_string)
student_id = kwargs.get('student_id')
if not course_home_mfe_progress_tab_is_active(course_key):
raise Http404
# Enable NR tracing for this view based on course
monitoring_utils.set_custom_attribute('course_id', course_key_string)
monitoring_utils.set_custom_attribute('user_id', request.user.id)
monitoring_utils.set_custom_attribute('is_staff', request.user.is_staff)
is_staff = bool(has_access(request.user, 'staff', course_key))
student = self._get_student_user(request, course_key, student_id, is_staff)
username = get_enterprise_learner_generic_name(request) or student.username
course = get_course_with_access(student, 'load', course_key, check_if_enrolled=False)
course_overview = CourseOverview.get_from_id(course_key)
enrollment = CourseEnrollment.get_enrollment(student, course_key)
enrollment_mode = getattr(enrollment, 'mode', None)
if not (enrollment and enrollment.is_active) and not is_staff:
return Response('User not enrolled.', status=401)
# The block structure is used for both the course_grade and has_scheduled content fields
# So it is called upfront and reused for optimization purposes
collected_block_structure = get_block_structure_manager(course_key).get_collected()
course_grade = CourseGradeFactory().read(student, collected_block_structure=collected_block_structure)
# recalculate course grade from visible grades (stored grade was calculated over all grades, visible or not)
course_grade.update(visible_grades_only=True, has_staff_access=is_staff)
# Get has_scheduled_content data
transformers = BlockStructureTransformers()
transformers += [start_date.StartDateTransformer(), ContentTypeGateTransformer()]
usage_key = collected_block_structure.root_block_usage_key
course_blocks = get_course_blocks(
student,
usage_key,
transformers=transformers,
collected_block_structure=collected_block_structure,
include_has_scheduled_content=True
)
has_scheduled_content = course_blocks.get_xblock_field(usage_key, 'has_scheduled_content')
# Get user_has_passing_grade data
user_has_passing_grade = False
if not student.is_anonymous:
user_grade = course_grade.percent
user_has_passing_grade = user_grade >= course.lowest_passing_grade
descriptor = modulestore().get_course(course_key)
grading_policy = descriptor.grading_policy
verification_status = IDVerificationService.user_status(student)
verification_link = None
if verification_status['status'] is None or verification_status['status'] == 'expired':
verification_link = IDVerificationService.get_verify_location(course_id=course_key)
elif verification_status['status'] == 'must_reverify':
verification_link = IDVerificationService.get_verify_location(course_id=course_key)
verification_data = {
'link': verification_link,
'status': verification_status['status'],
'status_date': verification_status['status_date'],
}
access_expiration = get_access_expiration_data(request.user, course_overview)
data = {
'access_expiration': access_expiration,
'certificate_data': get_cert_data(student, course, enrollment_mode, course_grade),
'completion_summary': get_course_blocks_completion_summary(course_key, student),
'course_grade': course_grade,
'credit_course_requirements': credit_course_requirements(course_key, student),
'end': course.end,
'enrollment_mode': enrollment_mode,
'grading_policy': grading_policy,
'has_scheduled_content': has_scheduled_content,
'section_scores': list(course_grade.chapter_grades.values()),
'studio_url': get_studio_url(course, 'settings/grading'),
'username': username,
'user_has_passing_grade': user_has_passing_grade,
'verification_data': verification_data,
}
context = self.get_serializer_context()
context['staff_access'] = is_staff
context['course_blocks'] = course_blocks
context['course_key'] = course_key
# course_overview and enrollment will be used by VerifiedModeSerializer
context['course_overview'] = course_overview
context['enrollment'] = enrollment
serializer = self.get_serializer_class()(data, context=context)
return Response(serializer.data)