-
Notifications
You must be signed in to change notification settings - Fork 48
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ENT-2117 | Adding enterprise_learner_portal djangoapp to house endpoi…
…nts needed for enterprise learner portal frontend (#544) Fixing indent Removing some whitespace Adding logic to serializer to make sure it is installed into openedx instance cleaning up imports; Adding tests Adding staff check on new endpoint. Also removing print statements Attempting to fix travis build Adding quality fixes more quality fixes Folding in refactors to get_course_run_status similar to what was done in edx-platform Changing user to query for based on discussion. changing tests Oops. missed a name change Tweaks around serializer request/user adding package to setup.py Updating readme and changelog Fixing changelog
- Loading branch information
1 parent
0eda44d
commit cd05c57
Showing
20 changed files
with
541 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# -*- coding: utf-8 -*- | ||
""" | ||
URL definitions for enterprise_learner_portal API endpoint. | ||
""" | ||
from __future__ import absolute_import, unicode_literals | ||
|
||
from django.conf.urls import include, url | ||
|
||
urlpatterns = [ | ||
url(r'^v1/', include('enterprise_learner_portal.api.v1.urls'), name='v1') | ||
] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
# -*- coding: utf-8 -*- | ||
""" | ||
enterprise_learner_portal serializer | ||
""" | ||
from __future__ import absolute_import, unicode_literals | ||
|
||
from django.utils.translation import ugettext as _ | ||
|
||
from rest_framework import serializers | ||
|
||
try: | ||
from lms.djangoapps.certificates.api import get_certificate_for_user | ||
from lms.djangoapps.program_enrollments.api.api import ( | ||
get_due_dates, | ||
get_course_run_url, | ||
get_emails_enabled, | ||
) | ||
except ImportError: | ||
get_certificate_for_user = None | ||
get_due_dates = None | ||
get_course_run_url = None | ||
get_emails_enabled = None | ||
|
||
from enterprise.utils import NotConnectedToOpenEdX | ||
from enterprise_learner_portal.utils import get_course_run_status | ||
|
||
|
||
class EnterpriseCourseEnrollmentSerializer(serializers.Serializer): | ||
""" | ||
A serializer for course enrollment information for a given course | ||
and enterprise customer user. | ||
""" | ||
|
||
def __init__(self, *args, **kwargs): | ||
if get_certificate_for_user is None: | ||
raise NotConnectedToOpenEdX( | ||
_('To use this EnterpriseCourseEnrollmentSerializer, this package must be ' | ||
'installed in an Open edX environment.') | ||
) | ||
super(EnterpriseCourseEnrollmentSerializer, self).__init__(*args, **kwargs) | ||
|
||
def to_representation(self, instance): | ||
representation = super(EnterpriseCourseEnrollmentSerializer, self).to_representation(instance) | ||
|
||
request = self.context['request'] | ||
user = request.user | ||
|
||
# Certificate | ||
certificate_info = get_certificate_for_user( | ||
user.username, | ||
instance['id'] | ||
) | ||
representation['certificate_download_url'] = certificate_info.get('download_url') | ||
|
||
# Email enabled | ||
emails_enabled = get_emails_enabled(user, instance['id']) | ||
if emails_enabled is not None: | ||
representation['emails_enabled'] = emails_enabled | ||
|
||
representation['course_run_id'] = instance['id'] | ||
representation['course_run_status'] = get_course_run_status( | ||
instance, | ||
certificate_info, | ||
) | ||
representation['start_date'] = instance['start'] | ||
representation['end_date'] = instance['end'] | ||
representation['display_name'] = instance['display_name_with_default'] | ||
representation['course_run_url'] = get_course_run_url(request, instance['id']) | ||
representation['due_dates'] = get_due_dates(request, instance['id'], user) | ||
representation['pacing'] = instance['pacing'] | ||
representation['org_name'] = instance['display_org_with_default'] | ||
|
||
return representation |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# -*- coding: utf-8 -*- | ||
""" | ||
URL definitions for enterprise_learner_portal API endpoint. | ||
""" | ||
from __future__ import absolute_import, unicode_literals | ||
|
||
from django.conf.urls import include, url | ||
|
||
from enterprise_learner_portal.api.v1.views import EnterpriseCourseEnrollmentView | ||
|
||
urlpatterns = [ | ||
url(r'^enterprise_course_enrollments/$', EnterpriseCourseEnrollmentView.as_view(), name="enterprise-learner-portal-course-enrollment-list"), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# -*- coding: utf-8 -*- | ||
from __future__ import unicode_literals | ||
|
||
|
||
from django.contrib.auth import get_user_model | ||
from django.core.exceptions import PermissionDenied | ||
from django.shortcuts import get_object_or_404 | ||
from django.utils.translation import ugettext as _ | ||
|
||
from rest_framework import permissions | ||
from rest_framework.authentication import SessionAuthentication | ||
from rest_framework.response import Response | ||
from rest_framework.views import APIView | ||
|
||
from edx_rest_framework_extensions.auth.bearer.authentication import BearerAuthentication | ||
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication | ||
|
||
from enterprise.models import EnterpriseCourseEnrollment, EnterpriseCustomerUser | ||
from enterprise.utils import NotConnectedToOpenEdX | ||
from enterprise_learner_portal.api.v1.serializers import EnterpriseCourseEnrollmentSerializer | ||
|
||
try: | ||
from openedx.core.djangoapps.content.course_overviews.api import get_course_overviews | ||
except ImportError: | ||
get_course_overviews = None | ||
|
||
User = get_user_model() | ||
|
||
|
||
class EnterpriseCourseEnrollmentView(APIView): | ||
permission_classes = (permissions.IsAuthenticated,) | ||
authentication_classes = (JwtAuthentication, BearerAuthentication, SessionAuthentication,) | ||
|
||
def get(self, request): | ||
if get_course_overviews is None: | ||
raise NotConnectedToOpenEdX( | ||
_('To use this endpoint, this package must be ' | ||
'installed in an Open edX environment.') | ||
) | ||
|
||
user = request.user | ||
enterprise_customer_user = get_object_or_404( | ||
EnterpriseCustomerUser, | ||
user_id=user.id | ||
) | ||
course_ids_for_ent_enrollments = EnterpriseCourseEnrollment.objects.filter( | ||
enterprise_customer_user=enterprise_customer_user | ||
).values_list('course_id', flat=True) | ||
|
||
overviews = get_course_overviews(course_ids_for_ent_enrollments) | ||
|
||
data = EnterpriseCourseEnrollmentSerializer( | ||
overviews, | ||
many=True, | ||
context={'request': request}, | ||
).data | ||
|
||
return Response(data) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# -*- coding: utf-8 -*- | ||
from __future__ import unicode_literals | ||
|
||
from django.apps import AppConfig | ||
|
||
|
||
class EnterpriseLearnerPortalConfig(AppConfig): | ||
name = 'enterprise_learner_portal' |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# -*- coding: utf-8 -*- | ||
""" | ||
URL definitions for enterprise API endpoint. | ||
""" | ||
from __future__ import absolute_import, unicode_literals | ||
|
||
from django.conf.urls import include, url | ||
|
||
urlpatterns = [ | ||
url(r'^api/', include('enterprise_learner_portal.api.urls'), name='api') | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# -*- coding: utf-8 -*- | ||
""" | ||
enterprise_learner_portal utils | ||
""" | ||
from __future__ import absolute_import, unicode_literals | ||
|
||
from datetime import datetime, timedelta | ||
from pytz import UTC | ||
|
||
|
||
class CourseRunProgressStatuses(object): | ||
""" | ||
Class to group statuses that a course run can be in with respect to user progress. | ||
""" | ||
IN_PROGRESS = 'in_progress' | ||
UPCOMING = 'upcoming' | ||
COMPLETED = 'completed' | ||
|
||
|
||
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, | ||
) | ||
None if pacing type is not matched | ||
""" | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Empty file.
98 changes: 98 additions & 0 deletions
98
tests/test_enterprise_learner_portal/api/test_serializers.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
# -*- coding: utf-8 -*- | ||
""" | ||
Tests for the EnterpriseCourseEnrollmentview of the enterprise_learner_portal app. | ||
""" | ||
|
||
from __future__ import absolute_import, unicode_literals | ||
|
||
from collections import OrderedDict | ||
|
||
import mock | ||
from pytest import mark | ||
|
||
from django.test import RequestFactory, TestCase | ||
|
||
from enterprise.utils import NotConnectedToOpenEdX | ||
from enterprise_learner_portal.api.v1.serializers import EnterpriseCourseEnrollmentSerializer | ||
from test_utils import factories | ||
|
||
|
||
@mark.django_db | ||
class TestEnterpriseCourseEnrollmentSerializer(TestCase): | ||
""" | ||
EnterpriseCourseEnrollmentSerializer tests. | ||
""" | ||
|
||
def setUp(self): | ||
super(TestEnterpriseCourseEnrollmentSerializer, self).setUp() | ||
|
||
self.user = factories.UserFactory.create(is_staff=True, is_active=True) | ||
self.factory = RequestFactory() | ||
|
||
@mock.patch('enterprise_learner_portal.api.v1.serializers.get_course_run_status') | ||
@mock.patch('enterprise_learner_portal.api.v1.serializers.get_emails_enabled') | ||
@mock.patch('enterprise_learner_portal.api.v1.serializers.get_course_run_url') | ||
@mock.patch('enterprise_learner_portal.api.v1.serializers.get_due_dates') | ||
@mock.patch('enterprise_learner_portal.api.v1.serializers.get_certificate_for_user') | ||
def test_serializer_representation( | ||
self, | ||
mock_get_cert, | ||
mock_get_due_dates, | ||
mock_get_course_run_url, | ||
mock_get_emails_enabled, | ||
mock_get_course_run_status, | ||
): | ||
""" | ||
EnterpriseCourseEnrollmentSerializer should create proper representation | ||
based on the instance data it receives (a course_overview) | ||
""" | ||
mock_get_cert.return_value = { | ||
'download_url': 'example.com', | ||
'is_passing': True, | ||
'created': 'a datetime object', | ||
} | ||
mock_get_due_dates.return_value = ['some', 'dates'] | ||
mock_get_course_run_url.return_value = 'example.com' | ||
mock_get_emails_enabled.return_value = True | ||
mock_get_course_run_status.return_value = 'completed' | ||
|
||
input_data = { | ||
'id': 'some+id+here', | ||
'start': 'a datetime object', | ||
'end': 'a datetime object', | ||
'display_name_with_default': 'a default name', | ||
'pacing': 'instructor', | ||
'display_org_with_default': 'my university', | ||
} | ||
|
||
request = self.factory.get('/') | ||
request.user = self.user | ||
|
||
serializer = EnterpriseCourseEnrollmentSerializer( | ||
[input_data], | ||
many=True, | ||
context={'request': request}, | ||
) | ||
|
||
expected = OrderedDict([ | ||
('certificate_download_url', 'example.com'), | ||
('emails_enabled', True), | ||
('course_run_id', 'some+id+here'), | ||
('course_run_status', 'completed'), | ||
('start_date', 'a datetime object'), | ||
('end_date', 'a datetime object'), | ||
('display_name', 'a default name'), | ||
('course_run_url', 'example.com'), | ||
('due_dates', ['some', 'dates']), | ||
('pacing', 'instructor'), | ||
('org_name', 'my university'), | ||
]) | ||
actual = serializer.data[0] | ||
self.assertDictEqual(actual, expected) | ||
|
||
def test_view_requires_openedx_installation(self): | ||
""" | ||
View should raise error if imports to helper methods fail. | ||
""" | ||
with self.assertRaises(NotConnectedToOpenEdX): | ||
EnterpriseCourseEnrollmentSerializer({}) |
Oops, something went wrong.