From 4829fd4fde0f80879bd4c04cf838d06aa33223df Mon Sep 17 00:00:00 2001 From: Chris Pappas Date: Wed, 14 Aug 2019 11:27:12 -0400 Subject: [PATCH] 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 --- lms/djangoapps/program_enrollments/api/api.py | 133 +++++++++++++++++ .../api/v1/tests/test_views.py | 2 +- .../program_enrollments/api/v1/views.py | 138 ++---------------- .../content/course_overviews/api.py | 17 +++ .../content/course_overviews/serializers.py | 25 ++++ .../course_overviews/tests/test_api.py | 44 ++++++ .../tests/test_serializers.py | 37 +++++ 7 files changed, 268 insertions(+), 128 deletions(-) create mode 100644 lms/djangoapps/program_enrollments/api/api.py create mode 100644 openedx/core/djangoapps/content/course_overviews/api.py create mode 100644 openedx/core/djangoapps/content/course_overviews/serializers.py create mode 100644 openedx/core/djangoapps/content/course_overviews/tests/test_api.py create mode 100644 openedx/core/djangoapps/content/course_overviews/tests/test_serializers.py diff --git a/lms/djangoapps/program_enrollments/api/api.py b/lms/djangoapps/program_enrollments/api/api.py new file mode 100644 index 0000000000..af3ec29e39 --- /dev/null +++ b/lms/djangoapps/program_enrollments/api/api.py @@ -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 diff --git a/lms/djangoapps/program_enrollments/api/v1/tests/test_views.py b/lms/djangoapps/program_enrollments/api/v1/tests/test_views.py index ffe0dd0035..d1785ff82d 100644 --- a/lms/djangoapps/program_enrollments/api/v1/tests/test_views.py +++ b/lms/djangoapps/program_enrollments/api/v1/tests/test_views.py @@ -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, diff --git a/lms/djangoapps/program_enrollments/api/v1/views.py b/lms/djangoapps/program_enrollments/api/v1/views.py index 0554c04c5f..1e94c932b1 100644 --- a/lms/djangoapps/program_enrollments/api/v1/views.py +++ b/lms/djangoapps/program_enrollments/api/v1/views.py @@ -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, diff --git a/openedx/core/djangoapps/content/course_overviews/api.py b/openedx/core/djangoapps/content/course_overviews/api.py new file mode 100644 index 0000000000..2b1f24c393 --- /dev/null +++ b/openedx/core/djangoapps/content/course_overviews/api.py @@ -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 diff --git a/openedx/core/djangoapps/content/course_overviews/serializers.py b/openedx/core/djangoapps/content/course_overviews/serializers.py new file mode 100644 index 0000000000..69936200f1 --- /dev/null +++ b/openedx/core/djangoapps/content/course_overviews/serializers.py @@ -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 diff --git a/openedx/core/djangoapps/content/course_overviews/tests/test_api.py b/openedx/core/djangoapps/content/course_overviews/tests/test_api.py new file mode 100644 index 0000000000..0735151fb2 --- /dev/null +++ b/openedx/core/djangoapps/content/course_overviews/tests/test_api.py @@ -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] diff --git a/openedx/core/djangoapps/content/course_overviews/tests/test_serializers.py b/openedx/core/djangoapps/content/course_overviews/tests/test_serializers.py new file mode 100644 index 0000000000..7182f0243e --- /dev/null +++ b/openedx/core/djangoapps/content/course_overviews/tests/test_serializers.py @@ -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