diff --git a/cms/constants.py b/cms/constants.py index 8e7e47e4c..7001a246b 100644 --- a/cms/constants.py +++ b/cms/constants.py @@ -23,3 +23,6 @@ WEBINAR_HEADER_BANNER = "images/webinars/webinar-header-banner.jpg" UPCOMING_WEBINAR_BUTTON_TITLE = "REGISTER" ON_DEMAND_WEBINAR_BUTTON_TITLE = "VIEW RECORDING" + +FORMAT_ONLINE = "Online" +FORMAT_OTHER = "Other" diff --git a/cms/migrations/0064_productpage_format_field.py b/cms/migrations/0064_productpage_format_field.py new file mode 100644 index 000000000..a3b8d491a --- /dev/null +++ b/cms/migrations/0064_productpage_format_field.py @@ -0,0 +1,53 @@ +# Generated by Django 3.2.21 on 2023-09-13 12:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("cms", "0063_webinarindexpage_banner_image"), + ] + + operations = [ + migrations.AddField( + model_name="coursepage", + name="format", + field=models.CharField( + choices=[("Online", "Online"), ("Other", "Other")], + default="Online", + help_text="A short description indicating the format of a program or course", + max_length=20, + ), + ), + migrations.AddField( + model_name="externalcoursepage", + name="format", + field=models.CharField( + choices=[("Online", "Online"), ("Other", "Other")], + default="Online", + help_text="A short description indicating the format of a program or course", + max_length=20, + ), + ), + migrations.AddField( + model_name="externalprogrampage", + name="format", + field=models.CharField( + choices=[("Online", "Online"), ("Other", "Other")], + default="Online", + help_text="A short description indicating the format of a program or course", + max_length=20, + ), + ), + migrations.AddField( + model_name="programpage", + name="format", + field=models.CharField( + choices=[("Online", "Online"), ("Other", "Other")], + default="Online", + help_text="A short description indicating the format of a program or course", + max_length=20, + ), + ), + ] diff --git a/cms/models.py b/cms/models.py index 0f644af86..b41b50902 100644 --- a/cms/models.py +++ b/cms/models.py @@ -55,6 +55,8 @@ WEBINAR_DEFAULT_IMAGES, WEBINAR_HEADER_BANNER, WEBINAR_INDEX_SLUG, + FORMAT_ONLINE, + FORMAT_OTHER, ) from cms.forms import CertificatePageForm from courses.constants import DEFAULT_COURSE_IMG_PATH, PROGRAM_RUN_ID_PATTERN @@ -787,6 +789,16 @@ class Meta: blank=True, help_text="A short description indicating how long it takes to complete (e.g. '4 weeks')", ) + FORMAT_CHOICES = [ + (FORMAT_ONLINE, FORMAT_ONLINE), + (FORMAT_OTHER, FORMAT_OTHER), + ] + format = models.CharField( + max_length=20, + choices=FORMAT_CHOICES, + default=FORMAT_ONLINE, + help_text="A short description indicating the format of a program or course", + ) background_image = models.ForeignKey( Image, null=True, @@ -835,6 +847,7 @@ class Meta: FieldPanel("video_title"), FieldPanel("video_url"), FieldPanel("duration"), + FieldPanel("format"), FieldPanel("time_commitment"), FieldPanel("description", classname="full"), FieldPanel("catalog_details", classname="full"), diff --git a/cms/models_test.py b/cms/models_test.py index a0f00eaa1..0b5eb5f36 100644 --- a/cms/models_test.py +++ b/cms/models_test.py @@ -16,6 +16,8 @@ UPCOMING_WEBINAR, UPCOMING_WEBINAR_BUTTON_TITLE, WEBINAR_HEADER_BANNER, + FORMAT_ONLINE, + FORMAT_OTHER, ) from cms.factories import ( CertificatePageFactory, @@ -827,6 +829,7 @@ def test_course_page_properties(): description="

desc

", catalog_details="

catalog desc

", duration="1 week", + format=FORMAT_ONLINE, video_title="

title

", video_url="http://test.com/mock.mp4", background_image__title="background-image", @@ -836,6 +839,7 @@ def test_course_page_properties(): assert course_page.description == "

desc

" assert course_page.catalog_details == "

catalog desc

" assert course_page.duration == "1 week" + assert course_page.format == FORMAT_ONLINE assert course_page.video_title == "

title

" assert course_page.video_url == "http://test.com/mock.mp4" assert course_page.background_image.title == "background-image" @@ -851,6 +855,7 @@ def test_external_course_page_properties(): description="

desc

", catalog_details="

catalog desc

", duration="1 week", + format=FORMAT_OTHER, video_title="

title

", video_url="http://test.com/mock.mp4", background_image__title="background-image", @@ -861,6 +866,7 @@ def test_external_course_page_properties(): assert external_course_page.description == "

desc

" assert external_course_page.catalog_details == "

catalog desc

" assert external_course_page.duration == "1 week" + assert external_course_page.format == FORMAT_OTHER assert external_course_page.video_title == "

title

" assert external_course_page.video_url == "http://test.com/mock.mp4" assert external_course_page.background_image.title == "background-image" @@ -870,6 +876,33 @@ def test_external_course_page_properties(): assert external_course_page.product == external_course_page.course +def test_course_page_format_field_default_value(): + """ + Verifies that the "format" field in a course page has the default value FORMAT_ONLINE. + """ + course_page = CoursePageFactory.create() + + assert course_page.format == FORMAT_ONLINE + + +@pytest.mark.parametrize("course_format", [FORMAT_ONLINE, FORMAT_OTHER]) +def test_course_page_format_field_choices(course_format, staff_user): + """ + Verifies that if the "format" field in a course page contains the values FORMAT_ONLINE and FORMAT_OTHER, + and they are in the same context. + """ + course_page = CoursePageFactory.create(format=course_format) + + rf = RequestFactory() + request = rf.get("/") + request.user = staff_user + + context = course_page.get_context(request=request) + context_format = context.get("page").format + + assert context_format == course_format + + def test_program_page_properties(): """ Wagtail-page-related properties should return expected values if the Wagtail page exists @@ -880,6 +913,7 @@ def test_program_page_properties(): description="

desc

", catalog_details="

catalog desc

", duration="1 week", + format=FORMAT_ONLINE, video_title="

title

", video_url="http://test.com/mock.mp4", background_image__title="background-image", @@ -889,6 +923,7 @@ def test_program_page_properties(): assert program_page.description == "

desc

" assert program_page.catalog_details == "

catalog desc

" assert program_page.duration == "1 week" + assert program_page.format == FORMAT_ONLINE assert program_page.video_title == "

title

" assert program_page.video_url == "http://test.com/mock.mp4" assert program_page.background_image.title == "background-image" @@ -904,6 +939,7 @@ def test_external_program_page_properties(): description="

desc

", catalog_details="

catalog desc

", duration="1 week", + format=FORMAT_OTHER, video_title="

title

", video_url="http://test.com/mock.mp4", background_image__title="background-image", @@ -914,11 +950,39 @@ def test_external_program_page_properties(): assert external_program_page.description == "

desc

" assert external_program_page.catalog_details == "

catalog desc

" assert external_program_page.duration == "1 week" + assert external_program_page.format == FORMAT_OTHER assert external_program_page.video_title == "

title

" assert external_program_page.video_url == "http://test.com/mock.mp4" assert external_program_page.background_image.title == "background-image" +def test_program_page_format_field_default_value(): + """ + Verifies that the "format" field in a program page has the default value FORMAT_ONLINE. + """ + program_page = ProgramPageFactory.create() + + assert program_page.format == FORMAT_ONLINE + + +@pytest.mark.parametrize("program_format", [FORMAT_ONLINE, FORMAT_OTHER]) +def test_program_page_format_field_choices(program_format, staff_user): + """ + Verifies that if the "format" field in a program page contains the values FORMAT_ONLINE and FORMAT_OTHER, + and they are in the same context. + """ + program_page = ProgramPageFactory.create(format=program_format) + + rf = RequestFactory() + request = rf.get("/") + request.user = staff_user + + context = program_page.get_context(request=request) + context_format = context.get("page").format + + assert context_format == program_format + + def test_course_page_learning_outcomes(): """ CoursePage related LearningOutcomesPage should return expected values if it exists diff --git a/cms/templates/partials/metadata-tiles.html b/cms/templates/partials/metadata-tiles.html index 00f8335f6..db60cc6ae 100644 --- a/cms/templates/partials/metadata-tiles.html +++ b/cms/templates/partials/metadata-tiles.html @@ -65,9 +65,8 @@ {% endif %}
  • FORMAT - Online + {{ page.format }}
  • - {% if page.product.current_price %}
  • PRICE diff --git a/courses/serializers.py b/courses/serializers.py index 6c3a0bf3e..1803acaa2 100644 --- a/courses/serializers.py +++ b/courses/serializers.py @@ -184,9 +184,7 @@ def get_credits(self, instance): def get_format(self, instance): # pylint: disable=unused-argument """Returns the format of the course""" - # Currently hardcoded at the frontend as well, At some point we'll need to move it into CMS to make it - # configurable. - return "Online" + return instance.page.format if instance.page and instance.page.format else None def get_platform(self, instance): """Returns the platform name of the course""" @@ -374,10 +372,8 @@ def get_credits(self, instance): ) def get_format(self, instance): # pylint: disable=unused-argument - """Returns the format of the course""" - # Currently hardcoded at the frontend as well, At some point we'll need to move it into CMS to make it - # configurable. - return "Online" + """Returns the format of the program""" + return instance.page.format if instance.page and instance.page.format else None def get_platform(self, instance): """Returns the platform name of the program""" diff --git a/courses/serializers_test.py b/courses/serializers_test.py index f05b4e603..305b2094d 100644 --- a/courses/serializers_test.py +++ b/courses/serializers_test.py @@ -10,6 +10,7 @@ from django.contrib.auth.models import AnonymousUser from cms.factories import FacultyMembersPageFactory +from cms.constants import FORMAT_ONLINE, FORMAT_OTHER from courses.factories import ( CourseFactory, CourseRunEnrollmentFactory, @@ -34,7 +35,6 @@ from ecommerce.serializers_test import datetime_format from mitxpro.test_utils import assert_drf_json_equal, drf_datetime - pytestmark = [pytest.mark.django_db] @@ -53,6 +53,7 @@ def test_base_program_serializer(): @pytest.mark.parametrize("has_product", [True, False]) @pytest.mark.parametrize("is_external", [True, False]) +@pytest.mark.parametrize("program_format", [FORMAT_ONLINE, FORMAT_OTHER]) @pytest.mark.parametrize( "duration, time_commitment, video_url, ceus, external_marketing_url", [ @@ -76,6 +77,7 @@ def test_serialize_program( mock_context, has_product, is_external, + program_format, duration, time_commitment, video_url, @@ -88,6 +90,7 @@ def test_serialize_program( is_external=is_external, page__certificate_page__CEUs=ceus, page__duration=duration, + page__format=program_format, page__time_commitment=time_commitment, page__video_url=video_url, page__external_marketing_url=external_marketing_url, @@ -147,9 +150,9 @@ def test_serialize_program( "topics": [{"name": topic.name} for topic in topics], "time_commitment": time_commitment, "duration": duration, + "format": program_format, "video_url": video_url, "credits": ceus, - "format": "Online", "is_external": is_external, "external_marketing_url": external_marketing_url, "platform": program.platform.name, @@ -174,6 +177,7 @@ def test_base_course_serializer(): @pytest.mark.parametrize("all_runs", [True, False]) @pytest.mark.parametrize("is_external", [True, False]) @pytest.mark.parametrize("course_page", [True, False]) +@pytest.mark.parametrize("course_format", [FORMAT_ONLINE, FORMAT_OTHER]) @pytest.mark.parametrize( "duration, time_commitment, video_url, ceus, external_marketing_url", [ @@ -199,6 +203,7 @@ def test_serialize_course( all_runs, is_external, course_page, + course_format, duration, time_commitment, video_url, @@ -219,6 +224,7 @@ def test_serialize_course( is_external=is_external, page__time_commitment=time_commitment, page__duration=duration, + page__format=course_format, page__video_url=video_url, page__certificate_page__CEUs=ceus, page__external_marketing_url=external_marketing_url, @@ -275,9 +281,9 @@ def test_serialize_course( "topics": [{"name": topic}] if course_page else [], "time_commitment": time_commitment if course_page else None, "duration": duration if course_page else None, + "format": course_format if course_page else None, "video_url": video_url if course_page else None, "credits": ceus if course_page else None, - "format": "Online", "is_external": is_external, "external_marketing_url": external_marketing_url if course_page else None, "platform": course.platform.name, diff --git a/localdev/seed/resources/seed_data.json b/localdev/seed/resources/seed_data.json index 4a59cf32c..fa4ea69db 100644 --- a/localdev/seed/resources/seed_data.json +++ b/localdev/seed/resources/seed_data.json @@ -5,6 +5,7 @@ "subhead": "A four-course online program leading to a Professional Certificate from the Massachusetts Institute of Technology.", "description": "

    A four-course online program leading to a Professional Certificate from the Massachusetts Institute of Technology.

    ", "readable_id": "program-v1:xPRO+SysEngx", + "format": "Other", "_product": { "price": 2499 } @@ -14,6 +15,7 @@ "subhead": "Learn stuff about digital learning.", "description": "

    Learn stuff about digital learning.

    ", "readable_id": "program-v1:xPRO+DgtlLearn", + "format": "Online", "_product": { "price": 2501 } @@ -22,7 +24,8 @@ "title": "Analog Learning", "subhead": "Learn stuff about analog learning.", "description": "

    Learn stuff about analog learning.

    ", - "readable_id": "program-v1:xPRO+AnlgLearn" + "readable_id": "program-v1:xPRO+AnlgLearn", + "format": "Online" } ], "courses": [ @@ -31,6 +34,7 @@ "description": "

    This is edX's demo course

    ", "subhead": "This is edX's demo course", "readable_id": "course-v1:edX+DemoX+Demo_Course", + "format": "Online", "course_runs": [ { "title": "edX Demonstration Course - Spring 2013", @@ -52,6 +56,7 @@ "position_in_program": 1, "description": "

    Course 1 of 4 that comprises the Architecture and Systems Engineering Professional Certificate Program.

    ", "readable_id": "course-v1:xPRO+SysEngxB1", + "format": "Online", "course_runs": [ { "title": "Architecture of Complex Systems - Fall 2019", @@ -75,6 +80,7 @@ "position_in_program": 2, "description": "

    Course 2 of 4 that comprises the Architecture and Systems Engineering Professional Certificate Program. This course may be taken individually, without enrolling in the professional certificate program.

    ", "readable_id": "course-v1:xPRO+SysEngxB2", + "format": "Online", "course_runs": [ { "title": "Models in Engineering - Spring 2020", @@ -98,6 +104,7 @@ "position_in_program": 3, "description": "

    Course 3 of 4 that comprises the Architecture and Systems Engineering Professional Certificate Program. This course may be taken individually, without enrolling in the professional certificate program.

    ", "readable_id": "course-v1:xPRO+SysEngxB3", + "format": "Online", "course_runs": [ { "title": "Model-Based Systems Engineering: Documentation and Analysis - Fall 2020", @@ -121,6 +128,7 @@ "position_in_program": 4, "description": "

    The course where the methods are quantitative and the systems are engineered.

    ", "readable_id": "course-v1:xPRO+SysEngxB4", + "format": "Online", "course_runs": [ { "title": "Quantitative Methods in Systems Engineering - August 2019", @@ -145,6 +153,7 @@ "position_in_program": 1, "description": "

    An introductory course to Digital Learning

    ", "readable_id": "course-v1:xPRO+DgtlLearn1", + "format": "Online", "course_runs": [ { "title": "Digital Learning 100 - August 2021", @@ -188,6 +197,7 @@ "position_in_program": 2, "description": "

    A more advanced course in Digital Learning

    ", "readable_id": "course-v1:xPRO+DgtlLearn2", + "format": "Online", "course_runs": [ { "title": "Digital Learning 200 - August 2016", @@ -223,6 +233,7 @@ "position_in_program": 3, "description": "

    The most advancedest course in Digital Learning

    ", "readable_id": "course-v1:xPRO+DgtlLearn3", + "format": "Online", "course_runs": [ { "title": "Digital Learning 300 - August 2016", @@ -261,6 +272,7 @@ "position_in_program": 1, "description": "

    An introductory course to Analog Learning

    ", "readable_id": "course-v1:xPRO+AnlgLearn1", + "format": "Online", "course_runs": [ { "title": "Analog Learning 100 - August 2016", @@ -299,6 +311,7 @@ "position_in_program": 2, "description": "

    A more advanced course in Analog Learning

    ", "readable_id": "course-v1:xPRO+AnlgLearn2", + "format": "Online", "course_runs": [ { "title": "Analog Learning 200 - August 2016", @@ -337,6 +350,7 @@ "position_in_program": 3, "description": "

    The most advancedest course in Analog Learning

    ", "readable_id": "course-v1:xPRO+AnlgLearn3", + "format": "Online", "course_runs": [ { "title": "Analog Learning 300 - August 2016", @@ -376,6 +390,7 @@ "subhead": "An introduction to a type of computing, and that type of computing is quantum.", "description": "

    An introduction to a type of computing, and that type of computing is quantum.

    ", "readable_id": "course-v1:xPRO+QuantCompx1", + "format": "Online", "course_runs": [ { "title": "Quantum Computing - August 2019", @@ -395,6 +410,7 @@ "subhead": "A course that is not live.", "description": "

    A course that is not live.

    ", "readable_id": "course-v1:xPRO+NotLive1", + "format": "Online", "live": false, "course_runs": [ { @@ -414,6 +430,7 @@ "subhead": "Additive Manufacturing for Innovative Design and Production.", "description": "

    Additive Manufacturing for Innovative Design and Production.

    ", "readable_id": "course-v1:SEED+AMx", + "format": "Online", "course_runs": [ { "title": "Additive Manufacturing for Innovative Design and Production - April 2018", @@ -447,6 +464,7 @@ "subhead": "Model-Based Systems Engineering: Documentation and Analysis 1.", "description": "

    Model-Based Systems Engineering: Documentation and Analysis 1.

    ", "readable_id": "course-v1:SEED+SysEngBx3", + "format": "Online", "course_runs": [ { "title": "Model-Based Systems Engineering: Documentation and Analysis - April 2018",