ENT-2117 | Creating new endpoint for enterprise learner portal. Includes small refactor of programs_enrollment GET logic, sowe do not need to duplicate the logic (#21258)
Adding new course_overview internal api CourseOverview serializer work Removing enterprise learner portal djangoapp from this repo Removing ent learner portal url Minor cleanups Updating serializers again typo adding some tests and quality fixes more quality fixes Fixing test Adding in an import i removed
This commit is contained in:
133
lms/djangoapps/program_enrollments/api/api.py
Normal file
133
lms/djangoapps/program_enrollments/api/api.py
Normal file
@@ -0,0 +1,133 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
ProgramEnrollment internal api
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from pytz import UTC
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from six import iteritems
|
||||
|
||||
from bulk_email.api import is_bulk_email_feature_enabled, is_user_opted_out_for_course
|
||||
from edx_when.api import get_dates_for_course
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from lms.djangoapps.program_enrollments.api.v1.constants import (
|
||||
CourseRunProgressStatuses,
|
||||
)
|
||||
|
||||
|
||||
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 course run 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
|
||||
@@ -1604,7 +1604,7 @@ class ProgramCourseEnrollmentOverviewViewTests(ProgramCacheTestCaseMixin, Shared
|
||||
display_name='unit_1'
|
||||
)
|
||||
|
||||
with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_dates_for_course') as mock_get_dates:
|
||||
with mock.patch('lms.djangoapps.program_enrollments.api.api.get_dates_for_course') as mock_get_dates:
|
||||
mock_get_dates.return_value = {
|
||||
(section_1.location, 'due'): section_1.due,
|
||||
(section_1.location, 'start'): section_1.start,
|
||||
|
||||
@@ -5,13 +5,10 @@ ProgramEnrollment Views
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from functools import wraps
|
||||
from pytz import UTC
|
||||
|
||||
from django.http import Http404
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from edx_rest_framework_extensions import permissions
|
||||
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
||||
@@ -23,13 +20,10 @@ from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from six import iteritems, text_type
|
||||
from six.moves import zip
|
||||
from six import text_type
|
||||
|
||||
from ccx_keys.locator import CCXLocator
|
||||
from bulk_email.api import is_bulk_email_feature_enabled, is_user_opted_out_for_course
|
||||
from course_modes.models import CourseMode
|
||||
from edx_when.api import get_dates_for_course
|
||||
from lms.djangoapps.certificates.api import get_certificate_for_user
|
||||
from lms.djangoapps.grades.api import (
|
||||
CourseGradeFactory,
|
||||
@@ -39,10 +33,15 @@ from lms.djangoapps.grades.api import (
|
||||
from lms.djangoapps.grades.rest_api.v1.utils import CourseEnrollmentPagination
|
||||
from lms.djangoapps.program_enrollments.api.v1.constants import (
|
||||
CourseEnrollmentResponseStatuses,
|
||||
CourseRunProgressStatuses,
|
||||
MAX_ENROLLMENT_RECORDS,
|
||||
ProgramEnrollmentResponseStatuses,
|
||||
)
|
||||
from lms.djangoapps.program_enrollments.api.api import (
|
||||
get_due_dates,
|
||||
get_course_run_url,
|
||||
get_emails_enabled,
|
||||
get_course_run_status
|
||||
)
|
||||
from lms.djangoapps.program_enrollments.api.v1.serializers import (
|
||||
CourseRunOverviewListSerializer,
|
||||
ProgramCourseEnrollmentListSerializer,
|
||||
@@ -59,7 +58,6 @@ from lms.djangoapps.program_enrollments.utils import get_user_by_program_id, Pro
|
||||
from student.helpers import get_resume_urls_for_enrollments
|
||||
from student.models import CourseEnrollment
|
||||
from student.roles import CourseInstructorRole, CourseStaffRole, UserBasedRole
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from openedx.core.djangoapps.catalog.utils import (
|
||||
course_run_keys_for_program,
|
||||
get_programs,
|
||||
@@ -1053,14 +1051,14 @@ class ProgramCourseEnrollmentOverviewView(DeveloperErrorViewMixin, ProgramSpecif
|
||||
course_run_dict = {
|
||||
'course_run_id': enrollment.course_id,
|
||||
'display_name': overview.display_name_with_default,
|
||||
'course_run_status': self.get_course_run_status(overview, certificate_info),
|
||||
'course_run_url': self.get_course_run_url(request, enrollment.course_id),
|
||||
'course_run_status': get_course_run_status(overview, certificate_info),
|
||||
'course_run_url': get_course_run_url(request, enrollment.course_id),
|
||||
'start_date': overview.start,
|
||||
'end_date': overview.end,
|
||||
'due_dates': self.get_due_dates(request, enrollment.course_id, user),
|
||||
'due_dates': get_due_dates(request, enrollment.course_id, user),
|
||||
}
|
||||
|
||||
emails_enabled = self.get_emails_enabled(user, enrollment.course_id)
|
||||
emails_enabled = get_emails_enabled(user, enrollment.course_id)
|
||||
if emails_enabled is not None:
|
||||
course_run_dict['emails_enabled'] = emails_enabled
|
||||
|
||||
@@ -1091,120 +1089,6 @@ class ProgramCourseEnrollmentOverviewView(DeveloperErrorViewMixin, ProgramSpecif
|
||||
if not program_enrollments:
|
||||
raise PermissionDenied
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
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 course run 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
|
||||
|
||||
|
||||
class ProgramCourseGradesView(
|
||||
DeveloperErrorViewMixin,
|
||||
|
||||
17
openedx/core/djangoapps/content/course_overviews/api.py
Normal file
17
openedx/core/djangoapps/content/course_overviews/api.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
CourseOverview internal api
|
||||
"""
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
|
||||
from openedx.core.djangoapps.content.course_overviews.serializers import (
|
||||
CourseOverviewBaseSerializer,
|
||||
)
|
||||
|
||||
|
||||
def get_course_overviews(course_ids):
|
||||
"""
|
||||
Return course_overview data for a given list of opaque_key course_ids.
|
||||
"""
|
||||
overviews = CourseOverview.objects.filter(id__in=course_ids)
|
||||
return CourseOverviewBaseSerializer(overviews, many=True).data
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
CourseOverview serializers
|
||||
"""
|
||||
from rest_framework import serializers
|
||||
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
|
||||
|
||||
class CourseOverviewBaseSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for a course run overview.
|
||||
"""
|
||||
|
||||
class Meta(object):
|
||||
model = CourseOverview
|
||||
fields = '__all__'
|
||||
|
||||
def to_representation(self, instance):
|
||||
representation = super(CourseOverviewBaseSerializer, self).to_representation(instance)
|
||||
representation['display_name_with_default'] = instance.display_name_with_default
|
||||
representation['has_started'] = instance.has_started
|
||||
representation['has_ended'] = instance.has_ended
|
||||
representation['pacing'] = instance.pacing
|
||||
return representation
|
||||
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
course_overview api tests
|
||||
"""
|
||||
from django.test import TestCase
|
||||
|
||||
from openedx.core.djangoapps.content.course_overviews.api import get_course_overviews
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
|
||||
from ..models import CourseOverview
|
||||
|
||||
|
||||
class TestCourseOverviewsApi(TestCase):
|
||||
"""
|
||||
TestCourseOverviewsApi tests.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestCourseOverviewsApi, self).setUp()
|
||||
for _ in range(3):
|
||||
CourseOverviewFactory.create()
|
||||
|
||||
def test_get_course_overviews(self):
|
||||
"""
|
||||
get_course_overviews should return the expected CourseOverview data
|
||||
in serialized form (a list of dicts)
|
||||
"""
|
||||
course_ids = []
|
||||
course_ids.append(str(CourseOverview.objects.first().id))
|
||||
course_ids.append(str(CourseOverview.objects.last().id))
|
||||
|
||||
data = get_course_overviews(course_ids)
|
||||
assert len(data) == 2
|
||||
for overview in data:
|
||||
assert overview['id'] in course_ids
|
||||
|
||||
fields = [
|
||||
'display_name_with_default',
|
||||
'has_started',
|
||||
'has_ended',
|
||||
'pacing',
|
||||
]
|
||||
for field in fields:
|
||||
assert field in data[0]
|
||||
@@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
CourseOverviewSerializer tests
|
||||
"""
|
||||
from django.test import TestCase
|
||||
|
||||
from openedx.core.djangoapps.content.course_overviews.serializers import CourseOverviewBaseSerializer
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
|
||||
from ..models import CourseOverview
|
||||
|
||||
|
||||
class TestCourseOverviewSerializer(TestCase):
|
||||
"""
|
||||
TestCourseOverviewSerializer tests.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestCourseOverviewSerializer, self).setUp()
|
||||
CourseOverviewFactory.create()
|
||||
|
||||
def test_get_course_overview_serializer(self):
|
||||
"""
|
||||
CourseOverviewBaseSerializer should add additional fields in the
|
||||
to_representation method that is overridden.
|
||||
"""
|
||||
overview = CourseOverview.objects.first()
|
||||
data = CourseOverviewBaseSerializer(overview).data
|
||||
|
||||
fields = [
|
||||
'display_name_with_default',
|
||||
'has_started',
|
||||
'has_ended',
|
||||
'pacing',
|
||||
]
|
||||
for field in fields:
|
||||
assert field in data
|
||||
Reference in New Issue
Block a user