From 3215a2137b0bed8fd748195a2241fc3a4c2d3970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20S=C3=A1nchez?= Date: Tue, 3 Dec 2024 05:00:37 -0300 Subject: [PATCH 01/11] add enable_custom_editor_assignment option --- src/core/logic.py | 4 ++++ .../admin/elements/forms/group_review.html | 1 + src/utils/install/journal_defaults.json | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/src/core/logic.py b/src/core/logic.py index 98268f3ae..2ab3d4924 100755 --- a/src/core/logic.py +++ b/src/core/logic.py @@ -424,6 +424,10 @@ def get_settings_to_edit(display_group, journal, user): 'name': 'display_completed_reviews_in_additional_rounds_text', 'object': setting_handler.get_setting('general', 'display_completed_reviews_in_additional_rounds_text', journal), }, + { + 'name': 'enable_custom_editor_assignment', + 'object': setting_handler.get_setting('general', 'enable_custom_editor_assignment', journal), + }, ] setting_group = 'general' diff --git a/src/templates/admin/elements/forms/group_review.html b/src/templates/admin/elements/forms/group_review.html index 77e9e51ef..c179b95ac 100644 --- a/src/templates/admin/elements/forms/group_review.html +++ b/src/templates/admin/elements/forms/group_review.html @@ -9,6 +9,7 @@

General Review Settings

{% include "admin/elements/forms/field.html" with field=edit_form.default_review_visibility %} {% include "admin/elements/forms/field.html" with field=edit_form.enable_one_click_access %} {% include "admin/elements/forms/field.html" with field=edit_form.draft_decisions %} + {% include "admin/elements/forms/field.html" with field=edit_form.enable_custom_editor_assignment %} {% include "admin/elements/forms/field.html" with field=edit_form.enable_suggested_reviewers %} diff --git a/src/utils/install/journal_defaults.json b/src/utils/install/journal_defaults.json index 4601c628e..c5ab9beb8 100644 --- a/src/utils/install/journal_defaults.json +++ b/src/utils/install/journal_defaults.json @@ -5132,5 +5132,24 @@ "value": { "default": "" } + }, + { + "group": { + "name": "general" + }, + "setting": { + "description": "If enabled, Editors can be assigned to an article in a custom way.", + "is_translatable": false, + "name": "enable_custom_editor_assignment", + "pretty_name": "Enable Custom Editor Assignment", + "type": "boolean" + }, + "value": { + "default": "" + }, + "editable_by": [ + "editor", + "journal-manager" + ] } ] From 6f0652926e8460877fe5b6fa629b2a4048caf690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20S=C3=A1nchez?= Date: Tue, 3 Dec 2024 05:56:46 -0300 Subject: [PATCH 02/11] add new custom editor assignment view --- src/review/forms.py | 38 ++++++ src/review/logic.py | 77 +++++++++++ src/review/urls.py | 1 + src/review/views.py | 99 ++++++++++++++ .../review/add_editor_table_custom_row.html | 15 +++ .../admin/review/add_editor_assignment.html | 122 ++++++++++++++++++ .../admin/review/unassigned_article.html | 3 + 7 files changed, 355 insertions(+) create mode 100644 src/templates/admin/elements/review/add_editor_table_custom_row.html create mode 100644 src/templates/admin/review/add_editor_assignment.html diff --git a/src/review/forms.py b/src/review/forms.py index 344886edd..339389b4a 100755 --- a/src/review/forms.py +++ b/src/review/forms.py @@ -132,6 +132,44 @@ def check_for_potential_errors(self): return potential_errors +class EditorAssignmentForm(core_forms.ConfirmableIfErrorsForm): + editor = forms.ModelChoiceField(queryset=None) + date_due = forms.DateField(required=False) + + def __init__(self, *args, **kwargs): + self.journal = kwargs.pop('journal', None) + self.article = kwargs.pop('article') + self.editors = kwargs.pop('editors') + + super(EditorAssignmentForm, self).__init__(*args, **kwargs) + + if self.editors: + self.fields['editor'].queryset = self.editors + + def clean(self): + cleaned_data = super().clean() + return cleaned_data + + def save(self, commit=True, request=None): + editor = self.cleaned_data['editor'] + + if request: + editor_assignment = models.EditorAssignment( + article=self.article, + editor=editor, + ) + + if editor_assignment.editor.is_editor(request): + editor_assignment.editor_type = 'editor' + elif editor_assignment.editor.is_section_editor(request): + editor_assignment.editor_type = 'section-editor' + + if commit: + editor_assignment.save() + + return editor_assignment + + class BulkReviewAssignmentForm(forms.ModelForm): template = forms.CharField( widget=TinyMCE, diff --git a/src/review/logic.py b/src/review/logic.py index b9c1e32c5..dc33eb68e 100755 --- a/src/review/logic.py +++ b/src/review/logic.py @@ -22,7 +22,10 @@ When, BooleanField, Value, + F, + Q, ) +from django.db.models.functions import Coalesce from django.shortcuts import redirect, reverse from django.utils import timezone from django.db import IntegrityError @@ -41,6 +44,71 @@ from submission import models as submission_models +def get_editors(article, candidate_queryset, exclude_pks): + prefetch_editor_assignment = Prefetch( + 'editor', + queryset=models.EditorAssignment.objects.filter( + article__journal=article.journal + ) + ) + active_assignments_count = models.EditorAssignment.objects.filter( + editor=OuterRef("id"), + ).values( + "editor_id", + ).annotate( + rev_count=Count("editor_id"), + ).values("rev_count") + + editors = candidate_queryset.exclude( + pk__in=exclude_pks, + ).prefetch_related( + prefetch_editor_assignment, + 'interest', + ) + order_by = [] + + editors = editors.annotate( + active_assignments_count=Subquery( + active_assignments_count, + output_field=IntegerField(), + ) + ).annotate( + active_assignments_count=Coalesce(F('active_assignments_count'), Value(0)), + ) + order_by.append('active_assignments_count') + + editors = editors.order_by(*order_by) + + return editors + + +def get_editors_candidates(article, user=None, editors_to_exclude=None): + """ Builds a queryset of candidates for editor assignment requests for the given article + :param article: an instance of submission.models.Article + :param user: The user requesting candidates who would be filtered out + :param editors_to_exclude: queryset of Account objects + """ + + editors = article.editorassignment_set.all() + editor_pks_to_exclude = [assignment.editor.pk for assignment in editors] + + if editors_to_exclude: + for editor in editors_to_exclude: + editor_pks_to_exclude.append( + editor.pk, + ) + + queryset_editor = article.journal.users_with_role('editor') + queryset_section_editor = article.journal.users_with_role('section-editor') + + return get_editors( + article, + queryset_editor | queryset_section_editor, + editor_pks_to_exclude + ) + + + def get_reviewers(article, candidate_queryset, exclude_pks): prefetch_review_assignment = Prefetch( 'reviewer', @@ -626,6 +694,15 @@ def quick_assign(request, article, reviewer_user=None): messages.add_message(request, messages.WARNING, error) +def handle_editor_form(request, new_editor_form, editor_type): + account = new_editor_form.save(commit=False) + account.is_active = True + account.save() + account.add_account_role(editor_type, request.journal) + messages.add_message(request, messages.INFO, 'A new account has been created.') + return account + + def handle_reviewer_form(request, new_reviewer_form): account = new_reviewer_form.save(commit=False) account.is_active = True diff --git a/src/review/urls.py b/src/review/urls.py index 692d4569c..ef2e93070 100755 --- a/src/review/urls.py +++ b/src/review/urls.py @@ -32,6 +32,7 @@ re_path(r'^unassigned/article/(?P\d+)/notify/(?P\d+)/$', views.assignment_notification, name='review_assignment_notification'), re_path(r'^unassigned/article/(?P\d+)/move/review/$', views.move_to_review, name='review_move_to_review'), + re_path(r'^article/(?P\d+)/editor/add/$', views.add_editor_assignment, name='add_editor_assignment'), re_path(r'^article/(?P\d+)/crosscheck/$', views.view_ithenticate_report, name='review_crosscheck'), re_path(r'^article/(?P\d+)/move/(?Paccept|decline|undecline)/$', views.review_decision, name='review_decision'), diff --git a/src/review/views.py b/src/review/views.py index aa13351be..c551407cb 100755 --- a/src/review/views.py +++ b/src/review/views.py @@ -229,6 +229,105 @@ def view_ithenticate_report(request, article_id): return render(request, template, context) +@editor_is_not_author +@editor_user_required +def add_editor_assignment(request, article_id): + """ + Allow an editor to add a new editor assignment + :param request: HttpRequest object + :param article_id: Article PK + :return: HttpResponse + """ + article = get_object_or_404(submission_models.Article, pk=article_id) + + editors = logic.get_editors_candidates( + article, + user=request.user, + ) + + form = forms.EditorAssignmentForm( + journal=request.journal, + article=article, + editors=editors + ) + + new_editor_form = core_forms.QuickUserForm() + + if request.POST: + + if 'assign' in request.POST: + # first check whether the user exists + new_editor_form = core_forms.QuickUserForm(request.POST) + try: + user = core_models.Account.objects.get(email=new_editor_form.data['email']) + user.add_account_role('section-editor', request.journal) + except core_models.Account.DoesNotExist: + user = None + + if user: + return redirect( + reverse( + 'add_editor_assignment', + kwargs={'article_id': article.pk} + ) + '?' + parse.urlencode({'user': new_editor_form.data['email'], 'id': str(user.pk)},) + ) + + valid = new_editor_form.is_valid() + + if valid: + acc = logic.handle_editor_form(request, new_editor_form, 'section-editor') + return redirect( + reverse( + 'add_editor_assignment', kwargs={'article_id': article.pk} + ) + '?' + parse.urlencode({'user': new_editor_form.data['email'], 'id': str(acc.pk)}), + ) + else: + form.modal = {'id': 'editor'} + + else: + form = forms.EditorAssignmentForm( + request.POST, + journal=request.journal, + article=article, + editors=editors, + ) + if form.is_valid() and form.is_confirmed(): + editor_assignment = form.save(request=request, commit=False) + editor = editor_assignment.editor + assignment_type = editor_assignment.editor_type + + if not editor.has_an_editor_role(request): + messages.add_message(request, messages.WARNING, 'User is not an Editor or Section Editor') + return redirect(reverse('review_unassigned_article', kwargs={'article_id': article.pk})) + + _, created = logic.assign_editor(article, editor, assignment_type, request) + messages.add_message(request, messages.SUCCESS, '{0} added as an Editor'.format(editor.full_name())) + if created and editor: + return redirect( + reverse( + 'review_assignment_notification', + kwargs={'article_id': article_id, 'editor_id': editor.pk} + ), + ) + else: + messages.add_message(request, messages.WARNING, + '{0} is already an Editor on this article.'.format(editor.full_name())) + + return redirect(reverse('review_unassigned_article', kwargs={'article_id': article_id})) + + template = 'admin/review/add_editor_assignment.html' + + context = { + 'article': article, + 'form': form, + 'editors': editors.filter(accountrole__role__slug='editor'), + 'section_editors': editors.filter(accountrole__role__slug='section-editor'), + 'new_editor_form': new_editor_form, + } + + return render(request, template, context) + + @senior_editor_user_required def assign_editor_move_to_review(request, article_id, editor_id, assignment_type): """Allows an editor to assign another editor to an article and moves to review.""" diff --git a/src/templates/admin/elements/review/add_editor_table_custom_row.html b/src/templates/admin/elements/review/add_editor_table_custom_row.html new file mode 100644 index 000000000..6a6ad2410 --- /dev/null +++ b/src/templates/admin/elements/review/add_editor_table_custom_row.html @@ -0,0 +1,15 @@ + + + + + {{ editor.full_name }} + {{ editor.email }} + {{ editor_type_label }} + {{ editor.active_assignments_count|default_if_none:0 }} + + {% for interest in editor.interest.all %}{{ interest.name }}{% if not forloop.last %}, {% endif %}{% endfor %} + + \ No newline at end of file diff --git a/src/templates/admin/review/add_editor_assignment.html b/src/templates/admin/review/add_editor_assignment.html new file mode 100644 index 000000000..7252e8963 --- /dev/null +++ b/src/templates/admin/review/add_editor_assignment.html @@ -0,0 +1,122 @@ +{% extends "admin/core/base.html" %} +{% load foundation %} +{% block title %}Add Editor Assignment{% endblock title %} +{% block title-section %}Add Editor Assignment{% endblock %} +{% block title-sub %}#{{ article.pk }} / {{ article.correspondence_author.last_name }} / {{ article.safe_title }}{% endblock %} + +{% block breadcrumbs %} + {{ block.super }} + {% include "elements/breadcrumbs/unassigned_base.html" %} + {% if article %}
  • {{ article.safe_title }}
  • {% endif %} +
  • Add Editor Assignment
  • +{% endblock breadcrumbs %} + +{% block body %} +
    +
    + {% include "elements/forms/errors.html" with form=form %} + {% csrf_token %} +
    + +
    +
    +

      + {% blocktrans %} + You can select an editor using the radio + buttons in the first column. + {% endblocktrans %} + {% blocktrans %} + If you cannot find the editor you want in + this list you can use Enroll Existing User to + search the database and give users the Editor + role, or Add New Editor to create a new + account for an editor (this process is silent, + they will not receive an account creation + email). + {% endblocktrans %} +

    +
    + + + + + + + + + + + + + + + {% for editor in editors %} + {% include "admin/elements/review/add_editor_table_custom_row.html" with editor_type_label='Editor' %} + {% endfor %} + {% for editor in section_editors %} + {% include "admin/elements/review/add_editor_table_custom_row.html" with editor_type_label='Section Editor' %} + {% endfor %} + {% if not editors and not section_editors %} + + + + + + + {% endif %} + +
    SelectNameEmail AddressTypeActive AssignmentsInterests
    No suitable editors.
    +
    +
    +
    + +   +
    +
    +
    +
       + + + + {% if journal_settings.general.enable_one_click_access %} +
    +
    +
    +

     Add New Editor

    +
    +
    + +
    +

    This form allows you to quickly create a new editor without having to input a full user's data.

    +
    + {% include "elements/forms/errors.html" with form=new_editor_form %} + {% csrf_token %} + {{ new_editor_form|foundation }} + +
    +
    +
    +
    +
    + {% endif %} + + {% if form.modal %} + {% include "admin/elements/confirm_modal.html" with modal=form.modal form_id="editor_assignment_form" %} + {% endif %} + +{% endblock body %} + +{% block js %} + {% include "elements/datatables.html" with target="#editors" %} + {% if form.modal %} + {% include "admin/elements/open_modal.html" with target=form.modal.id %} + {% endif %} + {% include "elements/datatables.html" with target="#enrolluser" %} +{% endblock js %} \ No newline at end of file diff --git a/src/templates/admin/review/unassigned_article.html b/src/templates/admin/review/unassigned_article.html index 536cc480d..9218dada0 100644 --- a/src/templates/admin/review/unassigned_article.html +++ b/src/templates/admin/review/unassigned_article.html @@ -210,6 +210,9 @@

    Files

    Editors

    + {% if journal_settings.general.enable_custom_editor_assignment %} + Add Editor + {% endif %}
    From 4c76fd62dedc2d507b8c35cf5b4f7fde55d0378e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20S=C3=A1nchez?= Date: Sun, 15 Dec 2024 08:08:31 -0300 Subject: [PATCH 03/11] update custom editor assignment view --- .../admin/review/add_editor_assignment.html | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/templates/admin/review/add_editor_assignment.html b/src/templates/admin/review/add_editor_assignment.html index 7252e8963..f1fa8730f 100644 --- a/src/templates/admin/review/add_editor_assignment.html +++ b/src/templates/admin/review/add_editor_assignment.html @@ -119,4 +119,34 @@

     Add New Editor

    {% include "admin/elements/open_modal.html" with target=form.modal.id %} {% endif %} {% include "elements/datatables.html" with target="#enrolluser" %} + + {% endblock js %} \ No newline at end of file From 5aebeeef01ebf57d276e271a4e5a4f4a87726484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20S=C3=A1nchez?= Date: Tue, 10 Dec 2024 07:02:01 -0300 Subject: [PATCH 04/11] add enable_research_topics option --- src/core/logic.py | 4 ++++ .../admin/elements/forms/group_review.html | 1 + src/utils/install/journal_defaults.json | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/src/core/logic.py b/src/core/logic.py index 2ab3d4924..e43d59d9e 100755 --- a/src/core/logic.py +++ b/src/core/logic.py @@ -363,6 +363,10 @@ def get_settings_to_edit(display_group, journal, user): 'name': 'draft_decisions', 'object': setting_handler.get_setting('general', 'draft_decisions', journal), }, + { + 'name': 'enable_study_topics', + 'object': setting_handler.get_setting('general', 'enable_study_topics', journal), + }, { 'name': 'default_review_form', 'object': setting_handler.get_setting('general', 'default_review_form', journal), diff --git a/src/templates/admin/elements/forms/group_review.html b/src/templates/admin/elements/forms/group_review.html index c179b95ac..7b314cb4d 100644 --- a/src/templates/admin/elements/forms/group_review.html +++ b/src/templates/admin/elements/forms/group_review.html @@ -8,6 +8,7 @@

    General Review Settings

    {% include "admin/elements/forms/field.html" with field=edit_form.default_review_days %} {% include "admin/elements/forms/field.html" with field=edit_form.default_review_visibility %} {% include "admin/elements/forms/field.html" with field=edit_form.enable_one_click_access %} + {% include "admin/elements/forms/field.html" with field=edit_form.enable_study_topics %} {% include "admin/elements/forms/field.html" with field=edit_form.draft_decisions %} {% include "admin/elements/forms/field.html" with field=edit_form.enable_custom_editor_assignment %} {% include "admin/elements/forms/field.html" with field=edit_form.enable_suggested_reviewers %} diff --git a/src/utils/install/journal_defaults.json b/src/utils/install/journal_defaults.json index c5ab9beb8..136b12db2 100644 --- a/src/utils/install/journal_defaults.json +++ b/src/utils/install/journal_defaults.json @@ -5151,5 +5151,24 @@ "editor", "journal-manager" ] + }, + { + "group": { + "name": "general" + }, + "setting": { + "description": "If enabled, Articles can be related to preconfigured research topics from the journal, with the purpose of improving review assignments based on account matches.", + "is_translatable": false, + "name": "enable_study_topics", + "pretty_name": "Enable Research Topics", + "type": "boolean" + }, + "value": { + "default": "" + }, + "editable_by": [ + "editor", + "journal-manager" + ] } ] From 646e25c886987a093cba3fac18330f5d50ef6361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20S=C3=A1nchez?= Date: Tue, 10 Dec 2024 07:14:14 -0300 Subject: [PATCH 05/11] add research topic model --- .../0100_topicgroup_topics_accounttopic.py | 57 ++++++++++++++ src/core/models.py | 74 +++++++++++++++++++ .../migrations/0085_articletopic.py | 27 +++++++ src/submission/models.py | 23 ++++++ 4 files changed, 181 insertions(+) create mode 100644 src/core/migrations/0100_topicgroup_topics_accounttopic.py create mode 100644 src/submission/migrations/0085_articletopic.py diff --git a/src/core/migrations/0100_topicgroup_topics_accounttopic.py b/src/core/migrations/0100_topicgroup_topics_accounttopic.py new file mode 100644 index 000000000..4914d449f --- /dev/null +++ b/src/core/migrations/0100_topicgroup_topics_accounttopic.py @@ -0,0 +1,57 @@ +# Generated by Django 4.2.16 on 2024-12-10 10:11 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('journal', '0066_issue_type_bleach_20240507_1359'), + ('core', '0099_alter_accountrole_options'), + ] + + operations = [ + migrations.CreateModel( + name='TopicGroup', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('pretty_name', models.CharField(max_length=100)), + ('description', models.TextField(blank=True, null=True)), + ('journal', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='journal.journal')), + ], + options={ + 'verbose_name_plural': 'study topic groups for articles and accounts', + 'unique_together': {('name', 'journal')}, + }, + ), + migrations.CreateModel( + name='Topics', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('pretty_name', models.CharField(max_length=100)), + ('description', models.TextField(blank=True, null=True)), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.topicgroup')), + ('journal', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='journal.journal')), + ], + options={ + 'verbose_name_plural': 'study topics for articles and accounts', + 'unique_together': {('name', 'journal', 'group')}, + }, + ), + migrations.CreateModel( + name='AccountTopic', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('topic_type', models.CharField(choices=[('PR', 'Primary'), ('SE', 'Secondary')], default='PR', max_length=2)), + ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('topic', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.topics')), + ], + options={ + 'unique_together': {('account', 'topic')}, + }, + ), + ] diff --git a/src/core/models.py b/src/core/models.py index f3ba5e03a..2b493f23b 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -1506,6 +1506,80 @@ class Meta: verbose_name_plural = 'task complete events' +class TopicGroup(models.Model): + name = models.CharField(max_length=100) + pretty_name = models.CharField(max_length=100) + journal = models.ForeignKey( + 'journal.Journal', + on_delete=models.CASCADE, + blank=True, + null=True, + ) + description = models.TextField(null=True, blank=True) + + class Meta: + unique_together = ('name', 'journal') + verbose_name_plural = 'study topic groups for articles and accounts' + + def __str__(self): + return self.pretty_name + + def topic_count(self): + return self.topics_set.all().count() + + +class Topics(models.Model): + name = models.CharField(max_length=100) + pretty_name = models.CharField(max_length=100) + journal = models.ForeignKey( + 'journal.Journal', + on_delete=models.CASCADE, + blank=True, + null=True, + ) + group = models.ForeignKey( + TopicGroup, + on_delete=models.CASCADE, + ) + description = models.TextField(null=True, blank=True) + + class Meta: + unique_together = ('name', 'journal', 'group') + verbose_name_plural = 'study topics for articles and accounts' + + def __str__(self): + return self.pretty_name + + def article_count(self): + return self.articletopic_set.all().count() + + def account_count(self): + return self.accounttopic_set.all().count() + + +class AccountTopic(models.Model): + PRIMARY = 'PR' + SECONDARY = 'SE' + TOPIC_TYPE_CHOICES = [ + (PRIMARY, 'Primary'), + (SECONDARY, 'Secondary'), + ] + + account = models.ForeignKey(Account, on_delete=models.CASCADE) + topic = models.ForeignKey(Topics, on_delete=models.CASCADE) + topic_type = models.CharField( + max_length=2, + choices=TOPIC_TYPE_CHOICES, + default=PRIMARY, + ) + + class Meta: + unique_together = ('account', 'topic') + + def __str__(self): + return f"{self.account} - {self.topic} ({self.topic_type})" + + class EditorialGroup(models.Model): name = models.CharField(max_length=500) press = models.ForeignKey( diff --git a/src/submission/migrations/0085_articletopic.py b/src/submission/migrations/0085_articletopic.py new file mode 100644 index 000000000..ed5a9cb65 --- /dev/null +++ b/src/submission/migrations/0085_articletopic.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.16 on 2024-12-10 10:12 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0100_topicgroup_topics_accounttopic'), + ('submission', '0084_remove_article_jats_article_type_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='ArticleTopic', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('topic_type', models.CharField(choices=[('PR', 'Primary'), ('SE', 'Secondary')], default='PR', max_length=2)), + ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='submission.article')), + ('topic', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.topics')), + ], + options={ + 'unique_together': {('article', 'topic')}, + }, + ), + ] diff --git a/src/submission/models.py b/src/submission/models.py index 9a544f929..774efa00b 100755 --- a/src/submission/models.py +++ b/src/submission/models.py @@ -2489,3 +2489,26 @@ def order_keywords(sender, instance, action, reverse, model, pk_set, **kwargs): m2m_changed.connect(order_keywords, sender=Article.keywords.through) + + +class ArticleTopic(models.Model): + PRIMARY = 'PR' + SECONDARY = 'SE' + TOPIC_TYPE_CHOICES = [ + (PRIMARY, 'Primary'), + (SECONDARY, 'Secondary'), + ] + + article = models.ForeignKey(Article, on_delete=models.CASCADE) + topic = models.ForeignKey('core.Topics', on_delete=models.CASCADE) + topic_type = models.CharField( + max_length=2, + choices=TOPIC_TYPE_CHOICES, + default=PRIMARY, + ) + + class Meta: + unique_together = ('article', 'topic') + + def __str__(self): + return f"{self.article} - {self.topic} ({self.topic_type})" From e8d24a54599d2ae695fbcc7f7719f9a89dfde147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20S=C3=A1nchez?= Date: Tue, 10 Dec 2024 07:48:36 -0300 Subject: [PATCH 06/11] add research topic manager --- src/core/forms/__init__.py | 2 + src/core/forms/forms.py | 53 +++++ src/core/include_urls.py | 16 ++ src/core/views.py | 201 ++++++++++++++++++ src/journal/models.py | 6 + src/templates/admin/core/manager/index.html | 1 + .../core/manager/topics/manage_topic.html | 45 ++++ .../core/manager/topics/topic_accounts.html | 58 +++++ .../core/manager/topics/topic_articles.html | 62 ++++++ .../admin/core/manager/topics/topic_list.html | 152 +++++++++++++ .../review/assigned_topics_table.html | 9 + 11 files changed, 605 insertions(+) create mode 100644 src/templates/admin/core/manager/topics/manage_topic.html create mode 100644 src/templates/admin/core/manager/topics/topic_accounts.html create mode 100644 src/templates/admin/core/manager/topics/topic_articles.html create mode 100644 src/templates/admin/core/manager/topics/topic_list.html create mode 100644 src/templates/admin/elements/review/assigned_topics_table.html diff --git a/src/core/forms/__init__.py b/src/core/forms/__init__.py index 168dc6766..40ff7a82a 100644 --- a/src/core/forms/__init__.py +++ b/src/core/forms/__init__.py @@ -31,6 +31,8 @@ SectionForm, SettingEmailForm, SimpleTinyMCEForm, + TopicForm, + TopicGroupForm, UserCreationFormExtended, XSLFileForm, ) diff --git a/src/core/forms/forms.py b/src/core/forms/forms.py index b5164dc4d..8de3727c5 100755 --- a/src/core/forms/forms.py +++ b/src/core/forms/forms.py @@ -3,6 +3,7 @@ __license__ = "AGPL v3" __maintainer__ = "Birkbeck Centre for Technology and Publishing" +import re import uuid import json @@ -496,6 +497,58 @@ def __init__(self, *args, **kwargs): self.fields['editors'].required = False +class TopicForm(forms.ModelForm): + class Meta: + model = models.Topics + fields = ['pretty_name', 'description', 'group'] + labels = { + 'pretty_name': 'Name', + 'description': 'Description', + 'group': 'Topic Group', + } + + def __init__(self, *args, **kwargs): + request = kwargs.pop('request', None) + super(TopicForm, self).__init__(*args, **kwargs) + if request: + self.fields['group'].queryset = request.journal.topic_groups() + self.fields['group'].label_from_instance = lambda obj: "%s" % obj.pretty_name + + +class TopicGroupForm(forms.ModelForm): + class Meta: + model = models.TopicGroup + fields = ['pretty_name', 'description'] + labels = { + 'pretty_name': 'Name', + 'description': 'Description', + } + + def __init__(self, *args, **kwargs): + super(TopicGroupForm, self).__init__(*args, **kwargs) + + def formatted_name(self, pretty_name: str): + return re.sub(r'\s+', '_', re.sub(r'[()]', '', pretty_name)).lower() + + def save(self, commit=True, request=None): + topic_group = super(TopicGroupForm, self).save(commit=False) + if request: + topic_group_pretty_name = topic_group.pretty_name + topic_group.journal = request.journal + topic_group.name = self.formatted_name(topic_group_pretty_name) + if commit: + topic_group.save() + default_topic_name = f'{topic_group_pretty_name} (others)' + models.Topics.objects.create( + pretty_name=default_topic_name, + name=self.formatted_name(default_topic_name), + journal=request.journal, + group=topic_group, + description='another topics' + ) + return topic_group + + class QuickUserForm(forms.ModelForm): class Meta: model = models.Account diff --git a/src/core/include_urls.py b/src/core/include_urls.py index dea31e2e0..c6d42397f 100644 --- a/src/core/include_urls.py +++ b/src/core/include_urls.py @@ -177,6 +177,22 @@ re_path(r'^manager/sections/(?P\d+)/articles/$', core_views.section_articles, name='core_manager_section_articles'), + # Journal Topics + re_path(r'^manager/topics/$', + core_views.topic_list, name='core_manager_topics'), + re_path(r'^manager/topics/add/$', + core_views.manage_topic, name='core_manager_topic_add'), + re_path(r'^manager/topics/group/add/$', + core_views.manage_topic_group, name='core_manager_topic_group_add'), + re_path(r'^manager/topics/(?P\d+)/$', + core_views.manage_topic, name='core_manager_topic'), + re_path(r'^manager/topics/group/(?P\d+)/$', + core_views.manage_topic_group, name='core_manager_topic_group'), + re_path(r'^manager/topics/(?P\d+)/accounts/$', + core_views.topic_accounts, name='core_manager_topic_accounts'), + re_path(r'^manager/topics/(?P\d+)/articles/$', + core_views.topic_articles, name='core_manager_topic_articles'), + # Pinned Articles re_path(r'^manager/articles/pinned/$', core_views.pinned_articles, name='core_pinned_articles'), diff --git a/src/core/views.py b/src/core/views.py index 2b3812f82..548c9730c 100755 --- a/src/core/views.py +++ b/src/core/views.py @@ -2154,6 +2154,207 @@ def section_articles(request, section_id): return render(request, template, context) +@editor_user_required +def topic_list(request): + """ + Displays a list of the journals topics. + :praram request: HttpRequest object + :return: HttpResponse + """ + topic_objects = core_models.Topics.objects.filter( + journal=request.journal, + ) + topic_group_objects = core_models.TopicGroup.objects.filter( + journal=request.journal, + ) + + group_filter = request.GET.get('group_filter') + if group_filter: + topic_objects = topic_objects.filter(group_id=group_filter) + + if request.POST and 'delete' in request.POST: + topic_id = request.POST.get('delete') + topic_to_delete = get_object_or_404(core_models.Topics, pk=topic_id) + + if topic_to_delete.article_count() or topic_to_delete.account_count(): + messages.add_message( + request, + messages.WARNING, + _( + 'You cannot remove a topic related to articles or accounts. Remove articles and accounts' + ' from the topic if you want to delete it.' + ), + ) + else: + topic_to_delete.delete() + return redirect(reverse('core_manager_topics')) + + if request.POST and 'delete_group' in request.POST: + topic_group_id = request.POST.get('delete_group') + topic_group_to_delete = get_object_or_404(core_models.TopicGroup, pk=topic_group_id) + + if topic_group_to_delete.topic_count(): + messages.add_message( + request, + messages.WARNING, + _( + 'You cannot remove a group related to existing topic. Remove the topics' + ' from the group if you want to delete it.' + ), + ) + else: + topic_group_to_delete.delete() + return redirect(reverse('core_manager_topics')) + + template = 'core/manager/topics/topic_list.html' + context = { + 'topic_objects': topic_objects, + 'topic_group_objects': topic_group_objects, + } + return render(request, template, context) + + +@editor_user_required +def manage_topic(request, topic_id=None): + """ + Displays a list of topics, allows them to be added, edited and deleted. + :param request: HttpRequest object + :param section_id: Section object PK, optional + :return: HttpResponse object + """ + topic = get_object_or_404(core_models.Topics, pk=topic_id, + journal=request.journal) if topic_id else None + topics = core_models.Topics.objects.filter(journal=request.journal) + + if topic: + form = forms.TopicForm(instance=topic, request=request) + else: + form = forms.TopicForm(request=request) + + if request.POST: + + if topic: + form = forms.TopicForm(request.POST, instance=topic, request=request) + else: + form = forms.TopicForm(request.POST, request=request) + + if form.is_valid(): + form_topic = form.save(commit=False) + topic_exists = core_models.Topics.objects.filter( + journal=request.journal, + pretty_name=form_topic.pretty_name, + group=form_topic.group + ) + + if topic_exists: + messages.add_message(request, messages.ERROR, + '{0} is already exists in this journal for {1} group.'.format(form_topic.pretty_name, form_topic.group.pretty_name)) + return redirect(reverse('core_manager_topic_add')) + + form_topic.journal = request.journal + form_topic.name = form_topic.pretty_name.lower().replace(" ", "_") + form_topic.save() + form.save_m2m() + messages.add_message(request, messages.SUCCESS, + '{0} topic saved'.format(form_topic.pretty_name)) + + return redirect(reverse('core_manager_topic_add')) + + template = 'core/manager/topics/manage_topic.html' + context = { + 'topics': topics, + 'topic': topic, + 'form': form, + } + return render(request, template, context) + + +@editor_user_required +def manage_topic_group(request, topic_group_id=None): + """ + Displays a list of topic groups, allows them to be added, edited and deleted. + :param request: HttpRequest object + :param section_id: Section object PK, optional + :return: HttpResponse object + """ + topic_group = get_object_or_404(core_models.TopicGroup, pk=topic_group_id, + journal=request.journal) if topic_group_id else None + topic_groups = core_models.TopicGroup.objects.filter(journal=request.journal) + + if topic_group: + form = forms.TopicGroupForm(instance=topic_group) + else: + form = forms.TopicGroupForm() + + if request.POST: + if topic_group: + form = forms.TopicGroupForm(request.POST, instance=topic_group) + else: + form = forms.TopicGroupForm(request.POST) + + if form.is_valid(): + form_topic_group = form.save(commit=False, request=request) + topic_group_exists = core_models.TopicGroup.objects.filter( + journal=request.journal, + pretty_name=form_topic_group.pretty_name, + ) + + if topic_group_exists: + messages.add_message(request, messages.ERROR, + '{0} is already exists in this journal'.format(form_topic_group.pretty_name)) + return redirect(reverse('core_manager_topic_group_add')) + + form.save(request=request) + form.save_m2m() + + messages.add_message(request, messages.SUCCESS, + '{0} topic group saved'.format(form_topic_group.pretty_name)) + + return redirect(reverse('core_manager_topic_group_add')) + + template = 'core/manager/topics/manage_topic.html' + context = { + 'topics': topic_groups, + 'topic': topic_group, + 'form': form, + } + return render(request, template, context) + + +@editor_user_required +def topic_accounts(request, topic_id): + """ + Displays a list of accounts in a given topic. + """ + topic = get_object_or_404( + core_models.Topics, + pk=topic_id, + journal=request.journal, + ) + template = 'core/manager/topics/topic_accounts.html' + context = { + 'topic': topic, + } + return render(request, template, context) + + +@editor_user_required +def topic_articles(request, topic_id): + """ + Displays a list of articles in a given section. + """ + topic = get_object_or_404( + core_models.Topics, + pk=topic_id, + journal=request.journal, + ) + template = 'core/manager/topics/topic_articles.html' + context = { + 'topic': topic, + } + return render(request, template, context) + + @editor_user_required def pinned_articles(request): """ diff --git a/src/journal/models.py b/src/journal/models.py index 1e7c44e33..ee0c92155 100644 --- a/src/journal/models.py +++ b/src/journal/models.py @@ -602,6 +602,12 @@ def setup_default_review_form(self): default_review_form.elements.add(main_element) + def topic_groups(self): + return self.topicgroup_set.all() + + def topics(self): + return self.study_topic.all() + def archive_published_articles(self): # Subquery will attempt to grab a DOI for this article. doi_subquery = identifier_models.Identifier.objects.filter( diff --git a/src/templates/admin/core/manager/index.html b/src/templates/admin/core/manager/index.html index f3e94a187..4ac41940c 100644 --- a/src/templates/admin/core/manager/index.html +++ b/src/templates/admin/core/manager/index.html @@ -102,6 +102,7 @@

    Articles & Issues

    Issue Manager Sections (Article Types) Licence Manager + Topic Manager
    diff --git a/src/templates/admin/core/manager/topics/manage_topic.html b/src/templates/admin/core/manager/topics/manage_topic.html new file mode 100644 index 000000000..ad9c7ec0e --- /dev/null +++ b/src/templates/admin/core/manager/topics/manage_topic.html @@ -0,0 +1,45 @@ +{% extends "admin/core/base.html" %} +{% load static %} +{% load foundation %} + +{% block title %}Topics{% endblock title %} +{% block title-section %}Topics{% endblock %} + +{% block breadcrumbs %} + {{ block.super }} +
  • Manager
  • +
  • Topics
  • +
  • {% if topic %}{% trans 'Editing Topics' %}: {{ topic.name }}{% else %}{% trans 'Add a Topic' %}{% endif %}
  • +{% endblock %} + +{% block body %} +
    +
    +
    +

    {% if topic %}{% trans 'Editing Topics' %}: {{ topic.name }}{% else %}{% trans 'Add a Topic' %}{% endif %}

    +
    +
    +
    + {% include "elements/forms/errors.html" with form=form %} + {% csrf_token %} + {{ form|foundation }} + + +
    +
    +
    +{% endblock body %} + +{% block js %} + + + +{% endblock js %} \ No newline at end of file diff --git a/src/templates/admin/core/manager/topics/topic_accounts.html b/src/templates/admin/core/manager/topics/topic_accounts.html new file mode 100644 index 000000000..fc2ff2b37 --- /dev/null +++ b/src/templates/admin/core/manager/topics/topic_accounts.html @@ -0,0 +1,58 @@ +{% extends "admin/core/base.html" %} +{% load static %} +{% load foundation %} +{% load bool_fa %} + +{% block title %}{% trans 'Topics' %}{% endblock title %} +{% block title-section %}{% trans 'Topics' %}{% endblock %} + +{% block breadcrumbs %} + {{ block.super }} +
  • {% trans 'Manager' %}
  • +
  • {% trans 'Topics' %}
  • +
  • {{ topic.pretty_name }} {% trans 'Accounts' %}
  • +{% endblock %} + +{% block body %} +
    +
    +
    +

    {{ topic.pretty_name }} {% trans 'Accounts' %}

    + {% trans 'Back' %} +
    +
    +

    + {% trans 'Accounts that belong to the' %} {{ topic.pretty_name }} {% trans 'topic are listed below.' %} +

    +
    + + + + + + + + + + + {% for account in topic.accounttopic_set.all %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
    {% trans 'Username' %}{% trans 'First Name' %}{% trans 'Last Name' %}{% trans 'Email' %}{% trans 'Topics' %}
    {{ account.account.username }}{{ account.account.first_name }}{{ account.account.last_name }}{{ account.account.email }} + {% include "admin/elements/review/assigned_topics_table.html" with topics_by_type=account.account.topics_by_type %} +
    {% trans 'This topic has no accounts.' %}
    +
    +
    + +{% endblock body %} \ No newline at end of file diff --git a/src/templates/admin/core/manager/topics/topic_articles.html b/src/templates/admin/core/manager/topics/topic_articles.html new file mode 100644 index 000000000..cbab6c4ce --- /dev/null +++ b/src/templates/admin/core/manager/topics/topic_articles.html @@ -0,0 +1,62 @@ +{% extends "admin/core/base.html" %} +{% load static %} +{% load foundation %} +{% load bool_fa %} + +{% block title %}{% trans 'Topics' %}{% endblock title %} +{% block title-section %}{% trans 'Topics' %}{% endblock %} + +{% block breadcrumbs %} + {{ block.super }} +
  • {% trans 'Manager' %}
  • +
  • {% trans 'Topics' %}
  • +
  • {{ topic.pretty_name }} {% trans 'Articles' %}
  • +{% endblock %} + +{% block body %} +
    +
    +
    +

    {{ topic.pretty_name }} {% trans 'Articles' %}

    + {% trans 'Back' %} +
    +
    +

    + {% trans 'Articles that belong to the' %} {{ topic.pretty_name }} {% trans 'topic are listed below.' %} +

    + + + + + + + + + + + + + + {% for article in topic.articletopic_set.all %} + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
    {% trans 'Title' %}{% trans 'D/T Submitted' %}{% trans 'Stage' %}{% trans 'Main Author' %}{% trans 'Editors' %}{% trans 'Topics' %}
    {{ article.article.safe_title }}{{ article.article.date_submitted }}{{ article.article.stage }}{{ article.article.correspondence_author.full_name }}{% for editor in article.article.editors %}{{ editor.editor.full_name }}{% if not forloop.last %}, {% endif %}{% endfor %} + {% include "elements/review/assigned_topics_table.html" with topics_by_type=article.article.topics_by_type %} + View
    {% trans 'This topic has no articles.' %}
    +
    +
    +
    +{% endblock body %} \ No newline at end of file diff --git a/src/templates/admin/core/manager/topics/topic_list.html b/src/templates/admin/core/manager/topics/topic_list.html new file mode 100644 index 000000000..a400d5d76 --- /dev/null +++ b/src/templates/admin/core/manager/topics/topic_list.html @@ -0,0 +1,152 @@ +{% extends "admin/core/base.html" %} +{% load static %} +{% load foundation %} +{% load bool_fa %} + +{% get_current_language as LANGUAGE_CODE %} +{% get_language_info for LANGUAGE_CODE as language_info %} + +{% block title %}Topics{% endblock title %} +{% block title-section %}Topics{% endblock %} + +{% block breadcrumbs %} + {{ block.super }} +
  • Manager
  • +
  • Topics
  • +{% endblock %} + +{% block body %} +
    +
    +
    +

    Topics

    + Add New Topic +
    +
    +

    + {% blocktrans %} + Here you can create and edit the journal research topics that classifiable an article. They allow you to give better recommendations for assigning users to the article + {% endblocktrans %} +

    +

    + NB. {% blocktrans %}You cannot remove a topic related to existing articles or accounts. Remove articles and accounts from the topic if you want to delete it.{% endblocktrans %} +

    +
    + + +
    +
    + {% csrf_token %} + + + + + + + + + + + + + + {% for topic in topic_objects %} + + + + + + + + + + {% empty %} + {% endfor %} + +
    IDNameTopic Group# Accounts# ArticlesEditDelete
    {{ topic.pk }}{{ topic.pretty_name }}{{ topic.group.pretty_name }}{{ topic.account_count }}{{ topic.article_count }} + + Edit + + + +
    +
    +
    +
    +
    +
    +

    Topic Groups

    + Add New Group +
    +
    +

    + {% blocktrans %} + Here you can create and edit the journal research topic groups. + {% endblocktrans %} +

    +

    + NB. {% blocktrans %}You cannot remove a group related to existing topic. Remove the topics from the group if you want to delete it.{% endblocktrans %} +

    +
    + {% csrf_token %} + + + + + + + + + + + + {% for topic_group in topic_group_objects %} + + + + + + + + {% empty %} + {% endfor %} + +
    IDName# TopicsEditDelete
    {{ topic_group.pk }}{{ topic_group.pretty_name }}{{ topic_group.topic_count }} + + Edit + + + +
    +
    +
    +
    +
    +{% endblock body %} + +{% block js %} + + + +{% endblock js %} \ No newline at end of file diff --git a/src/templates/admin/elements/review/assigned_topics_table.html b/src/templates/admin/elements/review/assigned_topics_table.html new file mode 100644 index 000000000..3b4ace5a5 --- /dev/null +++ b/src/templates/admin/elements/review/assigned_topics_table.html @@ -0,0 +1,9 @@ +{% for topic in topics_by_type.primary %} +{{ topic }} +{% endfor %} +{% for topic in topics_by_type.secondary %} +{{ topic }} +{% endfor %} +{% if not topics_by_type.primary and not topics_by_type.secondary %} +No Topics +{% endif %} \ No newline at end of file From 5659d073b6bf2de58b9251e67fe1f4787af75616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20S=C3=A1nchez?= Date: Sat, 14 Dec 2024 18:48:06 -0300 Subject: [PATCH 07/11] add research topic article selections --- requirements.txt | 1 + src/core/include_urls.py | 1 + src/core/janeway_global_settings.py | 1 + src/submission/forms.py | 76 +++++++++++++++++++ src/submission/models.py | 17 +++++ .../admin/review/unassigned_article.html | 33 ++++++++ .../admin/submission/edit/metadata.html | 26 ++++++- .../admin/submission/submit_info.html | 30 +++++++- .../admin/submission/submit_review.html | 26 +++++++ 9 files changed, 207 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index afad73d19..67129aa22 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,6 +20,7 @@ django-settings-export==1.2.1 django-recaptcha==3.0.0 django-simple-math-captcha==2.0.1 django-simple-history==3.3.0 +django-select2==8.1.2 django-summernote==0.8.20.0 django-tinymce==3.7.1 djangorestframework==3.15.2 diff --git a/src/core/include_urls.py b/src/core/include_urls.py index c6d42397f..a3002ca51 100644 --- a/src/core/include_urls.py +++ b/src/core/include_urls.py @@ -47,6 +47,7 @@ path('workflow/', include('workflow.urls')), path('discussion/', include('discussion.urls')), path('oidc/', include('mozilla_django_oidc.urls')), + path('select2/', include('django_select2.urls')), # Root Site URLS re_path(r'^$', press_views.index, name='website_index'), diff --git a/src/core/janeway_global_settings.py b/src/core/janeway_global_settings.py index 7d3a1c837..4ebc6badc 100755 --- a/src/core/janeway_global_settings.py +++ b/src/core/janeway_global_settings.py @@ -92,6 +92,7 @@ # 3rd Party 'mozilla_django_oidc', + 'django_select2', 'django_summernote', 'tinymce', 'bootstrap4', diff --git a/src/submission/forms.py b/src/submission/forms.py index d8d4fc611..28a44eb08 100755 --- a/src/submission/forms.py +++ b/src/submission/forms.py @@ -6,6 +6,8 @@ import re from django import forms +from django.db import transaction +from django_select2.forms import Select2MultipleWidget from django.utils.translation import gettext, gettext_lazy as _ from submission import models @@ -125,6 +127,12 @@ def __init__(self, *args, **kwargs): license_queryset = license_queryset.filter( available_for_submission=self.FILTER_PUBLIC_FIELDS, ) + + enable_study_topics = article.journal.get_setting( + 'general', + 'enable_study_topics', + ) + self.fields['section'].queryset = section_queryset self.fields['license'].queryset = license_queryset @@ -142,6 +150,43 @@ def __init__(self, *args, **kwargs): if submission_summary: self.fields['non_specialist_summary'].required = True + + if enable_study_topics: + topics_queryset = core_models.Topics.objects.filter( + journal=article.journal, + ).order_by('group__pretty_name', 'pretty_name') + + self.fields['primary_study_topic'] = forms.ModelChoiceField( + queryset=topics_queryset, + widget=forms.Select, + required=True, + label=_('Primary Research Topic'), + initial=article.topics('PR').first(), + help_text='Main research topic of the article', + ) + + self.fields['secondary_study_topic'] = forms.ModelMultipleChoiceField( + queryset=topics_queryset, + widget=Select2MultipleWidget, + required=False, + label=_('Secondary Research Topic'), + initial=article.topics('SE'), + help_text='Anothers research topics related to the article', + ) + + study_topic_choices = [('', '---------')] + [ + ( + group.pretty_name, + [ + (topic.id, topic.pretty_name) + for topic in core_models.Topics.objects.filter(group=group).order_by('pretty_name') + ] + ) + for group in core_models.TopicGroup.objects.all() + ] + + self.fields['primary_study_topic'].choices = study_topic_choices + self.fields['secondary_study_topic'].choices = study_topic_choices # Pop fields based on journal.submissionconfiguration if journal and self.pop_disabled_fields: @@ -234,6 +279,37 @@ def save(self, commit=True, request=None): if commit: article.save() + if article.journal.get_setting('general','enable_study_topics'): + selected_primary_topic = self.cleaned_data['primary_study_topic'] + selected_secondary_topics = set(self.cleaned_data['secondary_study_topic']) + + existing_topics = models.ArticleTopic.objects.filter(article=article) + + with transaction.atomic(): + + for topic in selected_secondary_topics: + models.ArticleTopic.objects.update_or_create( + article=article, + topic=topic, + defaults={'topic_type': models.ArticleTopic.SECONDARY} + ) + + models.ArticleTopic.objects.update_or_create( + article=article, + topic=selected_primary_topic, + defaults={'topic_type': models.ArticleTopic.PRIMARY} + ) + + for article_topic in existing_topics.filter(topic_type=models.ArticleTopic.PRIMARY): + if article_topic.topic != selected_primary_topic: + article_topic.delete() + + for article_topic in existing_topics.filter(topic_type=models.ArticleTopic.SECONDARY): + if article_topic.topic not in selected_secondary_topics: + article_topic.delete() + + article.save() + return article diff --git a/src/submission/models.py b/src/submission/models.py index 774efa00b..595b48869 100755 --- a/src/submission/models.py +++ b/src/submission/models.py @@ -1372,6 +1372,23 @@ def issues_list(self): from journal import models as journal_models return journal_models.Issue.objects.filter(journal=self.journal, articles__in=[self]) + def topics(self, topic_type=None): + if topic_type: + return core_models.Topics.objects.filter(articletopic__article=self, articletopic__topic_type=topic_type) + return core_models.Topics.objects.filter(articletopic__article=self) + + def topics_by_type(self): + primary_topics = core_models.Topics.objects.filter( + articletopic__article=self, articletopic__topic_type=ArticleTopic.PRIMARY + ) + secondary_topics = core_models.Topics.objects.filter( + articletopic__article=self, articletopic__topic_type=ArticleTopic.SECONDARY + ) + return { + 'primary': primary_topics, + 'secondary': secondary_topics, + } + @cache(7200) def altmetrics(self): alm = self.altmetric_set.all() diff --git a/src/templates/admin/review/unassigned_article.html b/src/templates/admin/review/unassigned_article.html index 9218dada0..85918ac2a 100644 --- a/src/templates/admin/review/unassigned_article.html +++ b/src/templates/admin/review/unassigned_article.html @@ -89,6 +89,39 @@

    Summary of Article

    + {% if journal_settings.general.enable_study_topics %} +
    +
    +

    Research Topics

    +
    +
    + + + + + + {% for topic in article.topics_by_type.primary %} + + + + + {% endfor %} + {% for topic in article.topics_by_type.secondary %} + + + + + {% endfor %} + {% if not article.topics %} + + + + {% endif %} +
    TopicType
    {{ topic }}Primary
    {{ topic }}Secondary
    No topics
    +
    +
    + {% endif %} +

    Authors

    diff --git a/src/templates/admin/submission/edit/metadata.html b/src/templates/admin/submission/edit/metadata.html index 0dcf7f11e..0a7bd8dd0 100644 --- a/src/templates/admin/submission/edit/metadata.html +++ b/src/templates/admin/submission/edit/metadata.html @@ -89,6 +89,29 @@

    Edit Metadata

    {{ info_form.primary_issue|foundation }}
    + {% if journal_settings.general.enable_study_topics %} +
    +
    +
    +
    + + {{ info_form.primary_study_topic }} +

    {{ info_form.primary_study_topic.help_text}}

    +
    + +
    +
    +
    +
    +
    + + {{ info_form.secondary_study_topic }} +
    +

    {{ info_form.secondary_study_topic.help_text}}

    +
    +
    +
    + {% endif %}
    @@ -302,7 +325,8 @@
    Add funder
    - + {{ info_form.media.css }} + {{ info_form.media.js }} - + {{ form.media.js }} +{{ form.media.css }} +{{ form.media.js }} {% endblock %} diff --git a/src/templates/admin/elements/accounts/user_form.html b/src/templates/admin/elements/accounts/user_form.html index 5f7b7d637..82d6e9b85 100644 --- a/src/templates/admin/elements/accounts/user_form.html +++ b/src/templates/admin/elements/accounts/user_form.html @@ -33,6 +33,28 @@
    {% trans 'Core Data' %}
    {{ form.country|foundation }}
    +
    +
    +
    +
    +
    + + {{ form.primary_study_topic }} +
    +

    {{ form.primary_study_topic.help_text}}

    +
    +
    +
    +
    +
    + + {{ form.secondary_study_topic }} +
    +

    {{ form.secondary_study_topic.help_text}}

    +
    +
    +
    +

    Social Media and Accounts
    diff --git a/src/templates/common/elements/edit_profile_js_block.html b/src/templates/common/elements/edit_profile_js_block.html index aaa4f8653..8cc70dc41 100644 --- a/src/templates/common/elements/edit_profile_js_block.html +++ b/src/templates/common/elements/edit_profile_js_block.html @@ -17,3 +17,6 @@ {% endif %} + +{{ form.media.css }} +{{ form.media.js }} diff --git a/src/themes/OLH/templates/elements/accounts/user_form.html b/src/themes/OLH/templates/elements/accounts/user_form.html index 98f3133d5..b5dff7d9e 100644 --- a/src/themes/OLH/templates/elements/accounts/user_form.html +++ b/src/themes/OLH/templates/elements/accounts/user_form.html @@ -37,6 +37,28 @@
    {% trans 'Core Data' %}
    {{ form.preferred_timezone|foundation }}
    +
    +
    +
    +
    +
    + + {{ form.primary_study_topic }} +
    +

    {{ form.primary_study_topic.help_text}}

    +
    +
    +
    +
    +
    + + {{ form.secondary_study_topic }} +
    +

    {{ form.secondary_study_topic.help_text}}

    +
    +
    +
    +

    {% trans 'Social Media and Accounts' %}
    diff --git a/src/themes/clean/templates/elements/accounts/user_form.html b/src/themes/clean/templates/elements/accounts/user_form.html index 88c03c13b..3385e533f 100644 --- a/src/themes/clean/templates/elements/accounts/user_form.html +++ b/src/themes/clean/templates/elements/accounts/user_form.html @@ -34,6 +34,14 @@
    {% trans 'Core Data' %}
    {% bootstrap_field form.preferred_timezone %}
    +
    +
    + {% bootstrap_field form.primary_study_topic %} +
    +
    + {% bootstrap_field form.secondary_study_topic %} +
    +

    {% trans 'Social Media and Accounts' %}
    diff --git a/src/themes/material/templates/elements/accounts/user_form.html b/src/themes/material/templates/elements/accounts/user_form.html index 41a2fc7d9..3d0c9efb1 100644 --- a/src/themes/material/templates/elements/accounts/user_form.html +++ b/src/themes/material/templates/elements/accounts/user_form.html @@ -35,6 +35,14 @@
    {% trans "Core Data" %}
    {% bootstrap_field form.preferred_timezone %}
    +
    +
    + {% bootstrap_field form.primary_study_topic %} +
    +
    + {% bootstrap_field form.secondary_study_topic %} +
    +

    {% trans "Social Media and Accounts" %}
    From 3c167263de5ff09e82d0a7b0390633c9f6408573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20S=C3=A1nchez?= Date: Sun, 15 Dec 2024 07:43:39 -0300 Subject: [PATCH 09/11] update article and account models with study_topic field --- .../migrations/0101_account_study_topic.py | 18 +++++++++++++++++ src/core/models.py | 1 + .../migrations/0086_article_study_topic.py | 20 +++++++++++++++++++ src/submission/models.py | 1 + 4 files changed, 40 insertions(+) create mode 100644 src/core/migrations/0101_account_study_topic.py create mode 100644 src/submission/migrations/0086_article_study_topic.py diff --git a/src/core/migrations/0101_account_study_topic.py b/src/core/migrations/0101_account_study_topic.py new file mode 100644 index 000000000..de2081381 --- /dev/null +++ b/src/core/migrations/0101_account_study_topic.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2024-12-15 10:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0100_topicgroup_topics_accounttopic'), + ] + + operations = [ + migrations.AddField( + model_name='account', + name='study_topic', + field=models.ManyToManyField(blank=True, null=True, through='core.AccountTopic', to='core.topics'), + ), + ] diff --git a/src/core/models.py b/src/core/models.py index 791636256..ca79b1c34 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -300,6 +300,7 @@ class Account(AbstractBaseUser, PermissionsMixin): verbose_name=_("Signature"), ) interest = models.ManyToManyField('Interest', null=True, blank=True) + study_topic = models.ManyToManyField('Topics', through='AccountTopic', null=True, blank=True) country = models.ForeignKey( Country, null=True, diff --git a/src/submission/migrations/0086_article_study_topic.py b/src/submission/migrations/0086_article_study_topic.py new file mode 100644 index 000000000..d8e6dae4d --- /dev/null +++ b/src/submission/migrations/0086_article_study_topic.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.16 on 2024-12-15 10:41 + +import core.model_utils +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0101_account_study_topic'), + ('submission', '0085_articletopic'), + ] + + operations = [ + migrations.AddField( + model_name='article', + name='study_topic', + field=models.ManyToManyField(blank=True, null=True, related_name='study_topics', through='submission.ArticleTopic', to='core.topics'), + ), + ] diff --git a/src/submission/models.py b/src/submission/models.py index 595b48869..b9d70674b 100755 --- a/src/submission/models.py +++ b/src/submission/models.py @@ -676,6 +676,7 @@ def jats_article_type(self): "of interests in the publication of this " "article please state them here.", ) + study_topic = models.ManyToManyField('core.Topics', through='ArticleTopic', blank=True, null=True, related_name='study_topics') rights = JanewayBleachField( blank=True, null=True, help_text="A custom statement on the usage rights for this article" From 985e9b18b8337d6061a1ab46ab1eca4c06d133fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20S=C3=A1nchez?= Date: Sun, 15 Dec 2024 07:47:05 -0300 Subject: [PATCH 10/11] update editor assignment based on research topic recommendations --- src/review/logic.py | 72 +++++++++++++++++++ .../admin/elements/info_tooltip.html | 5 ++ .../review/add_editor_table_custom_row.html | 8 ++- .../review/research_topic_switch.html | 12 ++++ .../admin/review/add_editor_assignment.html | 23 +++++- 5 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 src/templates/admin/elements/info_tooltip.html create mode 100644 src/templates/admin/elements/review/research_topic_switch.html diff --git a/src/review/logic.py b/src/review/logic.py index dc33eb68e..a1d158720 100755 --- a/src/review/logic.py +++ b/src/review/logic.py @@ -77,6 +77,78 @@ def get_editors(article, candidate_queryset, exclude_pks): ) order_by.append('active_assignments_count') + if article.journal.get_setting('general','enable_study_topics'): + primary_to_primary_matches = core_models.AccountTopic.objects.filter( + account=OuterRef("id"), + topic_type=core_models.AccountTopic.PRIMARY, + topic__in=article.study_topic.filter(articletopic__topic_type=submission_models.ArticleTopic.PRIMARY) + ).values( + "account_id", + ).annotate( + match_count=Count("account_id"), + ).values("match_count") + + primary_to_secondary_matches = core_models.AccountTopic.objects.filter( + account=OuterRef("id"), + topic_type=core_models.AccountTopic.PRIMARY, + topic__in=article.study_topic.filter(articletopic__topic_type=submission_models.ArticleTopic.SECONDARY) + ).values( + "account_id", + ).annotate( + match_count=Count("account_id"), + ).values("match_count") + + secondary_to_primary_matches = core_models.AccountTopic.objects.filter( + account=OuterRef("id"), + topic_type=core_models.AccountTopic.SECONDARY, + topic__in=article.study_topic.filter(articletopic__topic_type=submission_models.ArticleTopic.PRIMARY) + ).values( + "account_id", + ).annotate( + match_count=Count("account_id"), + ).values("match_count") + + secondary_to_secondary_matches = core_models.AccountTopic.objects.filter( + account=OuterRef("id"), + topic_type=core_models.AccountTopic.SECONDARY, + topic__in=article.study_topic.filter(articletopic__topic_type=submission_models.ArticleTopic.SECONDARY) + ).values( + "account_id", + ).annotate( + match_count=Count("account_id"), + ).values("match_count") + + editors = editors.annotate( + primary_to_primary_matches=Subquery( + primary_to_primary_matches, + output_field=IntegerField(), + ), + primary_to_secondary_matches=Subquery( + primary_to_secondary_matches, + output_field=IntegerField(), + ), + secondary_to_primary_matches=Subquery( + secondary_to_primary_matches, + output_field=IntegerField(), + ), + secondary_to_secondary_matches=Subquery( + secondary_to_secondary_matches, + output_field=IntegerField(), + ) + ).annotate( + primary_to_primary_matches_weighted=Coalesce(F('primary_to_primary_matches'), Value(0)) * 3, + primary_to_secondary_matches_weighted=Coalesce(F('primary_to_secondary_matches'), Value(0)) * 2, + secondary_to_primary_matches_weighted=Coalesce(F('secondary_to_primary_matches'), Value(0)) * 2, + secondary_to_secondary_matches_weighted=Coalesce(F('secondary_to_secondary_matches'), Value(0)) * 1, + total_topic_matches=( + F('primary_to_primary_matches_weighted') + + F('primary_to_secondary_matches_weighted') + + F('secondary_to_primary_matches_weighted') + + F('secondary_to_secondary_matches_weighted') + ) + ) + order_by.append('-total_topic_matches') + editors = editors.order_by(*order_by) return editors diff --git a/src/templates/admin/elements/info_tooltip.html b/src/templates/admin/elements/info_tooltip.html new file mode 100644 index 000000000..c582ff2ba --- /dev/null +++ b/src/templates/admin/elements/info_tooltip.html @@ -0,0 +1,5 @@ +{{ label_text }}  + \ No newline at end of file diff --git a/src/templates/admin/elements/review/add_editor_table_custom_row.html b/src/templates/admin/elements/review/add_editor_table_custom_row.html index 6a6ad2410..30536902c 100644 --- a/src/templates/admin/elements/review/add_editor_table_custom_row.html +++ b/src/templates/admin/elements/review/add_editor_table_custom_row.html @@ -9,7 +9,13 @@ {{ editor.email }} {{ editor_type_label }} {{ editor.active_assignments_count|default_if_none:0 }} - + {% for interest in editor.interest.all %}{{ interest.name }}{% if not forloop.last %}, {% endif %}{% endfor %} + + {% include "admin/elements/review/assigned_topics_table.html" with topics_by_type=editor.topics_by_type %} + + + {{ editor.total_topic_matches}} + \ No newline at end of file diff --git a/src/templates/admin/elements/review/research_topic_switch.html b/src/templates/admin/elements/review/research_topic_switch.html new file mode 100644 index 000000000..69b9f9758 --- /dev/null +++ b/src/templates/admin/elements/review/research_topic_switch.html @@ -0,0 +1,12 @@ +{% load static %} + \ No newline at end of file diff --git a/src/templates/admin/review/add_editor_assignment.html b/src/templates/admin/review/add_editor_assignment.html index f1fa8730f..034a8c2f4 100644 --- a/src/templates/admin/review/add_editor_assignment.html +++ b/src/templates/admin/review/add_editor_assignment.html @@ -41,6 +41,20 @@

    Select Editor

    + {% if journal_settings.general.enable_study_topics %} +
    + +
    + + +
    +
    +

    View Research Topics

    +
    + {% endif %} + @@ -49,7 +63,11 @@

    Select Editor

    - + + + @@ -119,6 +137,9 @@

     Add New Editor

    {% include "admin/elements/open_modal.html" with target=form.modal.id %} {% endif %} {% include "elements/datatables.html" with target="#enrolluser" %} + {% if journal_settings.general.enable_study_topics %} + {% include "elements/review/research_topic_switch.html" %} + {% endif %}
    Email Address Type Active AssignmentsInterests