Files
jawad khan 3cb9c00de2 Add organization logo field in mobile apis (#36940)
* feat: Add org_logo field in mobile apis

* fix: fixed quality checks
2025-06-24 19:42:36 +05:00

275 lines
10 KiB
Python

"""
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, get_course_organization_logo
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,
'org_logo': get_course_organization_logo(course_id),
# 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()