Commit 5cd39185 by Douglas Hall Committed by Douglas Hall

Add indexed has_enrollable_seats field to CourseRun model.

This field will make it possible for clients of the search/all
endpoint to filter CourseRun results to those that have enrollable
seats.

ENT-687
parent 9f228395
......@@ -56,7 +56,7 @@ COURSE_RUN_SEARCH_FIELDS = (
'enrollment_end', 'pacing_type', 'language', 'transcript_languages', 'marketing_url', 'content_type', 'org',
'number', 'seat_types', 'image_url', 'type', 'level_type', 'availability', 'published', 'partner', 'program_types',
'authoring_organization_uuids', 'subject_uuids', 'staff_uuids', 'mobile_available', 'logo_image_urls',
'aggregation_key', 'min_effort', 'max_effort', 'weeks_to_complete',
'aggregation_key', 'min_effort', 'max_effort', 'weeks_to_complete', 'has_enrollable_seats',
)
PROGRAM_FACET_FIELD_OPTIONS = {
......
......@@ -611,7 +611,7 @@ class MinimalProgramSerializerTests(TestCase):
courses = CourseFactory.create_batch(3)
for course in courses:
CourseRunFactory.create_batch(2, course=course, staff=[person], start=datetime.datetime.now())
CourseRunFactory.create_batch(2, course=course, staff=[person], start=datetime.datetime.now(pytz.UTC))
return ProgramFactory(
courses=courses,
......@@ -1248,6 +1248,7 @@ class CourseRunSearchSerializerTests(ElasticsearchTestMixin, TestCase):
'subject_uuids': get_uuids(course_run.subjects.all()),
'staff_uuids': get_uuids(course_run.staff.all()),
'aggregation_key': 'courserun:{}'.format(course_run.course.key),
'has_enrollable_seats': course_run.has_enrollable_seats,
}
assert serializer.data == expected
......
......@@ -3,6 +3,7 @@ import json
import urllib.parse
import ddt
import pytz
from django.urls import reverse
from haystack.query import SearchQuerySet
......@@ -173,7 +174,7 @@ class CourseRunSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT
def test_availability_faceting(self):
""" Verify the endpoint returns availability facets with the results. """
now = datetime.datetime.utcnow()
now = datetime.datetime.now(pytz.UTC)
archived = CourseRunFactory(course__partner=self.partner, start=now - datetime.timedelta(weeks=2),
end=now - datetime.timedelta(weeks=1), status=CourseRunStatus.Published)
current = CourseRunFactory(course__partner=self.partner, start=now - datetime.timedelta(weeks=2),
......@@ -401,7 +402,7 @@ class AggregateSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT
@ddt.data('start', '-start')
def test_results_ordered_by_start_date(self, ordering):
""" Verify the search results can be ordered by start date """
now = datetime.datetime.utcnow()
now = datetime.datetime.now(pytz.UTC)
archived = CourseRunFactory(course__partner=self.partner, start=now - datetime.timedelta(weeks=2))
current = CourseRunFactory(course__partner=self.partner, start=now - datetime.timedelta(weeks=1))
starting_soon = CourseRunFactory(course__partner=self.partner, start=now + datetime.timedelta(weeks=3))
......
......@@ -501,7 +501,7 @@ class CourseRun(TimeStampedModel):
return deadline
def enrollable_seats(self, types):
def enrollable_seats(self, types=None):
"""
Returns seats, of the given type(s), that can be enrolled in/purchased.
......@@ -523,6 +523,7 @@ class CourseRun(TimeStampedModel):
if self.enrollment_end and now > self.enrollment_end:
return enrollable_seats
types = types or Seat.SEAT_TYPES
for seat in self.seats.all():
if seat.type in types and (not seat.upgrade_deadline or now < seat.upgrade_deadline):
enrollable_seats.append(seat)
......@@ -530,6 +531,13 @@ class CourseRun(TimeStampedModel):
return enrollable_seats
@property
def has_enrollable_seats(self):
"""
Return a boolean indicating whether or not enrollable Seats are available for this CourseRun.
"""
return len(self.enrollable_seats()) > 0
@property
def image_url(self):
return self.course.image_url
......@@ -702,6 +710,8 @@ class Seat(TimeStampedModel):
PROFESSIONAL = 'professional'
CREDIT = 'credit'
SEAT_TYPES = [HONOR, AUDIT, VERIFIED, PROFESSIONAL, CREDIT]
# Seat types that may not be purchased without first purchasing another Seat type.
# EX: 'credit' seats may not be purchased without first purchasing a 'verified' Seat.
SEATS_WITH_PREREQUISITES = [CREDIT]
......
......@@ -170,6 +170,7 @@ class CourseRunIndex(BaseCourseIndex, indexes.Indexable):
has_enrollable_paid_seats = indexes.BooleanField(null=False)
paid_seat_enrollment_end = indexes.DateTimeField(null=True)
license = indexes.MultiValueField(model_attr='license', faceted=True)
has_enrollable_seats = indexes.BooleanField(model_attr='has_enrollable_seats', null=False)
def prepare_aggregation_key(self, obj):
# Aggregate CourseRuns by Course key since that is how we plan to dedup CourseRuns on the marketing site.
......
......@@ -5,6 +5,7 @@ from decimal import Decimal
import ddt
import mock
import pytest
import pytz
from dateutil.parser import parse
from django.conf import settings
from django.core.exceptions import ValidationError
......@@ -71,19 +72,26 @@ class CourseRunTests(TestCase):
course_run = factories.CourseRunFactory(start=None, end=None, enrollment_start=None, enrollment_end=None)
verified_seat = factories.SeatFactory(course_run=course_run, type=Seat.VERIFIED, upgrade_deadline=None)
professional_seat = factories.SeatFactory(course_run=course_run, type=Seat.PROFESSIONAL, upgrade_deadline=None)
factories.SeatFactory(course_run=course_run, type=Seat.HONOR, upgrade_deadline=None)
self.assertEqual(
course_run.enrollable_seats([Seat.VERIFIED, Seat.PROFESSIONAL]),
[verified_seat, professional_seat]
)
honor_seat = factories.SeatFactory(course_run=course_run, type=Seat.HONOR, upgrade_deadline=None)
assert course_run.enrollable_seats([Seat.VERIFIED, Seat.PROFESSIONAL]) == [verified_seat, professional_seat]
# The method should not care about the course run's start date.
course_run.start = datetime.datetime.utcnow() + datetime.timedelta(days=1)
course_run.start = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=1)
course_run.save()
self.assertEqual(
course_run.enrollable_seats([Seat.VERIFIED, Seat.PROFESSIONAL]),
[verified_seat, professional_seat]
)
assert course_run.enrollable_seats([Seat.VERIFIED, Seat.PROFESSIONAL]) == [verified_seat, professional_seat]
# Enrollable seats of any type should be returned when no type parameter is specified.
assert course_run.enrollable_seats() == [verified_seat, professional_seat, honor_seat]
def test_has_enrollable_seats(self):
""" Verify the expected value of has_enrollable_seats is returned. """
course_run = factories.CourseRunFactory(start=None, end=None, enrollment_start=None, enrollment_end=None)
factories.SeatFactory(course_run=course_run, type=Seat.VERIFIED, upgrade_deadline=None)
assert course_run.has_enrollable_seats is True
course_run.end = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=1)
course_run.save()
assert course_run.has_enrollable_seats is False
def test_str(self):
""" Verify casting an instance to a string returns a string containing the key and title. """
......@@ -564,8 +572,8 @@ class ProgramTests(TestCase):
def test_one_click_purchase_ineligible(self):
""" Verify that program is one click purchase ineligible. """
yesterday = datetime.datetime.utcnow() - datetime.timedelta(days=1)
tomorrow = datetime.datetime.utcnow() + datetime.timedelta(days=1)
yesterday = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=1)
tomorrow = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=1)
verified_seat_type, __ = SeatType.objects.get_or_create(name=Seat.VERIFIED)
program_type = factories.ProgramTypeFactory(applicable_seat_types=[verified_seat_type])
......@@ -786,7 +794,7 @@ class ProgramTests(TestCase):
course_run.course.save()
day_separation = 1
now = datetime.datetime.utcnow()
now = datetime.datetime.now(pytz.UTC)
for course_run in course_runs_same_course:
if set_all_dates or day_separation < 2:
......
import datetime
import urllib.parse
import pytz
from django.urls import reverse
from course_discovery.apps.api.v1.tests.test_views.mixins import APITestCase
......@@ -78,7 +79,7 @@ class DistinctCountsAggregateSearchViewSetTests(SerializationMixin, LoginMixin,
def test_query_facet_response(self):
""" Verify that query facets are included in the response and that they are properly formatted."""
now = datetime.datetime.now()
now = datetime.datetime.now(pytz.UTC)
current = (now - datetime.timedelta(days=1), now + datetime.timedelta(days=1))
starting_soon = (now + datetime.timedelta(days=1), now + datetime.timedelta(days=2))
upcoming = (now + datetime.timedelta(days=61), now + datetime.timedelta(days=62))
......@@ -130,7 +131,7 @@ class DistinctCountsAggregateSearchViewSetTests(SerializationMixin, LoginMixin,
def test_response_with_search_query(self):
""" Verify that the response is accurate when a search query is passed."""
now = datetime.datetime.now()
now = datetime.datetime.now(pytz.UTC)
current = (now - datetime.timedelta(days=1), now + datetime.timedelta(days=1))
course = CourseFactory(partner=self.partner)
......@@ -189,7 +190,7 @@ class DistinctCountsAggregateSearchViewSetTests(SerializationMixin, LoginMixin,
def test_selected_field_facet(self):
""" Verify that the response is accurate when a field facet is selected."""
now = datetime.datetime.now()
now = datetime.datetime.now(pytz.UTC)
current = (now - datetime.timedelta(days=1), now + datetime.timedelta(days=1))
archived = (now - datetime.timedelta(days=2), now - datetime.timedelta(days=1))
......@@ -226,7 +227,7 @@ class DistinctCountsAggregateSearchViewSetTests(SerializationMixin, LoginMixin,
def test_selected_query_facet(self):
""" Verify that the response is accurate when a query facet is selected."""
now = datetime.datetime.now()
now = datetime.datetime.now(pytz.UTC)
current = (now - datetime.timedelta(days=1), now + datetime.timedelta(days=1))
archived = (now - datetime.timedelta(days=2), now - datetime.timedelta(days=1))
......
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