diff --git a/blog/__init__.py b/blog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blog/admin.py b/blog/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/blog/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/blog/apps.py b/blog/apps.py new file mode 100644 index 0000000..94788a5 --- /dev/null +++ b/blog/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BlogConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'blog' diff --git a/blog/migrations/0001_initial.py b/blog/migrations/0001_initial.py new file mode 100644 index 0000000..da895fe --- /dev/null +++ b/blog/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 5.0.2 on 2024-02-29 17:53 + +import django.db.models.deletion +import wagtail.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('wagtailcore', '0091_remove_revision_submitted_for_moderation'), + ] + + 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/blog/migrations/0002_blogpage.py b/blog/migrations/0002_blogpage.py new file mode 100644 index 0000000..3b19f9b --- /dev/null +++ b/blog/migrations/0002_blogpage.py @@ -0,0 +1,29 @@ +# Generated by Django 5.0.2 on 2024-02-29 19:28 + +import django.db.models.deletion +import wagtail.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0001_initial'), + ('wagtailcore', '0091_remove_revision_submitted_for_moderation'), + ] + + 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/blog/migrations/0003_blogpagegalleryimage.py b/blog/migrations/0003_blogpagegalleryimage.py new file mode 100644 index 0000000..5f6fade --- /dev/null +++ b/blog/migrations/0003_blogpagegalleryimage.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.2 on 2024-02-29 19:40 + +import django.db.models.deletion +import modelcluster.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0002_blogpage'), + ('wagtailimages', '0025_alter_image_file_alter_rendition_file'), + ] + + 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/blog/migrations/0004_author.py b/blog/migrations/0004_author.py new file mode 100644 index 0000000..7c541be --- /dev/null +++ b/blog/migrations/0004_author.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0.2 on 2024-02-29 19:48 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0003_blogpagegalleryimage'), + ('wagtailimages', '0025_alter_image_file_alter_rendition_file'), + ] + + 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/blog/migrations/0005_blogpage_authors.py b/blog/migrations/0005_blogpage_authors.py new file mode 100644 index 0000000..a9d74be --- /dev/null +++ b/blog/migrations/0005_blogpage_authors.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.2 on 2024-02-29 19:56 + +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/blog/migrations/0006_blogpagetag_blogpage_tags.py b/blog/migrations/0006_blogpagetag_blogpage_tags.py new file mode 100644 index 0000000..097948c --- /dev/null +++ b/blog/migrations/0006_blogpagetag_blogpage_tags.py @@ -0,0 +1,33 @@ +# Generated by Django 5.0.2 on 2024-02-29 20:00 + +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/blog/migrations/0007_blogtagindexpage.py b/blog/migrations/0007_blogtagindexpage.py new file mode 100644 index 0000000..c670845 --- /dev/null +++ b/blog/migrations/0007_blogtagindexpage.py @@ -0,0 +1,25 @@ +# Generated by Django 5.0.2 on 2024-02-29 20:07 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0006_blogpagetag_blogpage_tags'), + ('wagtailcore', '0091_remove_revision_submitted_for_moderation'), + ] + + 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/blog/migrations/__init__.py b/blog/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blog/models.py b/blog/models.py new file mode 100644 index 0000000..cf3f23b --- /dev/null +++ b/blog/models.py @@ -0,0 +1,108 @@ +from django import forms +from django.db import models +from modelcluster.fields import ParentalKey, ParentalManyToManyField +from modelcluster.contrib.taggit import ClusterTaggableManager +from taggit.models import TaggedItemBase + +from wagtail.models import Page, Orderable +from wagtail.fields import RichTextField +from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel +from wagtail.search import index + +from wagtail.snippets.models import register_snippet + +class BlogIndexPage(Page): + intro = RichTextField(blank=True) + + 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 + + content_panels = Page.content_panels + [ + FieldPanel('intro') + ] + +class BlogTagIndexPage(Page): + + def get_context(self, request): + 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 + +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) + + def main_image(self): + gallery_item = self.gallery_images.first() + if gallery_item: + return gallery_item.image + else: + return None + + search_fields = Page.search_fields + [ + index.SearchField('intro'), + index.SearchField('body'), + ] + + content_panels = Page.content_panels + [ + MultiFieldPanel([ + FieldPanel('date'), + FieldPanel('authors', widget=forms.CheckboxSelectMultiple), + FieldPanel('tags'), + ], heading="Blog information"), + 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'), + ] + +@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' \ No newline at end of file diff --git a/blog/templates/blog/blog_index_page.html b/blog/templates/blog/blog_index_page.html new file mode 100644 index 0000000..5203dce --- /dev/null +++ b/blog/templates/blog/blog_index_page.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% load wagtailcore_tags wagtailimages_tags %} + +{% block body_class %}template-blogindexpage{% endblock %} + +{% block content %} +
{{ post.intro }}
+ {{ post.body|richtext }} + {% endwith %} +{% endfor %} + +{% endblock %} \ No newline at end of file diff --git a/blog/templates/blog/blog_page.html b/blog/templates/blog/blog_page.html new file mode 100644 index 0000000..4a46a21 --- /dev/null +++ b/blog/templates/blog/blog_page.html @@ -0,0 +1,50 @@ +{% extends "base.html" %} + +{% load wagtailcore_tags wagtailimages_tags %} + +{% block body_class %}template-blogpage{% endblock %} + +{% block content %} +{{ item.caption }}
+
+ {{ blogpage.title }}
+ Revised: {{ blogpage.latest_revision_created_at }}
+