diff --git a/docs/md/journal-visibility-and-publishing-status.md b/docs/md/journal-visibility-and-publishing-status.md new file mode 100644 index 0000000000..c9b6f0ef46 --- /dev/null +++ b/docs/md/journal-visibility-and-publishing-status.md @@ -0,0 +1,58 @@ +# Journal visibility and publishing status + +Janeway has several mechanisms for allowing editors and press managers to control content visibility and filter data streams for each journal. + +The following Journal fields provide the configuration options: + +- `Journal.hide_from_press` +- `Journal.status` + +## What are some example use cases for these configuration options? + +Users may find it tricky to distinguish `hide_from_press` and the "Test" publishing status. But they have to remain distinct for a few use cases. Here are some. + +### Not hidden from press + +* Active: publishing normally +* Archived: no longer publishing but have a backlist +* Coming soon: planning to start publishing soon +* Test: not applicable, as users should always hide test journals from the press + +### Hidden from press + +* Active: publishing normally but do not wish to be listed on the publisher site +* Archived: no longer published by the press but back content is still available at the journal site +* Coming soon: planning to start publishing soon but want to avoid appearing on the press site +* Test: testing options and training editors + +## What areas are affected by these configuration options? + +The `hide_from_press` field does exactly what it says--it puts up a wall between the journal and press, preventing records like `Journal`, like `Issue`, `Section`, `Article`, and `NewsItem` from showing up on press level websites and APIs. + +The "Test" publishing status prevents users from accidentally sending data to places it is difficult to remove, like DOI registration. It does not interfere with anything else, including sitemaps, APIs, or RSS feeds, because these too are features that users would want to test at the journal level. This is why it is important for test journals to have `hide_from_press` turned on. + +### User interfaces + +| Area | Hide from press | Test status | +|-------------------------------------------|-------------------|-------------| +| Lists of journals on press website | Does what it says | No effect | +| Journal submission in repository system | No effect | Prevented | +| Publications list on public user profiles | Does what it says | Not listed | +| Back-office menus that list journals | No effect | No effect | +| Django admin menus that list journals | No effect | No effect | +| Reporting (plugin) | Does what it says | No effect | + +### Data feeds and alternate user interfaces + +| Area | Hide from press | Test status | +|----------------------------|-------------------|-----------------------------------| +| sitemaps | Does what it says | No effect | +| APIs | Does what it says | No effect | +| RSS/Atom feed | Does what it says | No effect | +| reader notifications | Not applicable | No effect | +| Crossref deposits | Not applicable | Deposits use Crossref test server | +| Datacite deposits (plugin) | Not applicable | Deposits use Datacite test server | +| Galley healthcheck command | Not applicable | Articles ignored | +| DOI check command | Not applicable | Articles ignored | +| Store ithenticate command | Not applicable | Articles ignored | +| Metrics core app | Does what it says | Articles excluded | diff --git a/src/api/oai/views.py b/src/api/oai/views.py index 2d29da4f2e..b366a3d565 100644 --- a/src/api/oai/views.py +++ b/src/api/oai/views.py @@ -50,7 +50,6 @@ class OAIListRecords(OAIPagedModelView): def get_queryset(self): queryset = super().get_queryset() - if self.request.journal: queryset = queryset.filter(journal=self.request.journal) else: @@ -217,6 +216,10 @@ def get_context_data(self, *args, **kwargs): journal=self.request.journal, stage=submission_models.STAGE_PUBLISHED, ) + else: + articles = articles.filter( + journal__hide_from_press=False, + ) context["earliest_article"] = articles.earliest("date_published") context["verb"] = self.request.GET.get("verb") @@ -247,6 +250,16 @@ def get_context_data(self, *args, **kwargs): sections = sections.filter( journal=self.request.journal, ) + else: + journals = journals.filter( + hide_from_press=False, + ) + all_issues = all_issues.filter( + journal__hide_from_press=False, + ) + sections = sections.filter( + journal__hide_from_press=False, + ) context["journals"] = journals context["all_issues"] = all_issues diff --git a/src/api/tests/test_api.py b/src/api/tests/test_api.py index 876c562b46..0c5a44d201 100644 --- a/src/api/tests/test_api.py +++ b/src/api/tests/test_api.py @@ -1,17 +1,21 @@ from django.test import TestCase, override_settings from django.urls import reverse +from django.utils import timezone from rest_framework.test import APIClient from utils.testing import helpers from core import models as core_models +from submission import models as submission_models class TestAPI(TestCase): @classmethod def setUpTestData(cls): cls.press = helpers.create_press() - cls.journal, _ = helpers.create_journals() + cls.journal, cls.hidden_journal = helpers.create_journals() + cls.hidden_journal.hide_from_press = True + cls.hidden_journal.save() cls.staff_member = helpers.create_user( username="t.paris@voyager.com", roles=["author"], @@ -34,6 +38,14 @@ def setUpTestData(cls): helpers.create_roles( ["journal-manager"], ) + cls.hidden_article = helpers.create_article( + cls.hidden_journal, + with_author=True, + title="Article in journal hidden from press", + ) + cls.hidden_article.stage = submission_models.STAGE_PUBLISHED + cls.hidden_article.date_published = timezone.now() + cls.hidden_article.save() cls.api_client = APIClient() @override_settings(URL_CONFIG="domain") @@ -89,3 +101,39 @@ def test_editor_cannot_assign_journal_manager_role(self): journal_manager_role, ) ) + + @override_settings(URL_CONFIG="domain") + def test_press_api_excludes_journal_hidden_from_press(self): + url = self.press.site_url(reverse("journal-list")) + response = self.api_client.get( + url, + SERVER_NAME=self.press.domain, + ) + self.assertNotIn( + self.hidden_journal.pk, + [journal["pk"] for journal in response.json().get("results", [])], + ) + + @override_settings(URL_CONFIG="domain") + def test_press_api_excludes_article_in_journal_hidden_from_press(self): + url = self.press.site_url(reverse("article-list")) + response = self.api_client.get( + url, + SERVER_NAME=self.press.domain, + ) + self.assertNotIn( + self.hidden_article.title, + [article["title"] for article in response.json().get("results", [])], + ) + + @override_settings(URL_CONFIG="domain") + def test_api_works_at_journal_level_even_if_hidden_from_press(self): + url = self.hidden_journal.site_url(reverse("article-list")) + response = self.api_client.get( + url, + SERVER_NAME=self.hidden_journal.domain, + ) + self.assertIn( + self.hidden_article.title, + [article["title"] for article in response.json().get("results", [])], + ) diff --git a/src/api/tests/test_oai.py b/src/api/tests/test_oai.py index 520c79e45c..8a2ee17ae0 100644 --- a/src/api/tests/test_oai.py +++ b/src/api/tests/test_oai.py @@ -60,7 +60,9 @@ def assertXMLEqual(self, a, b): @classmethod def setUpTestData(cls): cls.press = helpers.create_press() - cls.journal, _ = helpers.create_journals() + cls.journal, cls.hidden_journal = helpers.create_journals() + cls.hidden_journal.hide_from_press = True + cls.hidden_journal.save() cls.author = helpers.create_author(cls.journal) cls.article = helpers.create_submission( journal_id=cls.journal.pk, @@ -86,6 +88,13 @@ def setUpTestData(cls): ) cls.article.primary_issue = cls.issue cls.article.save() + cls.hidden_article = helpers.create_article( + cls.hidden_journal, + with_author=True, + stage=sm_models.STAGE_PUBLISHED, + title="Article in journal hidden from press", + date_published="1986-07-12T17:00:00.000+0200", + ) @classmethod def validate_oai_schema(cls, xml): @@ -96,7 +105,9 @@ def validate_oai_schema(cls, xml): @override_settings(URL_CONFIG="domain") @freeze_time(FROZEN_DATETIME_2012) def test_list_records_dc(self): - expected = LIST_RECORDS_DATA_DC + expected = LIST_RECORDS_DATA_DC.format( + article_id=self.article.pk, + ) response = self.client.get( reverse("OAI_list_records"), SERVER_NAME="testserver" ) @@ -105,7 +116,9 @@ def test_list_records_dc(self): @override_settings(URL_CONFIG="domain") @freeze_time(FROZEN_DATETIME_2012) def test_list_records_jats(self): - expected = LIST_RECORDS_DATA_JATS + expected = LIST_RECORDS_DATA_JATS.format( + article_id=self.article.pk, + ) path = reverse("OAI_list_records") query_params = dict( verb="ListRecords", @@ -118,13 +131,15 @@ def test_list_records_jats(self): @override_settings(URL_CONFIG="domain") @freeze_time(FROZEN_DATETIME_2012) def test_get_record_dc(self): - expected = GET_RECORD_DATA_DC + expected = GET_RECORD_DATA_DC.format( + article_id=self.article.pk, + ) path = reverse("OAI_list_records") query_params = dict( verb="GetRecord", metadataPrefix="oai_dc", - identifier="oai:TST:id:1", + identifier=f"oai:TST:id:{self.article.pk}", ) query_string = urlencode(query_params) @@ -134,8 +149,6 @@ def test_get_record_dc(self): @override_settings(URL_CONFIG="domain") @freeze_time(FROZEN_DATETIME_1976) def test_get_records_until(self): - expected = GET_RECORD_DATA_UNTIL - path = reverse("OAI_list_records") query_params = dict( verb="ListRecords", @@ -145,13 +158,18 @@ def test_get_records_until(self): query_string = urlencode(query_params) # Create article that will be returned - helpers.create_submission( + returned_article = helpers.create_submission( + title="Returned article", journal_id=self.journal.pk, stage=sm_models.STAGE_PUBLISHED, date_published="1975-01-01T17:00:00.000+0200", authors=[self.author], ) + expected = GET_RECORD_DATA_UNTIL.format( + article_id=returned_article.pk, + ) + # Create article that will not be returned helpers.create_submission( journal_id=self.journal.pk, @@ -165,7 +183,9 @@ def test_get_records_until(self): @override_settings(URL_CONFIG="domain") @freeze_time(FROZEN_DATETIME_2012) def test_get_record_jats(self): - expected = GET_RECORD_DATA_JATS + expected = GET_RECORD_DATA_JATS.format( + article_id=self.article.pk, + ) # Add a non correspondence author author_2 = helpers.create_author(self.journal, email="no@email.com") author_2.snapshot_as_author(self.article) @@ -180,7 +200,7 @@ def test_get_record_jats(self): query_params = dict( verb="GetRecord", metadataPrefix="jats", - identifier="oai:TST:id:1", + identifier=f"oai:TST:id:{self.article.pk}", ) query_string = urlencode(query_params) @@ -190,7 +210,9 @@ def test_get_record_jats(self): @override_settings(URL_CONFIG="domain") @freeze_time(FROZEN_DATETIME_2012) def test_list_identifiers_jats(self): - expected = LIST_IDENTIFIERS_JATS + expected = LIST_IDENTIFIERS_JATS.format( + article_id=self.article.pk, + ) path = reverse("OAI_list_records") query_params = dict( @@ -297,3 +319,33 @@ def test_oai_resumption_token_encode(self): expected_encoded in unquote_plus(response.context["resumption_token"]), "Query parameter has not been encoded into resumption_token", ) + + @override_settings(URL_CONFIG="domain") + @freeze_time(FROZEN_DATETIME_2012) + def test_list_records_jats_excludes_hidden_journal(self): + path = self.press.site_url(reverse("OAI_list_records")) + query_params = dict( + verb="ListRecords", + metadataPrefix="jats", + ) + query_string = urlencode(query_params) + response = self.client.get( + f"{path}?{query_string}", + SERVER_NAME=self.press.domain, + ) + self.assertNotIn(self.hidden_article, response.context["object_list"]) + + @override_settings(URL_CONFIG="domain") + @freeze_time(FROZEN_DATETIME_2012) + def test_list_records_jats_works_at_journal_level_even_if_hidden_from_press(self): + path = self.hidden_journal.site_url(reverse("OAI_list_records")) + query_params = dict( + verb="ListRecords", + metadataPrefix="jats", + ) + query_string = urlencode(query_params) + response = self.client.get( + f"{path}?{query_string}", + SERVER_NAME=self.hidden_journal.domain, + ) + self.assertIn(self.hidden_article, response.context["object_list"]) diff --git a/src/api/tests/test_oai_data/get_record_data_dc.xml b/src/api/tests/test_oai_data/get_record_data_dc.xml index bd717a3b5f..993b6a60ed 100644 --- a/src/api/tests/test_oai_data/get_record_data_dc.xml +++ b/src/api/tests/test_oai_data/get_record_data_dc.xml @@ -10,7 +10,7 @@
- oai:TST:id:1 + oai:TST:id:{article_id} 1986-07-12T15:00:00Z
@@ -30,8 +30,8 @@ 1 Press Journal One - http://testserver/article/id/1/ - http://testserver/article/id/1/ + http://testserver/article/id/{article_id}/ + http://testserver/article/id/{article_id}/ 0000-0000 1 diff --git a/src/api/tests/test_oai_data/get_record_data_jats.xml b/src/api/tests/test_oai_data/get_record_data_jats.xml index 5f43b7e59b..bd1dc776ae 100644 --- a/src/api/tests/test_oai_data/get_record_data_jats.xml +++ b/src/api/tests/test_oai_data/get_record_data_jats.xml @@ -9,7 +9,7 @@
- oai:TST:id:1 + oai:TST:id:{article_id} 1986-07-12T15:00:00Z
@@ -34,7 +34,7 @@ - 1 + {article_id} Article @@ -96,14 +96,14 @@ 1 1 Test Issue from Utils Testing Helpers - 1 + {article_id} Copyright: © 1986 The Author(s) 1986

A Test article abstract

diff --git a/src/api/tests/test_oai_data/get_record_data_until.xml b/src/api/tests/test_oai_data/get_record_data_until.xml index a3a9f7af2d..a434e34f3c 100644 --- a/src/api/tests/test_oai_data/get_record_data_until.xml +++ b/src/api/tests/test_oai_data/get_record_data_until.xml @@ -9,7 +9,7 @@
- oai:TST:id:2 + oai:TST:id:{article_id} 1975-01-01T15:00:00Z
@@ -19,16 +19,16 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd" > - A Test Article - A Test Article + Returned article + Returned article User, Author A A Test article abstract 1975-01-01T15:00:00Z info:eu-repo/semantics/article Press Journal One - http://testserver/article/id/2/ - http://testserver/article/id/2/ + http://testserver/article/id/{article_id}/ + http://testserver/article/id/{article_id}/ 0000-0000 1 diff --git a/src/api/tests/test_oai_data/list_identifiers_jats.xml b/src/api/tests/test_oai_data/list_identifiers_jats.xml index b90b9c7948..e39b6f5468 100644 --- a/src/api/tests/test_oai_data/list_identifiers_jats.xml +++ b/src/api/tests/test_oai_data/list_identifiers_jats.xml @@ -8,7 +8,7 @@ http://testserver/api/oai/
- oai:TST:id:1 + oai:TST:id:{article_id} 1986-07-12T15:00:00Z
diff --git a/src/api/tests/test_oai_data/list_records_data_dc.xml b/src/api/tests/test_oai_data/list_records_data_dc.xml index 037397ee9b..d82c8cb327 100644 --- a/src/api/tests/test_oai_data/list_records_data_dc.xml +++ b/src/api/tests/test_oai_data/list_records_data_dc.xml @@ -9,7 +9,7 @@
- oai:TST:id:1 + oai:TST:id:{article_id} 1986-07-12T15:00:00Z
@@ -29,8 +29,8 @@ 1 Press Journal One - http://testserver/article/id/1/ - http://testserver/article/id/1/ + http://testserver/article/id/{article_id}/ + http://testserver/article/id/{article_id}/ 0000-0000 1 diff --git a/src/api/tests/test_oai_data/list_records_data_jats.xml b/src/api/tests/test_oai_data/list_records_data_jats.xml index 42d816001f..0d03a945ec 100644 --- a/src/api/tests/test_oai_data/list_records_data_jats.xml +++ b/src/api/tests/test_oai_data/list_records_data_jats.xml @@ -9,7 +9,7 @@
- oai:TST:id:1 + oai:TST:id:{article_id} 1986-07-12T15:00:00Z
@@ -34,7 +34,7 @@ - 1 + {article_id} Article @@ -73,14 +73,14 @@ 1 1 Test Issue from Utils Testing Helpers - 1 + {article_id} Copyright: © 1986 The Author(s) 1986

A Test article abstract

diff --git a/src/api/views.py b/src/api/views.py index b772f76f52..460b4791bd 100755 --- a/src/api/views.py +++ b/src/api/views.py @@ -3,6 +3,7 @@ import io import json import re +import warnings from django.http import HttpResponse from django.shortcuts import render @@ -76,35 +77,45 @@ class JournalViewSet(viewsets.ModelViewSet): API Endpoint for journals. """ - from journal import models as journal_models - - queryset = journal_models.Journal.objects.filter(hide_from_press=False) serializer_class = serializers.JournalSerializer http_method_names = ["get"] + def get_queryset(self): + if self.request.journal: + queryset = journal_models.Journal.objects.filter( + pk=self.request.journal.pk, + ) + else: + queryset = journal_models.Journal.objects.filter( + hide_from_press=False, + ) + return queryset + class IssueViewSet(viewsets.ModelViewSet): """ - API Endpoint for journals. + API endpoint for issues. """ serializer_class = serializers.IssueSerializer http_method_names = ["get"] def get_queryset(self): - from journal import models as journal_models - if self.request.journal: - queryset = journal_models.Issue.objects.filter(journal=self.request.journal) + queryset = journal_models.Issue.objects.filter( + journal=self.request.journal, + ) else: - queryset = journal_models.Issue.objects.all() + queryset = journal_models.Issue.objects.filter( + journal__hide_from_press=False, + ) return queryset class LicenceViewSet(viewsets.ModelViewSet): """ - API Endpoint for journals. + API Endpoint for licenses. """ serializer_class = serializers.LicenceSerializer @@ -117,7 +128,7 @@ def get_queryset(self): ) else: queryset = submission_models.Licence.objects.filter( - journal=self.request.press + press=self.request.press ) return queryset @@ -131,7 +142,7 @@ class KeywordsViewSet(viewsets.ModelViewSet): class ArticleViewSet(viewsets.ModelViewSet): """ - API Endpoint for journals. + API endpoint for articles. """ serializer_class = serializers.ArticleSerializer @@ -148,6 +159,7 @@ def get_queryset(self): queryset = submission_models.Article.objects.filter( stage=submission_models.STAGE_PUBLISHED, date_published__lte=timezone.now(), + journal__hide_from_press=False, ) return queryset @@ -170,6 +182,7 @@ def get_queryset(self): def oai(request): + warnings.warn("This view is deprecated. OAI views are in api/oai/views.py") articles = submission_models.Article.objects.filter( stage=submission_models.STAGE_PUBLISHED ) @@ -216,9 +229,15 @@ def kbart(request, tsv=True): has_header = False writer = None - for journal in journal_models.Journal.objects.filter( - is_remote=False, hide_from_press=False - ): + journals = journal_models.Journal.objects.filter( + is_remote=False, + ) + + if request.journal: + journals = journals.filter(pk=request.journal.pk) + else: + journals = journals.filter(hide_from_press=False) + for journal in journals: kbart_embargo = journal.get_setting("kbart", "embargo_period") # Note that we here use an OrderedDict. This is important as the # field headers are generated below at the late init of the TSV or diff --git a/src/comms/tests.py b/src/comms/tests.py index 9ac6d46fed..1050ca5d5a 100644 --- a/src/comms/tests.py +++ b/src/comms/tests.py @@ -1,4 +1,4 @@ -from django.test import TestCase +from django.test import TestCase, override_settings from django.urls import reverse from django.contrib.contenttypes.models import ContentType from django.utils import timezone @@ -13,18 +13,19 @@ class NewsViewsTest(TestCase): @classmethod def setUpTestData(cls): cls.press = helpers.create_press() - cls.journal_one, cls.journal_two = helpers.create_journals() + cls.journal_one, cls.hidden_journal = helpers.create_journals() + cls.hidden_journal.hide_from_press = True + cls.hidden_journal.save() cls.content_type = ContentType.objects.get_for_model(cls.journal_one) cls.editor = helpers.create_editor(cls.journal_one) cls.author = helpers.create_author(cls.journal_one) - - cls.news_item = models.NewsItem.objects.create( + cls.news_item = helpers.create_news_item( content_type=cls.content_type, object_id=cls.journal_one.pk, posted_by=cls.editor, title="Test News", - body="Some content", # Corrected from 'content' to 'body' + body="Some content", ) cls.news_url = reverse("core_manager_news") @@ -33,6 +34,13 @@ def setUpTestData(cls): kwargs={"news_pk": cls.news_item.pk}, ) cls.create_url = reverse("core_manager_create_news") + cls.hidden_news_item = helpers.create_news_item( + content_type=cls.content_type, + object_id=cls.hidden_journal.pk, + posted_by=cls.editor, + title="Hidden news item", + body="Secrets known only to those who can find the journal website", + ) def setUp(self): self.client.force_login(self.editor) @@ -116,3 +124,12 @@ def test_manage_news_delete_image(self): core_models.File.objects.filter(pk=image_file.pk).exists(), msg="File was not deleted as expected.", ) + + @override_settings(URL_CONFIG="domain") + def test_presswide_list_excludes_journals_hidden_from_press(self): + url = reverse("core_news_list_presswide", kwargs={"presswide": "all"}) + response = self.client.get( + url, + SERVER_NAME=self.press.domain, + ) + self.assertNotIn(self.hidden_news_item, response.context["news_items"]) diff --git a/src/comms/views.py b/src/comms/views.py index 1c272e869d..89023d4656 100755 --- a/src/comms/views.py +++ b/src/comms/views.py @@ -9,6 +9,7 @@ from comms import models, forms, logic from core import models as core_models +from journal import models as journal_models from security.decorators import editor_user_required, file_user_required, has_request from utils.decorators import GET_language_override from utils.shared import language_override_redirect @@ -208,8 +209,19 @@ def news_list(request, tag=None, presswide=False): """ news_objects = models.NewsItem.active_objects.all() + all_tags = models.Tag.objects.all() - if not presswide or request.model_content_type.model != "press": + if presswide or request.model_content_type.model == "press": + press_visible_journal_pks = [ + journal.pk + for journal in journal_models.Journal.objects.filter( + hide_from_press=False, + ) + ] + news_objects = news_objects.filter( + object_id__in=press_visible_journal_pks, + ) + else: news_objects = news_objects.filter( content_type=request.model_content_type, object_id=request.site_type.id, @@ -222,6 +234,14 @@ def news_list(request, tag=None, presswide=False): ) tag = get_object_or_404(models.Tag, text=unquoted_tag) + all_tags = ( + models.Tag.objects.filter( + tags__in=news_objects, + ) + .annotate(Count("tags")) + .order_by("-tags__count", "text") + ) + paginator = Paginator(news_objects, 12) page = request.GET.get("page", 1) @@ -232,12 +252,6 @@ def news_list(request, tag=None, presswide=False): except EmptyPage: news_items = paginator.page(paginator.num_pages) - all_tags = ( - models.Tag.objects.all() - .annotate(Count("tags")) - .order_by("-tags__count", "text") - ) - if not request.journal: template = "press/core/news/index.html" else: 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/core/homepage_elements/carousel/tests.py b/src/core/homepage_elements/carousel/tests.py index 157efd9878..035d64801c 100644 --- a/src/core/homepage_elements/carousel/tests.py +++ b/src/core/homepage_elements/carousel/tests.py @@ -12,7 +12,7 @@ from comms import models as comms_models from journal import models as journal_models from submission import models as sm_models -from utils.testing.helpers import create_journals +from utils.testing.helpers import create_journals, create_press FROZEN_DATETIME_20180628 = timezone.make_aware( timezone.datetime(2018, 6, 28, 8, 15, 27, 243860) @@ -26,25 +26,34 @@ class TestCarousel(TestCase): @classmethod def setUpTestData(cls): - cls.journal_one, cls.journal_2 = create_journals() + cls.press = create_press() + cls.journal_one, cls.hidden_journal = create_journals() + cls.hidden_journal.hide_from_press = True + cls.hidden_journal.save() cls.issue = journal_models.Issue.objects.create(journal=cls.journal_one) cls.news_item = comms_models.NewsItem.objects.create( posted=FROZEN_DATETIME_20180628, ) cls.news_item.content_type = ContentType.objects.get_for_model(cls.journal_one) cls.news_item.object_id = cls.journal_one.id - cls.article = sm_models.Article.objects.create( + cls.article_one = sm_models.Article.objects.create( journal=cls.journal_one, stage=sm_models.STAGE_PUBLISHED, date_published=FROZEN_DATETIME_20180628, title="Carousel Article One", ) - cls.article = sm_models.Article.objects.create( + cls.article_two = sm_models.Article.objects.create( journal=cls.journal_one, stage=sm_models.STAGE_PUBLISHED, date_published=FROZEN_DATETIME_20180629, title="Carousel Article Two", ) + cls.hidden_article = sm_models.Article.objects.create( + journal=cls.hidden_journal, + stage=sm_models.STAGE_PUBLISHED, + date_published=FROZEN_DATETIME_20180629, + title="Hidden article", + ) def test_carousel(self): carousel = models.Carousel.objects.create() @@ -54,7 +63,7 @@ def test_latest_articles(self): carousel = models.Carousel.objects.create(latest_articles=True) self.journal_one.carousel = carousel self.journal_one.save() - expected = self.article + expected = self.article_two result = self.journal_one.carousel.get_items()[0] self.assertEqual(expected, result) @@ -69,11 +78,26 @@ def test_latest_articles_limit(self): self.assertEqual(1, len(self.journal_one.carousel.get_items())) + def test_press_latest_articles(self): + carousel = models.Carousel.objects.create(latest_articles=True) + self.press.carousel = carousel + self.press.save() + carousel_items = [item.pk for item in self.press.carousel.get_items()] + expected = [self.article_two.pk, self.article_one.pk] + self.assertEqual(expected, carousel_items) + + def test_press_latest_articles_excludes_hidden(self): + carousel = models.Carousel.objects.create(latest_articles=True) + self.press.carousel = carousel + self.press.save() + carousel_items = [item.pk for item in self.press.carousel.get_items()] + self.assertNotIn(self.hidden_article.pk, carousel_items) + def test_selected_articles(self): carousel = models.Carousel.objects.create(latest_articles=False) article = sm_models.Article.objects.create( stage=sm_models.STAGE_PUBLISHED, - date_published=self.article.date_published + relativedelta(years=1), + date_published=self.article_two.date_published + relativedelta(years=1), ) carousel.articles.add(article) self.journal_one.carousel = carousel @@ -88,7 +112,7 @@ def test_latest_news(self): carousel = models.Carousel.objects.create(latest_articles=True) self.journal_one.carousel = carousel self.journal_one.save() - expected = self.article + expected = self.article_two result = self.journal_one.carousel.get_items()[0] self.assertEqual(expected, result) diff --git a/src/core/homepage_elements/journals/hooks.py b/src/core/homepage_elements/journals/hooks.py index f03c51b4bd..7af880b524 100755 --- a/src/core/homepage_elements/journals/hooks.py +++ b/src/core/homepage_elements/journals/hooks.py @@ -16,7 +16,7 @@ def get_random_journals(): sample_size = min(6, journals.count()) - return random.sample(set(journals), sample_size) + return random.sample(list(journals), sample_size) def yield_homepage_element_context(request, homepage_elements): diff --git a/src/core/homepage_elements/journals_and_html/hooks.py b/src/core/homepage_elements/journals_and_html/hooks.py index 44e81eaccd..eced320995 100755 --- a/src/core/homepage_elements/journals_and_html/hooks.py +++ b/src/core/homepage_elements/journals_and_html/hooks.py @@ -20,7 +20,7 @@ def get_random_journals(): sample_size = min(6, journals.count()) - return random.sample(set(journals), sample_size) + return random.sample(list(journals), sample_size) def yield_homepage_element_context(request, homepage_elements): diff --git a/src/core/logic.py b/src/core/logic.py index c37a42bf7f..d872426b2a 100755 --- a/src/core/logic.py +++ b/src/core/logic.py @@ -926,6 +926,7 @@ def latest_articles(carousel, object_type): carousel_objects = submission_models.Article.objects.filter( date_published__lte=timezone.now(), stage=submission_models.STAGE_PUBLISHED, + journal__hide_from_press=False, ).order_by("-date_published") return carousel_objects diff --git a/src/core/models.py b/src/core/models.py index e02a2ab8b3..31ffb94415 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -19,6 +19,7 @@ import zipfile from bs4 import BeautifulSoup +from django.apps import apps from django.conf import settings from django.contrib.auth.models import ( AbstractBaseUser, @@ -928,8 +929,13 @@ def published_articles(self): ) request = utils_logic.get_current_request() if request and request.journal: - articles.filter(journal=request.journal) - + articles = articles.filter(journal=request.journal) + else: + articles = articles.filter(journal__hide_from_press=False) + Journal = apps.get_model("journal.Journal") + articles = articles.exclude( + journal__status=Journal.PublishingStatus.TEST, + ) return articles def preprint_subjects(self): diff --git a/src/core/tests/test_models.py b/src/core/tests/test_models.py index af08dd985f..c8f840db58 100644 --- a/src/core/tests/test_models.py +++ b/src/core/tests/test_models.py @@ -1,6 +1,8 @@ from datetime import date, timedelta +from uuid import uuid4 from unittest.mock import patch +from django.conf import settings from django.core.files.uploadedfile import SimpleUploadedFile from django.db import IntegrityError from django.db.transaction import TransactionManagementError @@ -30,8 +32,47 @@ class TestAccount(TestCase): @classmethod def setUpTestData(cls): cls.press = helpers.create_press() - cls.journal_one, cls.journal_two = helpers.create_journals() + cls.journal_one, cls.hidden_journal = helpers.create_journals() + cls.hidden_journal.hide_from_press = True + cls.hidden_journal.save() + cls.test_journal = helpers.create_journal_with_test_status() + cls.author_user = helpers.create_user( + "{}{}".format(uuid4(), settings.DUMMY_EMAIL_DOMAIN), + roles=["author"], + journal=cls.journal_one, + ) + models.AccountRole.objects.get_or_create( + user=cls.author_user, + role=models.Role.objects.get(slug="author"), + journal=cls.hidden_journal, + ) + models.AccountRole.objects.get_or_create( + user=cls.author_user, + role=models.Role.objects.get(slug="author"), + journal=cls.test_journal, + ) cls.article_one = helpers.create_article(cls.journal_one) + cls.published_article = helpers.create_article( + cls.journal_one, + stage=submission_models.STAGE_PUBLISHED, + date_published=FROZEN_DATETIME_20210101, + title="Published article", + ) + cls.author_user.snapshot_as_author(cls.published_article) + cls.hidden_article = helpers.create_article( + cls.hidden_journal, + stage=submission_models.STAGE_PUBLISHED, + date_published=FROZEN_DATETIME_20210101, + title="Article published in hidden journal", + ) + cls.author_user.snapshot_as_author(cls.hidden_article) + cls.test_article = helpers.create_article( + cls.test_journal, + stage=submission_models.STAGE_PUBLISHED, + date_published=FROZEN_DATETIME_20210101, + title="Article published in test journal", + ) + cls.author_user.snapshot_as_author(cls.test_article) def test_creation(self): data = { @@ -271,6 +312,13 @@ def test_credits(self): "Conceptualization", ) + @patch("utils.logic.get_current_request") + def test_published_articles(self, get_request): + get_request.return_value = helpers.Request(press=self.press) + expected = set([self.published_article]) + published_articles = set(self.author_user.published_articles()) + self.assertSetEqual(expected, published_articles) + class TestSVGImageFormField(TestCase): def test_upload_svg_to_svg_image_form_field(self): diff --git a/src/identifiers/logic.py b/src/identifiers/logic.py index 646ec67b6c..d9f03e181e 100755 --- a/src/identifiers/logic.py +++ b/src/identifiers/logic.py @@ -27,7 +27,7 @@ from utils import setting_handler, render_template from crossref.restful import Depositor from identifiers import models -from submission import models as submission_models +from journal import models as journal_models logger = get_logger(__name__) @@ -50,6 +50,9 @@ def register_batch_of_crossref_dois(articles, **kwargs): use_crossref, test_mode, missing_settings = check_crossref_settings(journal) + if journal.status == journal_models.Journal.PublishingStatus.TEST: + test_mode = True + if use_crossref and not missing_settings: mode = "test" if test_mode else "live" desc = f"DOI registration running in f{mode} mode" diff --git a/src/identifiers/tests/test_logic.py b/src/identifiers/tests/test_logic.py index f4ad94be4f..d1df5e0b4c 100644 --- a/src/identifiers/tests/test_logic.py +++ b/src/identifiers/tests/test_logic.py @@ -29,6 +29,7 @@ def setUpTestData(cls): cls.press = helpers.create_press() cls.press.save() cls.journal_one, cls.journal_two = helpers.create_journals() + cls.test_journal = helpers.create_journal_with_test_status() # Configure settings for journal in [cls.journal_one, cls.journal_two]: @@ -143,6 +144,9 @@ def setUpTestData(cls): "schemas", ) + # Article in test journal + cls.test_article = helpers.create_article(cls.test_journal) + def test_create_crossref_doi_batch_context(self): self.maxDiff = None expected_data = {} @@ -485,3 +489,17 @@ def test_check_crossref_settings(self): missing_settings, ["crossref_prefix", "crossref_username", "crossref_password"], ) + + @mock.patch("identifiers.logic.get_dois_for_articles") + @mock.patch("identifiers.logic.check_crossref_settings") + @mock.patch("identifiers.logic.send_crossref_deposit") + def test_journals_with_test_status_use_crossref_test_mode( + self, + send_deposit, + check_settings, + get_dois, + ): + check_settings.return_value = True, False, [] + get_dois.return_value = [] + logic.register_batch_of_crossref_dois([self.test_article]) + send_deposit.assert_called_with(True, [], self.test_journal) diff --git a/src/journal/admin.py b/src/journal/admin.py index 4c14d96baf..37c2c4fe97 100755 --- a/src/journal/admin.py +++ b/src/journal/admin.py @@ -85,12 +85,14 @@ class JournalAdmin(admin.ModelAdmin): list_display = ( "name", "code", + "sequence", "domain", + "status", + "hide_from_press", "is_remote", "is_conference", - "hide_from_press", ) - list_filter = ("is_remote", "is_conference", "hide_from_press") + list_filter = ("status", "hide_from_press", "is_remote", "is_conference") raw_id_fields = ( "carousel", "current_issue", diff --git a/src/journal/management/commands/galley_healthcheck.py b/src/journal/management/commands/galley_healthcheck.py index c30c6b0154..6f5e740e41 100644 --- a/src/journal/management/commands/galley_healthcheck.py +++ b/src/journal/management/commands/galley_healthcheck.py @@ -30,6 +30,9 @@ def handle(self, *args, **options): journals = journal_models.Journal.objects.all() if journal_codes: journals = journals.filter(code__in=journal_codes) + journals = journals.exclude( + status=journal_models.Journal.PublishingStatus.TEST, + ) for journal in journals: articles = models.Article.objects.filter( 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..6dbb73f2ec 100644 --- a/src/journal/models.py +++ b/src/journal/models.py @@ -32,7 +32,8 @@ 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 modeltranslation.utils import build_localized_fieldname from core import ( files, @@ -97,6 +98,97 @@ def issue_large_image_path(instance, filename): return os.path.join(path, filename) +class JournalManager(models.Manager): + def _apply_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_ordering(self, journals): + press = press_models.Press.objects.all().first() + if press.order_journals_az: + return self._apply_ordering_az(journals) + else: + # Journals will already have been ordered according to Meta.ordering + return journals + + @property + def public_journals(self): + """ + 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. + """ + return self.get_queryset().filter( + hide_from_press=False, + is_conference=False, + ) + + @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. + """ + return self._apply_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. + """ + return self._apply_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. + """ + return self._apply_ordering( + self.public_journals.filter( + status=Journal.PublishingStatus.COMING_SOON, + ) + ) + + class Journal(AbstractSiteModel): AUTH_SUCCESS_URL = "core_dashboard" @@ -267,6 +359,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 +403,14 @@ class Journal(AbstractSiteModel): disable_front_end = models.BooleanField(default=False) + objects = JournalManager() + + 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/journal/tests/test_journal_frontend.py b/src/journal/tests/test_journal_frontend.py index 2fc6448704..6b94cbe600 100644 --- a/src/journal/tests/test_journal_frontend.py +++ b/src/journal/tests/test_journal_frontend.py @@ -237,7 +237,11 @@ def test_article_page(self): self.article_title, ) + @override_settings(ENABLE_FULL_TEXT_SEARCH=False) def test_search_includes_article(self): + """ + This test expects the logic in journal.views.old_search. + """ url = "{}?article_search=Test&sort=relevance".format( reverse( "search", @@ -250,7 +254,11 @@ def test_search_includes_article(self): self.article_title, ) + @override_settings(ENABLE_FULL_TEXT_SEARCH=False) def test_search_excludes_artucle(self): + """ + This test expects the logic in journal.views.old_search. + """ url = "{}?article_search=Janeway&sort=relevance".format( reverse( "search", diff --git a/src/journal/views.py b/src/journal/views.py index 32109a44e9..0c74bb7df5 100755 --- a/src/journal/views.py +++ b/src/journal/views.py @@ -2956,7 +2956,8 @@ def get_queryset(self, params_querydict=None): class PublishedArticlesListView(FacetedArticlesListView): """ A list of published articles that can be searched, - sorted, and filtered + sorted, and filtered. + Not for use at the press level. """ template_name = "journal/article_list.html" diff --git a/src/metrics/logic.py b/src/metrics/logic.py index 89e7e90978..1d9e168201 100755 --- a/src/metrics/logic.py +++ b/src/metrics/logic.py @@ -40,7 +40,11 @@ def get_press_totals(start_date, end_date, report_months, compat=False, do_yop=F for date in report_months: press_months["{0}-{1}".format(date.strftime("%b"), date.year)] = "" - for journal_object in journal_models.Journal.objects.all(): + for journal_object in journal_models.Journal.objects.filter( + hide_from_press=False, + ).exclude( + status=journal_models.Journal.PublishingStatus.TEST, + ): journal = {} journal["journal"] = journal_object 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/logic.py b/src/press/logic.py index 91f9efa665..a6f7432a64 100755 --- a/src/press/logic.py +++ b/src/press/logic.py @@ -3,11 +3,13 @@ __license__ = "AGPL v3" __maintainer__ = "Birkbeck Centre for Technology and Publishing" +import warnings from submission import models as submission_models def get_carousel_items(request): + warnings.warn("This method is deprecated.") if request.press.carousel_type == "articles": carousel_objects = submission_models.Article.objects.all().order_by( "-date_published" 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..9a7735b8c6 100755 --- a/src/press/models.py +++ b/src/press/models.py @@ -7,12 +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 modeltranslation.utils import build_localized_fieldname +from django.utils import timezone from django.apps import apps from core import models as core_models @@ -184,6 +185,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 +207,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 Journal.objects._apply_ordering(journals) @property def active_news_items(self): @@ -251,7 +233,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 +364,16 @@ 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 + Journal = apps.get_model("journal", "Journal") + return Journal.objects.public_journals.count() > 0 @cache(600) def live_repositories(self): @@ -442,12 +429,16 @@ def code(self): return "press" @property - def public_journals(self): - Journal = apps.get_model("journal.Journal") - return Journal.objects.filter( - hide_from_press=False, - is_conference=False, - ).order_by("sequence") + 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 Journal.objects._apply_ordering_az(Journal.objects.filter()) @property def published_articles(self): @@ -455,6 +446,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..6248a904f2 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": journal_models.Journal.objects.public_active_journals, + "archived_journals": journal_models.Journal.objects.public_archived_journals, + "coming_soon_journals": journal_models.Journal.objects.public_coming_soon_journals, + "journals": journal_models.Journal.objects.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/rss/tests/__init__.py b/src/rss/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/rss/tests/test_views.py b/src/rss/tests/test_views.py new file mode 100644 index 0000000000..5714d12aaf --- /dev/null +++ b/src/rss/tests/test_views.py @@ -0,0 +1,47 @@ +from unittest.mock import patch, Mock + +from django.shortcuts import reverse +from django.test import TestCase +from django.utils import timezone +from freezegun import freeze_time + +from submission import models as submission_models +from rss import views as rss_views +from utils.testing import helpers + +FROZEN_DATETIME_2012 = timezone.make_aware(timezone.datetime(2012, 1, 14, 0, 0, 0)) + + +class TestRSS(TestCase): + @classmethod + def setUpTestData(cls): + cls.press = helpers.create_press() + cls.journal_one, cls.hidden_journal = helpers.create_journals() + cls.hidden_journal.hide_from_press = True + cls.hidden_journal.save() + cls.published_article = helpers.create_article( + cls.journal_one, + with_author=True, + stage=submission_models.STAGE_PUBLISHED, + date_published=FROZEN_DATETIME_2012, + title="Published article", + ) + cls.hidden_article = helpers.create_article( + cls.hidden_journal, + with_author=True, + stage=submission_models.STAGE_PUBLISHED, + date_published=FROZEN_DATETIME_2012, + title="Article published in hidden journal", + ) + + def test_journal_articles_feed(self): + feed = rss_views.LatestArticlesFeed() + expected = set([self.published_article]) + result = set(item for item in feed.items(self.journal_one)) + self.assertSetEqual(expected, result) + + def test_press_articles_feed(self): + feed = rss_views.LatestArticlesFeed() + expected = set([self.published_article]) + result = set(item for item in feed.items(self.press)) + self.assertSetEqual(expected, result) diff --git a/src/rss/views.py b/src/rss/views.py index 309e10c47e..75458900bc 100755 --- a/src/rss/views.py +++ b/src/rss/views.py @@ -11,6 +11,7 @@ from comms import models as comms_models from core.templatetags.truncate import truncatesmart +from journal import models as journal_models from submission import models as submission_models from repository import models as repo_models @@ -72,15 +73,16 @@ def description(self, obj): def items(self, obj): try: - return submission_models.Article.objects.filter( - date_published__lte=timezone.now(), journal=obj - ).order_by("-date_published")[:10] + articles = submission_models.Article.objects.filter( + date_published__lte=timezone.now(), + journal=obj, + ) except ValueError: - return submission_models.Article.objects.filter( + articles = submission_models.Article.objects.filter( date_published__lte=timezone.now(), - journal__press=obj, journal__hide_from_press=False, - ).order_by("-date_published")[:10] + ) + return articles.order_by("-date_published")[:10] def item_title(self, item): return striptags(item.title) 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:
    - {% for journal in request.press.journals_az %} + {% for journal in request.press.journals %}
  • {{ journal.name }} diff --git a/src/templates/admin/elements/forms/group_journal.html b/src/templates/admin/elements/forms/group_journal.html index ff59b01c0d..6b994d63c1 100644 --- a/src/templates/admin/elements/forms/group_journal.html +++ b/src/templates/admin/elements/forms/group_journal.html @@ -1,3 +1,5 @@ +{% load fqdn %} +

    General Journal Settings

    @@ -77,7 +79,8 @@

    Other

    {% include "admin/elements/forms/field.html" with field=edit_form.google_analytics_code %} {% include "admin/elements/forms/field.html" with field=edit_form.use_ga_four %} -

    You can use this setting to stop this journal being list on the Press' journals page.

    +

    You can control the visibility of the journal and its articles, issues, and news items here. Journals that are archived or coming soon will be listed separately on the press-level Journals page. For journals that are used for testing and training, use the test status, and always select Hide from press.

    + {% include "admin/elements/forms/field.html" with field=attr_form.status %} {% include "admin/elements/forms/field.html" with field=attr_form.hide_from_press %}

    If you want to display a special message on the login page you can add it here.

    diff --git a/src/templates/admin/press/edit_press.html b/src/templates/admin/press/edit_press.html index 5ab5dda3a4..a13e122f7f 100644 --- a/src/templates/admin/press/edit_press.html +++ b/src/templates/admin/press/edit_press.html @@ -48,6 +48,7 @@

    Repositories

    Journals

    {{ form.disable_journals|foundation }} + {{ form.order_journals_az|foundation }}

    Security

    diff --git a/src/templates/common/apis/OAI_base.xml b/src/templates/common/apis/OAI_base.xml index cc2382fca2..9a481e84f3 100644 --- a/src/templates/common/apis/OAI_base.xml +++ b/src/templates/common/apis/OAI_base.xml @@ -3,6 +3,14 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/ http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd"> {% now "Y-m-d\TH:i:s\Z" %} - {% if request.repository %}{% repository_url 'OAI_list_records' %}{% else %}{% journal_url 'OAI_list_records' %}{% endif %} + {% spaceless %} + {% if request.repository %} + {% repository_url 'OAI_list_records' %} + {% elif request.journal %} + {% journal_url 'OAI_list_records' %} + {% else %} + {% stateless_site_url request.press url_name='OAI_list_records' %} + {% endif %} + {% endspaceless %} {% block body %}{% endblock body %} diff --git a/src/themes/OLH/templates/press/elements/journal_link.html b/src/themes/OLH/templates/press/elements/journal_link.html new file mode 100644 index 0000000000..c42c908ed1 --- /dev/null +++ b/src/themes/OLH/templates/press/elements/journal_link.html @@ -0,0 +1,12 @@ +{% if journal.is_remote %} + + {{ journal.name }} + {% include "elements/icons/link_external.html" %} + +{% else %} + + {{ journal.name }} + +{% endif %} diff --git a/src/themes/OLH/templates/press/press_journal_set.html b/src/themes/OLH/templates/press/press_journal_set.html new file mode 100644 index 0000000000..3caad67337 --- /dev/null +++ b/src/themes/OLH/templates/press/press_journal_set.html @@ -0,0 +1,87 @@ +{% load settings i18n %} +{% load static %} + +{% for journal in journals %} +
    +
    +
    +
    +
    +
    +
    + {% if journal.is_remote %} + + {% else %} + + {% endif %} + {% if journal.name != + +
    +
    +
    + {% if level == "deep" %} +

    {% include "press/elements/journal_link.html" %}

    + {% else %} +

    {% include "press/elements/journal_link.html" %}

    + {% endif %} + {{ journal.name|lower }} + {{ journal.description_for_press|safe }} + {% with keywords=journal.keywords.all %} + {% if keywords %} +

    {% trans 'Disciplines' %}

    + + {% endif %} + {% endwith %} +
    +
    + {% if journal.is_remote %} + {% if not journal_settings.general.disable_journal_submission %} + + {% trans 'Submit' %} + {% include "elements/icons/link_external.html" %} + + {% endif %} + + {% trans 'View' %} + {% include "elements/icons/link_external.html" %} + + {% else %} + {% if not journal_settings.general.disable_journal_submission %} + {% trans 'Submit' %} + {% endif %} + {% trans 'View' %} + {% if journal.current_issue %} + {% trans 'Current Issue' %} + {% elif journal.serial_issues.count > 0 %} + {{ journal.issue_type_plural_name }} + {% endif %} + {% endif %} +
    +
    +
    +
    + +
    +
    +
    +
    +{% endfor %} diff --git a/src/themes/OLH/templates/press/press_journals.html b/src/themes/OLH/templates/press/press_journals.html index 2badcf6d6f..4d2ff8f9a9 100644 --- a/src/themes/OLH/templates/press/press_journals.html +++ b/src/themes/OLH/templates/press/press_journals.html @@ -14,101 +14,20 @@

    {{ press.name }} {% trans 'Journals' %}

    - {% for current_journal in journals %} - {% setting_var current_journal 'disable_journal_submission' as submission_disabled %} -
    -
    -
    -
    -
    -
    -
    - {% if current_journal.is_remote %} - - {% else %} - - {% endif %} - {% if current_journal.name != - -
    -
    -
    -

    - {% if current_journal.is_remote %} - - {{ current_journal.name }} - {% include "elements/icons/link_external.html" %} - - {% else %} - - {{ current_journal.name }} - - {% endif %} -

    - {{ current_journal.name|lower }} - {{ current_journal.description_for_press|safe }} - {% with keywords=current_journal.keywords.all %} - {% if keywords %} -

    {% trans 'Disciplines' %}

    - - {% endif %} - {% endwith %} -
    -
    - {% if current_journal.is_remote %} - {% if not submission_disabled %} - - {% trans 'Submit' %} - {% include "elements/icons/link_external.html" %} - - {% endif %} - - {% trans 'View' %} - {% include "elements/icons/link_external.html" %} - - {% else %} - {% if not submission_disabled %} - {% trans 'Submit' %} - {% endif %} - {% trans 'View' %} - {% if current_journal.current_issue %} - {% trans 'Current Issue' %} - {% elif current_journal.serial_issues.count > 0 %} - {{ current_journal.issue_type_plural_name }} - {% endif %} - {% endif %} -
    -
    -
    -
    - -
    -
    -
    -
    - - {% endfor %} + {% if coming_soon_journals.exists %} +

    {% trans "Coming Soon" %}

    + {% include "press/press_journal_set.html" with journals=coming_soon_journals level="deep" %} + {% endif %} + {% if archived_journals.exists or coming_soon_journals.exists %} +

    {% trans "Active" %}

    + {% include "press/press_journal_set.html" with journals=active_journals level="deep" %} + {% else %} + {% include "press/press_journal_set.html" with journals=active_journals level="shallow" %} + {% endif %} + {% if archived_journals.exists %} +

    {% trans "Archived" %}

    + {% include "press/press_journal_set.html" with journals=archived_journals level="deep" %} + {% endif %}
    diff --git a/src/themes/clean/templates/press/press_journal_set.html b/src/themes/clean/templates/press/press_journal_set.html new file mode 100644 index 0000000000..96aaef64b3 --- /dev/null +++ b/src/themes/clean/templates/press/press_journal_set.html @@ -0,0 +1,56 @@ +{% load static %} +{% load settings i18n %} + +
    + {% for journal in journals %} + {% setting_var journal 'disable_journal_submission' as submission_disabled %} +
    +
    + {% if journal.name != +
    +
    + {{ journal.name|lower }} + {% if level == "deep" %} +

    {{ journal.name }}

    + {% else %} +

    {{ journal.name }}

    + {% endif %} + {{ journal.description_for_press|safe }} +
    + {% if journal.is_remote %} + {% if not submission_disabled %} + + {% trans 'Submit' %} + {% include "elements/icons/link_external.html" %} + + {% endif %} + + {% trans 'View' %} + {% include "elements/icons/link_external.html" %} + + {% else %} + {% if not submission_disabled %} + {% trans 'Submit' %} + {% endif %} + {% trans 'View' %} + {% if journal.current_issue %} + {% trans 'Current Issue' %} + {% elif journal.serial_issues.count > 0 %} + {{ journal.issue_type_plural_name }} + {% endif %} + {% endif %} +
    +
    +
    + {% empty %} +

    {% trans 'No journals to list' %}.

    + {% endfor %} +
    diff --git a/src/themes/clean/templates/press/press_journals.html b/src/themes/clean/templates/press/press_journals.html index 126b046833..a0ec8290a9 100644 --- a/src/themes/clean/templates/press/press_journals.html +++ b/src/themes/clean/templates/press/press_journals.html @@ -9,55 +9,26 @@

    {% trans 'Journals' %}

    -
    - {% for current_journal in journals %} - {% setting_var current_journal 'disable_journal_submission' as submission_disabled %} -
    -
    - {% if current_journal.name != -
    -
    - {{ current_journal.name|lower }} -

    {{ current_journal.name }}

    - {{ current_journal.description_for_press|safe }} -
    - {% if current_journal.is_remote %} - {% if not submission_disabled %} - - {% trans 'Submit' %} - {% include "elements/icons/link_external.html" %} - - {% endif %} - - {% trans 'View' %} - {% include "elements/icons/link_external.html" %} - - {% else %} - {% if not submission_disabled %} - {% trans 'Submit' %} - {% endif %} - {% trans 'View' %} - {% if current_journal.current_issue %} - {% trans 'Current Issue' %} - {% elif current_journal.serial_issues.count > 0 %} - {{ current_journal.issue_type_plural_name }} - {% endif %} - {% endif %} -
    -
    -
    - {% empty %} -

    {% trans 'No journals to list' %}.

    - {% endfor %} -
    + {% if coming_soon_journals.exists %} +
    +

    {% trans "Coming Soon" %}

    +
    + {% include "press/press_journal_set.html" with journals=coming_soon_journals level="deep" %} + {% endif %} + {% if archived_journals.exists or coming_soon_journals.exists %} +
    +

    {% trans "Active" %}

    +
    + {% include "press/press_journal_set.html" with journals=active_journals level="deep" %} + {% else %} + {% include "press/press_journal_set.html" with journals=active_journals level="shallow" %} + {% endif %} + {% if archived_journals.exists %} +
    +

    {% trans "Archived" %}

    +
    + {% include "press/press_journal_set.html" with journals=archived_journals level="deep" %} + {% endif %}