Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(aktuelt): minimal viable news pages #9

Merged
merged 4 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ migrate:
createsuperuser:
docker-compose exec web python manage.py createsuperuser

# Run to trigger publishing of scheduled content when developing locally
# in production we run a cronjob that runs the wagtail command every x minutes
publish-scheduled:
docker-compose exec web python manage.py publish_scheduled

test:
docker-compose exec web python manage.py test

Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ We use pre-commit for making sure all code conforms to the same formatting. This
1. Install [Pre-commit](https://pre-commit.com/#install) (Usually `pip install pre-commit` or `brew install pre-commit`)
2. Activate it via `pre-commit install`

## Production setup

Site should follow expected Wagtail and Django patterns if nothing else is mentioned.

### Things to keep in mind

- We allow for scheduled content, make sure to run `publish_scheduled` command every x minutes

## Tests

Use `python manage.py test` in container, or `make test` locally to run test suite. We generally try to rely on Wagtail and Djange framework as much as possible, but feel free to test custom behaviour/code and add sanity checks.
Expand Down
2 changes: 2 additions & 0 deletions aktuelt/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@
class NewsPagesAPIViewSet(PagesAPIViewSet):
model = NewsPage

meta_fields = PagesAPIViewSet.meta_fields + ["last_published_at"]


api_router.register_endpoint("news", NewsPagesAPIViewSet)
13 changes: 13 additions & 0 deletions aktuelt/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.db import models


class ContributionTypes(models.TextChoices):
# TODO: Localization? Can we store the constant and translate to norwegian when shown to the user?
# Ref:
# - https://docs.djangoproject.com/en/4.2/ref/models/fields/#enumeration-types
# - https://docs.wagtail.org/en/stable/advanced_topics/i18n.html#internationalisation
TEXT = "Text"
PHOTOGRAPHY = "Photography"
VIDEO = "Video"
AUDIO = "Audio"
OTHER = "Other"
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Generated by Django 4.2.7 on 2023-12-02 17:01

import django.db.models.deletion
import modelcluster.fields
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("wagtailimages", "0025_alter_image_file_alter_rendition_file"),
("aktuelt", "0007_newstagindexpage"),
]

operations = [
migrations.CreateModel(
name="Contributor",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("name", models.CharField(max_length=255)),
("default_contribution_type", models.CharField(blank=True, max_length=250)),
(
"image",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="wagtailimages.image",
),
),
],
options={
"verbose_name_plural": "Contributors",
},
),
migrations.CreateModel(
name="NewsPageContributor",
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)),
("contribution_type", models.CharField(blank=True, max_length=250)),
(
"contributor",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name="+", to="aktuelt.contributor"
),
),
],
options={
"ordering": ["sort_order"],
"abstract": False,
},
),
migrations.RemoveField(
model_name="newspage",
name="authors",
),
migrations.AddField(
model_name="newspage",
name="main_image",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="wagtailimages.image",
),
),
migrations.DeleteModel(
name="Author",
),
migrations.AddField(
model_name="newspagecontributor",
name="page",
field=modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="news_page_contributors",
to="aktuelt.newspage",
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Generated by Django 4.2.7 on 2023-12-02 19:00

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("aktuelt", "0008_contributor_newspagecontributor_and_more"),
]

operations = [
migrations.RemoveField(
model_name="newspage",
name="date",
),
migrations.AddField(
model_name="newspage",
name="custom_published_at",
field=models.DateTimeField(blank=True, null=True, verbose_name="Publish override"),
),
migrations.AddField(
model_name="newspage",
name="custom_updated_at",
field=models.DateTimeField(blank=True, null=True, verbose_name="Update override"),
),
migrations.AlterField(
model_name="contributor",
name="default_contribution_type",
field=models.CharField(
choices=[
("Text", "Text"),
("Photography", "Photography"),
("Video", "Video"),
("Audio", "Audio"),
("Other", "Other"),
],
default="Text",
max_length=250,
),
),
]
78 changes: 63 additions & 15 deletions aktuelt/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
from wagtail.search import index
from wagtail.snippets.models import register_snippet

from aktuelt.constants import ContributionTypes
from aktuelt.serializers import (
AuthorsSerializer,
ContributorsSerializer,
NewsPageGallerySerializer,
NewsPageTagsSerializer,
)
Expand All @@ -22,6 +23,8 @@ class NewsPageTag(TaggedItemBase):


class NewsTagIndexPage(Page):
page_description = "Page to list all published news items with given tag"

def get_context(self, request):
tag = request.GET.get("tag")
newspages = NewsPage.objects.filter(tags__name=tag)
Expand All @@ -32,6 +35,7 @@ def get_context(self, request):


class NewsIndexPage(Page):
page_description = "Page to list all published news items"
subpage_types = ["aktuelt.NewsPage"]

intro = RichTextField(blank=True)
Expand All @@ -46,16 +50,27 @@ def get_context(self, request):


class NewsPage(Page):
page_description = "A regular news page"
parent_page_types = ["aktuelt.NewsIndexPage"]
subpage_types = []

date = models.DateField("Post date")
custom_published_at = models.DateTimeField("Publish override", blank=True, null=True)
custom_updated_at = models.DateTimeField("Update override", blank=True, null=True)
intro = models.CharField(max_length=250)
body = RichTextField(blank=True)
authors = ParentalManyToManyField("aktuelt.Author", blank=True)
tags = ClusterTaggableManager(through=NewsPageTag, blank=True)
main_image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)

def get_main_image(self):
if self.main_image:
return self.main_image

def main_image(self):
gallery_item = self.gallery_images.first()

return gallery_item.image if gallery_item else None
Expand All @@ -65,32 +80,61 @@ def main_image(self):
index.SearchField("body"),
]

def schedule(self):
updated_at = self.custom_updated_at or self.last_published_at
published_at = self.custom_published_at or self.first_published_at

return {
"updated_at": updated_at.isoformat() if updated_at else None,
"published_at": published_at.isoformat() if published_at else None,
"unpublished_at": self.expire_at.isoformat() if self.expire_at else None,
}

# This is the equivalent of extending `meta_fields` on NewsPagesApiViewSet.
# Doing it here since I want to reference potentially non-existing/local
# fields, even if it should ideally behave consistenly between content types
api_meta_fields = ["schedule"]

api_filter_fields = ["first_published_at"]

api_fields = [
APIField("intro"),
APIField("body"),
APIField("date"),
# TODO: Replace with prettier (main model based?) serializer pattern?
APIField("authors", serializer=AuthorsSerializer()),
APIField("contributors", serializer=ContributorsSerializer(source="news_page_contributors")),
APIField("tags", serializer=NewsPageTagsSerializer()),
APIField("gallery_images", serializer=NewsPageGallerySerializer()),
APIField("main_image", serializer=ImageRenditionField("fill-100x100")),
APIField("main_image", serializer=ImageRenditionField("fill-100x100", source="get_main_image")),
]

content_panels = Page.content_panels + [
FieldPanel("intro"),
FieldPanel("main_image"),
FieldPanel("body"),
MultiFieldPanel(
[
FieldPanel("date"),
FieldPanel("authors", widget=forms.CheckboxSelectMultiple),
FieldPanel("custom_published_at"),
FieldPanel("custom_updated_at"),
FieldPanel("tags"),
InlinePanel("news_page_contributors", label="Contributors"),
],
heading="News information",
),
FieldPanel("intro"),
FieldPanel("body"),
InlinePanel("gallery_images", label="Gallery images"),
]


class NewsPageContributor(Orderable):
page = ParentalKey(NewsPage, on_delete=models.CASCADE, related_name="news_page_contributors")
contributor = models.ForeignKey("aktuelt.Contributor", on_delete=models.CASCADE, related_name="+")
contribution_type = models.CharField(blank=True, max_length=250)

panels = [
FieldPanel("contributor"),
FieldPanel("contribution_type"),
]


class NewsPageGalleryImage(Orderable):
page = ParentalKey(NewsPage, on_delete=models.CASCADE, related_name="gallery_images")
image = models.ForeignKey("wagtailimages.Image", on_delete=models.CASCADE, related_name="+")
Expand All @@ -103,23 +147,27 @@ class NewsPageGalleryImage(Orderable):


@register_snippet
class Author(models.Model):
class Contributor(models.Model):
name = models.CharField(max_length=255)
author_image = models.ForeignKey(
image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
default_contribution_type = models.CharField(
max_length=250, choices=ContributionTypes.choices, default=ContributionTypes.TEXT
)

panels = [
FieldPanel("name"),
FieldPanel("author_image"),
FieldPanel("image"),
FieldPanel("default_contribution_type"),
]

def __str__(self):
return self.name

class Meta:
verbose_name_plural = "Authors"
verbose_name_plural = "Contributors"
15 changes: 8 additions & 7 deletions aktuelt/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@
from wagtail.images.api.fields import ImageRenditionField


class AuthorsSerializer(Field):
def to_representation(self, value):
class ContributorsSerializer(Field):
def to_representation(self, news_page_contributors):
return [
{
"id": value.id,
"name": value.name,
"image": ImageRenditionField("fill-100x100").to_representation(value.author_image)
if value.author_image
"id": entry.contributor.id,
"name": entry.contributor.name,
"contribution_type": entry.contribution_type or entry.contributor.default_contribution_type,
"image": ImageRenditionField("fill-100x100").to_representation(entry.contributor.image)
if entry.contributor.image
else None,
}
for value in value.all()
for entry in news_page_contributors.all()
]


Expand Down
Loading