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:
Chris Pappas
2019-08-14 11:27:12 -04:00
committed by GitHub
parent 25bf85b9c5
commit 4829fd4fde
7 changed files with 268 additions and 128 deletions

View 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

View File

@@ -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,

View File

@@ -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,

View 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

View File

@@ -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

View File

@@ -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]

View File

@@ -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