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/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/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/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..f1fa8730f --- /dev/null +++ b/src/templates/admin/review/add_editor_assignment.html @@ -0,0 +1,152 @@ +{% 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 %}
    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" + ] } ]