diff --git a/cms/api.py b/cms/api.py index 73126ba32..3b32fedca 100644 --- a/cms/api.py +++ b/cms/api.py @@ -82,7 +82,11 @@ def filter_and_sort_catalog_pages( page.product.current_price, page.title, ) - default_sorting_key = lambda page: (page_run_dates[page], page.title) # noqa: E731 + default_sorting_key = lambda page: ( # noqa: E731 + page.language.priority, + page_run_dates[page], + page.title, + ) # Best Match and Start Date sorting has same logic sorting_key_map = defaultdict( diff --git a/cms/api_test.py b/cms/api_test.py index 52733e6f4..0b2b56b80 100644 --- a/cms/api_test.py +++ b/cms/api_test.py @@ -85,7 +85,6 @@ def test_filter_and_sort_catalog_pages_with_default_sorting(sort_by): initial_program_pages = [ run.course.program.page for run in [second_program_run, first_program_run] ] - all_pages, program_pages, course_pages = filter_and_sort_catalog_pages( initial_program_pages, initial_course_pages, @@ -114,13 +113,13 @@ def test_filter_and_sort_catalog_pages_with_default_sorting(sort_by): assert past_run.course not in ( None if page.is_external_course_page else page.course for page in course_pages ) - - # Pages should be sorted by next run date + # Pages should be sorted by language and then next run date (When language priority is the same) assert [page.program for page in program_pages] == [ first_program_run.course.program, second_program_run.course.program, later_external_program_page.program, ] + expected_course_run_sort = [ non_program_run, first_program_run, @@ -135,6 +134,41 @@ def test_filter_and_sort_catalog_pages_with_default_sorting(sort_by): run.course for run in expected_course_run_sort ] + # Pages should be sorted by language then next run date (When language priority is the different) + first_program_run.course.program.page.language.priority = 2 + first_program_run.course.program.page.save() + second_program_run.course.program.page.language.priority = 1 + second_program_run.course.program.page.save() + later_external_program_page.language.priority = 3 + later_external_program_page.save() + + all_pages, program_pages, course_pages = filter_and_sort_catalog_pages( + initial_program_pages, + initial_course_pages, + external_course_pages, + external_program_pages, + sort_by, + ) + + assert [page.program for page in program_pages] == [ + second_program_run.course.program, + first_program_run.course.program, + later_external_program_page.program, + ] + + expected_course_run_sort = [ + non_program_run, + first_program_run, + second_program_run, + later_external_course_page, + future_enrollment_end_run, + earlier_external_course_page, + ] + # The sort should also include external course pages as expected + assert [page.course for page in course_pages] == [ + run.course for run in expected_course_run_sort + ] + @pytest.mark.parametrize( ( diff --git a/cms/factories.py b/cms/factories.py index 623ff5136..a39403dc4 100644 --- a/cms/factories.py +++ b/cms/factories.py @@ -56,7 +56,12 @@ WebinarPage, WhoShouldEnrollPage, ) -from courses.factories import CourseFactory, PlatformFactory, ProgramFactory +from courses.factories import ( + CourseFactory, + CourseLanguageFactory, + PlatformFactory, + ProgramFactory, +) factory.Faker.add_provider(internet) @@ -78,6 +83,7 @@ class ProgramPageFactory(wagtail_factories.PageFactory): subhead = factory.fuzzy.FuzzyText(prefix="Subhead ") thumbnail_image = factory.SubFactory(wagtail_factories.ImageFactory) background_image = factory.SubFactory(wagtail_factories.ImageFactory) + language = factory.SubFactory(CourseLanguageFactory) parent = factory.SubFactory(wagtail_factories.PageFactory) certificate_page = factory.RelatedFactory( "cms.factories.CertificatePageFactory", "parent" @@ -108,6 +114,7 @@ class CoursePageFactory(wagtail_factories.PageFactory): subhead = factory.fuzzy.FuzzyText(prefix="Subhead ") thumbnail_image = factory.SubFactory(wagtail_factories.ImageFactory) background_image = factory.SubFactory(wagtail_factories.ImageFactory) + language = factory.SubFactory(CourseLanguageFactory) parent = factory.SubFactory(wagtail_factories.PageFactory) certificate_page = factory.RelatedFactory( "cms.factories.CertificatePageFactory", "parent" @@ -141,6 +148,7 @@ class ExternalCoursePageFactory(wagtail_factories.PageFactory): subhead = factory.fuzzy.FuzzyText(prefix="Subhead ") thumbnail_image = factory.SubFactory(wagtail_factories.ImageFactory) background_image = factory.SubFactory(wagtail_factories.ImageFactory) + language = factory.SubFactory(CourseLanguageFactory) parent = factory.SubFactory(wagtail_factories.PageFactory) class Meta: @@ -170,6 +178,7 @@ class ExternalProgramPageFactory(wagtail_factories.PageFactory): subhead = factory.fuzzy.FuzzyText(prefix="Subhead ") thumbnail_image = factory.SubFactory(wagtail_factories.ImageFactory) background_image = factory.SubFactory(wagtail_factories.ImageFactory) + language = factory.SubFactory(CourseLanguageFactory) parent = factory.SubFactory(wagtail_factories.PageFactory) class Meta: diff --git a/cms/migrations/0078_add_courseware_page_language.py b/cms/migrations/0078_add_courseware_page_language.py new file mode 100644 index 000000000..fa8c12bee --- /dev/null +++ b/cms/migrations/0078_add_courseware_page_language.py @@ -0,0 +1,81 @@ +# Generated by Django 4.2.17 on 2025-01-03 08:36 + +import django.db.models.deletion +from django.db import migrations, models + + +def populate_course_language(apps, schema_editor): + """Prepopulate the course language for the course pages""" + + CoursePage = apps.get_model("cms.CoursePage") + ExternalCoursePage = apps.get_model("cms.ExternalCoursePage") + ProgramPage = apps.get_model("cms.ProgramPage") + ExternalProgramPage = apps.get_model("cms.ExternalProgramPage") + + CourseLanguage = apps.get_model("courses.CourseLanguage") + # English is the default language for all the courses + course_language_english, _ = CourseLanguage.objects.get_or_create( + name="English", priority=1 + ) + + CoursePage.objects.update(language=course_language_english) + ExternalCoursePage.objects.update(language=course_language_english) + ProgramPage.objects.update(language=course_language_english) + ExternalProgramPage.objects.update(language=course_language_english) + + +class Migration(migrations.Migration): + dependencies = [ + ("courses", "0042_add_course_language"), + ("cms", "0077_alter_certificatepage_ceus"), + ] + + operations = [ + migrations.AddField( + model_name="coursepage", + name="language", + field=models.ForeignKey( + blank=True, + help_text="The course/program language for this page", + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="courses.courselanguage", + ), + ), + migrations.AddField( + model_name="externalcoursepage", + name="language", + field=models.ForeignKey( + blank=True, + help_text="The course/program language for this page", + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="courses.courselanguage", + ), + ), + migrations.AddField( + model_name="externalprogrampage", + name="language", + field=models.ForeignKey( + blank=True, + help_text="The course/program language for this page", + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="courses.courselanguage", + ), + ), + migrations.AddField( + model_name="programpage", + name="language", + field=models.ForeignKey( + blank=True, + help_text="The course/program language for this page", + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="courses.courselanguage", + ), + ), + migrations.RunPython( + populate_course_language, reverse_code=migrations.RunPython.noop + ), + ] diff --git a/cms/migrations/0079_make_language_non_nullable.py b/cms/migrations/0079_make_language_non_nullable.py new file mode 100644 index 000000000..46cf356e5 --- /dev/null +++ b/cms/migrations/0079_make_language_non_nullable.py @@ -0,0 +1,53 @@ +# Generated by Django 4.2.17 on 2025-01-03 08:39 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("cms", "0078_add_courseware_page_language"), + ] + + operations = [ + migrations.AlterField( + model_name="coursepage", + name="language", + field=models.ForeignKey( + help_text="The course/program language for this page", + on_delete=django.db.models.deletion.PROTECT, + to="courses.courselanguage", + ), + ), + migrations.AlterField( + model_name="externalcoursepage", + name="language", + field=models.ForeignKey( + help_text="The course/program language for this page", + on_delete=django.db.models.deletion.PROTECT, + to="courses.courselanguage", + ), + ), + migrations.AlterField( + model_name="externalprogrampage", + name="language", + field=models.ForeignKey( + default="", + help_text="The course/program language for this page", + on_delete=django.db.models.deletion.PROTECT, + to="courses.courselanguage", + ), + preserve_default=False, + ), + migrations.AlterField( + model_name="programpage", + name="language", + field=models.ForeignKey( + default="", + help_text="The course/program language for this page", + on_delete=django.db.models.deletion.PROTECT, + to="courses.courselanguage", + ), + preserve_default=False, + ), + ] diff --git a/cms/models.py b/cms/models.py index d2ee262b6..1190b5a49 100644 --- a/cms/models.py +++ b/cms/models.py @@ -528,7 +528,7 @@ def get_context(self, request, *args, **kwargs): # noqa: ARG002 ProgramPage.objects.live() .filter(program__live=True) .order_by("id") - .select_related("program") + .select_related("program", "language") .prefetch_related( Prefetch( "program__courses", @@ -538,16 +538,22 @@ def get_context(self, request, *args, **kwargs): # noqa: ARG002 ), ) ) - external_program_qset = ExternalProgramPage.objects.live().order_by("title") + external_program_qset = ( + ExternalProgramPage.objects.live() + .select_related("program", "language") + .order_by("title") + ) course_page_qset = ( CoursePage.objects.live() .filter(course__live=True) .order_by("id") - .select_related("course") + .select_related("course", "language") ) external_course_qset = ( - ExternalCoursePage.objects.live().select_related("course").order_by("title") + ExternalCoursePage.objects.live() + .select_related("course", "language") + .order_by("title") ) if topic_filter != ALL_TOPICS: @@ -922,6 +928,14 @@ class ProductPage(MetadataPageMixin, WagtailCachedPageMixin, Page): class Meta: abstract = True + language = models.ForeignKey( + "courses.CourseLanguage", + null=False, + blank=False, + on_delete=models.PROTECT, + help_text="The course/program language for this page", + ) + description = RichTextField( blank=True, help_text="The description shown on the product page" ) @@ -1030,6 +1044,7 @@ class Meta: use_json_field=True, ) content_panels = Page.content_panels + [ # noqa: RUF005 + FieldPanel("language"), FieldPanel("external_marketing_url"), FieldPanel("marketing_hubspot_form_id"), FieldPanel("subhead"), diff --git a/cms/models_test.py b/cms/models_test.py index 5aafb096f..3753420e2 100644 --- a/cms/models_test.py +++ b/cms/models_test.py @@ -83,6 +83,7 @@ from cms.wagtail_hooks import create_product_and_versions_for_courseware_pages from courses.factories import ( CourseFactory, + CourseLanguageFactory, CourseRunCertificateFactory, CourseRunFactory, ProgramCertificateFactory, @@ -2173,7 +2174,7 @@ def _create_external_course_page(superuser_client, course_id, slug): Asserts: Response status code is 302 (successful redirection). """ - + language = CourseLanguageFactory.create() post_data = { "course": course_id, "title": "Icon Grid #6064", @@ -2182,6 +2183,7 @@ def _create_external_course_page(superuser_client, course_id, slug): "content-count": 0, "slug": slug, "action-publish": "action-publish", + "language": language.id, } response = superuser_client.post( reverse( diff --git a/courses/admin.py b/courses/admin.py index e80e94ded..d80d87d07 100644 --- a/courses/admin.py +++ b/courses/admin.py @@ -13,6 +13,7 @@ from .models import ( Course, + CourseLanguage, CourseRun, CourseRunCertificate, CourseRunEnrollment, @@ -422,3 +423,12 @@ class PlatformAdmin(TimestampedModelAdmin): model = Platform list_display = ["id", "name", "created_on", "updated_on"] search_fields = ["name"] + + +@admin.register(CourseLanguage) +class CourseLanguageAdmin(admin.ModelAdmin): + """Admin for CourseLanguage""" + + model = CourseLanguage + list_display = ["id", "name", "priority"] + search_fields = ["name"] diff --git a/courses/factories.py b/courses/factories.py index 4ef1c89cb..94ecc65a8 100644 --- a/courses/factories.py +++ b/courses/factories.py @@ -14,6 +14,7 @@ from .models import ( Course, + CourseLanguage, CourseRun, CourseRunCertificate, CourseRunEnrollment, @@ -38,6 +39,15 @@ class Meta: model = Company +class CourseLanguageFactory(DjangoModelFactory): + """Factory for Course Language""" + + name = factory.Sequence(lambda n: f"Language_{n}") + + class Meta: + model = CourseLanguage + + class PlatformFactory(DjangoModelFactory): """Factory for Platform""" diff --git a/courses/migrations/0042_add_course_language.py b/courses/migrations/0042_add_course_language.py new file mode 100644 index 000000000..cdbb659b6 --- /dev/null +++ b/courses/migrations/0042_add_course_language.py @@ -0,0 +1,74 @@ +# Generated by Django 4.2.17 on 2025-01-07 13:31 + +import django.core.validators +import django.db.models.functions.text +from django.db import migrations, models + + +def add_default_supported_languages(apps, schema_editor): + """Add the languages that xPRO platform will support as default""" + CourseLanguage = apps.get_model("courses.CourseLanguage") + CourseLanguage.objects.bulk_create( + [ + CourseLanguage(name="English", priority=1), + CourseLanguage(name="Spanish"), + CourseLanguage(name="Portuguese"), + CourseLanguage(name="Mandarin"), + CourseLanguage(name="Italian"), + CourseLanguage(name="French"), + ] + ) + + +def remove_all_languages(apps, schema_editor): + """Remove all languages""" + CourseLanguage = apps.get_model("courses.CourseLanguage") + CourseLanguage.objects.all().delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("courses", "0041_platform_sync_daily"), + ] + + operations = [ + migrations.CreateModel( + name="CourseLanguage", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_on", models.DateTimeField(auto_now_add=True)), + ("updated_on", models.DateTimeField(auto_now=True)), + ("name", models.CharField(max_length=255, unique=True)), + ( + "priority", + models.PositiveSmallIntegerField( + blank=True, + default=100, + help_text="The priority of this language in the course/program sorting.", + null=True, + validators=[django.core.validators.MinValueValidator(1)], + ), + ), + ], + ), + migrations.AddConstraint( + model_name="courselanguage", + constraint=models.UniqueConstraint( + django.db.models.functions.text.Lower("name"), + name="unique_language_name", + violation_error_message="A language with this name already exists.", + ), + ), + migrations.RunPython( + code=add_default_supported_languages, + reverse_code=remove_all_languages, + ), + ] diff --git a/courses/models.py b/courses/models.py index f226f8021..fdf19a313 100644 --- a/courses/models.py +++ b/courses/models.py @@ -11,6 +11,7 @@ from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator from django.db import models +from django.db.models.functions import Lower from django.utils.functional import cached_property from cms.urls import detail_path_char_pattern @@ -242,6 +243,33 @@ def validate_unique(self, exclude=None): super().validate_unique(exclude=exclude) +class CourseLanguage(TimestampedModel, ValidateOnSaveMixin): + """ + Model for courseware language + """ + + name = models.CharField(max_length=255, unique=True) + priority = models.PositiveSmallIntegerField( + null=True, + blank=True, + default=100, + validators=[MinValueValidator(1)], + help_text="The priority of this language in the course/program sorting.", + ) + + class Meta: + constraints = [ + models.UniqueConstraint( + Lower("name"), + name="unique_language_name", + violation_error_message="A language with this name already exists.", + ) + ] + + def __str__(self): + return self.name + + class Program(TimestampedModel, PageProperties, ValidateOnSaveMixin): """Model for a course program""" diff --git a/courses/models_test.py b/courses/models_test.py index 7c13c5697..eac535fd8 100644 --- a/courses/models_test.py +++ b/courses/models_test.py @@ -5,6 +5,8 @@ import factory import pytest from django.core.exceptions import ValidationError +from django.db.models.deletion import ProtectedError +from django.db.utils import IntegrityError from cms.factories import ( CertificatePageFactory, @@ -16,6 +18,7 @@ from courses.factories import ( CompanyFactory, CourseFactory, + CourseLanguageFactory, CourseRunCertificateFactory, CourseRunEnrollmentFactory, CourseRunFactory, @@ -819,3 +822,22 @@ def test_platform_name_is_unique(): with pytest.raises(ValidationError): PlatformFactory.create(name=EMERITUS_PLATFORM_NAME.lower()) + + +def test_course_language_unique(): + """ + Tests that case-insensitive course language is unique. + """ + CourseLanguageFactory.create(name="UNIQUE_LANGUAGE") + + with pytest.raises(IntegrityError): + CourseLanguageFactory.create(name="unique_language") + + +def test_course_language_prevent_delete(): + """ + Tests that course language cannot be deleted if associated with a Courseware Page. + """ + course_page = CoursePageFactory.create() + with pytest.raises(ProtectedError): + course_page.language.delete() diff --git a/courses/serializers.py b/courses/serializers.py index fd4bf281e..eb38b4299 100644 --- a/courses/serializers.py +++ b/courses/serializers.py @@ -117,6 +117,7 @@ class BaseProductSerializer(serializers.ModelSerializer): availability = serializers.SerializerMethodField() prerequisites = serializers.SerializerMethodField() + language = serializers.SerializerMethodField() class Meta: model = ProductPage @@ -137,8 +138,13 @@ class Meta: "credits", "availability", "prerequisites", + "language", ] + def get_language(self, instance): + """Get the language of the product""" + return instance.page.language.name if instance.page else None + def get_prerequisites(self, instance): # noqa: ARG002 """Get product prerequisites""" diff --git a/courses/serializers_test.py b/courses/serializers_test.py index bde188d44..cbf09e2db 100644 --- a/courses/serializers_test.py +++ b/courses/serializers_test.py @@ -184,6 +184,7 @@ def test_serialize_program( # noqa: PLR0913 "platform": program.platform.name, "availability": "dated", "prerequisites": [], + "language": program.page.language.name if program.page else None, }, ) assert data["end_date"] != non_live_run.end_date.strftime(datetime_millis_format) @@ -341,6 +342,7 @@ def test_serialize_course( # noqa: PLR0913 "platform": course.platform.name, "availability": "dated", "prerequisites": [], + "language": course.page.language.name if course.page else None, }, ) diff --git a/courses/sync_external_courses/external_course_sync_api.py b/courses/sync_external_courses/external_course_sync_api.py index 9778e534f..8111476d5 100644 --- a/courses/sync_external_courses/external_course_sync_api.py +++ b/courses/sync_external_courses/external_course_sync_api.py @@ -25,7 +25,7 @@ ) from cms.wagtail_hooks import create_common_child_pages_for_external_courses from courses.api import generate_course_readable_id -from courses.models import Course, CourseRun, CourseTopic, Platform +from courses.models import Course, CourseLanguage, CourseRun, CourseTopic, Platform from courses.sync_external_courses.external_course_sync_api_client import ( ExternalCourseSyncAPIClient, ) @@ -155,6 +155,8 @@ def __init__(self, external_course_json, keymap): self.duration = f"{total_weeks} Weeks" if total_weeks != 0 else "" self.min_weeks = total_weeks self.max_weeks = total_weeks + # If there is no language in the API we will default it to "English" + self.language = external_course_json.get("language", "English").strip() # Description can be null in External Course API data, we cannot store `None` as description is Non-Nullable self.description = ( @@ -560,6 +562,9 @@ def create_or_update_external_course_page( # noqa: C901 course_page = ( ExternalCoursePage.objects.select_for_update().filter(course=course).first() ) + course_language, _ = CourseLanguage.objects.get_or_create( + name__icontains=external_course.language + ) image = None if external_course.image_name: @@ -589,6 +594,7 @@ def create_or_update_external_course_page( # noqa: C901 description=external_course.description, background_image=image, thumbnail_image=image, + language=course_language, ) course_index_page.add_child(instance=course_page) course_page.save_revision().publish() @@ -625,6 +631,11 @@ def create_or_update_external_course_page( # noqa: C901 latest_revision.thumbnail_image = image is_updated = True + # If the language is different from the course page language, update the language. + if latest_revision.language != course_language: + latest_revision.language = course_language + is_updated = True + if is_updated: save_page_revision(course_page, latest_revision) diff --git a/courses/sync_external_courses/external_course_sync_api_test.py b/courses/sync_external_courses/external_course_sync_api_test.py index d74f53c71..1ac330350 100644 --- a/courses/sync_external_courses/external_course_sync_api_test.py +++ b/courses/sync_external_courses/external_course_sync_api_test.py @@ -189,22 +189,24 @@ def test_generate_external_course_run_courseware_id( "is_live_and_draft", "create_image", "test_image_name_without_extension", + "has_language", ), [ - (True, False, False, True, True), - (True, True, True, True, False), - (True, True, False, True, False), - (False, False, False, False, False), + (True, False, False, True, True, True), + (True, True, True, True, False, True), + (True, True, False, True, False, True), + (False, False, False, False, False, False), ], ) @pytest.mark.django_db -def test_create_or_update_external_course_page( # noqa: PLR0913 +def test_create_or_update_external_course_page( # noqa: PLR0913, C901 create_course_page, publish_page, is_live_and_draft, create_image, test_image_name_without_extension, external_course_data, + has_language, ): """ Test that `create_or_update_external_course_page` creates a new course or updates the existing. @@ -240,6 +242,11 @@ def test_create_or_update_external_course_page( # noqa: PLR0913 external_course_page.unpublish() keymap = get_keymap(external_course_data["course_run_code"]) + + # Explicitly remove the language key from the dictionary to test the case where the language is not present + if not has_language: + external_course_data.pop("language") + external_course_page, course_page_created, course_page_updated = ( create_or_update_external_course_page( course_index_page, @@ -287,6 +294,12 @@ def test_create_or_update_external_course_page( # noqa: PLR0913 == external_course_data["image_name"] ) + # Check if the language is set correctly if it is present in the external course data, otherwise it should be English + if has_language: + assert external_course_page.language.name == external_course_data["language"] + else: + assert external_course_page.language.name == "English" + @pytest.mark.parametrize( "external_course_data", [{"platform": EMERITUS_PLATFORM_NAME}], indirect=True diff --git a/courses/sync_external_courses/test_data/batch_test.json b/courses/sync_external_courses/test_data/batch_test.json index 795b5b12f..0e0779a2b 100644 --- a/courses/sync_external_courses/test_data/batch_test.json +++ b/courses/sync_external_courses/test_data/batch_test.json @@ -111,7 +111,7 @@ "product_sub_type": "Short Form", "format": "Online", "suggested_duration": 49, - "language": "English", + "language": "Spanish", "image_name": "test_emeritus_image.jpg", "ceu": "2.8", "landing_page_url": "https://test-external-course-sync-api.io/Internet-of-things-iot-design-and-applications?utm_medium=EmWebsite&utm_campaign=direct_EmWebsite?utm_campaign=school_website&utm_medium=website&utm_source=MIT-web", @@ -157,7 +157,7 @@ "product_sub_type": "Online", "format": "Online", "suggested_duration": 203, - "language": "English", + "language": "Chinese", "image_name": "test_emeritus_image.jpg", "ceu": "28", "landing_page_url": null, diff --git a/courses/views/v1/__init__.py b/courses/views/v1/__init__.py index 86177ae15..bdd064e55 100644 --- a/courses/views/v1/__init__.py +++ b/courses/views/v1/__init__.py @@ -44,7 +44,11 @@ class ProgramViewSet(viewsets.ReadOnlyModelViewSet): courses_prefetch = Prefetch( "courses", Course.objects.select_related( - "coursepage", "externalcoursepage", "platform" + "coursepage", + "externalcoursepage", + "platform", + "coursepage__language", + "externalcoursepage__language", ).prefetch_related( course_runs_prefetch, "coursepage__topics", "externalcoursepage__topics" ), @@ -55,7 +59,13 @@ class ProgramViewSet(viewsets.ReadOnlyModelViewSet): queryset = ( Program.objects.filter(live=True) .exclude(products=None) - .select_related("programpage", "externalprogrampage", "platform") + .select_related( + "programpage", + "externalprogrampage", + "platform", + "programpage__language", + "externalprogrampage__language", + ) .prefetch_related(courses_prefetch, products_prefetch) .filter(Q(programpage__live=True) | Q(externalprogrampage__live=True)) ) @@ -79,6 +89,8 @@ def get_queryset(self): .prefetch_related( "coursepage__topics", "externalcoursepage__topics", + "coursepage__language", + "externalcoursepage__language", self.course_runs_prefetch, ) .filter(Q(coursepage__live=True) | Q(externalcoursepage__live=True)) diff --git a/courses/wagtail_hooks.py b/courses/wagtail_hooks.py index fe1540889..1f2d7d465 100644 --- a/courses/wagtail_hooks.py +++ b/courses/wagtail_hooks.py @@ -6,7 +6,7 @@ @hooks.register("register_admin_viewset") -def register_viewset(): +def register_topics_viewset(): """ Register `CourseTopicViewSet` in wagtail """ diff --git a/localdev/seed/api.py b/localdev/seed/api.py index 27e9da347..606725b45 100644 --- a/localdev/seed/api.py +++ b/localdev/seed/api.py @@ -24,6 +24,7 @@ from courses.constants import CONTENT_TYPE_MODEL_COURSERUN, DEFAULT_PLATFORM_NAME from courses.models import ( Course, + CourseLanguage, CourseRun, CourseRunEnrollment, CourseRunEnrollmentAudit, @@ -483,6 +484,11 @@ def _deserialize_courseware_cms_page(self, courseware_obj, data): return existing_page else: page_obj = cms_page_cls(**cms_model_data) + course_language, _ = CourseLanguage.objects.get_or_create( + name="English", priority=1 + ) + page_obj.language = course_language + courseware_page_parent = get_courseware_page_parent(courseware_obj) courseware_page_parent.add_child(instance=page_obj) self._set_page_topics(topics, page_obj)