Skip to content

Commit

Permalink
Merge pull request #80 from veselosky/79-allow-articles-to-be-linked-…
Browse files Browse the repository at this point in the history
…in-series

Allow Articles to be linked in Series #79
  • Loading branch information
veselosky authored May 27, 2024
2 parents fc4f3c3 + daa4942 commit c6c3317
Show file tree
Hide file tree
Showing 15 changed files with 471 additions and 80 deletions.
17 changes: 5 additions & 12 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
# KEEP ME!
# The Github worklows will choke at the setup-python step if there is no
# requirements.txt in the project (as of 2023-04-13).
#
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile --output-file=requirements.txt pyproject.toml
#
# This file was autogenerated by uv via the following command:
# uv pip compile -o requirements.txt pyproject.toml
asgiref==3.8.1
# via django
certifi==2024.2.2
Expand All @@ -15,11 +8,11 @@ charset-normalizer==3.3.2
# via requests
defusedxml==0.7.1
# via django-bootstrap-icons
django==5.0.4
django==5.0.6
# via
# django-genericsite (pyproject.toml)
# django-appconf
# django-bootstrap-icons
# django-genericsite (pyproject.toml)
# django-taggit
django-appconf==1.0.6
# via django-imagekit
Expand All @@ -37,7 +30,7 @@ pillow==10.3.0
# via
# django-genericsite (pyproject.toml)
# pilkit
requests==2.31.0
requests==2.32.2
# via django-bootstrap-icons
sqlparse==0.5.0
# via django
Expand Down
2 changes: 1 addition & 1 deletion src/genericsite/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.2.2"
__version__ = "0.2.3"
8 changes: 8 additions & 0 deletions src/genericsite/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from genericsite.models import (
Article,
ArticleSeries,
Author,
HomePage,
Image,
Expand Down Expand Up @@ -133,6 +134,7 @@ class ArticleAdmin(CreativeWorkAdmin):
"slug",
"site",
"section",
"series",
"status",
"date_published",
"description",
Expand Down Expand Up @@ -166,6 +168,12 @@ class ArticleAdmin(CreativeWorkAdmin):
)


#######################################################################################
@admin.register(ArticleSeries)
class ArticleSeriesAdmin(admin.ModelAdmin):
prepopulated_fields = {"slug": ("name",)}


#######################################################################################
@admin.register(Section)
class SectionAdmin(CreativeWorkAdmin):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Generated by Django 5.0.6 on 2024-05-26 17:41

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


class Migration(migrations.Migration):
dependencies = [
("genericsite", "0010_alter_attachment_file_alter_image_image_file"),
("sites", "0002_alter_domain_unique"),
]

operations = [
migrations.AlterModelOptions(
name="article",
options={
"get_latest_by": "date_published",
"verbose_name": "article",
"verbose_name_plural": "articles",
},
),
migrations.CreateModel(
name="ArticleSeries",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255, verbose_name="name")),
("slug", models.SlugField(verbose_name="slug")),
(
"description",
models.TextField(blank=True, verbose_name="description"),
),
(
"site",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="sites.site",
verbose_name="site",
),
),
],
),
migrations.AddField(
model_name="article",
name="series",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="genericsite.articleseries",
verbose_name="series",
),
),
migrations.AlterOrderWithRespectTo(
name="article",
order_with_respect_to="series",
),
]
91 changes: 87 additions & 4 deletions src/genericsite/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -797,17 +797,54 @@ def get_absolute_url(self):


#######################################################################
class ArticleSeries(models.Model):
"""
A model to represent a series of articles. This model is used to group articles
together in a series. The series is not a page, but a way to group articles for
display in feeds and to provide a way to navigate between articles in the series.
"""

name = models.CharField(_("name"), max_length=255)
slug = models.SlugField(_("slug"))
description = models.TextField(_("description"), blank=True)
site = models.ForeignKey(Site, on_delete=models.CASCADE, verbose_name=_("site"))

def __str__(self):
return self.name


#######################################################################
class ArticleQuerySet(models.QuerySet):
def live(self, **kwargs):
qs = self.filter(
models.Q(expires__isnull=True) | models.Q(expires__gt=timezone.now()),
status=Status.USABLE,
date_published__lte=timezone.now(),
)
if kwargs:
qs = qs.filter(**kwargs)
if "series" not in kwargs and not qs.ordered:
qs = qs.order_by("-date_published")
return qs


class ArticleManager(models.Manager):
def get_queryset(self):
return super().get_queryset().select_related("site", "section", "share_image")
return (
super()
.get_queryset()
.select_related("site", "section", "series", "share_image")
)


class Article(BasePage):
"Articles are the bread and butter of a site. They will appear in feeds."

class Meta(BasePage.Meta):
# Intentionally not inherting from AbstractCreativeWork's Meta because `ordering`
# and `order_with_respect_to` are not compatible with each other.
class Meta:
get_latest_by = "date_published"
ordering = ["-date_published"]
order_with_respect_to = "series"
verbose_name = _("article")
verbose_name_plural = _("articles")
constraints = [
Expand All @@ -832,15 +869,31 @@ class Meta(BasePage.Meta):
section = models.ForeignKey(
Section, verbose_name=_("section"), on_delete=models.PROTECT
)
series = models.ForeignKey(
ArticleSeries,
verbose_name=_("series"),
on_delete=models.SET_NULL,
blank=True,
null=True,
)
image_set = models.ManyToManyField(Image, verbose_name=_("related images"))
attachment_set = models.ManyToManyField(Attachment, verbose_name=_("attachments"))

objects = ArticleManager.from_queryset(CreativeWorkQuerySet)()
objects = ArticleManager.from_queryset(ArticleQuerySet)()

schema_type = "Article"
opengraph_type = "article"

def get_absolute_url(self):
if self.series:
return reverse(
"article_series_page",
kwargs={
"section_slug": self.section.slug,
"series_slug": self.series.slug,
"article_slug": self.slug,
},
)
return reverse(
"article_page",
kwargs={"article_slug": self.slug, "section_slug": self.section.slug},
Expand Down Expand Up @@ -868,6 +921,36 @@ def opengraph(self):
og.tag = tags
return og

@property
def series_part(self):
if not self.series:
return None

# Return a string formatted as "Part 1 of 3" based on the order of the article in the series
ids = list(self.series.get_article_order())
return f"Part {ids.index(self.id) + 1} of {len(ids)}"

def save(self, *args, **kwargs):
if not self.id:
# Django should do the Right Thing setting _order on new instances
return super().save(*args, **kwargs)

# When adding a series to an existing article, Django does not set _order,
# so it has the default 0, which breaks get_next_in_order
if self.series and not self._order:
# Order not assigned, place at end of series
self._order = self.series.get_article_order().count() + 1
elif not self.series:
# If the series is removed, reset the order
self._order = 0
# Save before resetting series order so the query finds this instance
retval = super().save(*args, **kwargs)
if self.series:
# Possible the series has changed, which could cause dupes. This
# will reset the _order for all articles in the series.
self.series.set_article_order(self.series.get_article_order())
return retval


#######################################################################
# Site Menus
Expand Down
5 changes: 5 additions & 0 deletions src/genericsite/templates/genericsite/article_detail.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% extends "genericsite/base.html" %}

{% block title %}{% if object.seo_title %}{{object.seo_title}}
{% else %}{{object.title}}{% if object.series %} | {{object.series.name}} {{object.series_part}}{% endif %}
{% endif %} | {% firstof brand request.site.name %}{% endblock %}

This file was deleted.

34 changes: 27 additions & 7 deletions src/genericsite/templates/genericsite/blocks/article_text.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
{% with object as article %}
{% load genericsite %}{% with object as article %}
<article class="article-full">
<h1 class="article-title">{% firstof article.headline article.title article.schema.headline %}</h1>
{% if article.author %}
<p class="article-meta">{{ article.date_published|date:"DATE_FORMAT"}}
<a href="{{ article.author.get_absolute_url }}">{{ article.author.name }}</a></p>
{% else %}
<p class="article-meta">{{ article.date_published|date:"DATE_FORMAT"}}</p>
<header class="article-header">
<h1 class="article-title">{% firstof article.headline article.title article.schema.headline %}</h1>
{% if article.author %}
<p class="article-meta">{{ article.date_published|date:"DATE_FORMAT"}}
<a href="{{ article.author.get_absolute_url }}">{{ article.author.name }}</a></p>
{% else %}
<p class="article-meta">{{ article.date_published|date:"DATE_FORMAT"}}</p>
{% endif %}
</header>
{% if article.series %}
<section class="article-series-info mb-3">
{% include "genericsite/includes/article_series.html" %}
</section>
{% endif %}
{% opengraph_image article as img %} {% if img %}
<p>
<img class="img-fluid" src="{{img.large.url}}" alt="{{img.alt_text}}" height="{{img.large.height}}" width="{{img.large.width}}" />
</p>
{% endif %}
{{ article.body|safe }}
{% if article.series %}
<footer class="article-footer">
{% if article.get_next_in_order %}
<p>Next in the series: <a href="{{article.get_next_in_order.get_absolute_url}}">{{article.get_next_in_order.title}}</a></p>
{% endif %}
{% include "genericsite/includes/article_series.html" %}
</footer>
{% endif %}
</article>
{% endwith %}
18 changes: 18 additions & 0 deletions src/genericsite/templates/genericsite/includes/article_series.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{% comment %}
Given an article that belongs to a series, display a list of all the articles in the
series, linking to all but the current article.
{% endcomment %}
{% with object.series as series %}
{% if series %}
<h2>Part of the Series: {{ series.name }}</h2>
<ol class="list-group list-group-numbered">
{% for article in series.article_set.all %}
{% if article != object %}
<li class="list-group-item"><a href="{{ article.get_absolute_url }}">{{ article.title }}</a></li>
{% else %}
<li class="list-group-item">{{ article.title }}</li>
{% endif %}
{% endfor %}
</ol>
{% endif %}
{% endwith %}
10 changes: 10 additions & 0 deletions src/genericsite/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@
generic.SectionView.as_view(),
name="section_paginated",
),
path(
"<slug:section_slug>/<slug:series_slug>/<slug:article_slug>.html",
generic.ArticleDetailView.as_view(),
name="article_series_page",
),
path(
"<slug:section_slug>/<slug:series_slug>/",
generic.ArticleSeriesView.as_view(),
name="series_page",
),
path(
"<slug:section_slug>/<slug:article_slug>.html",
generic.ArticleDetailView.as_view(),
Expand Down
Loading

0 comments on commit c6c3317

Please sign in to comment.