From 7e2633e426b59fae244a5398a2261c8ee355c1be Mon Sep 17 00:00:00 2001 From: Joseph Muller Date: Mon, 29 Sep 2025 15:16:25 +0100 Subject: [PATCH 1/7] feat #3970 Introduce journal publishing status --- src/core/forms/forms.py | 1 + src/journal/admin.py | 1 + ...69_alter_journal_options_journal_status.py | 31 ++++ src/journal/models.py | 21 ++- src/press/forms.py | 1 + .../0037_press_order_journals_az.py | 21 +++ src/press/models.py | 152 ++++++++++++++---- src/press/views.py | 15 +- .../admin/core/manager/users/list.html | 2 +- .../admin/elements/forms/group_journal.html | 5 +- src/templates/admin/press/edit_press.html | 1 + .../press/elements/journal_link.html | 12 ++ .../templates/press/press_journal_set.html | 88 ++++++++++ .../OLH/templates/press/press_journals.html | 109 ++----------- .../templates/press/press_journal_set.html | 56 +++++++ .../clean/templates/press/press_journals.html | 69 +++----- 16 files changed, 399 insertions(+), 186 deletions(-) create mode 100644 src/journal/migrations/0069_alter_journal_options_journal_status.py create mode 100644 src/press/migrations/0037_press_order_journals_az.py create mode 100644 src/themes/OLH/templates/press/elements/journal_link.html create mode 100644 src/themes/OLH/templates/press/press_journal_set.html create mode 100644 src/themes/clean/templates/press/press_journal_set.html diff --git a/src/core/forms/forms.py b/src/core/forms/forms.py index d64e964525..2e1b4fe31a 100755 --- a/src/core/forms/forms.py +++ b/src/core/forms/forms.py @@ -495,6 +495,7 @@ class Meta: "remote_view_url", "remote_submit_url", "hide_from_press", + "status", ) diff --git a/src/journal/admin.py b/src/journal/admin.py index 4c14d96baf..c90dc07c62 100755 --- a/src/journal/admin.py +++ b/src/journal/admin.py @@ -85,6 +85,7 @@ class JournalAdmin(admin.ModelAdmin): list_display = ( "name", "code", + "sequence", "domain", "is_remote", "is_conference", diff --git a/src/journal/migrations/0069_alter_journal_options_journal_status.py b/src/journal/migrations/0069_alter_journal_options_journal_status.py new file mode 100644 index 0000000000..2baf6cc24e --- /dev/null +++ b/src/journal/migrations/0069_alter_journal_options_journal_status.py @@ -0,0 +1,31 @@ +# Generated by Django 4.2.22 on 2025-09-23 14:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("journal", "0068_issue_cached_display_title_a11y_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="journal", + options={"ordering": ("sequence",)}, + ), + migrations.AddField( + model_name="journal", + name="status", + field=models.CharField( + choices=[ + ("active", "Active"), + ("archived", "Archived"), + ("coming_soon", "Coming soon"), + ("test", "Test"), + ], + default="active", + max_length=20, + verbose_name="Publishing status", + ), + ), + ] diff --git a/src/journal/models.py b/src/journal/models.py index b441b05cfe..eebf7c01cb 100644 --- a/src/journal/models.py +++ b/src/journal/models.py @@ -32,7 +32,7 @@ from django.urls import reverse from django.utils import timezone, translation from django.utils.functional import cached_property -from django.utils.translation import gettext +from django.utils.translation import gettext, gettext_lazy as _ from core import ( files, @@ -267,6 +267,19 @@ class Journal(AbstractSiteModel): # Boolean to determine if this journal should be hidden from the press hide_from_press = models.BooleanField(default=False) + class PublishingStatus(models.TextChoices): + ACTIVE = "active", _("Active") + ARCHIVED = "archived", _("Archived") + COMING_SOON = "coming_soon", _("Coming soon") + TEST = "test", _("Test") + + status = models.CharField( + max_length=20, + choices=PublishingStatus.choices, + default=PublishingStatus.ACTIVE, + verbose_name="Publishing status", + ) + # Display sequence on the Journals page sequence = models.PositiveIntegerField(default=0) @@ -298,6 +311,12 @@ class Journal(AbstractSiteModel): disable_front_end = models.BooleanField(default=False) + class Meta: + ordering = ("sequence",) + # Note that we also commonly want to order journals A-Z by name. + # We have built Press methods to handle this since it is not + # straightforward to do via 'Meta.ordering'. + def __str__(self): if self.domain: return "{0}: {1}".format(self.code, self.domain) diff --git a/src/press/forms.py b/src/press/forms.py index 10e321cf42..af1c8c388e 100755 --- a/src/press/forms.py +++ b/src/press/forms.py @@ -39,6 +39,7 @@ class Meta: "password_upper", "password_length", "tracking_code", + "order_journals_az", "disable_journals", "privacy_policy_url", ) diff --git a/src/press/migrations/0037_press_order_journals_az.py b/src/press/migrations/0037_press_order_journals_az.py new file mode 100644 index 0000000000..e0ddac6d0f --- /dev/null +++ b/src/press/migrations/0037_press_order_journals_az.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.22 on 2025-09-23 14:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("press", "0036_remove_press_password_reset_text_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="press", + name="order_journals_az", + field=models.BooleanField( + default=False, + help_text="Whether to order journals A-Z by journal name and ignore manually set sequence. Does not work for translated journal names.", + verbose_name="Order journals A-Z", + ), + ), + ] diff --git a/src/press/models.py b/src/press/models.py index e19c3649f3..7ae680f2eb 100755 --- a/src/press/models.py +++ b/src/press/models.py @@ -7,11 +7,13 @@ import json import os import uuid +import warnings from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core.validators import MinValueValidator from django.db import models +from django.utils import timezone from modeltranslation.utils import build_localized_fieldname from django.apps import apps @@ -184,6 +186,13 @@ class Press(AbstractSiteModel): disable_journals = models.BooleanField( default=False, help_text="If enabled, the journals page will no longer render." ) + order_journals_az = models.BooleanField( + default=False, + verbose_name="Order journals A-Z", + help_text="Whether to order journals A-Z by journal name " + "and ignore manually set sequence. Does not work " + "for translated journal names.", + ) def __str__(self): return "%s" % self.name @@ -199,36 +208,10 @@ def get_press(request): except BaseException: return None - @staticmethod - def journals(**filters): - from journal import models as journal_models - - if filters: - return journal_models.Journal.objects.filter(**filters) - return journal_models.Journal.objects.all() - - @property - def journals_az(self): - """ - Get the a queryset of journals, ordered A-Z by journal name. - Note that this does not support multilingual journal names: - more work is needed on django-modeltranslation to - support Django subqueries. - """ + def journals(self, **filters): Journal = apps.get_model("journal", "Journal") - localized_column = build_localized_fieldname("value", settings.LANGUAGE_CODE) - name = core_models.SettingValue.objects.filter( - journal=models.OuterRef("pk"), - setting__name="journal_name", - ) - journals = Journal.objects.all().annotate( - journal_name=models.Subquery( - name.values_list(localized_column, flat=True)[:1], - output_field=models.CharField(), - ) - ) - ordered = journals.order_by("journal_name") - return ordered + journals = Journal.objects.filter(**filters) + return self.apply_journal_ordering(journals) @property def active_news_items(self): @@ -251,7 +234,7 @@ def users(): def issues(self, **filters): if not filters: filters = {} - filters["journal__press"] = self + filters["journal__hide_from_press"] = False from journal import models as journal_models if filters: @@ -382,11 +365,15 @@ def get_setting_value(self, name): @property def publishes_conferences(self): - return self.journals(is_conference=True).count() > 0 + conferences = self.journals( + hide_from_press=False, + is_conference=True, + ) + return conferences.count() > 0 @property def publishes_journals(self): - return self.journals(is_conference=False).count() > 0 + return self.public_journals.count() > 0 @cache(600) def live_repositories(self): @@ -441,13 +428,109 @@ def navigation_items_for_sitemap(self): def code(self): return "press" + def apply_journal_ordering_az(self, journals): + """ + Order a queryset of journals A-Z on English-language journal name. + Note that this does not support multilingual journal names: + more work is needed on django-modeltranslation to + support Django subqueries. + :param journals: Queryset of Journal objects + """ + localized_column = build_localized_fieldname( + "value", + settings.LANGUAGE_CODE, # Assumed to be 'en' in default config + ) + name = core_models.SettingValue.objects.filter( + journal=models.OuterRef("pk"), + setting__name="journal_name", + ) + journals.annotate( + journal_name=models.Subquery( + name.values_list(localized_column, flat=True)[:1], + output_field=models.CharField(), + ) + ) + return journals.order_by("journal_name") + + def apply_journal_ordering(self, journals): + if self.order_journals_az: + return self.apply_journal_ordering_az(journals) + else: + # Journals will already have been ordered according to Meta.ordering + return journals + + @property + def journals_az(self): + """ + Deprecated. Use `journals` with Press.order_journals_az turned on. + """ + warnings.warn( + "`Press.journals_az` is deprecated. " + "Use `Press.journals` with Press.order_journals_az turned on." + ) + Journal = apps.get_model("journal", "Journal") + return self.apply_journal_ordering_az(Journal.objects.all()) + @property def public_journals(self): - Journal = apps.get_model("journal.Journal") + """ + Get all journals that are not hidden from the press + or designated as conferences. + Do not apply ordering yet, + since the caller may filter the queryset. + """ + Journal = apps.get_model("journal", "Journal") return Journal.objects.filter( hide_from_press=False, is_conference=False, - ).order_by("sequence") + ) + + @property + def public_active_journals(self): + """ + Get all journals that are visible to the press + and marked as 'Active' or 'Test' in the publishing status field. + + Note: Test journals are included so that users can test the journal + list safely. A separate mechanism exists to hide them from the press + once the press enters normal operation: + Journal.hide_from_press. + """ + Journal = apps.get_model("journal.Journal") + return self.apply_journal_ordering( + self.public_journals.filter( + status__in=[ + Journal.PublishingStatus.ACTIVE, + Journal.PublishingStatus.TEST, + ] + ) + ) + + @property + def public_archived_journals(self): + """ + Get all journals that are visible to the press + and marked as 'Archived' in the publishing status field. + """ + Journal = apps.get_model("journal.Journal") + return self.apply_journal_ordering( + self.public_journals.filter( + status=Journal.PublishingStatus.ARCHIVED, + ) + ) + + @property + def public_coming_soon_journals(self): + """ + Get all journals that are visible to the press + and marked as 'Coming soon' in the publishing status field. + """ + Journal = apps.get_model("journal.Journal") + return self.apply_journal_ordering( + self.public_journals.filter( + status=Journal.PublishingStatus.COMING_SOON, + ) + ) @property def published_articles(self): @@ -455,6 +538,7 @@ def published_articles(self): return Article.objects.filter( stage=submission_models.STAGE_PUBLISHED, date_published__lte=timezone.now(), + journal__hide_from_press=False, ) def next_group_order(self): diff --git a/src/press/views.py b/src/press/views.py index e935d6e55b..ce672dc8bf 100755 --- a/src/press/views.py +++ b/src/press/views.py @@ -125,7 +125,10 @@ def journals(request): template = "press/press_journals.html" context = { - "journals": request.press.public_journals, + "active_journals": request.press.public_active_journals, + "archived_journals": request.press.public_archived_journals, + "coming_soon_journals": request.press.public_coming_soon_journals, + "journals": request.press.public_journals, # Backwards compatibility } return render(request, template, context) @@ -139,10 +142,12 @@ def conferences(request): """ template = "press/press_journals.html" - journal_objects = journal_models.Journal.objects.filter( - hide_from_press=False, - is_conference=True, - ).order_by("sequence") + journal_objects = request.press.apply_journal_ordering( + journal_models.Journal.objects.filter( + hide_from_press=False, + is_conference=True, + ) + ) context = {"journals": journal_objects} diff --git a/src/templates/admin/core/manager/users/list.html b/src/templates/admin/core/manager/users/list.html index 541e739972..94c9b0edd8 100644 --- a/src/templates/admin/core/manager/users/list.html +++ b/src/templates/admin/core/manager/users/list.html @@ -37,7 +37,7 @@ account in {{ request.press.name }}. You can also manage users at the journal level: