diff --git a/common/djangoapps/entitlements/api/v1/views.py b/common/djangoapps/entitlements/api/v1/views.py index 8755dd7023..89481383d8 100644 --- a/common/djangoapps/entitlements/api/v1/views.py +++ b/common/djangoapps/entitlements/api/v1/views.py @@ -1,4 +1,3 @@ -import datetime import logging from django.db import IntegrityError, transaction @@ -7,7 +6,6 @@ from django_filters.rest_framework import DjangoFilterBackend from edx_rest_framework_extensions.authentication import JwtAuthentication from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey -from pytz import UTC from rest_framework import permissions, viewsets, status from rest_framework.authentication import SessionAuthentication from rest_framework.response import Response @@ -16,7 +14,7 @@ from entitlements.api.v1.filters import CourseEntitlementFilter from entitlements.api.v1.permissions import IsAdminOrAuthenticatedReadOnly from entitlements.api.v1.serializers import CourseEntitlementSerializer from entitlements.models import CourseEntitlement -from entitlements.utils import is_course_run_entitlement_fullfillable +from entitlements.utils import is_course_run_entitlement_fulfillable from lms.djangoapps.commerce.utils import refund_entitlement from openedx.core.djangoapps.catalog.utils import get_course_runs_for_course from openedx.core.djangoapps.cors_csrf.authentication import SessionAuthenticationCrossDomainCsrf @@ -332,7 +330,7 @@ class EntitlementEnrollmentViewSet(viewsets.GenericViewSet): ) # Verify that the run is fullfillable - if not is_course_run_entitlement_fullfillable(course_run_key, entitlement): + if not is_course_run_entitlement_fulfillable(course_run_key, entitlement): return Response( status=status.HTTP_400_BAD_REQUEST, data={ diff --git a/common/djangoapps/entitlements/docs/decisions/0001-course-uuid-retrieved-by-api.rst b/common/djangoapps/entitlements/docs/decisions/0001-course-uuid-retrieved-by-api.rst new file mode 100644 index 0000000000..365dc722a1 --- /dev/null +++ b/common/djangoapps/entitlements/docs/decisions/0001-course-uuid-retrieved-by-api.rst @@ -0,0 +1,30 @@ +1. Course UUID Retrieved from Discovery by API +---------------------------------------------- + +Status +------ + +Accepted + +Context +------- + +Course UUID is a more reliable and consistently unique identifier for a Course. + + +Decision +-------- + +The decision was made for consistency to not move the course UUID into the Platform data model. As a result the only +method available to get a Course UUID based on a Course Key is the Discovery Service. + +Consequences +------------ + +When there is a need to find a Course by UUID, but only the Course Key is available the Discovery API is required to +resolve the identifier. + +References +---------- + +* https://openedx.atlassian.net/wiki/spaces/LEARNER/pages/171180253/Program+Bundling diff --git a/common/djangoapps/entitlements/models.py b/common/djangoapps/entitlements/models.py index 32179785e0..5fabaac317 100644 --- a/common/djangoapps/entitlements/models.py +++ b/common/djangoapps/entitlements/models.py @@ -1,16 +1,25 @@ +"""Entitlement Models""" + +import logging import uuid as uuid_tools from datetime import timedelta from django.conf import settings from django.contrib.sites.models import Site from django.db import models +from django.db import transaction from django.utils.timezone import now from model_utils.models import TimeStampedModel from lms.djangoapps.certificates.models import GeneratedCertificate +from openedx.core.djangoapps.catalog.utils import get_course_uuid_for_course from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from student.models import CourseEnrollment +from student.models import CourseEnrollmentException from util.date_utils import strftime_localized +from entitlements.utils import is_course_run_entitlement_fulfillable + +log = logging.getLogger("common.entitlements.models") class CourseEntitlementPolicy(models.Model): @@ -320,6 +329,91 @@ class CourseEntitlement(TimeStampedModel): enrollment_course_run=None ).select_related('user').select_related('enrollment_course_run') + @classmethod + def get_fulfillable_entitlements(cls, user): + """ + Returns all fulfillable entitlements for a User + + Arguments: + user (User): The user we are looking at the entitlements of. + + Returns + Queryset: A queryset of course Entitlements ordered descending by creation date that a user can enroll in. + These must not be expired and not have a course run already assigned to it. + """ + + return cls.objects.filter( + user=user, + ).exclude( + expired_at__isnull=False, + enrollment_course_run__isnull=False + ).order_by('-created') + + @classmethod + def get_fulfillable_entitlement_for_user_course_run(cls, user, course_run_key): + """ + Retrieves a fulfillable entitlement for the user and the given course run. + + Arguments: + user (User): The user that we are inspecting the entitlements for. + course_run_key (CourseKey): The course run Key. + + Returns: + CourseEntitlement: The most recent fulfillable CourseEntitlement, None otherwise. + """ + # Check if the User has any fulfillable entitlements. + # Note: Wait to retrieve the Course UUID until we have confirmed the User has fulfillable entitlements. + # This was done to avoid calling the APIs when the User does not have an entitlement. + entitlements = cls.get_fulfillable_entitlements(user) + if entitlements: + course_uuid = get_course_uuid_for_course(course_run_key) + if course_uuid: + entitlement = entitlements.filter(course_uuid=course_uuid).first() + if is_course_run_entitlement_fulfillable(course_run_key=course_run_key, entitlement=entitlement): + return entitlement + return None + + @classmethod + @transaction.atomic + def enroll_user_and_fulfill_entitlement(cls, entitlement, course_run_key): + """ + Enrolls the user in the Course Run and updates the entitlement with the new Enrollment. + + Returns: + bool: True if successfully fulfills given entitlement by enrolling the user in the given course run. + """ + try: + enrollment = CourseEnrollment.enroll( + user=entitlement.user, + course_key=course_run_key, + mode=entitlement.mode + ) + except CourseEnrollmentException: + log.exception('Login for Course Entitlement {uuid} failed'.format(uuid=entitlement.uuid)) + return False + + entitlement.set_enrollment(enrollment) + return True + + @classmethod + def check_for_existing_entitlement_and_enroll(cls, user, course_run_key): + """ + Looks at the User's existing entitlements to see if the user already has a Course Entitlement for the + course run provided in the course_key. If the user does have an Entitlement with no run set, the User is + enrolled in the mode set in the Entitlement. + + Arguments: + user (User): The user that we are inspecting the entitlements for. + course_run_key (CourseKey): The course run Key. + Returns: + bool: True if the user had an eligible course entitlement to which an enrollment in the + given course run was applied. + """ + entitlement = cls.get_fulfillable_entitlement_for_user_course_run(user, course_run_key) + if entitlement: + return cls.enroll_user_and_fulfill_entitlement(entitlement, course_run_key) + return False + class CourseEntitlementSupportDetail(TimeStampedModel): """ diff --git a/common/djangoapps/entitlements/tests/test_models.py b/common/djangoapps/entitlements/tests/test_models.py index 391247ed5c..258785977e 100644 --- a/common/djangoapps/entitlements/tests/test_models.py +++ b/common/djangoapps/entitlements/tests/test_models.py @@ -6,16 +6,88 @@ from datetime import timedelta from django.conf import settings from django.test import TestCase from django.utils.timezone import now +from mock import patch +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory -from lms.djangoapps.certificates.models import CertificateStatuses # pylint: disable=import-error +from course_modes.models import CourseMode +from course_modes.tests.factories import CourseModeFactory from lms.djangoapps.certificates.api import MODES +from lms.djangoapps.certificates.models import CertificateStatuses from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory -from student.tests.factories import CourseEnrollmentFactory +from student.models import CourseEnrollment +from student.tests.factories import (TEST_PASSWORD, CourseEnrollmentFactory, UserFactory) # Entitlements is not in CMS' INSTALLED_APPS so these imports will error during test collection if settings.ROOT_URLCONF == 'lms.urls': from entitlements.tests.factories import CourseEntitlementFactory + from entitlements.models import CourseEntitlement + + +@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') +class TestCourseEntitlementModelHelpers(ModuleStoreTestCase): + """ + Series of tests for the helper methods in the CourseEntitlement Model Class. + """ + def setUp(self): + super(TestCourseEntitlementModelHelpers, self).setUp() + self.user = UserFactory() + self.client.login(username=self.user.username, password=TEST_PASSWORD) + + @patch("entitlements.models.get_course_uuid_for_course") + def test_check_for_existing_entitlement_and_enroll(self, mock_get_course_uuid): + course = CourseFactory() + CourseModeFactory( + course_id=course.id, + mode_slug=CourseMode.VERIFIED, + # This must be in the future to ensure it is returned by downstream code. + expiration_datetime=now() + timedelta(days=1) + ) + entitlement = CourseEntitlementFactory.create( + mode=CourseMode.VERIFIED, + user=self.user, + ) + mock_get_course_uuid.return_value = entitlement.course_uuid + + assert not CourseEnrollment.is_enrolled(user=self.user, course_key=course.id) + + CourseEntitlement.check_for_existing_entitlement_and_enroll( + user=self.user, + course_run_key=course.id, + ) + + assert CourseEnrollment.is_enrolled(user=self.user, course_key=course.id) + + entitlement.refresh_from_db() + assert entitlement.enrollment_course_run + + @patch("entitlements.models.get_course_uuid_for_course") + def test_check_for_no_entitlement_and_do_not_enroll(self, mock_get_course_uuid): + course = CourseFactory() + CourseModeFactory( + course_id=course.id, + mode_slug=CourseMode.VERIFIED, + # This must be in the future to ensure it is returned by downstream code. + expiration_datetime=now() + timedelta(days=1) + ) + entitlement = CourseEntitlementFactory.create( + mode=CourseMode.VERIFIED, + user=self.user, + ) + mock_get_course_uuid.return_value = None + + assert not CourseEnrollment.is_enrolled(user=self.user, course_key=course.id) + + CourseEntitlement.check_for_existing_entitlement_and_enroll( + user=self.user, + course_run_key=course.id, + ) + + assert not CourseEnrollment.is_enrolled(user=self.user, course_key=course.id) + + entitlement.refresh_from_db() + assert entitlement.enrollment_course_run is None @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @@ -28,6 +100,8 @@ class TestModels(TestCase): start=now() ) self.enrollment = CourseEnrollmentFactory.create(course_id=self.course.id) + self.user = UserFactory() + self.client.login(username=self.user.username, password=TEST_PASSWORD) def test_is_entitlement_redeemable(self): """ diff --git a/common/djangoapps/entitlements/tests/test_utils.py b/common/djangoapps/entitlements/tests/test_utils.py index 8cf5874825..a09f29717d 100644 --- a/common/djangoapps/entitlements/tests/test_utils.py +++ b/common/djangoapps/entitlements/tests/test_utils.py @@ -16,13 +16,13 @@ from student.tests.factories import (TEST_PASSWORD, UserFactory, CourseOverviewF # Entitlements is not in CMS' INSTALLED_APPS so these imports will error during test collection if settings.ROOT_URLCONF == 'lms.urls': from entitlements.tests.factories import CourseEntitlementFactory - from entitlements.utils import is_course_run_entitlement_fullfillable + from entitlements.utils import is_course_run_entitlement_fulfillable @skip_unless_lms class TestCourseRunFullfillableForEntitlement(ModuleStoreTestCase): """ - Tests for the utility function is_course_run_entitlement_fullfillable + Tests for the utility function is_course_run_entitlement_fulfillable """ def setUp(self): @@ -64,7 +64,7 @@ class TestCourseRunFullfillableForEntitlement(ModuleStoreTestCase): entitlement = CourseEntitlementFactory.create(mode=CourseMode.VERIFIED) - assert is_course_run_entitlement_fullfillable(course_overview.id, entitlement) + assert is_course_run_entitlement_fulfillable(course_overview.id, entitlement) def test_course_run_not_fullfillable_run_ended(self): course_overview = self.create_course( @@ -76,7 +76,7 @@ class TestCourseRunFullfillableForEntitlement(ModuleStoreTestCase): entitlement = CourseEntitlementFactory.create(mode=CourseMode.VERIFIED) - assert not is_course_run_entitlement_fullfillable(course_overview.id, entitlement) + assert not is_course_run_entitlement_fulfillable(course_overview.id, entitlement) def test_course_run_not_fullfillable_enroll_period_ended(self): course_overview = self.create_course( @@ -88,7 +88,7 @@ class TestCourseRunFullfillableForEntitlement(ModuleStoreTestCase): entitlement = CourseEntitlementFactory.create(mode=CourseMode.VERIFIED) - assert not is_course_run_entitlement_fullfillable(course_overview.id, entitlement) + assert not is_course_run_entitlement_fulfillable(course_overview.id, entitlement) def test_course_run_fullfillable_user_enrolled(self): course_overview = self.create_course( @@ -102,7 +102,7 @@ class TestCourseRunFullfillableForEntitlement(ModuleStoreTestCase): # Enroll User in the Course, but do not update the entitlement CourseEnrollmentFactory.create(user=entitlement.user, course_id=course_overview.id) - assert is_course_run_entitlement_fullfillable(course_overview.id, entitlement) + assert is_course_run_entitlement_fulfillable(course_overview.id, entitlement) def test_course_run_not_fullfillable_upgrade_ended(self): course_overview = self.create_course( @@ -115,4 +115,4 @@ class TestCourseRunFullfillableForEntitlement(ModuleStoreTestCase): entitlement = CourseEntitlementFactory.create(mode=CourseMode.VERIFIED) - assert not is_course_run_entitlement_fullfillable(course_overview.id, entitlement) + assert not is_course_run_entitlement_fulfillable(course_overview.id, entitlement) diff --git a/common/djangoapps/entitlements/utils.py b/common/djangoapps/entitlements/utils.py index 1c9ce8271b..5c80e481b8 100644 --- a/common/djangoapps/entitlements/utils.py +++ b/common/djangoapps/entitlements/utils.py @@ -1,10 +1,10 @@ -from course_modes.models import CourseMode from django.utils import timezone +from course_modes.models import CourseMode from openedx.core.djangoapps.content.course_overviews.models import CourseOverview -def is_course_run_entitlement_fullfillable(course_run_id, entitlement, compare_date=timezone.now()): +def is_course_run_entitlement_fulfillable(course_run_key, entitlement, compare_date=timezone.now()): """ Checks that the current run meets the following criteria for an entitlement @@ -13,14 +13,14 @@ def is_course_run_entitlement_fullfillable(course_run_id, entitlement, compare_d 3) A User can upgrade to the entitlement mode Arguments: - course_run_id (String): The id of the Course run that is being checked. + course_run_key (CourseKey): The id of the Course run that is being checked. entitlement: The Entitlement that we are checking against. compare_date: The date and time that we are comparing against. Defaults to timezone.now() Returns: bool: True if the Course Run is fullfillable for the CourseEntitlement. """ - course_overview = CourseOverview.get_from_id(course_run_id) + course_overview = CourseOverview.get_from_id(course_run_key) # Verify that the course is still running run_start = course_overview.start @@ -36,7 +36,7 @@ def is_course_run_entitlement_fullfillable(course_run_id, entitlement, compare_d ) # Ensure the course run is upgradeable and the mode matches the entitlement's mode - unexpired_paid_modes = [mode.slug for mode in CourseMode.paid_modes_for_course(course_run_id)] + unexpired_paid_modes = [mode.slug for mode in CourseMode.paid_modes_for_course(course_run_key)] can_upgrade = unexpired_paid_modes and entitlement.mode in unexpired_paid_modes return is_running and can_upgrade and can_enroll diff --git a/common/djangoapps/student/views/management.py b/common/djangoapps/student/views/management.py index fa82e83d68..8bd4a345cc 100644 --- a/common/djangoapps/student/views/management.py +++ b/common/djangoapps/student/views/management.py @@ -9,16 +9,20 @@ import uuid import warnings from collections import namedtuple +import analytics +import dogstats_wrapper as dog_stats_api +from bulk_email.models import Optout +from courseware.courses import get_courses, sort_by_announcement, sort_by_start_date from django.conf import settings from django.contrib import messages -from django.contrib.auth import authenticate, load_backend, login as django_login, logout +from django.contrib.auth import login as django_login from django.contrib.auth.decorators import login_required from django.contrib.auth.models import AnonymousUser, User from django.contrib.auth.views import password_reset_confirm from django.core import mail -from django.core.urlresolvers import NoReverseMatch, reverse, reverse_lazy +from django.core.urlresolvers import reverse from django.core.validators import ValidationError, validate_email -from django.db import IntegrityError, transaction +from django.db import transaction from django.db.models.signals import post_save from django.dispatch import Signal, receiver from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden @@ -26,12 +30,15 @@ from django.shortcuts import redirect from django.template.context_processors import csrf from django.template.response import TemplateResponse from django.utils.encoding import force_bytes, force_text -from django.utils.http import base36_to_int, is_safe_url, urlencode, urlsafe_base64_encode -from django.utils.translation import ugettext as _ +from django.utils.http import base36_to_int, urlsafe_base64_encode from django.utils.translation import get_language, ungettext +from django.utils.translation import ugettext as _ from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie from django.views.decorators.http import require_GET, require_POST +from eventtracking import tracker from ipware.ip import get_ip +# Note that this lives in LMS, so this dependency should be refactored. +from notification_prefs.views import enable_notifications from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey from pytz import UTC @@ -39,19 +46,14 @@ from requests import HTTPError from six import text_type, iteritems from social_core.exceptions import AuthAlreadyAssociated, AuthException from social_django import utils as social_utils +from xmodule.modulestore.django import modulestore -import analytics -import dogstats_wrapper as dog_stats_api import openedx.core.djangoapps.external_auth.views import third_party_auth import track.views -from bulk_email.models import Optout # pylint: disable=import-error from course_modes.models import CourseMode -from courseware.courses import get_courses, sort_by_announcement, sort_by_start_date # pylint: disable=import-error from edxmako.shortcuts import render_to_response, render_to_string -from eventtracking import tracker -# Note that this lives in LMS, so this dependency should be refactored. -from notification_prefs.views import enable_notifications +from entitlements.models import CourseEntitlement from openedx.core.djangoapps import monitoring_utils from openedx.core.djangoapps.catalog.utils import ( get_programs_with_type, @@ -103,7 +105,6 @@ from util.bad_request_rate_limiter import BadRequestRateLimiter from util.db import outer_atomic from util.json_request import JsonResponse from util.password_policy_validators import validate_password_length, validate_password_strength -from xmodule.modulestore.django import modulestore log = logging.getLogger("edx.student") @@ -403,6 +404,9 @@ def change_enrollment(request, check_access=True): if redirect_url: return HttpResponse(redirect_url) + if CourseEntitlement.check_for_existing_entitlement_and_enroll(user=user, course_run_key=course_id): + return HttpResponse(reverse('courseware', args=[unicode(course_id)])) + # Check that auto enrollment is allowed for this course # (= the course is NOT behind a paywall) if CourseMode.can_auto_enroll(course_id): diff --git a/lms/djangoapps/commerce/api/v0/views.py b/lms/djangoapps/commerce/api/v0/views.py index 99acb87226..a50e2afee8 100644 --- a/lms/djangoapps/commerce/api/v0/views.py +++ b/lms/djangoapps/commerce/api/v0/views.py @@ -1,6 +1,8 @@ """ API v0 views. """ import logging +from courseware import courses +from django.core.urlresolvers import reverse from edx_rest_api_client import exceptions from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey @@ -11,16 +13,15 @@ from rest_framework.views import APIView from six import text_type from course_modes.models import CourseMode -from courseware import courses from enrollment.api import add_enrollment from enrollment.views import EnrollmentCrossDomainSessionAuth +from entitlements.models import CourseEntitlement from openedx.core.djangoapps.commerce.utils import ecommerce_api_client from openedx.core.djangoapps.embargo import api as embargo_api from openedx.core.djangoapps.user_api.preferences.api import update_email_opt_in from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser from student.models import CourseEnrollment from util.json_request import JsonResponse - from ...constants import Messages from ...http import DetailResponse @@ -113,6 +114,14 @@ class BasketsView(APIView): honor_mode = CourseMode.mode_for_course(course_key, CourseMode.HONOR) audit_mode = CourseMode.mode_for_course(course_key, CourseMode.AUDIT) + # Check to see if the User has an entitlement and enroll them if they have one for this course + if CourseEntitlement.check_for_existing_entitlement_and_enroll(user=user, course_run_key=course_key): + return JsonResponse( + { + 'redirect_destination': reverse('courseware', args=[unicode(course_id)]), + }, + ) + # Accept either honor or audit as an enrollment mode to # maintain backwards compatibility with existing courses default_enrollment_mode = audit_mode or honor_mode diff --git a/lms/static/js/student_account/enrollment.js b/lms/static/js/student_account/enrollment.js index 596fa3e373..608ae2b389 100644 --- a/lms/static/js/student_account/enrollment.js +++ b/lms/static/js/student_account/enrollment.js @@ -41,12 +41,15 @@ this.redirect(redirectUrl); } } - }).done(function() { + }).done(function(response) { // If we successfully enrolled, redirect the user // to the next page (usually the student dashboard or payment flow) - if (redirectUrl) { + if (response.redirect_destination) { + this.redirect(response.redirect_destination) + } else if (redirectUrl) { this.redirect(redirectUrl); } + }); }, diff --git a/openedx/core/djangoapps/catalog/utils.py b/openedx/core/djangoapps/catalog/utils.py index bde8b66a8e..a7d8be8a2e 100644 --- a/openedx/core/djangoapps/catalog/utils.py +++ b/openedx/core/djangoapps/catalog/utils.py @@ -2,6 +2,7 @@ import copy import datetime import logging +import uuid import pycountry from django.conf import settings @@ -11,13 +12,13 @@ from edx_rest_api_client.client import EdxRestApiClient from opaque_keys.edx.keys import CourseKey from pytz import UTC -from entitlements.utils import is_course_run_entitlement_fullfillable -from student.models import CourseEnrollment +from entitlements.utils import is_course_run_entitlement_fulfillable from openedx.core.djangoapps.catalog.cache import (PROGRAM_CACHE_KEY_TPL, SITE_PROGRAM_UUIDS_CACHE_KEY_TPL) from openedx.core.djangoapps.catalog.models import CatalogIntegration from openedx.core.lib.edx_api_utils import get_edx_api_data from openedx.core.lib.token_utils import JwtBuilder +from student.models import CourseEnrollment logger = logging.getLogger(__name__) @@ -287,6 +288,69 @@ def get_course_runs_for_course(course_uuid): return [] +def get_course_uuid_for_course(course_run_key): + """ + Retrieve the Course UUID for a given course key + + Arguments: + course_run_key (CourseKey): A Key for a Course run that will be pulled apart to get just the information + required for a Course (e.g. org+course) + + Returns: + UUID: Course UUID and None if it was not retrieved. + """ + catalog_integration = CatalogIntegration.current() + + if catalog_integration.is_enabled(): + try: + user = catalog_integration.get_service_user() + except ObjectDoesNotExist: + logger.error( + 'Catalog service user with username [%s] does not exist. Course UUID will not be retrieved.', + catalog_integration.service_username, + ) + return [] + + api = create_catalog_api_client(user) + + run_cache_key = '{base}.course_run.{course_run_key}'.format( + base=catalog_integration.CACHE_KEY, + course_run_key=course_run_key + ) + + course_run_data = get_edx_api_data( + catalog_integration, + 'course_runs', + resource_id=unicode(course_run_key), + api=api, + cache_key=run_cache_key if catalog_integration.is_cache_enabled else None, + long_term_cache=True, + many=False, + ) + + course_key_str = course_run_data.get('course', None) + + if course_key_str: + run_cache_key = '{base}.course.{course_key}'.format( + base=catalog_integration.CACHE_KEY, + course_key=course_key_str + ) + + data = get_edx_api_data( + catalog_integration, + 'courses', + resource_id=course_key_str, + api=api, + cache_key=run_cache_key if catalog_integration.is_cache_enabled else None, + long_term_cache=True, + many=False, + ) + uuid_str = data.get('uuid', None) + if uuid_str: + return uuid.UUID(uuid_str) + return None + + def get_pseudo_session_for_entitlement(entitlement): """ This function is used to pass pseudo-data to the front end, returning a single session, regardless of whether that @@ -325,7 +389,7 @@ def get_fulfillable_course_runs_for_entitlement(entitlement, course_runs): for course_run in course_runs: course_id = CourseKey.from_string(course_run.get('key')) is_enrolled = CourseEnrollment.is_enrolled(entitlement.user, course_id) - if is_course_run_entitlement_fullfillable(course_id, entitlement, search_time): + if is_course_run_entitlement_fulfillable(course_id, entitlement, search_time): if (is_enrolled and entitlement.enrollment_course_run and course_id == entitlement.enrollment_course_run.course_id):