diff --git a/comments/managers.py b/comments/managers.py index 2875a7030b1cb75cf16b364cd6c409e70e90f0b2..823b4c0d79a732c224bc62bec58d1f70d1f416fb 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 a6f5f0997a85cec69f48f3dcf04ae0ed0b02ed2e..fd52703d222870e5694815c6b39acb632abfbea9 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 148d94f70f2d6b33c0cb8e1508a459a485de4960..44b7a24f2c45f9ab3eedc648d2014ba6a981aa59 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 2137c6f062b517ff78f1a3452cf90fdbec39a2c2..8fdfc9d7f22e682b56ea2ace42e1515df2b570b4 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 8838a5f4af4b7df45b061570e898baf6a2ca03fc..48b71c13d3452f512cb70d8c8ea45d4820370bba 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 9b5c3d28fff551614e67c8c4f0abb8df4bbf761a..f72279cdf4aa9901fa2ef9575bee41ba186064f1 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 68736208cf500daa45206848b5e6ae54ae8019e9..7e059bd73f33cf19e4c11c935fabcbc6717f0ef5 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 ed4ba37f61f4da762b7137600fcbae98dbc24c82..8ca304e55294203fff45105dcf6ec9fbabf95f6b 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 ef29d3600d15b69572cd88f5994cac60cbaaecb6..c5cc177ff3568ba332512714ab3afb37007c421b 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 2da991f6d3ce3c33c022055bee26bbbbc5d7c597..83a792a917a31309c6bb14fe9976a15be9e4337b 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 ccb2ead5dc1923f9562b5a1646622188cff58d9d..968c8ec174f64914958193887f1657100b204ba2 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 57ff467f99d0dff1e5d0782e21a8b6b8b6c0c239..9f0e786745617ff92a3b1a2b92bb71852c6310ef 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']))