From 86d919291eae42b69445da2a29e90e2a01329c81 Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Sun, 6 Oct 2024 13:20:11 +0100 Subject: [PATCH 01/40] Extend the home page model --- app/home/migrations/0003_homepage_body.py | 19 +++++++++++++++++++ app/home/models.py | 12 +++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 app/home/migrations/0003_homepage_body.py diff --git a/app/home/migrations/0003_homepage_body.py b/app/home/migrations/0003_homepage_body.py new file mode 100644 index 0000000..7a81014 --- /dev/null +++ b/app/home/migrations/0003_homepage_body.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.1 on 2024-10-06 12:19 + +import wagtail.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("home", "0002_create_homepage"), + ] + + operations = [ + migrations.AddField( + model_name="homepage", + name="body", + field=wagtail.fields.RichTextField(blank=True), + ), + ] diff --git a/app/home/models.py b/app/home/models.py index a8a0cc2..fe0adbe 100644 --- a/app/home/models.py +++ b/app/home/models.py @@ -1,9 +1,11 @@ -from wagtail import __version__ as WAGTAIL_VERSION +from wagtail.admin.panels import FieldPanel +from wagtail.fields import RichTextField from wagtail.models import Page class HomePage(Page): - def get_context(self, request): - context = super().get_context(request) - context["wagtail_version"] = WAGTAIL_VERSION - return context + body = RichTextField(blank=True) + + content_panels = Page.content_panels + [ + FieldPanel("body"), + ] From 73654ea43403de1709ed056adb041e773822b776 Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Sun, 6 Oct 2024 13:24:25 +0100 Subject: [PATCH 02/40] Update homepage template Remove welcome page --- app/home/templates/home/home_page.html | 144 +----------------- app/home/templates/home/welcome_page.html | 175 ---------------------- 2 files changed, 6 insertions(+), 313 deletions(-) delete mode 100644 app/home/templates/home/welcome_page.html diff --git a/app/home/templates/home/home_page.html b/app/home/templates/home/home_page.html index 0b1dc65..02be91b 100644 --- a/app/home/templates/home/home_page.html +++ b/app/home/templates/home/home_page.html @@ -1,143 +1,11 @@ {% extends "base.html" %} -{% load static %} - -{% block body_class %}template-homepage{% endblock %} - -{% block extra_css %} - -{% comment %} -Delete the line below if you're just getting started and want to remove the welcome screen! -{% endcomment %} - -{% endblock extra_css %} +{% block body_class %}template-homepage{% endblock %} + {% block content %} - -{% comment %} -Delete the line below if you're just getting started and want to remove the welcome screen! -{% endcomment %} -{% include 'home/welcome_page.html' %} - -{% endblock content %} + {{ page.body|richtext }} +{% endblock %} diff --git a/app/home/templates/home/welcome_page.html b/app/home/templates/home/welcome_page.html deleted file mode 100644 index 23e51fd..0000000 --- a/app/home/templates/home/welcome_page.html +++ /dev/null @@ -1,175 +0,0 @@ -{% load i18n wagtailcore_tags %} - -
- -
- -
-
-
- -
-
-
-

{% trans "Welcome to your new Wagtail site!" %}

-

{% trans " by Wagtail Starter Kit" %}

-
-
    -
  • - - archive-content - - - - - - - - Docker Development Environment -
  • -
  • - - database - - - - - - Postgresql, Mysql or Sqlite3 Database -
  • -
  • - - cycle - - - - - - Frontend Node SASS and Javascript compilation -
  • -
  • - - style - - - - - - - - - Pico CSS for almost classless styling -
  • -
  • - - questionnaire - - - - - - - - esbuild javascript bundler -
  • -
  • - - patch-19 - - - - - - - - - - - - - - - - - Wagtail CMS v{{ wagtail_version }} -
  • -
-
-
- -
- - From 84a8342d3d4962189558ebf5d0f0b7183c006198 Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Sun, 6 Oct 2024 13:27:04 +0100 Subject: [PATCH 03/40] Add blog app and BlogIndexPage model --- app/blog/__init__.py | 0 app/blog/apps.py | 6 +++++ app/blog/migrations/0001_initial.py | 38 +++++++++++++++++++++++++++++ app/blog/migrations/__init__.py | 0 app/blog/models.py | 9 +++++++ app/settings/base.py | 1 + 6 files changed, 54 insertions(+) create mode 100644 app/blog/__init__.py create mode 100644 app/blog/apps.py create mode 100644 app/blog/migrations/0001_initial.py create mode 100644 app/blog/migrations/__init__.py create mode 100644 app/blog/models.py diff --git a/app/blog/__init__.py b/app/blog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/blog/apps.py b/app/blog/apps.py new file mode 100644 index 0000000..e0e9de1 --- /dev/null +++ b/app/blog/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BlogConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "app.blog" diff --git a/app/blog/migrations/0001_initial.py b/app/blog/migrations/0001_initial.py new file mode 100644 index 0000000..8978d32 --- /dev/null +++ b/app/blog/migrations/0001_initial.py @@ -0,0 +1,38 @@ +# Generated by Django 5.1.1 on 2024-10-06 12:26 + +import django.db.models.deletion +import wagtail.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("wagtailcore", "0094_alter_page_locale"), + ] + + operations = [ + migrations.CreateModel( + name="BlogIndexPage", + 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", + ), + ), + ("intro", wagtail.fields.RichTextField(blank=True)), + ], + options={ + "abstract": False, + }, + bases=("wagtailcore.page",), + ), + ] diff --git a/app/blog/migrations/__init__.py b/app/blog/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/blog/models.py b/app/blog/models.py new file mode 100644 index 0000000..556f69b --- /dev/null +++ b/app/blog/models.py @@ -0,0 +1,9 @@ +from wagtail.admin.panels import FieldPanel +from wagtail.fields import RichTextField +from wagtail.models import Page + + +class BlogIndexPage(Page): + intro = RichTextField(blank=True) + + content_panels = Page.content_panels + [FieldPanel("intro")] diff --git a/app/settings/base.py b/app/settings/base.py index d0cf7b9..c85d76a 100644 --- a/app/settings/base.py +++ b/app/settings/base.py @@ -27,6 +27,7 @@ INSTALLED_APPS = [ "app.home", "app.search", + "app.blog", "wagtail.contrib.forms", "wagtail.contrib.redirects", "wagtail.contrib.table_block", From eb11f71553d4bfeb156145fa65e8aa69ca28bffe Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Sun, 6 Oct 2024 13:29:44 +0100 Subject: [PATCH 04/40] Add blog index page template --- app/blog/templates/blog/blog_index_page.html | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 app/blog/templates/blog/blog_index_page.html diff --git a/app/blog/templates/blog/blog_index_page.html b/app/blog/templates/blog/blog_index_page.html new file mode 100644 index 0000000..1b542cf --- /dev/null +++ b/app/blog/templates/blog/blog_index_page.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% load wagtailcore_tags %} + +{% block body_class %}template-blogindexpage{% endblock %} + +{% block content %} +

{{ page.title }}

+ +
{{ page.intro|richtext }}
+ + {% for post in page.get_children %} +

{{ post.title }}

+ {{ post.specific.intro }} + {{ post.specific.body|richtext }} + {% endfor %} + +{% endblock %} From 5d901c48b3b2e16dbd766408273c2c9b23863147 Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Sun, 6 Oct 2024 13:31:47 +0100 Subject: [PATCH 05/40] Add blog page model --- app/blog/migrations/0002_blogpage.py | 39 ++++++++++++++++++++++++++++ app/blog/models.py | 19 ++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 app/blog/migrations/0002_blogpage.py diff --git a/app/blog/migrations/0002_blogpage.py b/app/blog/migrations/0002_blogpage.py new file mode 100644 index 0000000..816ec55 --- /dev/null +++ b/app/blog/migrations/0002_blogpage.py @@ -0,0 +1,39 @@ +# Generated by Django 5.1.1 on 2024-10-06 12:31 + +import django.db.models.deletion +import wagtail.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("blog", "0001_initial"), + ("wagtailcore", "0094_alter_page_locale"), + ] + + operations = [ + migrations.CreateModel( + name="BlogPage", + 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", + ), + ), + ("date", models.DateField(verbose_name="Post date")), + ("intro", models.CharField(max_length=250)), + ("body", wagtail.fields.RichTextField(blank=True)), + ], + options={ + "abstract": False, + }, + bases=("wagtailcore.page",), + ), + ] diff --git a/app/blog/models.py b/app/blog/models.py index 556f69b..8154d62 100644 --- a/app/blog/models.py +++ b/app/blog/models.py @@ -1,9 +1,28 @@ +from django.db import models from wagtail.admin.panels import FieldPanel from wagtail.fields import RichTextField from wagtail.models import Page +from wagtail.search import index class BlogIndexPage(Page): intro = RichTextField(blank=True) content_panels = Page.content_panels + [FieldPanel("intro")] + + +class BlogPage(Page): + date = models.DateField("Post date") + intro = models.CharField(max_length=250) + body = RichTextField(blank=True) + + search_fields = Page.search_fields + [ + index.SearchField("intro"), + index.SearchField("body"), + ] + + content_panels = Page.content_panels + [ + FieldPanel("date"), + FieldPanel("intro"), + FieldPanel("body"), + ] From fbc8548304ea0118f0f988a5748c035be7876f0f Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Sun, 6 Oct 2024 13:34:51 +0100 Subject: [PATCH 06/40] Add blog page template --- app/blog/templates/blog/blog_page.html | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 app/blog/templates/blog/blog_page.html diff --git a/app/blog/templates/blog/blog_page.html b/app/blog/templates/blog/blog_page.html new file mode 100644 index 0000000..150118c --- /dev/null +++ b/app/blog/templates/blog/blog_page.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% load wagtailcore_tags %} + +{% block body_class %}template-blogpage{% endblock %} + +{% block content %} +

{{ page.title }}

+

{{ page.date }}

+ +
{{ page.intro }}
+ + {{ page.body|richtext }} + +

Return to blog

+ +{% endblock %} From 2b4adf1afb4242d825c150604e8425120e1da7c8 Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Sun, 6 Oct 2024 13:35:58 +0100 Subject: [PATCH 07/40] Modify blog index page --- app/blog/templates/blog/blog_index_page.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/blog/templates/blog/blog_index_page.html b/app/blog/templates/blog/blog_index_page.html index 1b542cf..3c13e30 100644 --- a/app/blog/templates/blog/blog_index_page.html +++ b/app/blog/templates/blog/blog_index_page.html @@ -10,9 +10,11 @@

{{ page.title }}

{{ page.intro|richtext }}
{% for post in page.get_children %} + {% with post=post.specific %}

{{ post.title }}

- {{ post.specific.intro }} - {{ post.specific.body|richtext }} - {% endfor %} +

{{ post.intro }}

+ {{ post.body|richtext }} + {% endwith %} +{% endfor %} {% endblock %} From 11fd3ffbb1cf610dd953a1a723f7d8ac2bff11a6 Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Sun, 6 Oct 2024 13:43:40 +0100 Subject: [PATCH 08/40] Order blog posts by reverse-chronological order --- app/blog/models.py | 7 +++++++ app/blog/templates/blog/blog_index_page.html | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/blog/models.py b/app/blog/models.py index 8154d62..632d9d3 100644 --- a/app/blog/models.py +++ b/app/blog/models.py @@ -10,6 +10,13 @@ class BlogIndexPage(Page): content_panels = Page.content_panels + [FieldPanel("intro")] + def get_context(self, request): + # Update context to include only published posts, ordered by reverse-chron + context = super().get_context(request) + blogpages = self.get_children().live().order_by("-first_published_at") + context["blogpages"] = blogpages + return context + class BlogPage(Page): date = models.DateField("Post date") diff --git a/app/blog/templates/blog/blog_index_page.html b/app/blog/templates/blog/blog_index_page.html index 3c13e30..e32f579 100644 --- a/app/blog/templates/blog/blog_index_page.html +++ b/app/blog/templates/blog/blog_index_page.html @@ -9,7 +9,7 @@

{{ page.title }}

{{ page.intro|richtext }}
- {% for post in page.get_children %} + {% for post in blogpages %} {% with post=post.specific %}

{{ post.title }}

{{ post.intro }}

From 3f4374e6db7c01d3ccd62e9f35c26a1d04cc7af6 Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Sun, 6 Oct 2024 13:47:36 +0100 Subject: [PATCH 09/40] Add gallery images to BlogPage --- .../migrations/0003_blogpagegalleryimage.py | 55 +++++++++++++++++++ app/blog/models.py | 21 ++++++- 2 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 app/blog/migrations/0003_blogpagegalleryimage.py diff --git a/app/blog/migrations/0003_blogpagegalleryimage.py b/app/blog/migrations/0003_blogpagegalleryimage.py new file mode 100644 index 0000000..7b75e12 --- /dev/null +++ b/app/blog/migrations/0003_blogpagegalleryimage.py @@ -0,0 +1,55 @@ +# Generated by Django 5.1.1 on 2024-10-06 12:46 + +import django.db.models.deletion +import modelcluster.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("blog", "0002_blogpage"), + ("wagtailimages", "0026_delete_uploadedimage"), + ] + + operations = [ + migrations.CreateModel( + name="BlogPageGalleryImage", + 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), + ), + ("caption", models.CharField(blank=True, max_length=250)), + ( + "image", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="wagtailimages.image", + ), + ), + ( + "page", + modelcluster.fields.ParentalKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="gallery_images", + to="blog.blogpage", + ), + ), + ], + options={ + "ordering": ["sort_order"], + "abstract": False, + }, + ), + ] diff --git a/app/blog/models.py b/app/blog/models.py index 632d9d3..329692e 100644 --- a/app/blog/models.py +++ b/app/blog/models.py @@ -1,7 +1,8 @@ from django.db import models -from wagtail.admin.panels import FieldPanel +from modelcluster.fields import ParentalKey +from wagtail.admin.panels import FieldPanel, InlinePanel from wagtail.fields import RichTextField -from wagtail.models import Page +from wagtail.models import Orderable, Page from wagtail.search import index @@ -32,4 +33,20 @@ class BlogPage(Page): FieldPanel("date"), FieldPanel("intro"), FieldPanel("body"), + InlinePanel("gallery_images", label="Gallery images"), + ] + + +class BlogPageGalleryImage(Orderable): + page = ParentalKey( + BlogPage, on_delete=models.CASCADE, related_name="gallery_images" + ) + image = models.ForeignKey( + "wagtailimages.Image", on_delete=models.CASCADE, related_name="+" + ) + caption = models.CharField(blank=True, max_length=250) + + panels = [ + FieldPanel("image"), + FieldPanel("caption"), ] From fb67059f6131eb77a0756146a56a09f9b1a34010 Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Sun, 6 Oct 2024 13:49:50 +0100 Subject: [PATCH 10/40] Update blog page template to display gallery images --- app/blog/templates/blog/blog_page.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/blog/templates/blog/blog_page.html b/app/blog/templates/blog/blog_page.html index 150118c..350fc24 100644 --- a/app/blog/templates/blog/blog_page.html +++ b/app/blog/templates/blog/blog_page.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% load wagtailcore_tags %} +{% load wagtailcore_tags wagtailimages_tags %} {% block body_class %}template-blogpage{% endblock %} @@ -12,6 +12,13 @@

{{ page.title }}

{{ page.body|richtext }} + {% for item in page.gallery_images.all %} +
+ {% image item.image fill-320x240 %} +

{{ item.caption }}

+
+ {% endfor %} +

Return to blog

{% endblock %} From 12c05f48d4d300db1473db8962e186097a12dc39 Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Sun, 6 Oct 2024 13:50:55 +0100 Subject: [PATCH 11/40] Add main_image method to BlogPage model --- app/blog/models.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/blog/models.py b/app/blog/models.py index 329692e..828b100 100644 --- a/app/blog/models.py +++ b/app/blog/models.py @@ -36,6 +36,13 @@ class BlogPage(Page): InlinePanel("gallery_images", label="Gallery images"), ] + def main_image(self): + gallery_item = self.gallery_images.first() + if gallery_item: + return gallery_item.image + else: + return None + class BlogPageGalleryImage(Orderable): page = ParentalKey( From 0285b51435046bc7ca16c5e4394b8d85b3758e1d Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Sun, 6 Oct 2024 21:08:48 +0100 Subject: [PATCH 12/40] Add main image to blog index page template --- app/blog/templates/blog/blog_index_page.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/blog/templates/blog/blog_index_page.html b/app/blog/templates/blog/blog_index_page.html index e32f579..435ece8 100644 --- a/app/blog/templates/blog/blog_index_page.html +++ b/app/blog/templates/blog/blog_index_page.html @@ -1,17 +1,21 @@ {% extends "base.html" %} -{% load wagtailcore_tags %} +{% load wagtailcore_tags wagtailimages_tags %} {% block body_class %}template-blogindexpage{% endblock %} {% block content %}

{{ page.title }}

+
{{ page.intro|richtext }}
{% for post in blogpages %} {% with post=post.specific %} -

{{ post.title }}

+

{{ post.title }}

+ {% with post.main_image as main_image %} + {% if main_image %}{% image main_image fill-160x100 %}{% endif %} + {% endwith %}

{{ post.intro }}

{{ post.body|richtext }} {% endwith %} From 22362c0b5cbd263fd748b6d88cbaa8f78a15a608 Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Sun, 6 Oct 2024 21:12:19 +0100 Subject: [PATCH 13/40] Add Author model --- app/blog/migrations/0004_author.py | 43 ++++++++++++++++++++++++++++++ app/blog/models.py | 24 +++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 app/blog/migrations/0004_author.py diff --git a/app/blog/migrations/0004_author.py b/app/blog/migrations/0004_author.py new file mode 100644 index 0000000..6c63a3d --- /dev/null +++ b/app/blog/migrations/0004_author.py @@ -0,0 +1,43 @@ +# Generated by Django 5.1.1 on 2024-10-06 20:11 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("blog", "0003_blogpagegalleryimage"), + ("wagtailimages", "0026_delete_uploadedimage"), + ] + + operations = [ + migrations.CreateModel( + name="Author", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ( + "author_image", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="wagtailimages.image", + ), + ), + ], + options={ + "verbose_name_plural": "Authors", + }, + ), + ] diff --git a/app/blog/models.py b/app/blog/models.py index 828b100..bbe49d4 100644 --- a/app/blog/models.py +++ b/app/blog/models.py @@ -4,6 +4,7 @@ from wagtail.fields import RichTextField from wagtail.models import Orderable, Page from wagtail.search import index +from wagtail.snippets.models import register_snippet class BlogIndexPage(Page): @@ -57,3 +58,26 @@ class BlogPageGalleryImage(Orderable): FieldPanel("image"), FieldPanel("caption"), ] + + +@register_snippet +class Author(models.Model): + name = models.CharField(max_length=255) + author_image = models.ForeignKey( + "wagtailimages.Image", + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="+", + ) + + panels = [ + FieldPanel("name"), + FieldPanel("author_image"), + ] + + def __str__(self): + return self.name + + class Meta: + verbose_name_plural = "Authors" From c5adc97dee8afd146a66ccd681e49f5d42a96fac Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Sun, 6 Oct 2024 21:17:02 +0100 Subject: [PATCH 14/40] Add authors to BlogPage model --- app/blog/migrations/0005_blogpage_authors.py | 21 ++++++++++++++++++++ app/blog/models.py | 14 ++++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 app/blog/migrations/0005_blogpage_authors.py diff --git a/app/blog/migrations/0005_blogpage_authors.py b/app/blog/migrations/0005_blogpage_authors.py new file mode 100644 index 0000000..e0e4965 --- /dev/null +++ b/app/blog/migrations/0005_blogpage_authors.py @@ -0,0 +1,21 @@ +# Generated by Django 5.1.1 on 2024-10-06 20:15 + +import modelcluster.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("blog", "0004_author"), + ] + + operations = [ + migrations.AddField( + model_name="blogpage", + name="authors", + field=modelcluster.fields.ParentalManyToManyField( + blank=True, to="blog.author" + ), + ), + ] diff --git a/app/blog/models.py b/app/blog/models.py index bbe49d4..babc7ef 100644 --- a/app/blog/models.py +++ b/app/blog/models.py @@ -1,6 +1,7 @@ +from django import forms from django.db import models -from modelcluster.fields import ParentalKey -from wagtail.admin.panels import FieldPanel, InlinePanel +from modelcluster.fields import ParentalKey, ParentalManyToManyField +from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel from wagtail.fields import RichTextField from wagtail.models import Orderable, Page from wagtail.search import index @@ -24,6 +25,7 @@ class BlogPage(Page): date = models.DateField("Post date") intro = models.CharField(max_length=250) body = RichTextField(blank=True) + authors = ParentalManyToManyField("blog.Author", blank=True) search_fields = Page.search_fields + [ index.SearchField("intro"), @@ -31,7 +33,13 @@ class BlogPage(Page): ] content_panels = Page.content_panels + [ - FieldPanel("date"), + MultiFieldPanel( + [ + FieldPanel("date"), + FieldPanel("authors", widget=forms.CheckboxSelectMultiple), + ], + heading="Blog information", + ), FieldPanel("intro"), FieldPanel("body"), InlinePanel("gallery_images", label="Gallery images"), From 392cac6389a3d0ffb0a576affddee664d80011bb Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Sun, 6 Oct 2024 21:21:30 +0100 Subject: [PATCH 15/40] Add author details to blog post template --- app/blog/templates/blog/blog_page.html | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/blog/templates/blog/blog_page.html b/app/blog/templates/blog/blog_page.html index 350fc24..57cdc2d 100644 --- a/app/blog/templates/blog/blog_page.html +++ b/app/blog/templates/blog/blog_page.html @@ -8,6 +8,20 @@

{{ page.title }}

{{ page.date }}

+ {% with authors=page.authors.all %} + {% if authors %} +

Posted by:

+
    + {% for author in authors %} +
  • + {% image author.author_image fill-40x60 style="vertical-align: middle" %} + {{ author.name }} +
  • + {% endfor %} +
+ {% endif %} + {% endwith %} +
{{ page.intro }}
{{ page.body|richtext }} From 095a4de2f4c306d31aba8be2d4ca2f5bd725895c Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Sun, 6 Oct 2024 21:25:08 +0100 Subject: [PATCH 16/40] Add tags to the blog pages --- .../0006_blogpagetag_blogpage_tags.py | 64 +++++++++++++++++++ app/blog/models.py | 10 +++ 2 files changed, 74 insertions(+) create mode 100644 app/blog/migrations/0006_blogpagetag_blogpage_tags.py diff --git a/app/blog/migrations/0006_blogpagetag_blogpage_tags.py b/app/blog/migrations/0006_blogpagetag_blogpage_tags.py new file mode 100644 index 0000000..fc4b7b6 --- /dev/null +++ b/app/blog/migrations/0006_blogpagetag_blogpage_tags.py @@ -0,0 +1,64 @@ +# Generated by Django 5.1.1 on 2024-10-06 20:23 + +import django.db.models.deletion +import modelcluster.contrib.taggit +import modelcluster.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("blog", "0005_blogpage_authors"), + ( + "taggit", + "0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx", + ), + ] + + operations = [ + migrations.CreateModel( + name="BlogPageTag", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "content_object", + modelcluster.fields.ParentalKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="tagged_items", + to="blog.blogpage", + ), + ), + ( + "tag", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)s_items", + to="taggit.tag", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AddField( + model_name="blogpage", + name="tags", + field=modelcluster.contrib.taggit.ClusterTaggableManager( + blank=True, + help_text="A comma-separated list of tags.", + through="blog.BlogPageTag", + to="taggit.Tag", + verbose_name="Tags", + ), + ), + ] diff --git a/app/blog/models.py b/app/blog/models.py index babc7ef..7ea4388 100644 --- a/app/blog/models.py +++ b/app/blog/models.py @@ -1,6 +1,8 @@ from django import forms from django.db import models +from modelcluster.contrib.taggit import ClusterTaggableManager from modelcluster.fields import ParentalKey, ParentalManyToManyField +from taggit.models import TaggedItemBase from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel from wagtail.fields import RichTextField from wagtail.models import Orderable, Page @@ -21,11 +23,18 @@ def get_context(self, request): return context +class BlogPageTag(TaggedItemBase): + content_object = ParentalKey( + "BlogPage", related_name="tagged_items", on_delete=models.CASCADE + ) + + class BlogPage(Page): date = models.DateField("Post date") intro = models.CharField(max_length=250) body = RichTextField(blank=True) authors = ParentalManyToManyField("blog.Author", blank=True) + tags = ClusterTaggableManager(through=BlogPageTag, blank=True) search_fields = Page.search_fields + [ index.SearchField("intro"), @@ -37,6 +46,7 @@ class BlogPage(Page): [ FieldPanel("date"), FieldPanel("authors", widget=forms.CheckboxSelectMultiple), + FieldPanel("tags"), ], heading="Blog information", ), From f51d1c194eeb6591643b989ff0cc5b14b6db81c0 Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Sun, 6 Oct 2024 21:27:49 +0100 Subject: [PATCH 17/40] Add tags to blog page template --- app/blog/templates/blog/blog_page.html | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/blog/templates/blog/blog_page.html b/app/blog/templates/blog/blog_page.html index 57cdc2d..cb17ae0 100644 --- a/app/blog/templates/blog/blog_page.html +++ b/app/blog/templates/blog/blog_page.html @@ -35,4 +35,15 @@

Posted by:

Return to blog

+ {% with tags=page.tags.all %} + {% if tags %} +
+

Tags

+ {% for tag in tags %} + + {% endfor %} +
+ {% endif %} + {% endwith %} + {% endblock %} From a4e60122b28eef891417dfaaa54574c8baf6881b Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Sun, 6 Oct 2024 21:30:35 +0100 Subject: [PATCH 18/40] Add BlogTagIndexPage to models --- app/blog/migrations/0007_blogtagindexpage.py | 35 ++++++++++++++++++++ app/blog/models.py | 14 ++++++++ 2 files changed, 49 insertions(+) create mode 100644 app/blog/migrations/0007_blogtagindexpage.py diff --git a/app/blog/migrations/0007_blogtagindexpage.py b/app/blog/migrations/0007_blogtagindexpage.py new file mode 100644 index 0000000..0087249 --- /dev/null +++ b/app/blog/migrations/0007_blogtagindexpage.py @@ -0,0 +1,35 @@ +# Generated by Django 5.1.1 on 2024-10-06 20:31 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("blog", "0006_blogpagetag_blogpage_tags"), + ("wagtailcore", "0094_alter_page_locale"), + ] + + operations = [ + migrations.CreateModel( + name="BlogTagIndexPage", + 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", + ), + ), + ], + options={ + "abstract": False, + }, + bases=("wagtailcore.page",), + ), + ] diff --git a/app/blog/models.py b/app/blog/models.py index 7ea4388..e71e935 100644 --- a/app/blog/models.py +++ b/app/blog/models.py @@ -99,3 +99,17 @@ def __str__(self): class Meta: verbose_name_plural = "Authors" + + +class BlogTagIndexPage(Page): + + def get_context(self, request): + + # Filter by tag + tag = request.GET.get("tag") + blogpages = BlogPage.objects.filter(tags__name=tag) + + # Update template context + context = super().get_context(request) + context["blogpages"] = blogpages + return context From 53b1aae7840f9f3c891d9c87b28fd7ddedb4c131 Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Sun, 6 Oct 2024 21:36:25 +0100 Subject: [PATCH 19/40] Add blog tag index page template --- .../templates/blog/blog_tag_index_page.html | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 app/blog/templates/blog/blog_tag_index_page.html diff --git a/app/blog/templates/blog/blog_tag_index_page.html b/app/blog/templates/blog/blog_tag_index_page.html new file mode 100644 index 0000000..c8ddcca --- /dev/null +++ b/app/blog/templates/blog/blog_tag_index_page.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} +{% load wagtailcore_tags %} + +{% block content %} + + {% if request.GET.tag %} +

Showing pages tagged "{{ request.GET.tag }}"

+ {% endif %} + + {% for blogpage in blogpages %} + +

+ {{ blogpage.title }}
+ Revised: {{ blogpage.latest_revision_created_at }}
+

+ + {% empty %} + No pages found with that tag. + {% endfor %} + +{% endblock %} From 2ad7d86dfd6644cf0173dcf491c58c3165186588 Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Sun, 6 Oct 2024 22:38:40 +0100 Subject: [PATCH 20/40] Implement pico styling --- app/blog/templates/blog/blog_index_page.html | 48 ++++++--- app/blog/templates/blog/blog_page.html | 98 ++++++++++++------- .../templates/blog/blog_tag_index_page.html | 34 ++++--- app/home/templates/home/home_page.html | 6 +- app/home/templatetags/__init__.py | 0 app/home/templatetags/navigation_tags.py | 10 ++ app/templates/base.html | 25 ++++- 7 files changed, 157 insertions(+), 64 deletions(-) create mode 100644 app/home/templatetags/__init__.py create mode 100644 app/home/templatetags/navigation_tags.py diff --git a/app/blog/templates/blog/blog_index_page.html b/app/blog/templates/blog/blog_index_page.html index 435ece8..f208bca 100644 --- a/app/blog/templates/blog/blog_index_page.html +++ b/app/blog/templates/blog/blog_index_page.html @@ -5,20 +5,44 @@ {% block body_class %}template-blogindexpage{% endblock %} {% block content %} -

{{ page.title }}

+
+

{{ page.title }}

+
{{ page.intro|richtext }}
+
+ {% for post in blogpages %} -
{{ page.intro|richtext }}
+ {% if forloop.first or forloop.counter0|divisibleby:2 %} + {% comment %} Start a new row {% endcomment %} +
+ {% endif %} - {% for post in blogpages %} - {% with post=post.specific %} -

{{ post.title }}

- {% with post.main_image as main_image %} - {% if main_image %}{% image main_image fill-160x100 %}{% endif %} - {% endwith %} -

{{ post.intro }}

- {{ post.body|richtext }} - {% endwith %} -{% endfor %} +
+ {% with post=post.specific %} +
+

{{ post.title }}

+
+
+ {% with post.main_image as main_image %} + {% if main_image %} +
+ {% image main_image fill-160x100 %} +
+ {% endif %} + {% endwith %} +
+

{{ post.intro }}

+ {{ post.body|richtext }} +
+
+ {% endwith %} +
+ + {% if forloop.last or forloop.counter|divisibleby:2 %} + {% comment %} End the row {% endcomment %} +
+ {% endif %} + + {% endfor %} {% endblock %} diff --git a/app/blog/templates/blog/blog_page.html b/app/blog/templates/blog/blog_page.html index cb17ae0..1ffb43d 100644 --- a/app/blog/templates/blog/blog_page.html +++ b/app/blog/templates/blog/blog_page.html @@ -1,49 +1,77 @@ {% extends "base.html" %} -{% load wagtailcore_tags wagtailimages_tags %} +{% load static wagtailcore_tags wagtailimages_tags wagtailadmin_tags %} {% block body_class %}template-blogpage{% endblock %} {% block content %} -

{{ page.title }}

-

{{ page.date }}

- - {% with authors=page.authors.all %} - {% if authors %} -

Posted by:

-
    - {% for author in authors %} -
  • - {% image author.author_image fill-40x60 style="vertical-align: middle" %} + +
    + + +
    + {% with authors=page.authors.all %} + {% if authors %} +

    Posted by:

    + {% for author in authors %} + + {% image author.author_image fill-40x40 as author_image %} + {% if author_image %} + {{ author.name }} + {% else %} + user + {% endif %} {{ author.name }} -
  • - {% endfor %} -
- {% endif %} - {% endwith %} + + {% endfor %} + {% endif %} + {% endwith %} +
+ {% with tags=page.tags.all %} + {% if tags %} + Tags: + {% for tag in tags %} + {{ tag }} + {% endfor %} + {% endif %} + {% endwith %} + -
{{ page.intro }}
+ - {{ page.body|richtext }} +
+
+
+

{{ page.intro }}

+ {{ page.body|richtext }} +
- {% for item in page.gallery_images.all %} -
- {% image item.image fill-320x240 %} -

{{ item.caption }}

- {% endfor %} - -

Return to blog

- - {% with tags=page.tags.all %} - {% if tags %} -
-

Tags

- {% for tag in tags %} - - {% endfor %} -
+ + {% for item in page.gallery_images.all %} + {% if forloop.first or forloop.counter0|divisibleby:3 %} + {% comment %} Start a new row {% endcomment %} +
{% endif %} - {% endwith %} +
+ {% image item.image fill-600x400 %} +
+ {{ item.caption }} +
+
+ {% if forloop.last or forloop.counter|divisibleby:3 %} +
+ {% endif %} + {% endfor %} + + +
+ {% endblock %} diff --git a/app/blog/templates/blog/blog_tag_index_page.html b/app/blog/templates/blog/blog_tag_index_page.html index c8ddcca..8b70a85 100644 --- a/app/blog/templates/blog/blog_tag_index_page.html +++ b/app/blog/templates/blog/blog_tag_index_page.html @@ -1,21 +1,31 @@ {% extends "base.html" %} + {% load wagtailcore_tags %} -{% block content %} +{% block body_class %}template-blogtagindexpage{% endblock %} - {% if request.GET.tag %} -

Showing pages tagged "{{ request.GET.tag }}"

- {% endif %} +{% block content %} - {% for blogpage in blogpages %} +
+ {% if request.GET.tag %} +

Showing pages tagged "{{ request.GET.tag }}"

+ {% endif %} -

- {{ blogpage.title }}
- Revised: {{ blogpage.latest_revision_created_at }}
-

+ - {% empty %} - No pages found with that tag. - {% endfor %} +
{% endblock %} diff --git a/app/home/templates/home/home_page.html b/app/home/templates/home/home_page.html index 02be91b..c79b903 100644 --- a/app/home/templates/home/home_page.html +++ b/app/home/templates/home/home_page.html @@ -1,11 +1,11 @@ {% extends "base.html" %} - {% load wagtailcore_tags %} {% block body_class %}template-homepage{% endblock %} - {% block content %} - {{ page.body|richtext }} +
+ {{ page.body|richtext }} +
{% endblock %} diff --git a/app/home/templatetags/__init__.py b/app/home/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/home/templatetags/navigation_tags.py b/app/home/templatetags/navigation_tags.py new file mode 100644 index 0000000..c6e6309 --- /dev/null +++ b/app/home/templatetags/navigation_tags.py @@ -0,0 +1,10 @@ +from django import template + +from app.blog.models import BlogIndexPage + +register = template.Library() + + +@register.simple_tag +def get_blog_index_url(): + return BlogIndexPage.objects.first().url diff --git a/app/templates/base.html b/app/templates/base.html index 92e1099..53acc39 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1,4 +1,4 @@ -{% load static wagtailcore_tags wagtailuserbar %} +{% load static wagtailcore_tags wagtailuserbar navigation_tags %} @@ -34,7 +34,28 @@ {% wagtailuserbar %} - {% block content %}{% endblock %} +
+ +
+ +
+ {% block content %}{% endblock %} +
+ + {# Global javascript #} From 89bb1832c50b4eefcead7a6b8bcf008ce66ba49d Mon Sep 17 00:00:00 2001 From: Nick Moreton Date: Sat, 14 Dec 2024 15:37:03 +0000 Subject: [PATCH 21/40] Fixup the blog index page template tag --- app/home/templatetags/navigation_tags.py | 8 ++++++-- app/templates/base.html | 7 ++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/home/templatetags/navigation_tags.py b/app/home/templatetags/navigation_tags.py index c6e6309..7b38fe0 100644 --- a/app/home/templatetags/navigation_tags.py +++ b/app/home/templatetags/navigation_tags.py @@ -6,5 +6,9 @@ @register.simple_tag -def get_blog_index_url(): - return BlogIndexPage.objects.first().url +def blog_index_page(): + """ + Get the blog index page if it exists. + """ + + return BlogIndexPage.objects.live().first() or None diff --git a/app/templates/base.html b/app/templates/base.html index 53acc39..62a5c63 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -34,6 +34,7 @@ {% wagtailuserbar %} + {% blog_index_page as blog %}
From dbfe7111af0b123e7364f90e40f642d7d6095390 Mon Sep 17 00:00:00 2001 From: Nick Moreton Date: Sat, 14 Dec 2024 15:55:41 +0000 Subject: [PATCH 22/40] Move blog tags to blog app --- app/{home => blog}/templatetags/__init__.py | 0 app/blog/templatetags/blog_tags.py | 23 +++++++++++++++++++++ app/home/templatetags/navigation_tags.py | 14 ------------- app/templates/base.html | 2 +- 4 files changed, 24 insertions(+), 15 deletions(-) rename app/{home => blog}/templatetags/__init__.py (100%) create mode 100644 app/blog/templatetags/blog_tags.py delete mode 100644 app/home/templatetags/navigation_tags.py diff --git a/app/home/templatetags/__init__.py b/app/blog/templatetags/__init__.py similarity index 100% rename from app/home/templatetags/__init__.py rename to app/blog/templatetags/__init__.py diff --git a/app/blog/templatetags/blog_tags.py b/app/blog/templatetags/blog_tags.py new file mode 100644 index 0000000..310d0ae --- /dev/null +++ b/app/blog/templatetags/blog_tags.py @@ -0,0 +1,23 @@ +from django import template + +from app.blog.models import BlogIndexPage, BlogTagIndexPage + +register = template.Library() + + +@register.simple_tag +def blog_index_page(): + """ + Get the blog index page if it exists. + """ + + return BlogIndexPage.objects.live().first() or None + + +@register.simple_tag +def blog_tags_page(): + """ + Get the blog tags page if it exists. + """ + + return BlogTagIndexPage.objects.live().first() or None diff --git a/app/home/templatetags/navigation_tags.py b/app/home/templatetags/navigation_tags.py deleted file mode 100644 index 7b38fe0..0000000 --- a/app/home/templatetags/navigation_tags.py +++ /dev/null @@ -1,14 +0,0 @@ -from django import template - -from app.blog.models import BlogIndexPage - -register = template.Library() - - -@register.simple_tag -def blog_index_page(): - """ - Get the blog index page if it exists. - """ - - return BlogIndexPage.objects.live().first() or None diff --git a/app/templates/base.html b/app/templates/base.html index 62a5c63..0d550de 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1,4 +1,4 @@ -{% load static wagtailcore_tags wagtailuserbar navigation_tags %} +{% load static wagtailcore_tags wagtailuserbar blog_tags %} From bc451f05eeacf943792b1d5c774cdd78bf4587bc Mon Sep 17 00:00:00 2001 From: Nick Moreton Date: Sat, 14 Dec 2024 15:56:04 +0000 Subject: [PATCH 23/40] Show message if blog tags page is not created --- app/blog/templates/blog/blog_page.html | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/blog/templates/blog/blog_page.html b/app/blog/templates/blog/blog_page.html index 1ffb43d..9313091 100644 --- a/app/blog/templates/blog/blog_page.html +++ b/app/blog/templates/blog/blog_page.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% load static wagtailcore_tags wagtailimages_tags wagtailadmin_tags %} +{% load static wagtailcore_tags wagtailimages_tags wagtailadmin_tags blog_tags %} {% block body_class %}template-blogpage{% endblock %} @@ -33,12 +33,17 @@

{{ page.title }}

{% endif %} {% endwith %}
+ {% blog_tags_page as tags_page %} {% with tags=page.tags.all %} {% if tags %} Tags: + {% if tags_page %} {% for tag in tags %} - {{ tag }} + {{ tag }} {% endfor %} + {% else %} +

Create a blog tags page

+ {% endif %} {% endif %} {% endwith %} From 15a0acc5a625df26241f0ea615865285ef37d71b Mon Sep 17 00:00:00 2001 From: Nick Moreton Date: Sat, 14 Dec 2024 16:01:53 +0000 Subject: [PATCH 24/40] Show home page message if no content has been added to the page body yet --- app/home/templates/home/home_page.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/home/templates/home/home_page.html b/app/home/templates/home/home_page.html index c79b903..86b072e 100644 --- a/app/home/templates/home/home_page.html +++ b/app/home/templates/home/home_page.html @@ -6,6 +6,10 @@ {% block content %}
+ {% if page.body %} {{ page.body|richtext }} + {% else %} +

No content has been added to this page body yet.

+ {% endif %}
{% endblock %} From 2f34fa779eaf114bbfd74942a17e887eb94c709d Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Fri, 25 Jul 2025 16:57:27 +0100 Subject: [PATCH 25/40] Update test for the home page content. Dependent on the content bring changed to "Wagtail Blog Tutorial". --- app/home/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/home/tests.py b/app/home/tests.py index 5b9f502..bb76b39 100644 --- a/app/home/tests.py +++ b/app/home/tests.py @@ -30,7 +30,7 @@ def test_home_frontend_returns_200(self): """Test that the home page frontend returns 200 OK.""" response = self.client.get("/") self.assertEqual(response.status_code, 200) - self.assertContains(response, "Welcome to your new Wagtail site!") + self.assertContains(response, "Wagtail Blog Tutorial") self.assertTemplateUsed(response, "home/home_page.html") def test_home_admin_edit_returns_200(self): From 346ae866dd583a9523b212f3aa370cc601835bad Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Fri, 25 Jul 2025 17:23:10 +0100 Subject: [PATCH 26/40] Create a management command to populate the home page with sample content --- .../management/commands/populate_homepage.py | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 app/home/management/commands/populate_homepage.py diff --git a/app/home/management/commands/populate_homepage.py b/app/home/management/commands/populate_homepage.py new file mode 100644 index 0000000..29f39c7 --- /dev/null +++ b/app/home/management/commands/populate_homepage.py @@ -0,0 +1,121 @@ +import random + +from django.core.management.base import BaseCommand +from wagtail.images.models import Image + +from app.home.models import HomePage + + +class Command(BaseCommand): + help = "Populates the existing home page with sample body content" + + def add_arguments(self, parser): + parser.add_argument( + "--overwrite", + action="store_true", + help="Overwrite existing body content if it exists", + ) + + def handle(self, *args, **options): + try: + # Get the existing home page + home_page = HomePage.objects.get() + + # Check if body content already exists + if home_page.body and not options["overwrite"]: + self.stdout.write( + self.style.WARNING( + "Home page already has body content. Use --overwrite to replace it." + ) + ) + self.stdout.write( + f"Current content length: {len(home_page.body)} characters" + ) + return + + # Check for existing images + available_images = Image.objects.all() + selected_image = None + image_embed = "" + + if available_images.exists(): + # Select a random image + selected_image = random.choice(available_images) + # Create embed code for the image + image_embed = ( + f'' + ) + self.stdout.write(f"Selected image: {selected_image.title}") + else: + self.stdout.write( + "No images found - content will be created without images" + ) + + # Sample body content with optional image + sample_content = f""" +

Welcome to Your Wagtail Site

+ +

This is your homepage, built with Wagtail CMS. Wagtail is a powerful, +developer-friendly content management system built on Django that makes it easy to create +beautiful, engaging websites.

+ +{image_embed} + +

Getting Started

+ +

You can edit this content by going to the Wagtail admin and +selecting your home page. From there, you can:

+ +
    +
  • Add and edit content using the rich text editor
  • +
  • Create new pages and organize your site structure
  • +
  • Upload and manage images and documents
  • +
  • Customize your site's design and functionality
  • +
+ +

Features

+ +

This starter kit includes:

+ +
    +
  • Docker Development Environment - Easy setup and consistent development experience
  • +
  • Multiple Database Options - PostgreSQL, MySQL, or SQLite3
  • +
  • Frontend Build Tools - SASS compilation and JavaScript bundling
  • +
  • Pico CSS Framework - Clean, semantic styling
  • +
  • Management Commands - Useful utilities for development and testing
  • +
+ +

Next Steps

+ +

Now that you have your site running, consider:

+ +
    +
  1. Exploring the Style Guide to see available components
  2. +
  3. Reading the Wagtail documentation to learn more
  4. +
  5. Creating custom page types for your specific needs
  6. +
  7. Setting up your production deployment
  8. +
+ +

Happy building with Wagtail!

+ """.strip() + + # Update the home page body content + home_page.body = sample_content + home_page.save() + + # Create a new revision to track the change + home_page.save_revision().publish() + + success_message = f"Successfully populated home page body content ({len(sample_content)} characters)" + if selected_image: + success_message += f" with image: {selected_image.title}" + + self.stdout.write(self.style.SUCCESS(success_message)) + + except HomePage.DoesNotExist: + self.stdout.write( + self.style.ERROR("No home page found. Please create a home page first.") + ) + except Exception as e: + self.stdout.write(self.style.ERROR(f"An error occurred: {str(e)}")) From 34cb41f451b4fc5e098f7d92f032face91ed9667 Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Fri, 25 Jul 2025 17:23:55 +0100 Subject: [PATCH 27/40] Add a test for the populate_homepage management command --- app/home/tests.py | 126 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/app/home/tests.py b/app/home/tests.py index bb76b39..c681f59 100644 --- a/app/home/tests.py +++ b/app/home/tests.py @@ -1,5 +1,10 @@ +from io import StringIO + from django.contrib.auth.models import User +from django.core.management import call_command from django.test import TestCase +from wagtail.images.models import Image +from wagtail.images.tests.utils import get_test_image_file from app.home.models import HomePage @@ -64,3 +69,124 @@ def test_home_admin_history_returns_200(self): self._login_as_admin() response = self.client.get(f"/admin/pages/{self.home_page.pk}/history/") self.assertEqual(response.status_code, 200) + + def test_populate_homepage_command(self): + """Test the populate_homepage management command""" + home_page = HomePage.objects.first() + + # Ensure home page starts with empty body + home_page.body = "" + home_page.save() + + # Create output capture + out = StringIO() + + # Run the command + call_command("populate_homepage", stdout=out) + + # Refresh from database + home_page.refresh_from_db() + + # Check that body content was added + self.assertNotEqual(home_page.body, "") + self.assertIn("Welcome to Your Wagtail Site", home_page.body) + self.assertIn("Successfully populated home page body content", out.getvalue()) + + def test_populate_homepage_command_with_existing_content(self): + """Test the populate_homepage management command when content already exists""" + home_page = HomePage.objects.first() + + # Set some existing content + home_page.body = "Existing content" + home_page.save() + + # Create output capture + out = StringIO() + + # Run the command without overwrite + call_command("populate_homepage", stdout=out) + + # Refresh from database + home_page.refresh_from_db() + + # Check that content wasn't changed + self.assertEqual(home_page.body, "Existing content") + self.assertIn("already has body content", out.getvalue()) + + def test_populate_homepage_command_with_overwrite(self): + """Test the populate_homepage management command with overwrite option""" + home_page = HomePage.objects.first() + + # Set some existing content + home_page.body = "Existing content" + home_page.save() + + # Create output capture + out = StringIO() + + # Run the command with overwrite + call_command("populate_homepage", "--overwrite", stdout=out) + + # Refresh from database + home_page.refresh_from_db() + + # Check that content was changed + self.assertNotEqual(home_page.body, "Existing content") + self.assertIn("Welcome to Your Wagtail Site", home_page.body) + self.assertIn("Successfully populated home page body content", out.getvalue()) + + def test_populate_homepage_command_with_image(self): + """Test the populate_homepage command when images are available""" + home_page = HomePage.objects.first() + + # Create a test image + Image.objects.create( + title="Test Image", + file=get_test_image_file(), + ) + + # Ensure home page starts with empty body + home_page.body = "" + home_page.save() + + # Create output capture + out = StringIO() + + # Run the command + call_command("populate_homepage", stdout=out) + + # Refresh from database + home_page.refresh_from_db() + + # Check that body content was added with image + self.assertNotEqual(home_page.body, "") + self.assertIn("Welcome to Your Wagtail Site", home_page.body) + self.assertIn('embedtype="image"', home_page.body) + self.assertIn("Selected image:", out.getvalue()) + self.assertIn("with image:", out.getvalue()) + + def test_populate_homepage_command_without_images(self): + """Test the populate_homepage command when no images are available""" + home_page = HomePage.objects.first() + + # Remove all images + Image.objects.all().delete() + + # Ensure home page starts with empty body + home_page.body = "" + home_page.save() + + # Create output capture + out = StringIO() + + # Run the command + call_command("populate_homepage", stdout=out) + + # Refresh from database + home_page.refresh_from_db() + + # Check that body content was added without image + self.assertNotEqual(home_page.body, "") + self.assertIn("Welcome to Your Wagtail Site", home_page.body) + self.assertNotIn('embedtype="image"', home_page.body) + self.assertIn("No images found", out.getvalue()) From a435daf375b74b046277a3e8e8ef6acc7250c251 Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Fri, 25 Jul 2025 17:24:17 +0100 Subject: [PATCH 28/40] Update documentation --- README.md | 3 ++ docs/management-commands.md | 76 ++++++++++++++++++++++++++++++++++--- 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3adec25..ee5fc26 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,9 @@ For detailed documentation on all available commands, see [Management Commands D ### Quick Examples ```bash +# Populate the home page with sample content +docker exec -it wagtail-starter-kit-app-1 python manage.py populate_homepage + # Create sample images and documents for testing docker exec -it wagtail-starter-kit-app-1 python manage.py create_sample_media diff --git a/docs/management-commands.md b/docs/management-commands.md index 669be3e..d4ca828 100644 --- a/docs/management-commands.md +++ b/docs/management-commands.md @@ -4,18 +4,82 @@ This document provides detailed information about the custom Django management c ## Table of Contents +- [populate_homepage](#populate_homepage) - [create_sample_media](#create_sample_media) - [Future Commands](#future-commands) --- +## populate_homepage + +**Location**: `app/home/management/commands/populate_homepage.py` + +**Purpose**: Populates the existing home page with sample body content for testing and demonstration purposes. If images exist in the media library, one will be randomly selected and included in the content. + +### Description + +The `populate_homepage` command adds rich HTML content to the body field of your site's home page. This is useful for: + +- Setting up a new Wagtail site with meaningful placeholder content +- Demonstrating Wagtail's rich text editing capabilities including image embeds +- Providing a starting point for content editors +- Testing page layouts and styling with realistic content and media + +The generated content includes: + +- **Welcome message** with Wagtail introduction +- **Random image embed** (if images are available in the media library) +- **Getting started guide** with admin links +- **Feature overview** of the starter kit +- **Next steps** for development + +### Usage + +```bash +# Basic usage (populates empty home page) +python manage.py populate_homepage + +# Overwrite existing content +python manage.py populate_homepage --overwrite +``` + +### Options + +| Option | Description | +|--------|-------------| +| `--overwrite` | Overwrite existing body content if it already exists | + +### Examples + +#### Populate empty home page +```bash +docker exec -it wagtail-starter-kit-app-1 python manage.py populate_homepage +``` + +#### Replace existing content +```bash +docker exec -it wagtail-starter-kit-app-1 python manage.py populate_homepage --overwrite +``` + +### Technical Implementation + +- Uses Django's management command framework +- Safely updates the HomePage model via Django ORM +- Automatically selects a random image from the media library if available +- Embeds images using Wagtail's rich text embed format +- Creates a new revision and publishes it to track the change +- Provides clear feedback on success/failure states and image selection +- Handles cases where no home page exists or no images are available + +--- + ## create_sample_media **Location**: `app/home/management/commands/create_sample_media.py` **Purpose**: Creates sample images and documents (media files) for testing and demonstration purposes in your Wagtail CMS. -### Description +### Overview The `create_sample_media` command generates realistic sample content including: @@ -29,7 +93,7 @@ This command is perfect for: - Testing search functionality with varied content - Creating realistic data for development and staging environments -### Usage +### Command Usage ```bash # Basic usage (creates 75 images and 50 documents by default) @@ -48,7 +112,7 @@ python manage.py create_sample_media --no-zip python manage.py create_sample_media --reset ``` -### Options +### Command Options | Option | Type | Default | Description | |--------|------|---------|-------------| @@ -58,7 +122,7 @@ python manage.py create_sample_media --reset | `--no-zip` | Flag | False | Skip creating ZIP archives of the documents | | `--reset` | Flag | False | Delete all existing images and documents without creating new ones | -### Generated Content Details +### Content Details #### Images - **Formats**: JPEG with 85% quality @@ -101,7 +165,7 @@ Each ZIP file includes: - A README.txt file explaining the archive contents - Proper file organization -### Examples +### Command Examples #### Creating a small test set ```bash @@ -118,7 +182,7 @@ docker exec -it wagtail-starter-kit-app-1 python manage.py create_sample_media - docker exec -it wagtail-starter-kit-app-1 python manage.py create_sample_media --reset ``` -### Technical Implementation +### Implementation Details #### Dependencies - **PIL (Pillow)**: For dynamic image generation From 5fd9367705a136db78efd6a8f46decc635148eff Mon Sep 17 00:00:00 2001 From: Nick Moreton Date: Sat, 26 Jul 2025 21:37:56 +0100 Subject: [PATCH 29/40] Create command to create blog content --- app/blog/management/__init__.py | 0 app/blog/management/commands/__init__.py | 0 app/blog/management/commands/populate_blog.py | 364 ++++++++++++++++++ app/blog/tests.py | 122 ++++++ 4 files changed, 486 insertions(+) create mode 100644 app/blog/management/__init__.py create mode 100644 app/blog/management/commands/__init__.py create mode 100644 app/blog/management/commands/populate_blog.py create mode 100644 app/blog/tests.py diff --git a/app/blog/management/__init__.py b/app/blog/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/blog/management/commands/__init__.py b/app/blog/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/blog/management/commands/populate_blog.py b/app/blog/management/commands/populate_blog.py new file mode 100644 index 0000000..07c3ffe --- /dev/null +++ b/app/blog/management/commands/populate_blog.py @@ -0,0 +1,364 @@ +import random +from datetime import date, timedelta + +from django.core.management.base import BaseCommand +from wagtail.images.models import Image + +from app.blog.models import ( + Author, + BlogIndexPage, + BlogPage, + BlogPageGalleryImage, + BlogTagIndexPage, +) +from app.home.models import HomePage + + +class Command(BaseCommand): + help = "Creates sample blog content including blog index, blog pages, and authors" + + def add_arguments(self, parser): + parser.add_argument( + "--posts", + type=int, + default=10, + help="Number of blog posts to create (default: 10)", + ) + parser.add_argument( + "--authors", + type=int, + default=3, + help="Number of authors to create (default: 3)", + ) + parser.add_argument( + "--clear", + action="store_true", + help="Clear existing blog content before creating new content", + ) + + def handle(self, *args, **options): + try: + # Get the home page + home_page = HomePage.objects.first() + if not home_page: + self.stdout.write( + self.style.ERROR( + "No home page found. Please create a home page first." + ) + ) + return + + # Clear existing content if requested + if options["clear"]: + self.clear_blog_content() + # Refresh home page from database after clearing content + home_page.refresh_from_db() + + # Create or get blog index page + blog_index = self.create_blog_index(home_page) + + # Create blog tag index page + self.create_blog_tag_index(home_page) + + # Create sample authors + authors = self.create_authors(options["authors"]) + + # Create blog posts + self.create_blog_posts(blog_index, authors, options["posts"]) + + self.stdout.write( + self.style.SUCCESS( + f"Successfully created blog structure with {options['posts']} posts " + f"and {len(authors)} authors" + ) + ) + + except Exception as e: + self.stdout.write(self.style.ERROR(f"An error occurred: {str(e)}")) + + def clear_blog_content(self): + """Clear existing blog content""" + # Delete blog posts first (children) + blog_posts = BlogPage.objects.all() + for post in blog_posts: + post.delete() + self.stdout.write("Cleared existing blog posts") + + # Then delete the index pages + blog_indices = BlogIndexPage.objects.all() + for index in blog_indices: + index.delete() + self.stdout.write("Cleared existing blog index pages") + + tag_indices = BlogTagIndexPage.objects.all() + for tag_index in tag_indices: + tag_index.delete() + self.stdout.write("Cleared existing blog tag pages") + + # # Finally delete authors + # Author.objects.all().delete() + # self.stdout.write("Cleared existing authors") + + def create_blog_index(self, home_page): + """Create or get the blog index page""" + # Try to get existing blog index that's a child of home page + blog_index = home_page.get_children().type(BlogIndexPage).first() + + if not blog_index: + blog_index = BlogIndexPage( + title="Blog", + slug="blog", + intro=( + "

Welcome to our blog! Here you'll find the latest news, " + "insights, and stories from our team.

" + ), + ) + home_page.add_child(instance=blog_index) + blog_index.save_revision().publish() + self.stdout.write("Created blog index page") + else: + self.stdout.write("Blog index page already exists") + + return blog_index + + def create_blog_tag_index(self, home_page): + """Create or get the blog tag index page""" + # Try to get existing blog tag index that's a child of home page + tag_index = home_page.get_children().type(BlogTagIndexPage).first() + + if not tag_index: + tag_index = BlogTagIndexPage( + title="Blog Tags", + slug="blog-tags", + ) + home_page.add_child(instance=tag_index) + tag_index.save_revision().publish() + self.stdout.write("Created blog tag index page") + else: + self.stdout.write("Blog tag index page already exists") + + return tag_index + + def create_authors(self, num_authors): + """Create sample authors""" + existing_authors = Author.objects.all() + if existing_authors.exists(): + self.stdout.write(f"Using {existing_authors.count()} existing authors") + return list(existing_authors) + + author_names = [ + "Alice Johnson", + "Bob Smith", + "Carol Davis", + "David Wilson", + "Eva Brown", + "Frank Miller", + "Grace Lee", + "Henry Taylor", + "Ivy Chen", + "Jack Thompson", + ] + + authors = [] + available_images = list(Image.objects.all()) + + for i in range(min(num_authors, len(author_names))): + author = Author.objects.create( + name=author_names[i], + author_image=( + random.choice(available_images) if available_images else None + ), + ) + authors.append(author) + + self.stdout.write(f"Created {len(authors)} authors") + return authors + + def create_blog_posts(self, blog_index, authors, num_posts): + """Create sample blog posts""" + + # Sample blog post data + post_topics = [ + "Getting Started with Wagtail CMS", + "Building Modern Web Applications", + "The Future of Content Management", + "Django Development Best Practices", + "Responsive Web Design Tips", + "SEO Optimization Strategies", + "User Experience Design Principles", + "Digital Marketing Trends", + "Web Performance Optimization", + "Accessibility in Web Development", + "Progressive Web Applications", + "API Design and Development", + "Database Optimization Techniques", + "Security Best Practices", + "DevOps and Deployment Strategies", + ] + + blog_tags = [ + "wagtail", + "django", + "python", + "web-development", + "cms", + "frontend", + "backend", + "design", + "ux", + "seo", + "performance", + "security", + "api", + "database", + "devops", + ] + + available_images = list(Image.objects.all()) + + for i in range(num_posts): + # Generate unique blog post content + topic = post_topics[i % len(post_topics)] + post_date = date.today() - timedelta(days=random.randint(1, 365)) + + # Select random authors (1-3 authors per post) + post_authors = random.sample( + authors, k=random.randint(1, min(3, len(authors))) + ) + + # Select random tags (2-4 tags per post) + post_tags = random.sample(blog_tags, k=random.randint(2, 4)) + + # Generate unique title and slug using date and random number + random_suffix = random.randint(100, 999) + unique_title = f"{topic} - {post_date.strftime('%B %Y')} Edition" + base_slug = topic.lower().replace(" ", "-").replace("'", "") + unique_slug = f"{base_slug}-{post_date.strftime('%Y%m%d')}-{random_suffix}" + + # Ensure slug uniqueness by checking existing slugs + existing_slugs = BlogPage.objects.filter( + slug__startswith=base_slug + ).values_list("slug", flat=True) + + counter = 1 + original_slug = unique_slug + while unique_slug in existing_slugs: + unique_slug = f"{original_slug}-{counter}" + counter += 1 + + intro = self.generate_intro(topic) + body = self.generate_body(topic) + + # Create the blog post using create method instead of manual instantiation + blog_post = blog_index.add_child( + instance=BlogPage( + title=unique_title, + slug=unique_slug, + date=post_date, + intro=intro, + body=body, + ) + ) + + # Publish the page + blog_post.save_revision().publish() + + # Add authors + blog_post.authors.set(post_authors) + + # Add tags + for tag in post_tags: + blog_post.tags.add(tag) + + # Add gallery images (0-3 images per post) + if available_images: + num_images = random.randint(0, min(3, len(available_images))) + selected_images = random.sample(available_images, k=num_images) + + for idx, image in enumerate(selected_images): + BlogPageGalleryImage.objects.create( + page=blog_post, + image=image, + caption=f"Image {idx + 1} for {blog_post.title}", + ) + + blog_post.save() + + self.stdout.write(f"Created {num_posts} blog posts") + + def generate_intro(self, topic): + """Generate sample introduction for blog post""" + intros = { + "Getting Started with Wagtail CMS": ( + "Learn the basics of Wagtail CMS and how to build your first " + "content-driven website with this powerful Django-based platform." + ), + "Building Modern Web Applications": ( + "Explore the latest techniques and tools for creating responsive, " + "scalable web applications that meet today's user expectations." + ), + "The Future of Content Management": ( + "Discover emerging trends in content management systems and how " + "they're shaping the digital landscape." + ), + "Django Development Best Practices": ( + "Master the art of Django development with proven patterns, " + "conventions, and techniques used by experienced developers." + ), + "Responsive Web Design Tips": ( + "Create websites that look great on any device with these essential " + "responsive design principles and practical implementation tips." + ), + } + return intros.get( + topic, + f"An in-depth look at {topic.lower()} and its practical applications " + f"in modern web development.", + ) + + def generate_body(self, topic): + """Generate sample body content for blog post""" + return f""" +

Introduction

+

In this comprehensive guide, we'll explore {topic.lower()} and its + importance in modern web development. Whether you're a beginner or an experienced developer, + this article will provide valuable insights and practical examples.

+ +

Key Concepts

+

Understanding the fundamental concepts is crucial for success. We'll cover the most + important aspects that every developer should know about this topic.

+ +
    +
  • Core principles and methodologies
  • +
  • Best practices and common patterns
  • +
  • Tools and frameworks that can help
  • +
  • Real-world applications and use cases
  • +
+ +

Implementation Guide

+

Let's dive into the practical implementation details. This section provides step-by-step + instructions and code examples to help you get started.

+ +

The implementation process involves several key steps:

+
    +
  1. Planning and Setup: Prepare your development environment and plan + your approach
  2. +
  3. Core Implementation: Build the main functionality following best + practices
  4. +
  5. Testing and Optimization: Ensure your implementation works correctly + and performs well
  6. +
  7. Deployment and Maintenance: Deploy your solution and keep it + updated
  8. +
+ +

Advanced Techniques

+

For those looking to take their skills to the next level, here are some advanced + techniques and considerations that can make a significant difference in your projects.

+ +

Conclusion

+

By following the guidelines and best practices outlined in this article, you'll be + well-equipped to tackle {topic.lower()} in your own projects. Remember to + keep learning and stay updated with the latest developments in the field.

+ +

Happy coding!

+ """.strip() diff --git a/app/blog/tests.py b/app/blog/tests.py new file mode 100644 index 0000000..ffbef33 --- /dev/null +++ b/app/blog/tests.py @@ -0,0 +1,122 @@ +from io import StringIO + +from django.core.management import call_command +from django.test import TestCase +from wagtail.models import Site + +from app.blog.models import Author, BlogIndexPage, BlogPage, BlogTagIndexPage +from app.home.models import HomePage + + +class PopulateBlogTestCase(TestCase): + def setUp(self): + # Get or create the root page and site + from wagtail.models import Page + + try: + root = Page.objects.get(slug="root") + except Page.DoesNotExist: + root = Page.add_root(title="Root", slug="root") + + # Create a home page for testing + self.home_page = HomePage( + title="Test Home", + slug="test-home", + ) + root.add_child(instance=self.home_page) + + # Create site if it doesn't exist + if not Site.objects.exists(): + Site.objects.create( + hostname="localhost", + port=8000, + root_page=self.home_page, + is_default_site=True, + ) + + def test_populate_blog_basic(self): + """Test basic blog population""" + out = StringIO() + + # Run the command + call_command("populate_blog", "--posts", "3", "--authors", "2", stdout=out) + + # Check that content was created + self.assertEqual(BlogIndexPage.objects.count(), 1) + self.assertEqual(BlogTagIndexPage.objects.count(), 1) + self.assertEqual(BlogPage.objects.count(), 3) + self.assertEqual(Author.objects.count(), 2) + + # Check success message + self.assertIn( + "Successfully created blog structure with 3 posts and 2 authors", + out.getvalue(), + ) + + def test_populate_blog_with_existing_authors(self): + """Test that existing authors are reused""" + # Create an existing author + Author.objects.create(name="Existing Author") + + out = StringIO() + + # Run the command + call_command("populate_blog", "--posts", "2", "--authors", "3", stdout=out) + + # Should still have only 1 author (the existing one) + self.assertEqual(Author.objects.count(), 1) + self.assertIn("Using 1 existing authors", out.getvalue()) + + def test_populate_blog_clear_option(self): + """Test the clear option functionality""" + # Create some initial content + call_command("populate_blog", "--posts", "2", "--authors", "1") + + # Verify initial content exists + self.assertEqual(BlogPage.objects.count(), 2) + + out = StringIO() + + # Run with clear option - focus on testing the clear messages + call_command( + "populate_blog", "--clear", "--posts", "1", "--authors", "1", stdout=out + ) + + # Check that clear messages appeared + output = out.getvalue() + self.assertIn("Cleared existing blog posts", output) + self.assertIn("Cleared existing blog index pages", output) + self.assertIn("Cleared existing blog tag pages", output) + # Note: Authors are not cleared by default to preserve them across runs + + def test_blog_post_structure(self): + """Test that blog posts have proper structure and relationships""" + call_command("populate_blog", "--posts", "2", "--authors", "2") + + blog_post = BlogPage.objects.first() + + # Check that the blog post has required fields + self.assertTrue(blog_post.title) + self.assertTrue(blog_post.intro) + self.assertTrue(blog_post.body) + self.assertTrue(blog_post.date) + + # Check that it has authors and tags + self.assertTrue(blog_post.authors.exists()) + self.assertTrue(blog_post.tags.exists()) + + # Check that it's a child of blog index + blog_index = BlogIndexPage.objects.first() + self.assertEqual(blog_post.get_parent().specific, blog_index) + + def test_blog_index_structure(self): + """Test that blog index page is properly structured""" + call_command("populate_blog", "--posts", "1", "--authors", "1") + + blog_index = BlogIndexPage.objects.first() + home_page = HomePage.objects.first() + + # Check that blog index is a child of home page + self.assertEqual(blog_index.get_parent().specific, home_page) + self.assertEqual(blog_index.slug, "blog") + self.assertTrue(blog_index.intro) From 2105d14c7476551247a81353959fd8aebe04f74f Mon Sep 17 00:00:00 2001 From: Nick Moreton Date: Sat, 26 Jul 2025 21:52:14 +0100 Subject: [PATCH 30/40] Update the documentation for the `populate_blog` management command --- README.md | 3 + app/blog/management/commands/populate_blog.py | 21 +++- docs/management-commands.md | 109 ++++++++++++++++++ 3 files changed, 129 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ee5fc26..94ef11c 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,9 @@ docker exec -it wagtail-starter-kit-app-1 python manage.py populate_homepage # Create sample images and documents for testing docker exec -it wagtail-starter-kit-app-1 python manage.py create_sample_media +# Create a complete blog structure with sample posts and authors +docker exec -it wagtail-starter-kit-app-1 python manage.py populate_blog + # Reset all sample content docker exec -it wagtail-starter-kit-app-1 python manage.py create_sample_media --reset ``` diff --git a/app/blog/management/commands/populate_blog.py b/app/blog/management/commands/populate_blog.py index 07c3ffe..c1e9740 100644 --- a/app/blog/management/commands/populate_blog.py +++ b/app/blog/management/commands/populate_blog.py @@ -15,25 +15,38 @@ class Command(BaseCommand): - help = "Creates sample blog content including blog index, blog pages, and authors" + help = ( + "Creates a complete blog structure with sample content including blog index pages, " + "blog posts with unique titles, authors, tags, and gallery images. Can be run " + "multiple times safely to add more content without conflicts." + ) def add_arguments(self, parser): parser.add_argument( "--posts", type=int, default=10, - help="Number of blog posts to create (default: 10)", + help=( + "Number of blog posts to create (default: 10). Each post will have " + "unique titles, slugs, content, authors, and tags." + ), ) parser.add_argument( "--authors", type=int, default=3, - help="Number of authors to create (default: 3)", + help=( + "Number of authors to create (default: 3). If authors already exist, " + "they will be reused instead of creating new ones." + ), ) parser.add_argument( "--clear", action="store_true", - help="Clear existing blog content before creating new content", + help=( + "Clear existing blog content (posts and index pages) before creating " + "new content. Authors are preserved to maintain relationships." + ), ) def handle(self, *args, **options): diff --git a/docs/management-commands.md b/docs/management-commands.md index d4ca828..eb92b99 100644 --- a/docs/management-commands.md +++ b/docs/management-commands.md @@ -6,6 +6,7 @@ This document provides detailed information about the custom Django management c - [populate_homepage](#populate_homepage) - [create_sample_media](#create_sample_media) +- [populate_blog](#populate_blog) - [Future Commands](#future-commands) --- @@ -191,6 +192,114 @@ docker exec -it wagtail-starter-kit-app-1 python manage.py create_sample_media - --- +## populate_blog + +**Location**: `app/blog/management/commands/populate_blog.py` + +**Purpose**: Creates a complete blog structure with sample blog posts, authors, and index pages for testing and demonstration purposes in your Wagtail CMS. + +### Overview + +The `populate_blog` command generates a fully functional blog section including: + +- **Blog Index Page**: Main landing page for the blog section +- **Blog Tag Index Page**: Page for organizing and filtering blog posts by tags +- **Sample Authors**: Realistic author profiles with optional images +- **Blog Posts**: Rich content blog posts with unique titles, content, tags, and gallery images +- **Relationships**: Proper parent-child page relationships and many-to-many author associations + +### Command Usage + +```bash +# Basic usage (creates 10 posts and 3 authors by default) +python manage.py populate_blog + +# Custom amounts +python manage.py populate_blog --posts 5 --authors 2 + +# Clear existing blog content and create new +python manage.py populate_blog --clear --posts 15 --authors 4 + +# Add more posts to existing blog (runs multiple times safely) +python manage.py populate_blog --posts 3 +python manage.py populate_blog --posts 5 +``` + +### Command Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `--posts` | Integer | 10 | Number of blog posts to create | +| `--authors` | Integer | 3 | Number of authors to create (reuses existing authors if available) | +| `--clear` | Flag | False | Clear existing blog content before creating new content | + +### Content Structure + +#### Blog Posts +- **Topics**: 15 predefined topics covering web development, CMS, Django, design, SEO, security, and more +- **Unique Titles**: Format: `"{Topic} - {Month Year} Edition"` (e.g., "Building Modern Web Applications - March 2025 Edition") +- **Smart Slugs**: Format: `{topic-slug}-{YYYYMMDD}-{random-3-digits}` with collision detection +- **Rich Content**: Structured HTML with headings, paragraphs, lists, and emphasized text +- **Random Dates**: Posts dated within the last year with random distribution +- **Tags**: 2-4 relevant tags per post from: wagtail, django, python, web-development, cms, frontend, backend, design, ux, seo, performance, security, api, database, devops + +#### Authors +- **Predefined Names**: 10 realistic author names (Alice Johnson, Bob Smith, etc.) +- **Image Assignment**: Random selection from available images in media library +- **Reuse Logic**: Existing authors are reused when command runs multiple times +- **Preserved on Clear**: Authors are preserved when using `--clear` flag (by design) + +#### Page Structure +``` +Home Page +├── Blog (BlogIndexPage) +│ ├── Blog Post 1 (BlogPage) +│ ├── Blog Post 2 (BlogPage) +│ └── ... +└── Blog Tags (BlogTagIndexPage) +``` + +### Command Examples + +#### Set up initial blog +```bash +docker exec -it wagtail-starter-kit-app-1 python manage.py populate_blog +``` + +#### Create a large blog for demo +```bash +docker exec -it wagtail-starter-kit-app-1 python manage.py populate_blog --clear --posts 25 --authors 5 +``` + +#### Add more content to existing blog +```bash +docker exec -it wagtail-starter-kit-app-1 python manage.py populate_blog --posts 8 +``` + +#### Reset and start fresh +```bash +docker exec -it wagtail-starter-kit-app-1 python manage.py populate_blog --clear --posts 12 --authors 3 +``` + +### Sample Output + +When running the command, you'll see output like: +``` +Blog index page already exists +Blog tag index page already exists +Using 3 existing authors +Created 5 blog posts +Successfully created blog structure with 5 posts and 3 authors +``` + +The command provides clear feedback on: +- Whether blog structure already exists +- Author creation vs. reuse +- Number of posts created +- Final summary of created content + +--- + ## Future Commands This section will be expanded as additional management commands are added to the project. From 5478e291b80d6353b146253d872feeb78ec1ceff Mon Sep 17 00:00:00 2001 From: Nick Moreton Date: Sat, 26 Jul 2025 22:02:21 +0100 Subject: [PATCH 31/40] Reorder command documentation --- README.md | 11 ++- docs/management-commands.md | 138 ++++++++++++++---------------------- 2 files changed, 56 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 94ef11c..f75d9da 100644 --- a/README.md +++ b/README.md @@ -77,20 +77,17 @@ The project includes custom Django management commands to help with development For detailed documentation on all available commands, see [Management Commands Documentation](./docs/management-commands.md). -### Quick Examples +### Quick Examples (recommended running order) ```bash -# Populate the home page with sample content -docker exec -it wagtail-starter-kit-app-1 python manage.py populate_homepage - # Create sample images and documents for testing docker exec -it wagtail-starter-kit-app-1 python manage.py create_sample_media +# Populate the home page with sample content +docker exec -it wagtail-starter-kit-app-1 python manage.py populate_homepage + # Create a complete blog structure with sample posts and authors docker exec -it wagtail-starter-kit-app-1 python manage.py populate_blog - -# Reset all sample content -docker exec -it wagtail-starter-kit-app-1 python manage.py create_sample_media --reset ``` ## View the site diff --git a/docs/management-commands.md b/docs/management-commands.md index eb92b99..bab6374 100644 --- a/docs/management-commands.md +++ b/docs/management-commands.md @@ -4,76 +4,13 @@ This document provides detailed information about the custom Django management c ## Table of Contents -- [populate_homepage](#populate_homepage) - [create_sample_media](#create_sample_media) +- [populate_homepage](#populate_homepage) - [populate_blog](#populate_blog) - [Future Commands](#future-commands) --- -## populate_homepage - -**Location**: `app/home/management/commands/populate_homepage.py` - -**Purpose**: Populates the existing home page with sample body content for testing and demonstration purposes. If images exist in the media library, one will be randomly selected and included in the content. - -### Description - -The `populate_homepage` command adds rich HTML content to the body field of your site's home page. This is useful for: - -- Setting up a new Wagtail site with meaningful placeholder content -- Demonstrating Wagtail's rich text editing capabilities including image embeds -- Providing a starting point for content editors -- Testing page layouts and styling with realistic content and media - -The generated content includes: - -- **Welcome message** with Wagtail introduction -- **Random image embed** (if images are available in the media library) -- **Getting started guide** with admin links -- **Feature overview** of the starter kit -- **Next steps** for development - -### Usage - -```bash -# Basic usage (populates empty home page) -python manage.py populate_homepage - -# Overwrite existing content -python manage.py populate_homepage --overwrite -``` - -### Options - -| Option | Description | -|--------|-------------| -| `--overwrite` | Overwrite existing body content if it already exists | - -### Examples - -#### Populate empty home page -```bash -docker exec -it wagtail-starter-kit-app-1 python manage.py populate_homepage -``` - -#### Replace existing content -```bash -docker exec -it wagtail-starter-kit-app-1 python manage.py populate_homepage --overwrite -``` - -### Technical Implementation - -- Uses Django's management command framework -- Safely updates the HomePage model via Django ORM -- Automatically selects a random image from the media library if available -- Embeds images using Wagtail's rich text embed format -- Creates a new revision and publishes it to track the change -- Provides clear feedback on success/failure states and image selection -- Handles cases where no home page exists or no images are available - ---- - ## create_sample_media **Location**: `app/home/management/commands/create_sample_media.py` @@ -183,12 +120,58 @@ docker exec -it wagtail-starter-kit-app-1 python manage.py create_sample_media - docker exec -it wagtail-starter-kit-app-1 python manage.py create_sample_media --reset ``` -### Implementation Details +--- + +## populate_homepage + +**Location**: `app/home/management/commands/populate_homepage.py` + +**Purpose**: Populates the existing home page with sample body content for testing and demonstration purposes. If images exist in the media library, one will be randomly selected and included in the content. + +### Description + +The `populate_homepage` command adds rich HTML content to the body field of your site's home page. This is useful for: + +- Setting up a new Wagtail site with meaningful placeholder content +- Demonstrating Wagtail's rich text editing capabilities including image embeds +- Providing a starting point for content editors +- Testing page layouts and styling with realistic content and media + +The generated content includes: + +- **Welcome message** with Wagtail introduction +- **Random image embed** (if images are available in the media library) +- **Getting started guide** with admin links +- **Feature overview** of the starter kit +- **Next steps** for development + +### Usage + +```bash +# Basic usage (populates empty home page) +python manage.py populate_homepage + +# Overwrite existing content +python manage.py populate_homepage --overwrite +``` + +### Options + +| Option | Description | +|--------|-------------| +| `--overwrite` | Overwrite existing body content if it already exists | + +### Examples -#### Dependencies -- **PIL (Pillow)**: For dynamic image generation -- **zipfile**: For creating compressed archives -- **Wagtail**: Uses `wagtail.images.models.Image` and `wagtail.documents.models.Document` +#### Populate empty home page +```bash +docker exec -it wagtail-starter-kit-app-1 python manage.py populate_homepage +``` + +#### Replace existing content +```bash +docker exec -it wagtail-starter-kit-app-1 python manage.py populate_homepage --overwrite +``` --- @@ -281,23 +264,6 @@ docker exec -it wagtail-starter-kit-app-1 python manage.py populate_blog --posts docker exec -it wagtail-starter-kit-app-1 python manage.py populate_blog --clear --posts 12 --authors 3 ``` -### Sample Output - -When running the command, you'll see output like: -``` -Blog index page already exists -Blog tag index page already exists -Using 3 existing authors -Created 5 blog posts -Successfully created blog structure with 5 posts and 3 authors -``` - -The command provides clear feedback on: -- Whether blog structure already exists -- Author creation vs. reuse -- Number of posts created -- Final summary of created content - --- ## Future Commands From f4b5e2da486c0c81002ae4fc4b3b19f7b0abe3ad Mon Sep 17 00:00:00 2001 From: Nick Moreton Date: Sat, 26 Jul 2025 22:12:04 +0100 Subject: [PATCH 32/40] change description heading Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/management-commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/management-commands.md b/docs/management-commands.md index bab6374..69afb31 100644 --- a/docs/management-commands.md +++ b/docs/management-commands.md @@ -128,7 +128,7 @@ docker exec -it wagtail-starter-kit-app-1 python manage.py create_sample_media - **Purpose**: Populates the existing home page with sample body content for testing and demonstration purposes. If images exist in the media library, one will be randomly selected and included in the content. -### Description +### Overview The `populate_homepage` command adds rich HTML content to the body field of your site's home page. This is useful for: From 6eed693083842dd42f4695d3b9a8093cc721f0b4 Mon Sep 17 00:00:00 2001 From: Nick Moreton Date: Sat, 26 Jul 2025 22:12:40 +0100 Subject: [PATCH 33/40] change usage heading Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/management-commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/management-commands.md b/docs/management-commands.md index 69afb31..53ed5ac 100644 --- a/docs/management-commands.md +++ b/docs/management-commands.md @@ -145,7 +145,7 @@ The generated content includes: - **Feature overview** of the starter kit - **Next steps** for development -### Usage +### Command Usage ```bash # Basic usage (populates empty home page) From 954be6bce76c745dca4f0f0c37c4a91684bfbbdf Mon Sep 17 00:00:00 2001 From: Nick Moreton Date: Sat, 26 Jul 2025 22:12:59 +0100 Subject: [PATCH 34/40] change options heading Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/management-commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/management-commands.md b/docs/management-commands.md index 53ed5ac..43f42f0 100644 --- a/docs/management-commands.md +++ b/docs/management-commands.md @@ -155,7 +155,7 @@ python manage.py populate_homepage python manage.py populate_homepage --overwrite ``` -### Options +### Command Options | Option | Description | |--------|-------------| From bb4a0b479d4e3abc1c4ba5ab40b26de018226581 Mon Sep 17 00:00:00 2001 From: Nick Moreton Date: Sat, 26 Jul 2025 22:13:25 +0100 Subject: [PATCH 35/40] change examples heading Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/management-commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/management-commands.md b/docs/management-commands.md index 43f42f0..38bf4f2 100644 --- a/docs/management-commands.md +++ b/docs/management-commands.md @@ -161,7 +161,7 @@ python manage.py populate_homepage --overwrite |--------|-------------| | `--overwrite` | Overwrite existing body content if it already exists | -### Examples +### Command Examples #### Populate empty home page ```bash From 3ce027a7ddf697e8852bce3626d483ad1c390b10 Mon Sep 17 00:00:00 2001 From: Nick Moreton Date: Sat, 26 Jul 2025 22:13:42 +0100 Subject: [PATCH 36/40] remove old comments Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/blog/management/commands/populate_blog.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/blog/management/commands/populate_blog.py b/app/blog/management/commands/populate_blog.py index c1e9740..454cc2c 100644 --- a/app/blog/management/commands/populate_blog.py +++ b/app/blog/management/commands/populate_blog.py @@ -108,9 +108,10 @@ def clear_blog_content(self): tag_index.delete() self.stdout.write("Cleared existing blog tag pages") - # # Finally delete authors - # Author.objects.all().delete() - # self.stdout.write("Cleared existing authors") + def clear_authors(self): + """Clear all authors""" + Author.objects.all().delete() + self.stdout.write("Cleared all authors") def create_blog_index(self, home_page): """Create or get the blog index page""" From 49b5af7cd09fd2052e945bc7ba34e7c721702a7d Mon Sep 17 00:00:00 2001 From: Nick Moreton Date: Sun, 27 Jul 2025 16:22:59 +0100 Subject: [PATCH 37/40] Correct the management command for populating blog data --- app/blog/management/commands/populate_blog.py | 41 ++++++------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/app/blog/management/commands/populate_blog.py b/app/blog/management/commands/populate_blog.py index 454cc2c..1b6a339 100644 --- a/app/blog/management/commands/populate_blog.py +++ b/app/blog/management/commands/populate_blog.py @@ -64,6 +64,8 @@ def handle(self, *args, **options): # Clear existing content if requested if options["clear"]: self.clear_blog_content() + self.clear_authors() + self.clear_tags() # Refresh home page from database after clearing content home_page.refresh_from_db() @@ -91,13 +93,8 @@ def handle(self, *args, **options): def clear_blog_content(self): """Clear existing blog content""" - # Delete blog posts first (children) - blog_posts = BlogPage.objects.all() - for post in blog_posts: - post.delete() - self.stdout.write("Cleared existing blog posts") - # Then delete the index pages + # Then delete the index pages, children also deleted blog_indices = BlogIndexPage.objects.all() for index in blog_indices: index.delete() @@ -113,6 +110,11 @@ def clear_authors(self): Author.objects.all().delete() self.stdout.write("Cleared all authors") + def clear_tags(self): + """Clear all blog tags""" + BlogPage.tags.through.objects.all().delete() + self.stdout.write("Cleared all blog tags") + def create_blog_index(self, home_page): """Create or get the blog index page""" # Try to get existing blog index that's a child of home page @@ -243,40 +245,20 @@ def create_blog_posts(self, blog_index, authors, num_posts): # Select random tags (2-4 tags per post) post_tags = random.sample(blog_tags, k=random.randint(2, 4)) - # Generate unique title and slug using date and random number - random_suffix = random.randint(100, 999) + # Generate unique title unique_title = f"{topic} - {post_date.strftime('%B %Y')} Edition" - base_slug = topic.lower().replace(" ", "-").replace("'", "") - unique_slug = f"{base_slug}-{post_date.strftime('%Y%m%d')}-{random_suffix}" - - # Ensure slug uniqueness by checking existing slugs - existing_slugs = BlogPage.objects.filter( - slug__startswith=base_slug - ).values_list("slug", flat=True) - - counter = 1 - original_slug = unique_slug - while unique_slug in existing_slugs: - unique_slug = f"{original_slug}-{counter}" - counter += 1 - intro = self.generate_intro(topic) body = self.generate_body(topic) - # Create the blog post using create method instead of manual instantiation blog_post = blog_index.add_child( instance=BlogPage( title=unique_title, - slug=unique_slug, date=post_date, intro=intro, body=body, ) ) - # Publish the page - blog_post.save_revision().publish() - # Add authors blog_post.authors.set(post_authors) @@ -286,7 +268,7 @@ def create_blog_posts(self, blog_index, authors, num_posts): # Add gallery images (0-3 images per post) if available_images: - num_images = random.randint(0, min(3, len(available_images))) + num_images = random.randint(1, min(6, len(available_images))) selected_images = random.sample(available_images, k=num_images) for idx, image in enumerate(selected_images): @@ -296,7 +278,8 @@ def create_blog_posts(self, blog_index, authors, num_posts): caption=f"Image {idx + 1} for {blog_post.title}", ) - blog_post.save() + revision = blog_post.save_revision() + revision.publish() self.stdout.write(f"Created {num_posts} blog posts") From 79e691c7f9c8312a2840a60b176abd8cb9b776cf Mon Sep 17 00:00:00 2001 From: Nick Moreton Date: Sun, 27 Jul 2025 16:26:27 +0100 Subject: [PATCH 38/40] Update test for expected output of populate_blog command --- app/blog/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/blog/tests.py b/app/blog/tests.py index ffbef33..fb5c465 100644 --- a/app/blog/tests.py +++ b/app/blog/tests.py @@ -84,10 +84,10 @@ def test_populate_blog_clear_option(self): # Check that clear messages appeared output = out.getvalue() - self.assertIn("Cleared existing blog posts", output) self.assertIn("Cleared existing blog index pages", output) self.assertIn("Cleared existing blog tag pages", output) - # Note: Authors are not cleared by default to preserve them across runs + self.assertIn("Cleared all authors", output) + self.assertIn("Cleared all blog tags", output) def test_blog_post_structure(self): """Test that blog posts have proper structure and relationships""" From acacfdfc5b32b83f518ea71ad3ab1d17d4f30843 Mon Sep 17 00:00:00 2001 From: Nick Moreton Date: Sun, 27 Jul 2025 16:33:24 +0100 Subject: [PATCH 39/40] correct comment --- app/blog/management/commands/populate_blog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/blog/management/commands/populate_blog.py b/app/blog/management/commands/populate_blog.py index 1b6a339..bfaf936 100644 --- a/app/blog/management/commands/populate_blog.py +++ b/app/blog/management/commands/populate_blog.py @@ -266,7 +266,7 @@ def create_blog_posts(self, blog_index, authors, num_posts): for tag in post_tags: blog_post.tags.add(tag) - # Add gallery images (0-3 images per post) + # Add gallery images (1-6 images per post) if available_images: num_images = random.randint(1, min(6, len(available_images))) selected_images = random.sample(available_images, k=num_images) From 564c0d619d661fc476dba88701aab0efa11756f7 Mon Sep 17 00:00:00 2001 From: Nick Moreton Date: Sun, 28 Dec 2025 21:29:55 +0000 Subject: [PATCH 40/40] Alter order of methods in Author model. --- app/blog/models.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/blog/models.py b/app/blog/models.py index e71e935..76c8e66 100644 --- a/app/blog/models.py +++ b/app/blog/models.py @@ -94,17 +94,15 @@ class Author(models.Model): FieldPanel("author_image"), ] - def __str__(self): - return self.name - class Meta: verbose_name_plural = "Authors" + def __str__(self): + return self.name + class BlogTagIndexPage(Page): - def get_context(self, request): - # Filter by tag tag = request.GET.get("tag") blogpages = BlogPage.objects.filter(tags__name=tag)