Skip to content
Snippets Groups Projects
Unverified Commit b41019f3 authored by Albert (AJ) St. Aubin's avatar Albert (AJ) St. Aubin Committed by GitHub
Browse files

Merge pull request #17308 from edx/aj/LEARNER-3800

Aj/learner 3800
parents cebbc7d6 90d29550
No related merge requests found
......@@ -350,6 +350,20 @@ class EntitlementEnrollmentViewSetTest(ModuleStoreTestCase):
self.course = CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course')
self.course2 = CourseFactory.create(org='edX', number='DemoX2', display_name='Demo_Course 2')
self.course_mode = CourseModeFactory(
course_id=self.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)
)
self.course_mode = CourseModeFactory(
course_id=self.course2.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)
)
self.return_values = [
{'key': str(self.course.id)},
{'key': str(self.course2.id)}
......@@ -357,7 +371,7 @@ class EntitlementEnrollmentViewSetTest(ModuleStoreTestCase):
@patch("entitlements.api.v1.views.get_course_runs_for_course")
def test_user_can_enroll(self, mock_get_course_runs):
course_entitlement = CourseEntitlementFactory.create(user=self.user)
course_entitlement = CourseEntitlementFactory.create(user=self.user, mode=CourseMode.VERIFIED)
mock_get_course_runs.return_value = self.return_values
url = reverse(
self.ENTITLEMENTS_ENROLLMENT_NAMESPACE,
......@@ -381,7 +395,7 @@ class EntitlementEnrollmentViewSetTest(ModuleStoreTestCase):
@patch("entitlements.api.v1.views.get_course_runs_for_course")
def test_user_can_unenroll(self, mock_get_course_runs):
course_entitlement = CourseEntitlementFactory.create(user=self.user)
course_entitlement = CourseEntitlementFactory.create(user=self.user, mode=CourseMode.VERIFIED)
mock_get_course_runs.return_value = self.return_values
url = reverse(
......@@ -416,7 +430,7 @@ class EntitlementEnrollmentViewSetTest(ModuleStoreTestCase):
@patch("entitlements.api.v1.views.get_course_runs_for_course")
def test_user_can_switch(self, mock_get_course_runs):
mock_get_course_runs.return_value = self.return_values
course_entitlement = CourseEntitlementFactory.create(user=self.user)
course_entitlement = CourseEntitlementFactory.create(user=self.user, mode=CourseMode.VERIFIED)
url = reverse(
self.ENTITLEMENTS_ENROLLMENT_NAMESPACE,
......@@ -453,7 +467,7 @@ class EntitlementEnrollmentViewSetTest(ModuleStoreTestCase):
@patch("entitlements.api.v1.views.get_course_runs_for_course")
def test_user_already_enrolled(self, mock_get_course_runs):
course_entitlement = CourseEntitlementFactory.create(user=self.user)
course_entitlement = CourseEntitlementFactory.create(user=self.user, mode=CourseMode.VERIFIED)
mock_get_course_runs.return_value = self.return_values
url = reverse(
......@@ -474,16 +488,13 @@ class EntitlementEnrollmentViewSetTest(ModuleStoreTestCase):
assert response.status_code == 201
assert CourseEnrollment.is_enrolled(self.user, self.course.id)
course_entitlement.refresh_from_db()
assert CourseEnrollment.is_enrolled(self.user, self.course.id)
assert course_entitlement.enrollment_course_run is not None
@patch("entitlements.api.v1.views.get_course_runs_for_course")
def test_user_cannot_enroll_in_unknown_course_run_id(self, mock_get_course_runs):
fake_course_str = str(self.course.id) + 'fake'
fake_course_key = CourseKey.from_string(fake_course_str)
course_entitlement = CourseEntitlementFactory.create(user=self.user)
course_entitlement = CourseEntitlementFactory.create(user=self.user, mode=CourseMode.VERIFIED)
mock_get_course_runs.return_value = self.return_values
url = reverse(
......@@ -508,7 +519,7 @@ class EntitlementEnrollmentViewSetTest(ModuleStoreTestCase):
@patch('entitlements.api.v1.views.refund_entitlement', return_value=True)
@patch('entitlements.api.v1.views.get_course_runs_for_course')
def test_user_can_revoke_and_refund(self, mock_get_course_runs, mock_refund_entitlement):
course_entitlement = CourseEntitlementFactory.create(user=self.user)
course_entitlement = CourseEntitlementFactory.create(user=self.user, mode=CourseMode.VERIFIED)
mock_get_course_runs.return_value = self.return_values
url = reverse(
......@@ -555,7 +566,7 @@ class EntitlementEnrollmentViewSetTest(ModuleStoreTestCase):
mock_refund_entitlement,
mock_is_refundable
):
course_entitlement = CourseEntitlementFactory.create(user=self.user)
course_entitlement = CourseEntitlementFactory.create(user=self.user, mode=CourseMode.VERIFIED)
mock_get_course_runs.return_value = self.return_values
url = reverse(
......@@ -599,7 +610,7 @@ class EntitlementEnrollmentViewSetTest(ModuleStoreTestCase):
mock_refund_entitlement,
mock_is_refundable
):
course_entitlement = CourseEntitlementFactory.create(user=self.user)
course_entitlement = CourseEntitlementFactory.create(user=self.user, mode=CourseMode.VERIFIED)
mock_get_course_runs.return_value = self.return_values
url = reverse(
......
import datetime
import logging
from django.db import IntegrityError, transaction
......@@ -6,6 +7,7 @@ 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
......@@ -14,6 +16,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 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
......@@ -318,9 +321,8 @@ class EntitlementEnrollmentViewSet(viewsets.GenericViewSet):
}
)
# Determine if this is a Switch session or a simple enroll and handle both.
try:
course_run_string = CourseKey.from_string(course_run_id)
course_run_key = CourseKey.from_string(course_run_id)
except InvalidKeyError:
return Response(
status=status.HTTP_400_BAD_REQUEST,
......@@ -328,10 +330,23 @@ class EntitlementEnrollmentViewSet(viewsets.GenericViewSet):
'message': 'Invalid {course_id}'.format(course_id=course_run_id)
}
)
# Verify that the run is fullfillable
if not is_course_run_entitlement_fullfillable(course_run_key, entitlement):
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
'message': 'The User is unable to enroll in Course Run {course_id}, it is not available.'.format(
course_id=course_run_id
)
}
)
# Determine if this is a Switch session or a simple enroll and handle both.
if entitlement.enrollment_course_run is None:
response = self._enroll_entitlement(
entitlement=entitlement,
course_run_key=course_run_string,
course_run_key=course_run_key,
user=request.user
)
if response:
......@@ -343,7 +358,7 @@ class EntitlementEnrollmentViewSet(viewsets.GenericViewSet):
)
response = self._enroll_entitlement(
entitlement=entitlement,
course_run_key=course_run_string,
course_run_key=course_run_key,
user=request.user
)
if response:
......
"""
Test entitlements utilities
"""
from datetime import timedelta
from django.conf import settings
from django.utils.timezone import now
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from openedx.core.djangolib.testing.utils import skip_unless_lms
from student.tests.factories import (TEST_PASSWORD, UserFactory, CourseOverviewFactory, CourseEnrollmentFactory)
# 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
@skip_unless_lms
class TestCourseRunFullfillableForEntitlement(ModuleStoreTestCase):
"""
Tests for the utility function is_course_run_entitlement_fullfillable
"""
def setUp(self):
super(TestCourseRunFullfillableForEntitlement, self).setUp()
self.user = UserFactory(is_staff=True)
self.client.login(username=self.user.username, password=TEST_PASSWORD)
def create_course(
self,
start_from_now,
end_from_now,
enrollment_start_from_now,
enrollment_end_from_now,
upgraded_ended_from_now=1
):
course_overview = CourseOverviewFactory.create(
start=now() + timedelta(days=start_from_now),
end=now() + timedelta(days=end_from_now),
enrollment_start=now() + timedelta(days=enrollment_start_from_now),
enrollment_end=now() + timedelta(days=enrollment_end_from_now)
)
CourseModeFactory(
course_id=course_overview.id,
mode_slug=CourseMode.VERIFIED,
# This must be in the future to ensure it is returned by downstream code.
expiration_datetime=now() + timedelta(days=upgraded_ended_from_now)
)
return course_overview
def test_course_run_fullfillble(self):
course_overview = self.create_course(
start_from_now=-2,
end_from_now=2,
enrollment_start_from_now=-1,
enrollment_end_from_now=1
)
entitlement = CourseEntitlementFactory.create(mode=CourseMode.VERIFIED)
assert is_course_run_entitlement_fullfillable(course_overview.id, entitlement)
def test_course_run_not_fullfillable_run_ended(self):
course_overview = self.create_course(
start_from_now=-3,
end_from_now=-1,
enrollment_start_from_now=-3,
enrollment_end_from_now=-2
)
entitlement = CourseEntitlementFactory.create(mode=CourseMode.VERIFIED)
assert not is_course_run_entitlement_fullfillable(course_overview.id, entitlement)
def test_course_run_not_fullfillable_enroll_period_ended(self):
course_overview = self.create_course(
start_from_now=-3,
end_from_now=2,
enrollment_start_from_now=-2,
enrollment_end_from_now=-1
)
entitlement = CourseEntitlementFactory.create(mode=CourseMode.VERIFIED)
assert not is_course_run_entitlement_fullfillable(course_overview.id, entitlement)
def test_course_run_fullfillable_user_enrolled(self):
course_overview = self.create_course(
start_from_now=-3,
end_from_now=2,
enrollment_start_from_now=-2,
enrollment_end_from_now=1
)
entitlement = CourseEntitlementFactory.create(mode=CourseMode.VERIFIED)
# 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)
def test_course_run_not_fullfillable_upgrade_ended(self):
course_overview = self.create_course(
start_from_now=-3,
end_from_now=2,
enrollment_start_from_now=-2,
enrollment_end_from_now=1,
upgraded_ended_from_now=-1
)
entitlement = CourseEntitlementFactory.create(mode=CourseMode.VERIFIED)
assert not is_course_run_entitlement_fullfillable(course_overview.id, entitlement)
from course_modes.models import CourseMode
from django.utils import timezone
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
def is_course_run_entitlement_fullfillable(course_run_id, entitlement, compare_date=timezone.now()):
"""
Checks that the current run meets the following criteria for an entitlement
1) Is currently running or start in the future
2) A User can enroll in
3) A User can upgrade to the entitlement mode
Arguments:
course_run_id (String): 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)
# Verify that the course is still running
run_start = course_overview.start
run_end = course_overview.end
is_running = run_start and (not run_end or (run_end and (run_end > compare_date)))
# Verify that the course run can currently be enrolled
enrollment_start = course_overview.enrollment_start
enrollment_end = course_overview.enrollment_end
can_enroll = (
(not enrollment_start or enrollment_start < compare_date)
and (not enrollment_end or enrollment_end > compare_date)
)
# 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)]
can_upgrade = unexpired_paid_modes and entitlement.mode in unexpired_paid_modes
return is_running and can_upgrade and can_enroll
......@@ -2,21 +2,22 @@
import copy
import datetime
import logging
import pycountry
from dateutil.parser import parse as datetime_parse
import pycountry
from django.conf import settings
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
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 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__)
......@@ -315,48 +316,16 @@ def get_fulfillable_course_runs_for_entitlement(entitlement, course_runs):
"""
Takes a list of course runs and returns only the course runs, sorted by start date, that:
1) Are currently running or in the future
2) A user can enroll in
3) A user can upgrade in
4) Are published
5) Are not enrolled in already for an active session
These are the only sessions that can be selected for an entitlement.
"""
enrollable_sessions = []
enrollments_for_user = CourseEnrollment.enrollments_for_user(entitlement.user).filter(mode=entitlement.mode)
enrolled_sessions = frozenset([str(e.course_id) for e in enrollments_for_user])
# Only show published course runs that can still be enrolled and upgraded
now = datetime.datetime.now(UTC)
search_time = datetime.datetime.now(UTC)
for course_run in course_runs:
# Only courses that have not ended will be displayed
run_start = course_run.get('start')
run_end = course_run.get('end')
is_running = run_start and (not run_end or datetime_parse(run_end) > now)
# Only courses that can currently be enrolled in will be displayed
enrollment_start = course_run.get('enrollment_start')
enrollment_end = course_run.get('enrollment_end')
can_enroll = ((not enrollment_start or datetime_parse(enrollment_start) < now)
and (not enrollment_end or datetime_parse(enrollment_end) > now)
and course_run.get('key') not in enrolled_sessions)
# Only upgrade-able courses will be displayed
can_upgrade = False
for seat in course_run.get('seats', []):
if seat.get('type') == entitlement.mode:
upgrade_deadline = seat.get('upgrade_deadline', None)
can_upgrade = not upgrade_deadline or (datetime_parse(upgrade_deadline) > now)
break
# Only published courses will be displayed
is_published = course_run.get('status') == 'published'
if is_running and can_upgrade and can_enroll and is_published:
course_id = CourseKey.from_string(course_run.get('key'))
is_enrolled = CourseEnrollment.is_enrolled(entitlement.user, str(course_id))
if is_course_run_entitlement_fullfillable(course_id, entitlement, search_time) and not is_enrolled:
enrollable_sessions.append(course_run)
enrollable_sessions.sort(key=lambda session: session.get('start'))
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment