Skip to content

Commit

Permalink
#136 add cover_image and alt text fields for blog and use them if no …
Browse files Browse the repository at this point in the history
…cover image for a post was set
  • Loading branch information
ephes committed Jun 12, 2024
1 parent 0cf5a76 commit f1dd0a1
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 24 deletions.
3 changes: 2 additions & 1 deletion cast/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ def get_show(self, _instance: Audio) -> dict:
context = self.context.copy()
if episode.cover_image is not None:
context["cover_image_url"] = episode.cover_image.file.url
metadata["poster"] = episode.get_cover_image_url(context, podcast)
cover_image_context = episode.get_cover_image_context(context, podcast)
metadata["poster"] = cover_image_context["cover_image_url"]
return metadata

@staticmethod
Expand Down
32 changes: 32 additions & 0 deletions cast/migrations/0058_add_cover_image_to_blog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 5.0.4 on 2024-06-12 16:10

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


class Migration(migrations.Migration):

dependencies = [
("cast", "0057_rename_cover_image_and_add_alt_text"),
("wagtailimages", "0025_alter_image_file_alter_rendition_file"),
]

operations = [
migrations.AddField(
model_name="blog",
name="cover_alt_text",
field=models.CharField(blank=True, default="", max_length=255),
),
migrations.AddField(
model_name="blog",
name="cover_image",
field=models.ForeignKey(
blank=True,
help_text="An optional cover image.",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="wagtailimages.image",
),
),
]
43 changes: 36 additions & 7 deletions cast/models/index_pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from wagtail.admin.panels import FieldPanel
from wagtail.admin.panels import FieldPanel, MultiFieldPanel
from wagtail.api import APIField
from wagtail.fields import RichTextField
from wagtail.images.models import Image
from wagtail.models import Page, PageManager

from cast import appsettings
Expand Down Expand Up @@ -53,6 +54,15 @@ class Blog(Page):
default=True,
help_text=_("Whether comments are enabled for this blog." ""),
)
cover_image = models.ForeignKey(
Image,
help_text=_("An optional cover image."),
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
cover_alt_text = models.CharField(max_length=255, blank=True, default="")
noindex = models.BooleanField(
"noindex",
default=False,
Expand All @@ -79,6 +89,18 @@ class Blog(Page):
FieldPanel("email"),
FieldPanel("author"),
FieldPanel("template_base_dir"),
MultiFieldPanel(
[
FieldPanel("cover_image"),
FieldPanel("cover_alt_text"),
],
heading="Cover Image",
classname="collapsed",
help_text=_(
"The cover image for this post. It will be used in the feed, "
"in the twitter card and maybe on the blog index page."
),
),
]
promote_panels = Page.promote_panels + [
FieldPanel("noindex"),
Expand Down Expand Up @@ -242,8 +264,12 @@ def get_theme_form(self, next_path: str, template_base_dir: str) -> django.forms
}
)

def get_cover_image_url(self):
return ""
def get_cover_image_context(self) -> dict[str, str]:
context = {"cover_image_url": "", "cover_alt_text": ""}
if self.cover_image is not None:
context["cover_image_url"] = self.cover_image.file.url
context["cover_alt_text"] = self.cover_alt_text
return context

@staticmethod
def get_context_from_repository(context: ContextDict, repository: BlogIndexRepository) -> ContextDict:
Expand Down Expand Up @@ -348,10 +374,13 @@ def itunes_categories_parsed(self) -> dict[str, list[str]]:
except json.decoder.JSONDecodeError:
return {}

def get_cover_image_url(self):
if self.itunes_artwork is not None:
return self.itunes_artwork.original.url
return ""
def get_cover_image_context(self) -> dict[str, str]:
context = super().get_cover_image_context()
if context["cover_image_url"] == "":
# fallback to itunes artwork
if self.itunes_artwork is not None:
context["cover_image_url"] = self.itunes_artwork.original.url
return context

def get_context(self, request: HtmxHttpRequest, *args, **kwargs) -> ContextDict:
context = super().get_context(request, *args, **kwargs)
Expand Down
16 changes: 10 additions & 6 deletions cast/models/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,12 +512,15 @@ def podlove_players(self) -> list[tuple[str, str]]:
return result

@staticmethod
def get_cover_image_url(context: "ContextDict", blog: Optional["Blog"]) -> str:
if context.get("cover_image_url", ""):
return context["cover_image_url"]
def get_cover_image_context(context: "ContextDict", blog: Optional["Blog"]) -> dict[str, str]:
if (cover_image_url_from_post := context.get("cover_image_url")) is not None:
# if the cover image is set in the context, use it
return {"cover_image_url": cover_image_url_from_post, "cover_alt_text": context.get("cover_alt_text", "")}
if blog is not None:
return blog.get_cover_image_url()
return ""
return blog.get_cover_image_context()

# no cover image set
return {"cover_image_url": "", "cover_alt_text": ""}

def get_description(
self,
Expand Down Expand Up @@ -680,7 +683,8 @@ def get_template(self, request: HttpRequest, *args, **kwargs) -> str:
def get_context(self, request, **kwargs) -> "ContextDict":
context = super().get_context(request, **kwargs)
context["episode"] = self
context["cover_image_url"] = self.get_cover_image_url(context, self.podcast)
# context.update(self.get_cover_image_context(context, self.podcast))
context["cover_image_url"] = self.get_cover_image_context(context, self.podcast)
context["cover_alt_text"] = self.cover_alt_text if self.cover_alt_text else "iTunes Artwork"
if hasattr(request, "build_absolute_uri"):
blog_slug = context["repository"].blog.slug
Expand Down
5 changes: 3 additions & 2 deletions docs/episode.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ In addition to the effect setting a cover image for a post has, setting a
cover image for an episode will also be used as the episode's artwork in the
podcast feed and in the Podlove Web Player.

And if no cover image is set for the episode, the iTunes artwork of the podcast
will be used as the episode's cover image / artwork.
If no cover image is set for the episode, the blog’s cover image will be used.
If the :ref:`blog <blog_overview>` does not have a cover image, the podcast’s
iTunes artwork will be used as the episode’s cover image.

Podcast Audio
=============
Expand Down
3 changes: 2 additions & 1 deletion docs/post.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ The cover image for a post is used for meta tags like `twitter:image` or
or the post detail page. If you set `cover_alt_text`, it can be used as the
`alt` attribute of the cover image.

For posts that don't have a cover image, I usually generate one using the
For posts without a cover image, the :ref:`blog <blog_overview>`’s cover
image will be used. Alternatively, I often generate one using the
`shot-scraper <https://github.com/simonw/shot-scraper>`_ tool:

.. code-block:: shell
Expand Down
39 changes: 32 additions & 7 deletions tests/models_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,31 @@ def test_get_django_repository(self, blog, simple_request, use_django_blog_index
repository = blog.get_repository(simple_request, {})
assert isinstance(repository, BlogIndexRepository)

def test_get_cover_image_url_for_blog(self):
assert "" == Blog(id=1).get_cover_image_url()
def test_get_cover_image_url_for_blog(self, mocker):
# just empty cover image
blog = Blog(id=1)
assert blog.get_cover_image_context() == {"cover_image_url": "", "cover_alt_text": ""}

# cover image via blog.cover_image - using mock object because of the ImageField setter
mock_image = mocker.MagicMock()
mock_image.file.url = "https://example.org/cover.jpg"
mocker.patch("cast.models.Blog.cover_image", mock_image)
assert blog.get_cover_image_context() == {
"cover_image_url": "https://example.org/cover.jpg",
"cover_alt_text": "",
}


class TestPodcastModel:
def test_cover_image_from_super(self, mocker):
podcast = Podcast(id=1)
mocker_image = mocker.MagicMock()
mocker_image.file.url = "https://example.org/cover.jpg"
mocker.patch("cast.models.Blog.cover_image", mocker_image)
assert podcast.get_cover_image_context() == {
"cover_image_url": "https://example.org/cover.jpg",
"cover_alt_text": "",
}


class TestPostModel:
Expand Down Expand Up @@ -357,14 +380,15 @@ def test_get_updated_timestamp(self):
post.last_published_at = timezone.now()
assert post.get_updated_timestamp() == int(post.last_published_at.timestamp())

def test_get_cover_image_url(self):
def test_get_cover_image_context(self):
post = Post(id=1) #
# no cover_image
cover_image_url = post.get_cover_image_url({}, None)
assert cover_image_url == ""
context = post.get_cover_image_context({}, None)
assert context == {"cover_alt_text": "", "cover_image_url": ""}

# test return early, because episode.cover_image was set
cover_image_url = post.get_cover_image_url({"cover_image_url": "https://example.org/cover.jpg"}, None)
context = post.get_cover_image_context({"cover_image_url": "https://example.org/cover.jpg"}, None)
cover_image_url = context["cover_image_url"]
assert cover_image_url == "https://example.org/cover.jpg"

# get cover image from iTunes artwork
Expand All @@ -376,7 +400,8 @@ class Original:
artwork = ItunesArtWork(original=Original())
podcast = Podcast(id=1, itunes_artwork=artwork)

cover_image_url = post.get_cover_image_url({}, podcast)
context = post.get_cover_image_context({}, podcast)
cover_image_url = context["cover_image_url"]
assert cover_image_url == "https://example.org/itunes.jpg"


Expand Down

0 comments on commit f1dd0a1

Please sign in to comment.