diff --git a/scipost/models.py b/scipost/models.py index 1b2fa091fee75a4422c4f6186913904e4c3a48a8..dd2bc50535f6bef22f63975dbebd01b167c19593 100644 --- a/scipost/models.py +++ b/scipost/models.py @@ -8,7 +8,6 @@ from django.contrib.postgres.fields import ArrayField from django.db import models from django.template import Template, Context from django.utils import timezone -from django.utils.safestring import mark_safe from django_countries.fields import CountryField diff --git a/scipost/templates/scipost/personal_page.html b/scipost/templates/scipost/personal_page.html index 04decd986cf50c0916fe695ee7a0acdcb963de38..883f8dcdfa54f0006aa954ec0574c7fa7cf2763b 100644 --- a/scipost/templates/scipost/personal_page.html +++ b/scipost/templates/scipost/personal_page.html @@ -34,20 +34,18 @@ <a href="#account" class="nav-link active" data-toggle="tab">Account</a> </li> {% if 'SciPost Administrators' in user_groups or 'Editorial Administrators' in user_groups or 'Editorial College' in user_groups or 'Advisory Board' in user_groups or 'Vetting Editors' in user_groups or 'Ambassadors' in user_groups or 'Junior Ambassadors' in user_groups %} - <li class="nav-item btn btn-secondary"> - <a href="#editorial-actions" class="nav-link" data-toggle="tab">Editorial Actions</a> - </li> - {% endif %} - {% if perms.scipost.can_view_production %} - <li class="nav-item btn btn-secondary"> - <a href="#production" class="nav-link" data-toggle="tab">Production</a> - </li> - {% endif %} + <li class="nav-item btn btn-secondary"> + <a href="#editorial-actions" class="nav-link" data-toggle="tab">Editorial Actions</a> + </li> + {% endif %} + {% if perms.scipost.can_view_production %} + <li class="nav-item btn btn-secondary"> + <a href="#production" class="nav-link" data-toggle="tab">Production</a> + </li> + {% endif %} {% if perms.scipost.can_referee %} <li class="nav-item btn btn-secondary"> - {% with pending_count=pending_ref_tasks|length %} - <a class="nav-link" data-toggle="tab" href="#refereeing">Refereeing {% if nr_ref_inv_to_consider|add:pending_count %}({{nr_ref_inv_to_consider|add:pending_count}}){% endif %}</a> - {% endwith %} + <a class="nav-link" data-toggle="tab" href="#refereeing">Refereeing {% if refereeing_tab_total_count %}({{refereeing_tab_total_count}}){% endif %}</a> </li> {% endif %} <li class="nav-item btn btn-secondary"> @@ -371,6 +369,24 @@ </ul> </div> </div> + + {% if unfinished_reports %} + <div class="row"> + <div class="col-12"> + <h3>Unfinished reports:</h3> + </div> + <div class="col-12"> + <ul class="list-group list-group-flush"> + {% for report in unfinished_reports %} + <li class="list-group-item"> + <div class="w-100">{% include 'submissions/_submission_card_content.html' with submission=report.submission %}</div> + <div class="px-2"><a class="px-1" href="{% url 'submissions:submit_report' report.submission.arxiv_identifier_w_vn_nr %}">Finish report</a></div> + </li> + {% endfor %} + </ul> + </div> + </div> + {% endif %} </div><!-- End tab --> {% endif %} diff --git a/scipost/views.py b/scipost/views.py index 728cc1755b43ef6de564c15e29fb14ff2cb9dcbb..d2b41d229c45cc2c5308465d837e11c7bf6e6563 100644 --- a/scipost/views.py +++ b/scipost/views.py @@ -806,8 +806,8 @@ def personal_page(request): .count()) active_assignments = EditorialAssignment.objects.filter( to=contributor, accepted=True, completed=False) - nr_reports_to_vet = Report.objects.filter( - status=0, submission__editor_in_charge=contributor).count() + nr_reports_to_vet = (Report.objects.awaiting_vetting() + .filter(submission__editor_in_charge=contributor).count()) nr_commentary_page_requests_to_vet = 0 nr_comments_to_vet = 0 nr_thesislink_requests_to_vet = 0 @@ -818,10 +818,16 @@ def personal_page(request): nr_comments_to_vet = Comment.objects.filter(status=0).count() nr_thesislink_requests_to_vet = ThesisLink.objects.filter(vetted=False).count() nr_authorship_claims_to_vet = AuthorshipClaim.objects.filter(status='0').count() + + # Refereeing nr_ref_inv_to_consider = RefereeInvitation.objects.filter( referee=contributor, accepted=None, cancelled=False).count() pending_ref_tasks = RefereeInvitation.objects.filter( referee=contributor, accepted=True, fulfilled=False) + unfinished_reports = Report.objects.in_draft().filter(author=contributor) + refereeing_tab_total_count = nr_ref_inv_to_consider + len(pending_ref_tasks) + refereeing_tab_total_count += len(unfinished_reports) + # Verify if there exist objects authored by this contributor, # whose authorship hasn't been claimed yet own_submissions = (Submission.objects @@ -879,11 +885,14 @@ def personal_page(request): 'nr_thesis_authorships_to_claim': nr_thesis_authorships_to_claim, 'nr_ref_inv_to_consider': nr_ref_inv_to_consider, 'pending_ref_tasks': pending_ref_tasks, + 'refereeing_tab_total_count': refereeing_tab_total_count, + 'unfinished_reports': unfinished_reports, 'own_submissions': own_submissions, 'own_commentaries': own_commentaries, 'own_thesislinks': own_thesislinks, 'own_comments': own_comments, 'own_authorreplies': own_authorreplies, } + return render(request, 'scipost/personal_page.html', context) diff --git a/submissions/admin.py b/submissions/admin.py index 10fa602e0f82a198e68d393f362720d504addee9..dfc3dff7b8c223034d8d13ac0136463d1d54be6d 100644 --- a/submissions/admin.py +++ b/submissions/admin.py @@ -3,7 +3,8 @@ from django import forms from guardian.admin import GuardedModelAdmin -from submissions.models import * +from submissions.models import Submission, EditorialAssignment, RefereeInvitation, Report,\ + EditorialCommunication, EICRecommendation from scipost.models import Contributor @@ -27,6 +28,7 @@ class SubmissionAdminForm(forms.ModelForm): model = Submission fields = '__all__' + class SubmissionAdmin(GuardedModelAdmin): search_fields = ['submitted_by__user__last_name', 'title', 'author_list', 'abstract'] list_display = ('title', 'author_list', 'status', 'submission_date', 'publication',) @@ -34,6 +36,7 @@ class SubmissionAdmin(GuardedModelAdmin): list_filter = ('status', 'discipline', 'submission_type', ) form = SubmissionAdminForm + admin.site.register(Submission, SubmissionAdmin) @@ -45,6 +48,7 @@ class EditorialAssignmentAdminForm(forms.ModelForm): model = EditorialAssignment fields = '__all__' + class EditorialAssignmentAdmin(admin.ModelAdmin): search_fields = ['submission__title', 'submission__author_list', 'to__user__last_name'] list_display = ('to', submission_short_title, 'accepted', 'completed', 'date_created',) @@ -52,6 +56,7 @@ class EditorialAssignmentAdmin(admin.ModelAdmin): list_filter = ('accepted', 'deprecated', 'completed', ) form = EditorialAssignmentAdminForm + admin.site.register(EditorialAssignment, EditorialAssignmentAdmin) @@ -63,6 +68,7 @@ class RefereeInvitationAdminForm(forms.ModelForm): model = RefereeInvitation fields = '__all__' + class RefereeInvitationAdmin(admin.ModelAdmin): search_fields = ['submission__title', 'submission__author_list', 'referee__user__last_name', @@ -72,6 +78,7 @@ class RefereeInvitationAdmin(admin.ModelAdmin): date_hierarchy = 'date_invited' form = RefereeInvitationAdminForm + admin.site.register(RefereeInvitation, RefereeInvitationAdmin) @@ -83,6 +90,7 @@ class ReportAdminForm(forms.ModelForm): model = Report fields = '__all__' + class ReportAdmin(admin.ModelAdmin): search_fields = ['author__user__last_name', 'submission'] list_display = ('author', 'status', submission_short_title, 'date_submitted', ) @@ -91,6 +99,7 @@ class ReportAdmin(admin.ModelAdmin): list_filter = ('status',) form = ReportAdminForm + admin.site.register(Report, ReportAdmin) @@ -134,4 +143,5 @@ class EICRecommendationAdmin(admin.ModelAdmin): search_fields = ['submission__title'] form = EICRecommendationAdminForm + admin.site.register(EICRecommendation, EICRecommendationAdmin) diff --git a/submissions/constants.py b/submissions/constants.py index 84638110f30c4a2829a9f8a29c125b2a57baf46c..2b247ffa6a78a9c83e72934b3871e42c21ec3435 100644 --- a/submissions/constants.py +++ b/submissions/constants.py @@ -38,16 +38,24 @@ SUBMISSION_STATUS = ( SUBMISSION_HTTP404_ON_EDITORIAL_PAGE = [ 'assignment_failed', - 'published', + STATUS_PUBLISHED, 'withdrawn', - 'rejected', - 'rejected_visible', + STATUS_REJECTED, + STATUS_REJECTED_VISIBLE, ] SUBMISSION_STATUS_OUT_OF_POOL = SUBMISSION_HTTP404_ON_EDITORIAL_PAGE + [ 'resubmitted' ] +SUBMISSION_EXCLUDE_FROM_REPORTING = SUBMISSION_HTTP404_ON_EDITORIAL_PAGE + [ + STATUS_AWAITING_ED_REC, + STATUS_REVIEW_CLOSED, + STATUS_ACCEPTED, + 'voting_in_preparation', + 'put_to_EC_voting', + 'withdrawn', +] # Submissions which are allowed/required to submit a EIC Recommendation SUBMISSION_EIC_RECOMMENDATION_REQUIRED = [ @@ -156,23 +164,23 @@ REPORT_REC = ( # # Reports # -REPORT_ACTION_ACCEPT = 1 -REPORT_ACTION_REFUSE = 2 +REPORT_ACTION_ACCEPT = 'accept' +REPORT_ACTION_REFUSE = 'refuse' REPORT_ACTION_CHOICES = ( (REPORT_ACTION_ACCEPT, 'accept'), (REPORT_ACTION_REFUSE, 'refuse'), ) -STATUS_VETTED = 1 -STATUS_UNVETTED = 0 -STATUS_UNCLEAR = -1 -STATUS_INCORRECT = -2 -STATUS_NOT_USEFUL = -3 -STATUS_NOT_ACADEMIC = -4 +STATUS_DRAFT = 'draft' +STATUS_VETTED = 'vetted' +STATUS_UNVETTED = 'unvetted' +STATUS_UNCLEAR = 'unclear' +STATUS_INCORRECT = 'incorrect' +STATUS_NOT_USEFUL = 'notuseful' +STATUS_NOT_ACADEMIC = 'notacademic' -REPORT_REFUSAL_NONE = 0 REPORT_REFUSAL_CHOICES = ( - (STATUS_UNVETTED, '-'), + (None, '-'), (STATUS_UNCLEAR, 'insufficiently clear'), (STATUS_INCORRECT, 'not fully factually correct'), (STATUS_NOT_USEFUL, 'not useful for the authors'), @@ -180,6 +188,7 @@ REPORT_REFUSAL_CHOICES = ( ) REPORT_STATUSES = ( + (STATUS_DRAFT, 'Draft'), (STATUS_VETTED, 'Vetted'), (STATUS_UNVETTED, 'Unvetted'), (STATUS_INCORRECT, 'Rejected (incorrect)'), diff --git a/submissions/exceptions.py b/submissions/exceptions.py index 19c5684bd004f175ffe4169cc2938d312d66edf0..0e8794a8cc841af0df2896138ce2ec0209ee0c7e 100644 --- a/submissions/exceptions.py +++ b/submissions/exceptions.py @@ -4,3 +4,11 @@ class CycleUpdateDeadlineError(Exception): def __str__(self): return self.name + + +class InvalidReportVettingValue(Exception): + def __init__(self, name): + self.name = name + + def __str__(self): + return self.name diff --git a/submissions/factories.py b/submissions/factories.py index 6c602275452772293c4f8303caf37c5baf63152a..d9c32c745fd079caed4ab17d02fd3511c0605ff9 100644 --- a/submissions/factories.py +++ b/submissions/factories.py @@ -9,8 +9,10 @@ from journals.constants import SCIPOST_JOURNALS_DOMAINS from common.helpers import random_arxiv_identifier_without_version_number, random_scipost_journal from .constants import STATUS_UNASSIGNED, STATUS_EIC_ASSIGNED, STATUS_RESUBMISSION_INCOMING,\ - STATUS_PUBLISHED, SUBMISSION_TYPE, STATUS_RESUBMITTED -from .models import Submission + STATUS_PUBLISHED, SUBMISSION_TYPE, STATUS_RESUBMITTED, STATUS_VETTED,\ + REFEREE_QUALIFICATION, RANKING_CHOICES, QUALITY_SPEC, REPORT_REC,\ + REPORT_STATUSES, STATUS_UNVETTED, STATUS_DRAFT +from .models import Submission, Report, RefereeInvitation from faker import Faker @@ -142,3 +144,64 @@ class PublishedSubmissionFactory(SubmissionFactory): status = STATUS_PUBLISHED open_for_commenting = False open_for_reporting = False + + +class ReportFactory(factory.django.DjangoModelFactory): + class Meta: + model = Report + + status = factory.Iterator(REPORT_STATUSES, getter=lambda c: c[0]) + submission = factory.Iterator(Submission.objects.all()) + date_submitted = Faker().date_time_between(start_date="-3y", end_date="now", tzinfo=pytz.UTC) + vetted_by = factory.Iterator(Contributor.objects.all()) + author = factory.Iterator(Contributor.objects.all()) + qualification = factory.Iterator(REFEREE_QUALIFICATION, getter=lambda c: c[0]) + strengths = Faker().paragraph() + weaknesses = Faker().paragraph() + report = Faker().paragraph() + requested_changes = Faker().paragraph() + validity = factory.Iterator(RANKING_CHOICES, getter=lambda c: c[0]) + significance = factory.Iterator(RANKING_CHOICES, getter=lambda c: c[0]) + originality = factory.Iterator(RANKING_CHOICES, getter=lambda c: c[0]) + clarity = factory.Iterator(RANKING_CHOICES, getter=lambda c: c[0]) + formatting = factory.Iterator(QUALITY_SPEC, getter=lambda c: c[0]) + grammar = factory.Iterator(QUALITY_SPEC, getter=lambda c: c[0]) + recommendation = factory.Iterator(REPORT_REC, getter=lambda c: c[0]) + remarks_for_editors = Faker().paragraph() + + +class DraftReportFactory(ReportFactory): + status = STATUS_DRAFT + vetted_by = None + + +class UnVettedReportFactory(ReportFactory): + status = STATUS_UNVETTED + vetted_by = None + + +class VettedReportFactory(ReportFactory): + status = STATUS_VETTED + + +class RefereeInvitationFactory(factory.django.DjangoModelFactory): + class Meta: + model = RefereeInvitation + + submission = factory.SubFactory('submissions.factories.SubmissionFactory') + referee = factory.Iterator(Contributor.objects.all()) + + invitation_key = factory.Faker('md5') + invited_by = factory.Iterator(Contributor.objects.all()) + + @factory.post_generation + def contributor_fields(self, create, extracted, **kwargs): + self.title = self.referee.title + self.first_name = self.referee.user.first_name + self.last_name = self.referee.user.last_name + self.email_address = self.referee.user.email + + +class AcceptedRefereeInvitationFactory(RefereeInvitationFactory): + accepted = True + date_responded = Faker().date_time_between(start_date="-1y", end_date="now", tzinfo=pytz.UTC) diff --git a/submissions/forms.py b/submissions/forms.py index 4b6fdcc8425010d118d279a7838a8b4a3b3c336b..7f2d39772f1086f15e09c39ef36facb7bda4cb61 100644 --- a/submissions/forms.py +++ b/submissions/forms.py @@ -1,13 +1,16 @@ from django import forms from django.contrib.auth.models import Group -from django.core.validators import RegexValidator -from django.db import models, transaction +from django.db import transaction +from django.utils import timezone from guardian.shortcuts import assign_perm from .constants import ASSIGNMENT_BOOL, ASSIGNMENT_REFUSAL_REASONS, STATUS_RESUBMITTED,\ REPORT_ACTION_CHOICES, REPORT_REFUSAL_CHOICES, STATUS_REVISION_REQUESTED,\ - STATUS_REJECTED, STATUS_REJECTED_VISIBLE, STATUS_RESUBMISSION_INCOMING + STATUS_REJECTED, STATUS_REJECTED_VISIBLE, STATUS_RESUBMISSION_INCOMING,\ + STATUS_DRAFT, STATUS_UNVETTED, REPORT_ACTION_ACCEPT, REPORT_ACTION_REFUSE,\ + STATUS_VETTED +from .exceptions import InvalidReportVettingValue from .models import Submission, RefereeInvitation, Report, EICRecommendation, EditorialAssignment from scipost.constants import SCIPOST_SUBJECT_AREAS @@ -418,17 +421,55 @@ class ReportForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(ReportForm, self).__init__(*args, **kwargs) - self.fields['strengths'].widget.attrs.update( - {'placeholder': 'Give a point-by-point (numbered 1-, 2-, ...) list of the paper\'s strengths', - 'rows': 10, 'cols': 100}) - self.fields['weaknesses'].widget.attrs.update( - {'placeholder': 'Give a point-by-point (numbered 1-, 2-, ...) list of the paper\'s weaknesses', - 'rows': 10, 'cols': 100}) + self.fields['strengths'].widget.attrs.update({ + 'placeholder': ('Give a point-by-point ' + '(numbered 1-, 2-, ...) list of the paper\'s strengths'), + 'rows': 10, + 'cols': 100 + }) + self.fields['weaknesses'].widget.attrs.update({ + 'placeholder': ('Give a point-by-point ' + '(numbered 1-, 2-, ...) list of the paper\'s weaknesses'), + 'rows': 10, + 'cols': 100 + }) self.fields['report'].widget.attrs.update({'placeholder': 'Your general remarks', 'rows': 10, 'cols': 100}) - self.fields['requested_changes'].widget.attrs.update( - {'placeholder': 'Give a numbered (1-, 2-, ...) list of specifically requested changes', - 'cols': 100}) + self.fields['requested_changes'].widget.attrs.update({ + 'placeholder': 'Give a numbered (1-, 2-, ...) list of specifically requested changes', + 'cols': 100 + }) + + def save(self, submission, current_contributor): + """ + Update meta data if ModelForm is submitted (non-draft). + Possibly overwrite the default status if user asks for saving as draft. + """ + report = super().save(commit=False) + + report.submission = submission + report.author = current_contributor + report.date_submitted = timezone.now() + + # Save with right status asked by user + if 'save_draft' in self.data: + report.status = STATUS_DRAFT + elif 'save_submit' in self.data: + report.status = STATUS_UNVETTED + + # Update invitation and report meta data if exist + invitation = submission.referee_invitations.filter(referee=current_contributor).first() + if invitation: + invitation.fulfilled = True + invitation.save() + report.invited = True + + # Check if report author if the report is being flagged on the submission + if submission.referees_flagged: + if current_contributor.user.last_name in submission.referees_flagged: + report.flagged = True + report.save() + return report class VetReportForm(forms.Form): @@ -438,12 +479,41 @@ class VetReportForm(forms.Form): refusal_reason = forms.ChoiceField(choices=REPORT_REFUSAL_CHOICES, required=False) email_response_field = forms.CharField(widget=forms.Textarea(), label='Justification (optional)', required=False) + report = forms.ModelChoiceField(queryset=Report.objects.awaiting_vetting(), required=True, + widget=forms.HiddenInput()) def __init__(self, *args, **kwargs): super(VetReportForm, self).__init__(*args, **kwargs) - self.fields['email_response_field'].widget.attrs.update( - {'placeholder': 'Optional: give a textual justification (will be included in the email to the Report\'s author)', - 'rows': 5}) + self.fields['email_response_field'].widget.attrs.update({ + 'placeholder': ('Optional: give a textual justification ' + '(will be included in the email to the Report\'s author)'), + 'rows': 5 + }) + + def clean_refusal_reason(self): + '''Require a refusal reason if report is rejected.''' + reason = self.cleaned_data['refusal_reason'] + if self.cleaned_data['action_option'] == REPORT_ACTION_REFUSE: + if not reason: + self.add_error('refusal_reason', 'A reason must be given to refuse a report.') + return reason + + def process_vetting(self, current_contributor): + '''Set the right report status and update submission fields if needed.''' + report = self.cleaned_data['report'] + report.vetted_by = current_contributor + if self.cleaned_data['action_option'] == REPORT_ACTION_ACCEPT: + # Accept the report as is + report.status = STATUS_VETTED + report.submission.latest_activity = timezone.now() + report.submission.save() + elif self.cleaned_data['action_option'] == REPORT_ACTION_REFUSE: + # The report is rejected + report.status = self.cleaned_data['refusal_reason'] + else: + raise InvalidReportVettingValue(self.cleaned_data['action_option']) + report.save() + return report ################### diff --git a/submissions/managers.py b/submissions/managers.py index f1ce5d8a9ec241367cdfa27d31e7624bbea4c4ac..0b9d6cd8cf831490a0974b2ba14a815ac12e90b1 100644 --- a/submissions/managers.py +++ b/submissions/managers.py @@ -4,7 +4,8 @@ from django.db.models import Q from .constants import SUBMISSION_STATUS_OUT_OF_POOL, SUBMISSION_STATUS_PUBLICLY_UNLISTED,\ SUBMISSION_STATUS_PUBLICLY_INVISIBLE, STATUS_UNVETTED, STATUS_VETTED,\ STATUS_UNCLEAR, STATUS_INCORRECT, STATUS_NOT_USEFUL, STATUS_NOT_ACADEMIC,\ - SUBMISSION_HTTP404_ON_EDITORIAL_PAGE + SUBMISSION_HTTP404_ON_EDITORIAL_PAGE, STATUS_DRAFT,\ + SUBMISSION_EXCLUDE_FROM_REPORTING class SubmissionManager(models.Manager): @@ -73,6 +74,13 @@ class SubmissionManager(models.Manager): queryset = self.exclude(status__in=SUBMISSION_STATUS_PUBLICLY_INVISIBLE) return self._newest_version_only(queryset) + def open_for_reporting(self): + """ + This query should filter submissions that do not have the right status to receive + a new report. + """ + return self.exclude(status__in=SUBMISSION_EXCLUDE_FROM_REPORTING) + class EditorialAssignmentManager(models.Manager): def get_for_user_in_pool(self, user): @@ -110,7 +118,7 @@ class EICRecommendationManager(models.Manager): class ReportManager(models.Manager): def accepted(self): - return self.filter(status__gte=STATUS_VETTED) + return self.filter(status=STATUS_VETTED) def awaiting_vetting(self): return self.filter(status=STATUS_UNVETTED) @@ -118,3 +126,6 @@ class ReportManager(models.Manager): def rejected(self): return self.filter(status__in=[STATUS_UNCLEAR, STATUS_INCORRECT, STATUS_NOT_USEFUL, STATUS_NOT_ACADEMIC]) + + def in_draft(self): + return self.filter(status=STATUS_DRAFT) diff --git a/submissions/migrations/0044_auto_20170602_1836.py b/submissions/migrations/0044_auto_20170602_1836.py new file mode 100644 index 0000000000000000000000000000000000000000..329d7db77ef5620d4a4bc4ecb7c07d12d685660c --- /dev/null +++ b/submissions/migrations/0044_auto_20170602_1836.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-02 16:36 +from __future__ import unicode_literals + +from django.db import migrations, models + + +status_map_to_new = { + '1': 'vetted', + '0': 'unvetted', + '-1': 'unclear', + '-2': 'incorrect', + '-3': 'notuseful', + '-4': 'notacademic' +} +status_map_to_old = dict((v, int(k)) for k, v in status_map_to_new.items()) + + +def map_reports_to_new_status_codes(apps, schema_editor): + Report = apps.get_model('submissions', 'Report') + reports = Report.objects.all() + for report in reports: + try: + new_status = status_map_to_new[report.status] + except KeyError: + new_status = 'unvetted' + report.status = new_status + report.save() + print('\nUpdated %i reports.' % len(reports)) + + +def map_reports_to_old_status_codes(apps, schema_editor): + Report = apps.get_model('submissions', 'Report') + reports = Report.objects.all() + for report in reports: + try: + new_status = status_map_to_old[report.status] + except KeyError: + new_status = 0 + report.status = new_status + report.save() + print('\nUpdated %i reports.' % len(reports)) + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0043_auto_20170512_0836'), + ] + + operations = [ + migrations.AlterField( + model_name='report', + name='status', + field=models.CharField(choices=[('vetted', 'Vetted'), ('unvetted', 'Unvetted'), ('incorrect', 'Rejected (incorrect)'), ('unclear', 'Rejected (unclear)'), ('notuseful', 'Rejected (not useful)'), ('notacademic', 'Rejected (not academic in style)')], default='unvetted', max_length=16), + ), + migrations.RunPython(map_reports_to_new_status_codes, map_reports_to_old_status_codes), + ] diff --git a/submissions/models.py b/submissions/models.py index 6bf5ae9c6c7b1b9378efa71e54f26a7a8074ee88..17a3643441ffd49857f4e637262444881de02fb2 100644 --- a/submissions/models.py +++ b/submissions/models.py @@ -1,16 +1,15 @@ import datetime from django.utils import timezone -from django.db import models, transaction +from django.db import models from django.contrib.postgres.fields import JSONField from django.urls import reverse from .constants import ASSIGNMENT_REFUSAL_REASONS, ASSIGNMENT_NULLBOOL,\ SUBMISSION_TYPE, ED_COMM_CHOICES, REFEREE_QUALIFICATION, QUALITY_SPEC,\ RANKING_CHOICES, REPORT_REC, SUBMISSION_STATUS, STATUS_UNASSIGNED,\ - REPORT_STATUSES, STATUS_UNVETTED, STATUS_RESUBMISSION_INCOMING,\ - SUBMISSION_CYCLES, CYCLE_DEFAULT, CYCLE_SHORT, CYCLE_DIRECT_REC,\ - SUBMISSION_EIC_RECOMMENDATION_REQUIRED + REPORT_STATUSES, STATUS_UNVETTED, SUBMISSION_EIC_RECOMMENDATION_REQUIRED,\ + SUBMISSION_CYCLES, CYCLE_DEFAULT, CYCLE_SHORT, CYCLE_DIRECT_REC from .managers import SubmissionManager, EditorialAssignmentManager, EICRecommendationManager,\ ReportManager from .utils import ShortSubmissionCycle, DirectRecommendationSubmissionCycle,\ @@ -148,19 +147,19 @@ class Submission(ArxivCallable, models.Model): return self.referee_invitations.filter(accepted=None).count() def count_invited_reports(self): - return self.reports.filter(status=1, invited=True).count() + return self.reports.accepted().filter(invited=True).count() def count_contrib_reports(self): - return self.reports.filter(status=1, invited=False).count() + return self.reports.accepted().filter(invited=False).count() def count_obtained_reports(self): - return self.reports.filter(status=1, invited__isnull=False).count() + return self.reports.accepted().filter(invited__isnull=False).count() def count_refused_reports(self): - return self.reports.filter(status__lte=-1).count() + return self.reports.rejected().count() def count_awaiting_vetting(self): - return self.reports.filter(status=0).count() + return self.reports.awaiting_vetting().count() ###################### @@ -236,7 +235,7 @@ class RefereeInvitation(models.Model): class Report(models.Model): """ Both types of reports, invited or contributed. """ - status = models.SmallIntegerField(choices=REPORT_STATUSES, default=STATUS_UNVETTED) + status = models.CharField(max_length=16, choices=REPORT_STATUSES, default=STATUS_UNVETTED) submission = models.ForeignKey('submissions.Submission', related_name='reports', on_delete=models.CASCADE) vetted_by = models.ForeignKey('scipost.Contributor', related_name="report_vetted_by", @@ -264,7 +263,7 @@ class Report(models.Model): verbose_name="Quality of paper formatting") grammar = models.SmallIntegerField(choices=QUALITY_SPEC, verbose_name="Quality of English grammar") - # + recommendation = models.SmallIntegerField(choices=REPORT_REC) remarks_for_editors = models.TextField(default='', blank=True, verbose_name='optional remarks for the Editors only') diff --git a/submissions/templates/submissions/submission_detail.html b/submissions/templates/submissions/submission_detail.html index c10ff311dbc77527b3a1fd60c593d843f299b907..f255cf191d11632a6776dda1ba0baf10e76e4ffd 100644 --- a/submissions/templates/submissions/submission_detail.html +++ b/submissions/templates/submissions/submission_detail.html @@ -58,6 +58,12 @@ {% endfor %} </div> {% endif %} + + {% if unfinished_report_for_user %} + <blockquote class="blockquote"> + <p class="mb-0">You have an unfinished report for this submission, <a href="{% url 'submissions:submit_report' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}">finish your report here.</a></p> + </blockquote> + {% endif %} </div> </div> @@ -110,7 +116,8 @@ {% if submission.open_for_reporting %} {% if perms.scipost.can_referee and not is_author and not is_author_unchecked %} <li> - <h3><a href="{% url 'submissions:submit_report' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}">Contribute a Report</a></h3> + + <h3><a href="{% url 'submissions:submit_report' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}">{% if unfinished_report_for_user %}Finish your report{% else %}Contribute a Report{% endif %}</a></h3> <div class="text-danger">Deadline for reporting: {{ submission.reporting_deadline|date:"Y-m-d" }}</div> </li> {% elif is_author_unchecked %} diff --git a/submissions/templates/submissions/submit_report.html b/submissions/templates/submissions/submit_report.html index 72a253c84058ac31f315ed052402b541edd3d08b..63f80e447b69a04de026d1a51db389b58df83e29 100644 --- a/submissions/templates/submissions/submit_report.html +++ b/submissions/templates/submissions/submit_report.html @@ -88,8 +88,9 @@ <form action="{% url 'submissions:submit_report' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}" method="post"> {% csrf_token %} {{ form|bootstrap:'3,9' }} - <input class="btn btn-primary" type="submit" value="Submit your report"/> - <div class="mt-2"> + <input class="btn btn-primary" type="submit" name="save_submit" value="Submit your report"/> + <input class="btn btn-secondary ml-2" type="submit" name="save_draft" value="Save your report as draft"/> + <div class="my-4"> <em>By clicking on Submit, you state that you abide by the <a href="{% url 'journals:journals_terms_and_conditions' %}#referee_code_of_conduct">referee code of conduct</a>.</em> </div> </form> diff --git a/submissions/templates/submissions/submit_report_ack.html b/submissions/templates/submissions/submit_report_ack.html deleted file mode 100644 index 8e5b948e98fd58a108b0f265fa2b21ab02ce8ed1..0000000000000000000000000000000000000000 --- a/submissions/templates/submissions/submit_report_ack.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends 'scipost/base.html' %} - -{% block pagetitle %}: submit report (ack){% endblock pagetitle %} - -{% block bodysup %} - -<section> - {% if errormessage %} - <p>{{ errormessage }}</p> - {% else %} - <h1>Thank you for your Report.</h1> - {% endif %} -</section> - -{% endblock bodysup %} diff --git a/submissions/templates/submissions/vet_submitted_reports.html b/submissions/templates/submissions/vet_submitted_reports.html index d8fe13476f55abced58e18b00decfd07cbd72976..eb2278ad82292a53d9b7195ae33dc9ff30fc68c2 100644 --- a/submissions/templates/submissions/vet_submitted_reports.html +++ b/submissions/templates/submissions/vet_submitted_reports.html @@ -8,18 +8,15 @@ <script> $(document).ready(function(){ - - $('#refusal').hide(); - $('[name="action_option"]').on('change', function() { - if ($('#id_action_option_1').is(':checked')) { + if ($('[name="action_option"][value="refuse"]').is(':checked')) { $('#refusal').show(); } else { $('#refusal').hide(); } - }); - }); + }).trigger('change'); +}); </script> {% endblock headsup %} @@ -36,6 +33,7 @@ $(document).ready(function(){ <div class="col-12"> {% if not report_to_vet %} <h1>There are no Reports for you to vet.</h1> + <p>Go back to my <a href="{% url 'scipost:personal_page' %}">personal page</a>.</p> {% else %} <h1 class="highlight">SciPost Report to vet:</h1> @@ -52,8 +50,9 @@ $(document).ready(function(){ <hr class="small"> <h2>Please vet this Report:</h2> - <form action="{% url 'submissions:vet_submitted_report_ack' report_id=report_to_vet.id %}" method="post"> + <form action="{% url 'submissions:vet_submitted_reports' %}" method="post"> {% csrf_token %} + {{ form.report }} {{ form.action_option|bootstrap }} <div class="col-md-6" id="refusal"> {{ form.refusal_reason|bootstrap }} diff --git a/submissions/test_views.py b/submissions/test_views.py index 11e9c210a19bd604c2791a96a4359d6f1c7672b4..cc6905f3e018523147e0df80f8c49fecae9bd4c2 100644 --- a/submissions/test_views.py +++ b/submissions/test_views.py @@ -1,20 +1,21 @@ -import json - from django.core.urlresolvers import reverse -from django.test import TestCase +from django.test import TestCase, tag from django.test import Client from common.helpers import random_arxiv_identifier_without_version_number from common.helpers.test import add_groups_and_permissions from scipost.factories import ContributorFactory -# from scipost.models import Contributor -from .constants import STATUS_UNASSIGNED +from .constants import STATUS_UNASSIGNED, STATUS_DRAFT, STATUS_UNVETTED from .factories import UnassignedSubmissionFactory, EICassignedSubmissionFactory,\ ResubmittedSubmissionFactory, ResubmissionFactory,\ - PublishedSubmissionFactory -from .forms import SubmissionForm, SubmissionIdentifierForm -from .models import Submission + PublishedSubmissionFactory, DraftReportFactory,\ + AcceptedRefereeInvitationFactory +from .forms import RequestSubmissionForm, SubmissionIdentifierForm, ReportForm +from .models import Submission, Report, RefereeInvitation + +from faker import Faker + # This is content of a real arxiv submission. As long as it isn't published it should # be possible to run tests using this submission. @@ -54,7 +55,7 @@ class BaseContributorTestCase(TestCase): def setUp(self): add_groups_and_permissions() ContributorFactory.create_batch(5) - ContributorFactory.create( + self.current_contrib = ContributorFactory.create( user__last_name='Linder', # To pass the author check in create submissions view user__username='Test', user__password='testpw' @@ -79,6 +80,8 @@ class PrefillUsingIdentifierTest(BaseContributorTestCase): # Registered Contributor should get 200 response = self.client.get(self.url) + self.assertIsInstance(response.context['form'], SubmissionIdentifierForm) + self.assertFalse(response.context['form'].is_valid()) self.assertEqual(response.status_code, 200) def test_retrieving_existing_arxiv_paper(self): @@ -87,13 +90,11 @@ class PrefillUsingIdentifierTest(BaseContributorTestCase): {'identifier': TEST_SUBMISSION['arxiv_identifier_w_vn_nr']}) self.assertEqual(response.status_code, 200) - self.assertIsInstance(response.context['form'], SubmissionForm) - self.assertIsInstance(response.context['identifierform'], SubmissionIdentifierForm) - self.assertTrue(response.context['identifierform'].is_valid()) + self.assertIsInstance(response.context['form'], RequestSubmissionForm) # Explicitly compare fields instead of assertDictEqual as metadata field may be outdated - self.assertEqual(TEST_SUBMISSION['is_resubmission'], - response.context['form'].initial['is_resubmission']) + # self.assertEqual(TEST_SUBMISSION['is_resubmission'], + # response.context['form'].initial['is_resubmission']) self.assertEqual(TEST_SUBMISSION['title'], response.context['form'].initial['title']) self.assertEqual(TEST_SUBMISSION['author_list'], response.context['form'].initial['author_list']) @@ -138,7 +139,6 @@ class SubmitManuscriptTest(BaseContributorTestCase): 'submission_type': 'Article', 'domain': 'T' }) - params['metadata'] = json.dumps(params['metadata'], separators=(',', ':')) # Submit new Submission form response = client.post(reverse('submissions:submit_manuscript'), params) @@ -179,11 +179,13 @@ class SubmitManuscriptTest(BaseContributorTestCase): 'submission_type': 'Article', 'domain': 'T' }) - params['metadata'] = json.dumps(params['metadata'], separators=(',', ':')) # Submit new Submission form response = client.post(reverse('submissions:submit_manuscript'), params) - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 200) + self.assertIsInstance(response.context['form'], RequestSubmissionForm) + self.assertFalse(response.context['form'].is_valid()) + self.assertIn('author_list', response.context['form'].errors.keys()) # No real check is done here to see if submission submit is aborted. # To be implemented after Arxiv caller. @@ -246,3 +248,169 @@ class SubmissionListTest(BaseContributorTestCase): returned_submissions_ids.sort() visible_submission_ids.sort() self.assertListEqual(returned_submissions_ids, visible_submission_ids) + + +class SubmitReportTest(BaseContributorTestCase): + TEST_DATA = { + 'anonymous': 'on', + 'clarity': '60', + 'formatting': '4', + 'grammar': '5', + 'originality': '100', + 'qualification': '3', + 'recommendation': '3', + 'remarks_for_editors': 'Lorem Ipsum1', + 'report': 'Lorem Ipsum', + 'requested_changes': 'Lorem Ipsum2', + 'significance': '0', + 'strengths': 'Lorem Ipsum3', + 'validity': '60', + 'weaknesses': 'Lorem Ipsum4' + } + + def setUp(self): + super().setUp() + self.client = Client() + report_deadline = Faker().date_time_between(start_date="now", end_date="+30d", tzinfo=None) + self.submission = EICassignedSubmissionFactory(reporting_deadline=report_deadline) + self.submission.authors.remove(self.current_contrib) + self.submission.authors_false_claims.add(self.current_contrib) + self.target = reverse('submissions:submit_report', + args=(self.submission.arxiv_identifier_w_vn_nr,)) + self.assertTrue(self.client.login(username="Test", password="testpw")) + + @tag('reports') + def test_status_code_200_no_report_set(self): + '''Test response for view if no report is submitted yet.''' + report_deadline = Faker().date_time_between(start_date="now", end_date="+30d", tzinfo=None) + submission = EICassignedSubmissionFactory(reporting_deadline=report_deadline) + submission.authors.remove(self.current_contrib) + submission.authors_false_claims.add(self.current_contrib) + + target = reverse('submissions:submit_report', args=(submission.arxiv_identifier_w_vn_nr,)) + client = Client() + + # Login and call view + self.assertTrue(client.login(username="Test", password="testpw")) + response = client.get(target) + + self.assertEqual(response.status_code, 200) + self.assertIsNone(response.context['form'].instance.id) + + @tag('reports') + def test_status_code_200_report_in_draft(self): + '''Test response for view if report in draft exists.''' + report = DraftReportFactory(submission=self.submission, author=self.current_contrib) + response = self.client.get(self.target) + + self.assertEqual(response.status_code, 200) + self.assertIsInstance(response.context['form'], ReportForm) + self.assertEqual(response.context['form'].instance, report) + + @tag('reports') + def test_post_report_for_draft_status(self): + '''Test response of view if report is saved as draft.''' + response = self.client.post(self.target, {**self.TEST_DATA, 'save_draft': 'True'}) + + # Check if form is returned with saved report as instance + self.assertEqual(response.status_code, 200) + self.assertIsInstance(response.context['form'], ReportForm) + self.assertIsInstance(response.context['form'].instance, Report) + + # Briefly do cross checks if report submit is complete + report_db = Report.objects.last() + self.assertEqual(response.context['form'].instance, report_db) + self.assertTrue(report_db.anonymous) + self.assertEqual(report_db.status, STATUS_DRAFT) + self.assertFalse(report_db.invited) # Set by view only if non-draft + self.assertFalse(report_db.flagged) # Set by view only if non-draft + + self.assertEqual(report_db.clarity, 60) + self.assertEqual(report_db.formatting, 4) + self.assertEqual(report_db.grammar, 5) + self.assertEqual(report_db.originality, 100) + self.assertEqual(report_db.qualification, 3) + self.assertEqual(report_db.significance, 0) + self.assertEqual(report_db.validity, 60) + self.assertEqual(report_db.remarks_for_editors, 'Lorem Ipsum1') + self.assertEqual(report_db.requested_changes, 'Lorem Ipsum2') + self.assertEqual(report_db.strengths, 'Lorem Ipsum3') + self.assertEqual(report_db.weaknesses, 'Lorem Ipsum4') + + @tag('reports') + def test_post_report(self): + '''Test response of view if report submitted.''' + response = self.client.post(self.target, {**self.TEST_DATA, 'save_submit': 'True'}) + + # Check if user is redirected + self.assertEqual(response.status_code, 302) + + # Briefly do cross checks if report submit is complete + report_db = Report.objects.last() + self.assertEqual(report_db.status, STATUS_UNVETTED) + + # Check if invited value has only changed if valid to do so + self.assertIsNone(self.submission.referee_invitations + .filter(referee=self.current_contrib).first()) + self.assertFalse(report_db.invited) + + # Cross-check if flagged can't be assigned, as this should only happen if author is + # flagged on the submission involved + self.assertIsNone(self.submission.referees_flagged) + self.assertFalse(report_db.flagged) + + self.assertTrue(report_db.anonymous) + self.assertEqual(report_db.clarity, 60) + self.assertEqual(report_db.formatting, 4) + self.assertEqual(report_db.grammar, 5) + self.assertEqual(report_db.originality, 100) + self.assertEqual(report_db.qualification, 3) + self.assertEqual(report_db.significance, 0) + self.assertEqual(report_db.validity, 60) + self.assertEqual(report_db.remarks_for_editors, 'Lorem Ipsum1') + self.assertEqual(report_db.requested_changes, 'Lorem Ipsum2') + self.assertEqual(report_db.strengths, 'Lorem Ipsum3') + self.assertEqual(report_db.weaknesses, 'Lorem Ipsum4') + + @tag('reports') + def test_post_report_flagged_author(self): + '''Test if report is `flagged` if author is flagged on related submission.''' + report_deadline = Faker().date_time_between(start_date="now", end_date="+30d", tzinfo=None) + submission = EICassignedSubmissionFactory(reporting_deadline=report_deadline, + referees_flagged=str(self.current_contrib)) + submission.authors.remove(self.current_contrib) + submission.authors_false_claims.add(self.current_contrib) + + target = reverse('submissions:submit_report', args=(submission.arxiv_identifier_w_vn_nr,)) + client = Client() + + # Login and call view + self.assertTrue(client.login(username="Test", password="testpw")) + self.TEST_DATA['save_submit'] = 'Submit your report' + response = client.post(target, self.TEST_DATA) + self.assertEqual(response.status_code, 302) + + # Briefly checks if report is valid + report_db = Report.objects.last() + self.assertEqual(report_db.status, STATUS_UNVETTED) + self.assertTrue(report_db.flagged) + + @tag('reports') + def test_post_report_with_invitation(self): + '''Test if report is submission is valid using invitation.''' + AcceptedRefereeInvitationFactory(submission=self.submission, referee=self.current_contrib) + + # Post Data + response = self.client.post(self.target, {**self.TEST_DATA, 'save_submit': 'True'}) + self.assertEqual(response.status_code, 302) + + # Briefly checks if report is valid + report_db = Report.objects.last() + self.assertEqual(report_db.status, STATUS_UNVETTED) + self.assertTrue(report_db.invited) + + # Check if Invitation has changed correctly + invitation = RefereeInvitation.objects.last() + self.assertEqual(invitation.referee, self.current_contrib) + self.assertEqual(invitation.submission, self.submission) + self.assertTrue(invitation.fulfilled) diff --git a/submissions/urls.py b/submissions/urls.py index 6191c3b6638edf6933853c961bcfc033ef83a840..6e776fcf71b60a8500c7f7fc8657ac7eb0f9e8e2 100644 --- a/submissions/urls.py +++ b/submissions/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import include, url +from django.conf.urls import url from django.views.generic import TemplateView from . import views @@ -6,7 +6,8 @@ from . import views urlpatterns = [ # Submissions url(r'^$', views.SubmissionListView.as_view(), name='submissions'), - url(r'^browse/(?P<discipline>[a-z]+)/(?P<nrweeksback>[0-9]{1,3})/$', views.SubmissionListView.as_view(), name='browse'), + url(r'^browse/(?P<discipline>[a-z]+)/(?P<nrweeksback>[0-9]{1,3})/$', + views.SubmissionListView.as_view(), name='browse'), url(r'^sub_and_ref_procedure$', TemplateView.as_view(template_name='submissions/sub_and_ref_procedure.html'), name='sub_and_ref_procedure'), @@ -69,17 +70,12 @@ urlpatterns = [ views.communication, name='communication'), url(r'^eic_recommendation/(?P<arxiv_identifier_w_vn_nr>[0-9]{4,}.[0-9]{5,}v[0-9]{1,2})$', views.eic_recommendation, name='eic_recommendation'), - url(r'^cycle/(?P<arxiv_identifier_w_vn_nr>[0-9]{4,}.[0-9]{5,}v[0-9]{1,2})/submit$', views.cycle_form_submit, - name='cycle_confirmation'), + url(r'^cycle/(?P<arxiv_identifier_w_vn_nr>[0-9]{4,}.[0-9]{5,}v[0-9]{1,2})/submit$', + views.cycle_form_submit, name='cycle_confirmation'), # Reports url(r'^submit_report/(?P<arxiv_identifier_w_vn_nr>[0-9]{4,}.[0-9]{5,}v[0-9]{1,2})$', views.submit_report, name='submit_report'), - url(r'^submit_report_ack$', - TemplateView.as_view(template_name='submissions/submit_report_ack.html'), name='submit_report_ack'), - url(r'^vet_submitted_reports$', - views.vet_submitted_reports, name='vet_submitted_reports'), - url(r'^vet_submitted_report_ack/(?P<report_id>[0-9]+)$', - views.vet_submitted_report_ack, name='vet_submitted_report_ack'), + url(r'^vet_submitted_reports$', views.vet_submitted_reports, name='vet_submitted_reports'), # Voting url(r'^prepare_for_voting/(?P<rec_id>[0-9]+)$', views.prepare_for_voting, name='prepare_for_voting'), url(r'^vote_on_rec/(?P<rec_id>[0-9]+)$', views.vote_on_rec, name='vote_on_rec'), diff --git a/submissions/utils.py b/submissions/utils.py index 29ed7e85be4febc089c51bd4b476b66f3db4c21a..302496959f2b8ee166e16745553014af1323b828 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 NO_REQUIRED_ACTION_STATUSES,\ +from .constants import NO_REQUIRED_ACTION_STATUSES, STATUS_VETTED, STATUS_UNCLEAR,\ + STATUS_INCORRECT, STATUS_NOT_USEFUL, STATUS_NOT_ACADEMIC,\ STATUS_REVISION_REQUESTED, STATUS_EIC_ASSIGNED,\ STATUS_RESUBMISSION_INCOMING, STATUS_AWAITING_ED_REC from .exceptions import CycleUpdateDeadlineError @@ -1023,7 +1024,7 @@ class SubmissionUtils(BaseMailUtil): '<p>Dear {{ ref_title }} {{ ref_last_name }},</p>' '<p>Many thanks for your Report on Submission</p>' '<p>{{ sub_title }}</p>\n<p>by {{ author_list }}.</p>') - if cls.report.status == 1: + if cls.report.status == STATUS_VETTED: email_text += ('\n\nYour Report has been vetted through and is viewable at ' 'https://scipost.org/submissions/' + cls.report.submission.arxiv_identifier_w_vn_nr + '.') @@ -1047,7 +1048,7 @@ class SubmissionUtils(BaseMailUtil): '\n\nThe SciPost Team.') email_text_html += ('<p>Many thanks for your collaboration,</p>' '<p>The SciPost Team.</p>') - if cls.report.status != 1: + if cls.report.status != STATUS_VETTED: if cls.email_response is not None: email_text += '\n\nAdditional info from the Editor-in-charge: \n' email_text += cls.email_response @@ -1078,7 +1079,8 @@ class SubmissionUtils(BaseMailUtil): 'requested_changes': cls.report.requested_changes, 'remarks_for_editors': cls.report.remarks_for_editors, }) - if cls.report.status < 0: + if cls.report.status in [STATUS_UNCLEAR, STATUS_INCORRECT, + STATUS_NOT_USEFUL, STATUS_NOT_ACADEMIC]: email_context['refusal_reason'] = cls.report.get_status_display() email_text_html += '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) diff --git a/submissions/views.py b/submissions/views.py index a3764b96f5a4e937c229c7f4026b76b3739b9952..cd0cfd8d71ee905b868db8975f5cabed7b455dc7 100644 --- a/submissions/views.py +++ b/submissions/views.py @@ -15,8 +15,9 @@ from django.utils.decorators import method_decorator from guardian.decorators import permission_required_or_403 from guardian.shortcuts import assign_perm -from .constants import SUBMISSION_STATUS_VOTING_DEPRECATED,\ - SUBMISSION_STATUS_PUBLICLY_INVISIBLE, SUBMISSION_STATUS, ED_COMM_CHOICES +from .constants import SUBMISSION_STATUS_VOTING_DEPRECATED, STATUS_VETTED, STATUS_EIC_ASSIGNED,\ + SUBMISSION_STATUS_PUBLICLY_INVISIBLE, SUBMISSION_STATUS, ED_COMM_CHOICES,\ + STATUS_DRAFT from .models import Submission, EICRecommendation, EditorialAssignment,\ RefereeInvitation, Report, EditorialCommunication from .forms import SubmissionIdentifierForm, RequestSubmissionForm, SubmissionSearchForm,\ @@ -167,12 +168,18 @@ def submission_detail(request, arxiv_identifier_w_vn_nr): submission = get_object_or_404(Submission, arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) try: is_author = request.user.contributor in submission.authors.all() - is_author_unchecked = (not is_author and not - (request.user.contributor in submission.authors_false_claims.all()) and - (request.user.last_name in submission.author_list)) + is_author_unchecked = (not is_author and + request.user.contributor not in submission.authors_false_claims.all() + and request.user.last_name in submission.author_list) + try: + unfinished_report_for_user = (submission.reports.in_draft() + .get(author=request.user.contributor)) + except Report.DoesNotExist: + unfinished_report_for_user = None except AttributeError: is_author = False is_author_unchecked = False + unfinished_report_for_user = None if (submission.status in SUBMISSION_STATUS_PUBLICLY_INVISIBLE and not request.user.groups.filter(name__in=['SciPost Administrators', 'Editorial Administrators', @@ -187,8 +194,10 @@ def submission_detail(request, arxiv_identifier_w_vn_nr): invited_reports = submission.reports.accepted().filter(invited=True) contributed_reports = submission.reports.accepted().filter(invited=False) - comments = submission.comments.vetted().filter(is_author_reply=False).order_by('-date_submitted') - author_replies = submission.comments.vetted().filter(is_author_reply=True).order_by('-date_submitted') + comments = (submission.comments.vetted() + .filter(is_author_reply=False).order_by('-date_submitted')) + author_replies = (submission.comments.vetted() + .filter(is_author_reply=True).order_by('-date_submitted')) try: recommendation = (EICRecommendation.objects.filter_for_user(request.user) @@ -202,6 +211,7 @@ def submission_detail(request, arxiv_identifier_w_vn_nr): 'comments': comments, 'invited_reports': invited_reports, 'contributed_reports': contributed_reports, + 'unfinished_report_for_user': unfinished_report_for_user, 'author_replies': author_replies, 'form': form, 'is_author': is_author, @@ -520,8 +530,8 @@ def editorial_page(request, arxiv_identifier_w_vn_nr): .filter(arxiv_identifier_wo_vn_nr=submission.arxiv_identifier_wo_vn_nr) .exclude(pk=submission.id)) ref_invitations = RefereeInvitation.objects.filter(submission=submission) - nr_reports_to_vet = (Report.objects - .filter(status=0, submission=submission, + nr_reports_to_vet = (Report.objects.awaiting_vetting() + .filter(submission=submission, submission__editor_in_charge=request.user.contributor) .count()) communications = (EditorialCommunication.objects @@ -861,9 +871,9 @@ def close_refereeing_round(request, arxiv_identifier_w_vn_nr): @permission_required('scipost.can_oversee_refereeing', raise_exception=True) def refereeing_overview(request): - submissions_under_refereeing = Submission.objects.filter( - status='EICassigned').order_by('submission_date') - context= {'submissions_under_refereeing': submissions_under_refereeing,} + submissions_under_refereeing = (Submission.objects.filter(status=STATUS_EIC_ASSIGNED) + .order_by('submission_date')) + context = {'submissions_under_refereeing': submissions_under_refereeing} return render(request, 'submissions/refereeing_overview.html', context) @@ -979,28 +989,42 @@ def eic_recommendation(request, arxiv_identifier_w_vn_nr): ########### # Reports ########### - @login_required @permission_required('scipost.can_referee', raise_exception=True) @transaction.atomic def submit_report(request, arxiv_identifier_w_vn_nr): - submission = get_object_or_404(Submission.objects.all(), + """ + A form to submit a report on a submission will be shown and processed here. + + Important checks to be aware of include an author check for the submission, + has the reporting deadline not been reached yet and does there exist any invitation + for the current user on this submission. + """ + submission = get_object_or_404(Submission.objects.open_for_reporting(), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) # Check whether the user can submit a report: - is_author = request.user.contributor in submission.authors.all() + current_contributor = request.user.contributor + is_author = current_contributor in submission.authors.all() is_author_unchecked = (not is_author and not - (request.user.contributor in submission.authors_false_claims.all()) and + (current_contributor in submission.authors_false_claims.all()) and (request.user.last_name in submission.author_list)) - try: - invitation = RefereeInvitation.objects.get(submission=submission, - referee=request.user.contributor) - except RefereeInvitation.DoesNotExist: - invitation = None + invitation = submission.referee_invitations.filter(referee=current_contributor).first() errormessage = None - if not invitation and timezone.now() > submission.reporting_deadline + datetime.timedelta(days=1): - errormessage = ('The reporting deadline has passed. You cannot submit' - ' a Report anymore.') + if not invitation: + if timezone.now() > submission.reporting_deadline + datetime.timedelta(days=1): + errormessage = ('The reporting deadline has passed. You cannot submit' + ' a Report anymore.') + elif not submission.open_for_reporting: + errormessage = ('Reporting for this submission has closed. You cannot submit' + ' a Report anymore.') + + if errormessage: + # Remove old drafts from the database + reports_in_draft_to_remove = (submission.reports.in_draft() + .filter(author=current_contributor)) + if reports_in_draft_to_remove: + reports_in_draft_to_remove.delete() if is_author: errormessage = 'You are an author of this Submission and cannot submit a Report.' if is_author_unchecked: @@ -1011,27 +1035,23 @@ def submit_report(request, arxiv_identifier_w_vn_nr): messages.warning(request, errormessage) return redirect(reverse('scipost:personal_page')) - form = ReportForm(request.POST or None) - if form.is_valid(): - author = request.user.contributor - newreport = form.save(commit=False) - newreport.submission = submission - newreport.author = request.user.contributor - if invitation: - invitation.fulfilled = True - newreport.invited = True - invitation.save() - - if submission.referees_flagged is not None: - if author.user.last_name in submission.referees_flagged: - newreport.flagged = True - - newreport.date_submitted = timezone.now() - newreport.save() + # Find and fill earlier version of report + try: + report_in_draft = submission.reports.in_draft().get(author=current_contributor) + except Report.DoesNotExist: + report_in_draft = None + form = ReportForm(request.POST or None, instance=report_in_draft) - # Update user stats - author.nr_reports = Report.objects.filter(author=author).count() - author.save() + # Check if data sent is valid + if form.is_valid(): + newreport = form.save(submission, current_contributor) + if newreport.status == STATUS_DRAFT: + messages.success(request, ('Your Report has been saved. ' + 'You may leave the page and finish it later.')) + context = {'submission': submission, 'form': form} + return render(request, 'submissions/submit_report.html', context) + + # Send mails if report is submitted SubmissionUtils.load({'report': newreport}, request) SubmissionUtils.email_EIC_report_delivered() SubmissionUtils.email_referee_report_delivered() @@ -1049,42 +1069,39 @@ def submit_report(request, arxiv_identifier_w_vn_nr): @login_required @permission_required('scipost.can_take_charge_of_submissions', raise_exception=True) def vet_submitted_reports(request): - contributor = Contributor.objects.get(user=request.user) - report_to_vet = Report.objects.filter(status=0, - submission__editor_in_charge=contributor).first() - form = VetReportForm() - context = {'contributor': contributor, 'report_to_vet': report_to_vet, 'form': form} - return(render(request, 'submissions/vet_submitted_reports.html', context)) + """ + Reports with status `unvetted` will be shown one-by-one. An user may only + vet reports of submissions he/she is EIC of. + After vetting an email is sent to the report author, bcc EIC. If report + has not been refused, the submission author is also mailed. + """ + contributor = Contributor.objects.get(user=request.user) + report_to_vet = (Report.objects.awaiting_vetting() + .select_related('submission') + .filter(submission__editor_in_charge=contributor).first()) -@permission_required('scipost.can_take_charge_of_submissions', raise_exception=True) -@transaction.atomic -def vet_submitted_report_ack(request, report_id): - report = get_object_or_404(Report, pk=report_id, - submission__editor_in_charge=request.user.contributor) - form = VetReportForm(request.POST or None) + form = VetReportForm(request.POST or None, initial={'report': report_to_vet}) if form.is_valid(): - report.vetted_by = request.user.contributor - if form.cleaned_data['action_option'] == '1': - # accept the report as is - report.status = 1 - report.save() - report.submission.latest_activity = timezone.now() - report.submission.save() - elif form.cleaned_data['action_option'] == '2': - # the report is simply rejected - report.status = int(form.cleaned_data['refusal_reason']) - report.save() + report = form.process_vetting(request.user.contributor) + # email report author SubmissionUtils.load({'report': report, 'email_response': form.cleaned_data['email_response_field']}) SubmissionUtils.acknowledge_report_email() # email report author, bcc EIC - if report.status == 1: + if report.status == STATUS_VETTED: SubmissionUtils.send_author_report_received_email() - messages.success(request, 'Submitted Report vetted.') - return redirect(reverse('submissions:editorial_page', - args=[report.submission.arxiv_identifier_w_vn_nr])) - return redirect(reverse('submissions:vet_submitted_reports')) + + message = 'Submitted Report vetted for <a href="%s">%s</a>.' % ( + reverse('submissions:editorial_page', + args=(report.submission.arxiv_identifier_w_vn_nr,)), + report.submission.arxiv_identifier_w_vn_nr + ) + messages.success(request, message) + # Redirect instead of render to loose the POST call and make it a GET + return redirect(reverse('submissions:vet_submitted_reports')) + context = {'contributor': contributor, 'report_to_vet': report_to_vet, 'form': form} + return render(request, 'submissions/vet_submitted_reports.html', context) @permission_required('scipost.can_prepare_recommendations_for_voting', raise_exception=True)