Commit cf05b1f4 by Matthew Piatetsky

Add marketable_enrollable_course_runs_with_archived filter

ECOM-6820
parent 6ad30b26
...@@ -514,6 +514,10 @@ class CourseWithProgramsSerializer(CourseSerializer): ...@@ -514,6 +514,10 @@ class CourseWithProgramsSerializer(CourseSerializer):
# closed - achieving this requires applying both the marketable and active filters. # closed - achieving this requires applying both the marketable and active filters.
course_runs = course_runs.marketable().active() 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'): if self.context.get('published_course_runs_only'):
course_runs = course_runs.filter(status=CourseRunStatus.Published) course_runs = course_runs.filter(status=CourseRunStatus.Published)
......
...@@ -207,36 +207,27 @@ class CourseWithProgramsSerializerTests(CourseSerializerTests): ...@@ -207,36 +207,27 @@ class CourseWithProgramsSerializerTests(CourseSerializerTests):
Verify that the marketable_course_runs_only option is respected, restricting returned 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. 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( enrollable_course_run = CourseRunFactory(
status=CourseRunStatus.Published, status=CourseRunStatus.Published,
end=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=10), end=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=10),
enrollment_start=None, enrollment_start=None,
enrollment_end=None, enrollment_end=None,
course=self.course
) )
SeatFactory(course_run=enrollable_course_run) SeatFactory(course_run=enrollable_course_run)
# Unpublished course run with a seat. unpublished_course_run = CourseRunFactory(status=CourseRunStatus.Unpublished, course=self.course)
unpublished_course_run = CourseRunFactory(status=CourseRunStatus.Unpublished)
SeatFactory(course_run=unpublished_course_run) SeatFactory(course_run=unpublished_course_run)
# Published course run with no seats. CourseRunFactory(status=CourseRunStatus.Published, course=self.course)
no_seats_course_run = CourseRunFactory(status=CourseRunStatus.Published)
# Published course run with a seat and an end date in the past.
closed_course_run = CourseRunFactory( closed_course_run = CourseRunFactory(
status=CourseRunStatus.Published, status=CourseRunStatus.Published,
end=datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=10), end=datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=10),
course=self.course
) )
SeatFactory(course_run=closed_course_run) 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( serializer = self.serializer_class(
self.course, self.course,
context={'request': self.request, 'marketable_course_runs_only': marketable_course_runs_only} context={'request': self.request, 'marketable_course_runs_only': marketable_course_runs_only}
...@@ -247,6 +238,46 @@ class CourseWithProgramsSerializerTests(CourseSerializerTests): ...@@ -247,6 +238,46 @@ class CourseWithProgramsSerializerTests(CourseSerializerTests):
1 if marketable_course_runs_only else 4 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) @ddt.data(0, 1)
def test_published_course_runs_only(self, published_course_runs_only): def test_published_course_runs_only(self, published_course_runs_only):
""" """
......
...@@ -71,32 +71,27 @@ class CourseViewSetTests(SerializationMixin, APITestCase): ...@@ -71,32 +71,27 @@ class CourseViewSetTests(SerializationMixin, APITestCase):
end=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=10), end=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=10),
enrollment_start=None, enrollment_start=None,
enrollment_end=None, enrollment_end=None,
course=self.course
) )
SeatFactory(course_run=enrollable_course_run) SeatFactory(course_run=enrollable_course_run)
# Unpublished course run with a seat. # 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) SeatFactory(course_run=unpublished_course_run)
# Published course run with no seats. # 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. # Published course run with a seat and an end date in the past.
closed_course_run = CourseRunFactory( closed_course_run = CourseRunFactory(
status=CourseRunStatus.Published, status=CourseRunStatus.Published,
end=datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=10), end=datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=10),
course=self.course
) )
SeatFactory(course_run=closed_course_run) 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 = 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) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -109,16 +104,45 @@ class CourseViewSetTests(SerializationMixin, APITestCase): ...@@ -109,16 +104,45 @@ class CourseViewSetTests(SerializationMixin, APITestCase):
) )
@ddt.data(1, 0) @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): def test_get_include_published_course_run(self, published_course_runs_only):
""" """
Verify the endpoint returns hides unpublished programs if Verify the endpoint returns hides unpublished programs if
the 'published_course_runs_only' flag is set to True the 'published_course_runs_only' flag is set to True
""" """
published_course_run = CourseRunFactory(status=CourseRunStatus.Published) CourseRunFactory(status=CourseRunStatus.Published, course=self.course)
unpublished_course_run = CourseRunFactory(status=CourseRunStatus.Unpublished) CourseRunFactory(status=CourseRunStatus.Unpublished, course=self.course)
self.course.course_runs.add(published_course_run, unpublished_course_run)
url = reverse('api:v1:course-detail', kwargs={'key': self.course.key}) 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) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual( self.assertEqual(
......
...@@ -38,12 +38,10 @@ class CourseViewSet(viewsets.ReadOnlyModelViewSet): ...@@ -38,12 +38,10 @@ class CourseViewSet(viewsets.ReadOnlyModelViewSet):
def get_serializer_context(self, *args, **kwargs): def get_serializer_context(self, *args, **kwargs):
context = super().get_serializer_context(*args, **kwargs) context = super().get_serializer_context(*args, **kwargs)
context.update({ query_params = ['exclude_utm', 'include_deleted_programs', 'marketable_course_runs_only',
'exclude_utm': get_query_param(self.request, 'exclude_utm'), 'marketable_enrollable_course_runs_with_archived', 'published_course_runs_only']
'include_deleted_programs': get_query_param(self.request, 'include_deleted_programs'), for query_param in query_params:
'marketable_course_runs_only': get_query_param(self.request, 'marketable_course_runs_only'), context[query_param] = get_query_param(self.request, query_param)
'published_course_runs_only': get_query_param(self.request, 'published_course_runs_only'),
})
return context return context
...@@ -71,7 +69,14 @@ class CourseViewSet(viewsets.ReadOnlyModelViewSet): ...@@ -71,7 +69,14 @@ class CourseViewSet(viewsets.ReadOnlyModelViewSet):
multiple: false multiple: false
- name: marketable_course_runs_only - name: marketable_course_runs_only
description: Restrict returned course runs to those that are published, have seats, 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 required: false
type: integer type: integer
paramType: query paramType: query
......
...@@ -43,6 +43,27 @@ class CourseRunQuerySet(models.QuerySet): ...@@ -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): def marketable(self):
""" Returns CourseRuns that can be marketed to learners. """ Returns CourseRuns that can be marketed to learners.
......
...@@ -68,6 +68,22 @@ class CourseRunQuerySetTests(TestCase): ...@@ -68,6 +68,22 @@ class CourseRunQuerySetTests(TestCase):
self.assertEqual(set(CourseRun.objects.active()), {active_enrollment_end, active_no_enrollment_end}) 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): def test_marketable(self):
""" Verify the method filters CourseRuns to those with slugs. """ """ Verify the method filters CourseRuns to those with slugs. """
course_run = CourseRunFactory() 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