diff --git a/cms/api.py b/cms/api.py index 4704dff3d..72c5e658c 100644 --- a/cms/api.py +++ b/cms/api.py @@ -5,19 +5,84 @@ from datetime import MAXYEAR, UTC, datetime from django.contrib.contenttypes.models import ContentType +from django.db.models import Prefetch, Q from wagtail.models import Page, Site from cms import models as cms_models from cms.constants import CERTIFICATE_INDEX_SLUG, ENTERPRISE_PAGE_SLUG +from mitxpro.utils import now_in_utc log = logging.getLogger(__name__) DEFAULT_HOMEPAGE_PROPS = dict(title="Home Page", subhead="This is the home page") # noqa: C408 DEFAULT_SITE_PROPS = dict(hostname="localhost", port=80) # noqa: C408 -def filter_and_sort_catalog_pages( - program_pages, course_pages, external_course_pages, external_program_pages -): +def filter_program_pages(is_external=False): # noqa: FBT002 + """Filter the internal and external program objects""" + now = now_in_utc() + program_page_cls = cms_models.ProgramPage + prefetch_type = "coursepage" + if is_external: + program_page_cls = cms_models.ExternalProgramPage + prefetch_type = "externalcoursepage" + + return ( + program_page_cls.objects.live() + .filter( + ( + Q( + program__courses__courseruns__start_date__isnull=False, + program__courses__courseruns__start_date__gte=now, + ) + | Q( + program__courses__courseruns__enrollment_end__isnull=False, + program__courses__courseruns__enrollment_end__gte=now, + ) + ), + program__live=True, + ) + .order_by("id") + .select_related("program") + .prefetch_related( + Prefetch( + "program__courses", + cms_models.Course.objects.order_by( + "position_in_program" + ).select_related(prefetch_type), + ), + ) + ) + + +def filter_course_pages(is_external=False): # noqa: FBT002 + """Filter the internal and external course pages""" + now = now_in_utc() + course_page_cls = ( + cms_models.CoursePage if is_external else cms_models.ExternalCoursePage + ) + + return ( + course_page_cls.objects.live() + .filter( + ( + Q( + course__courseruns__start_date__isnull=False, + course__courseruns__start_date__gte=now, + ) + | Q( + course__courseruns__enrollment_end__isnull=False, + course__courseruns__enrollment_end__gte=now, + ) + ), + course__live=True, + ) + .order_by("id") + .select_related("course") + .distinct() + ) + + +def filter_and_sort_catalog_pages(program_pages, course_pages): """ Filters program and course pages to only include those that should be visible in the catalog, then returns a tuple of sorted lists of pages @@ -32,27 +97,18 @@ def filter_and_sort_catalog_pages( tuple of (list of Pages): A tuple containing a list of combined ProgramPages, CoursePages, ExternalCoursePages and ExternalProgramPages, a list of ProgramPages and ExternalProgramPages, and a list of CoursePages and ExternalCoursePages, all sorted by the next course/program run date and title """ - all_program_pages = program_pages + external_program_pages - all_course_pages = course_pages + external_course_pages - - valid_program_pages = [ - page for page in all_program_pages if page.product.is_catalog_visible - ] - valid_course_pages = [ - page for page in all_course_pages if page.product.is_catalog_visible - ] page_run_dates = { page: page.product.next_run_date or datetime(year=MAXYEAR, month=1, day=1, tzinfo=UTC) for page in itertools.chain( - valid_program_pages, - valid_course_pages, + program_pages, + course_pages, ) } return ( sorted( - valid_program_pages + valid_course_pages, + program_pages + course_pages, # ProgramPages with the same next run date as a CoursePage should be sorted first key=lambda page: ( page_run_dates[page], @@ -61,11 +117,11 @@ def filter_and_sort_catalog_pages( ), ), sorted( - valid_program_pages, + program_pages, key=lambda page: (page_run_dates[page], page.title), ), sorted( - valid_course_pages, + course_pages, key=lambda page: (page_run_dates[page], page.title), ), ) diff --git a/cms/models.py b/cms/models.py index 71aa94dad..f6da861c5 100644 --- a/cms/models.py +++ b/cms/models.py @@ -44,7 +44,11 @@ from wagtailmetadata.models import MetadataPageMixin from blog.api import fetch_blog -from cms.api import filter_and_sort_catalog_pages +from cms.api import ( + filter_and_sort_catalog_pages, + filter_course_pages, + filter_program_pages, +) from cms.blocks import ( BannerHeadingBlock, CourseRunCertificateOverrides, @@ -511,31 +515,11 @@ def get_context(self, request, *args, **kwargs): # noqa: ARG002 Populate the context with live programs, courses and programs + courses """ topic_filter = request.GET.get("topic", ALL_TOPICS) - program_page_qset = ( - ProgramPage.objects.live() - .filter(program__live=True) - .order_by("id") - .select_related("program") - .prefetch_related( - Prefetch( - "program__courses", - Course.objects.order_by("position_in_program").select_related( - "coursepage" - ), - ), - ) - ) - external_program_qset = ExternalProgramPage.objects.live().order_by("title") + program_page_qset = filter_program_pages() + external_program_qset = filter_program_pages(is_external=True) - course_page_qset = ( - CoursePage.objects.live() - .filter(course__live=True) - .order_by("id") - .select_related("course") - ) - external_course_qset = ( - ExternalCoursePage.objects.live().select_related("course").order_by("title") - ) + course_page_qset = filter_course_pages() + external_course_qset = filter_course_pages(is_external=True) if topic_filter != ALL_TOPICS: program_page_qset = program_page_qset.related_pages(topic_filter) @@ -544,27 +528,21 @@ def get_context(self, request, *args, **kwargs): # noqa: ARG002 course_page_qset = course_page_qset.related_pages(topic_filter) external_course_qset = external_course_qset.related_pages(topic_filter) - program_page_qset = list(program_page_qset) - external_program_qset = list(external_program_qset) - course_page_qset = list(course_page_qset) - external_course_qset = list(external_course_qset) + program_page_qset = list(program_page_qset) + list(external_program_qset) + course_page_qset = list(course_page_qset) + list(external_course_qset) # prefetch thumbnail images for all the pages in one query prefetch_related_objects( [ *program_page_qset, *course_page_qset, - *external_course_qset, - *external_program_qset, ], "thumbnail_image", ) - programs = [ - page.program for page in [*program_page_qset, *external_program_qset] - ] + programs = [page.program for page in [*program_page_qset]] courses = [ - *[page.course for page in [*course_page_qset, *external_course_qset]], + *[page.course for page in [*course_page_qset]], *[course for program in programs for course in program.courses.all()], ] @@ -596,10 +574,7 @@ def get_context(self, request, *args, **kwargs): # noqa: ARG002 ) all_pages, program_pages, course_pages = filter_and_sort_catalog_pages( - program_page_qset, - course_page_qset, - external_course_qset, - external_program_qset, + program_page_qset, course_page_qset ) return dict( **super().get_context(request), diff --git a/cms/templates/partials/card_details_top.html b/cms/templates/partials/card_details_top.html index 00711c226..e9366b9d2 100644 --- a/cms/templates/partials/card_details_top.html +++ b/cms/templates/partials/card_details_top.html @@ -37,12 +37,7 @@

{{ courseware_page.duration }} {% endif %} - {% if courseware_page.is_external_page and courseware_page.next_run_date %} -
  • - Next Start Date: - {{ courseware_page.next_run_date|date:"F j, Y" }} -
  • - {% elif courseware_page.product.next_run_date %} + {% if courseware_page.product.next_run_date %}
  • Next Start Date: {{ courseware_page.product.next_run_date|date:"F j, Y" }} diff --git a/courses/views/v1/__init__.py b/courses/views/v1/__init__.py index 0ee5dd328..b728bee6f 100644 --- a/courses/views/v1/__init__.py +++ b/courses/views/v1/__init__.py @@ -28,11 +28,13 @@ ProgramSerializer, ) from ecommerce.models import Product +from mitxpro.utils import now_in_utc class ProgramViewSet(viewsets.ReadOnlyModelViewSet): """API view set for Programs""" + now = now_in_utc() products_prefetch = Prefetch("products", Product.objects.with_ordered_versions()) course_runs_prefetch = Prefetch( "courseruns", CourseRun.objects.prefetch_related(products_prefetch) @@ -49,7 +51,19 @@ class ProgramViewSet(viewsets.ReadOnlyModelViewSet): permission_classes = [] serializer_class = ProgramSerializer queryset = ( - Program.objects.filter(live=True) + Program.objects.filter( + ( + Q( + courses__courseruns__start_date__isnull=False, + courses__courseruns__start_date__gte=now, + ) + | Q( + courses__courseruns__enrollment_end__isnull=False, + courses__courseruns__enrollment_end__gte=now, + ) + ), + live=True, + ) .exclude(products=None) .select_related("programpage", "externalprogrampage", "platform") .prefetch_related(courses_prefetch, products_prefetch) @@ -69,8 +83,21 @@ class CourseViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = CourseSerializer def get_queryset(self): + now = now_in_utc() queryset = ( - Course.objects.filter(live=True) + Course.objects.filter( + ( + Q( + courseruns__start_date__isnull=False, + courseruns__start_date__gte=now, + ) + | Q( + courseruns__enrollment_end__isnull=False, + courseruns__enrollment_end__gte=now, + ) + ), + live=True, + ) .select_related("coursepage", "externalcoursepage", "platform") .prefetch_related( "coursepage__topics",