diff --git a/app/base/__init__.py b/app/base/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/base/apps.py b/app/base/apps.py
new file mode 100644
index 0000000..141b278
--- /dev/null
+++ b/app/base/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class BaseConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "app.base"
diff --git a/app/base/blocks.py b/app/base/blocks.py
new file mode 100644
index 0000000..a70ed80
--- /dev/null
+++ b/app/base/blocks.py
@@ -0,0 +1,48 @@
+from wagtail.blocks import (
+ CharBlock,
+ ChoiceBlock,
+ RichTextBlock,
+ StreamBlock,
+ StructBlock,
+)
+from wagtail.embeds.blocks import EmbedBlock
+from wagtail.images.blocks import ImageBlock
+
+
+class CaptionedImageBlock(StructBlock):
+ image = ImageBlock(required=True)
+ caption = CharBlock(required=False)
+ attribution = CharBlock(required=False)
+
+ class Meta:
+ icon = "image"
+ template = "base/blocks/captioned_image_block.html"
+
+
+class HeadingBlock(StructBlock):
+ heading_text = CharBlock(classname="title", required=True)
+ size = ChoiceBlock(
+ choices=[
+ ("", "Select a heading size"),
+ ("h2", "H2"),
+ ("h3", "H3"),
+ ("h4", "H4"),
+ ],
+ blank=True,
+ required=False,
+ )
+
+ class Meta:
+ icon = "title"
+ template = "base/blocks/heading_block.html"
+
+
+class BaseStreamBlock(StreamBlock):
+ heading_block = HeadingBlock()
+ paragraph_block = RichTextBlock(icon="pilcrow")
+ image_block = CaptionedImageBlock()
+ embed_block = EmbedBlock(
+ help_text="Insert a URL to embed. For example, https://www.youtube.com/watch?v=SGJFWirQ3ks",
+ icon="media",
+ template="base/blocks/embed_block.html",
+ )
diff --git a/app/base/migrations/0001_create_footer.py b/app/base/migrations/0001_create_footer.py
new file mode 100644
index 0000000..585c65c
--- /dev/null
+++ b/app/base/migrations/0001_create_footer.py
@@ -0,0 +1,152 @@
+# Generated by Django 5.1.4 on 2024-12-14 17:23
+
+import uuid
+
+import django.db.models.deletion
+import wagtail.fields
+import wagtail.models
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ("wagtailcore", "0094_alter_page_locale"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="NavigationSettings",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "linkedin_url",
+ models.URLField(blank=True, verbose_name="LinkedIn URL"),
+ ),
+ ("github_url", models.URLField(blank=True, verbose_name="GitHub URL")),
+ (
+ "mastodon_url",
+ models.URLField(blank=True, verbose_name="Mastodon URL"),
+ ),
+ ],
+ options={
+ "abstract": False,
+ },
+ ),
+ migrations.CreateModel(
+ name="FooterText",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "translation_key",
+ models.UUIDField(default=uuid.uuid4, editable=False),
+ ),
+ (
+ "live",
+ models.BooleanField(
+ default=True, editable=False, verbose_name="live"
+ ),
+ ),
+ (
+ "has_unpublished_changes",
+ models.BooleanField(
+ default=False,
+ editable=False,
+ verbose_name="has unpublished changes",
+ ),
+ ),
+ (
+ "first_published_at",
+ models.DateTimeField(
+ blank=True,
+ db_index=True,
+ null=True,
+ verbose_name="first published at",
+ ),
+ ),
+ (
+ "last_published_at",
+ models.DateTimeField(
+ editable=False, null=True, verbose_name="last published at"
+ ),
+ ),
+ (
+ "go_live_at",
+ models.DateTimeField(
+ blank=True, null=True, verbose_name="go live date/time"
+ ),
+ ),
+ (
+ "expire_at",
+ models.DateTimeField(
+ blank=True, null=True, verbose_name="expiry date/time"
+ ),
+ ),
+ (
+ "expired",
+ models.BooleanField(
+ default=False, editable=False, verbose_name="expired"
+ ),
+ ),
+ ("body", wagtail.fields.RichTextField()),
+ (
+ "latest_revision",
+ models.ForeignKey(
+ blank=True,
+ editable=False,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="+",
+ to="wagtailcore.revision",
+ verbose_name="latest revision",
+ ),
+ ),
+ (
+ "live_revision",
+ models.ForeignKey(
+ blank=True,
+ editable=False,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="+",
+ to="wagtailcore.revision",
+ verbose_name="live revision",
+ ),
+ ),
+ (
+ "locale",
+ models.ForeignKey(
+ editable=False,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="+",
+ to="wagtailcore.locale",
+ verbose_name="locale",
+ ),
+ ),
+ ],
+ options={
+ "verbose_name_plural": "Footer Text",
+ "abstract": False,
+ "unique_together": {("translation_key", "locale")},
+ },
+ bases=(wagtail.models.PreviewableMixin, models.Model),
+ ),
+ ]
diff --git a/app/base/migrations/0002_add_form_page.py b/app/base/migrations/0002_add_form_page.py
new file mode 100644
index 0000000..054ed08
--- /dev/null
+++ b/app/base/migrations/0002_add_form_page.py
@@ -0,0 +1,162 @@
+# Generated by Django 5.0.9 on 2024-12-14 17:50
+
+import django.db.models.deletion
+import modelcluster.fields
+import wagtail.contrib.forms.models
+import wagtail.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("base", "0001_create_footer"),
+ ("wagtailcore", "0094_alter_page_locale"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="FormPage",
+ 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",
+ ),
+ ),
+ (
+ "to_address",
+ models.CharField(
+ blank=True,
+ help_text="Optional - form submissions will be emailed to these addresses. Separate multiple addresses by comma.", # noqa: E501
+ max_length=255,
+ validators=[wagtail.contrib.forms.models.validate_to_address],
+ verbose_name="to address",
+ ),
+ ),
+ (
+ "from_address",
+ models.EmailField(
+ blank=True, max_length=255, verbose_name="from address"
+ ),
+ ),
+ (
+ "subject",
+ models.CharField(
+ blank=True, max_length=255, verbose_name="subject"
+ ),
+ ),
+ ("intro", wagtail.fields.RichTextField(blank=True)),
+ ("thank_you_text", wagtail.fields.RichTextField(blank=True)),
+ ],
+ options={
+ "abstract": False,
+ },
+ bases=(
+ wagtail.contrib.forms.models.FormMixin,
+ "wagtailcore.page",
+ models.Model,
+ ),
+ ),
+ migrations.CreateModel(
+ name="FormField",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "sort_order",
+ models.IntegerField(blank=True, editable=False, null=True),
+ ),
+ (
+ "clean_name",
+ models.CharField(
+ blank=True,
+ default="",
+ help_text="Safe name of the form field, the label converted to ascii_snake_case",
+ max_length=255,
+ verbose_name="name",
+ ),
+ ),
+ (
+ "label",
+ models.CharField(
+ help_text="The label of the form field",
+ max_length=255,
+ verbose_name="label",
+ ),
+ ),
+ (
+ "field_type",
+ models.CharField(
+ choices=[
+ ("singleline", "Single line text"),
+ ("multiline", "Multi-line text"),
+ ("email", "Email"),
+ ("number", "Number"),
+ ("url", "URL"),
+ ("checkbox", "Checkbox"),
+ ("checkboxes", "Checkboxes"),
+ ("dropdown", "Drop down"),
+ ("multiselect", "Multiple select"),
+ ("radio", "Radio buttons"),
+ ("date", "Date"),
+ ("datetime", "Date/time"),
+ ("hidden", "Hidden field"),
+ ],
+ max_length=16,
+ verbose_name="field type",
+ ),
+ ),
+ (
+ "required",
+ models.BooleanField(default=True, verbose_name="required"),
+ ),
+ (
+ "choices",
+ models.TextField(
+ blank=True,
+ help_text="Comma or new line separated list of choices. Only applicable in checkboxes, radio and dropdown.", # noqa: E501
+ verbose_name="choices",
+ ),
+ ),
+ (
+ "default_value",
+ models.TextField(
+ blank=True,
+ help_text="Default value. Comma or new line separated values supported for checkboxes.",
+ verbose_name="default value",
+ ),
+ ),
+ (
+ "help_text",
+ models.CharField(
+ blank=True, max_length=255, verbose_name="help text"
+ ),
+ ),
+ (
+ "page",
+ modelcluster.fields.ParentalKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="form_fields",
+ to="base.formpage",
+ ),
+ ),
+ ],
+ options={
+ "ordering": ["sort_order"],
+ "abstract": False,
+ },
+ ),
+ ]
diff --git a/app/base/migrations/__init__.py b/app/base/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/base/models.py b/app/base/models.py
new file mode 100644
index 0000000..1540386
--- /dev/null
+++ b/app/base/models.py
@@ -0,0 +1,94 @@
+from django.db import models
+from modelcluster.fields import ParentalKey
+from wagtail.admin.panels import (
+ FieldPanel,
+ FieldRowPanel,
+ InlinePanel,
+ MultiFieldPanel,
+ PublishingPanel,
+)
+from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField
+from wagtail.contrib.forms.panels import FormSubmissionsPanel
+from wagtail.contrib.settings.models import BaseGenericSetting, register_setting
+from wagtail.fields import RichTextField
+from wagtail.models import (
+ DraftStateMixin,
+ PreviewableMixin,
+ RevisionMixin,
+ TranslatableMixin,
+)
+from wagtail.snippets.models import register_snippet
+
+
+@register_setting
+class NavigationSettings(BaseGenericSetting):
+ linkedin_url = models.URLField(verbose_name="LinkedIn URL", blank=True)
+ github_url = models.URLField(verbose_name="GitHub URL", blank=True)
+ mastodon_url = models.URLField(verbose_name="Mastodon URL", blank=True)
+
+ panels = [
+ MultiFieldPanel(
+ [
+ FieldPanel("linkedin_url"),
+ FieldPanel("github_url"),
+ FieldPanel("mastodon_url"),
+ ],
+ "Social settings",
+ )
+ ]
+
+
+@register_snippet
+class FooterText(
+ DraftStateMixin,
+ RevisionMixin,
+ PreviewableMixin,
+ TranslatableMixin,
+ models.Model,
+):
+ body = RichTextField()
+
+ panels = [
+ FieldPanel("body"),
+ PublishingPanel(),
+ ]
+
+ class Meta(TranslatableMixin.Meta):
+ verbose_name_plural = "Footer Text"
+
+ def __str__(self):
+ return "Footer text"
+
+ def get_preview_template(self, request, mode_name):
+ return "base.html"
+
+ def get_preview_context(self, request, mode_name):
+ return {"footer_text": self.body}
+
+
+class FormField(AbstractFormField):
+ page = ParentalKey("FormPage", on_delete=models.CASCADE, related_name="form_fields")
+
+
+class FormPage(AbstractEmailForm):
+ intro = RichTextField(blank=True)
+ thank_you_text = RichTextField(blank=True)
+
+ content_panels = AbstractEmailForm.content_panels + [
+ FormSubmissionsPanel(),
+ FieldPanel("intro"),
+ InlinePanel("form_fields", label="Form fields"),
+ FieldPanel("thank_you_text"),
+ MultiFieldPanel(
+ [
+ FieldRowPanel(
+ [
+ FieldPanel("from_address"),
+ FieldPanel("to_address"),
+ ]
+ ),
+ FieldPanel("subject"),
+ ],
+ "Email",
+ ),
+ ]
diff --git a/app/base/templates/base/blocks/captioned_image_block.html b/app/base/templates/base/blocks/captioned_image_block.html
new file mode 100644
index 0000000..453ed66
--- /dev/null
+++ b/app/base/templates/base/blocks/captioned_image_block.html
@@ -0,0 +1,9 @@
+{% load wagtailimages_tags %}
+
+
Welcome to our blog! Here you'll find the latest news, " "insights, and stories from our team.
" ), + show_in_menus=True, ) home_page.add_child(instance=blog_index) blog_index.save_revision().publish() diff --git a/app/blog/templates/blog/blog_index_page.html b/app/blog/templates/blog/blog_index_page.html index f208bca..20227cb 100644 --- a/app/blog/templates/blog/blog_index_page.html +++ b/app/blog/templates/blog/blog_index_page.html @@ -5,44 +5,49 @@ {% block body_class %}template-blogindexpage{% endblock %} {% block content %} -{{ post.intro }}
- {{ post.body|richtext }} -{{ post.intro }}
+ {% comment %} Don't display the body text here + {{ post.body|richtext }} + {% endcomment %} +Posted by:
- {% for author in authors %} - - {% image author.author_image fill-40x40 as author_image %} - {% if author_image %} -{{ page.intro }}
- {{ page.body|richtext }} - - -Happy building with Wagtail!
""".strip() + if hero_image: + # Update the hero section fields + home_page.image = hero_image + home_page.hero_text = hero_text + home_page.hero_cta = hero_cta + # Update the home page body content home_page.body = sample_content home_page.save() diff --git a/app/home/migrations/0004_customise_home_page.py b/app/home/migrations/0004_customise_home_page.py new file mode 100644 index 0000000..5b1ee9d --- /dev/null +++ b/app/home/migrations/0004_customise_home_page.py @@ -0,0 +1,60 @@ +# Generated by Django 5.1.4 on 2024-12-14 17:13 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("home", "0003_homepage_body"), + ("wagtailcore", "0094_alter_page_locale"), + ("wagtailimages", "0027_image_description"), + ] + + operations = [ + migrations.AddField( + model_name="homepage", + name="hero_cta", + field=models.CharField( + blank=True, + help_text="Text to display on Call to Action", + max_length=255, + verbose_name="Hero CTA", + ), + ), + migrations.AddField( + model_name="homepage", + name="hero_cta_link", + field=models.ForeignKey( + blank=True, + help_text="Choose a page to link to for the Call to Action", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="wagtailcore.page", + verbose_name="Hero CTA link", + ), + ), + migrations.AddField( + model_name="homepage", + name="hero_text", + field=models.CharField( + blank=True, + help_text="Write an introduction for the site", + max_length=255, + ), + ), + migrations.AddField( + model_name="homepage", + name="image", + field=models.ForeignKey( + blank=True, + help_text="Homepage image", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="wagtailimages.image", + ), + ), + ] diff --git a/app/home/models.py b/app/home/models.py index fe0adbe..7527f15 100644 --- a/app/home/models.py +++ b/app/home/models.py @@ -1,11 +1,48 @@ -from wagtail.admin.panels import FieldPanel +from django.db import models +from wagtail.admin.panels import FieldPanel, MultiFieldPanel from wagtail.fields import RichTextField from wagtail.models import Page class HomePage(Page): + image = models.ForeignKey( + "wagtailimages.Image", + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="+", + help_text="Homepage image", + ) + hero_text = models.CharField( + blank=True, max_length=255, help_text="Write an introduction for the site" + ) + hero_cta = models.CharField( + blank=True, + verbose_name="Hero CTA", + max_length=255, + help_text="Text to display on Call to Action", + ) + hero_cta_link = models.ForeignKey( + "wagtailcore.Page", + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="+", + verbose_name="Hero CTA link", + help_text="Choose a page to link to for the Call to Action", + ) + body = RichTextField(blank=True) content_panels = Page.content_panels + [ + MultiFieldPanel( + [ + FieldPanel("image"), + FieldPanel("hero_text"), + FieldPanel("hero_cta"), + FieldPanel("hero_cta_link"), + ], + heading="Hero section", + ), FieldPanel("body"), ] diff --git a/app/home/templates/home/home_page.html b/app/home/templates/home/home_page.html index 86b072e..7513ec9 100644 --- a/app/home/templates/home/home_page.html +++ b/app/home/templates/home/home_page.html @@ -1,15 +1,24 @@ {% extends "base.html" %} - -{% load wagtailcore_tags %} +{% load wagtailcore_tags wagtailimages_tags %} {% block body_class %}template-homepage{% endblock %} {% block content %} -No content has been added to this page body yet.
- {% endif %} -{{ self.text|richtext }}
+ {% endif %} + +{{ page.specific.date }}
+You searched{% if search_query %} for “{{ search_query }}”{% endif %}, {{ search_results.paginator.count }} result{{ search_results.paginator.count|pluralize }} found.
+ +Page {{ search_results.number }} of {{ search_results.paginator.num_pages }}, showing {{ search_results|length }} result{{ search_results|pluralize }} out of {{ search_results.paginator.count }}
+ {% endif %} + + {% if search_results.has_previous %} + Previous + {% endif %} + + {% if search_results.has_next %} + Next + {% endif %} + + {% elif search_query %} + + No results found + + {% endif %} + + {% endblock %} + +