This is the second in a series of commits to create a Python API for the LMS program_enrollments app. We rename and reorganize some functions and classes in order to move towards the creation of that API. EDUCATOR-4321
239 lines
7.7 KiB
Python
239 lines
7.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
ProgramEnrollment V1 API internal utilities.
|
|
"""
|
|
from __future__ import absolute_import, unicode_literals
|
|
|
|
from datetime import datetime, timedelta
|
|
from functools import wraps
|
|
|
|
from django.urls import reverse
|
|
from django.utils.functional import cached_property
|
|
from edx_when.api import get_dates_for_course
|
|
from opaque_keys.edx.keys import CourseKey
|
|
from pytz import UTC
|
|
from rest_framework import status
|
|
from six import iteritems
|
|
|
|
from bulk_email.api import is_bulk_email_feature_enabled, is_user_opted_out_for_course
|
|
from lms.djangoapps.grades.rest_api.v1.utils import CourseEnrollmentPagination
|
|
from openedx.core.djangoapps.catalog.utils import get_programs, is_course_run_in_program
|
|
from openedx.core.lib.api.view_utils import verify_course_exists
|
|
from xmodule.modulestore.django import modulestore
|
|
|
|
from .constants import CourseRunProgressStatuses
|
|
|
|
|
|
class ProgramEnrollmentPagination(CourseEnrollmentPagination):
|
|
"""
|
|
Pagination class for views in the Program Enrollments app.
|
|
"""
|
|
page_size = 100
|
|
|
|
|
|
class ProgramSpecificViewMixin(object):
|
|
"""
|
|
A mixin for views that operate on or within a specific program.
|
|
|
|
Requires `program_uuid` to be one of the kwargs to the view.
|
|
"""
|
|
|
|
@cached_property
|
|
def program(self):
|
|
"""
|
|
The program specified by the `program_uuid` URL parameter.
|
|
"""
|
|
return get_programs(uuid=self.program_uuid)
|
|
|
|
@property
|
|
def program_uuid(self):
|
|
"""
|
|
The program specified by the `program_uuid` URL parameter.
|
|
"""
|
|
return self.kwargs['program_uuid']
|
|
|
|
|
|
class ProgramCourseSpecificViewMixin(ProgramSpecificViewMixin):
|
|
"""
|
|
A mixin for views that operate on or within a specific course run in a program
|
|
|
|
Requires `course_id` to be one of the kwargs to the view.
|
|
"""
|
|
|
|
@cached_property
|
|
def course_key(self):
|
|
"""
|
|
The course key for the course run specified by the `course_id` URL parameter.
|
|
"""
|
|
return CourseKey.from_string(self.kwargs['course_id'])
|
|
|
|
|
|
def verify_program_exists(view_func):
|
|
"""
|
|
Raises:
|
|
An API error if the `program_uuid` kwarg in the wrapped function
|
|
does not exist in the catalog programs cache.
|
|
|
|
Expects to be used within a ProgramSpecificViewMixin subclass.
|
|
"""
|
|
@wraps(view_func)
|
|
def wrapped_function(self, request, **kwargs):
|
|
"""
|
|
Wraps the given view_function.
|
|
"""
|
|
if self.program is None:
|
|
raise self.api_error(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
developer_message='no program exists with given key',
|
|
error_code='program_does_not_exist'
|
|
)
|
|
return view_func(self, request, **kwargs)
|
|
return wrapped_function
|
|
|
|
|
|
def verify_course_exists_and_in_program(view_func):
|
|
"""
|
|
Raises:
|
|
An api error if the course run specified by the `course_id` kwarg
|
|
in the wrapped function is not part of the curriculum of the program
|
|
specified by the `program_uuid` kwarg
|
|
|
|
This decorator guarantees existance of the program and course, so wrapping
|
|
alongside `verify_{program,course}_exists` is redundant.
|
|
|
|
Expects to be used within a subclass of ProgramCourseSpecificViewMixin.
|
|
"""
|
|
@wraps(view_func)
|
|
@verify_program_exists
|
|
@verify_course_exists
|
|
def wrapped_function(self, request, **kwargs):
|
|
"""
|
|
Wraps view function
|
|
"""
|
|
if not is_course_run_in_program(self.course_key, self.program):
|
|
raise self.api_error(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
developer_message="the program's curriculum does not contain the given course",
|
|
error_code='course_not_in_program'
|
|
)
|
|
return view_func(self, request, **kwargs)
|
|
return wrapped_function
|
|
|
|
|
|
def get_due_dates(request, course_key, user):
|
|
"""
|
|
Get due date information for a user for blocks in a course.
|
|
|
|
Arguments:
|
|
request: the request object
|
|
course_key (CourseKey): the CourseKey for the course
|
|
user: the user object for which we want due date information
|
|
|
|
Returns:
|
|
due_dates (list): a list of dictionaries containing due date information
|
|
keys:
|
|
name: the display name of the block
|
|
url: the deep link to the block
|
|
date: the due date for the block
|
|
"""
|
|
dates = get_dates_for_course(
|
|
course_key,
|
|
user,
|
|
)
|
|
|
|
store = modulestore()
|
|
|
|
due_dates = []
|
|
for (block_key, date_type), date in iteritems(dates):
|
|
if date_type == 'due':
|
|
block = store.get_item(block_key)
|
|
|
|
# get url to the block in the course
|
|
block_url = reverse('jump_to', args=[course_key, block_key])
|
|
block_url = request.build_absolute_uri(block_url)
|
|
|
|
due_dates.append({
|
|
'name': block.display_name,
|
|
'url': block_url,
|
|
'date': date,
|
|
})
|
|
return due_dates
|
|
|
|
|
|
def get_course_run_url(request, course_id):
|
|
"""
|
|
Get the URL to a course run.
|
|
|
|
Arguments:
|
|
request: the request object
|
|
course_id (string): the course id of the course
|
|
|
|
Returns:
|
|
(string): the URL to the course run associated with course_id
|
|
"""
|
|
course_run_url = reverse('openedx.course_experience.course_home', args=[course_id])
|
|
return request.build_absolute_uri(course_run_url)
|
|
|
|
|
|
def get_emails_enabled(user, course_id):
|
|
"""
|
|
Get whether or not emails are enabled in the context of a course.
|
|
|
|
Arguments:
|
|
user: the user object for which we want to check whether emails are enabled
|
|
course_id (string): the course id of the course
|
|
|
|
Returns:
|
|
(bool): True if emails are enabled for the course associated with course_id for the user;
|
|
False otherwise
|
|
"""
|
|
if is_bulk_email_feature_enabled(course_id=course_id):
|
|
return not is_user_opted_out_for_course(user=user, course_id=course_id)
|
|
return None
|
|
|
|
|
|
def get_course_run_status(course_overview, certificate_info):
|
|
"""
|
|
Get the progress status of a course run, given the state of a user's
|
|
certificate in the course.
|
|
|
|
In the case of self-paced course runs, the run is considered completed when
|
|
either the courserun has ended OR the user has earned a passing certificate
|
|
30 days ago or longer.
|
|
|
|
Arguments:
|
|
course_overview (CourseOverview): the overview for the course run
|
|
certificate_info: A dict containing the following keys:
|
|
``is_passing``: whether the user has a passing certificate in the course run
|
|
``created``: the date the certificate was created
|
|
|
|
Returns:
|
|
status: one of (
|
|
CourseRunProgressStatuses.COMPLETE,
|
|
CourseRunProgressStatuses.IN_PROGRESS,
|
|
CourseRunProgressStatuses.UPCOMING,
|
|
)
|
|
"""
|
|
is_certificate_passing = certificate_info.get('is_passing', False)
|
|
certificate_creation_date = certificate_info.get('created', datetime.max)
|
|
|
|
if course_overview.pacing == 'instructor':
|
|
if course_overview.has_ended():
|
|
return CourseRunProgressStatuses.COMPLETED
|
|
elif course_overview.has_started():
|
|
return CourseRunProgressStatuses.IN_PROGRESS
|
|
else:
|
|
return CourseRunProgressStatuses.UPCOMING
|
|
elif course_overview.pacing == 'self':
|
|
thirty_days_ago = datetime.now(UTC) - timedelta(30)
|
|
certificate_completed = is_certificate_passing and (
|
|
certificate_creation_date <= thirty_days_ago
|
|
)
|
|
if course_overview.has_ended() or certificate_completed:
|
|
return CourseRunProgressStatuses.COMPLETED
|
|
elif course_overview.has_started():
|
|
return CourseRunProgressStatuses.IN_PROGRESS
|
|
else:
|
|
return CourseRunProgressStatuses.UPCOMING
|
|
return None
|