diff --git a/CHANGELOG.md b/CHANGELOG.md index d727de996e..cd1eb95c83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ Versioning](https://semver.org/spec/v2.0.0.html). ## [Unrealeased] +### Added + +- Notification plugin for Course Detail pages + ## [2.25.0-beta.1] ### Fixed diff --git a/sandbox/settings.py b/sandbox/settings.py index 30364cd0e7..4d9c91a646 100644 --- a/sandbox/settings.py +++ b/sandbox/settings.py @@ -420,6 +420,7 @@ class Base(StyleguideMixin, DRFMixin, RichieCoursesConfigurationMixin, Configura "richie.plugins.simple_picture", "richie.plugins.simple_text_ckeditor", "richie.plugins.lti_consumer", + "richie.plugins.notification", "richie", # Third party apps "dj_pagination", diff --git a/src/frontend/scss/colors/_theme.scss b/src/frontend/scss/colors/_theme.scss index 0ee5b3ce14..1624dd2ecc 100644 --- a/src/frontend/scss/colors/_theme.scss +++ b/src/frontend/scss/colors/_theme.scss @@ -557,6 +557,15 @@ $r-theme: ( content-color: r-color(purplish-grey), title-color: r-color('charcoal'), ), + notification-plugin: ( + info-background-color: r-color('info-300'), + text-color: r-color('black'), + warn-background-color: r-color('warning-400'), + border-radius: 10px, + icon-stroke-color: r-color('black'), + icon-width: 24px, + icon-height: 24px, + ) ) !default; // On a Richie child project you can easily change a specific value using: diff --git a/src/frontend/scss/components/_index.scss b/src/frontend/scss/components/_index.scss index 2dd0c102f4..ebdc460d63 100644 --- a/src/frontend/scss/components/_index.scss +++ b/src/frontend/scss/components/_index.scss @@ -53,6 +53,7 @@ @import './templates/courses/plugins/category_plugin'; @import './templates/courses/plugins/licence_plugin'; +@import './templates/courses/plugins/notifications_plugin'; @import './templates/richie/section/section'; @import './templates/richie/large_banner/large_banner'; @import './templates/richie/nesteditem/nesteditem'; diff --git a/src/frontend/scss/components/templates/courses/plugins/_notifications_plugin.scss b/src/frontend/scss/components/templates/courses/plugins/_notifications_plugin.scss new file mode 100644 index 0000000000..341c125dfe --- /dev/null +++ b/src/frontend/scss/components/templates/courses/plugins/_notifications_plugin.scss @@ -0,0 +1,48 @@ +// change measurement units to rem +.notification-alert { + &__wrapper { + display: flex; + padding: 1rem; + border-radius: r-theme-val(notification-plugin, border-radius); + width: 100%; + margin-block-end: 1rem; + } + + &__icon { + display: flex; + align-items: center; + padding: 1rem; + margin-inline-end: 0.4rem; + + & svg { + fill: none !important; + stroke: r-theme-val(notification-plugin, icon-stroke-color); + height: r-theme-val(notification-plugin, icon-height); + width: r-theme-val(notification-plugin, icon-width); + flex-shrink: 0; + } + } + + &__content { + h2 { + margin-block-start: 0.6rem; + font-size: 0.9rem; + } + + p { + margin-block-end: 0.6rem; + font-size: 0.9rem; + } + } +} + +.info { + background-color: r-theme-val(notification-plugin, info-background-color); + color: r-theme-val(notification-plugin, text-color); + +} + +.warning { + background-color: r-theme-val(notification-plugin, warn-background-color); + color: r-theme-val(notification-plugin, text-color); +} \ No newline at end of file diff --git a/src/richie/apps/courses/settings/__init__.py b/src/richie/apps/courses/settings/__init__.py index f73e39c328..9f2745af84 100644 --- a/src/richie/apps/courses/settings/__init__.py +++ b/src/richie/apps/courses/settings/__init__.py @@ -245,6 +245,10 @@ def richie_placeholder_conf(name): "name": _("Assessment and Certification"), "plugins": ["CKEditorPlugin"], }, + "courses/cms/course_detail.html course_notifications": { + "name": _("Notifications"), + "plugins": ["NotificationPlugin"], + }, # Organization detail "courses/cms/organization_detail.html banner": { "name": _("Banner"), diff --git a/src/richie/apps/courses/templates/courses/cms/course_detail.html b/src/richie/apps/courses/templates/courses/cms/course_detail.html index 1e4149d496..e098384287 100644 --- a/src/richie/apps/courses/templates/courses/cms/course_detail.html +++ b/src/richie/apps/courses/templates/courses/cms/course_detail.html @@ -61,6 +61,14 @@

{% render_model current_page "title {% endif %} {% endblock title %} + {% block notifications %} + {% if current_page.publisher_is_draft %} +

{% trans 'Notifications' %}

+ {% endif %} + {% placeholder "course_notifications" %} + {% endblock notifications %} + + {% block categories %} {% if current_page.publisher_is_draft or not current_page|is_empty_placeholder:"course_icons" or not current_page|is_empty_placeholder:"course_categories" %}
@@ -226,6 +234,7 @@

{% render_model current_page "title {% endif %} {% endblock snapshot %} + {% block cover %} {% placeholder_as_plugins "course_cover" as cover_plugins %} diff --git a/src/richie/plugins/notification/__init__.py b/src/richie/plugins/notification/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/richie/plugins/notification/cms_plugins.py b/src/richie/plugins/notification/cms_plugins.py new file mode 100644 index 0000000000..8cb2bbc5f7 --- /dev/null +++ b/src/richie/plugins/notification/cms_plugins.py @@ -0,0 +1,36 @@ +""" +Notification CMS plugin +""" +from django.utils.translation import gettext_lazy as _ + +from cms.plugin_base import CMSPluginBase +from cms.plugin_pool import plugin_pool + +from richie.apps.core.defaults import PLUGINS_GROUP + +from .models import Notification + + +@plugin_pool.register_plugin +class NotificationPlugin(CMSPluginBase): + """ + A plugin to add plain text. + """ + + allow_children = False + cache = True + disable_child_plugins = True + fieldsets = ((None, {"fields": ["title", "message", "template"]}),) + model = Notification + module = PLUGINS_GROUP + name = _("Notification") + render_template = "richie/notification/notification.html" + + # TODO: add the remaining fields to the form + def render(self, context, instance, placeholder): + """ + Build plugin context passed to its template to perform rendering + """ + context = super().render(context, instance, placeholder) + context.update({"instance": instance}) + return context diff --git a/src/richie/plugins/notification/factories.py b/src/richie/plugins/notification/factories.py new file mode 100644 index 0000000000..de0d728c58 --- /dev/null +++ b/src/richie/plugins/notification/factories.py @@ -0,0 +1,25 @@ +""" +PlainText CMS plugin factories +""" +import random + +import factory + +from .models import Notification + + +class NotificationFactory(factory.django.DjangoModelFactory): + """ + Factory to create random instances of Notification for testing. + """ + + class Meta: + model = Notification + + title = factory.Faker("text", max_nb_chars=42) + message = factory.Faker("text", max_nb_chars=42) + template = ( + Notification.NOTIFICATION_TYPES[1][0] + if random.randint(0, 1) > 0 + else Notification.NOTIFICATION_TYPES[0][0] + ) diff --git a/src/richie/plugins/notification/migrations/0001_initial.py b/src/richie/plugins/notification/migrations/0001_initial.py new file mode 100644 index 0000000000..82149afabc --- /dev/null +++ b/src/richie/plugins/notification/migrations/0001_initial.py @@ -0,0 +1,47 @@ +# Generated by Django 3.2.23 on 2024-01-15 14:58 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("cms", "0022_auto_20180620_1551"), + ] + + operations = [ + migrations.CreateModel( + name="Notification", + fields=[ + ( + "cmsplugin_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + related_name="notification_notification", + serialize=False, + to="cms.cmsplugin", + ), + ), + ("title", models.CharField(blank=True, max_length=255)), + ("message", models.CharField(max_length=255, verbose_name="Message")), + ( + "template", + models.CharField( + choices=[("info", "Information"), ("warning", "Warning")], + default=("info", "Information"), + max_length=16, + verbose_name="Type", + ), + ), + ], + options={ + "abstract": False, + }, + bases=("cms.cmsplugin",), + ), + ] diff --git a/src/richie/plugins/notification/migrations/__init__.py b/src/richie/plugins/notification/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/richie/plugins/notification/models.py b/src/richie/plugins/notification/models.py new file mode 100644 index 0000000000..76d85d09ae --- /dev/null +++ b/src/richie/plugins/notification/models.py @@ -0,0 +1,29 @@ +""" +Notification plugin models +""" +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from cms.models.pluginmodel import CMSPlugin + + +class Notification(CMSPlugin): + """ + Notification plugin model. + + To be user to output notification messages on course pages. + """ + + NOTIFICATION_TYPES = ( + ("info", _("Information")), + ("warning", _("Warning")), + ) + + title = models.CharField(max_length=255, blank=True) + message = models.CharField(_("Message"), max_length=255) + template = models.CharField( + _("Type"), + choices=NOTIFICATION_TYPES, + default=NOTIFICATION_TYPES[0], + max_length=16, + ) diff --git a/src/richie/plugins/notification/templates/richie/notification/notification.html b/src/richie/plugins/notification/templates/richie/notification/notification.html new file mode 100644 index 0000000000..c71047a5ae --- /dev/null +++ b/src/richie/plugins/notification/templates/richie/notification/notification.html @@ -0,0 +1,16 @@ +{% load i18n %} + +
+
+ {% if instance.template == 'info' %} + + {% else %} + + {% endif %} +
+
+ {% trans 'Warning' as transl_title %} +

{{ instance.title|default_if_none:transl_title }}

+

{{ instance.message }}

+
+
\ No newline at end of file diff --git a/tests/plugins/notification/__init__.py b/tests/plugins/notification/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/plugins/notification/test_cms_plugins.py b/tests/plugins/notification/test_cms_plugins.py new file mode 100644 index 0000000000..478977a035 --- /dev/null +++ b/tests/plugins/notification/test_cms_plugins.py @@ -0,0 +1,114 @@ +from django.test import TestCase +from django.test.client import RequestFactory + +from cms.api import add_plugin +from cms.models import Placeholder +from cms.plugin_rendering import ContentRenderer + +from richie.plugins.notification.cms_plugins import NotificationPlugin +from richie.plugins.notification.factories import NotificationFactory + + +class NotificationPluginTestCase(TestCase): + """Test suite for the notification plugin.""" + + def test_cms_plugins_notification_context_and_html(self): + """ + Instanciating this plugin with an instance should populate the context + and render in the template. + """ + placeholder = Placeholder.objects.create(slot="test") + + # Create random values for parameters with a factory + notification = NotificationFactory() + fields_list = [ + "title", + "message", + "template", + ] + + model_instance = add_plugin( + placeholder, + NotificationPlugin, + "en", + **{field: getattr(notification, field) for field in fields_list}, + ) + plugin_instance = model_instance.get_plugin_class_instance() + context = plugin_instance.render({}, model_instance, None) + + # Check if "instance" is in context + self.assertIn("instance", context) + + # Check if parameters, generated by the factory, are correctly set in "instance" of context + self.assertEqual(context["instance"].title, notification.title) + self.assertEqual(context["instance"].message, notification.message) + self.assertEqual(context["instance"].template, notification.template) + + # Get generated html for plain text body + renderer = ContentRenderer(request=RequestFactory()) + html = renderer.render_plugin(model_instance, {}) + + # Check rendered body is correct after save and sanitize + self.assertIn(notification.title, html) + self.assertIn(notification.message, html) + self.assertIn(notification.template, html) + + def test_cms_plugins_notification_info_template(self): + """ + Instanciating this plugin with an instance to populate the context + """ + placeholder = Placeholder.objects.create(slot="test") + + # Create random values for parameters with a factory with an info template + notification = NotificationFactory(template="info") + fields_list = [ + "title", + "message", + "template", + ] + + model_instance = add_plugin( + placeholder, + NotificationPlugin, + "en", + **{field: getattr(notification, field) for field in fields_list}, + ) + + # Get the generated html + renderer = ContentRenderer(request=RequestFactory()) + html = renderer.render_plugin(model_instance, {}) + + # Check that all expected elements are in the html + self.assertIn('class="notification-alert__wrapper info"', html) + self.assertIn('class="notification-alert__icon"', html) + self.assertTrue('id="info-icon"' in html) + + def test_cms_plugins_notification_warn_template(self): + """ + Instanciating this plugin with an instance to populate the context + """ + placeholder = Placeholder.objects.create(slot="test") + + # Create random values for parameters with a factory with a warning template + notification = NotificationFactory(template="warning") + fields_list = [ + "title", + "message", + "template", + ] + + model_instance = add_plugin( + placeholder, + NotificationPlugin, + "en", + **{field: getattr(notification, field) for field in fields_list}, + ) + + # Get the generated html + renderer = ContentRenderer(request=RequestFactory()) + html = renderer.render_plugin(model_instance, {}) + + # Check that all expected elements are in the html + self.assertIn('class="notification-alert__wrapper warning"', html) + self.assertIn('class="notification-alert__icon"', html) + self.assertTrue('id="warn-icon"' in html)