Skip to content

Commit

Permalink
feat(mobile_api): Add course access object to mobile course info API (o…
Browse files Browse the repository at this point in the history
…penedx#34273)

* feat: include access serializer into mobile info api view

* test: add tests for serializer and view methods

* test: move tests to common directory and update test case

* fix: cr fixes and use snake case for functions

* test: fix additional get call assertion

* feat: add required course access messages to mobile endpoint

* test: [AXM-229] Improve test coverage

* style: [AXM-229] Try to fix linters

* fix: remove redundant comment

* refactor: change names for the test files

---------

Co-authored-by: KyryloKireiev <[email protected]>
  • Loading branch information
2 people authored and OmarIthawi committed May 31, 2024
1 parent 5c1fff6 commit 8a43fac
Show file tree
Hide file tree
Showing 4 changed files with 470 additions and 20 deletions.
95 changes: 94 additions & 1 deletion lms/djangoapps/mobile_api/course_info/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,35 @@
Course Info serializers
"""
from rest_framework import serializers
from typing import Union

from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.util.course import get_encoded_course_sharing_utm_params, get_link_for_about_page
from common.djangoapps.util.milestones_helpers import (
get_pre_requisite_courses_not_completed,
)
from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.courseware.access import administrative_accesses_to_course_for_user
from lms.djangoapps.courseware.access_utils import check_course_open_for_learner
from lms.djangoapps.mobile_api.users.serializers import ModeSerializer
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.features.course_duration_limits.access import get_user_course_expiration_date


class CourseInfoOverviewSerializer(serializers.ModelSerializer):
"""
Serializer for serialize additional fields in BlocksInfoInCourseView.
Serializer for additional course fields that should be returned in BlocksInfoInCourseView.
"""

name = serializers.CharField(source='display_name')
number = serializers.CharField(source='display_number_with_default')
org = serializers.CharField(source='display_org_with_default')
is_self_paced = serializers.BooleanField(source='self_paced')
media = serializers.SerializerMethodField()
course_sharing_utm_parameters = serializers.SerializerMethodField()
course_about = serializers.SerializerMethodField('get_course_about_url')
course_modes = serializers.SerializerMethodField()

class Meta:
model = CourseOverview
Expand All @@ -29,8 +44,86 @@ class Meta:
'end',
'is_self_paced',
'media',
'course_sharing_utm_parameters',
'course_about',
'course_modes',
)

@staticmethod
def get_media(obj):
"""
Return course images in the correct format.
"""
return {'image': obj.image_urls}

def get_course_sharing_utm_parameters(self, obj):
return get_encoded_course_sharing_utm_params()

def get_course_about_url(self, course_overview):
return get_link_for_about_page(course_overview)

def get_course_modes(self, course_overview):
"""
Retrieve course modes associated with the course.
"""
course_modes = CourseMode.modes_for_course(
course_overview.id,
only_selectable=False
)
return [
ModeSerializer(mode).data
for mode in course_modes
]


class MobileCourseEnrollmentSerializer(serializers.ModelSerializer):
"""
Serializer for the CourseEnrollment object used in the BlocksInfoInCourseView.
"""

class Meta:
fields = ('created', 'mode', 'is_active')
model = CourseEnrollment
lookup_field = 'username'


class CourseAccessSerializer(serializers.Serializer):
"""
Get info whether a user should be able to view course material.
"""

has_unmet_prerequisites = serializers.SerializerMethodField(method_name='get_has_unmet_prerequisites')
is_too_early = serializers.SerializerMethodField(method_name='get_is_too_early')
is_staff = serializers.SerializerMethodField(method_name='get_is_staff')
audit_access_expires = serializers.SerializerMethodField()
courseware_access = serializers.SerializerMethodField()

def get_has_unmet_prerequisites(self, data: dict) -> bool:
"""
Check whether or not a course has unmet prerequisites.
"""
return any(get_pre_requisite_courses_not_completed(data.get('user'), [data.get('course_id')]))

def get_is_too_early(self, data: dict) -> bool:
"""
Determine if the course is open to a learner (course has started or user has early beta access).
"""
return not check_course_open_for_learner(data.get('user'), data.get('course'))

def get_is_staff(self, data: dict) -> bool:
"""
Determine whether a user has staff access to this course.
"""
return any(administrative_accesses_to_course_for_user(data.get('user'), data.get('course_id')))

def get_audit_access_expires(self, data: dict) -> Union[str, None]:
"""
Returns expiration date for a course audit expiration, if any or null
"""
return get_user_course_expiration_date(data.get('user'), data.get('course'))

def get_courseware_access(self, data: dict) -> dict:
"""
Determine if the learner has access to the course, otherwise show error message.
"""
return has_access(data.get('user'), 'load_mobile', data.get('course')).to_json()
94 changes: 80 additions & 14 deletions lms/djangoapps/mobile_api/course_info/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,28 @@
"""

import logging
from typing import Optional, Union

import django
from django.contrib.auth import get_user_model
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from rest_framework import generics, status
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.views import APIView

from common.djangoapps.student.models import CourseEnrollment, User as StudentUser
from common.djangoapps.static_replace import make_static_urls_absolute
from lms.djangoapps.certificates.api import certificate_downloadable_status
from lms.djangoapps.courseware.courses import get_course_info_section_block
from lms.djangoapps.course_goals.models import UserActivity
from lms.djangoapps.course_api.blocks.views import BlocksInCourseView
from lms.djangoapps.mobile_api.course_info.serializers import CourseInfoOverviewSerializer
from lms.djangoapps.mobile_api.course_info.serializers import (
CourseInfoOverviewSerializer,
CourseAccessSerializer,
MobileCourseEnrollmentSerializer
)
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.lib.api.view_utils import view_auth_classes
from openedx.core.lib.xblock_utils import get_course_update_items
Expand All @@ -26,6 +34,8 @@
User = get_user_model()
log = logging.getLogger(__name__)

UserType = Union[django.contrib.auth.models.User, django.contrib.auth.models.AnonymousUser, StudentUser]


@mobile_view()
class CourseUpdatesList(generics.ListAPIView):
Expand Down Expand Up @@ -268,31 +278,52 @@ class BlocksInfoInCourseView(BlocksInCourseView):
* 404 if the course is not available or cannot be seen.
"""

def get_certificate(self, request, course_id):
def get_requested_user(self, user: UserType, username: Optional[str] = None) -> Union[UserType, None]:
"""
Return a user for whom the course blocks are fetched.
Arguments:
user: current user from request.
username: string with username.
Returns: A user object or None.
"""
if user.is_anonymous:
return None

if not username or (username and user.username == username):
return user
if username and (user.is_staff or user.is_superuser):
try:
return User.objects.get(username=username)
except User.DoesNotExist:
log.warning('Provided username does not correspond to an existing user %s', username)
return None

def get_certificate(self, request, user, course_id):
"""
Returns the information about the user's certificate in the course.
Return the information about the user's certificate in the course.
Arguments:
request (Request): The request object.
user (User): The user object.
course_id (str): The identifier of the course.
Returns:
(dict): A dict containing information about location of the user's certificate
or an empty dictionary, if there is no certificate.
"""
if request.user.is_authenticated:
certificate_info = certificate_downloadable_status(request.user, course_id)
if certificate_info['is_downloadable']:
return {
'url': request.build_absolute_uri(
certificate_info['download_url']
),
}
certificate_info = certificate_downloadable_status(user, course_id)
if certificate_info['is_downloadable']:
return {
'url': request.build_absolute_uri(
certificate_info['download_url']
),
}
return {}

def list(self, request, **kwargs): # pylint: disable=W0221
"""
REST API endpoint for listing all the blocks information in the course and
information about the course while regarding user access and roles.
information about the course considering user access and roles.
Arguments:
request - Django request object
Expand All @@ -301,13 +332,48 @@ def list(self, request, **kwargs): # pylint: disable=W0221
response = super().list(request, kwargs)

if request.GET.get('return_type', 'dict') == 'dict':
api_version = self.kwargs.get('api_version')
course_id = request.query_params.get('course_id', None)
course_key = CourseKey.from_string(course_id)
course_overview = CourseOverview.get_from_id(course_key)
requested_username = request.query_params.get('username', None)

course_data = {
'id': course_id,
'certificate': self.get_certificate(request, course_key),
'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,
),
}
course_data.update(CourseInfoOverviewSerializer(course_overview).data)

course_info_context = {}
if requested_user := self.get_requested_user(request.user, requested_username):
course_info_context = {
'user': requested_user
}
user_enrollment = CourseEnrollment.get_enrollment(user=requested_user, course_key=course_key)
course_data.update({
'discussion_url': reverse(
'discussion_course',
kwargs={'course_id': course_id},
request=request,
) if course_overview.is_discussion_tab_enabled(requested_user) else None,
'course_access_details': CourseAccessSerializer({
'user': requested_user,
'course': course_overview,
'course_id': course_key
}).data,
'certificate': self.get_certificate(request, requested_user, course_key),
'enrollment_details': MobileCourseEnrollmentSerializer(user_enrollment).data,
})

course_data.update(CourseInfoOverviewSerializer(course_overview, context=course_info_context).data)

response.data.update(course_data)
return response
Loading

0 comments on commit 8a43fac

Please sign in to comment.