""" Serializer for user API """ from typing import Dict, List, Optional from completion.exceptions import UnavailableCompletionData from completion.utilities import get_key_to_last_completed_block from opaque_keys.edx.keys import UsageKey from rest_framework import serializers from rest_framework.reverse import reverse from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.student.models import CourseEnrollment, User from common.djangoapps.util.course import get_encoded_course_sharing_utm_params, get_link_for_about_page from lms.djangoapps.certificates.api import certificate_downloadable_status from lms.djangoapps.courseware.access import has_access from lms.djangoapps.courseware.courses import get_assignments_completions, get_past_and_future_course_assignments from lms.djangoapps.course_home_api.dates.serializers import DateSummarySerializer from lms.djangoapps.mobile_api.utils import API_V4 from openedx.features.course_duration_limits.access import get_user_course_expiration_date from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem from xmodule.modulestore.search import path_to_location class CourseOverviewField(serializers.RelatedField): # lint-amnesty, pylint: disable=abstract-method """ Custom field to wrap a CourseOverview object. Read-only. """ def to_representation(self, course_overview): # lint-amnesty, pylint: disable=arguments-differ course_id = str(course_overview.id) request = self.context.get('request') api_version = self.context.get('api_version') enrollment = CourseEnrollment.get_enrollment(user=self.context.get('request').user, course_key=course_id) return { # identifiers 'id': course_id, 'name': course_overview.display_name, 'number': course_overview.display_number_with_default, 'org': course_overview.display_org_with_default, # dates 'start': course_overview.start, 'start_display': course_overview.start_display, 'start_type': course_overview.start_type, 'end': course_overview.end, 'dynamic_upgrade_deadline': enrollment.upgrade_deadline, # notification info 'subscription_id': course_overview.clean_id(padding_char='_'), # access info 'courseware_access': has_access( request.user, 'load_mobile', course_overview ).to_json(), # various URLs # course_image is sent in both new and old formats # (within media to be compatible with the new Course API) 'media': { 'course_image': { 'uri': course_overview.course_image_url, 'name': 'Course Image', } }, 'course_image': course_overview.course_image_url, 'course_about': get_link_for_about_page(course_overview), 'course_sharing_utm_parameters': get_encoded_course_sharing_utm_params(), 'course_updates': reverse( 'course-updates-list', kwargs={'api_version': api_version, 'course_id': course_id}, request=request, ), 'course_handouts': reverse( 'course-handouts-list', kwargs={'api_version': api_version, 'course_id': course_id}, request=request, ), 'discussion_url': reverse( 'discussion_course', kwargs={'course_id': course_id}, request=request, ) if course_overview.is_discussion_tab_enabled(request.user) else None, # This is an old API that was removed as part of DEPR-4. We keep the # field present in case API parsers expect it, but this API is now # removed. 'video_outline': None, 'is_self_paced': course_overview.self_paced } class CourseEnrollmentSerializer(serializers.ModelSerializer): """ Serializes CourseEnrollment models """ course = CourseOverviewField(source="course_overview", read_only=True) certificate = serializers.SerializerMethodField() audit_access_expires = serializers.SerializerMethodField() course_modes = serializers.SerializerMethodField() def get_audit_access_expires(self, model): """ Returns expiration date for a course audit expiration, if any or null """ return get_user_course_expiration_date(model.user, model.course, model) def get_certificate(self, model): """Returns the information about the user's certificate in the course.""" certificate_info = certificate_downloadable_status(model.user, model.course_id) if certificate_info['is_downloadable']: return { 'url': self.context['request'].build_absolute_uri( certificate_info['download_url'] ), } else: return {} def get_course_modes(self, obj): """ Retrieve course modes associated with the course. """ course_modes = CourseMode.modes_for_course( obj.course.id, only_selectable=False ) return [ ModeSerializer(mode).data for mode in course_modes ] def to_representation(self, instance: CourseEnrollment) -> 'OrderedDict': # lint-amnesty, pylint: disable=unused-variable, line-too-long """ Override the to_representation method to add the course_status field to the serialized data. """ data = super().to_representation(instance) if 'course_progress' in self.context.get('requested_fields', []) and self.context.get('api_version') == API_V4: data['course_progress'] = get_assignments_completions(instance.course_id, instance.user) return data class Meta: model = CourseEnrollment fields = ('audit_access_expires', 'created', 'mode', 'is_active', 'course', 'certificate', 'course_modes') lookup_field = 'username' class CourseEnrollmentSerializerv05(CourseEnrollmentSerializer): """ Serializes CourseEnrollment models for v0.5 api Does not include 'audit_access_expires' field that is present in v1 api """ class Meta: model = CourseEnrollment fields = ('created', 'mode', 'is_active', 'course', 'certificate') lookup_field = 'username' class CourseEnrollmentSerializerModifiedForPrimary(CourseEnrollmentSerializer): """ Serializes CourseEnrollment models for API v4. Adds `course_status` field into serializer data. """ course_status = serializers.SerializerMethodField() course_progress = serializers.SerializerMethodField() course_assignments = serializers.SerializerMethodField() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.course = modulestore().get_course(self.instance.course.id) def get_course_status(self, model: CourseEnrollment) -> Optional[Dict[str, List[str]]]: """ Gets course status for the given user's enrollments. """ try: block_key = get_key_to_last_completed_block(model.user, model.course.id) path = path_to_location(modulestore(), block_key, self.context['request'], full_path=True) except (ItemNotFoundError, NoPathToItem, UnavailableCompletionData): return None path_ids = [str(block) for block in path] unit = modulestore().get_item(UsageKey.from_string(path_ids[3]), depth=0) return { 'last_visited_module_id': path_ids[2], 'last_visited_module_path': path_ids[:3], 'last_visited_block_id': path_ids[-1], 'last_visited_unit_display_name': unit.display_name, } def get_course_progress(self, model: CourseEnrollment) -> Dict[str, int]: """ Returns the progress of the user in the course. """ return get_assignments_completions(model.course_id, model.user) def get_course_assignments(self, model: CourseEnrollment) -> Dict[str, Optional[List[Dict[str, str]]]]: """ Returns the future assignment data and past assignments data for the user in the course. """ next_assignments, past_assignments = get_past_and_future_course_assignments( self.context.get('request'), model.user, self.course ) return { 'future_assignments': DateSummarySerializer(next_assignments, many=True).data, 'past_assignments': DateSummarySerializer(past_assignments, many=True).data, } class Meta: model = CourseEnrollment fields = ( 'audit_access_expires', 'created', 'mode', 'is_active', 'course', 'certificate', 'course_modes', 'course_status', 'course_progress', 'course_assignments', ) lookup_field = 'username' class UserSerializer(serializers.ModelSerializer): """ Serializes User models """ name = serializers.ReadOnlyField(source='profile.name') course_enrollments = serializers.SerializerMethodField() def get_course_enrollments(self, model): request = self.context.get('request') api_version = self.context.get('api_version') return reverse( 'courseenrollment-detail', kwargs={'api_version': api_version, 'username': model.username}, request=request ) class Meta: model = User fields = ('id', 'username', 'email', 'name', 'course_enrollments') lookup_field = 'username' # For disambiguating within the drf-yasg swagger schema ref_name = 'mobile_api.User' class ModeSerializer(serializers.Serializer): # pylint: disable=abstract-method """ Serializes a course's 'Mode' tuples Returns a serialized representation of the modes available for course enrollment. The course modes models are designed to return a tuple instead of the model object itself. This serializer handles the given tuple. """ slug = serializers.CharField(max_length=100) sku = serializers.CharField() android_sku = serializers.CharField() ios_sku = serializers.CharField() min_price = serializers.IntegerField()