From f7056c1430d490fc65d61d272516f66de63cc8d1 Mon Sep 17 00:00:00 2001 From: Jorran de Wit <jorrandewit@outlook.com> Date: Sat, 15 Apr 2017 12:47:36 +0200 Subject: [PATCH] Centralize the Submission cycle handling All required actions are determined and saved in `submission.cycle` now. The actions required are determined at initialization of the submission. Different cycles now possibly can have different required actions. All template logic to determine required actions is removed from the codebase. --- comments/managers.py | 5 + comments/models.py | 2 +- submissions/constants.py | 3 +- submissions/managers.py | 15 +- submissions/models.py | 31 +++-- .../submissions/_submission_card_in_pool.html | 6 +- .../_submission_refereeing_status.html | 2 +- .../templates/submissions/assignments.html | 13 +- .../templates/submissions/editorial_page.html | 27 ++-- .../templatetags/submissions_extras.py | 55 -------- submissions/utils.py | 129 +++++++++++++++++- submissions/views.py | 4 +- 12 files changed, 193 insertions(+), 99 deletions(-) diff --git a/comments/managers.py b/comments/managers.py index 2875a7030..823b4c0d7 100644 --- a/comments/managers.py +++ b/comments/managers.py @@ -1,6 +1,11 @@ from django.db import models +from .constants import STATUS_PENDING + class CommentManager(models.Manager): def vetted(self): return self.filter(status__gte=1) + + def awaiting_vetting(self): + return self.filter(status=STATUS_PENDING) diff --git a/comments/models.py b/comments/models.py index a6f5f0997..fd52703d2 100644 --- a/comments/models.py +++ b/comments/models.py @@ -24,7 +24,7 @@ class Comment(TimeStampedModel): commentary = models.ForeignKey('commentaries.Commentary', blank=True, null=True, on_delete=models.CASCADE) submission = models.ForeignKey('submissions.Submission', blank=True, null=True, - on_delete=models.CASCADE) + on_delete=models.CASCADE, related_name='comments') thesislink = models.ForeignKey('theses.ThesisLink', blank=True, null=True, on_delete=models.CASCADE) is_author_reply = models.BooleanField(default=False) diff --git a/submissions/constants.py b/submissions/constants.py index 148d94f70..44b7a24f2 100644 --- a/submissions/constants.py +++ b/submissions/constants.py @@ -1,5 +1,6 @@ STATUS_UNASSIGNED = 'unassigned' STATUS_RESUBMISSION_SCREENING = 'resubmitted_incomin' +STATUS_REVISION_REQUESTED = 'revision_requested' SUBMISSION_STATUS = ( (STATUS_UNASSIGNED, 'Unassigned, undergoing pre-screening'), (STATUS_RESUBMISSION_SCREENING, 'Resubmission incoming, undergoing pre-screening'), @@ -7,7 +8,7 @@ SUBMISSION_STATUS = ( ('EICassigned', 'Editor-in-charge assigned, manuscript under review'), ('review_closed', 'Review period closed, editorial recommendation pending'), # If revisions required: resubmission creates a new Submission object - ('revision_requested', 'Editor-in-charge has requested revision'), + (STATUS_REVISION_REQUESTED, 'Editor-in-charge has requested revision'), ('resubmitted', 'Has been resubmitted'), ('resubmitted_and_rejected', 'Has been resubmitted and subsequently rejected'), ('resubmitted_and_rejected_visible', diff --git a/submissions/managers.py b/submissions/managers.py index 2137c6f06..8fdfc9d7f 100644 --- a/submissions/managers.py +++ b/submissions/managers.py @@ -2,7 +2,8 @@ from django.db import models from django.db.models import Q from .constants import SUBMISSION_STATUS_OUT_OF_POOL, SUBMISSION_STATUS_PUBLICLY_UNLISTED,\ - SUBMISSION_STATUS_PUBLICLY_INVISIBLE + SUBMISSION_STATUS_PUBLICLY_INVISIBLE, STATUS_UNVETTED, STATUS_VETTED,\ + STATUS_UNCLEAR, STATUS_INCORRECT, STATUS_NOT_USEFUL, STATUS_NOT_ACADEMIC class SubmissionManager(models.Manager): @@ -51,3 +52,15 @@ class EICRecommendationManager(models.Manager): related submission. """ return self.filter(submission__authors=user.contributor).filter(**kwargs) + + +class ReportManager(models.Manager): + def accepted(self): + return self.filter(status__gte=STATUS_VETTED) + + def awaiting_vetting(self): + return self.filter(status=STATUS_UNVETTED) + + def rejected(self): + return self.filter(status__in=[STATUS_UNCLEAR, STATUS_INCORRECT, + STATUS_NOT_USEFUL, STATUS_NOT_ACADEMIC]) diff --git a/submissions/models.py b/submissions/models.py index 8838a5f4a..48b71c13d 100644 --- a/submissions/models.py +++ b/submissions/models.py @@ -10,7 +10,8 @@ from .constants import ASSIGNMENT_REFUSAL_REASONS, ASSIGNMENT_NULLBOOL,\ RANKING_CHOICES, REPORT_REC, SUBMISSION_STATUS, STATUS_UNASSIGNED,\ REPORT_STATUSES, STATUS_UNVETTED, STATUS_RESUBMISSION_SCREENING,\ SUBMISSION_CYCLES, CYCLE_DEFAULT, CYCLE_SHORT, CYCLE_DIRECT_REC -from .managers import SubmissionManager, EditorialAssignmentManager, EICRecommendationManager +from .managers import SubmissionManager, EditorialAssignmentManager, EICRecommendationManager,\ + ReportManager from .utils import ShortSubmissionCycle, DirectRecommendationSubmissionCycle,\ GeneralSubmissionCycle @@ -128,7 +129,7 @@ class Submission(ArxivCallable, models.Model): self.copy_authors_from_previous_version() self.copy_EIC_from_previous_version() self.set_resubmission_defaults() - self.update_status(STATUS_RESUBMISSION_SCREENING) + self.status = STATUS_RESUBMISSION_SCREENING else: self.authors.add(self.submitted_by) @@ -152,11 +153,6 @@ class Submission(ArxivCallable, models.Model): ) assignment.save() - def update_status(self, status_code): - if status_code in SUBMISSION_STATUS: - self.status = status_code - self.save() - def set_resubmission_defaults(self): self.open_for_reporting = True self.open_for_commenting = True @@ -194,13 +190,13 @@ class Submission(ArxivCallable, models.Model): # Underneath: All very inefficient methods as they initiate a new query def count_accepted_invitations(self): - return self.refereeinvitation_set.filter(accepted=True).count() + return self.referee_invitations.filter(accepted=True).count() def count_declined_invitations(self): - return self.refereeinvitation_set.filter(accepted=False).count() + return self.referee_invitations.filter(accepted=False).count() def count_pending_invitations(self): - return self.refereeinvitation_set.filter(accepted=None).count() + return self.referee_invitations.filter(accepted=None).count() def count_invited_reports(self): return self.reports.filter(status=1, invited=True).count() @@ -243,9 +239,10 @@ class EditorialAssignment(models.Model): class RefereeInvitation(models.Model): - submission = models.ForeignKey('submissions.Submission', on_delete=models.CASCADE) - referee = models.ForeignKey('scipost.Contributor', related_name='referee', blank=True, null=True, - on_delete=models.CASCADE) + submission = models.ForeignKey('submissions.Submission', on_delete=models.CASCADE, + related_name='referee_invitations') + referee = models.ForeignKey('scipost.Contributor', related_name='referee', blank=True, + null=True, on_delete=models.CASCADE) # Why is this blank/null=True title = models.CharField(max_length=4, choices=TITLE_CHOICES) first_name = models.CharField(max_length=30, default='') last_name = models.CharField(max_length=30, default='') @@ -269,6 +266,12 @@ class RefereeInvitation(models.Model): self.submission.title[:30] + ' by ' + self.submission.author_list[:30] + ', invited on ' + self.date_invited.strftime('%Y-%m-%d')) + @property + def referee_str(self): + if self.referee: + return str(self.referee) + return self.last_name + ', ' + self.first_name + ########### # Reports: @@ -310,6 +313,8 @@ class Report(models.Model): verbose_name='optional remarks for the Editors only') anonymous = models.BooleanField(default=True, verbose_name='Publish anonymously') + objects = ReportManager() + def __str__(self): return (self.author.user.first_name + ' ' + self.author.user.last_name + ' on ' + self.submission.title[:50] + ' by ' + self.submission.author_list[:50]) diff --git a/submissions/templates/submissions/_submission_card_in_pool.html b/submissions/templates/submissions/_submission_card_in_pool.html index 9b5c3d28f..f72279cdf 100644 --- a/submissions/templates/submissions/_submission_card_in_pool.html +++ b/submissions/templates/submissions/_submission_card_in_pool.html @@ -21,12 +21,12 @@ {% get_obj_perms request.user for submission as "sub_perms" %} {% if "can_take_editorial_actions" in sub_perms or is_ECAdmin %} - {% if submission|required_actions %} + {% if submission.cycle.get_required_actions %} <div class="required-actions"> <h3 class="pt-0">Required actions:</h3> <ul> - {% for todoitem in submission|required_actions %} - <li>{{ todoitem }}</li> + {% for action in submission.cycle.get_required_actions %} + <li>{{action.1}}</li> {% endfor %} </ul> </div> diff --git a/submissions/templates/submissions/_submission_refereeing_status.html b/submissions/templates/submissions/_submission_refereeing_status.html index 68736208c..7e059bd73 100644 --- a/submissions/templates/submissions/_submission_refereeing_status.html +++ b/submissions/templates/submissions/_submission_refereeing_status.html @@ -1,4 +1,4 @@ <div class="card-block"> - <p class="card-text">Nr referees invited: {{submission.refereeinvitation_set.count}} <span>[{{submission.count_accepted_invitations}} acccepted / {{submission.count_declined_invitations}} declined / {{submission.count_pending_invitations}} response pending]</span></p> + <p class="card-text">Nr referees invited: {{submission.referee_invitations.count}} <span>[{{submission.count_accepted_invitations}} acccepted / {{submission.count_declined_invitations}} declined / {{submission.count_pending_invitations}} response pending]</span></p> <p class="card-text">Nr reports obtained: {{submission.count_obtained_reports}} [{{submission.count_invited_reports}} invited / {{submission.count_contrib_reports}} contributed], nr refused: {{submission.count_refused_resports}}, nr awaiting vetting: {{submission.count_awaiting_vetting}}</p> </div> diff --git a/submissions/templates/submissions/assignments.html b/submissions/templates/submissions/assignments.html index ed4ba37f6..8ca304e55 100644 --- a/submissions/templates/submissions/assignments.html +++ b/submissions/templates/submissions/assignments.html @@ -1,9 +1,5 @@ {% extends 'scipost/base.html' %} -{% load guardian_tags %} -{% load scipost_extras %} -{% load submissions_extras %} - {% block pagetitle %}: Assignments{% endblock pagetitle %} {% block content %} @@ -22,7 +18,6 @@ $(document).ready(function(){ } }); }); - </script> {% if assignments_to_consider %} @@ -60,14 +55,14 @@ $(document).ready(function(){ <li class="list-group-item"> {% include 'submissions/_submission_card_fellow_content.html' with submission=assignment.submission %} <div class="card-block"> - {% with actions=assignment.submission|required_actions %} + {% with actions=assignment.submission.cycle.get_required_actions %} <div class="required-actions{% if not actions %} no-actions{% endif %}"> <h3>{% if actions %}Required actions{% else %}No required actions{% endif %}</h3> {% if actions %} <ul> - {% for todoitem in assignment.submission|required_actions %} - <li>{{ todoitem }}</li> - {% endfor %} + {% for action in actions %} + <li>{{action.1}}</li> + {% endfor %} </ul> {% endif %} </div> diff --git a/submissions/templates/submissions/editorial_page.html b/submissions/templates/submissions/editorial_page.html index ef29d3600..c5cc177ff 100644 --- a/submissions/templates/submissions/editorial_page.html +++ b/submissions/templates/submissions/editorial_page.html @@ -3,7 +3,6 @@ {% block pagetitle %}: editorial page for submission{% endblock pagetitle %} {% load scipost_extras %} -{% load submissions_extras %} {% load bootstrap %} {% block breadcrumb_items %} @@ -101,12 +100,12 @@ <div class="row"> <div class="col-md-10 col-lg-8"> - <div class="card {% if submission|required_actions %}card-danger text-white{% else %}card-outline-success text-success{% endif %}"> + <div class="card {% if submission.cycle.get_required_actions %}card-danger text-white{% else %}card-outline-success text-success{% endif %}"> <div class="card-block"> <h3 class="card-title pt-0">Required actions:</h3> <ul class="mb-0"> - {% for todoitem in submission|required_actions %} - <li>{{ todoitem }}</li> + {% for action in submission.cycle.get_required_actions %} + <li>{{action.1}}</li> {% empty %} <li>No actions required</li> {% endfor %} @@ -151,10 +150,22 @@ {% if submission.status == 'resubmitted_incomin' %} <div class="row"> <div class="col-12"> - <div class="card"> - <div class="card-block"> - <h3 class="card-title">This submission is a resubmission, please choose which submission cycle to proceed with</h3> - {{submission.cycle}} + <h3 class="highlight">This submission is a resubmission, please choose which submission cycle to proceed with</h3> + <div class="card-deck"> + <div class="card"> + <div class="card-block"> + HENK + </div> + </div> + <div class="card"> + <div class="card-block"> + INGRID + </div> + </div> + <div class="card"> + <div class="card-block"> + DE BUURVROUW + </div> </div> </div> </div> diff --git a/submissions/templatetags/submissions_extras.py b/submissions/templatetags/submissions_extras.py index 2da991f6d..83a792a91 100644 --- a/submissions/templatetags/submissions_extras.py +++ b/submissions/templatetags/submissions_extras.py @@ -24,58 +24,3 @@ def is_viewable_by_authors(recommendation): return recommendation.submission.status in ['revision_requested', 'resubmitted', 'accepted', 'rejected', 'published', 'withdrawn'] - - -@register.filter(name='required_actions') -def required_actions(submission): - """ - This method returns a list of required actions on a Submission. - Each list element is a textual statement. - """ - if (submission.status in SUBMISSION_STATUS_OUT_OF_POOL - or submission.status == 'revision_requested' - or submission.eicrecommendation_set.exists()): - return [] - todo = [] - for comment in submission.comment_set.all(): - if comment.status == 0: - todo.append('A Comment from %s has been delivered but is not yet vetted. ' - 'Please vet it.' % comment.author) - nr_ref_inv = submission.refereeinvitation_set.count() - if (submission.is_resubmission and nr_ref_inv == 0 - and not submission.eicrecommendation_set.exists()): - todo.append('This resubmission requires attention: either (re)invite referees ' - 'or formulate an Editorial Recommendation.') - if nr_ref_inv == 0 and not submission.is_resubmission: - todo.append('No Referees have yet been invited. ' - 'At least 3 should be.') - elif nr_ref_inv < 3 and not submission.is_resubmission: - todo.append('Only %s Referees have been invited. ' - 'At least 3 should be.' % nr_ref_inv) - for ref_inv in submission.refereeinvitation_set.all(): - refname = ref_inv.last_name + ', ' + ref_inv.first_name - if ref_inv.referee: - refname = str(ref_inv.referee) - timelapse = timezone.now() - ref_inv.date_invited - timeleft = submission.reporting_deadline - timezone.now() - if (ref_inv.accepted is None and not ref_inv.cancelled - and timelapse > datetime.timedelta(days=3)): - todo.append('Referee %s has not responded for %s days. ' - 'Consider sending a reminder ' - 'or cancelling the invitation.' % (refname, str(timelapse.days))) - if (ref_inv.accepted and not ref_inv.fulfilled and not ref_inv.cancelled - and timeleft < datetime.timedelta(days=7)): - todo.append('Referee %s has accepted to send a Report, ' - 'but not yet delivered it (with %s days left). ' - 'Consider sending a reminder or cancelling the invitation.' - % (refname, str(timeleft.days))) - if submission.reporting_deadline < timezone.now(): - todo.append('The refereeing deadline has passed. Please either extend it, ' - 'or formulate your Editorial Recommendation if at least ' - 'one Report has been received.') - reports = submission.reports.all() - for report in reports: - if report.status == 0: - todo.append('The Report from %s has been delivered but is not yet vetted. ' - 'Please vet it.' % report.author) - return todo diff --git a/submissions/utils.py b/submissions/utils.py index ccb2ead5d..968c8ec17 100644 --- a/submissions/utils.py +++ b/submissions/utils.py @@ -4,7 +4,8 @@ from django.core.mail import EmailMessage, EmailMultiAlternatives from django.template import Context, Template from django.utils import timezone -# from .constants import SUBMISSION_STATUS +from .constants import STATUS_RESUBMISSION_SCREENING, SUBMISSION_STATUS_OUT_OF_POOL,\ + STATUS_REVISION_REQUESTED # from .models import EditorialAssignment from scipost.utils import EMAIL_FOOTER @@ -17,20 +18,89 @@ class BaseSubmissionCycle: is meant as an abstract blueprint for the overall submission cycle and its needed actions. """ - submission = None - name = None default_days = 28 + may_add_referees = True + may_reinvite_referees = True + minimum_referees = 3 + name = None + required_actions = [] + submission = None def __init__(self, submission): self.submission = submission + self._update_actions() def __str__(self): return self.submission.get_refereeing_cycle_display() + def _update_actions(self): + """ + Create the list of required_actions for the current submission to be used on the + editorial page. + """ + self.required_actions = [] + if self.submission.status in SUBMISSION_STATUS_OUT_OF_POOL: + '''Submission does not appear in the pool, no action required.''' + return False + + if self.submission.status == STATUS_REVISION_REQUESTED: + ''''Editor-in-charge has requested revision''' + return False + + if self.submission.eicrecommendation_set.exists(): + '''A Editorial Recommendation has already been submitted. Cycle done.''' + return False + + if self.submission.status == STATUS_RESUBMISSION_SCREENING: + """ + Submission is a resubmission and the EIC still has to determine which + cycle to proceed with. + """ + self.required_actions.append(('choose_cycle', + 'Choose the submission cycle to proceed with.',)) + return False + + comments_to_vet = self.submission.comments.awaiting_vetting().count() + if comments_to_vet > 0: + '''There are comments on the submission awaiting vetting.''' + if comments_to_vet > 1: + text = 'One Comment has' + else: + text = '%i Comment\'s have' % comments_to_vet + text += ' been delivered but is not yet vetted. Please vet it.' + self.required_actions.append(('vet_comments', text,)) + + nr_ref_inv = self.submission.referee_invitations.count() + if nr_ref_inv < self.minimum_referees: + """ + The submission cycle does not meet the criteria of a minimum of + `self.minimum_referees` referees yet. + """ + text = 'No' if nr_ref_inv == 0 else 'Only %i' % nr_ref_inv + text += ' Referees have yet been invited.' + text += ' At least %i should be.' % self.minimum_referees + self.required_actions.append(('invite_referees', text,)) + + reports_awaiting_vetting = self.submission.reports.awaiting_vetting().count() + if reports_awaiting_vetting > 0: + '''There are reports on the submission awaiting vetting.''' + if reports_awaiting_vetting > 1: + text = 'One Report has' + else: + text = '%i Reports have' % reports_awaiting_vetting + text += ' been delivered but is not yet vetted. Please vet it.' + self.required_actions.append(('vet_reports', text,)) + + return True + def update_deadline(self, period=None): deadline = timezone.now() + datetime.timedelta(days=(period or self.default_days)) self.submission.reporting_deadline = deadline + def get_required_actions(self): + '''Return list of the submission its required actions''' + return self.required_actions + def update_status(self): """ Implement: @@ -42,7 +112,51 @@ class BaseSubmissionCycle: raise NotImplementedError -class GeneralSubmissionCycle(BaseSubmissionCycle): +class BaseRefereeSubmissionCycle(BaseSubmissionCycle): + """ + This *abstract* submission cycle adds the specific actions needed for submission cycles + that require referees to be invited. + """ + def _update_actions(self): + continue_update = super()._update_actions() + if not continue_update: + return False + + for ref_inv in self.submission.referee_invitations.all(): + if not ref_inv.cancelled: + if ref_inv.accepted is None: + '''An invited referee may have not responsed yet.''' + timelapse = timezone.now() - ref_inv.date_invited + if timelapse > datetime.timedelta(days=3): + text = ('Referee %s has not responded for %i days. ' + 'Consider sending a reminder or cancelling the invitation.' + % (ref_inv.referee_str, timelapse.days)) + self.required_actions.append(('referee_no_response', text,)) + elif ref_inv.accepted and not ref_inv.fulfilled: + '''A referee has not fulfilled its duty and the deadline is closing in.''' + timeleft = self.submission.reporting_deadline - timezone.now() + if timeleft < datetime.timedelta(days=7): + text = ('Referee %s has accepted to send a Report, ' + 'but not yet delivered it ' % ref_inv.referee_str) + if timeleft.days < 0: + text += '(%i days overdue). ' % (- timeleft.days) + elif timeleft.days == 1: + text += '(with 1 day left). ' + else: + text += '(with %i days left). ' % timeleft.days + text += 'Consider sending a reminder or cancelling the invitation.' + self.required_actions.append(('referee_no_delivery', text,)) + + if self.submission.reporting_deadline < timezone.now(): + text = ('The refereeing deadline has passed. Please either extend it, ' + 'or formulate your Editorial Recommendation if at least ' + 'one Report has been received.') + self.required_actions.append(('deadline_passed', text,)) + + return True + + +class GeneralSubmissionCycle(BaseRefereeSubmissionCycle): """ The default submission cycle assigned to all 'regular' submissions and resubmissions which are explicitly assigned to go trough the default cycle by the EIC. @@ -51,7 +165,7 @@ class GeneralSubmissionCycle(BaseSubmissionCycle): pass -class ShortSubmissionCycle(BaseSubmissionCycle): +class ShortSubmissionCycle(BaseRefereeSubmissionCycle): """ This cycle is used if the EIC has explicitly chosen to do a short version of the general submission cycle. The deadline is within two weeks instead of the default four weeks. @@ -59,6 +173,8 @@ class ShortSubmissionCycle(BaseSubmissionCycle): This cycle is only available for resubmitted submissions! """ default_days = 14 + may_add_referees = False + minimum_referees = 1 pass @@ -69,6 +185,9 @@ class DirectRecommendationSubmissionCycle(BaseSubmissionCycle): This cycle is only available for resubmitted submissions! """ + may_add_referees = False + may_reinvite_referees = False + minimum_referees = 0 pass diff --git a/submissions/views.py b/submissions/views.py index 57ff467f9..9f0e78674 100644 --- a/submissions/views.py +++ b/submissions/views.py @@ -299,7 +299,7 @@ def submission_detail(request, arxiv_identifier_w_vn_nr): .get(submission=submission)) except (EICRecommendation.DoesNotExist, AttributeError): recommendation = None - comments = submission.comment_set.all() + comments = submission.comments.all() context = {'submission': submission, 'other_versions': other_versions, 'recommendation': recommendation, @@ -336,7 +336,7 @@ def pool(request): All members of the Editorial College have access. """ submissions_in_pool = (Submission.objects.get_pool(request.user) - .prefetch_related('refereeinvitation_set', 'remark_set', 'comment_set')) + .prefetch_related('referee_invitations', 'remark_set', 'comments')) recommendations_undergoing_voting = (EICRecommendation.objects .get_for_user_in_pool(request.user) .filter(submission__status__in=['put_to_EC_voting'])) -- GitLab