Skip to content

Commit

Permalink
✨(plugin) add notifications to courses
Browse files Browse the repository at this point in the history
Add a new plugin to create warning or information notifications.
This plugin is only available on course detail pages.
  • Loading branch information
sandroscosta committed Jan 22, 2024
1 parent e1ae633 commit 2272d34
Show file tree
Hide file tree
Showing 16 changed files with 342 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Versioning](https://semver.org/spec/v2.0.0.html).

## [Unrealeased]

### Added

- Notification plugin for Course Detail pages

### Changed

- Switch from setup.cfg to pyproject.toml
Expand Down
1 change: 1 addition & 0 deletions sandbox/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 9 additions & 0 deletions src/frontend/scss/colors/_theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions src/frontend/scss/components/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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);
}
4 changes: 4 additions & 0 deletions src/richie/apps/courses/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ <h1 class="subheader__title" property="name">{% render_model current_page "title
{% endif %}
{% endblock title %}

{% block notifications %}
{% if current_page.publisher_is_draft %}
<h2 class="course-detail__title">{% trans 'Notifications' %}</h2>
{% 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" %}
<div class="category-badge-list subheader__badges">
Expand Down Expand Up @@ -226,6 +234,7 @@ <h1 class="subheader__title" property="name">{% render_model current_page "title
{% endif %}
{% endblock snapshot %}


{% block cover %}
{% placeholder_as_plugins "course_cover" as cover_plugins %}
<meta property="image" content="{% thumbnail cover_plugins.0.picture 300x170 replace_alpha='#FFFFFF' crop upscale subject_location=cover_plugins.0.picture.subject_location %}" />
Expand Down
Empty file.
35 changes: 35 additions & 0 deletions src/richie/plugins/notification/cms_plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""
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"

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
25 changes: 25 additions & 0 deletions src/richie/plugins/notification/factories.py
Original file line number Diff line number Diff line change
@@ -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]
)
47 changes: 47 additions & 0 deletions src/richie/plugins/notification/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -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",),
),
]
Empty file.
29 changes: 29 additions & 0 deletions src/richie/plugins/notification/models.py
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{% load i18n %}

<div class="notification-alert__wrapper {{instance.template}}">
<div class="notification-alert__icon">
{% if instance.template == 'info' %}
<svg xmlns="http://www.w3.org/2000/svg" id="info-icon" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
{% else %}
<svg xmlns="http://www.w3.org/2000/svg" id="warn-icon" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></path></svg>
{% endif %}
</div>
<div class="notification-alert__content">
{% trans 'Warning' as transl_title %}
<h2>{{ instance.title|default_if_none:transl_title }}</h2>
<p>{{ instance.message }}</p>
</div>
</div>
Empty file.
115 changes: 115 additions & 0 deletions tests/plugins/notification/test_cms_plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""Testing DjangoCMS plugin declaration for Richie's notifications plugin."""
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)

0 comments on commit 2272d34

Please sign in to comment.