+
+
+
+ The Hydra plugin is installed
+Use this form to enable the Hydra navbar hook. Linked articles will have jump links rendered in the left hand menu.
+ +diff --git a/events.py b/events.py new file mode 100644 index 0000000..81d8c27 --- /dev/null +++ b/events.py @@ -0,0 +1,50 @@ +from django.core.management import call_command + +from plugins.hydra import models, events + + +def distribute_articles(**kwargs): + print('We are distributing articles') + article = kwargs.get('article') + journal = article.journal + + # Try direct access via reverse relation + try: + linked_group = journal.linked_journals + except models.LinkedJournals.DoesNotExist: + try: + linked_group = models.LinkedJournals.objects.get(links=journal) + except models.LinkedJournals.DoesNotExist: + return + + # Build set of journals: primary + linked ones + group_journals = {linked_group.journal} + group_journals.update( + link.to_journal for link in + linked_group.journal_link.select_related("to_journal") + ) + + # Exclude the current journal + group_journals.discard(journal) + + results = [] + + for journal in group_journals: + try: + call_command( + "copy_articles", + journal.code, + article=article.pk, + target_lang="en", + ) + results.append( + { + "journal": journal.code, + "status": "success", + } + ) + except Exception as e: + print(e) + + + return group_journals \ No newline at end of file diff --git a/forms.py b/forms.py new file mode 100644 index 0000000..62f2ec3 --- /dev/null +++ b/forms.py @@ -0,0 +1,44 @@ +from django import forms + + +from utils import setting_handler, models +from plugins.hydra import plugin_settings + + +class HydraAdminForm(forms.Form): + hydra_enable_sidebar = forms.BooleanField(label="Enable Hydra Sidebar", required=False) + + def __init__(self, *args, journal=None, **kwargs): + """Initialize form with current plugin settings and apply help text.""" + super().__init__(*args, **kwargs) + self.journal = journal + self.plugin = models.Plugin.objects.get( + name=plugin_settings.SHORT_NAME, + ) + + # Initialize fields with settings values and help texts + hydra_enable_sidebar_setting = setting_handler.get_plugin_setting( + self.plugin, + 'hydra_enable_sidebar', + self.journal, + create=True, + pretty='Enable Hydra Sidebar', + types='boolean' + ) + self.fields[ + 'hydra_enable_sidebar' + ].initial = hydra_enable_sidebar_setting.processed_value + + def save(self): + """Save each setting in the cleaned data to the plugin settings.""" + for setting_name, setting_value in self.cleaned_data.items(): + if setting_value: + setting_value = 'On' + else: + setting_value = '' + setting_handler.save_plugin_setting( + plugin=self.plugin, + setting_name=setting_name, + value=setting_value, + journal=self.journal + ) diff --git a/hooks.py b/hooks.py index 282c3bb..ee4185d 100644 --- a/hooks.py +++ b/hooks.py @@ -1,13 +1,15 @@ -from itertools import groupby -from operator import attrgetter +from urllib.parse import urlparse from django.db.models import Q, Subquery -from django.apps import apps +from django.http import Http404 from django.template.loader import render_to_string from django.utils.translation import get_language_info -from plugins.hydra import models +from plugins.hydra import models, utils from utils.logger import get_logger +from submission import models as submission_models + +from utils import setting_handler logger = get_logger(__name__) @@ -36,7 +38,8 @@ def sidebar_article_links(context, article): def language_header_switcher(context): """ - Provides a language switcher for linked journal sites. + Provides a language switcher for journals linked by LinkedArticle records, + or (when not on an article page) switches to equivalent paths on linked journals. """ request = context["request"] @@ -47,64 +50,183 @@ def language_header_switcher(context): if not journal: return "" - # Find the linked group this journal belongs to - try: - linked_group = journal.linked_journals - except models.LinkedJournals.DoesNotExist: + links = [] + + # Normalise path + prefix = f"/{journal.code}/" + stripped_path = path[len(prefix):] if path.startswith(prefix) else path + normalised_path = stripped_path.lstrip("/") + + if not normalised_path: + corrected_path = "/" + elif normalised_path.startswith("issue/"): + corrected_path = "/issues/" + else: + corrected_path = f"/{normalised_path}" + + if article: + # Build set of transitive linked article PKs + visited = set() + to_visit = {article.pk} + + while to_visit: + current_ids = to_visit + to_visit = set() + + linked = models.LinkedArticle.objects.filter( + Q(from_article_id__in=current_ids) | Q(to_article_id__in=current_ids) + ).values("from_article_id", "to_article_id") + + for link in linked: + from_id = link["from_article_id"] + to_id = link["to_article_id"] + + if from_id not in visited: + to_visit.add(from_id) + if to_id not in visited: + to_visit.add(to_id) + + visited.update(current_ids) + + linked_articles = submission_models.Article.objects.filter( + pk__in=visited - {article.pk}, + ).select_related("journal") + + for linked_article in linked_articles: + linked_journal = linked_article.journal + + try: + linked_language = linked_journal.get_setting( + group_name="general", + setting_name="default_journal_language", + ) + except Exception: + continue + + links.append({ + "name_local": get_language_info(linked_language)["name_local"], + "url": linked_journal.site_url(path=corrected_path), + }) + + else: + # No article: find the LinkedJournals group try: - linked_group = models.LinkedJournals.objects.get(links=journal) + linked_set = journal.linked_journals except models.LinkedJournals.DoesNotExist: - return "" + try: + journal_link = models.JournalLink.objects.get(to_journal=journal) + linked_set = journal_link.parent + except models.JournalLink.DoesNotExist: + linked_set = None + + if linked_set: + for linked_journal in linked_set.links.all(): + if linked_journal == journal: + continue + + try: + linked_language = linked_journal.get_setting( + group_name="general", + setting_name="default_journal_language", + ) + except Exception: + continue + + links.append({ + "name_local": get_language_info(linked_language)["name_local"], + "url": linked_journal.site_url(path=corrected_path), + }) + + # Add parent journal if we're not it + if linked_set.journal != journal: + try: + parent_language = linked_set.journal.get_setting( + group_name="general", + setting_name="default_journal_language", + ) + links.append({ + "name_local": get_language_info(parent_language)["name_local"], + "url": linked_set.journal.site_url(path=corrected_path), + }) + except Exception: + pass - if not linked_group.journal_link.exists(): + if not links: return "" - # Strip the journal-specific path prefix - prefix = f"/{journal.code}/" - stripped_path = path[len(prefix):] if path.startswith(prefix) else path - corrected_path = "/issues/" if stripped_path.startswith("issue/") else f"/{stripped_path}" - - # Build the full set of group journals - group_journals = {linked_group.journal} - group_journals.update( - link.to_journal for link in linked_group.journal_link.select_related("to_journal") + return render_to_string( + "hydra/elements/language_links.html", + { + "language_links": links, + } ) + + + +def editor_nav_article_switcher(context): + """ + Adds backend links to linked articles. + """ + request = context["request"] + journal = getattr(request, "journal", None) + article = context.get("article") + + sidebar_enabled = setting_handler.get_setting( + "plugin:hydra", + "hydra_enable_sidebar", + journal=journal, + ).processed_value + + if not sidebar_enabled: + return "" + + if not article or not journal: + return "" + + related = article.linked_from.select_related( + "from_article__journal", + "to_article__journal", + ).all() + related |= article.linked_to.select_related( + "from_article__journal", + "to_article__journal", + ).all() + + linked_articles = utils.get_interlinked_articles(article.pk) + linked_articles = {a for a in linked_articles if a.pk != article.pk} + + if not linked_articles: + return "" + + current_code = journal.code links = [] - for linked_journal in group_journals: - if linked_journal.pk == journal.pk: - continue + for a in linked_articles: + raw_url = a.current_workflow_element_url or "" + parsed = urlparse(raw_url) + path_parts = parsed.path.strip("/").split("/") - try: - linked_language = linked_journal.get_setting( - group_name="general", - setting_name="default_journal_language", - ) - except Exception: - continue - - if article: - related = article.linked_from.select_related("from_article__journal", "to_article__journal").all() - related |= article.linked_to.select_related("from_article__journal", "to_article__journal").all() - if not any( - a.to_article.journal_id == linked_journal.pk - or a.from_article.journal_id == linked_journal.pk - for a in related - ): - continue + # If the current journal code appears early AND this is a *different* journal + if ( + current_code in path_parts[:2] + and a.journal_id != journal.pk + ): + path_parts = [part for part in path_parts if part != current_code] + + cleaned_path = "/" + "/".join(path_parts) links.append({ - "name_local": get_language_info(linked_language)["name_local"], - "url": linked_journal.site_url(path=corrected_path), + "article": a, + "journal": a.journal.code if hasattr(a, "journal") else "", + "url": cleaned_path, }) - if not links: - return "" - return render_to_string( - "hydra/elements/language_links.html", + "hydra/elements/linked_article_admin_links.html", { - "language_links": links, + "article_links": links, } ) + + diff --git a/install/settings.json b/install/settings.json new file mode 100644 index 0000000..a331238 --- /dev/null +++ b/install/settings.json @@ -0,0 +1,17 @@ +[ + { + "group": { + "name": "plugin:hydra" + }, + "setting": { + "description": "Enable Hydra Sidebar", + "is_translatable": true, + "name": "hydra_enable_sidebar", + "pretty_name": "Enable Hydra Sidebar", + "type": "char" + }, + "value": { + "default": "" + } + } +] diff --git a/management/commands/copy_articles.py b/management/commands/copy_articles.py index 57b95ab..cf06b1c 100644 --- a/management/commands/copy_articles.py +++ b/management/commands/copy_articles.py @@ -1,5 +1,6 @@ import os import shutil +import itertools from django.core.management.base import BaseCommand from django.db import transaction @@ -41,10 +42,18 @@ def add_arguments(self, parser): type=int, help='Optional single article ID to copy', ) + parser.add_argument( + "--stage", + type=str, + help="Optional stage to copy articles into" + ) parser.add_argument( '--target-lang', type=str, - help='Language code of the target journal eg "es" or "fr"', + help=( + 'Language code of the target journal eg "es" or "fr".' + ' will use the journal default if not provided', + ), ) def handle(self, *args, **options): @@ -52,6 +61,7 @@ def handle(self, *args, **options): target_code = options['target'] issue_id = options.get('issue') article_id = options.get('article') + stage = options.get('stage') self.target_lang = options.get('target_lang') try: @@ -70,10 +80,10 @@ def handle(self, *args, **options): for article in articles: with transaction.atomic(): - new_article = self.copy_article(article, target_journal) + new_article = self.copy_article(article, target_journal, stage) self.link_articles(article, new_article) - def copy_article(self, article, target_journal): + def copy_article(self, article, target_journal, stage): # Check for existing pubid in target journal pub_id = article.pk existing = identifiers_models.Identifier.objects.filter( @@ -99,7 +109,7 @@ def copy_article(self, article, target_journal): new_article = submission_models.Article( journal=target_journal, language=article.language, - stage=article.stage, + stage=stage if stage else article.stage, is_import=True, ) @@ -124,7 +134,7 @@ def copy_article(self, article, target_journal): new_article.last_page = article.last_page new_article.page_numbers = article.page_numbers new_article.total_pages = article.total_pages - new_article.stage = article.stage + new_article.stage = stage if stage else article.stage new_article.publication_fees = article.publication_fees new_article.submission_requirements = article.submission_requirements new_article.copyright_notice = article.copyright_notice @@ -147,9 +157,12 @@ def copy_article(self, article, target_journal): setattr(new_article, f"title_{lang_code}", getattr(article, f"title_{lang_code}", None)) - if self.target_lang: - new_article.title_en = getattr(article, f"title_{self.target_lang}") - new_article.title = getattr(article, f"title_{self.target_lang}") + # TODO: Remove once outgoing feeds/apis support multilang + language = self.target_lang or get_journal_lang_code(new_article.journal) + if language: + # Set default / en language to translated language + new_article.title_en = getattr(article, f"title_{language}") + new_article.title = getattr(article, f"title_{language}") # Section if article.section: @@ -295,7 +308,11 @@ def copy_file(self, file, new_article): return new_file def copy_galleys(self, source_article, target_article): - for galley in source_article.galley_set.filter(label__icontains=self.target_lang): + language = self.target_lang or get_journal_lang_code(target_article.journal) + if not language: + self.stderr.write(self.style.ERROR("No language provided and no default found")) + return + for galley in source_article.galley_set.filter(label__icontains=f"_{language}"): new_galley = core_models.Galley.objects.get(pk=galley.pk) new_galley.pk = None new_galley.article = target_article @@ -364,14 +381,19 @@ def create_doi(self, source_article, target_article): return '{0}/{1}'.format(doi_prefix, doi_suffix) def link_articles(self, source_article, target_article): - to_link = identifiers_models.Identifier.objects.filter( - id_type="linkid", - identifier=source_article.pk, + models.LinkedArticle.objects.get_or_create( + from_article=source_article, + to_article=target_article, ) - if to_link.count() < 2: - return - for a, b in itertools.permutations(to_link, 2): - models.LinkedArticle.objects.get_or_create( - from_article=a, - to_article=b - ) + +def get_journal_lang_code(journal): + language = None + try: + language = journal.get_setting( + group_name="general", + setting_name="default_journal_language", + ) + except Exception: + pass + + return language \ No newline at end of file diff --git a/plugin_settings.py b/plugin_settings.py index bf52020..967710c 100644 --- a/plugin_settings.py +++ b/plugin_settings.py @@ -1,4 +1,7 @@ from utils import plugins +from utils.install import update_settings +from events import logic as events_logic +from plugins.hydra import events PLUGIN_NAME = 'Hydra' DISPLAY_NAME = 'Hydra' @@ -10,8 +13,12 @@ JANEWAY_VERSION = '1.7.4' # Workflow Settings -IS_WORKFLOW_PLUGIN = False # For now -# JUMP_URL = '' +IS_WORKFLOW_PLUGIN = True +HANDSHAKE_URL = 'hydra_handshake_url' +JUMP_URL = 'hydra_jump_url' +ARTICLE_PK_IN_HANDSHAKE_URL = False +STAGE = 'hydra' +KANBAN_CARD = 'hydra/elements/card.html' class HydraPlugin(plugins.Plugin): @@ -27,10 +34,18 @@ class HydraPlugin(plugins.Plugin): janeway_version = JANEWAY_VERSION is_workflow_plugin = IS_WORKFLOW_PLUGIN + handshake_url = HANDSHAKE_URL + article_pk_in_handshake_url = ARTICLE_PK_IN_HANDSHAKE_URL + jump_url = JUMP_URL def install(): HydraPlugin.install() + update_settings(file_path="plugins/hydra/install/settings.json") + +def register_for_events(): + pass + def hook_registry(): return { @@ -43,5 +58,10 @@ def hook_registry(): { 'module': 'plugins.hydra.hooks', 'function': 'language_header_switcher', + }, + 'journal_editor_nav_block': + { + 'module': 'plugins.hydra.hooks', + 'function': 'editor_nav_article_switcher', } } diff --git a/templates/hydra/elements/linked_article_admin_links.html b/templates/hydra/elements/linked_article_admin_links.html new file mode 100644 index 0000000..05189d4 --- /dev/null +++ b/templates/hydra/elements/linked_article_admin_links.html @@ -0,0 +1,8 @@ +{% for link in article_links %} +
{% trans 'No articles to display.' %}
+ {% endfor %} + + {% include "common/elements/pagination.html" with form_id=facet_form.id %} +The Hydra plugin is installed
-{% endblock body %} + +