diff --git a/courses/migrations/0044_alter_courserun_live.py b/courses/migrations/0044_alter_courserun_live.py new file mode 100644 index 0000000000..632f203d87 --- /dev/null +++ b/courses/migrations/0044_alter_courserun_live.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.20 on 2023-09-15 12:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("courses", "0043_course_program_live_index"), + ] + + operations = [ + migrations.AlterField( + model_name="courserun", + name="live", + field=models.BooleanField(db_index=True, default=False), + ), + ] diff --git a/courses/models.py b/courses/models.py index f0eb360633..ae2e8d88f1 100644 --- a/courses/models.py +++ b/courses/models.py @@ -632,7 +632,7 @@ class CourseRun(TimestampedModel): help_text="The date beyond which the learner can not enroll in paid course mode.", ) - live = models.BooleanField(default=False) + live = models.BooleanField(default=False, db_index=True) is_self_paced = models.BooleanField(default=False) products = GenericRelation( "ecommerce.Product", related_query_name="courserunproducts" diff --git a/courses/views/v1/__init__.py b/courses/views/v1/__init__.py index a79b530b7c..2188214031 100644 --- a/courses/views/v1/__init__.py +++ b/courses/views/v1/__init__.py @@ -1,11 +1,12 @@ """Course views verson 1""" import logging +import django_filters from typing import Optional, Tuple, Union from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.db import transaction -from django.db.models import Q, Count +from django.db.models import Q, Count, Prefetch from django.http import HttpResponse, HttpResponseRedirect from django.urls import reverse from django_filters.rest_framework import DjangoFilterBackend @@ -58,6 +59,7 @@ USER_MSG_TYPE_ENROLLED, ) from main.utils import encode_json_cookie_value +from mitol.common.utils import now_in_utc from openedx.api import ( subscribe_to_edx_course_emails, sync_enrollments_with_edx, @@ -102,6 +104,44 @@ def paginate_queryset(self, queryset): return super().paginate_queryset(queryset) +class CourseFilterSet(django_filters.FilterSet): + + courserun_is_enrollable = django_filters.BooleanFilter( + field_name="courserun_is_enrollable", + method="filter_courserun_is_enrollable", + ) + + def filter_courserun_is_enrollable(self, queryset, _, value): + """ + courserun_is_enrollable filter to narrow down runs that are open for + enrollments + """ + now = now_in_utc() + + if value is True: + enrollable_runs = CourseRun.objects.filter( + Q(live=True) + & Q(start_date__isnull=False) + & Q(enrollment_start__lt=now) + & (Q(enrollment_end=None) | Q(enrollment_end__gt=now)) + ) + return queryset.prefetch_related( + Prefetch("courseruns", queryset=enrollable_runs) + ).filter(courseruns__id__in=enrollable_runs.values_list("id", flat=True)) + + else: + unenrollable_runs = CourseRun.objects.filter( + Q(live=False) | Q(start_date__isnull=True) | Q(enrollment_end__lte=now) + ) + return queryset.prefetch_related( + Prefetch("courseruns", queryset=unenrollable_runs) + ).filter(courseruns__id__in=unenrollable_runs.values_list("id", flat=True)) + + class Meta: + model = Course + fields = ["id", "live", "readable_id", "page__live", "courserun_is_enrollable"] + + class CourseViewSet(viewsets.ReadOnlyModelViewSet): """API view set for Courses""" @@ -109,9 +149,20 @@ class CourseViewSet(viewsets.ReadOnlyModelViewSet): permission_classes = [] filter_backends = [DjangoFilterBackend] serializer_class = CourseWithCourseRunsSerializer - filterset_fields = ["id", "live", "readable_id"] + filterset_class = CourseFilterSet def get_queryset(self): + courserun_is_enrollable = self.request.query_params.get( + "courserun_is_enrollable", None + ) + + if courserun_is_enrollable: + return ( + Course.objects.filter() + .select_related("page") + .prefetch_related("departments") + .all() + ) return ( Course.objects.filter() .select_related("page") diff --git a/frontend/public/src/containers/pages/CatalogPage_test.js b/frontend/public/src/containers/pages/CatalogPage_test.js index 611e7ccc90..75d2f7172e 100644 --- a/frontend/public/src/containers/pages/CatalogPage_test.js +++ b/frontend/public/src/containers/pages/CatalogPage_test.js @@ -786,7 +786,7 @@ describe("CatalogPage", function() { sinon.assert.calledWith( helper.handleRequestStub, - "/api/courses/?page=2&live=true", + "/api/courses/?page=2&live=true&page__live=true&courserun_is_enrollable=true", "GET" ) diff --git a/frontend/public/src/lib/queries/courses.js b/frontend/public/src/lib/queries/courses.js index e3f8ee21ca..65294d934c 100644 --- a/frontend/public/src/lib/queries/courses.js +++ b/frontend/public/src/lib/queries/courses.js @@ -14,7 +14,7 @@ export const coursesQueryKey = "courses" export const coursesQuery = page => ({ queryKey: coursesQueryKey, - url: `/api/courses/?page=${page}&live=true`, + url: `/api/courses/?page=${page}&live=true&page__live=true&courserun_is_enrollable=true`, transform: json => ({ courses: json }),