diff --git a/tbx/courses/__init__.py b/tbx/courses/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tbx/courses/blocks.py b/tbx/courses/blocks.py new file mode 100644 index 000000000..314894525 --- /dev/null +++ b/tbx/courses/blocks.py @@ -0,0 +1,33 @@ +from tbx.core import blocks as tbx_blocks +from wagtail import blocks + + +class CourseOutlineItemBlock(blocks.StructBlock): + title = blocks.CharBlock() + text = blocks.RichTextBlock() + + +class CourseOutlineBlock(blocks.StructBlock): + heading = blocks.CharBlock() + course_outline = blocks.ListBlock( + CourseOutlineItemBlock(), + ) + + class Meta: + template = ("patterns/molecules/streamfield/blocks/course_outline_block.html",) + + +class ExternalLinkCTABlock(blocks.StructBlock): + link_url = blocks.URLBlock(label="External Link") + heading = blocks.CharBlock() + text = blocks.CharBlock() + + class Meta: + template = ( + "patterns/molecules/streamfield/blocks/external_link_cta_block.html", + ) + + +class CourseDetailStoryBlock(tbx_blocks.StoryBlock): + course_outline = CourseOutlineBlock() + external_link_cta = ExternalLinkCTABlock() diff --git a/tbx/courses/migrations/0001_initial.py b/tbx/courses/migrations/0001_initial.py new file mode 100644 index 000000000..aa28343fe --- /dev/null +++ b/tbx/courses/migrations/0001_initial.py @@ -0,0 +1,337 @@ +# Generated by Django 4.2.8 on 2024-01-02 15:35 + +from django.db import migrations, models +import django.db.models.deletion +import tbx.core.blocks +import wagtail.blocks +import wagtail.embeds.blocks +import wagtail.fields +import wagtail.images.blocks +import wagtailmarkdown.blocks +import wagtailmedia.blocks + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("wagtailcore", "0089_log_entry_data_json_null_to_object"), + ("images", "0005_alter_rendition_file"), + ] + + operations = [ + migrations.CreateModel( + name="CourseLandingPage", + fields=[ + ( + "page_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="wagtailcore.page", + ), + ), + ("social_text", models.CharField(blank=True, max_length=255)), + ( + "social_image", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="images.customimage", + ), + ), + ], + options={ + "abstract": False, + }, + bases=("wagtailcore.page", models.Model), + ), + migrations.CreateModel( + name="CourseDetailPage", + fields=[ + ( + "page_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="wagtailcore.page", + ), + ), + ("social_text", models.CharField(blank=True, max_length=255)), + ( + "strapline", + models.CharField( + help_text="Words in tag will display in a contrasting colour.", + max_length=255, + ), + ), + ( + "sessions", + models.CharField( + blank=True, help_text="e.g. 4 sessions", max_length=25 + ), + ), + ( + "cost", + models.CharField( + blank=True, + help_text="e.g.£999 / $1,299 per person ", + max_length=255, + ), + ), + ("intro", wagtail.fields.RichTextField(blank=True)), + ( + "header_link", + models.URLField( + blank=True, help_text="e.g. https://www.example.com" + ), + ), + ( + "header_link_text", + models.CharField( + blank=True, + help_text="e.g. Visit example.com for dates", + max_length=255, + ), + ), + ( + "body", + wagtail.fields.StreamField( + [ + ( + "h2", + wagtail.blocks.CharBlock( + form_classname="title", + icon="title", + template="patterns/molecules/streamfield/blocks/heading2_block.html", + ), + ), + ( + "h3", + wagtail.blocks.CharBlock( + form_classname="title", + icon="title", + template="patterns/molecules/streamfield/blocks/heading3_block.html", + ), + ), + ( + "h4", + wagtail.blocks.CharBlock( + form_classname="title", + icon="title", + template="patterns/molecules/streamfield/blocks/heading4_block.html", + ), + ), + ( + "intro", + wagtail.blocks.RichTextBlock( + icon="pilcrow", + template="patterns/molecules/streamfield/blocks/intro_block.html", + ), + ), + ( + "paragraph", + wagtail.blocks.RichTextBlock( + icon="pilcrow", + template="patterns/molecules/streamfield/blocks/paragraph_block.html", + ), + ), + ( + "aligned_image", + wagtail.blocks.StructBlock( + [ + ( + "image", + wagtail.images.blocks.ImageChooserBlock(), + ), + ( + "alignment", + tbx.core.blocks.ImageFormatChoiceBlock(), + ), + ("caption", wagtail.blocks.CharBlock()), + ( + "attribution", + wagtail.blocks.CharBlock(required=False), + ), + ], + label="Aligned image", + template="patterns/molecules/streamfield/blocks/aligned_image_block.html", + ), + ), + ( + "wide_image", + wagtail.blocks.StructBlock( + [ + ( + "image", + wagtail.images.blocks.ImageChooserBlock(), + ) + ], + label="Wide image", + template="patterns/molecules/streamfield/blocks/wide_image_block.html", + ), + ), + ( + "bustout", + wagtail.blocks.StructBlock( + [ + ( + "image", + wagtail.images.blocks.ImageChooserBlock(), + ), + ("text", wagtail.blocks.RichTextBlock()), + ], + template="patterns/molecules/streamfield/blocks/bustout_block.html", + ), + ), + ( + "pullquote", + wagtail.blocks.StructBlock( + [ + ( + "quote", + wagtail.blocks.CharBlock( + form_classname="quote title" + ), + ), + ("attribution", wagtail.blocks.CharBlock()), + ], + template="patterns/molecules/streamfield/blocks/pullquote_block.html", + ), + ), + ( + "raw_html", + wagtail.blocks.RawHTMLBlock( + icon="code", + label="Raw HTML", + template="patterns/molecules/streamfield/blocks/raw_html_block.html", + ), + ), + ( + "mailchimp_form", + wagtail.blocks.RawHTMLBlock( + icon="code", + label="Mailchimp embedded form", + template="patterns/molecules/streamfield/blocks/mailchimp_form_block.html", + ), + ), + ( + "markdown", + wagtailmarkdown.blocks.MarkdownBlock( + icon="code", + template="patterns/molecules/streamfield/blocks/markdown_block.html", + ), + ), + ( + "embed", + wagtail.embeds.blocks.EmbedBlock( + group="Media", + icon="code", + template="patterns/molecules/streamfield/blocks/embed_block.html", + ), + ), + ( + "video_block", + wagtail.blocks.StructBlock( + [ + ( + "video", + wagtailmedia.blocks.VideoChooserBlock(), + ), + ( + "autoplay", + wagtail.blocks.BooleanBlock( + default=False, + help_text="Automatically start and loop the video. Please use sparingly.", + required=False, + ), + ), + ( + "use_original_width", + wagtail.blocks.BooleanBlock( + default=False, + help_text="Use the original width of the video instead of the default content width. Note that videos wider than the content width will be limited to the content width.", + required=False, + ), + ), + ], + group="Media", + ), + ), + ( + "story_embed", + tbx.core.blocks.ExternalStoryEmbedBlock( + icon="code", + template="patterns/molecules/streamfield/blocks/external_story_block.html", + ), + ), + ( + "course_outline", + wagtail.blocks.StructBlock( + [ + ("heading", wagtail.blocks.CharBlock()), + ( + "course_outline", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ( + "title", + wagtail.blocks.CharBlock(), + ), + ( + "text", + wagtail.blocks.RichTextBlock(), + ), + ] + ) + ), + ), + ] + ), + ), + ( + "external_link_cta", + wagtail.blocks.StructBlock( + [ + ( + "link_url", + wagtail.blocks.URLBlock( + label="External Link" + ), + ), + ("heading", wagtail.blocks.CharBlock()), + ("text", wagtail.blocks.CharBlock()), + ] + ), + ), + ], + use_json_field=True, + ), + ), + ( + "social_image", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="images.customimage", + ), + ), + ], + options={ + "abstract": False, + }, + bases=("wagtailcore.page", models.Model), + ), + ] diff --git a/tbx/courses/migrations/__init__.py b/tbx/courses/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tbx/courses/models.py b/tbx/courses/models.py new file mode 100644 index 000000000..c1c47452c --- /dev/null +++ b/tbx/courses/models.py @@ -0,0 +1,98 @@ +from django.core import exceptions as django_exceptions +from django.db import models + +from tbx.core.utils import models as utils_models +from tbx.courses import blocks as tbx_course_blocks +from wagtail import fields as wagtail_fields +from wagtail import models as wagtail_models +from wagtail.admin import panels +from wagtail.search import index + +INTRO_RICHTEXT_FEATURES = ["bold", "italic", "link", "document-link", "strikethrough"] + + +class CourseLandingPage(utils_models.SocialFields, wagtail_models.Page): + # stubbed out for now, is incoming + + template = "patterns/pages/courses/course_landing_page.html" + + content_panels = wagtail_models.Page.content_panels + [] + + promote_panels = [ + panels.MultiFieldPanel( + wagtail_models.Page.promote_panels, "Common page configuration" + ), + panels.MultiFieldPanel( + utils_models.SocialFields.promote_panels, "Social fields" + ), + ] + + +class CourseDetailPage(utils_models.SocialFields, wagtail_models.Page): + parent_page_types = ["courses.CourseLandingPage"] + + template = "patterns/pages/courses/course_detail_page.html" + + strapline = models.CharField( + max_length=255, + help_text="Words in tag will display in a contrasting colour.", + ) + sessions = models.CharField(max_length=25, blank=True, help_text="e.g. 4 sessions") + cost = models.CharField( + max_length=255, blank=True, help_text="e.g.£999 / $1,299 per person " + ) + intro = wagtail_fields.RichTextField(blank=True, features=INTRO_RICHTEXT_FEATURES) + header_link = models.URLField(blank=True, help_text="e.g. https://www.example.com") + header_link_text = models.CharField( + max_length=255, blank=True, help_text="e.g. Visit example.com for dates" + ) + + body = wagtail_fields.StreamField( + tbx_course_blocks.CourseDetailStoryBlock(), use_json_field=True + ) + + search_fields = wagtail_models.Page.search_fields + [ + index.SearchField("intro"), + index.SearchField("strapline"), + ] + + content_panels = wagtail_models.Page.content_panels + [ + panels.MultiFieldPanel( + [ + panels.FieldPanel("strapline", classname="full title"), + panels.FieldPanel("sessions"), + panels.FieldPanel("cost"), + panels.FieldPanel("intro", classname="full"), + panels.FieldPanel("header_link", classname="col6"), + panels.FieldPanel("header_link_text", classname="col6"), + ], + heading="Hero", + classname="collapsible", + ), + panels.FieldPanel("body"), + ] + + promote_panels = [ + panels.MultiFieldPanel( + wagtail_models.Page.promote_panels, "Common page configuration" + ), + panels.MultiFieldPanel( + utils_models.SocialFields.promote_panels, "Social fields" + ), + ] + + def clean(self): + errors = {} + + if self.header_link and not self.header_link_text: + errors["header_link_text"] = "You must set a text value for the link." + + if self.header_link_text and not self.header_link: + errors["header_link"] = "You must set a link value for the header link." + + if errors: + raise django_exceptions.ValidationError(errors) + + def get_context(self, request): + context = super().get_context(request) + return context diff --git a/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/course_outline_block.html b/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/course_outline_block.html new file mode 100644 index 000000000..00e63aba9 --- /dev/null +++ b/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/course_outline_block.html @@ -0,0 +1,14 @@ +{% load wagtailcore_tags %} + +{% if value.heading %} +

{{ value.heading }}

+{% endif %} + +
    + {% for block in value.course_outline %} +
  1. +

    {{ block.title }}

    + {{ block.text|richtext }} +
  2. + {% endfor %} +
diff --git a/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/course_outline_block.yaml b/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/course_outline_block.yaml new file mode 100644 index 000000000..ed60c7b8e --- /dev/null +++ b/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/course_outline_block.yaml @@ -0,0 +1,16 @@ +context: + value: + heading: About the course + course_outline: + - title: UX Research and Design + text: The UX Research and Design course is a 10-week, part-time course + that will teach you core user experience design skills and how to apply + them in the real world. + - title: UX Research and Design + text: The UX Research and Design course is a 10-week, part-time course + that will teach you core user experience design skills and how to apply + them in the real world. + - title: UX Research and Design + text: The UX Research and Design course is a 10-week, part-time course + that will teach you core user experience design skills and how to apply + them in the real world. diff --git a/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/external_link_cta_block.html b/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/external_link_cta_block.html new file mode 100644 index 000000000..bd9c6b81b --- /dev/null +++ b/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/external_link_cta_block.html @@ -0,0 +1,4 @@ + +

{{ value.heading }}

+

{{ value.text }}

+
diff --git a/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/external_link_cta_block.yaml b/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/external_link_cta_block.yaml new file mode 100644 index 000000000..eeeee59a2 --- /dev/null +++ b/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/external_link_cta_block.yaml @@ -0,0 +1,5 @@ +context: + value: + link: '#' + heading: Check upcoming course dates and apply + text: If the dates don’t work, we can notify you of future courses diff --git a/tbx/project_styleguide/templates/patterns/pages/courses/course_detail_page.html b/tbx/project_styleguide/templates/patterns/pages/courses/course_detail_page.html new file mode 100644 index 000000000..14bb2b27f --- /dev/null +++ b/tbx/project_styleguide/templates/patterns/pages/courses/course_detail_page.html @@ -0,0 +1,27 @@ +{% extends "patterns/base_page.html" %} +{% load wagtailcore_tags wagtailimages_tags static %} + +{% block content %} + +
+
+

+ {{ page.strapline|richtext }} +

+ {% if page.sessions or page.cost %} +

+ {{ page.sessions }} + {% if page.cost and page.sessions %} | {% endif %} {{ page.cost }} +

+ {% endif %} + +
{{ page.intro|richtext }}
+ + {{ page.header_link_text }} + +
+
+ + {% include_block page.body %} + +{% endblock %} diff --git a/tbx/project_styleguide/templates/patterns/pages/courses/course_detail_page.yaml b/tbx/project_styleguide/templates/patterns/pages/courses/course_detail_page.yaml new file mode 100644 index 000000000..71459261d --- /dev/null +++ b/tbx/project_styleguide/templates/patterns/pages/courses/course_detail_page.yaml @@ -0,0 +1,13 @@ +context: + page: + title: Course page + strapline: Course page Example + sessions: 4 sessions + cost: $100 + header_link_text: Find out more + header_link: '#' + +tags: + include_block: + page.body: + template_name: 'patterns/molecules/streamfield/streamfield-example-standard.html' diff --git a/tbx/project_styleguide/templates/patterns/pages/courses/course_landing_page.html b/tbx/project_styleguide/templates/patterns/pages/courses/course_landing_page.html new file mode 100644 index 000000000..06e19875e --- /dev/null +++ b/tbx/project_styleguide/templates/patterns/pages/courses/course_landing_page.html @@ -0,0 +1,10 @@ +{% extends "patterns/base_page.html" %} +{% load wagtailcore_tags wagtailimages_tags static %} + +{% block content %} + + {% include "patterns/molecules/hero/hero.html" with title=page.title %} + + {% include_block page.content %} + +{% endblock %} diff --git a/tbx/project_styleguide/templates/patterns/pages/courses/course_landing_page.yaml b/tbx/project_styleguide/templates/patterns/pages/courses/course_landing_page.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/tbx/settings/base.py b/tbx/settings/base.py index 66ee7ffb9..c073dab21 100644 --- a/tbx/settings/base.py +++ b/tbx/settings/base.py @@ -40,6 +40,7 @@ "scout_apm.django", "tbx.blog", "tbx.core.apps.TorchboxCoreAppConfig", + "tbx.courses", "tbx.events", "tbx.impact_reports", "tbx.navigation",