diff --git a/CHANGELOG.md b/CHANGELOG.md index dcb2e62c58..9570eb10a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ Versioning](https://semver.org/spec/v2.0.0.html). ## [Unrealeased] +### Added + +- Add offer and price fields to courseRun displayed at admin + view. + ## [2.28.1] ### Fixed diff --git a/cookiecutter/{{cookiecutter.organization}}-richie-site-factory/template/{{cookiecutter.site}}/src/backend/{{cookiecutter.site}}/settings.py b/cookiecutter/{{cookiecutter.organization}}-richie-site-factory/template/{{cookiecutter.site}}/src/backend/{{cookiecutter.site}}/settings.py index b54d9ce7c2..b1b29ead8b 100644 --- a/cookiecutter/{{cookiecutter.organization}}-richie-site-factory/template/{{cookiecutter.site}}/src/backend/{{cookiecutter.site}}/settings.py +++ b/cookiecutter/{{cookiecutter.organization}}-richie-site-factory/template/{{cookiecutter.site}}/src/backend/{{cookiecutter.site}}/settings.py @@ -332,6 +332,13 @@ class Base(StyleguideMixin, DRFMixin, RichieCoursesConfigurationMixin, Configura environ_prefix=None, ) + # Course run price currency value that would be shown on course detail page + RICHIE_DEFAULT_COURSE_RUN_PRICE_CURRENCY = values.Value( + "EUR", + environ_name="RICHIE_DEFAULT_COURSE_RUN_PRICE_CURRENCY", + environ_prefix=None, + ) + # Internationalization TIME_ZONE = "Europe/Paris" USE_I18N = True diff --git a/sandbox/settings.py b/sandbox/settings.py index b584e5a304..70a985a2aa 100644 --- a/sandbox/settings.py +++ b/sandbox/settings.py @@ -598,6 +598,13 @@ class Base(StyleguideMixin, DRFMixin, RichieCoursesConfigurationMixin, Configura environ_prefix=None, ) + # Course run price currency value that would be shown on course detail page + RICHIE_DEFAULT_COURSE_RUN_PRICE_CURRENCY = values.Value( + "EUR", + environ_name="RICHIE_DEFAULT_COURSE_RUN_PRICE_CURRENCY", + environ_prefix=None, + ) + @classmethod def _get_environment(cls): """Environment in which the application is launched.""" diff --git a/src/richie/apps/courses/admin.py b/src/richie/apps/courses/admin.py index 1378b8972d..cbc76af3fa 100644 --- a/src/richie/apps/courses/admin.py +++ b/src/richie/apps/courses/admin.py @@ -57,6 +57,8 @@ class Meta: "languages", "enrollment_count", "catalog_visibility", + "offer", + "price", "sync_mode", "display_mode", ] @@ -150,6 +152,8 @@ class CourseRunAdmin(FrontendEditableAdminMixin, TranslatableAdmin): "languages", "enrollment_count", "catalog_visibility", + "offer", + "price", "sync_mode", ) list_display = ["id"] diff --git a/src/richie/apps/courses/factories.py b/src/richie/apps/courses/factories.py index be9b319d05..ecb91027fd 100644 --- a/src/richie/apps/courses/factories.py +++ b/src/richie/apps/courses/factories.py @@ -13,6 +13,7 @@ import factory from cms.api import add_plugin +from richie.apps.courses.models.course import CourseRunOffer from richie.plugins.nesteditem.defaults import ACCORDION from ..core.defaults import ALL_LANGUAGES @@ -529,6 +530,22 @@ def enrollment_count(self): """ return random.randint(0, 10000) # nosec + @factory.lazy_attribute + def price(self): + """ + The price of a course run is a random float between 1 and 100. + """ + return random.randint(100, 10000) / 100 # nosec + + @factory.lazy_attribute + def offer(self): + """ + The offer of a course run is read from Django settings. + """ + return ( + CourseRunOffer.FREE if self.price == 0.0 else CourseRunOffer.PAID + ) # nosec + class CategoryFactory(BLDPageExtensionDjangoModelFactory): """ diff --git a/src/richie/apps/courses/migrations/0035_courserun_offer_and_more.py b/src/richie/apps/courses/migrations/0035_courserun_offer_and_more.py new file mode 100644 index 0000000000..38c3982de9 --- /dev/null +++ b/src/richie/apps/courses/migrations/0035_courserun_offer_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.13 on 2024-07-09 10:56 + +from django.db import migrations, models + +import richie.apps.core.fields.multiselect + + +class Migration(migrations.Migration): + dependencies = [ + ("courses", "0034_auto_20230817_1736"), + ] + + operations = [ + migrations.AddField( + model_name="courserun", + name="offer", + field=models.CharField( + choices=[ + ( + "free", + "free - The entire course can be completed without cost", + ), + ( + "partially_free", + "partially_free - More than half of the course is for free", + ), + ( + "subscription", + "subscription - The user must be a subscriber or paid member in order to complete the entire course", + ), + ( + "paid", + "paid - The user must pay to complete the course", + ), + ], + default="free", + max_length=20, + verbose_name="offer", + ), + ), + ] diff --git a/src/richie/apps/courses/migrations/0036_courserun_price_and_more.py b/src/richie/apps/courses/migrations/0036_courserun_price_and_more.py new file mode 100644 index 0000000000..125dc26cdb --- /dev/null +++ b/src/richie/apps/courses/migrations/0036_courserun_price_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.13 on 2024-07-09 14:37 + +from django.db import migrations, models + +import richie.apps.core.fields.multiselect + + +class Migration(migrations.Migration): + dependencies = [ + ("courses", "0035_courserun_offer_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="courserun", + name="price", + field=models.FloatField( + default=0.0, help_text="The cost of the course", verbose_name="price" + ), + ), + ] diff --git a/src/richie/apps/courses/models/course.py b/src/richie/apps/courses/models/course.py index cfe1896ecd..bac8d7d6a6 100644 --- a/src/richie/apps/courses/models/course.py +++ b/src/richie/apps/courses/models/course.py @@ -702,6 +702,19 @@ class CourseRunCatalogVisibility(models.TextChoices): HIDDEN = "hidden", _("hidden - hide on the course page and from search results") +class CourseRunOffer(models.TextChoices): + """Course run offer choices.""" + + FREE = "free", _("free - The entire course can be completed without cost") + PARTIALLY_FREE = "partially_free", _( + "partially_free - More than half of the course is for free" + ) + SUBSCRIPTION = "subscription", _( + "subscription - The user must be a subscriber or paid member to complete the entire course" + ) + PAID = "paid", _("paid - The user must pay to complete the course") + + class CourseRunDisplayMode(models.TextChoices): """Course run catalog display modes.""" @@ -771,6 +784,19 @@ class CourseRun(TranslatableModel): blank=False, max_length=20, ) + offer = models.CharField( + _("offer"), + choices=lazy(lambda: CourseRunOffer.choices, tuple)(), + default=CourseRunOffer.FREE, + blank=False, + max_length=20, + ) + price = models.FloatField( + _("price"), + default=0.0, + blank=True, + help_text=_("The cost of the course"), + ) display_mode = models.CharField( choices=CourseRunDisplayMode.choices, default=CourseRunDisplayMode.DETAILED, @@ -856,6 +882,7 @@ def mark_course_dirty(self): def save(self, *args, **kwargs): """Enforce validation each time an instance is saved.""" self.full_clean() + self.price = float(self.price) super().save(*args, **kwargs) # pylint: disable=signature-differs diff --git a/src/richie/apps/courses/serializers.py b/src/richie/apps/courses/serializers.py index fc78811999..8469ec38bc 100644 --- a/src/richie/apps/courses/serializers.py +++ b/src/richie/apps/courses/serializers.py @@ -75,6 +75,8 @@ class Meta: "state", "enrollment_count", "catalog_visibility", + "offer", + "price", ] diff --git a/tests/apps/courses/test_admin_course_run.py b/tests/apps/courses/test_admin_course_run.py index 081c08021f..33182f9dc3 100644 --- a/tests/apps/courses/test_admin_course_run.py +++ b/tests/apps/courses/test_admin_course_run.py @@ -346,6 +346,8 @@ def _prepare_add_view_post(self, course, status_code): "enrollment_end_0": "2015-01-23", "enrollment_end_1": "09:07:11", "catalog_visibility": "course_and_search", + "offer": "free", + "price": 0.0, "sync_mode": "manual", "display_mode": "detailed", } @@ -450,6 +452,8 @@ def _prepare_change_view_post(self, course_run, course, status_code, check_metho "enrollment_end_1": "09:07:11", "enrollment_count": "5", "catalog_visibility": "course_and_search", + "offer": "free", + "price": 0.0, "sync_mode": "manual", "display_mode": "detailed", } diff --git a/tests/apps/courses/test_admin_form_course_run.py b/tests/apps/courses/test_admin_form_course_run.py index d927c4e34b..921b456ff3 100644 --- a/tests/apps/courses/test_admin_form_course_run.py +++ b/tests/apps/courses/test_admin_form_course_run.py @@ -36,6 +36,8 @@ def _get_admin_form(course, user): "enrollment_end_0": "2015-01-23", "enrollment_end_1": "09:07:11", "catalog_visibility": "course_and_search", + "offer": "free", + "price": 0.0, "sync_mode": "manual", "display_mode": "detailed", } @@ -117,6 +119,7 @@ def test_admin_form_course_run_superuser_empty_form(self): "display_mode": ["This field is required."], "languages": ["This field is required."], "catalog_visibility": ["This field is required."], + "offer": ["This field is required."], "sync_mode": ["This field is required."], }, ) diff --git a/tests/apps/courses/test_models_course_run.py b/tests/apps/courses/test_models_course_run.py index 29f772f2a0..9bba7eef34 100644 --- a/tests/apps/courses/test_models_course_run.py +++ b/tests/apps/courses/test_models_course_run.py @@ -19,7 +19,7 @@ from richie.apps.core.helpers import create_i18n_page from richie.apps.courses.factories import CourseFactory, CourseRunFactory from richie.apps.courses.models import CourseRun, CourseRunTranslation -from richie.apps.courses.models.course import CourseRunCatalogVisibility +from richie.apps.courses.models.course import CourseRunCatalogVisibility, CourseRunOffer # pylint: disable=too-many-public-methods @@ -623,6 +623,8 @@ def test_models_course_run_mark_dirty_any_field(self): stub = CourseRunFactory( sync_mode="manual", catalog_visibility=CourseRunCatalogVisibility.COURSE_ONLY, + offer=CourseRunOffer.SUBSCRIPTION, + price=3.0, display_mode="compact", ) # New random values to update our course run