Merge pull request #17347 from edx/revert-17308-aj/LEARNER-3800
Revert "Aj/learner 3800"
This commit is contained in:
@@ -350,20 +350,6 @@ 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)}
|
||||
@@ -371,7 +357,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, mode=CourseMode.VERIFIED)
|
||||
course_entitlement = CourseEntitlementFactory.create(user=self.user)
|
||||
mock_get_course_runs.return_value = self.return_values
|
||||
url = reverse(
|
||||
self.ENTITLEMENTS_ENROLLMENT_NAMESPACE,
|
||||
@@ -395,7 +381,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, mode=CourseMode.VERIFIED)
|
||||
course_entitlement = CourseEntitlementFactory.create(user=self.user)
|
||||
mock_get_course_runs.return_value = self.return_values
|
||||
|
||||
url = reverse(
|
||||
@@ -430,7 +416,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, mode=CourseMode.VERIFIED)
|
||||
course_entitlement = CourseEntitlementFactory.create(user=self.user)
|
||||
|
||||
url = reverse(
|
||||
self.ENTITLEMENTS_ENROLLMENT_NAMESPACE,
|
||||
@@ -467,7 +453,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, mode=CourseMode.VERIFIED)
|
||||
course_entitlement = CourseEntitlementFactory.create(user=self.user)
|
||||
mock_get_course_runs.return_value = self.return_values
|
||||
|
||||
url = reverse(
|
||||
@@ -488,13 +474,16 @@ 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, mode=CourseMode.VERIFIED)
|
||||
course_entitlement = CourseEntitlementFactory.create(user=self.user)
|
||||
mock_get_course_runs.return_value = self.return_values
|
||||
|
||||
url = reverse(
|
||||
@@ -519,7 +508,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, mode=CourseMode.VERIFIED)
|
||||
course_entitlement = CourseEntitlementFactory.create(user=self.user)
|
||||
mock_get_course_runs.return_value = self.return_values
|
||||
|
||||
url = reverse(
|
||||
@@ -566,7 +555,7 @@ class EntitlementEnrollmentViewSetTest(ModuleStoreTestCase):
|
||||
mock_refund_entitlement,
|
||||
mock_is_refundable
|
||||
):
|
||||
course_entitlement = CourseEntitlementFactory.create(user=self.user, mode=CourseMode.VERIFIED)
|
||||
course_entitlement = CourseEntitlementFactory.create(user=self.user)
|
||||
mock_get_course_runs.return_value = self.return_values
|
||||
|
||||
url = reverse(
|
||||
@@ -610,7 +599,7 @@ class EntitlementEnrollmentViewSetTest(ModuleStoreTestCase):
|
||||
mock_refund_entitlement,
|
||||
mock_is_refundable
|
||||
):
|
||||
course_entitlement = CourseEntitlementFactory.create(user=self.user, mode=CourseMode.VERIFIED)
|
||||
course_entitlement = CourseEntitlementFactory.create(user=self.user)
|
||||
mock_get_course_runs.return_value = self.return_values
|
||||
|
||||
url = reverse(
|
||||
|
||||
@@ -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,6 @@ 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
|
||||
@@ -321,8 +318,9 @@ class EntitlementEnrollmentViewSet(viewsets.GenericViewSet):
|
||||
}
|
||||
)
|
||||
|
||||
# Determine if this is a Switch session or a simple enroll and handle both.
|
||||
try:
|
||||
course_run_key = CourseKey.from_string(course_run_id)
|
||||
course_run_string = CourseKey.from_string(course_run_id)
|
||||
except InvalidKeyError:
|
||||
return Response(
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -330,23 +328,10 @@ 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_key,
|
||||
course_run_key=course_run_string,
|
||||
user=request.user
|
||||
)
|
||||
if response:
|
||||
@@ -358,7 +343,7 @@ class EntitlementEnrollmentViewSet(viewsets.GenericViewSet):
|
||||
)
|
||||
response = self._enroll_entitlement(
|
||||
entitlement=entitlement,
|
||||
course_run_key=course_run_key,
|
||||
course_run_key=course_run_string,
|
||||
user=request.user
|
||||
)
|
||||
if response:
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
"""
|
||||
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)
|
||||
@@ -1,42 +0,0 @@
|
||||
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,22 +2,21 @@
|
||||
import copy
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
import pycountry
|
||||
|
||||
from dateutil.parser import parse as datetime_parse
|
||||
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__)
|
||||
|
||||
@@ -316,16 +315,48 @@ 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
|
||||
search_time = datetime.datetime.now(UTC)
|
||||
now = datetime.datetime.now(UTC)
|
||||
for course_run in course_runs:
|
||||
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:
|
||||
|
||||
# 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:
|
||||
enrollable_sessions.append(course_run)
|
||||
|
||||
enrollable_sessions.sort(key=lambda session: session.get('start'))
|
||||
|
||||
Reference in New Issue
Block a user