From ad14a69bfdc9b8bc1ab5e56a070d551dcdbfaea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Caux?= <git@jscaux.org> Date: Mon, 30 Jan 2023 07:36:26 +0100 Subject: [PATCH] Add calculated fields to Forum, Post, with signal receivers etc --- scipost_django/forums/admin.py | 5 ++- scipost_django/forums/apps.py | 19 ++++++++ .../management/commands/update_forum_cfs.py | 22 ++++++++++ .../management/commands/update_post_cfs.py | 22 ++++++++++ .../migrations/0015_auto_20230130_0459.py | 24 ++++++++++ .../migrations/0016_forum_cf_nr_posts.py | 18 ++++++++ scipost_django/forums/models.py | 44 +++++++++++++++---- scipost_django/forums/signals.py | 33 ++++++++++++++ .../forums/templates/forums/forum_detail.html | 24 +++++----- scipost_django/organizations/signals.py | 1 - scipost_django/scipost/signals.py | 1 - 11 files changed, 188 insertions(+), 25 deletions(-) create mode 100644 scipost_django/forums/management/commands/update_forum_cfs.py create mode 100644 scipost_django/forums/management/commands/update_post_cfs.py create mode 100644 scipost_django/forums/migrations/0015_auto_20230130_0459.py create mode 100644 scipost_django/forums/migrations/0016_forum_cf_nr_posts.py create mode 100644 scipost_django/forums/signals.py diff --git a/scipost_django/forums/admin.py b/scipost_django/forums/admin.py index 089438aec..4e0d4f504 100644 --- a/scipost_django/forums/admin.py +++ b/scipost_django/forums/admin.py @@ -32,7 +32,8 @@ admin.site.register(Meeting, MeetingAdmin) class PostAdmin(admin.ModelAdmin): - search_fields = ["posted_by", "subject", "text"] + list_display = ["anchor", "posted_by", "posted_on"] + search_fields = ["posted_by__last_name", "subject", "text"] autocomplete_fields = [ "posted_by", "vetted_by", @@ -43,7 +44,7 @@ admin.site.register(Post, PostAdmin) class MotionAdmin(admin.ModelAdmin): - search_fields = ["posted_by", "subject", "text"] + search_fields = ["posted_by__last_name", "subject", "text"] autocomplete_fields = [ "posted_by", "vetted_by", diff --git a/scipost_django/forums/apps.py b/scipost_django/forums/apps.py index a4baef8f5..f6a570219 100644 --- a/scipost_django/forums/apps.py +++ b/scipost_django/forums/apps.py @@ -1,5 +1,24 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + from django.apps import AppConfig +from django.db.models.signals import post_save, post_delete class ForumsConfig(AppConfig): name = "forums" + + def ready(self): + super().ready() + + from . import signals + from forums.models import Post + post_save.connect( + signals.post_save_update_cfs_in_post_hierarchy, + sender=Post, + ) + post_delete.connect( + signals.post_delete_update_cfs_in_post_hierarchy, + sender=Post, + ) diff --git a/scipost_django/forums/management/commands/update_forum_cfs.py b/scipost_django/forums/management/commands/update_forum_cfs.py new file mode 100644 index 000000000..72f00c25f --- /dev/null +++ b/scipost_django/forums/management/commands/update_forum_cfs.py @@ -0,0 +1,22 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django.core.management.base import BaseCommand + +from forums.models import Forum + + +class Command(BaseCommand): + help = ( + "For all Forum instances, this updates the calculated fields." + ) + + def handle(self, *args, **kwargs): + for forum in Forum.objects.all(): + forum.update_cfs() + self.stdout.write( + self.style.SUCCESS( + "Successfully updated Forum calculated fields" + ) + ) diff --git a/scipost_django/forums/management/commands/update_post_cfs.py b/scipost_django/forums/management/commands/update_post_cfs.py new file mode 100644 index 000000000..67a6c2fdf --- /dev/null +++ b/scipost_django/forums/management/commands/update_post_cfs.py @@ -0,0 +1,22 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django.core.management.base import BaseCommand + +from forums.models import Post + + +class Command(BaseCommand): + help = ( + "For all Post instances, this updates the calculated fields." + ) + + def handle(self, *args, **kwargs): + for post in Post.objects.all(): + post.update_cfs() + self.stdout.write( + self.style.SUCCESS( + "Successfully updated Post calculated fields" + ) + ) diff --git a/scipost_django/forums/migrations/0015_auto_20230130_0459.py b/scipost_django/forums/migrations/0015_auto_20230130_0459.py new file mode 100644 index 000000000..8494d75c3 --- /dev/null +++ b/scipost_django/forums/migrations/0015_auto_20230130_0459.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.16 on 2023-01-30 03:59 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('forums', '0014_auto_20230129_1918'), + ] + + operations = [ + migrations.AddField( + model_name='post', + name='cf_latest_followup_in_hierarchy', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='latest_followup_in_hierarchy_of', to='forums.post'), + ), + migrations.AddField( + model_name='post', + name='cf_nr_followups', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + ] diff --git a/scipost_django/forums/migrations/0016_forum_cf_nr_posts.py b/scipost_django/forums/migrations/0016_forum_cf_nr_posts.py new file mode 100644 index 000000000..4f4cc712d --- /dev/null +++ b/scipost_django/forums/migrations/0016_forum_cf_nr_posts.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2023-01-30 04:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('forums', '0015_auto_20230130_0459'), + ] + + operations = [ + migrations.AddField( + model_name='forum', + name='cf_nr_posts', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + ] diff --git a/scipost_django/forums/models.py b/scipost_django/forums/models.py index 3ca9bda51..f6bf29feb 100644 --- a/scipost_django/forums/models.py +++ b/scipost_django/forums/models.py @@ -72,6 +72,9 @@ class Forum(models.Model): related_query_name="parent_forums", ) + # calculated fields + cf_nr_posts = models.PositiveSmallIntegerField(blank=True, null=True) + objects = ForumQuerySet.as_manager() class Meta: @@ -89,6 +92,11 @@ class Forum(models.Model): def get_absolute_url(self): return reverse("forums:forum_detail", kwargs={"slug": self.slug}) + def update_cfs(self): + self.update_cf_nr_posts() + if self.parent: + self.parent.update_cfs() + @property def nr_posts(self): """Recursively counts the number of posts in this Forum.""" @@ -99,6 +107,10 @@ class Forum(models.Model): nr += self.posts.all().count() return nr + def update_cf_nr_posts(self): + self.cf_nr_posts = self.nr_posts + self.save() + def posts_hierarchy_id_list(self): id_list = [] for post in self.posts.all(): @@ -113,14 +125,6 @@ class Forum(models.Model): except: return None - # @property - # def posts_all(self): - # """ - # Return all posts in the hierarchy. - # """ - # posts_id_list = self.posts_hierarchy_id_list() - # return Post.objects.filter(id__in=posts_id_list) - class Meeting(Forum): """ @@ -258,6 +262,16 @@ class Post(models.Model): ) absolute_url = models.URLField(blank=True) + # calculated fields + cf_nr_followups = models.PositiveSmallIntegerField(blank=True, null=True) + cf_latest_followup_in_hierarchy = models.ForeignKey( + "forums.Post", + on_delete=models.SET_NULL, + blank=True, + null=True, + related_name="latest_followup_in_hierarchy_of", + ) + objects = PostQuerySet.as_manager() class Meta: @@ -283,6 +297,12 @@ class Post(models.Model): self.save() return self.absolute_url + def update_cfs(self): + self.update_cf_nr_followups() + self.update_cf_latest_followup_in_hierarchy() + if self.parent: + self.parent.update_cfs() + @property def nr_followups(self): nr = 0 @@ -292,6 +312,10 @@ class Post(models.Model): nr += self.followup_posts.all().count() return nr + def update_cf_nr_followups(self): + self.cf_nr_followups = self.nr_followups + self.save() + @property def latest_followup(self): return self.followup_posts.last() @@ -307,6 +331,10 @@ class Post(models.Model): id_list = self.posts_hierarchy_id_list() return Post.objects.filter(pk__in=id_list).exclude(pk=self.id).last() + def update_cf_latest_followup_in_hierarchy(self): + self.cf_latest_followup_in_hierarchy = self.latest_followup_in_hierarchy + self.save() + def get_anchor_forum_or_meeting(self): """ Climb back the hierarchy up to the original Forum. diff --git a/scipost_django/forums/signals.py b/scipost_django/forums/signals.py new file mode 100644 index 000000000..332b08593 --- /dev/null +++ b/scipost_django/forums/signals.py @@ -0,0 +1,33 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django.db.models.signals import post_save, post_delete +from django.dispatch import receiver + +from forums.models import Post + + +@receiver(post_save, sender=Post) +def post_save_update_cfs_in_post_hierarchy(sender, instance, created, **kwargs): + """ + When a Post is created, update all related Post (and Forum) calculated fields. + """ + + if created: + if instance.parent: + instance.parent.update_cfs() + + +@receiver(post_delete, sender=Post) +def post_delete_update_cfs_in_post_hierarchy(sender, instance, **kwargs): + """ + When a Post is deleted, update all related Post (and Forum) calculated fields. + """ + + if instance.parent: + instance.parent.update_cfs() + # to cover multi-instance Post deletion in admin, + # which might temporarily break recursive climbing of the hierarchy: + if instance.anchor: + instance.anchor.update_cfs() diff --git a/scipost_django/forums/templates/forums/forum_detail.html b/scipost_django/forums/templates/forums/forum_detail.html index dbc75188b..308c47364 100644 --- a/scipost_django/forums/templates/forums/forum_detail.html +++ b/scipost_django/forums/templates/forums/forum_detail.html @@ -32,7 +32,7 @@ <span class="d-flex flex-wrap justify-content-between"> <a href="{{ forum.get_absolute_url }}">{{ forum }}</a> - <span class="badge bg-primary rounded-pill">{% with nr_posts=forum.nr_posts %}{{ nr_posts }} post{{ nr_posts|pluralize }}{% endwith %}</span> + <span class="badge bg-primary rounded-pill">{{ forum.cf_nr_posts }} post{{ forum.cf_nr_posts|pluralize }}</span> </span> </h2> @@ -124,12 +124,12 @@ <h3>(most recent first)</h3> <ul> {% for post in forum.posts_all.all reversed %} - <li> - <a href="{{ post.get_absolute_url }}">{{ post.subject }}</a> posted by {{ post.posted_by.first_name }} {{ post.posted_by.last_name }} on {{ post.posted_on|date:"Y-m-d H:m" }} - {% if post.parent and not post.motion %} - - regarding <a href="{{ post.parent.get_absolute_url }}">{{ post.parent }}</a> - {% endif %} - </li> + <li> + <a href="{{ post.get_absolute_url }}">{{ post.subject }}</a> posted by {{ post.posted_by.first_name }} {{ post.posted_by.last_name }} on {{ post.posted_on|date:"Y-m-d H:i" }} + {% if post.parent and not post.motion %} + - regarding <a href="{{ post.parent.get_absolute_url }}">{{ post.parent }}</a> + {% endif %} + </li> {% endfor %} </ul> </div> @@ -149,18 +149,16 @@ <ul> {% for post in forum.posts_all.anchors %} <li> - <a href="{{ post.get_absolute_url }}">{{ post.subject }}</a> posted by {{ post.posted_by.first_name }} {{ post.posted_by.last_name }} on {{ post.posted_on|date:"Y-m-d H:m" }} + <a href="{{ post.get_absolute_url }}">{{ post.subject }}</a> posted by {{ post.posted_by.first_name }} {{ post.posted_by.last_name }} on {{ post.posted_on|date:"Y-m-d H:i" }} <p> - {% with post.nr_followups as nr_followups %} - {{ nr_followups }} followup{{ nr_followups|pluralize }} - {% endwith %} - {% with post.latest_followup_in_hierarchy as latest_followup %} + {{ post.cf_nr_followups }} followup{{ post.cf_nr_followups|pluralize }} + {% with post.cf_latest_followup_in_hierarchy as latest_followup %} {% if latest_followup %} , latest: <a href="{{ latest_followup.get_absolute_url }}"> {{ latest_followup.subject }}</a> posted by {{ latest_followup.posted_by.first_name }} {{ latest_followup.posted_by.last_name }} - on {{ latest_followup.posted_on|date:"Y-m-d H:m" }} + on {{ latest_followup.posted_on|date:"Y-m-d H:i" }} {% endif %} {% endwith %} </p> diff --git a/scipost_django/organizations/signals.py b/scipost_django/organizations/signals.py index e3c1f6d93..322b6b465 100644 --- a/scipost_django/organizations/signals.py +++ b/scipost_django/organizations/signals.py @@ -2,7 +2,6 @@ __copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" __license__ = "AGPL v3" -from django.core.exceptions import ObjectDoesNotExist from django.db.models.signals import post_save from django.dispatch import receiver diff --git a/scipost_django/scipost/signals.py b/scipost_django/scipost/signals.py index f02c46a31..6cdf1fb7c 100644 --- a/scipost_django/scipost/signals.py +++ b/scipost_django/scipost/signals.py @@ -2,7 +2,6 @@ __copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" __license__ = "AGPL v3" -from django.core.exceptions import ObjectDoesNotExist from django.db.models.signals import post_save from django.dispatch import receiver -- GitLab