Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TGDK][Feature] Add Editor Invitation System to Extend Editor Assignment Workflow #4556

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
12 changes: 12 additions & 0 deletions src/core/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,10 @@ def get_settings_to_edit(display_group, journal, user):
'name': 'default_review_days',
'object': setting_handler.get_setting('general', 'default_review_days', journal),
},
{
'name': 'default_editor_assignment_request_days',
'object': setting_handler.get_setting('general', 'default_editor_assignment_request_days', journal),
},
{
'name': 'enable_save_review_progress',
'object': setting_handler.get_setting('general', 'enable_save_review_progress', journal),
Expand All @@ -363,6 +367,10 @@ def get_settings_to_edit(display_group, journal, user):
'name': 'draft_decisions',
'object': setting_handler.get_setting('general', 'draft_decisions', journal),
},
{
'name': 'enable_invite_editor',
'object': setting_handler.get_setting('general', 'enable_invite_editor', journal),
},
{
'name': 'default_review_form',
'object': setting_handler.get_setting('general', 'default_review_form', journal),
Expand Down Expand Up @@ -424,6 +432,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'

Expand Down
11 changes: 11 additions & 0 deletions src/events/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ class Events:
# kwargs: editor_assignment, request, email_data, acknowledgement (true), skip (boolean)
# raised when an editor is manually assigned to an article(or skip the acknowledgement)
ON_EDITOR_MANUALLY_ASSIGNED = 'on_editor_manually_assigned'
# kwargs: editor_assignment, request, email_data, acknowledgement (true), skip (boolean)
# raised when an editor decides to notify to another editor with a custom message or skipped the email
ON_EDITOR_REQUESTED_NOTIFICATION = 'on_editor_requested_notification'
ON_EDITOR_REQUEST_REMINDED = 'on_editor_request_reminded'
# kwargs: review_assignment, request, user_message_content, skip (boolean)
# raised when an editor decides to notify the reviewer of a assignment withdrawl (or skip the notification)
ON_EDITOR_REQUEST_WITHDRAWL = 'on_editor_request_withdrawl'
# kwargs: editor_assignment, request, accepted (boolean)
# raised when an editor accepts or declines to assignment request
ON_EDITOR_ASSIGNMENT_ACCEPTED = 'on_editor_assignment_accepted'
ON_EDITOR_ASSIGNMENT_DECLINED = 'on_editor_assignment_declined'

# kwargs: request, editor_assignment, user_message_content (will be blank), acknowledgement (false)
# raised when an editor is assigned to an article
Expand Down
13 changes: 11 additions & 2 deletions src/events/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,18 @@
transactional_emails.send_editor_assigned_acknowledgements)
event_logic.Events.register_for_event(event_logic.Events.ON_EDITOR_MANUALLY_ASSIGNED,
transactional_emails.send_editor_manually_assigned)
event_logic.Events.register_for_event(event_logic.Events.ON_EDITOR_REQUESTED_NOTIFICATION,
transactional_emails.send_editor_assignment_requested)
event_logic.Events.register_for_event(event_logic.Events.ON_EDITOR_REQUEST_REMINDED,
transactional_emails.send_editor_assignment_reminder)
event_logic.Events.register_for_event(event_logic.Events.ON_EDITOR_REQUEST_WITHDRAWL,
transactional_emails.send_editor_assignment_withdrawl)
event_logic.Events.register_for_event(event_logic.Events.ON_ARTICLE_UNASSIGNED,
transactional_emails.send_editor_unassigned_notice)

transactional_emails.send_editor_unassigned_notice)
event_logic.Events.register_for_event(event_logic.Events.ON_EDITOR_ASSIGNMENT_ACCEPTED,
transactional_emails.send_editor_assign_accepted_or_decline_acknowledgements)
event_logic.Events.register_for_event(event_logic.Events.ON_EDITOR_ASSIGNMENT_DECLINED,
transactional_emails.send_editor_assign_accepted_or_decline_acknowledgements)
# Review
event_logic.Events.register_for_event(event_logic.Events.ON_REVIEWER_REQUESTED_NOTIFICATION,
transactional_emails.send_reviewer_requested)
Expand Down
75 changes: 75 additions & 0 deletions src/review/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,81 @@ 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')
self.invite_editor = kwargs.pop('invite_editor', False)

super(EditorAssignmentForm, self).__init__(*args, **kwargs)

default_due = setting_handler.get_setting(
'general',
'default_review_days',
self.journal,
create=True,
).value

if default_due:
due_date = timezone.now() + timedelta(days=int(default_due))
self.fields['date_due'].initial = due_date
self.fields['date_due'].required = False

if self.editors:
self.fields['editor'].queryset = self.editors

def clean(self):
cleaned_data = super().clean()
if self.invite_editor and not cleaned_data.get('date_due'):
self.add_error('date_due', 'This field is required for inviting an editor.')
return cleaned_data

def save(self, commit=True, request=None):
editor = self.cleaned_data['editor']
date_due = self.cleaned_data['date_due']

if request:
if self.invite_editor:
editor_assignment = models.EditorAssignmentRequest(
article=self.article,
editor=editor,
date_due=date_due,
)
editor_assignment.requesting_editor = request.user
else:
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 EditEditorAssignmentForm(forms.ModelForm):
class Meta:
model = models.EditorAssignmentRequest
fields = ('date_due',)
widgets = {
'date_due': HTMLDateInput,
}

def __init__(self, *args, **kwargs):
self.journal = kwargs.pop('journal', None)
super(EditEditorAssignmentForm, self).__init__(*args, **kwargs)


class BulkReviewAssignmentForm(forms.ModelForm):
template = forms.CharField(
widget=TinyMCE,
Expand Down
104 changes: 104 additions & 0 deletions src/review/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -41,6 +44,77 @@
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
"""
editor_assignment_requests = article.editorassignmentrequest_set.filter(
Q(is_complete=False) &
Q(article__stage__in=submission_models.EDITOR_REVIEW_STAGES) &
Q(date_accepted__isnull=True) &
Q(date_declined__isnull=True)
)
editors = article.editorassignment_set.all()
editor_pks_to_exclude = [assignment.editor.pk for assignment in editor_assignment_requests]
editor_pks_to_exclude = 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',
Expand Down Expand Up @@ -225,6 +299,27 @@ def get_article_details_for_review(article):
return mark_safe(detail_string)


def get_editor_notification_context(
request, article, editor,
editor_assignment,
):
review_unassigned_url = request.journal.site_url(path=reverse(
'review_unassigned_article', kwargs={'article_id': article.id}
))

article_details = get_article_details_for_review(article)

email_context = {
'article': article,
'editor': editor,
'editor_assignment': editor_assignment,
'review_unassigned_url': review_unassigned_url,
'article_details': article_details,
}

return email_context


def get_reviewer_notification_context(
request, article, editor,
review_assignment,
Expand Down Expand Up @@ -626,6 +721,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
Expand Down
36 changes: 36 additions & 0 deletions src/review/migrations/0024_editorassignmentrequest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Generated by Django 4.2.16 on 2024-12-04 08:50

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('submission', '0084_remove_article_jats_article_type_and_more'),
('review', '0023_auto_20240312_0922'),
]

operations = [
migrations.CreateModel(
name='EditorAssignmentRequest',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('editor_type', models.CharField(choices=[('editor', 'Editor'), ('section-editor', 'Section Editor')], max_length=20)),
('notified', models.BooleanField(default=False)),
('date_requested', models.DateTimeField(auto_now_add=True, null=True)),
('date_due', models.DateField(null=True)),
('date_accepted', models.DateTimeField(blank=True, null=True)),
('date_declined', models.DateTimeField(blank=True, null=True)),
('date_complete', models.DateTimeField(blank=True, null=True)),
('date_reminded', models.DateField(blank=True, null=True)),
('is_complete', models.BooleanField(default=False)),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='submission.article')),
('editor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('editor_assignment', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='review.editorassignment')),
('requesting_editor', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='requesting_editor', to=settings.AUTH_USER_MODEL)),
],
),
]
37 changes: 37 additions & 0 deletions src/review/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,43 @@ class Meta:
unique_together = ('article', 'editor')


class EditorAssignmentRequest(models.Model):

article = models.ForeignKey(
'submission.Article',
on_delete=models.CASCADE,
)
editor = models.ForeignKey(
'core.Account',
on_delete=models.CASCADE,
)
requesting_editor = models.ForeignKey(
'core.Account',
on_delete=models.CASCADE,
related_name='requesting_editor',
null=True,
)

editor_assignment = models.ForeignKey(
EditorAssignment,
on_delete=models.CASCADE,
null=True,
)

editor_type = models.CharField(max_length=20, choices=assignment_choices)
notified = models.BooleanField(default=False)

# Dates
date_requested = models.DateTimeField(auto_now_add=True, null=True)
date_due = models.DateField(null=True)
date_accepted = models.DateTimeField(blank=True, null=True)
date_declined = models.DateTimeField(blank=True, null=True)
date_complete = models.DateTimeField(blank=True, null=True)
date_reminded = models.DateField(blank=True, null=True)

is_complete = models.BooleanField(default=False)


class ReviewRound(models.Model):
article = models.ForeignKey(
'submission.Article',
Expand Down
10 changes: 10 additions & 0 deletions src/review/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
re_path(r'^unassigned/article/(?P<article_id>\d+)/notify/(?P<editor_id>\d+)/$', views.assignment_notification,
name='review_assignment_notification'),
re_path(r'^unassigned/article/(?P<article_id>\d+)/move/review/$', views.move_to_review, name='review_move_to_review'),
re_path(r'^article/(?P<article_id>\d+)/editor/add/$', views.add_editor_assignment, name='add_editor_assignment'),
re_path(r'^article/(?P<article_id>\d+)/editor/invite/(?P<editor_assignment_id>\d+)/notify/$', views.notify_invite_editor,
name='notify_invite_editor_assignment'),
re_path(r'^article/(?P<article_id>\d+)/crosscheck/$', views.view_ithenticate_report, name='review_crosscheck'),
re_path(r'^article/(?P<article_id>\d+)/move/(?P<decision>accept|decline|undecline)/$', views.review_decision,
name='review_decision'),
Expand Down Expand Up @@ -101,6 +104,13 @@
views.upload_review_file,
name='upload_review_file'),

re_path(r'^requests/editor/$', views.editor_assignment_requests, name='editor_assignment_requests'),
re_path(r'^requests/editor/(?P<assignment_id>\d+)/accept/$', views.accept_editor_assignment_request, name='accept_editor_assignment'),
re_path(r'^requests/editor/(?P<assignment_id>\d+)/decline/$', views.decline_editor_assignment_request, name='decline_editor_assignment'),
re_path(r'^requests/editor/(?P<assignment_id>\d+)/delete/$', views.delete_editor_assignment_request, name='delete_editor_assignment'),
re_path(r'^requests/editor/(?P<assignment_id>\d+)/edit/$', views.edit_editor_assignment_request, name='edit_editor_assignment'),
re_path(r'^requests/editor/(?P<assignment_id>\d+)/withdraw/$', views.withdraw_editor_assignment_request, name='withdraw_editor_assignment'),
re_path(r'^requests/editor/(?P<assignment_id>\d+)/reminder/$', views.remind_editor_assignment_request, name='remind_editor_assignment'),

re_path(r'^author/(?P<article_id>\d+)/$', views.author_view_reviews, name='review_author_view'),

Expand Down
Loading