feat: implement blog features

This commit is contained in:
Konstantin 2024-02-29 21:13:03 +01:00
parent b93eabd429
commit 75662f9d98
28 changed files with 411 additions and 0 deletions

0
blog/__init__.py Normal file
View file

3
blog/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
blog/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class BlogConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'blog'

View file

@ -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',),
),
]

View file

@ -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',),
),
]

View file

@ -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,
},
),
]

View file

@ -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',
},
),
]

View file

@ -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'),
),
]

View file

@ -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'),
),
]

View file

@ -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',),
),
]

View file

108
blog/models.py Normal file
View file

@ -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'

View file

@ -0,0 +1,26 @@
{% extends "base.html" %}
{% load wagtailcore_tags wagtailimages_tags %}
{% block body_class %}template-blogindexpage{% endblock %}
{% block content %}
<h1>{{ page.title }}</h1>
<div class="intro">{{ page.intro|richtext }}</div>
{% for post in blogpages %}
{% with post=post.specific %}
<h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
<!-- Add this: -->
{% with post.main_image as main_image %}
{% if main_image %}{% image main_image fill-160x100 %}{% endif %}
{% endwith %}
<p>{{ post.intro }}</p>
{{ post.body|richtext }}
{% endwith %}
{% endfor %}
{% endblock %}

View file

@ -0,0 +1,50 @@
{% extends "base.html" %}
{% load wagtailcore_tags wagtailimages_tags %}
{% block body_class %}template-blogpage{% endblock %}
{% block content %}
<h1>{{ page.title }}</h1>
<p class="meta">{{ page.date }}</p>
{% with authors=page.authors.all %}
{% if authors %}
<h3>Posted by:</h3>
<ul>
{% for author in authors %}
<li style="display: inline">
{% image author.author_image fill-40x60 style="vertical-align: middle" %}
{{ author.name }}
</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<div class="intro">{{ page.intro }}</div>
{{ page.body|richtext }}
{% for item in page.gallery_images.all %}
<div style="float: inline-start; margin: 10px">
{% image item.image fill-320x240 %}
<p>{{ item.caption }}</p>
</div>
{% endfor %}
<p><a href="{{ page.get_parent.url }}">Return to blog</a></p>
{% with tags=page.tags.all %}
{% if tags %}
<div class="tags">
<h3>Tags</h3>
{% for tag in tags %}
<a href="{% slugurl 'tags' %}?tag={{ tag }}">{{ tag }}</a>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% endblock %}

View file

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% load wagtailcore_tags %}
{% block content %}
{% if request.GET.tag %}
<h4>Showing pages tagged "{{ request.GET.tag }}"</h4>
{% endif %}
{% for blogpage in blogpages %}
<p>
<strong><a href="{% pageurl blogpage %}">{{ blogpage.title }}</a></strong><br />
<small>Revised: {{ blogpage.latest_revision_created_at }}</small><br />
</p>
{% empty %}
No pages found with that tag.
{% endfor %}
{% endblock %}

3
blog/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
blog/views.py Normal file
View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View file

@ -24,6 +24,7 @@ BASE_DIR = os.path.dirname(PROJECT_DIR)
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
"blog",
"home", "home",
"search", "search",
"wagtail.contrib.forms", "wagtail.contrib.forms",

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB