Commit cf05b1f4 by Matthew Piatetsky

Add marketable_enrollable_course_runs_with_archived filter

ECOM-6820
parent 6ad30b26
......@@ -514,6 +514,10 @@ class CourseWithProgramsSerializer(CourseSerializer):
# closed - achieving this requires applying both the marketable and active filters.
course_runs = course_runs.marketable().active()
if self.context.get('marketable_enrollable_course_runs_with_archived'):
# Same as "marketable_course_runs_only", but includes courses with an end date in the past
course_runs = course_runs.marketable().enrollable()
if self.context.get('published_course_runs_only'):
course_runs = course_runs.filter(status=CourseRunStatus.Published)
......
......@@ -207,36 +207,27 @@ class CourseWithProgramsSerializerTests(CourseSerializerTests):
Verify that the marketable_course_runs_only option is respected, restricting returned
course runs to those that are published, have seats, and can still be enrolled in.
"""
# Published course run with a seat, no enrollment start or end, and an end date in the future.
enrollable_course_run = CourseRunFactory(
status=CourseRunStatus.Published,
end=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=10),
enrollment_start=None,
enrollment_end=None,
course=self.course
)
SeatFactory(course_run=enrollable_course_run)
# Unpublished course run with a seat.
unpublished_course_run = CourseRunFactory(status=CourseRunStatus.Unpublished)
unpublished_course_run = CourseRunFactory(status=CourseRunStatus.Unpublished, course=self.course)
SeatFactory(course_run=unpublished_course_run)
# Published course run with no seats.
no_seats_course_run = CourseRunFactory(status=CourseRunStatus.Published)
CourseRunFactory(status=CourseRunStatus.Published, course=self.course)
# Published course run with a seat and an end date in the past.
closed_course_run = CourseRunFactory(
status=CourseRunStatus.Published,
end=datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=10),
course=self.course
)
SeatFactory(course_run=closed_course_run)
self.course.course_runs.add(
enrollable_course_run,
unpublished_course_run,
no_seats_course_run,
closed_course_run,
)
serializer = self.serializer_class(
self.course,
context={'request': self.request, 'marketable_course_runs_only': marketable_course_runs_only}
......@@ -247,6 +238,46 @@ class CourseWithProgramsSerializerTests(CourseSerializerTests):
1 if marketable_course_runs_only else 4
)
def test_marketable_enrollable_course_runs_with_archived(self):
"""
Verify that the marketable_enrollable_course_runs_with_archived option is respected, restricting returned
course runs to those that are published, have seats, and can still be enrolled in
(including courses with an end date in the past.)
"""
enrollable_course_run = CourseRunFactory(
status=CourseRunStatus.Published,
end=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=10),
enrollment_start=None,
enrollment_end=None,
course=self.course
)
unpublished_course_run = CourseRunFactory(status=CourseRunStatus.Unpublished, course=self.course)
CourseRunFactory(status=CourseRunStatus.Published, course=self.course)
archived_course_run = CourseRunFactory(
status=CourseRunStatus.Published,
end=datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=10),
enrollment_start=None,
enrollment_end=None,
course=self.course
)
SeatFactory(course_run=unpublished_course_run)
SeatFactory(course_run=enrollable_course_run)
SeatFactory(course_run=archived_course_run)
context = {
'request': self.request,
'marketable_enrollable_course_runs_with_archived': 1
}
course_serializer = self.serializer_class(
self.course,
context=context
)
course_run_keys = [course_run['key'] for course_run in course_serializer.data['course_runs']]
# order doesn't matter
assert sorted(course_run_keys) == sorted([enrollable_course_run.key, archived_course_run.key])
@ddt.data(0, 1)
def test_published_course_runs_only(self, published_course_runs_only):
"""
......
......@@ -71,32 +71,27 @@ class CourseViewSetTests(SerializationMixin, APITestCase):
end=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=10),
enrollment_start=None,
enrollment_end=None,
course=self.course
)
SeatFactory(course_run=enrollable_course_run)
# Unpublished course run with a seat.
unpublished_course_run = CourseRunFactory(status=CourseRunStatus.Unpublished)
unpublished_course_run = CourseRunFactory(status=CourseRunStatus.Unpublished, course=self.course)
SeatFactory(course_run=unpublished_course_run)
# Published course run with no seats.
no_seats_course_run = CourseRunFactory(status=CourseRunStatus.Published)
CourseRunFactory(status=CourseRunStatus.Published, course=self.course)
# Published course run with a seat and an end date in the past.
closed_course_run = CourseRunFactory(
status=CourseRunStatus.Published,
end=datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=10),
course=self.course
)
SeatFactory(course_run=closed_course_run)
self.course.course_runs.add(
enrollable_course_run,
unpublished_course_run,
no_seats_course_run,
closed_course_run,
)
url = reverse('api:v1:course-detail', kwargs={'key': self.course.key})
url += '?marketable_course_runs_only={}'.format(marketable_course_runs_only)
url = '{}?marketable_course_runs_only={}'.format(url, marketable_course_runs_only)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
......@@ -109,16 +104,45 @@ class CourseViewSetTests(SerializationMixin, APITestCase):
)
@ddt.data(1, 0)
def test_marketable_enrollable_course_runs_with_archived(self, marketable_enrollable_course_runs_with_archived):
""" Verify the endpoint filters course runs to those that are marketable and
enrollable, including archived course runs (with an end date in the past). """
past = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=2)
future = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=2)
CourseRunFactory(enrollment_start=None, enrollment_end=future, course=self.course)
CourseRunFactory(enrollment_start=None, enrollment_end=None, course=self.course)
CourseRunFactory(
enrollment_start=past, enrollment_end=future, course=self.course
)
CourseRunFactory(enrollment_start=future, course=self.course)
CourseRunFactory(enrollment_end=past, course=self.course)
url = reverse('api:v1:course-detail', kwargs={'key': self.course.key})
url = '{}?marketable_enrollable_course_runs_with_archived={}'.format(
url, marketable_enrollable_course_runs_with_archived
)
response = self.client.get(url)
assert response.status_code == 200
assert response.data == self.serialize_course(
self.course,
extra_context={
'marketable_enrollable_course_runs_with_archived': marketable_enrollable_course_runs_with_archived
}
)
@ddt.data(1, 0)
def test_get_include_published_course_run(self, published_course_runs_only):
"""
Verify the endpoint returns hides unpublished programs if
the 'published_course_runs_only' flag is set to True
"""
published_course_run = CourseRunFactory(status=CourseRunStatus.Published)
unpublished_course_run = CourseRunFactory(status=CourseRunStatus.Unpublished)
self.course.course_runs.add(published_course_run, unpublished_course_run)
CourseRunFactory(status=CourseRunStatus.Published, course=self.course)
CourseRunFactory(status=CourseRunStatus.Unpublished, course=self.course)
url = reverse('api:v1:course-detail', kwargs={'key': self.course.key})
url += '?published_course_runs_only={}'.format(published_course_runs_only)
url = '{}?published_course_runs_only={}'.format(url, published_course_runs_only)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(
......
......@@ -38,12 +38,10 @@ class CourseViewSet(viewsets.ReadOnlyModelViewSet):
def get_serializer_context(self, *args, **kwargs):
context = super().get_serializer_context(*args, **kwargs)
context.update({
'exclude_utm': get_query_param(self.request, 'exclude_utm'),
'include_deleted_programs': get_query_param(self.request, 'include_deleted_programs'),
'marketable_course_runs_only': get_query_param(self.request, 'marketable_course_runs_only'),
'published_course_runs_only': get_query_param(self.request, 'published_course_runs_only'),
})
query_params = ['exclude_utm', 'include_deleted_programs', 'marketable_course_runs_only',
'marketable_enrollable_course_runs_with_archived', 'published_course_runs_only']
for query_param in query_params:
context[query_param] = get_query_param(self.request, query_param)
return context
......@@ -71,7 +69,14 @@ class CourseViewSet(viewsets.ReadOnlyModelViewSet):
multiple: false
- name: marketable_course_runs_only
description: Restrict returned course runs to those that are published, have seats,
and can still be enrolled in.
and are enrollable or will be enrollable in the future
required: false
type: integer
paramType: query
mulitple: false
- name: marketable_enrollable_course_runs_with_archived
description: Restrict returned course runs to those that are published, have seats,
and can be enrolled in now. Includes archived courses.
required: false
type: integer
paramType: query
......
......@@ -43,6 +43,27 @@ class CourseRunQuerySet(models.QuerySet):
)
)
def enrollable(self):
""" Returns course runs that are currently open for enrollment.
A course run is considered open for enrollment if its enrollment start date
has passed, is now or is None, AND its enrollment end date is in the future or is None.
Returns:
QuerySet
"""
now = datetime.datetime.now(pytz.UTC)
return self.filter(
(
Q(enrollment_end__gt=now) |
Q(enrollment_end__isnull=True)
) & (
Q(enrollment_start__lte=now) |
Q(enrollment_start__isnull=True)
)
)
def marketable(self):
""" Returns CourseRuns that can be marketed to learners.
......
......@@ -68,6 +68,22 @@ class CourseRunQuerySetTests(TestCase):
self.assertEqual(set(CourseRun.objects.active()), {active_enrollment_end, active_no_enrollment_end})
def test_enrollable(self):
""" Verify the method returns only course runs currently open for enrollment. """
past = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=2)
future = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=2)
enrollable = CourseRunFactory(enrollment_start=past, enrollment_end=future)
enrollable_no_enrollment_end = CourseRunFactory(enrollment_start=past, enrollment_end=None)
enrollable_no_enrollment_start = CourseRunFactory(enrollment_start=None, enrollment_end=future)
CourseRunFactory(enrollment_start=future)
CourseRunFactory(enrollment_end=past)
# order doesn't matter
assert list(CourseRun.objects.enrollable().order_by('id')) == sorted([
enrollable, enrollable_no_enrollment_end, enrollable_no_enrollment_start
], key=lambda x: x.id)
def test_marketable(self):
""" Verify the method filters CourseRuns to those with slugs. """
course_run = CourseRunFactory()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment