diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b3cd48d3b..80add17306 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ Versioning](https://semver.org/spec/v2.0.0.html).
- Add Additional Information section for a category and
use them in a course page
- Added new page extension `MainMenuEntry`
+- Update Programs page with new fields
### Fixed
diff --git a/src/frontend/scss/colors/_theme.scss b/src/frontend/scss/colors/_theme.scss
index 8088b1e524..543d0d661c 100644
--- a/src/frontend/scss/colors/_theme.scss
+++ b/src/frontend/scss/colors/_theme.scss
@@ -483,6 +483,8 @@ $r-theme: (
),
program-detail: (
cover-empty-background: r-color('smoke'),
+ checkmark-list-decoration: url('../../richie/images/components/checkmark.svg'),
+ checkmark-list-decoration-color: r-color('indianred3'),
),
registered-credit-card: (
title-color: r-color('charcoal'),
diff --git a/src/frontend/scss/components/templates/courses/cms/_program_detail.scss b/src/frontend/scss/components/templates/courses/cms/_program_detail.scss
index 6766768ee7..88f788e40b 100644
--- a/src/frontend/scss/components/templates/courses/cms/_program_detail.scss
+++ b/src/frontend/scss/components/templates/courses/cms/_program_detail.scss
@@ -2,6 +2,7 @@
//
.program-detail {
+ $detail-selector: &;
margin: 0 auto;
padding: 0;
@@ -54,6 +55,79 @@
padding-right: $grid-gutter-width;
}
+ &__objectives {
+ @if r-theme-val(program-detail, checkmark-list-decoration) {
+ ul {
+ padding-left: 0.3rem;
+ list-style-type: none;
+
+ li {
+ position: relative;
+ margin-top: 0.5rem;
+ font-size: 1rem;
+ padding-left: 1.5rem;
+
+ &::before {
+ content: '';
+ display: block;
+ position: absolute;
+ top: 0.2rem;
+ left: 0;
+ width: 0.8rem;
+ height: 0.8rem;
+ background-repeat: no-repeat;
+ background-color: r-theme-val(program-detail, checkmark-list-decoration-color);
+ -webkit-mask: r-theme-val(program-detail, checkmark-list-decoration);
+ mask: r-theme-val(program-detail, checkmark-list-decoration);
+ -webkit-mask-size: cover;
+ mask-size: cover;
+ }
+ }
+ }
+ }
+ }
+
+ @if $r-program-aside {
+ &__content {
+ @include media-breakpoint-up(lg) {
+ //@include sv-flex(1, 0, #{100% - $r-program-aside});
+ padding-right: 3rem;
+ }
+ }
+
+ &__aside {
+ padding: 1rem 0;
+
+ @include media-breakpoint-up(lg) {
+ @include sv-flex(1, 0, $r-program-aside);
+ padding: 3rem 1rem;
+ }
+
+ #{$detail-selector}__row {
+ margin-bottom: 1.5rem;
+ }
+
+ #{$detail-selector}__title {
+ @include font-size($h3-font-size);
+ padding-bottom: 1rem;
+ @if r-theme-val(course-detail, aside-title-border) {
+ border-bottom: $onepixel solid r-theme-val(course-detail, aside-title-border);
+ }
+ }
+ }
+
+ &__main {
+ @include media-breakpoint-up(lg) {
+ @include sv-flex(1, 0, calc(100% - #{$r-program-subheader-aside}));
+ display: flex;
+ justify-content: flex-start;
+ align-content: flex-start;
+ align-items: flex-start;
+ flex-wrap: wrap;
+ }
+ }
+ }
+
&__courses {
@include make-col-ready();
@include make-col(12);
diff --git a/src/frontend/scss/settings/_variables.scss b/src/frontend/scss/settings/_variables.scss
index 60d8572c05..3cd6b4cfd1 100644
--- a/src/frontend/scss/settings/_variables.scss
+++ b/src/frontend/scss/settings/_variables.scss
@@ -152,11 +152,13 @@ $r-footer-logo-width-lg: 11.875rem !default;
// full width like common blocks. Usefull if you plan to remove course run from
// template
$r-course-aside: 35% !default;
+$r-program-aside: 25% !default;
// Subheader aside column width in course detail. Unlike, $r-course-aside this
// variable cannot be null. Otherwise to homegenize layout, it should have the
// same value than $r-course-aside.
$r-course-subheader-aside: 35% !default;
+$r-program-subheader-aside: 25% !default;
// Course search page shared variables
$r-search-filters-gutter: 0.2rem !default;
diff --git a/src/richie/apps/core/templates/richie/icons.html b/src/richie/apps/core/templates/richie/icons.html
index bf725bbb2f..556df024e2 100644
--- a/src/richie/apps/core/templates/richie/icons.html
+++ b/src/richie/apps/core/templates/richie/icons.html
@@ -192,6 +192,14 @@
+
+
+
+
+
+
+
+
diff --git a/src/richie/apps/courses/admin.py b/src/richie/apps/courses/admin.py
index 4b7c3029ba..76748bd547 100644
--- a/src/richie/apps/courses/admin.py
+++ b/src/richie/apps/courses/admin.py
@@ -351,6 +351,24 @@ def title(self, obj):
return obj.extended_object.get_title()
+class ProgramAdmin(PageExtensionAdmin):
+ """Admin class for the Program model"""
+
+ list_display = ["title"]
+ frontend_editable_fields = (
+ "duration",
+ "effort",
+ "price",
+ )
+
+ # pylint: disable=no-self-use
+ def title(self, obj):
+ """
+ Display the course title as a read-only field from the related page
+ """
+ return obj.extended_object.get_title()
+
+
class LicenceAdmin(TranslatableAdmin):
"""
Admin class for the Licence model
@@ -368,3 +386,4 @@ class LicenceAdmin(TranslatableAdmin):
admin.site.register(models.Organization, OrganizationAdmin)
admin.site.register(models.PageRole, PageRoleAdmin)
admin.site.register(models.Person, PersonAdmin)
+admin.site.register(models.Program, ProgramAdmin)
diff --git a/src/richie/apps/courses/cms_toolbars.py b/src/richie/apps/courses/cms_toolbars.py
index 1797282c18..ff73acbc4f 100644
--- a/src/richie/apps/courses/cms_toolbars.py
+++ b/src/richie/apps/courses/cms_toolbars.py
@@ -13,7 +13,7 @@
from cms.utils.urlutils import admin_reverse
from .defaults import PAGE_EXTENSION_TOOLBAR_ITEM_POSITION
-from .models import Category, Course, MainMenuEntry, Organization, Person
+from .models import Category, Course, MainMenuEntry, Organization, Person, Program
class BaseExtensionToolbar(ExtensionToolbar):
@@ -181,3 +181,12 @@ def populate(self):
disabled=not self.toolbar.edit_mode_active,
position=PAGE_EXTENSION_TOOLBAR_ITEM_POSITION,
)
+
+
+@toolbar_pool.register
+class ProgramExtensionToolbar(BaseExtensionToolbar):
+ """
+ This extension class customizes the toolbar for the program page extension
+ """
+
+ model = Program
diff --git a/src/richie/apps/courses/factories.py b/src/richie/apps/courses/factories.py
index d428151442..851eb206ed 100644
--- a/src/richie/apps/courses/factories.py
+++ b/src/richie/apps/courses/factories.py
@@ -26,6 +26,8 @@
from . import defaults, models
from .defaults import ROLE_CHOICES
+# pylint: disable=too-many-lines
+
VideoSample = namedtuple("VideoSample", ["label", "image", "url"])
VIDEO_SAMPLE_LINKS = (
@@ -838,6 +840,87 @@ class Meta:
# fields concerning the related page
page_template = models.Program.PAGE["template"]
+ # pylint: disable=no-self-use
+ @factory.lazy_attribute
+ def duration(self):
+ """Generate a random duration for the course between 1 day and 10 months."""
+ return [
+ random.randint(1, 10), # nosec
+ random.choice(list(defaults.TIME_UNITS.keys())[2:]), # nosec
+ ]
+
+ # pylint: disable=no-self-use
+ @factory.lazy_attribute
+ def effort(self):
+ """Generate a random effort for the course between 1 minute and 500 hours."""
+ return [
+ random.randint(1, 500), # nosec
+ random.choice(list(defaults.EFFORT_UNITS.keys())), # nosec
+ ]
+
+ # pylint: disable=no-self-use
+ @factory.lazy_attribute
+ def price(self):
+ """Generate a random price for the program between 1 and 100 euros."""
+ return random.randint(1, 500) # nosec
+
+ @factory.post_generation
+ # pylint: disable=unused-argument
+ def fill_teaser(self, create, extracted, **kwargs):
+ """
+ Add a video plugin for teaser with a random url
+ """
+
+ if create and extracted:
+ for language in self.extended_object.get_languages():
+ placeholder = self.extended_object.placeholders.get(
+ slot="program_teaser"
+ )
+
+ video_sample = (
+ extracted
+ if isinstance(extracted, VideoSample)
+ else random.choice(VIDEO_SAMPLE_LINKS) # nosec
+ )
+
+ add_plugin(
+ language=language,
+ placeholder=placeholder,
+ plugin_type="VideoPlayerPlugin",
+ label=video_sample.label,
+ embed_link=video_sample.url,
+ )
+
+ @factory.post_generation
+ # pylint: disable=unused-argument
+ def fill_categories(self, create, extracted, **kwargs):
+ """Add categories plugin to organization from a given list of category instances."""
+ if create and extracted:
+ associate_extensions(
+ self, extracted, "program_categories", "CategoryPlugin"
+ )
+
+ @factory.post_generation
+ # pylint: disable=unused-argument
+ def fill_team(self, create, extracted, **kwargs):
+ """
+ Add person plugin for course team from given person instance list
+ """
+ if create and extracted:
+ associate_extensions(self, extracted, "program_team", "PersonPlugin")
+
+ @factory.post_generation
+ # pylint: disable=unused-argument
+ def fill_organizations(self, create, extracted, **kwargs):
+ """
+ Add organizations plugin to course from a given list of organization instances.
+ """
+
+ if create and extracted:
+ associate_extensions(
+ self, extracted, "program_organizations", "OrganizationPlugin"
+ )
+
@factory.post_generation
# pylint: disable=unused-argument
def fill_body(self, create, extracted, **kwargs):
@@ -853,6 +936,21 @@ def fill_body(self, create, extracted, **kwargs):
plugin_type="TextPlugin",
)
+ @factory.post_generation
+ # pylint: disable=unused-argument
+ def fill_objectives(self, create, extracted, **kwargs):
+ """
+ Add simple text plugin for objectives with a random number of paragraphs
+ """
+ if create and extracted:
+ create_text_plugin(
+ self.extended_object,
+ "program_objectives",
+ nb_paragraphs=random.randint(2, 4), # nosec
+ languages=self.extended_object.get_languages(),
+ plugin_type="TextPlugin",
+ )
+
@factory.post_generation
# pylint: disable=unused-argument
def fill_courses(self, create, extracted, **kwargs):
diff --git a/src/richie/apps/courses/migrations/0036_program_duration_program_effort_program_price_and_more.py b/src/richie/apps/courses/migrations/0036_program_duration_program_effort_program_price_and_more.py
new file mode 100644
index 0000000000..de9910109f
--- /dev/null
+++ b/src/richie/apps/courses/migrations/0036_program_duration_program_effort_program_price_and_more.py
@@ -0,0 +1,275 @@
+# Generated by Django 4.2.17 on 2024-12-10 11:25
+
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+import richie.apps.core.fields.duration
+import richie.apps.core.fields.multiselect
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("cms", "0022_auto_20180620_1551"),
+ ("courses", "0035_add_menuentry"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="program",
+ name="duration",
+ field=richie.apps.core.fields.duration.CompositeDurationField(
+ blank=True,
+ default_unit="hour",
+ help_text="The program time range.",
+ max_length=80,
+ null=True,
+ time_units={
+ "day": ("day", "days"),
+ "hour": ("hour", "hours"),
+ "minute": ("minute", "minutes"),
+ "month": ("month", "months"),
+ "week": ("week", "weeks"),
+ },
+ ),
+ ),
+ migrations.AddField(
+ model_name="program",
+ name="effort",
+ field=richie.apps.core.fields.duration.CompositeDurationField(
+ blank=True,
+ default_unit="hour",
+ help_text="Total amount of time to complete this program.",
+ max_length=80,
+ null=True,
+ time_units={"hour": ("hour", "hours"), "minute": ("minute", "minutes")},
+ ),
+ ),
+ migrations.AddField(
+ model_name="program",
+ name="price",
+ field=models.DecimalField(
+ blank=True,
+ decimal_places=2,
+ default=0,
+ help_text="The price of the program.",
+ max_digits=10,
+ null=True,
+ validators=[django.core.validators.MinValueValidator(0)],
+ verbose_name="price",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="blogpostpluginmodel",
+ name="cmsplugin_ptr",
+ field=models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ related_name="%(app_label)s_%(class)s",
+ serialize=False,
+ to="cms.cmsplugin",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="categorypluginmodel",
+ name="cmsplugin_ptr",
+ field=models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ related_name="%(app_label)s_%(class)s",
+ serialize=False,
+ to="cms.cmsplugin",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="coursepluginmodel",
+ name="cmsplugin_ptr",
+ field=models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ related_name="%(app_label)s_%(class)s",
+ serialize=False,
+ to="cms.cmsplugin",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="courserun",
+ name="languages",
+ field=richie.apps.core.fields.multiselect.MultiSelectField(
+ choices=[
+ ("af", "Afrikaans"),
+ ("ar", "Arabic"),
+ ("ar-dz", "Algerian Arabic"),
+ ("ast", "Asturian"),
+ ("az", "Azerbaijani"),
+ ("bg", "Bulgarian"),
+ ("be", "Belarusian"),
+ ("bn", "Bengali"),
+ ("br", "Breton"),
+ ("bs", "Bosnian"),
+ ("ca", "Catalan"),
+ ("ckb", "Central Kurdish (Sorani)"),
+ ("cs", "Czech"),
+ ("cy", "Welsh"),
+ ("da", "Danish"),
+ ("de", "German"),
+ ("dsb", "Lower Sorbian"),
+ ("el", "Greek"),
+ ("en", "English"),
+ ("en-au", "Australian English"),
+ ("en-gb", "British English"),
+ ("eo", "Esperanto"),
+ ("es", "Spanish"),
+ ("es-ar", "Argentinian Spanish"),
+ ("es-co", "Colombian Spanish"),
+ ("es-mx", "Mexican Spanish"),
+ ("es-ni", "Nicaraguan Spanish"),
+ ("es-ve", "Venezuelan Spanish"),
+ ("et", "Estonian"),
+ ("eu", "Basque"),
+ ("fa", "Persian"),
+ ("fi", "Finnish"),
+ ("fr", "French"),
+ ("fy", "Frisian"),
+ ("ga", "Irish"),
+ ("gd", "Scottish Gaelic"),
+ ("gl", "Galician"),
+ ("he", "Hebrew"),
+ ("hi", "Hindi"),
+ ("hr", "Croatian"),
+ ("hsb", "Upper Sorbian"),
+ ("hu", "Hungarian"),
+ ("hy", "Armenian"),
+ ("ia", "Interlingua"),
+ ("id", "Indonesian"),
+ ("ig", "Igbo"),
+ ("io", "Ido"),
+ ("is", "Icelandic"),
+ ("it", "Italian"),
+ ("ja", "Japanese"),
+ ("ka", "Georgian"),
+ ("kab", "Kabyle"),
+ ("kk", "Kazakh"),
+ ("km", "Khmer"),
+ ("kn", "Kannada"),
+ ("ko", "Korean"),
+ ("ky", "Kyrgyz"),
+ ("lb", "Luxembourgish"),
+ ("lt", "Lithuanian"),
+ ("lv", "Latvian"),
+ ("mk", "Macedonian"),
+ ("ml", "Malayalam"),
+ ("mn", "Mongolian"),
+ ("mr", "Marathi"),
+ ("ms", "Malay"),
+ ("my", "Burmese"),
+ ("nb", "Norwegian Bokmål"),
+ ("ne", "Nepali"),
+ ("nl", "Dutch"),
+ ("nn", "Norwegian Nynorsk"),
+ ("os", "Ossetic"),
+ ("pa", "Punjabi"),
+ ("pl", "Polish"),
+ ("pt", "Portuguese"),
+ ("pt-br", "Brazilian Portuguese"),
+ ("ro", "Romanian"),
+ ("ru", "Russian"),
+ ("sk", "Slovak"),
+ ("sl", "Slovenian"),
+ ("sq", "Albanian"),
+ ("sr", "Serbian"),
+ ("sr-latn", "Serbian Latin"),
+ ("sv", "Swedish"),
+ ("sw", "Swahili"),
+ ("ta", "Tamil"),
+ ("te", "Telugu"),
+ ("tg", "Tajik"),
+ ("th", "Thai"),
+ ("tk", "Turkmen"),
+ ("tr", "Turkish"),
+ ("tt", "Tatar"),
+ ("udm", "Udmurt"),
+ ("uk", "Ukrainian"),
+ ("ur", "Urdu"),
+ ("uz", "Uzbek"),
+ ("vi", "Vietnamese"),
+ ("zh-hans", "Simplified Chinese"),
+ ("zh-hant", "Traditional Chinese"),
+ ],
+ help_text="The list of languages in which the course content is available.",
+ max_choices=50,
+ max_length=255,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="licencepluginmodel",
+ name="cmsplugin_ptr",
+ field=models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ related_name="%(app_label)s_%(class)s",
+ serialize=False,
+ to="cms.cmsplugin",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="organizationpluginmodel",
+ name="cmsplugin_ptr",
+ field=models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ related_name="%(app_label)s_%(class)s",
+ serialize=False,
+ to="cms.cmsplugin",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="organizationsbycategorypluginmodel",
+ name="cmsplugin_ptr",
+ field=models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ related_name="%(app_label)s_%(class)s",
+ serialize=False,
+ to="cms.cmsplugin",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="personpluginmodel",
+ name="cmsplugin_ptr",
+ field=models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ related_name="%(app_label)s_%(class)s",
+ serialize=False,
+ to="cms.cmsplugin",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="programpluginmodel",
+ name="cmsplugin_ptr",
+ field=models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ related_name="%(app_label)s_%(class)s",
+ serialize=False,
+ to="cms.cmsplugin",
+ ),
+ ),
+ ]
diff --git a/src/richie/apps/courses/models/program.py b/src/richie/apps/courses/models/program.py
index 60258ed2a3..07040e156c 100644
--- a/src/richie/apps/courses/models/program.py
+++ b/src/richie/apps/courses/models/program.py
@@ -2,6 +2,7 @@
Declare and configure the models for the program part
"""
+from django.core.validators import MinValueValidator
from django.db import models
from django.utils.translation import gettext_lazy as _
@@ -9,8 +10,12 @@
from cms.extensions.extension_pool import extension_pool
from cms.models.pluginmodel import CMSPlugin
+from ...core.fields.duration import CompositeDurationField
from ...core.models import BasePageExtension
-from ..defaults import PROGRAMS_PAGE
+from .. import defaults
+from .category import Category, CategoryPluginModel
+from .organization import Organization, OrganizationPluginModel
+from .person import Person, PersonPluginModel
class Program(BasePageExtension):
@@ -18,7 +23,36 @@ class Program(BasePageExtension):
The program extension represents and records a program.
"""
- PAGE = PROGRAMS_PAGE
+ PAGE = defaults.PROGRAMS_PAGE
+
+ duration = CompositeDurationField(
+ time_units=defaults.TIME_UNITS,
+ default_unit=defaults.DEFAULT_TIME_UNIT,
+ max_length=80,
+ blank=True,
+ null=True,
+ help_text=_("The program time range."),
+ )
+
+ effort = CompositeDurationField(
+ time_units=defaults.EFFORT_UNITS,
+ default_unit=defaults.DEFAULT_EFFORT_UNIT,
+ max_length=80,
+ blank=True,
+ null=True,
+ help_text=_("Total amount of time to complete this program."),
+ )
+
+ price = models.DecimalField(
+ _("price"),
+ max_digits=10,
+ decimal_places=2,
+ default=0,
+ blank=True,
+ null=True,
+ help_text=_("The price of the program."),
+ validators=[MinValueValidator(0)],
+ )
class Meta:
db_table = "richie_program"
@@ -32,6 +66,61 @@ def __str__(self):
name = self.extended_object.get_title()
return f"{model:s}: {name:s}"
+ @property
+ def pt_effort(self):
+ """Return effort as a PT string for schema.org metadata."""
+ if not self.effort:
+ return ""
+
+ (effort, effort_unit) = self.effort
+ unit_letter = effort_unit[0].upper()
+ return f"PT{effort:d}{unit_letter:s}"
+
+ @property
+ def price_with_currency(self):
+ """Return price with currency for schema.org metadata."""
+ if not self.price:
+ return ""
+
+ return f"€{self.price}"
+
+ def get_categories(self, language=None):
+ """
+ Return the categories linked to the program via a category plugin in any of the
+ placeholders on the program detail page, ranked by their `path` to respect the
+ order in the categories tree.
+ """
+ return self.get_direct_related_page_extensions(
+ Category, CategoryPluginModel, language=language
+ )
+
+ def get_organizations(self, language=None):
+ """
+ Return the organizations linked to the course via an organization plugin in any
+ of the placeholders on the course detail page, ranked by their `path` to respect
+ the order in the organizations tree.
+ """
+ return self.get_direct_related_page_extensions(
+ Organization, OrganizationPluginModel, language=language
+ )
+
+ def get_persons(self, language=None):
+ """
+ Return the persons linked to the course via a person plugin in any of the
+ placeholders on the course detail page, ranked by their `path` to respect
+ the order in the persons tree.
+ """
+ return self.get_direct_related_page_extensions(
+ Person, PersonPluginModel, language=language
+ )
+
+ def save(self, *args, **kwargs):
+ """
+ Enforce validation each time an instance is saved
+ """
+ self.full_clean()
+ super().save(*args, **kwargs)
+
class ProgramPluginModel(CMSPlugin):
"""
diff --git a/src/richie/apps/courses/settings/__init__.py b/src/richie/apps/courses/settings/__init__.py
index 7250f85dd4..0e791f4b49 100644
--- a/src/richie/apps/courses/settings/__init__.py
+++ b/src/richie/apps/courses/settings/__init__.py
@@ -381,25 +381,58 @@ def richie_placeholder_conf(name):
"child_classes": {"SectionPlugin": ["CKEditorPlugin"]},
},
# Program page detail
+ "courses/cms/program_detail.html program_categories": {
+ "name": _("Categories"),
+ "plugins": ["CategoryPlugin"],
+ },
"courses/cms/program_detail.html program_cover": {
"name": _("Cover"),
"plugins": ["SimplePicturePlugin"],
"limits": {"SimplePicturePlugin": 1},
},
+ "courses/cms/program_detail.html program_teaser": {
+ "name": _("Teaser"),
+ "plugins": ["VideoPlayerPlugin"],
+ "limits": {"VideoPlayerPlugin": 1},
+ },
"courses/cms/program_detail.html program_excerpt": {
"name": _("Excerpt"),
"plugins": ["PlainTextPlugin"],
"limits": {"PlainTextPlugin": 1},
},
+ "courses/cms/program_detail.html program_organizations": {
+ "name": _("Organizations"),
+ "plugins": ["OrganizationPlugin"],
+ },
"courses/cms/program_detail.html program_body": {
"name": _("Body"),
"plugins": ["CKEditorPlugin"],
"limits": {"CKEditorPlugin": 1},
},
+ "courses/cms/program_detail.html program_objectives": {
+ "name": _("What you will learn"),
+ "plugins": ["CKEditorPlugin"],
+ },
"courses/cms/program_detail.html program_courses": {
"name": _("Courses"),
"plugins": ["CoursePlugin"],
},
+ "courses/cms/program_detail.html program_team": {
+ "name": _("Instructors"),
+ "plugins": ["PersonPlugin"],
+ },
+ "courses/cms/program_detail.html program_information": {
+ "name": _("Complementary information"),
+ "plugins": ["SectionPlugin"],
+ "parent_classes": {
+ "CKEditorPlugin": ["SectionPlugin"],
+ "SimplePicturePlugin": ["SectionPlugin"],
+ "GlimpsePlugin": ["SectionPlugin"],
+ },
+ "child_classes": {
+ "SectionPlugin": ["CKEditorPlugin", "SimplePicturePlugin", "GlimpsePlugin"]
+ },
+ },
"courses/cms/program_list.html maincontent": {
"name": _("Main content"),
"plugins": ["SectionPlugin"],
diff --git a/src/richie/apps/courses/templates/courses/cms/program_detail.html b/src/richie/apps/courses/templates/courses/cms/program_detail.html
index 2efccef7d5..26e3642b9f 100644
--- a/src/richie/apps/courses/templates/courses/cms/program_detail.html
+++ b/src/richie/apps/courses/templates/courses/cms/program_detail.html
@@ -31,19 +31,81 @@
{% block subheader_content %}