diff --git a/colleges/managers.py b/colleges/managers.py index 8dfcbb54c42020d959f96b8bd7f0a5a2283411f7..638887cb1cde17e3fe2f27168b3d2e1b49c62faf 100644 --- a/colleges/managers.py +++ b/colleges/managers.py @@ -21,7 +21,11 @@ class FellowQuerySet(models.QuerySet): Q(start_date__isnull=True, until_date__gte=today) | Q(start_date__lte=today, until_date__gte=today) | Q(start_date__isnull=True, until_date__isnull=True) - ).order_by('contributor__user__last_name') + ).ordered() + + def ordered(self): + """Return ordered queryset explicitly, since this may have big affect on performance.""" + return self.order_by('contributor__user__last_name') def return_active_for_submission(self, submission): """ diff --git a/colleges/permissions.py b/colleges/permissions.py index edfd198daba4bae9d7879cd27f371cb384fb8ee6..1130c79fce04a6cd65a74abb9d782f8b62922f68 100644 --- a/colleges/permissions.py +++ b/colleges/permissions.py @@ -7,9 +7,7 @@ from django.core.exceptions import PermissionDenied def fellowship_required(): - """ - Require user to have any Fellowship or Administrational permissions. - """ + """Require user to have any Fellowship or Administrational permissions.""" def test(u): if u.is_authenticated(): if hasattr(u, 'contributor') and u.contributor.fellowships.exists(): @@ -20,9 +18,7 @@ def fellowship_required(): def fellowship_or_admin_required(): - """ - Require user to have any Fellowship or Administrational permissions. - """ + """Require user to have any Fellowship or Administrational permissions.""" def test(u): if u.is_authenticated(): if hasattr(u, 'contributor') and u.contributor.fellowships.exists(): diff --git a/comments/constants.py b/comments/constants.py index 3a4a26e69e27ede64830339641278fffe229b446..eb9075628daf3798a24569cf07aad32faf6ba4e3 100644 --- a/comments/constants.py +++ b/comments/constants.py @@ -12,11 +12,11 @@ STATUS_UNCLEAR = -1 STATUS_INCORRECT = -2 STATUS_NOT_USEFUL = -3 COMMENT_STATUS = ( - (STATUS_VETTED, 'vetted'), - (STATUS_PENDING, 'not yet vetted (pending)'), - (STATUS_UNCLEAR, 'rejected (unclear)'), - (STATUS_INCORRECT, 'rejected (incorrect)'), - (STATUS_NOT_USEFUL, 'rejected (not useful)'), + (STATUS_VETTED, 'Vetted'), + (STATUS_PENDING, 'Not yet vetted (pending)'), + (STATUS_UNCLEAR, 'Rejected (unclear)'), + (STATUS_INCORRECT, 'Rejected (incorrect)'), + (STATUS_NOT_USEFUL, 'Rejected (not useful)'), ) COMMENT_CATEGORIES = ( diff --git a/comments/migrations/0004_auto_20180519_1313.py b/comments/migrations/0004_auto_20180519_1313.py new file mode 100644 index 0000000000000000000000000000000000000000..bb9c0f13c1fbce0e80f3760ece1e17ea13318a92 --- /dev/null +++ b/comments/migrations/0004_auto_20180519_1313.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-05-19 11:13 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('comments', '0003_auto_20180314_1502'), + ] + + operations = [ + migrations.AlterField( + model_name='comment', + name='status', + field=models.SmallIntegerField(choices=[(1, 'Vetted'), (0, 'Not yet vetted (pending)'), (-1, 'Rejected (unclear)'), (-2, 'Rejected (incorrect)'), (-3, 'Rejected (not useful)')], default=0), + ), + ] diff --git a/comments/models.py b/comments/models.py index 23ceb52188aef09e7d162499b8e09e2a88786b4f..f822ca0217824d5eb1aba8422865af1533e2f4ba 100644 --- a/comments/models.py +++ b/comments/models.py @@ -17,7 +17,9 @@ from scipost.models import Contributor from commentaries.constants import COMMENTARY_PUBLISHED from .behaviors import validate_file_extension, validate_max_file_size -from .constants import COMMENT_STATUS, STATUS_PENDING +from .constants import ( + COMMENT_STATUS, STATUS_PENDING, STATUS_UNCLEAR, STATUS_INCORRECT, STATUS_NOT_USEFUL, + STATUS_VETTED) from .managers import CommentQuerySet @@ -126,6 +128,21 @@ class Comment(TimeStampedModel): else: raise Exception + @property + def is_vetted(self): + """Check if Comment is vetted.""" + return self.status == STATUS_VETTED + + @property + def is_unvetted(self): + """Check if Comment is awaiting vetting.""" + return self.status == STATUS_PENDING + + @property + def is_rejected(self): + """Check if Comment is rejected.""" + return self.status in [STATUS_UNCLEAR, STATUS_INCORRECT, STATUS_NOT_USEFUL] + def create_doi_label(self): self.doi_label = 'SciPost.Comment.' + str(self.id) self.save() diff --git a/comments/templates/comments/_comment_identifier.html b/comments/templates/comments/_comment_identifier.html index 9b7ac2da98169b92f313d3ddd9fa041ce0bc3d40..618e5fca7d342a261902de36609cc5c864528968 100644 --- a/comments/templates/comments/_comment_identifier.html +++ b/comments/templates/comments/_comment_identifier.html @@ -6,9 +6,13 @@ <div class="commentid" id="comment_id{{ comment.id }}"> <h3> - {% if request.user.contributor and request.user.contributor == comment.core_content_object.editor_in_charge or is_edcol_admin and request.user|is_not_author_of_submission:comment.core_content_object.arxiv_identifier_w_vn_nr %} - {% if comment.anonymous %}(chose public anonymity) {% endif %}<a href="{{ comment.author.get_absolute_url }}">{{ comment.author.user.first_name }} {{ comment.author.user.last_name }}</a> - on {{ comment.date_submitted|date:'Y-m-d' }} + {% if request.user.contributor and request.user.contributor == comment.core_content_object.editor_in_charge or is_edcol_admin %} + {% if request.user|is_possible_author_of_submission:comment.core_content_object %} + Anonymous on {{comment.date_submitted|date:'Y-m-d'}} + {% else %} + {% if comment.anonymous %}(chose public anonymity) {% endif %}<a href="{{ comment.author.get_absolute_url }}">{{ comment.author.user.first_name }} {{ comment.author.user.last_name }}</a> + on {{ comment.date_submitted|date:'Y-m-d' }} + {% endif %} {% elif comment.anonymous %} Anonymous on {{comment.date_submitted|date:'Y-m-d'}} {% else %} diff --git a/journals/forms.py b/journals/forms.py index 3bda2b4d1a31926f423b4cbdc89a037547cf1128..ee50f2522245e2e98af619cb787c90047598e9f3 100644 --- a/journals/forms.py +++ b/journals/forms.py @@ -31,6 +31,7 @@ from production.models import ProductionEvent from production.signals import notify_stream_status_change from scipost.forms import RequestFormMixin from scipost.services import DOICaller +from submissions.constants import STATUS_PUBLISHED from submissions.models import Submission @@ -624,7 +625,7 @@ class PublicationPublishForm(RequestFormMixin, forms.ModelForm): # Mark the submission as having been published: submission = self.instance.accepted_submission submission.published_as = self.instance - submission.status = 'published' + submission.status = STATUS_PUBLISHED submission.save() # Add SubmissionEvents diff --git a/journals/migrations/0027_auto_20180414_1627.py b/journals/migrations/0027_auto_20180414_1627.py new file mode 100644 index 0000000000000000000000000000000000000000..de6436d562ae0b80a2dc9b2474e08bc14d1ac3ca --- /dev/null +++ b/journals/migrations/0027_auto_20180414_1627.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-14 14:27 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0026_auto_20180327_1937'), + ] + + operations = [ + migrations.AlterField( + model_name='publication', + name='doi_label', + field=models.CharField(db_index=True, max_length=200, unique=True, validators=[django.core.validators.RegexValidator('^(SciPostPhysProc|SciPostPhysSel|SciPostPhysLectNotes|SciPostPhys).[0-9]+(.[0-9]+.[0-9]{3,})?$', 'Only valid DOI expressions are allowed (`[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,}` or `[a-zA-Z]+.[0-9]+`)')]), + ), + ] diff --git a/journals/migrations/0028_merge_20180426_1023.py b/journals/migrations/0028_merge_20180426_1023.py new file mode 100644 index 0000000000000000000000000000000000000000..3d78290e6be0e61d1179827d3315bd9afd3f68d7 --- /dev/null +++ b/journals/migrations/0028_merge_20180426_1023.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-26 08:23 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0027_auto_20180414_1627'), + ('journals', '0027_auto_20180414_2053'), + ] + + operations = [ + ] diff --git a/journals/migrations/0029_merge_20180519_1308.py b/journals/migrations/0029_merge_20180519_1308.py new file mode 100644 index 0000000000000000000000000000000000000000..aee7261a6f4e6f34da8a29733279e14cc10c72bd --- /dev/null +++ b/journals/migrations/0029_merge_20180519_1308.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-05-19 11:08 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0028_publication_number_of_citations'), + ('journals', '0028_merge_20180426_1023'), + ] + + operations = [ + ] diff --git a/package.json b/package.json index 27c87b864b31e483f9dfb6fea3e702ab646d9d36..d016723390225a29a1dd65e2e610e90218a7b962 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "homepage": "https://www.scipost.org", "devDependencies": { "ajv": "^5.2.2", - "bootstrap": "^4.0.0", + "bootstrap": "^4.1.0", "bootstrap-loader": "^2.1.0", "clean-webpack-plugin": "^0.1.15", "css-loader": "^0.28.4", @@ -32,7 +32,7 @@ "jquery-ui": "^1.12.1", "node-loader": "^0.6.0", "node-sass": "^4.4.0", - "popper.js": "^1.14.1", + "popper.js": "^1.14.3", "postcss-load-config": "^1.2.0", "postcss-loader": "^2.0.6", "resolve-url-loader": "^1.6.1", diff --git a/production/utils.py b/production/utils.py index 9e2b31de48d0109d0bb9e3e50bce8c8bb2bd29eb..1691347a2ea47f89243787959209630529872fe5 100644 --- a/production/utils.py +++ b/production/utils.py @@ -1,6 +1,8 @@ __copyright__ = "Copyright 2016-2018, Stichting SciPost (SciPost Foundation)" __license__ = "AGPL v3" +from django.contrib.auth.models import Group +from guardian.shortcuts import assign_perm from common.utils import BaseMailUtil @@ -13,6 +15,18 @@ def proofs_slug_to_id(slug): return int(slug) - 8932 +def get_or_create_production_stream(submission): + """Get or create a ProductionStream for the given Submission.""" + from .models import ProductionStream + + prodstream, created = ProductionStream.objects.get_or_create(submission=submission) + if created: + ed_admins = Group.objects.get(name='Editorial Administrators') + assign_perm('can_perform_supervisory_actions', ed_admins, prodstream) + assign_perm('can_work_for_stream', ed_admins, prodstream) + return prodstream + + class ProductionUtils(BaseMailUtil): mail_sender = 'no-reply@scipost.org' mail_sender_title = 'SciPost Production' diff --git a/scipost/feeds.py b/scipost/feeds.py index e5426b9bf44f00ca6709654d9637da739cc56dea..1e2501c9edf458c6f9f203c437b5ca5220e81d36 100644 --- a/scipost/feeds.py +++ b/scipost/feeds.py @@ -15,7 +15,6 @@ from commentaries.models import Commentary from journals.models import Publication from news.models import NewsItem from scipost.models import subject_areas_dict -from submissions.constants import SUBMISSION_STATUS_PUBLICLY_INVISIBLE from submissions.models import Submission from theses.models import ThesisLink @@ -98,12 +97,11 @@ class LatestSubmissionsFeedRSS(Feed): if subject_area != '': queryset = Submission.objects.filter( Q(subject_area=subject_area) | Q(secondary_areas__contains=[subject_area]) - ).exclude(status__in=SUBMISSION_STATUS_PUBLICLY_INVISIBLE - ).order_by('-submission_date')[:10] + ).filter(visible_public=True).order_by('-submission_date')[:10] queryset.subject_area = subject_area else: - queryset = Submission.objects.exclude(status__in=SUBMISSION_STATUS_PUBLICLY_INVISIBLE - ).order_by('-submission_date')[:10] + queryset = Submission.objects.filter( + visible_public=True).order_by('-submission_date')[:10] queryset.subject_area = None return queryset diff --git a/scipost/management/commands/add_groups_and_permissions.py b/scipost/management/commands/add_groups_and_permissions.py index d8da56200da91a2974e81fab4ba6cc1f4a38a5f3..6e060bd60948f810bfaca8bc1ea7e233e2bf6fca 100644 --- a/scipost/management/commands/add_groups_and_permissions.py +++ b/scipost/management/commands/add_groups_and_permissions.py @@ -17,7 +17,6 @@ class Command(BaseCommand): def handle(self, *args, verbose=True, **options): """Append all user Groups and setup a Contributor roles to user.""" - # Create Groups SciPostAdmin, created = Group.objects.get_or_create(name='SciPost Administrators') FinancialAdmin, created = Group.objects.get_or_create(name='Financial Administrators') @@ -40,8 +39,6 @@ class Command(BaseCommand): # Create Permissions content_type = ContentType.objects.get_for_model(Contributor) - content_type_contact = ContentType.objects.get_for_model(Contact) - content_type_draft_invitation = ContentType.objects.get_for_model(DraftInvitation) # Supporting Partners can_manage_SPB, created = Permission.objects.get_or_create( @@ -188,6 +185,10 @@ class Command(BaseCommand): codename='can_oversee_refereeing', name='Can oversee refereeing', content_type=content_type) + can_run_pre_screening, created = Permission.objects.get_or_create( + codename='can_run_pre_screening', + name='Can run pre-screening on Submissions', + content_type=content_type) # Reports can_manage_reports, created = Permission.objects.get_or_create( @@ -331,6 +332,7 @@ class Command(BaseCommand): can_assign_submissions, can_do_plagiarism_checks, can_oversee_refereeing, + can_run_pre_screening, can_prepare_recommendations_for_voting, can_manage_college_composition, can_fix_College_decision, diff --git a/scipost/static/scipost/assets/config/preconfig.scss b/scipost/static/scipost/assets/config/preconfig.scss index 7d9fe9c8c204335662b7a470525619c76bfb6b36..536ffe6b45367fc4609831e9942b1cc2002b025f 100644 --- a/scipost/static/scipost/assets/config/preconfig.scss +++ b/scipost/static/scipost/assets/config/preconfig.scss @@ -60,7 +60,6 @@ $alert-border-radius: $base-border-radius; // Cards // $card-border-radius: $base-border-radius; -$card-border-color: $gray-200; $card-spacer-x: 0.75rem; $card-spacer-y: 0.5rem; $card-shadow-color: #ccc; @@ -125,7 +124,7 @@ $close-font-weight: 100; // Tables // -$table-cell-padding: 0.25rem 0.5rem; +$table-body-cell-padding: 0.25rem 0.5rem; // Tooltip diff --git a/scipost/static/scipost/assets/css/_cards.scss b/scipost/static/scipost/assets/css/_cards.scss index 6e1e57b63d1eba55342a323af040498dd0d65d1e..2a927df994d200cac69f44f5dcfd6df64767e8a1 100644 --- a/scipost/static/scipost/assets/css/_cards.scss +++ b/scipost/static/scipost/assets/css/_cards.scss @@ -36,17 +36,6 @@ .card-outline-secondary { border-color: #f1f1f1; } - -// .card-header { -// padding: 0.5rem 0; -// margin: 0 0.75rem; -// } - -// .card-footer { -// padding: 0.75rem 0 0 0; -// margin: 0 0.75rem 0.75rem 0.75rem; -// } - .list-group-item > .card-body { padding: 0.5rem; } diff --git a/scipost/static/scipost/assets/css/_labels.scss b/scipost/static/scipost/assets/css/_labels.scss index 6105f7d481ac7b7498b1fa701ef59ffb0e17d294..9b475466ed430c8ef917cd8b8ed04cfe667e46ca 100644 --- a/scipost/static/scipost/assets/css/_labels.scss +++ b/scipost/static/scipost/assets/css/_labels.scss @@ -3,9 +3,9 @@ // -------------------------------------------------- // For each of Bootstrap's buttons, define text, background and border color. -$label-padding-x: 5px !default; -$label-padding-y: 3px !default; -$label-line-height: inherit; +$label-padding-x: 0.6rem !default; +$label-padding-y: 0.25rem !default; +$label-line-height: 1.2; $label-font-weight: $font-weight-normal !default; $label-box-shadow: none; $label-font-size: inherit; @@ -38,10 +38,10 @@ $label-danger-color: $white; $label-danger-bg: $red !default; $label-danger-border: $red !default; -$label-padding-x-sm: .5rem; -$label-padding-y-sm: .1rem; +$label-padding-x-sm: 0.25rem; +$label-padding-y-sm: 0.15rem; -$label-padding-x-lg: 1rem !default; +$label-padding-x-lg: 0.75rem !default; $label-padding-y-lg: 0.5rem !default; $label-label-spacing-y: .5rem !default; @@ -55,9 +55,9 @@ $label-border-radius-sm: 4px; $label-transition: all .2s ease-in-out !default; .label { - display: inline; + display: inline-block; font-weight: $label-font-weight; - line-height: $label-line-height; + line-height: $label-line-height !important; text-align: center; white-space: nowrap; vertical-align: middle; diff --git a/scipost/static/scipost/assets/css/_tables.scss b/scipost/static/scipost/assets/css/_tables.scss index 7c3d0fdc6c1d930fd04287fd8b90807a6e4410b8..175b068a19f5ed8027637ead80cf5d0ae12b3ed3 100644 --- a/scipost/static/scipost/assets/css/_tables.scss +++ b/scipost/static/scipost/assets/css/_tables.scss @@ -1,8 +1,13 @@ .table { - th, + &.v-center { + th, + td { + vertical-align: middle; + } + } + td { - padding: $table-cell-padding; - vertical-align: middle; + padding: $table-body-cell-padding; } } diff --git a/scipost/templates/partials/scipost/personal_page/submissions.html b/scipost/templates/partials/scipost/personal_page/submissions.html index 9dc38d1e5e07e686bdacece95ab11ed8e5699d7b..55b261d029084f8bccac40ccb1ed40427f32af00 100644 --- a/scipost/templates/partials/scipost/personal_page/submissions.html +++ b/scipost/templates/partials/scipost/personal_page/submissions.html @@ -32,7 +32,7 @@ {% if sub.editor_in_charge %} <a href="{% url 'submissions:communication' sub.arxiv_identifier_w_vn_nr 'AtoE' %}">Write to the Editor-in-charge</a> {% endif %} - {% if sub.status == 'revision_requested' %} + {% if sub.revision_requested %} · <a href="{% url 'submissions:prefill_using_identifier' %}?identifier={{ sub.arxiv_identifier_wo_vn_nr }}">Resubmit this manuscript</a> {% endif %} </p> diff --git a/scipost/views.py b/scipost/views.py index 9e4ee6cb791cb4bb3db7b6c3e3e199cfbd375f63..b58d42b0cc5b6b1b82cb6c1fbe27c0d33d3c5a09 100644 --- a/scipost/views.py +++ b/scipost/views.py @@ -443,8 +443,8 @@ def _personal_page_editorial_actions(request): context['nr_reg_to_vet'] = Contributor.objects.awaiting_vetting().count() context['nr_reg_awaiting_validation'] = Contributor.objects.awaiting_validation().count() context['nr_submissions_to_assign'] = Submission.objects.prescreening().count() - context['nr_recommendations_to_prepare_for_voting'] = EICRecommendation.objects.filter( - submission__status='voting_in_preparation').count() + context['nr_recommendations_to_prepare_for_voting'] = \ + EICRecommendation.objects.voting_in_preparation().count() if contributor.is_VE(): context['nr_commentary_page_requests_to_vet'] = (Commentary.objects.awaiting_vetting() @@ -461,7 +461,7 @@ def _personal_page_editorial_actions(request): if contributor.is_EdCol_Admin(): context['nr_reports_without_pdf'] = Report.objects.accepted().filter(pdf_report='').count() - context['nr_treated_submissions_without_pdf'] = Submission.objects.treated().filter( + context['nr_treated_submissions_without_pdf'] = Submission.objects.treated().public().filter( pdf_refereeing_pack='').count() return render(request, 'partials/scipost/personal_page/editorial_actions.html', context) @@ -867,7 +867,7 @@ def contributor_info(request, contributor_id): """ contributor = get_object_or_404(Contributor, pk=contributor_id) contributor_publications = Publication.objects.published().filter(authors_registered=contributor) - contributor_submissions = Submission.objects.public_unlisted().filter(authors=contributor) + contributor_submissions = Submission.objects.public_listed().filter(authors=contributor) contributor_commentaries = Commentary.objects.filter(authors=contributor) contributor_theses = ThesisLink.objects.vetted().filter(author_as_cont=contributor) contributor_comments = (Comment.objects.vetted().publicly_visible() diff --git a/submissions/admin.py b/submissions/admin.py index 872076347e9f2415d6c2b8ffa096b97b4a3686a0..55a74af1245328e21ae9bb82409b2fbceaa4dd42 100644 --- a/submissions/admin.py +++ b/submissions/admin.py @@ -107,6 +107,7 @@ class SubmissionAdmin(GuardedModelAdmin): 'fields': ( 'editor_in_charge', 'status', + ('visible_public', 'visible_pool'), 'refereeing_cycle', ('open_for_commenting', 'open_for_reporting'), 'reporting_deadline', @@ -237,7 +238,7 @@ class EICRecommendationAdminForm(forms.ModelForm): class EICRecommendationAdmin(admin.ModelAdmin): search_fields = ['submission__title'] - list_display = (submission_short_title, 'recommendation', 'active', 'version') + list_display = (submission_short_title, 'recommendation', 'status', 'active', 'version') form = EICRecommendationAdminForm diff --git a/submissions/constants.py b/submissions/constants.py index 299d132dd96e1d95c9dbef6cf972ff79f7efacf6..80cfdc7f191bc81cb63682acebee427410b00045 100644 --- a/submissions/constants.py +++ b/submissions/constants.py @@ -5,99 +5,40 @@ __license__ = "AGPL v3" from journals.constants import SCIPOST_JOURNAL_PHYSICS +# All Submission statuses +STATUS_INCOMING = 'incoming' STATUS_UNASSIGNED = 'unassigned' +STATUS_EIC_ASSIGNED = 'assigned' STATUS_ASSIGNMENT_FAILED = 'assignment_failed' -STATUS_RESUBMISSION_INCOMING = 'resubmitted_incoming' -STATUS_REVISION_REQUESTED = 'revision_requested' -STATUS_EIC_ASSIGNED = 'EICassigned' -STATUS_AWAITING_ED_REC = 'awaiting_ed_rec' -STATUS_REVIEW_CLOSED = 'review_closed' +STATUS_RESUBMITTED = 'resubmitted' STATUS_ACCEPTED = 'accepted' -STATUS_PUBLISHED = 'published' STATUS_REJECTED = 'rejected' -STATUS_REJECTED_VISIBLE = 'rejected_visible' -STATUS_RESUBMITTED = 'resubmitted' -STATUS_RESUBMITTED_REJECTED = 'resubmitted_and_rejected' -STATUS_RESUBMITTED_REJECTED_VISIBLE = 'resubmitted_and_rejected_visible' -STATUS_VOTING_IN_PREPARATION = 'voting_in_preparation' -STATUS_PUT_TO_EC_VOTING = 'put_to_EC_voting' -STATUS_EC_VOTE_COMPLETED = 'EC_vote_completed' STATUS_WITHDRAWN = 'withdrawn' +STATUS_PUBLISHED = 'published' + +# Deprecated statuses +# TODO: Make sure cycles are chosen for this status: +# STATUS_RESUBMISSION_INCOMING = 'resubmitted_incoming' + +# All possible Submission statuses SUBMISSION_STATUS = ( - (STATUS_UNASSIGNED, 'Unassigned, undergoing pre-screening'), - (STATUS_RESUBMISSION_INCOMING, 'Resubmission incoming'), - (STATUS_ASSIGNMENT_FAILED, 'Failed to assign Editor-in-charge; manuscript rejected'), + (STATUS_INCOMING, 'Submission incoming, undergoing pre-screening'), + (STATUS_UNASSIGNED, 'Unassigned, awaiting editor assignment'), (STATUS_EIC_ASSIGNED, 'Editor-in-charge assigned, manuscript under review'), - (STATUS_REVIEW_CLOSED, 'Review period closed, editorial recommendation pending'), - # If revisions required: resubmission creates a new Submission object - (STATUS_REVISION_REQUESTED, 'Editor-in-charge has requested revision'), + (STATUS_ASSIGNMENT_FAILED, 'Failed to assign Editor-in-charge; manuscript rejected'), (STATUS_RESUBMITTED, 'Has been resubmitted'), - (STATUS_RESUBMITTED_REJECTED, 'Has been resubmitted and subsequently rejected'), - (STATUS_RESUBMITTED_REJECTED_VISIBLE, - 'Has been resubmitted and subsequently rejected (still publicly visible)'), - # If acceptance/rejection: - (STATUS_VOTING_IN_PREPARATION, 'Voting in preparation (eligible Fellows being selected)'), - (STATUS_PUT_TO_EC_VOTING, 'Undergoing voting at the Editorial College'), - (STATUS_AWAITING_ED_REC, 'Awaiting Editorial Recommendation'), - (STATUS_EC_VOTE_COMPLETED, 'Editorial College voting rounded up'), (STATUS_ACCEPTED, 'Publication decision taken: accept'), (STATUS_REJECTED, 'Publication decision taken: reject'), - (STATUS_REJECTED_VISIBLE, 'Publication decision taken: reject (still publicly visible)'), - (STATUS_PUBLISHED, 'Published'), - # If withdrawn: (STATUS_WITHDRAWN, 'Withdrawn by the Authors'), + (STATUS_PUBLISHED, 'Published'), ) -SUBMISSION_HTTP404_ON_EDITORIAL_PAGE = [ - STATUS_ASSIGNMENT_FAILED, - STATUS_PUBLISHED, - STATUS_WITHDRAWN, - STATUS_REJECTED, - STATUS_REJECTED_VISIBLE, -] - -SUBMISSION_STATUS_OUT_OF_POOL = SUBMISSION_HTTP404_ON_EDITORIAL_PAGE + [ - STATUS_RESUBMITTED -] - -SUBMISSION_EXCLUDE_FROM_REPORTING = SUBMISSION_HTTP404_ON_EDITORIAL_PAGE + [ - # STATUS_AWAITING_ED_REC, - # STATUS_REVIEW_CLOSED, - # STATUS_ACCEPTED, - # STATUS_VOTING_IN_PREPARATION, - # STATUS_PUT_TO_EC_VOTING, - STATUS_WITHDRAWN, -] - -# Submissions which are allowed/required to submit a EIC Recommendation -SUBMISSION_EIC_RECOMMENDATION_REQUIRED = [ - STATUS_EIC_ASSIGNED, - STATUS_REVIEW_CLOSED, - STATUS_AWAITING_ED_REC -] - -# Submissions which should not be viewable (except by admins, Fellows and authors) -SUBMISSION_STATUS_PUBLICLY_INVISIBLE = [ +# Submissions with these statuses never have required actions. +NO_REQUIRED_ACTION_STATUSES = [ STATUS_UNASSIGNED, - STATUS_RESUBMISSION_INCOMING, STATUS_ASSIGNMENT_FAILED, - STATUS_RESUBMITTED_REJECTED, - STATUS_REJECTED, - STATUS_WITHDRAWN, -] - -# Submissions which should not appear in search lists -SUBMISSION_STATUS_PUBLICLY_UNLISTED = SUBMISSION_STATUS_PUBLICLY_INVISIBLE + [ - STATUS_RESUBMITTED, - STATUS_RESUBMITTED_REJECTED_VISIBLE, - STATUS_PUBLISHED -] - -# Submissions for which voting on a related recommendation is deprecated: -SUBMISSION_STATUS_VOTING_DEPRECATED = [ STATUS_REJECTED, - STATUS_PUBLISHED, STATUS_WITHDRAWN, ] @@ -107,14 +48,6 @@ SUBMISSION_TYPE = ( ('Review', 'Review (candid snapshot of current research in a given area)'), ) -NO_REQUIRED_ACTION_STATUSES = [ - STATUS_UNASSIGNED, - STATUS_ASSIGNMENT_FAILED, - STATUS_RESUBMITTED_REJECTED, - STATUS_REJECTED, - STATUS_WITHDRAWN, -] - ED_COMM_CHOICES = ( ('EtoA', 'Editor-in-charge to Author'), ('EtoR', 'Editor-in-charge to Referee'), @@ -169,14 +102,17 @@ RANKING_CHOICES = ( (0, 'poor') ) +REPORT_PUBLISH_1, REPORT_PUBLISH_2, REPORT_PUBLISH_3 = 1, 2, 3 +REPORT_MINOR_REV, REPORT_MAJOR_REV = -1, -2 +REPORT_REJECT = -3 REPORT_REC = ( (None, '-'), - (1, 'Publish as Tier I (top 10% of papers in this journal, qualifies as Select)'), - (2, 'Publish as Tier II (top 50% of papers in this journal)'), - (3, 'Publish as Tier III (meets the criteria of this journal)'), - (-1, 'Ask for minor revision'), - (-2, 'Ask for major revision'), - (-3, 'Reject') + (REPORT_PUBLISH_1, 'Publish as Tier I (top 10% of papers in this journal, qualifies as Select)'), + (REPORT_PUBLISH_2, 'Publish as Tier II (top 50% of papers in this journal)'), + (REPORT_PUBLISH_3, 'Publish as Tier III (meets the criteria of this journal)'), + (REPORT_MINOR_REV, 'Ask for minor revision'), + (REPORT_MAJOR_REV, 'Ask for major revision'), + (REPORT_REJECT, 'Reject') ) # @@ -222,22 +158,14 @@ REPORT_TYPES = ( (REPORT_POST_EDREC, 'Post-Editorial Recommendation Report'), ) -POST_PUBLICATION_STATUSES = [ - STATUS_AWAITING_ED_REC, - STATUS_REVIEW_CLOSED, - STATUS_ACCEPTED, - STATUS_VOTING_IN_PREPARATION, - STATUS_PUT_TO_EC_VOTING, -] - -CYCLE_DEFAULT = 'default' -CYCLE_SHORT = 'short' -CYCLE_DIRECT_REC = 'direct_rec' -SUBMISSION_CYCLES = ( +CYCLE_UNDETERMINED = '' +CYCLE_DEFAULT, CYCLE_SHORT, CYCLE_DIRECT_REC = 'default', 'short', 'direct_rec' +SUBMISSION_CYCLE_CHOICES = ( (CYCLE_DEFAULT, 'Default cycle'), (CYCLE_SHORT, 'Short cycle'), (CYCLE_DIRECT_REC, 'Direct editorial recommendation'), ) +SUBMISSION_CYCLES = ((CYCLE_UNDETERMINED, 'Cycle undetermined'),) + SUBMISSION_CYCLE_CHOICES EVENT_GENERAL = 'gen' EVENT_FOR_EIC = 'eic' @@ -248,6 +176,16 @@ EVENT_TYPES = ( (EVENT_FOR_AUTHOR, 'Comment for author'), ) +VOTING_IN_PREP, PUT_TO_VOTING, VOTE_COMPLETED = 'voting_in_prep', 'put_to_voting', 'vote_completed' +DECISION_FIXED, DEPRECATED = 'decision_fixed', 'deprecated' +EIC_REC_STATUSES = ( + (VOTING_IN_PREP, 'Voting in preparation'), + (PUT_TO_VOTING, 'Undergoing voting at the Editorial College'), + (VOTE_COMPLETED, 'Editorial College voting rounded up'), # Seemlingly dead? + (DECISION_FIXED, 'Editorial Recommendation fixed'), + (DEPRECATED, 'Editorial Recommendation deprecated'), +) + # Use `.format()` https://docs.python.org/3.5/library/string.html#format-string-syntax # In your regex multiple brackets may occur; # Please make sure to double them in that case as per instructions in the reference! diff --git a/submissions/factories.py b/submissions/factories.py index fbd863c4b1131c6f402d95e6c4833c1171b73ab9..41829f4569a70fe8bed33043f2f65edc282dc81f 100644 --- a/submissions/factories.py +++ b/submissions/factories.py @@ -13,10 +13,10 @@ from journals.constants import SCIPOST_JOURNALS_DOMAINS from common.helpers import random_arxiv_identifier_without_version_number, random_scipost_journal,\ random_scipost_report_doi_label -from .constants import STATUS_UNASSIGNED, STATUS_EIC_ASSIGNED, STATUS_RESUBMISSION_INCOMING,\ - STATUS_PUBLISHED, SUBMISSION_TYPE, STATUS_RESUBMITTED, STATUS_VETTED,\ - REFEREE_QUALIFICATION, RANKING_CHOICES, QUALITY_SPEC, REPORT_REC,\ - REPORT_STATUSES, STATUS_UNVETTED, STATUS_DRAFT +from .constants import ( + STATUS_UNASSIGNED, STATUS_EIC_ASSIGNED, STATUS_INCOMING, 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, EICRecommendation, EditorialAssignment from faker import Faker @@ -178,7 +178,7 @@ class ResubmissionFactory(EICassignedSubmissionFactory): This Submission is a newer version of a Submission which is already known by the SciPost database. """ - status = STATUS_RESUBMISSION_INCOMING + status = STATUS_INCOMING open_for_commenting = True open_for_reporting = True is_resubmission = True @@ -361,7 +361,7 @@ class EICRecommendationFactory(factory.django.DjangoModelFactory): class EditorialAssignmentFactory(factory.django.DjangoModelFactory): """ - A EditorialAssignmentFactory should always have a `submission` explicitly assigned. This will + An EditorialAssignmentFactory should always have a `submission` explicitly assigned. This will mostly be done using the post_generation hook in any SubmissionFactory. """ submission = None diff --git a/submissions/forms.py b/submissions/forms.py index 7aa45b91c4a90757b431f9743a3f1e89715d7c64..b7e5ae4dbd635e6e9197a6942b4da6e864217565 100644 --- a/submissions/forms.py +++ b/submissions/forms.py @@ -12,19 +12,23 @@ from django.utils import timezone 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_DRAFT, STATUS_UNVETTED, REPORT_ACTION_ACCEPT, - REPORT_ACTION_REFUSE, STATUS_VETTED, EXPLICIT_REGEX_MANUSCRIPT_CONSTRAINTS, SUBMISSION_STATUS, - POST_PUBLICATION_STATUSES, REPORT_POST_EDREC, REPORT_NORMAL) + REPORT_REFUSAL_CHOICES, STATUS_REJECTED, STATUS_INCOMING, REPORT_POST_EDREC, REPORT_NORMAL, + STATUS_DRAFT, STATUS_UNVETTED, REPORT_ACTION_ACCEPT, REPORT_ACTION_REFUSE, STATUS_UNASSIGNED, + EXPLICIT_REGEX_MANUSCRIPT_CONSTRAINTS, SUBMISSION_STATUS, PUT_TO_VOTING, CYCLE_UNDETERMINED, + SUBMISSION_CYCLE_CHOICES, REPORT_PUBLISH_1, REPORT_PUBLISH_2, REPORT_PUBLISH_3, STATUS_VETTED, + REPORT_MINOR_REV, REPORT_MAJOR_REV, REPORT_REJECT, STATUS_ACCEPTED, DECISION_FIXED, DEPRECATED, + STATUS_EIC_ASSIGNED, CYCLE_DEFAULT, CYCLE_DIRECT_REC) from . import exceptions, helpers from .models import ( Submission, RefereeInvitation, Report, EICRecommendation, EditorialAssignment, iThenticateReport, EditorialCommunication) +from .signals import notify_manuscript_accepted from common.helpers import get_new_secrets_key from colleges.models import Fellowship from invitations.models import RegistrationInvitation from journals.constants import SCIPOST_JOURNAL_PHYSICS_PROC, SCIPOST_JOURNAL_PHYSICS +from production.utils import get_or_create_production_stream from scipost.constants import SCIPOST_SUBJECT_AREAS, INVITATION_REFEREEING from scipost.services import ArxivCaller from scipost.models import Contributor @@ -34,6 +38,8 @@ import iThenticate class SubmissionSearchForm(forms.Form): + """Filter a Submission queryset using basic search fields.""" + author = forms.CharField(max_length=100, required=False, label="Author(s)") title = forms.CharField(max_length=100, required=False) abstract = forms.CharField(max_length=1000, required=False) @@ -41,7 +47,7 @@ class SubmissionSearchForm(forms.Form): choices=((None, 'Show all'),) + SCIPOST_SUBJECT_AREAS[0][1])) def search_results(self): - """Return all Submission objects according to search""" + """Return all Submission objects according to search.""" return Submission.objects.public_newest().filter( title__icontains=self.cleaned_data.get('title', ''), author_list__icontains=self.cleaned_data.get('author', ''), @@ -60,7 +66,7 @@ class SubmissionPoolFilterForm(forms.Form): def search(self, queryset, current_user): if self.cleaned_data.get('status'): # Do extra check on non-required field to never show errors on template - queryset = queryset.pool_full(current_user).filter(status=self.cleaned_data['status']) + queryset = queryset.pool_editable(current_user).filter(status=self.cleaned_data['status']) else: # If no specific status if requested, just return the Pool by default queryset = queryset.pool(current_user) @@ -82,10 +88,8 @@ class SubmissionPoolFilterForm(forms.Form): ############################### class SubmissionChecks: - """ - Use this class as a blueprint containing checks which should be run - in multiple forms. - """ + """Mixin with checks run at least the Submission creation form.""" + is_resubmission = False last_submission = None @@ -130,7 +134,7 @@ class SubmissionChecks: params={'published_id': published_id}) def _submission_previous_version_is_valid_for_submission(self, identifier): - '''Check if previous submitted versions have the appropriate status.''' + """Check if previous submitted versions have the appropriate status.""" identifiers = self.identifier_into_parts(identifier) submission = (Submission.objects .filter(arxiv_identifier_wo_vn_nr=identifiers['arxiv_identifier_wo_vn_nr']) @@ -139,14 +143,14 @@ class SubmissionChecks: # If submissions are found; check their statuses if submission: self.last_submission = submission - if submission.status == STATUS_REVISION_REQUESTED: + if submission.revision_requested: self.is_resubmission = True if self.requested_by.contributor not in submission.authors.all(): error_message = ('There exists a preprint with this arXiv identifier ' 'but an earlier version number. Resubmission is only possible' ' if you are a registered author of this manuscript.') raise forms.ValidationError(error_message) - elif submission.status in [STATUS_REJECTED, STATUS_REJECTED_VISIBLE]: + elif submission.status == STATUS_REJECTED: error_message = ('This arXiv preprint has previously undergone refereeing ' 'and has been rejected. Resubmission is only possible ' 'if the manuscript has been substantially reworked into ' @@ -164,6 +168,7 @@ class SubmissionChecks: raise forms.ValidationError(error_message) def arxiv_meets_regex(self, identifier, journal_code): + """Check if arXiv identifier is valid for the Journal submitting to.""" if journal_code in EXPLICIT_REGEX_MANUSCRIPT_CONSTRAINTS.keys(): regex = EXPLICIT_REGEX_MANUSCRIPT_CONSTRAINTS[journal_code] else: @@ -178,9 +183,11 @@ class SubmissionChecks: raise forms.ValidationError(error_message, code='submitted_to_journal') def submission_is_resubmission(self): + """Check if the Submission is a resubmission.""" return self.is_resubmission def identifier_into_parts(self, identifier): + """Split the arXiv identifier into parts.""" data = { 'arxiv_identifier_w_vn_nr': identifier, 'arxiv_identifier_wo_vn_nr': identifier.rpartition('v')[0], @@ -189,6 +196,7 @@ class SubmissionChecks: return data def do_pre_checks(self, identifier): + """Group call of different checks.""" self._submission_already_exists(identifier) self._call_arxiv(identifier) self._submission_is_already_published(identifier) @@ -196,6 +204,8 @@ class SubmissionChecks: class SubmissionIdentifierForm(SubmissionChecks, forms.Form): + """Prefill SubmissionForm using this form that takes an arXiv ID only.""" + IDENTIFIER_PATTERN_NEW = r'^[0-9]{4,}\.[0-9]{4,5}v[0-9]{1,2}$' IDENTIFIER_PLACEHOLDER = 'new style (with version nr) ####.####(#)v#(#)' @@ -205,12 +215,13 @@ class SubmissionIdentifierForm(SubmissionChecks, forms.Form): widget=forms.TextInput({'placeholder': IDENTIFIER_PLACEHOLDER})) def clean_identifier(self): + """Do basic prechecks based on the arXiv ID only.""" identifier = self.cleaned_data['identifier'] self.do_pre_checks(identifier) return identifier def _gather_data_from_last_submission(self): - '''Return dictionary with data coming from previous submission version.''' + """Return dictionary with data coming from previous submission version.""" if self.submission_is_resubmission(): data = { 'is_resubmission': True, @@ -226,7 +237,7 @@ class SubmissionIdentifierForm(SubmissionChecks, forms.Form): return data or {} def request_arxiv_preprint_form_prefill_data(self): - '''Return dictionary to prefill `RequestSubmissionForm`.''' + """Return dictionary to prefill `RequestSubmissionForm`.""" form_data = self.arxiv_data form_data.update(self.identifier_into_parts(self.cleaned_data['identifier'])) if self.submission_is_resubmission(): @@ -235,6 +246,8 @@ class SubmissionIdentifierForm(SubmissionChecks, forms.Form): class RequestSubmissionForm(SubmissionChecks, forms.ModelForm): + """Form to submit a new Submission.""" + class Meta: model = Submission fields = [ @@ -324,7 +337,7 @@ class RequestSubmissionForm(SubmissionChecks, forms.ModelForm): def clean_author_list(self): """Check if author list matches the Contributor submitting. - + The submitting user must be an author of the submission. Also possibly may be extended to check permissions and give ultimate submission power to certain user groups. @@ -355,16 +368,16 @@ class RequestSubmissionForm(SubmissionChecks, forms.ModelForm): # Close last submission Submission.objects.filter(id=self.last_submission.id).update( - is_current=False, - open_for_reporting=False, - status=STATUS_RESUBMITTED) + is_current=False, open_for_reporting=False, status=STATUS_RESUBMITTED) # Open for comment and reporting and copy EIC info Submission.objects.filter(id=submission.id).update( open_for_reporting=True, open_for_commenting=True, + is_resubmission=True, + visible_pool=True, editor_in_charge=self.last_submission.editor_in_charge, - status=STATUS_RESUBMISSION_INCOMING) + status=STATUS_EIC_ASSIGNED) # Add author(s) (claim) fields submission.authors.add(*self.last_submission.authors.all()) @@ -372,13 +385,11 @@ class RequestSubmissionForm(SubmissionChecks, forms.ModelForm): submission.authors_false_claims.add(*self.last_submission.authors_false_claims.all()) # Create new EditorialAssigment for the current Editor-in-Charge - assignment = EditorialAssignment( - submission=submission, - to=self.last_submission.editor_in_charge, - accepted=True) - assignment.save() + EditorialAssignment.objects.create( + submission=submission, to=self.last_submission.editor_in_charge, accepted=True) def set_pool(self, submission): + """Set the default set of (guest) Fellows for this Submission.""" qs = Fellowship.objects.active() fellows = qs.regular().filter( contributor__discipline=submission.discipline).return_active_for_submission(submission) @@ -392,9 +403,9 @@ class RequestSubmissionForm(SubmissionChecks, forms.ModelForm): @transaction.atomic def save(self): - """ - Prefill instance before save. + """Fill, create and transfer data to the new Submission. + Prefill instance before save. Because of the ManyToManyField on `authors`, commit=False for this form is disabled. Saving the form without the database call may loose `authors` data without notice. @@ -410,10 +421,17 @@ class RequestSubmissionForm(SubmissionChecks, forms.ModelForm): submission.arxiv_identifier_wo_vn_nr = identifiers['arxiv_identifier_wo_vn_nr'] submission.arxiv_vn_nr = identifiers['arxiv_vn_nr'] - # Save - submission.save() if self.submission_is_resubmission(): + # Reset Refereeing Cycle. EIC needs to pick a cycle on resubmission. + submission.refereeing_cycle = CYCLE_UNDETERMINED + submission.save() # Save before filling from old Submission. + self.copy_and_save_data_from_resubmission(submission) + else: + # Save! + submission.save() + + # Gather first known author and Fellows. submission.authors.add(self.requested_by.contributor) self.set_pool(submission) @@ -422,16 +440,50 @@ class RequestSubmissionForm(SubmissionChecks, forms.ModelForm): class SubmissionReportsForm(forms.ModelForm): + """Update refereeing pdf for Submission.""" + class Meta: model = Submission fields = ['pdf_refereeing_pack'] +class SubmissionPrescreeningForm(forms.ModelForm): + """Processing decision for pre-screening of Submission.""" + + PASS = 'pass' + CHOICES = ((PASS, 'Pass pre-screening. Proceed to the Pool.'),) + decision = forms.ChoiceField(widget=forms.RadioSelect, choices=CHOICES, required=False) + + class Meta: + model = Submission + fields = () + + def __init__(self, *args, **kwargs): + """Add related submission as argument.""" + self.submission = kwargs.pop('submission') + super().__init__(*args, **kwargs) + + def clean(self): + """Check if Submission has right status.""" + data = super().clean() + if self.instance.status != STATUS_INCOMING: + self.add_error(None, 'This Submission is currently not in pre-screening.') + return data + + @transaction.atomic + def save(self): + """Update Submission status.""" + Submission.objects.filter(id=self.instance.id).update( + status=STATUS_UNASSIGNED, visible_pool=True) + + ###################### # Editorial workflow # ###################### -class EditorialAssignmentForm(forms.ModelForm): +class InviteEditorialAssignmentForm(forms.ModelForm): + """Invite new Fellow; create EditorialAssignment for Submission.""" + class Meta: model = EditorialAssignment fields = ('to',) @@ -440,6 +492,7 @@ class EditorialAssignmentForm(forms.ModelForm): } def __init__(self, *args, **kwargs): + """Add related submission as argument.""" self.submission = kwargs.pop('submission') super().__init__(*args, **kwargs) self.fields['to'].queryset = Contributor.objects.available().filter( @@ -450,19 +503,106 @@ class EditorialAssignmentForm(forms.ModelForm): return super().save(commit) +class EditorialAssignmentForm(forms.ModelForm): + """Create and/or process new EditorialAssignment for Submission.""" + + DECISION_CHOICES = ( + ('accept', 'Accept'), + ('decline', 'Decline')) + CYCLE_CHOICES = ( + (CYCLE_DEFAULT, 'Normal refereeing cycle'), + (CYCLE_DIRECT_REC, 'Directly formulate Editorial Recommendation for rejection')) + + decision = forms.ChoiceField( + widget=forms.RadioSelect, choices=DECISION_CHOICES, + label="Are you willing to take charge of this Submission?") + refereeing_cycle = forms.ChoiceField( + widget=forms.RadioSelect, choices=CYCLE_CHOICES, initial=CYCLE_DEFAULT) + refusal_reason = forms.ChoiceField( + choices=ASSIGNMENT_REFUSAL_REASONS) + + class Meta: + model = EditorialAssignment + fields = () # Don't use the default fields options because of the ordering of fields. + + def __init__(self, *args, **kwargs): + """Add related submission as argument.""" + self.submission = kwargs.pop('submission') + self.request = kwargs.pop('request') + super().__init__(*args, **kwargs) + if not self.instance.id: + del self.fields['decision'] + del self.fields['refusal_reason'] + + def has_accepted_invite(self): + """Check if invite is accepted or if voluntered to become EIC.""" + return 'decision' not in self.cleaned_data or self.cleaned_data['decision'] == 'accept' + + def is_normal_cycle(self): + """Check if normal refereeing cycle is chosen.""" + return self.cleaned_data['refereeing_cycle'] == CYCLE_DEFAULT + + def save(self, commit=True): + """Save Submission to EditorialAssignment.""" + self.instance.submission = self.submission + self.instance.date_answered = timezone.now() + self.instance.to = self.request.user.contributor + recommendation = super().save() # Save already, in case it's a new recommendation. + + if self.is_normal_cycle(): + # Default Refereeing process! + + deadline = timezone.now() + datetime.timedelta(days=28) + if recommendation.submission.submitted_to_journal == 'SciPostPhysLectNotes': + deadline += datetime.timedelta(days=28) + + # Update related Submission. + Submission.objects.filter(id=self.submission.id).update( + refereeing_cycle=CYCLE_DEFAULT, + status=STATUS_EIC_ASSIGNED, + editor_in_charge=self.request.user.contributor, + reporting_deadline=deadline, + open_for_reporting=True, + visible_public=True, + latest_activity=timezone.now()) + else: + # Formulate rejection recommendation instead + + # Update related Submission. + Submission.objects.filter(id=self.submission.id).update( + refereeing_cycle=CYCLE_DIRECT_REC, + status=STATUS_EIC_ASSIGNED, + editor_in_charge=self.request.user.contributor, + reporting_deadline=timezone.now(), + open_for_reporting=False, + visible_public=False, + latest_activity=timezone.now()) + + if self.has_accepted_invite(): + # Implicitly or explicity accept the assignment and deprecate others. + recommendation.accepted = True + EditorialAssignment.objects.filter(submission=self.submission, accepted=None).exclude( + id=recommendation.id).update(deprecated=True) + else: + recommendation.accepted = False + recommendation.refusal_reason = self.cleaned_data['refusal_reason'] + recommendation.save() # Save again to register acceptance + return recommendation + + class ConsiderAssignmentForm(forms.Form): + """Process open EditorialAssignment.""" + accept = forms.ChoiceField(widget=forms.RadioSelect, choices=ASSIGNMENT_BOOL, label="Are you willing to take charge of this Submission?") refusal_reason = forms.ChoiceField(choices=ASSIGNMENT_REFUSAL_REASONS, required=False) class RefereeSelectForm(forms.Form): - last_name = forms.CharField() + """Pre-fill form to get the last name of the requested referee.""" - def __init__(self, *args, **kwargs): - super(RefereeSelectForm, self).__init__(*args, **kwargs) - self.fields['last_name'].widget.attrs.update( - {'size': 20, 'placeholder': 'Search in contributors database'}) + last_name = forms.CharField(widget=forms.TextInput({ + 'placeholder': 'Search in contributors database'})) class RefereeRecruitmentForm(forms.ModelForm): @@ -530,6 +670,8 @@ class SetRefereeingDeadlineForm(forms.Form): class VotingEligibilityForm(forms.ModelForm): + """Assign Fellows to vote for EICRecommendation and open its status for voting.""" + eligible_fellows = forms.ModelMultipleChoiceField( queryset=Contributor.objects.none(), widget=forms.CheckboxSelectMultiple({'checked': 'checked'}), @@ -540,23 +682,23 @@ class VotingEligibilityForm(forms.ModelForm): fields = () def __init__(self, *args, **kwargs): + """Get queryset of Contributors eligibile for voting.""" super().__init__(*args, **kwargs) self.fields['eligible_fellows'].queryset = Contributor.objects.filter( - fellowships__pool=self.instance.submission, - expertises__contains=[self.instance.submission.subject_area] - ).order_by('user__last_name') + fellowships__pool=self.instance.submission, + expertises__contains=[ + self.instance.submission.subject_area]).order_by('user__last_name') def save(self, commit=True): - recommendation = self.instance - recommendation.eligible_to_vote = self.cleaned_data['eligible_fellows'] - submission = self.instance.submission - submission.status = 'put_to_EC_voting' + """Update EICRecommendation status and save its voters.""" + self.instance.eligible_to_vote = self.cleaned_data['eligible_fellows'] + self.instance.status = PUT_TO_VOTING if commit: - recommendation.save() - submission.save() - recommendation.voted_for.add(recommendation.submission.editor_in_charge) - return recommendation + self.instance.save() + self.instance.submission.touch() + self.instance.voted_for.add(self.instance.submission.editor_in_charge) + return self.instance ############ @@ -570,13 +712,15 @@ class ReportPDFForm(forms.ModelForm): class ReportForm(forms.ModelForm): + """Write Report form.""" + report_type = REPORT_NORMAL class Meta: model = Report fields = ['qualification', 'strengths', 'weaknesses', 'report', 'requested_changes', 'validity', 'significance', 'originality', 'clarity', 'formatting', 'grammar', - 'recommendation', 'remarks_for_editors', 'anonymous'] + 'recommendation', 'remarks_for_editors', 'anonymous', 'file_attachment'] def __init__(self, *args, **kwargs): if kwargs.get('instance'): @@ -592,7 +736,7 @@ class ReportForm(forms.ModelForm): self.submission = kwargs.pop('submission') - super(ReportForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.fields['strengths'].widget.attrs.update({ 'placeholder': ('Give a point-by-point ' '(numbered 1-, 2-, ...) list of the paper\'s strengths'), @@ -633,9 +777,16 @@ class ReportForm(forms.ModelForm): if self.fields[field].required: self.fields[field].label += ' *' - if self.submission.status in POST_PUBLICATION_STATUSES: + if self.submission.eicrecommendations.active().exists(): + # An active EICRecommendation is already formulated. This Report will be flagged. self.report_type = REPORT_POST_EDREC + # def clean_file_attachment(self): + # f = self.cleaned_data['file_attachment'] + # r = f.file + # raise + # return f + def save(self): """ Update meta data if ModelForm is submitted (non-draft). @@ -686,7 +837,7 @@ class VetReportForm(forms.Form): }) def clean_refusal_reason(self): - '''Require a refusal reason if report is rejected.''' + """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: @@ -694,7 +845,7 @@ class VetReportForm(forms.Form): return reason def process_vetting(self, current_contributor): - '''Set the right report status and update submission fields if needed.''' + """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: @@ -732,6 +883,8 @@ class EditorialCommunicationForm(forms.ModelForm): ###################### class EICRecommendationForm(forms.ModelForm): + """Formulate an EICRecommendation.""" + DAYS_TO_VOTE = 7 assignment = None earlier_recommendations = None @@ -760,6 +913,11 @@ class EICRecommendationForm(forms.ModelForm): } def __init__(self, *args, **kwargs): + """Accept two additional kwargs. + + -- submission: The Submission to formulate an EICRecommendation for. + -- reformulate (bool): Reformulate the currently available EICRecommendations. + """ self.submission = kwargs.pop('submission') self.reformulate = kwargs.pop('reformulate', False) if self.reformulate: @@ -777,7 +935,7 @@ class EICRecommendationForm(forms.ModelForm): super().__init__(*args, **kwargs) self.load_assignment() - def save(self, commit=True): + def save(self): recommendation = super().save(commit=False) recommendation.submission = self.submission recommendation.voting_deadline += datetime.timedelta(days=self.DAYS_TO_VOTE) # Test this @@ -788,44 +946,37 @@ class EICRecommendationForm(forms.ModelForm): else: event_text = 'An Editorial Recommendation has been formulated: {}.' - if recommendation.recommendation in [1, 2, 3, -3]: - # Accept/Reject: Forward to the Editorial College for voting - self.submission.status = 'voting_in_preparation' - - if commit: - # Add SubmissionEvent for EIC only - self.submission.add_event_for_eic(event_text.format( - recommendation.get_recommendation_display())) - elif recommendation.recommendation in [-1, -2]: + if recommendation.recommendation in [REPORT_MINOR_REV, REPORT_MAJOR_REV]: # Minor/Major revision: return to Author; ask to resubmit - self.submission.status = 'revision_requested' - self.submission.open_for_reporting = False + recommendation.status = DECISION_FIXED + Submission.objects.filter(id=self.submission.id).update(open_for_reporting=False) - if commit: - # Add SubmissionEvents for both Author and EIC - self.submission.add_general_event(event_text.format( - recommendation.get_recommendation_display())) + # Add SubmissionEvents for both Author and EIC + self.submission.add_general_event(event_text.format( + recommendation.get_recommendation_display())) + else: + # Add SubmissionEvent for EIC only + self.submission.add_event_for_eic(event_text.format( + recommendation.get_recommendation_display())) - if commit: - if self.earlier_recommendations: - self.earlier_recommendations.update(active=False) + if self.earlier_recommendations: + self.earlier_recommendations.update(active=False, status=DEPRECATED) - # All reports already submitted are now formulated *after* eic rec formulation - Report.objects.filter( - submission__eicrecommendations__in=self.earlier_recommendations).update( - report_type=REPORT_NORMAL) + # All reports already submitted are now formulated *after* eic rec formulation + Report.objects.filter( + submission__eicrecommendations__in=self.earlier_recommendations).update( + report_type=REPORT_NORMAL) - recommendation.save() - self.submission.save() + recommendation.save() - if self.assignment: - # The EIC has fulfilled this editorial assignment. - self.assignment.completed = True - self.assignment.save() + if self.assignment: + # The EIC has fulfilled this editorial assignment. + self.assignment.completed = True + self.assignment.save() return recommendation def revision_requested(self): - return self.instance.recommendation in [-1, -2] + return self.instance.recommendation in [REPORT_MINOR_REV, REPORT_MAJOR_REV] def has_assignment(self): return self.assignment is not None @@ -840,6 +991,7 @@ class EICRecommendationForm(forms.ModelForm): return False def load_earlier_recommendations(self): + """Load and save EICRecommendations related to Submission of the instance.""" self.earlier_recommendations = self.submission.eicrecommendations.all() @@ -848,25 +1000,25 @@ class EICRecommendationForm(forms.ModelForm): ############### class RecommendationVoteForm(forms.Form): - vote = forms.ChoiceField(widget=forms.RadioSelect, - choices=[('agree', 'Agree'), - ('disagree', 'Disagree'), - ('abstain', 'Abstain')], - label='', - ) - remark = forms.CharField(widget=forms.Textarea(), label='', required=False) + """Cast vote on EICRecommendation form.""" - def __init__(self, *args, **kwargs): - super(RecommendationVoteForm, self).__init__(*args, **kwargs) - self.fields['remark'].widget.attrs.update( - {'rows': 3, 'cols': 30, 'placeholder': 'Your remarks (optional)'}) + vote = forms.ChoiceField( + widget=forms.RadioSelect, choices=[ + ('agree', 'Agree'), ('disagree', 'Disagree'), ('abstain', 'Abstain')], label='') + remark = forms.CharField(widget=forms.Textarea(attrs={ + 'rows': 3, + 'cols': 30, + 'placeholder': 'Your remarks (optional)' + }), label='', required=False) class SubmissionCycleChoiceForm(forms.ModelForm): - referees_reinvite = forms.ModelMultipleChoiceField(queryset=RefereeInvitation.objects.none(), - widget=forms.CheckboxSelectMultiple({ - 'checked': 'checked'}), - required=False, label='Reinvite referees') + """Make a decision on the Submission's cycle and make publicly available.""" + + referees_reinvite = forms.ModelMultipleChoiceField( + queryset=RefereeInvitation.objects.none(), + widget=forms.CheckboxSelectMultiple({'checked': 'checked'}), + required=False, label='Reinvite referees') class Meta: model = Submission @@ -874,13 +1026,19 @@ class SubmissionCycleChoiceForm(forms.ModelForm): widgets = {'refereeing_cycle': forms.RadioSelect} def __init__(self, *args, **kwargs): + """Update choices and queryset.""" super().__init__(*args, **kwargs) - self.fields['refereeing_cycle'].default = None + self.fields['refereeing_cycle'].choices = SUBMISSION_CYCLE_CHOICES other_submissions = self.instance.other_versions.all() if other_submissions: self.fields['referees_reinvite'].queryset = RefereeInvitation.objects.filter( submission__in=other_submissions).distinct() + def save(self): + """Make Submission publicly available after decision.""" + self.instance.visible_public = True + return super().save() + class iThenticateReportForm(forms.ModelForm): class Meta: @@ -901,7 +1059,7 @@ class iThenticateReportForm(forms.ModelForm): if not doc_id and not self.fields.get('file'): try: cleaned_data['document'] = helpers.retrieve_pdf_from_arxiv( - self.submission.arxiv_identifier_w_vn_nr) + self.submission.arxiv_identifier_w_vn_nr) except exceptions.ArxivPDFNotFound: self.add_error(None, ('The pdf could not be found at arXiv.' ' Please upload the pdf manually.')) @@ -947,8 +1105,7 @@ class iThenticateReportForm(forms.ModelForm): pass else: report.save() - self.submission.plagiarism_report = report - self.submission.save() + Submission.objects.filter(id=self.submission.id).update(plagiarism_report=report) return report def call_ithenticate(self): @@ -990,3 +1147,86 @@ class iThenticateReportForm(forms.ModelForm): self.add_error(None, msg) return None return data + + +class FixCollegeDecisionForm(forms.ModelForm): + """Fix EICRecommendation decision.""" + + FIX, DEPRECATE = 'fix', 'deprecate' + action = forms.ChoiceField(choices=((FIX, FIX), (DEPRECATE, DEPRECATE))) + + class Meta: + model = EICRecommendation + fields = () + + def __init__(self, *args, **kwargs): + """Accept request as argument.""" + self.submission = kwargs.pop('submission', None) + self.request = kwargs.pop('request', None) + return super().__init__(*args, **kwargs) + + def clean(self): + """Check if EICRecommendation has the right decision.""" + data = super().clean() + if self.instance.status == DECISION_FIXED: + self.add_error(None, 'This EICRecommendation is already fixed.') + elif self.instance.status == DEPRECATED: + self.add_error(None, 'This EICRecommendation is deprecated.') + return data + + def is_fixed(self): + """Check if decision is fixed.""" + return self.cleaned_data['action'] == self.FIX + + def fix_decision(self, recommendation): + """Fix decision of EICRecommendation.""" + EICRecommendation.objects.filter(id=recommendation.id).update(status=DECISION_FIXED) + submission = recommendation.submission + if recommendation.recommendation in [REPORT_PUBLISH_1, REPORT_PUBLISH_2, REPORT_PUBLISH_3]: + # Publish as Tier I, II or III + Submission.objects.filter(id=submission.id).update( + visible_public=True, status=STATUS_ACCEPTED, acceptance_date=datetime.date.today(), + latest_activity=timezone.now()) + + # Start a new ProductionStream + get_or_create_production_stream(submission) + + if self.request: + # Add SubmissionEvent for authors + notify_manuscript_accepted(self.request.user, submission, False) + elif recommendation.recommendation == REPORT_REJECT: + # Decision: Rejection. Auto hide from public and Pool. + Submission.objects.filter(id=submission.id).update( + visible_public=False, visible_pool=False, + status=STATUS_REJECTED, latest_activity=timezone.now()) + submission.get_other_versions().update(visible_public=False) + + # Add SubmissionEvent for authors + submission.add_event_for_author( + 'The Editorial Recommendation has been formulated: {0}.'.format( + recommendation.get_recommendation_display())) + submission.add_event_for_eic( + 'The Editorial Recommendation has been fixed: {0}.'.format( + recommendation.get_recommendation_display())) + return recommendation + + def deprecate_decision(self, recommendation): + """Deprecate decision of EICRecommendation.""" + EICRecommendation.objects.filter(id=recommendation.id).update( + status=DEPRECATED, active=False) + recommendation.submission.add_event_for_eic( + 'The Editorial Recommendation (version {version}) has been deprecated: {decision}.'.format( + version=recommendation.version, + decision=recommendation.get_recommendation_display())) + + return recommendation + + def save(self): + """Update EICRecommendation and related Submission.""" + if self.is_fixed(): + return self.fix_decision(self.instance) + elif self.cleaned_data['action'] == self.DEPRECATE: + return self.deprecate_decision(self.instance) + else: + raise ValueError('The decision given is invalid') + return self.instance diff --git a/submissions/helpers.py b/submissions/helpers.py index 766a11c8f493b2833674f2a1b5fb6681c6a9a0f4..f5f6c2eec90610caaee1abe4aa4ce9d3560ba11a 100644 --- a/submissions/helpers.py +++ b/submissions/helpers.py @@ -8,8 +8,7 @@ from .exceptions import ArxivPDFNotFound def retrieve_pdf_from_arxiv(arxiv_id): - """ - Try to download the pdf as bytes object from arXiv for a certain arXiv Identifier. + """Try to download the pdf as bytes object from arXiv for a certain arXiv Identifier. Raise ArxivPDFNotFound instead. :arxiv_id: Arxiv Identifier with or without (takes latest version instead) version number @@ -19,3 +18,28 @@ def retrieve_pdf_from_arxiv(arxiv_id): if response.status_code != 200: raise ArxivPDFNotFound('No pdf found on arXiv.') return response.content + + +def check_verified_author(submission, user): + """Check if user is verified author of Submission.""" + if not hasattr(user, 'contributor'): + return False + + return submission.authors.filter(user=user).exists() + + +def check_unverified_author(submission, user): + """Check if user may be author of Submission. + + Only return true if author is unverified. Verified authors will return false. + """ + if not hasattr(user, 'contributor'): + return False + + if submission.authors.filter(user=user).exists(): + # User is verified author. + return False + + return ( + user.last_name in submission.author_list and + not submission.authors_false_claims.filter(user=user).exists()) diff --git a/submissions/managers.py b/submissions/managers.py index a639e9e5d43ec62085ad9b5c3ce73b0bda237936..b1d213b696e5c033883ca3858f968bce460c6ef7 100644 --- a/submissions/managers.py +++ b/submissions/managers.py @@ -8,17 +8,7 @@ from django.db import models from django.db.models import Q from django.utils import timezone -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, STATUS_DRAFT, STATUS_PUBLISHED,\ - SUBMISSION_EXCLUDE_FROM_REPORTING,\ - STATUS_REJECTED, STATUS_REJECTED_VISIBLE,\ - STATUS_ACCEPTED, STATUS_RESUBMITTED, STATUS_RESUBMITTED_REJECTED_VISIBLE,\ - EVENT_FOR_EIC, EVENT_GENERAL, EVENT_FOR_AUTHOR,\ - STATUS_UNASSIGNED, STATUS_ASSIGNMENT_FAILED, STATUS_WITHDRAWN,\ - STATUS_PUT_TO_EC_VOTING, STATUS_VOTING_IN_PREPARATION,\ - SUBMISSION_STATUS_VOTING_DEPRECATED, STATUS_REVISION_REQUESTED +from . import constants now = timezone.now() @@ -41,7 +31,8 @@ class SubmissionQuerySet(models.QuerySet): return queryset.filter(id__in=ids) def user_filter(self, user): - """ + """Filter on basic conflict of interests. + Prevent conflict of interest by filtering submissions possibly related to user. This filter should be inherited by other filters. """ @@ -53,7 +44,8 @@ class SubmissionQuerySet(models.QuerySet): return self.none() def _pool(self, user): - """ + """Return the user-dependent pool of Submissions. + This filter creates 'the complete pool' for a user. This new-style pool does explicitly not have the author filter anymore, but registered pools for every Submission. @@ -75,86 +67,70 @@ class SubmissionQuerySet(models.QuerySet): return self.filter(fellows__in=qs) def pool(self, user): - """ - Return the pool for a certain user: filtered to "in active referee phase". - """ - qs = self._pool(user) - qs = qs.exclude(is_current=False).exclude(status__in=SUBMISSION_STATUS_OUT_OF_POOL) - return qs + """Return the user-dependent pool of Submissions in active referee phase.""" + return self.pool_editable(user).filter(is_current=True, status__in=[ + constants.STATUS_UNASSIGNED, + constants.STATUS_EIC_ASSIGNED, + constants.STATUS_ACCEPTED]) def pool_editable(self, user): - """ - Return the editable pool for a certain user. + """Return the editable pool for a certain user. This is similar to the regular pool, however it also contains submissions that are hidden in the regular pool, but should still be able to be opened by for example the Editor-in-charge. """ qs = self._pool(user) - qs = qs.exclude(status__in=SUBMISSION_HTTP404_ON_EDITORIAL_PAGE) - return qs - - def pool_full(self, user): - """ - Return the *FULL* pool for a certain user. - This makes sure the user can see all history of Submissions related to its Fellowship(s). - - Do not use this filter by default however, as this also contains Submissions - that are for example either rejected or accepted already and thus "inactive." - """ - qs = self._pool(user) - return qs + return qs.filter(visible_pool=True) def filter_for_eic(self, user): - """ - Return the set of Submissions the user is Editor-in-charge for or return the pool if - User is Editorial Administrator. + """Return the set of Submissions the user is Editor-in-charge for. + + If user is an Editorial Administrator: return the full pool. """ qs = self._pool(user) - if not user.has_perm('scipost.can_oversee_refereeing') and hasattr(user, 'contributor'): - qs = qs.filter(editor_in_charge=user.contributor) + if not user.has_perm('scipost.can_oversee_refereeing'): + qs = qs.filter(editor_in_charge__user=user) return qs def filter_for_author(self, user): - """ - Return the set of Submissions for which the user is a registered author. - """ + """Return the set of Submissions for which the user is a registered author.""" if not hasattr(user, 'contributor'): return self.none() return self.filter(authors=user.contributor) def prescreening(self): - """ - Return submissions just coming in and going through pre-screening. - """ - return self.filter(status=STATUS_UNASSIGNED) + """Return submissions just coming in and going through pre-screening.""" + return self.filter(status=constants.STATUS_INCOMING) + + def unassigned(self): + """Return submissions passed pre-screening, but unassigned.""" + return self.filter(status=constants.STATUS_UNASSIGNED) + + def without_eic(self): + """Return Submissions that still need Editorial Assignment.""" + return self.filter(status__in=[constants.STATUS_INCOMING, constants.STATUS_UNASSIGNED]) def actively_refereeing(self): - """ - Return submission currently in some point of the refereeing round. - """ - return (self.exclude(is_current=False) - .exclude(status__in=SUBMISSION_STATUS_OUT_OF_POOL) - .exclude(status__in=[STATUS_UNASSIGNED, STATUS_ACCEPTED, - STATUS_REVISION_REQUESTED])) + """Return submission currently in some point of the refereeing round.""" + return self.filter(status=constants.STATUS_EIC_ASSIGNED).exclude( + eicrecommendations__status=constants.DECISION_FIXED) def public(self): - """ - This query contains set of public submissions, i.e. also containing - submissions with status "published" or "resubmitted". - """ - return self.exclude(status__in=SUBMISSION_STATUS_PUBLICLY_INVISIBLE) + """Return all publicly available Submissions.""" + return self.filter(visible_public=True) - def public_unlisted(self): - """ - List only all public submissions. Should be used as a default filter! + def public_listed(self): + """List all public Submissions if not published and submitted. Implement: Use this filter to also determine, using a optional user argument, if the query should be filtered or not as a logged in EdCol Admin should be able to view *all* submissions. """ - return self.exclude(status__in=SUBMISSION_STATUS_PUBLICLY_UNLISTED) + return self.filter(visible_public=True).exclude(status__in=[ + constants.RESUBMITTED, + constants.PUBLISHED]) def public_newest(self): """ @@ -164,11 +140,12 @@ class SubmissionQuerySet(models.QuerySet): return self._newest_version_only(self.public()) def treated(self): - """ - This query returns all Submissions that are expected to be 'done'. - """ - return self.filter(status__in=[STATUS_ACCEPTED, STATUS_REJECTED_VISIBLE, STATUS_PUBLISHED, - STATUS_RESUBMITTED, STATUS_RESUBMITTED_REJECTED_VISIBLE]) + """This query returns all Submissions that are presumed to be 'done'.""" + return self.filter(status__in=[ + constants.STATUS_ACCEPTED, + constants.STATUS_REJECTED, + constants.STATUS_PUBLISHED, + constants.STATUS_RESUBMITTED]) def originally_submitted(self, from_date, until_date): """ @@ -182,34 +159,38 @@ class SubmissionQuerySet(models.QuerySet): return self.filter(arxiv_identifier_wo_vn_nr__in=identifiers) def accepted(self): - return self.filter(status=STATUS_ACCEPTED) + """Return accepted Submissions.""" + return self.filter(status=constants.STATUS_ACCEPTED) def revision_requested(self): - return self.filter(status=STATUS_REVISION_REQUESTED) + """Return Submissions with a fixed EICRecommendation: minor or major revision.""" + return self.filter( + eicrecommendations__status=constants.DECISION_FIXED, + eicrecommendations__recommendation__in=[ + constants.REPORT_MINOR_REV, constants.REPORT_MAJOR_REV]) def published(self): - return self.filter(status=STATUS_PUBLISHED) + """Return published Submissions.""" + return self.filter(status=constants.STATUS_PUBLISHED) def assignment_failed(self): - return self.filter(status=STATUS_ASSIGNMENT_FAILED) + """Return Submissions which have failed assignment.""" + return self.filter(status=constants.STATUS_ASSIGNMENT_FAILED) def rejected(self): - return self._newest_version_only(self.filter(status__in=[STATUS_REJECTED, - STATUS_REJECTED_VISIBLE])) + """Return rejected Submissions.""" + return self._newest_version_only(self.filter(status=constants.STATUS_REJECTED)) def withdrawn(self): - return self._newest_version_only(self.filter(status=STATUS_WITHDRAWN)) + """Return withdrawn Submissions.""" + return self._newest_version_only(self.filter(status=constants.STATUS_WITHDRAWN)) def open_for_reporting(self): - """ - Return Submissions that have appropriate status for reporting. - The `open_for_reporting` property is not filtered as some invited visitors - still need to have access. - """ - return self.exclude(status__in=SUBMISSION_EXCLUDE_FROM_REPORTING) + """Return Submission that allow for reporting.""" + return self.filter(open_for_reporting=True) def open_for_commenting(self): - """ Return Submission that allow for commenting. """ + """Return Submission that allow for commenting.""" return self.filter(open_for_commenting=True) @@ -218,13 +199,13 @@ class SubmissionEventQuerySet(models.QuerySet): """ Return all events that are meant to be for the author(s) of a submission. """ - return self.filter(event__in=[EVENT_FOR_AUTHOR, EVENT_GENERAL]) + return self.filter(event__in=[constants.EVENT_FOR_AUTHOR, constants.EVENT_GENERAL]) def for_eic(self): """ Return all events that are meant to be for the Editor-in-charge of a submission. """ - return self.filter(event__in=[EVENT_FOR_EIC, EVENT_GENERAL]) + return self.filter(event__in=[constants.EVENT_FOR_EIC, constants.EVENT_GENERAL]) def last_hours(self, hours=24): """ @@ -267,70 +248,59 @@ class EditorialAssignmentQuerySet(models.QuerySet): class EICRecommendationQuerySet(models.QuerySet): - def get_for_user_in_pool(self, user): - """ - -- DEPRECATED -- - - Return list of EICRecommendation which are filtered as these objects - are not related to the Contributor, by checking last_name and author_list of - the linked Submission. - """ - try: - return self.exclude(submission__authors=user.contributor)\ - .exclude(Q(submission__author_list__icontains=user.last_name), - ~Q(submission__authors_false_claims=user.contributor)) - except AttributeError: - return self.none() - - def filter_for_user(self, user, **kwargs): - """ - -- DEPRECATED -- - - Return list of EICRecommendation's which are owned/assigned author through the - related submission. - """ - try: - return self.filter(submission__authors=user.contributor).filter(**kwargs) - except AttributeError: - return self.none() + """QuerySet for the EICRecommendation model.""" def user_may_vote_on(self, user): + """Return the subset of EICRecommendation the User is eligable to vote on.""" if not hasattr(user, 'contributor'): return self.none() - return (self.filter(eligible_to_vote=user.contributor) - .exclude(recommendation__in=[-1, -2]) - .exclude(voted_for=user.contributor) - .exclude(voted_against=user.contributor) - .exclude(voted_abstain=user.contributor) - .exclude(submission__status__in=SUBMISSION_STATUS_VOTING_DEPRECATED)) + return self.put_to_voting().filter(eligible_to_vote=user.contributor).exclude( + recommendation__in=[-1, -2]).exclude( + models.Q(voted_for=user.contributor) | models.Q(voted_against=user.contributor) | + models.Q(voted_abstain=user.contributor)).exclude(submission__status__in=[ + constants.STATUS_REJECTED, + constants.STATUS_PUBLISHED, + constants.STATUS_WITHDRAWN]) def put_to_voting(self): - return self.filter(submission__status=STATUS_PUT_TO_EC_VOTING) + """Return the subset of EICRecommendation currently undergoing voting.""" + return self.filter(status=constants.PUT_TO_VOTING) def voting_in_preparation(self): - return self.filter(submission__status=STATUS_VOTING_IN_PREPARATION) + """Return the subset of EICRecommendation currently undergoing preparation for voting.""" + return self.filter(status=constants.VOTING_IN_PREP) def active(self): - return self.filter(active=True) + """Return the subset of EICRecommendation most recent, valid versions.""" + return self.exclude(status=constants.DEPRECATED) + + def fixed(self): + """Return the subset of fixed EICRecommendations.""" + return self.filter(status=constants.DECISION_FIXED) + + def asking_revision(self): + """Return EICRecommendation asking for a minor or major revision.""" + return self.filter(recommendation__in=[-1, -2]) class ReportQuerySet(models.QuerySet): def accepted(self): - return self.filter(status=STATUS_VETTED) + return self.filter(status=constants.STATUS_VETTED) def awaiting_vetting(self): - return self.filter(status=STATUS_UNVETTED) + return self.filter(status=constants.STATUS_UNVETTED) def rejected(self): - return self.filter(status__in=[STATUS_UNCLEAR, STATUS_INCORRECT, - STATUS_NOT_USEFUL, STATUS_NOT_ACADEMIC]) + return self.filter(status__in=[ + constants.STATUS_UNCLEAR, constants.STATUS_INCORRECT, constants.STATUS_NOT_USEFUL, + constants.STATUS_NOT_ACADEMIC]) def in_draft(self): - return self.filter(status=STATUS_DRAFT) + return self.filter(status=constants.STATUS_DRAFT) def non_draft(self): - return self.exclude(status=STATUS_DRAFT) + return self.exclude(status=constants.STATUS_DRAFT) def contributed(self): return self.filter(invited=False) @@ -367,10 +337,7 @@ class RefereeInvitationQuerySet(models.QuerySet): return qs def overdue(self): - qs = self.in_process() - deadline = now - qs = qs.filter(submission__reporting_deadline__lte=deadline) - return qs + return self.in_process().filter(submission__reporting_deadline__lte=now) class EditorialCommunicationQueryset(models.QuerySet): diff --git a/submissions/migrations/0011_auto_20180414_1627.py b/submissions/migrations/0011_auto_20180414_1627.py new file mode 100644 index 0000000000000000000000000000000000000000..7e613135d5c48aa049a2314fd18e9a6c37f424c0 --- /dev/null +++ b/submissions/migrations/0011_auto_20180414_1627.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-14 14:27 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0010_auto_20180314_1607'), + ] + + operations = [ + migrations.AddField( + model_name='submission', + name='visible_pool', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='submission', + name='visible_public', + field=models.BooleanField(default=False), + ), + ] diff --git a/submissions/migrations/0011_report_file_attachment.py b/submissions/migrations/0011_report_file_attachment.py new file mode 100644 index 0000000000000000000000000000000000000000..f4d463f7dfa685bd3dcf5703468ce3c7656657b9 --- /dev/null +++ b/submissions/migrations/0011_report_file_attachment.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-27 07:31 +from __future__ import unicode_literals + +import comments.behaviors +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0010_auto_20180314_1607'), + ] + + operations = [ + migrations.AddField( + model_name='report', + name='file_attachment', + field=models.FileField(blank=True, upload_to='uploads/reports/%Y/%m/%d/', validators=[comments.behaviors.validate_file_extension, comments.behaviors.validate_max_file_size]), + ), + ] diff --git a/submissions/migrations/0012_auto_20180414_1627.py b/submissions/migrations/0012_auto_20180414_1627.py new file mode 100644 index 0000000000000000000000000000000000000000..f8d6158fc0b848584d2b34e6cc6570d6b88ede93 --- /dev/null +++ b/submissions/migrations/0012_auto_20180414_1627.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-14 14:27 +from __future__ import unicode_literals + +from django.db import migrations + + +def update_visibility_booleans(apps, schema_editor): + Submission = apps.get_model('submissions', 'Submission') + + # Publicly show Submissions if status is correct. + Submission.objects.exclude(status__in=[ + 'unassigned', + 'assignment_failed', + 'resubmitted_incoming', + 'rejected_visible', + 'resubmitted_and_rejected_visible', + 'withdrawn']).update(visible_public=True) + + # Hide from pool if decision is taken. + Submission.objects.filter(status__in=[ + 'rejected_visible', + 'rejected', + 'withdrawn', + 'published', + 'assignment_failed', + 'resubmitted']).update(visible_pool=False) + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0011_auto_20180414_1627'), + ] + + operations = [ + migrations.RunPython(update_visibility_booleans), + ] diff --git a/submissions/migrations/0012_auto_20180519_1047.py b/submissions/migrations/0012_auto_20180519_1047.py new file mode 100644 index 0000000000000000000000000000000000000000..b67c1f3d4929ec9f87d5a1bfc4ce0787adb7a63c --- /dev/null +++ b/submissions/migrations/0012_auto_20180519_1047.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-05-19 08:47 +from __future__ import unicode_literals + +import comments.behaviors +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0011_report_file_attachment'), + ] + + operations = [ + migrations.AlterField( + model_name='report', + name='file_attachment', + field=models.FileField(blank=True, null=True, upload_to='uploads/reports/%Y/%m/%d/', validators=[comments.behaviors.validate_file_extension, comments.behaviors.validate_max_file_size]), + ), + ] diff --git a/submissions/migrations/0013_auto_20180414_1729.py b/submissions/migrations/0013_auto_20180414_1729.py new file mode 100644 index 0000000000000000000000000000000000000000..dc8ae084238a00265fc54aa347f1947dd947def2 --- /dev/null +++ b/submissions/migrations/0013_auto_20180414_1729.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-14 15:29 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0012_auto_20180414_1627'), + ] + + operations = [ + migrations.AddField( + model_name='eicrecommendation', + name='status', + field=models.CharField(choices=[('voting_in_prep', 'Voting in preparation'), ('put_to_voting', 'Undergoing voting at the Editorial College'), ('vote_completed', 'Editorial College voting rounded up'), ('decision_fixed', 'Editorial Recommendation fixed'), ('deprecated', 'Editorial Recommendation deprecated')], default='voting_in_prep', max_length=32), + ), + migrations.AlterField( + model_name='submission', + name='visible_pool', + field=models.BooleanField(default=True, verbose_name='Is visible in the Pool'), + ), + migrations.AlterField( + model_name='submission', + name='visible_public', + field=models.BooleanField(default=False, verbose_name='Is publicly visible'), + ), + ] diff --git a/submissions/migrations/0014_auto_20180414_1729.py b/submissions/migrations/0014_auto_20180414_1729.py new file mode 100644 index 0000000000000000000000000000000000000000..3e613038f6abd122460f55db5c1ab3be284c8869 --- /dev/null +++ b/submissions/migrations/0014_auto_20180414_1729.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-14 15:29 +from __future__ import unicode_literals + +from django.db import migrations + + +def update_eic_rec_statuses(apps, schema_editor): + Submission = apps.get_model('submissions', 'Submission') + EICRecommendation = apps.get_model('submissions', 'EICRecommendation') + + # Update EICRecommendation statuses + for sub in Submission.objects.filter(status='voting_in_preparation'): + EICRecommendation.objects.filter(submission__id=sub.id).update(status='voting_in_prep') + + for sub in Submission.objects.filter(status='put_to_EC_voting'): + EICRecommendation.objects.filter(submission__id=sub.id).update(status='put_to_voting') + + for sub in Submission.objects.filter(status='EC_vote_completed'): + EICRecommendation.objects.filter(submission__id=sub.id).update(status='vote_completed') + + for sub in Submission.objects.filter(status__in=[ + 'accepted', + 'published', + 'rejected', + 'rejected_visible', + 'resubmitted', + 'resubmitted_and_rejected', + 'resubmitted_and_rejected_visible', + 'revision_requested']): + EICRecommendation.objects.filter(submission__id=sub.id).update(status='decision_fixed') + EICRecommendation.objects.filter(active=False).update(status='deprecated') + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0013_auto_20180414_1729'), + ] + + operations = [ + migrations.RunPython(update_eic_rec_statuses), + ] diff --git a/submissions/migrations/0015_auto_20180414_1742.py b/submissions/migrations/0015_auto_20180414_1742.py new file mode 100644 index 0000000000000000000000000000000000000000..8e876b3e896cea422847ab9957b6895c41cb87f5 --- /dev/null +++ b/submissions/migrations/0015_auto_20180414_1742.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-14 15:42 +from __future__ import unicode_literals + +from django.db import migrations + +def merge_submission_statuses(apps, schema_editor): + Submission = apps.get_model('submissions', 'Submission') + + # Rejected Submissions + Submission.objects.filter(status='rejected_visible').update(status='rejected') + + # Resubmitted Submissions + Submission.objects.filter(status__in=[ + 'resubmitted_and_rejected', + 'resubmitted_and_rejected_visible']).update(status='resubmitted') + + # Rec. formulated Submissions + Submission.objects.filter(status__in=[ + 'voting_in_preparation', + 'put_to_EC_voting', + 'EC_vote_completed', + 'revision_requested']).update(status='recommendation_formulated') + + # Closed formulated Submissions + Submission.objects.filter(status='review_closed').update(status='awaiting_ed_rec') + + # Incoming Submissions + Submission.objects.filter(status__in=[ + 'unassigned', + 'resubmitted_incoming']).update(status='unassigned_incoming') + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0014_auto_20180414_1729'), + ] + + operations = [ + migrations.RunPython(merge_submission_statuses), + ] diff --git a/submissions/migrations/0016_auto_20180414_1825.py b/submissions/migrations/0016_auto_20180414_1825.py new file mode 100644 index 0000000000000000000000000000000000000000..f70fbb575ff7f9421528c93ad62f13aee1b36e04 --- /dev/null +++ b/submissions/migrations/0016_auto_20180414_1825.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-14 16:25 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0015_auto_20180414_1742'), + ] + + operations = [ + migrations.AlterField( + model_name='submission', + name='status', + field=models.CharField(choices=[('unassigned_incoming', 'Unassigned, undergoing pre-screening'), ('assignment_failed', 'Failed to assign Editor-in-charge; manuscript rejected'), ('recommendation_formulated', 'Editorial Recommendation formulated'), ('awaiting_ed_rec', 'Awaiting Editorial Recommendation'), ('resubmitted', 'Has been resubmitted'), ('accepted', 'Publication decision taken: accept'), ('rejected', 'Publication decision taken: reject'), ('withdrawn', 'Withdrawn by the Authors'), ('published', 'Published')], default='unassigned', max_length=30), + ), + ] diff --git a/submissions/migrations/0017_auto_20180426_1018.py b/submissions/migrations/0017_auto_20180426_1018.py new file mode 100644 index 0000000000000000000000000000000000000000..09d83ce871dc6ebab6c981b1143980d2a546ce5f --- /dev/null +++ b/submissions/migrations/0017_auto_20180426_1018.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-26 08:18 +from __future__ import unicode_literals + +from django.db import migrations + + +def update_submission_statuses(apps, schema_editor): + """Update Submission incoming status to unassigned.""" + Submission = apps.get_model('submissions', 'Submission') + + # Update Submission statuses + Submission.objects.filter(status='unassigned_incoming').update(status='unassigned') + Submission.objects.filter(status='recommendation_formulated').update(status='assigned') + Submission.objects.filter(status='EICassigned').update(status='assigned') # Renaming of key + Submission.objects.filter(status='awaiting_ed_rec').update(status='assigned') + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0016_auto_20180414_1825'), + ] + + operations = [ + migrations.RunPython(update_submission_statuses), + ] diff --git a/submissions/migrations/0018_auto_20180426_1023.py b/submissions/migrations/0018_auto_20180426_1023.py new file mode 100644 index 0000000000000000000000000000000000000000..e5c0816335f19c90d4d7ec19a52d1b8fe8b80e38 --- /dev/null +++ b/submissions/migrations/0018_auto_20180426_1023.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-26 08:23 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0017_auto_20180426_1018'), + ] + + operations = [ + migrations.AlterField( + model_name='submission', + name='status', + field=models.CharField(choices=[('unassigned_incoming', 'Submission incoming, undergoing pre-screening'), ('unassigned', 'Unassigned, awaiting editor assignment'), ('assigned', 'Editor-in-charge assigned, manuscript under review'), ('assignment_failed', 'Failed to assign Editor-in-charge; manuscript rejected'), ('resubmitted', 'Has been resubmitted'), ('accepted', 'Publication decision taken: accept'), ('rejected', 'Publication decision taken: reject'), ('withdrawn', 'Withdrawn by the Authors'), ('published', 'Published')], default='unassigned', max_length=30), + ), + ] diff --git a/submissions/migrations/0019_auto_20180426_1246.py b/submissions/migrations/0019_auto_20180426_1246.py new file mode 100644 index 0000000000000000000000000000000000000000..336b39d1867d4241980f77af64562d95aa1dae0c --- /dev/null +++ b/submissions/migrations/0019_auto_20180426_1246.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-26 10:46 +from __future__ import unicode_literals + +from django.db import migrations + + +def rename_submission_status(apps, schema_editor): + """Rename Submission incoming status.""" + Submission = apps.get_model('submissions', 'Submission') + + # Update Submission statuses + Submission.objects.filter(status='unassigned_incoming').update(status='incoming') + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0018_auto_20180426_1023'), + ] + + operations = [ + migrations.RunPython(rename_submission_status), + ] diff --git a/submissions/migrations/0020_auto_20180426_1247.py b/submissions/migrations/0020_auto_20180426_1247.py new file mode 100644 index 0000000000000000000000000000000000000000..83961ba467e1945a685e77816819fe441dfd5e06 --- /dev/null +++ b/submissions/migrations/0020_auto_20180426_1247.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-26 10:47 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0019_auto_20180426_1246'), + ] + + operations = [ + migrations.AlterField( + model_name='submission', + name='status', + field=models.CharField(choices=[('incoming', 'Submission incoming, undergoing pre-screening'), ('unassigned', 'Unassigned, awaiting editor assignment'), ('assigned', 'Editor-in-charge assigned, manuscript under review'), ('assignment_failed', 'Failed to assign Editor-in-charge; manuscript rejected'), ('resubmitted', 'Has been resubmitted'), ('accepted', 'Publication decision taken: accept'), ('rejected', 'Publication decision taken: reject'), ('withdrawn', 'Withdrawn by the Authors'), ('published', 'Published')], default='unassigned', max_length=30), + ), + ] diff --git a/submissions/migrations/0021_auto_20180502_2029.py b/submissions/migrations/0021_auto_20180502_2029.py new file mode 100644 index 0000000000000000000000000000000000000000..d8cd53cefbaf5719b5f251dd23f0aa563451b9d2 --- /dev/null +++ b/submissions/migrations/0021_auto_20180502_2029.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-05-02 18:29 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0020_auto_20180426_1247'), + ] + + operations = [ + migrations.AlterField( + model_name='submission', + name='refereeing_cycle', + field=models.CharField(choices=[('', 'Cycle undetermined'), ('default', 'Default cycle'), ('short', 'Short cycle'), ('direct_rec', 'Direct editorial recommendation')], default='default', max_length=30), + ), + ] diff --git a/submissions/migrations/0022_merge_20180519_1308.py b/submissions/migrations/0022_merge_20180519_1308.py new file mode 100644 index 0000000000000000000000000000000000000000..a838be818e55540b8e4bba2d04443662f54475ac --- /dev/null +++ b/submissions/migrations/0022_merge_20180519_1308.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-05-19 11:08 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0021_auto_20180502_2029'), + ('submissions', '0012_auto_20180519_1047'), + ] + + operations = [ + ] diff --git a/submissions/migrations/0023_auto_20180519_1313.py b/submissions/migrations/0023_auto_20180519_1313.py new file mode 100644 index 0000000000000000000000000000000000000000..12a6e95eeb9721e2efca203f39c8208920de79b9 --- /dev/null +++ b/submissions/migrations/0023_auto_20180519_1313.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-05-19 11:13 +from __future__ import unicode_literals + +import comments.behaviors +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0022_merge_20180519_1308'), + ] + + operations = [ + migrations.AlterField( + model_name='report', + name='file_attachment', + field=models.FileField(blank=True, default='', upload_to='uploads/reports/%Y/%m/%d/', validators=[comments.behaviors.validate_file_extension, comments.behaviors.validate_max_file_size]), + preserve_default=False, + ), + migrations.AlterField( + model_name='submission', + name='refereeing_cycle', + field=models.CharField(blank=True, choices=[('', 'Cycle undetermined'), ('default', 'Default cycle'), ('short', 'Short cycle'), ('direct_rec', 'Direct editorial recommendation')], default='default', max_length=30), + ), + migrations.AlterField( + model_name='submission', + name='status', + field=models.CharField(choices=[('incoming', 'Submission incoming, undergoing pre-screening'), ('unassigned', 'Unassigned, awaiting editor assignment'), ('assigned', 'Editor-in-charge assigned, manuscript under review'), ('assignment_failed', 'Failed to assign Editor-in-charge; manuscript rejected'), ('resubmitted', 'Has been resubmitted'), ('accepted', 'Publication decision taken: accept'), ('rejected', 'Publication decision taken: reject'), ('withdrawn', 'Withdrawn by the Authors'), ('published', 'Published')], default='incoming', max_length=30), + ), + migrations.AlterField( + model_name='submission', + name='visible_pool', + field=models.BooleanField(default=False, verbose_name='Is visible in the Pool'), + ), + ] diff --git a/submissions/models.py b/submissions/models.py index 09c58957e2ad88a84504bb4eee9d96f27968d0a3..7730c39bccfbf08d9904a45d7d2139947aa189b1 100644 --- a/submissions/models.py +++ b/submissions/models.py @@ -17,22 +17,24 @@ from .behaviors import SubmissionRelatedObjectMixin 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, - SUBMISSION_EIC_RECOMMENDATION_REQUIRED, SUBMISSION_CYCLES, CYCLE_DEFAULT, CYCLE_SHORT, + SUBMISSION_STATUS, REPORT_STATUSES, STATUS_UNVETTED, STATUS_INCOMING, + SUBMISSION_CYCLES, CYCLE_DEFAULT, CYCLE_SHORT, STATUS_RESUBMITTED, DECISION_FIXED, CYCLE_DIRECT_REC, EVENT_GENERAL, EVENT_TYPES, EVENT_FOR_AUTHOR, EVENT_FOR_EIC, REPORT_TYPES, - REPORT_NORMAL, STATUS_DRAFT, STATUS_VETTED, STATUS_VOTING_IN_PREPARATION, - STATUS_PUT_TO_EC_VOTING) + REPORT_NORMAL, STATUS_DRAFT, STATUS_VETTED, EIC_REC_STATUSES, VOTING_IN_PREP, + STATUS_INCORRECT, STATUS_UNCLEAR, STATUS_NOT_USEFUL, STATUS_NOT_ACADEMIC, DEPRECATED) from .managers import ( SubmissionQuerySet, EditorialAssignmentQuerySet, EICRecommendationQuerySet, ReportQuerySet, SubmissionEventQuerySet, RefereeInvitationQuerySet, EditorialCommunicationQueryset) from .utils import ( ShortSubmissionCycle, DirectRecommendationSubmissionCycle, GeneralSubmissionCycle) +from comments.behaviors import validate_file_extension, validate_max_file_size from comments.models import Comment from scipost.behaviors import TimeStampedModel from scipost.constants import TITLE_CHOICES from scipost.constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS from scipost.fields import ChoiceArrayField +from scipost.storage import SecureFileStorage from journals.constants import SCIPOST_JOURNALS_SUBMIT, SCIPOST_JOURNALS_DOMAINS from journals.models import Publication @@ -52,8 +54,7 @@ class Submission(models.Model): domain = models.CharField(max_length=3, choices=SCIPOST_JOURNALS_DOMAINS) editor_in_charge = models.ForeignKey('scipost.Contributor', related_name='EIC', blank=True, null=True, on_delete=models.CASCADE) - is_current = models.BooleanField(default=True) - is_resubmission = models.BooleanField(default=False) + list_of_changes = models.TextField(blank=True) open_for_commenting = models.BooleanField(default=False) open_for_reporting = models.BooleanField(default=False) @@ -65,14 +66,18 @@ class Submission(models.Model): models.CharField(max_length=10, choices=SCIPOST_SUBJECT_AREAS), blank=True, null=True) - # Refereeing fields - status = models.CharField(max_length=30, choices=SUBMISSION_STATUS, default=STATUS_UNASSIGNED) - refereeing_cycle = models.CharField(max_length=30, choices=SUBMISSION_CYCLES, - default=CYCLE_DEFAULT) + # Submission status fields + status = models.CharField(max_length=30, choices=SUBMISSION_STATUS, default=STATUS_INCOMING) + is_current = models.BooleanField(default=True) + visible_public = models.BooleanField("Is publicly visible", default=False) + visible_pool = models.BooleanField("Is visible in the Pool", default=False) + is_resubmission = models.BooleanField(default=False) + refereeing_cycle = models.CharField( + max_length=30, choices=SUBMISSION_CYCLES, default=CYCLE_DEFAULT, blank=True) + fellows = models.ManyToManyField('colleges.Fellowship', blank=True, related_name='pool') - # visible_pool = models.BooleanField(default=True) - # visible_public = models.BooleanField(default=False) + subject_area = models.CharField(max_length=10, choices=SCIPOST_SUBJECT_AREAS, verbose_name='Primary subject area', default='Phys:QP') submission_type = models.CharField(max_length=10, choices=SUBMISSION_TYPE) @@ -192,13 +197,25 @@ class Submission(models.Model): @property def eic_recommendation_required(self): """Return if Submission needs a EICRecommendation to be formulated.""" - return self.status in SUBMISSION_EIC_RECOMMENDATION_REQUIRED + return not self.eicrecommendations.active().exists() + + @property + def revision_requested(self): + """Check if Submission has fixed EICRecommendation asking for revision.""" + if self.status != STATUS_RESUBMITTED: + return False + return self.eicrecommendations.fixed().asking_revision().exists() @property def reporting_deadline_has_passed(self): """Check if Submission has passed it's reporting deadline.""" return timezone.now() > self.reporting_deadline + @property + def is_open_for_reporting(self): + """Check if Submission is open for reporting and within deadlines.""" + return self.open_for_reporting and not self.reporting_deadline_has_passed + @property def original_submission_date(self): """Return the submission_date of the first Submission in the thread.""" @@ -214,16 +231,17 @@ class Submission(models.Model): @cached_property def other_versions_public(self): """Return other (public) Submissions in the database in this ArXiv identifier series.""" - return Submission.objects.public().filter( - arxiv_identifier_wo_vn_nr=self.arxiv_identifier_wo_vn_nr - ).exclude(pk=self.id).order_by('-arxiv_vn_nr') + return self.get_other_versions().order_by('-arxiv_vn_nr') @cached_property def other_versions(self): """Return other Submissions in the database in this ArXiv identifier series.""" + return self.get_other_versions().order_by('-arxiv_vn_nr') + + def get_other_versions(self): + """Return queryset of other Submissions with this ArXiv identifier series.""" return Submission.objects.filter( - arxiv_identifier_wo_vn_nr=self.arxiv_identifier_wo_vn_nr).exclude( - pk=self.id).order_by('-arxiv_vn_nr') + arxiv_identifier_wo_vn_nr=self.arxiv_identifier_wo_vn_nr).exclude(pk=self.id) def count_accepted_invitations(self): """Count number of accepted RefereeInvitations for this Submission.""" @@ -276,10 +294,8 @@ class Submission(models.Model): ) event.save() - """ - Identify coauthorships from arXiv, using author surname matching. - """ def flag_coauthorships_arxiv(self, fellows): + """Identify coauthorships from arXiv, using author surname matching.""" coauthorships = {} if self.metadata and 'entries' in self.metadata: author_last_names = [] @@ -515,6 +531,12 @@ class Report(SubmissionRelatedObjectMixin, models.Model): anonymous = models.BooleanField(default=True, verbose_name='Publish anonymously') pdf_report = models.FileField(upload_to='UPLOADS/REPORTS/%Y/%m/', max_length=200, blank=True) + # Attachment + file_attachment = models.FileField( + upload_to='uploads/reports/%Y/%m/%d/', blank=True, + validators=[validate_file_extension, validate_max_file_size], + storage=SecureFileStorage()) + objects = ReportQuerySet.as_manager() class Meta: @@ -543,6 +565,12 @@ class Report(SubmissionRelatedObjectMixin, models.Model): """Return url of the Report on the Submission detail page.""" return self.submission.get_absolute_url() + '#report_' + str(self.report_nr) + def get_attachment_url(self): + """Return url of the Report its attachment if exists.""" + return reverse('submissions:report_attachment', kwargs={ + 'arxiv_identifier_w_vn_nr': self.submission.arxiv_identifier_w_vn_nr, + 'report_nr': self.report_nr}) + @property def is_in_draft(self): """Return if Report is in draft.""" @@ -553,6 +581,17 @@ class Report(SubmissionRelatedObjectMixin, models.Model): """Return if Report is publicly available.""" return self.status == STATUS_VETTED + @property + def is_unvetted(self): + """Return if Report is awaiting vetting.""" + return self.status == STATUS_UNVETTED + + @property + def is_rejected(self): + """Return if Report is rejected.""" + return self.status in [ + STATUS_INCORRECT, STATUS_UNCLEAR, STATUS_NOT_USEFUL, STATUS_NOT_ACADEMIC] + @property def notification_name(self): """Return string representation of this Report as shown in Notifications.""" @@ -694,6 +733,7 @@ class EICRecommendation(SubmissionRelatedObjectMixin, models.Model): verbose_name='optional remarks for the' ' Editorial College') recommendation = models.SmallIntegerField(choices=REPORT_REC) + status = models.CharField(max_length=32, choices=EIC_REC_STATUSES, default=VOTING_IN_PREP) version = models.SmallIntegerField(default=1) active = models.BooleanField(default=True) @@ -750,16 +790,22 @@ class EICRecommendation(SubmissionRelatedObjectMixin, models.Model): """Return the number of votes 'abstained'.""" return self.voted_abstain.count() - def get_other_versions(self): - """Return other versions of EICRecommendations for this Submission.""" - return self.submission.eicrecommendations.exclude(id=self.id) + @property + def is_deprecated(self): + """Check if Recommendation is deprecated.""" + return self.status == DEPRECATED + @property def may_be_reformulated(self): """Check if this EICRecommdation is allowed to be reformulated in a new version.""" - if not self.active: + if not self.status == DEPRECATED: # Already reformulated before; please use the latest version return self.submission.eicrecommendations.last() == self - return self.submission.status in [STATUS_VOTING_IN_PREPARATION, STATUS_PUT_TO_EC_VOTING] + return self.status != DECISION_FIXED + + def get_other_versions(self): + """Return other versions of EICRecommendations for this Submission.""" + return self.submission.eicrecommendations.exclude(id=self.id) class iThenticateReport(TimeStampedModel): diff --git a/submissions/plagiarism.py b/submissions/plagiarism.py index 96aa68e46eafc76c525b6c0bfb00451ffe93d7da..f09bab2c44e210a0af433c59f85e00d4601ad175 100644 --- a/submissions/plagiarism.py +++ b/submissions/plagiarism.py @@ -14,14 +14,18 @@ class iThenticate: self.client = self.get_client() def get_client(self): - client = iThenticateAPI.API.Client(settings.ITHENTICATE_USERNAME, - settings.ITHENTICATE_PASSWORD) + client = iThenticateAPI.API.Client( + settings.ITHENTICATE_USERNAME, settings.ITHENTICATE_PASSWORD) if client.login(): return client self.add_error(None, "Failed to login to iThenticate.") return None def determine_folder_group(self, group_re): + """Return the folder group id to which the system should upload a new document to. + + Generates a new folder group if needed. + """ groups = self.client.groups.all() if groups['status'] != 200: raise InvalidDocumentError("Uploading failed. iThenticate didn't return" @@ -41,8 +45,7 @@ class iThenticate: return response['data'][0]['id'] def determine_folder_id(self, submission): - """ - Return the folder id to which the system should upload a new document to. + """Return the folder id to which the system should upload a new document to. Generates a new folder and id if needed. """ @@ -72,8 +75,7 @@ class iThenticate: return data['data'][0]['id'] def upload_submission(self, document, submission): - """ - Upload a document related to a submission + """Upload a document related to a submission. :document: The document to upload :submission: submission which should be uploaded @@ -96,9 +98,7 @@ class iThenticate: return None def get_url(self, document_id): - """ - Return report url for given document - """ + """Return report url for given document.""" response = self.client.reports.get(document_id) if response['status'] == 200: diff --git a/submissions/templates/partials/submissions/pool/referee_invitations.html b/submissions/templates/partials/submissions/pool/referee_invitations.html index 8af3a7a1081f5007ba03fcb6be4d6a32a8322caf..0c56191df0752ab22a655b26719427d621f1eff1 100644 --- a/submissions/templates/partials/submissions/pool/referee_invitations.html +++ b/submissions/templates/partials/submissions/pool/referee_invitations.html @@ -1,20 +1,28 @@ -<table class="table table-invitations"> +<table class="table bg-light table-hover v-center"> + <thead> + <tr> + <th>Referee</th> + <th>Invitation date</th> + <th>Task status</th> + <th colspan="4">Actions</th> + </tr> + </thead> <tbody> {% for invitation in invitations %} <tr> - <td>{{invitation.first_name}} {{invitation.last_name}}</td> + <td>{{ invitation.get_title_display }} {{invitation.first_name}} {{invitation.last_name}}</td> <td> - invited <br/> + invited <br> {{invitation.date_invited}} </td> <td> {% if invitation.fulfilled %} - <strong style="color: green">task fulfilled</strong> + <strong class="text-success">task fulfilled</strong> {% elif invitation.cancelled %} <strong class="text-danger">cancelled</strong> {% elif invitation.accepted is not None %} {% if invitation.accepted %} - <strong style="color: green">task accepted</strong> + <strong class="text-success">task accepted</strong> {% else %} <strong class="text-danger">task declined</strong> {% endif %} @@ -56,7 +64,7 @@ </tr> {% empty %} <tr> - <td class="text-center py-3">You have not invited any referees yet.</td> + <td class="text-center py-3" colspan="7">You have not invited any referees yet.</td> </tr> {% endfor %} </tbody> diff --git a/submissions/templates/partials/submissions/pool/referee_invitations_status.html b/submissions/templates/partials/submissions/pool/referee_invitations_status.html index 225405c15291193e2e4c3bbdabb2a24d6ad3f67e..c7b8d4d44f5514e5cde8967022781b00a8ee454c 100644 --- a/submissions/templates/partials/submissions/pool/referee_invitations_status.html +++ b/submissions/templates/partials/submissions/pool/referee_invitations_status.html @@ -1,7 +1,18 @@ -{% if submission.refereeing_cycle != 'direct_rec' %} -<p> - 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> - <br> - Nr reports obtained: {{submission.count_obtained_reports}} [{{submission.count_invited_reports}} invited / {{submission.count_contrib_reports}} contributed], nr refused: {{submission.reports.rejected.count}}, nr awaiting vetting: {{submission.reports.awaiting_vetting.count}} -</p> -{% endif %} +<div class="table-responsive-md"> + <table class="table table-borderless"> + <tbody> + <tr> + <td>Nr referees invited:</td> + <td> + {{submission.referee_invitations.count}} <span>[{{submission.count_accepted_invitations}} acccepted / {{submission.count_declined_invitations}} declined / {{submission.count_pending_invitations}} response pending]</span> + </td> + </tr> + <tr> + <td>Nr reports obtained:</td> + <td> + {{submission.count_obtained_reports}} [{{submission.count_invited_reports}} invited / {{submission.count_contrib_reports}} contributed], nr refused: {{submission.reports.rejected.count}}, nr awaiting vetting: {{submission.reports.awaiting_vetting.count}} + </td> + </tr> + </tbody> + </table> +</div> diff --git a/submissions/templates/partials/submissions/pool/submission_comments_summary_table.html b/submissions/templates/partials/submissions/pool/submission_comments_summary_table.html new file mode 100644 index 0000000000000000000000000000000000000000..68ed6a2d1b9f2fb9855fbcbffc2a47637d14594d --- /dev/null +++ b/submissions/templates/partials/submissions/pool/submission_comments_summary_table.html @@ -0,0 +1,46 @@ +<table class="table bg-light table-hover v-center"> + <thead> + <tr> + <th>Referee</th> + <th>Status</th> + <th>Recommendation</th> + <th>Type</th> + <th>Date</th> + </tr> + </thead> + <tbody> + {% for comment in submission.comments.all %} + <tr> + <td> + {{ comment.author }} + {% if comment.anonymous %} + <br> + <b><span class="text-danger">Chose public anonymity</span></b> + {% endif %} + </td> + <td> + {% if comment.is_vetted %} + <i class="fa fa-check-circle text-success"></i> + {% elif comment.is_rejected %} + <i class="fa fa-times-circle text-danger"></i> + {% endif %} + {{ comment.get_status_display }} + {% if comment.is_unvetted %} + <br> + <a href="{% url 'comments:vet_submitted_comment' comment.id %}">Vet this Comment here</a> + {% elif comment.is_vetted %} + <br> + <a href="{{ comment.get_absolute_url }}">View full Comment here</a> + {% endif %} + </td> + <td><em>{{ comment.comment_text|truncatewords:6 }}</em></td> + <td>{% if comment.is_author_reply %}Author Reply{% else %}Comment{% endif %}</td> + <td>{{ comment.date_submitted }}</td> + </tr> + {% empty %} + <tr> + <td class="text-center py-3" colspan="5">There are no Comments yet.</td> + </tr> + {% endfor %} + </tbody> +</table> diff --git a/submissions/templates/partials/submissions/pool/submission_li.html b/submissions/templates/partials/submissions/pool/submission_li.html index fdd9854706c11655521eb30a85470216a5f4aeef..6d9e78d4812f7854bdb391217a068fa54f86d636 100644 --- a/submissions/templates/partials/submissions/pool/submission_li.html +++ b/submissions/templates/partials/submissions/pool/submission_li.html @@ -1,3 +1,5 @@ +{% load submissions_pool %} + <div class="icons"> {% include 'partials/submissions/pool/submission_tooltip.html' with submission=submission %} @@ -6,24 +8,33 @@ {% endif %} </div> <div class="pool-item"> - <p class="mb-1"> - <a href="{% url 'submissions:pool' submission.arxiv_identifier_w_vn_nr %}">{{ submission.title }}</a><br> - <em>by {{ submission.author_list }}</em> - </p> + <div class="row mb-1"> + <div class="col-md-7"> + <a href="{% url 'submissions:pool' submission.arxiv_identifier_w_vn_nr %}" data-toggle="dynamic" data-target="#container_{{ submission.id }}">{{ submission.title }}</a><br> + <em>by {{ submission.author_list }}</em> + </div> + <div class="col-md-5"> + {% if submission.eicrecommendations.active.first %} + <small class="text-muted">EIC Recommendation Status</small> + <br> + <span class="label label-sm label-secondary">{{ submission.eicrecommendations.active.first.get_status_display }}</span> + {% endif %} + </div> + </div> <div class="row mb-0"> - <div class="col-3"> + <div class="col-md-3"> <small class="text-muted">Editor-in-charge</small> <br> {% if submission.status == 'unassigned' %} - <span class="card-text text-danger">You can volunteer to become Editor-in-charge by <a href="{% url 'submissions:volunteer_as_EIC' submission.arxiv_identifier_w_vn_nr %}">clicking here</a>.</span> + <span class="card-text text-danger">You can volunteer to become Editor-in-charge by <a href="{% url 'submissions:editorial_assignment' submission.arxiv_identifier_w_vn_nr %}">clicking here</a>.</span> {% elif submission.editor_in_charge == request.user.contributor %} <strong>You are Editor-in-charge</strong> {% else %} {{ submission.editor_in_charge }} {% endif %} </div> - <div class="col-2"> + <div class="col-md-2"> <small class="text-muted">Actions</small> <br> <a href="{% url 'submissions:pool' submission.arxiv_identifier_w_vn_nr %}" data-toggle="dynamic" data-target="#container_{{ submission.id }}">See details</a> @@ -31,13 +42,13 @@ · <a href="{% url 'submissions:editorial_page' submission.arxiv_identifier_w_vn_nr %}">Editorial page</a> {% endif %} </div> - <div class="col-2"> + <div class="col-md-2"> <small class="text-muted">Original Submission date</small> <br> {{ submission.original_submission_date }} </div> - <div class="col-5"> - <small class="text-muted">Status</small> + <div class="col-md-5"> + <small class="text-muted">Submission Status</small> <br> <span class="label label-sm label-secondary">{{ submission.get_status_display }}</span> </div> @@ -49,4 +60,14 @@ </div> {% endif %} + {% if submission.status == 'unassigned' %} + {% get_editor_invitations submission request.user as invitations %} + {% if invitations %} + <div class="border border-warning mt-1 py-1 px-2"> + <i class="fa fa-exclamation mt-1 px-1 text-danger"></i> + You are invited to become Editor-in-charge of this Submission. <a href="{% url 'submissions:editorial_assignment' submission.arxiv_identifier_w_vn_nr %}">You can reply to this invitation here</a>. + </div> + {% endif %} + {% endif %} + </div> diff --git a/submissions/templates/partials/submissions/pool/submission_reports_summary_table.html b/submissions/templates/partials/submissions/pool/submission_reports_summary_table.html new file mode 100644 index 0000000000000000000000000000000000000000..d30352c127972003112e2033b07c0a515f49fa25 --- /dev/null +++ b/submissions/templates/partials/submissions/pool/submission_reports_summary_table.html @@ -0,0 +1,46 @@ +<table class="table bg-light table-hover v-center"> + <thead> + <tr> + <th>Referee</th> + <th>Status</th> + <th>Recommendation</th> + <th>Type</th> + <th>Date</th> + </tr> + </thead> + <tbody> + {% for report in submission.reports.all %} + <tr> + <td> + {{ report.author }} + {% if report.anonymous %} + <br> + <b><span class="text-danger">Chose public anonymity</span></b> + {% endif %} + </td> + <td> + {% if report.is_vetted %} + <i class="fa fa-check-circle text-success"></i> + {% elif report.is_rejected %} + <i class="fa fa-times-circle text-danger"></i> + {% endif %} + {{ report.get_status_display }} + {% if report.is_unvetted %} + <br> + <a href="{% url 'submissions:vet_submitted_report' report.id %}">Vet this Report here</a> + {% elif report.is_vetted %} + <br> + <a href="{{ report.get_absolute_url }}">View full Report here</a> + {% endif %} + </td> + <td>{{ report.get_recommendation_display }}</td> + <td>{% if report.invited %}Invited Report{% else %}Contributed Report{% endif %}</td> + <td>{{ report.date_submitted }}</td> + </tr> + {% empty %} + <tr> + <td class="text-center py-3" colspan="5">There are no Reports yet.</td> + </tr> + {% endfor %} + </tbody> +</table> diff --git a/submissions/templates/partials/submissions/recommendation_author_content.html b/submissions/templates/partials/submissions/recommendation_author_content.html index 39743bb936a0139a8e3325bc8477a2669f79ea6b..e2fa6fd467b168aac96c6a80fabd1e3cd9a45696 100644 --- a/submissions/templates/partials/submissions/recommendation_author_content.html +++ b/submissions/templates/partials/submissions/recommendation_author_content.html @@ -1,11 +1,21 @@ <div class="card bg-white"> <div class="card-body"> - <h2 class="card-title mb-0">Editorial Recommendation <small>(version {{ recommendation.version }}){% if not recommendation.active %} (Deprecated: This Editorial Recommendation has been reformulated){% endif %}</small></h2> - + <h2 class="card-title mb-0">Editorial Recommendation</h2> {% block recommendation_header %} <h3 class="card-title text-muted">Date {{recommendation.date_submitted}}</h3> {% endblock %} + <table class="mb-2"> + <tr> + <td class="pr-2">Version:</td> + <td>{{ recommendation.version }}</td> + </tr> + <tr> + <td class="pr-2">Status:</td> + <td><span class="label label-secondary">{{ recommendation.get_status_display }}</span></td> + </tr> + </table> + <h3 class="pb-0">Remarks for authors</h3> <p class="pl-md-3">{{recommendation.remarks_for_authors|default:'-'}}</p> diff --git a/submissions/templates/partials/submissions/report_content.html b/submissions/templates/partials/submissions/report_content.html index a93c921270cdc2ca611d13804c7985cd26094d1c..0c22ff9f63094d9bb383ba94ab4097f6a360ac50 100644 --- a/submissions/templates/partials/submissions/report_content.html +++ b/submissions/templates/partials/submissions/report_content.html @@ -34,4 +34,14 @@ </div> {% endif %} +{% if report.file_attachment %} + <div class="row"> + <div class="col-12"> + <h3 class="highlight tight">Attachment</h3> + <div class="pl-md-4"><i class="fa fa-download"></i> <a href="{{ report.get_attachment_url }}">Download attachment</a></div> + <br> + </div> + </div> +{% endif %} + {% include 'partials/submissions/report_ratings.html' with report=report %} diff --git a/submissions/templates/partials/submissions/report_public_without_comments.html b/submissions/templates/partials/submissions/report_public_without_comments.html index 9555c67c431dcabe7495f5bb1b48ef030249c6c8..839cbbbbfc194fe5cfe5c8ef5efb34e4ec308e60 100644 --- a/submissions/templates/partials/submissions/report_public_without_comments.html +++ b/submissions/templates/partials/submissions/report_public_without_comments.html @@ -4,7 +4,7 @@ <div class="row"> <div class="col-12"> <div class="report" id="report_{{report.report_nr}}"> - {% if user.contributor == submission.editor_in_charge or user|is_in_group:'Editorial Administrators' and user|is_not_author_of_submission:submission.arxiv_identifier_w_vn_nr %} + {% if user.contributor == submission.editor_in_charge or user|is_in_group:'Editorial Administrators' and not user|is_possible_author_of_submission:submission %} <div class="reportid"> <h3> diff --git a/submissions/templates/partials/submissions/submission_author_information.html b/submissions/templates/partials/submissions/submission_author_information.html new file mode 100644 index 0000000000000000000000000000000000000000..f5e7d4d4ace297fb38ee0d2aa8aaeae8e6d64d9e --- /dev/null +++ b/submissions/templates/partials/submissions/submission_author_information.html @@ -0,0 +1,95 @@ +{% load bootstrap %} + +<h3 class="highlight">Author information <small><small class="text-muted"><i class="fa fa-question-circle-o" data-toggle="tooltip" data-title="You see this information because you are a verified author of this Submission."></i></small></small></h3> +<a href="javascript:;" class="btn btn-default mb-2" data-toggle="toggle" data-target="#authorinformation">Show/hide author information</a> + +<div id="authorinformation" class="mt-2"> + <div class="mb-4"> + <h3>Status summary:</h3> + <table class="table table-borderless"> + <tr> + <td>Submission status:</td> + <td><span class="label label-secondary">{{ submission.get_status_display }}</span></td> + </tr> + <tr> + <td>Recommendation status:</td> + <td> + {% if submission.eicrecommendations.active.first %} + <span class="label label-secondary">{{ submission.eicrecommendations.active.first.get_status_display }}</span> + {% else %} + <span class="label label-secondary">No Editorial Recommendation is formulated yet.</span> + {% endif %} + </td> + </tr> + <tr> + <td>Submission is publicly available:</td> + <td> + {% if submission.visible_public %} + <i class="fa fa-check-circle text-success" aria-hidden="true"></i> + <span class="text-muted">Available in public pages and search results.</span> + {% else %} + <i class="fa fa-times-circle text-danger" aria-hidden="true"></i> + <span class="text-muted">Only available for editors and authors.</span> + {% endif %} + </td> + </tr> + {% if submission.publication and submission.publication.is_published %} + <tr> + <td>Submission is published as:</td> + <td> + <a href="{{submission.publication.get_absolute_url}}"> + {% if submission.publication.in_issue %} + {{submission.publication.in_issue.in_volume.in_journal.abbreviation_citation}} <strong>{{submission.publication.in_issue.in_volume.number}}</strong>, {{submission.publication.get_paper_nr}} + {% else %} + {{submission.publication.in_journal.abbreviation_citation}}, {{submission.publication.paper_nr}} + {% endif %} + ({{submission.publication.publication_date|date:'Y'}})</a> + </td> + </tr> + {% endif %} + </table> + </div> + + <div class="mb-4"> + <h3 class="mb-2">Editorial Recommendation:</h3> + {% for recommendation in submission.eicrecommendations.active %} + {% include 'partials/submissions/recommendation_author_content.html' with recommendation=recommendation %} + {% empty %} + No Editorial Recommendation is formulated yet. + {% endfor %} + </div> + + <div class="mb-4"> + <h3>Events:</h3> + <div id="eventslist"> + {% include 'partials/submissions/submission_events.html' with events=submission.events.for_author %} + </div> + </div> + + + <div class="mb-4" id="proofsslist"> + <h3>Proofs:</h3> + <ul class="list-group list-group-flush events-list"> + {% for proofs in submission.production_stream.proofs.for_authors %} + <li> + <a href="{{ proofs.get_absolute_url }}" target="_blank">Download version {{ proofs.version }}</a> · uploaded: {{ proofs.created|date:"DATE_FORMAT" }} · + status: <span class="label label-secondary label-sm">{{ proofs.get_status_display }}</span> + {% if proofs.status == 'accepted_sup' or proofs.status == 'sent' %} + {% if proofs_decision_form and is_author %} + <h3 class="mb-0 mt-2">Please advise the Production Team on your findings on Proofs version {{ proofs.version }}</h3> + <form method="post" enctype="multipart/form-data" action="{% url 'production:author_decision' proofs.slug %}" class="my-2"> + {% csrf_token %} + {{ proofs_decision_form|bootstrap }} + <input class="btn btn-primary btn-sm" type="submit" value="Submit"> + </form> + {% endif %} + {% endif %} + </li> + {% empty %} + <li class="list-group-item">No proofs are available yet.</li> + {% endfor %} + </ul> + </div> +</div> + +<hr class="divider"> diff --git a/submissions/templates/partials/submissions/submission_editorial_information.html b/submissions/templates/partials/submissions/submission_editorial_information.html new file mode 100644 index 0000000000000000000000000000000000000000..67390baf870c6463734b314e92f5a8fcb3c2b03a --- /dev/null +++ b/submissions/templates/partials/submissions/submission_editorial_information.html @@ -0,0 +1,113 @@ +{% load bootstrap %} + +<h3 class="highlight">Editorial information</h3> +<a href="javascript:;" class="btn btn-default mb-2" data-toggle="toggle" data-target="#editorialinformation">Show/hide editorial information</a> + +<div id="editorialinformation" class="mt-2" style="display:none;"> + <div class="mb-4"> + {% if submission.editor_in_charge == request.user.contributor %} + <p><strong>You are the Editor-in-charge, go to the <a href="{% url 'submissions:editorial_page' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}">Editorial Page</a> to take editorial actions.</strong></p> + {% elif perms.scipost.can_oversee_refereeing and not is_author and not is_author_unchecked %} + <p><strong>You are Editorial Administrator. See <a href="{% url 'submissions:editorial_page' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}">Editorial Page</a> for detailed information.</strong></p> + {% endif %} + + <h3>Status summary:</h3> + <table class="table table-borderless"> + <tr> + <td>Submission status:</td> + <td><span class="label label-secondary">{{ submission.get_status_display }}</span></td> + </tr> + <tr> + <td>Recommendation status:</td> + <td> + {% if submission.eicrecommendations.active.first %} + <span class="label label-secondary">{{ submission.eicrecommendations.active.first.get_status_display }}</span> + {% else %} + <span class="label label-secondary">No Editorial Recommendation is formulated yet.</span> + {% endif %} + </td> + </tr> + <tr> + <td>Submission is publicly available:</td> + <td> + {% if submission.visible_public %} + <i class="fa fa-check-circle text-success" aria-hidden="true"></i> + <span class="text-muted">Available in public pages and search results.</span> + {% else %} + <i class="fa fa-times-circle text-danger" aria-hidden="true"></i> + <span class="text-muted">Only available for editors and authors.</span> + {% endif %} + </td> + </tr> + <tr> + <td>Submission is current version:</td> + <td> + {% if submission.is_current %} + <i class="fa fa-check-circle text-success" aria-hidden="true"></i> + <span class="text-muted">This is the current version.</span> + {% else %} + <i class="fa fa-times-circle text-danger" aria-hidden="true"></i> + <span class="text-muted">This is not the current version.</span> + {% endif %} + </td> + </tr> + {% if submission.publication and submission.publication.is_published %} + <tr> + <td>Submission is published as:</td> + <td> + <a href="{{submission.publication.get_absolute_url}}"> + {% if submission.publication.in_issue %} + {{submission.publication.in_issue.in_volume.in_journal.abbreviation_citation}} <strong>{{submission.publication.in_issue.in_volume.number}}</strong>, {{submission.publication.get_paper_nr}} + {% else %} + {{submission.publication.in_journal.abbreviation_citation}}, {{submission.publication.paper_nr}} + {% endif %} + ({{submission.publication.publication_date|date:'Y'}})</a> + </td> + </tr> + {% endif %} + </table> + </div> + + <div class="mb-4"> + <h3 class="mb-2">Editorial Recommendation:</h3> + {% for recommendation in submission.eicrecommendations.active %} + {% include 'partials/submissions/recommendation_author_content.html' with recommendation=recommendation %} + {% empty %} + No Editorial Recommendation is formulated yet. + {% endfor %} + </div> + + <div class="mb-4"> + <h3>Events:</h3> + <div id="eventslist"> + {% include 'partials/submissions/submission_events.html' with events=submission.events.for_author %} + </div> + </div> + + + <div class="mb-4" id="proofsslist"> + <h3>Proofs:</h3> + <ul class="list-group list-group-flush events-list"> + {% for proofs in submission.production_stream.proofs.for_authors %} + <li> + <a href="{{ proofs.get_absolute_url }}" target="_blank">Download version {{ proofs.version }}</a> · uploaded: {{ proofs.created|date:"DATE_FORMAT" }} · + status: <span class="label label-secondary label-sm">{{ proofs.get_status_display }}</span> + {% if proofs.status == 'accepted_sup' or proofs.status == 'sent' %} + {% if proofs_decision_form and is_author %} + <h3 class="mb-0 mt-2">Please advise the Production Team on your findings on Proofs version {{ proofs.version }}</h3> + <form method="post" enctype="multipart/form-data" action="{% url 'production:author_decision' proofs.slug %}" class="my-2"> + {% csrf_token %} + {{ proofs_decision_form|bootstrap }} + <input class="btn btn-primary btn-sm" type="submit" value="Submit"> + </form> + {% endif %} + {% endif %} + </li> + {% empty %} + <li class="list-group-item">No proofs are available yet.</li> + {% endfor %} + </ul> + </div> +</div> + +<hr class="divider"> diff --git a/submissions/templates/submissions/admin/submission_prescreening.html b/submissions/templates/submissions/admin/submission_prescreening.html new file mode 100644 index 0000000000000000000000000000000000000000..8ebe8e61ded688eaa48a2ec20db361fafdfbca7e --- /dev/null +++ b/submissions/templates/submissions/admin/submission_prescreening.html @@ -0,0 +1,79 @@ +{% extends 'submissions/admin/base.html' %} + +{% load bootstrap %} +{% load scipost_extras %} + +{% block pagetitle %}: pre-screening ({{ submission.arxiv_identifier_w_vn_nr }}){% endblock pagetitle %} + +{% block breadcrumb_items %} + {{block.super}} + <span class="breadcrumb-item">Pre-screening {{ submission.arxiv_identifier_w_vn_nr }}</span> +{% endblock %} + +{% block content %} + <h1 class="highlight">Pre-screening of Submission</h1> + <h3>Submission summary</h3> + {% include 'partials/submissions/submission_summary.html' with submission=submission hide_title=1 show_abstract=1 %} + + <h3>Fellows ({{ submission.fellows.ordered|length }})</h3> + <a href="{% url 'colleges:submission' submission.arxiv_identifier_w_vn_nr %}">Manage Pool composition</a><br> + <a href="javascript:;" data-toggle="toggle" data-target="#fellows">Show/hide all Fellows</a> + <table style="display: none;" class="table table-hover my-2" id="fellows"> + <thead> + <tr> + <th>Fellow name</th> + <th>Expertises</th> + </tr> + </thead> + <tbody> + {% for fellow in submission.fellows.ordered %} + <tr> + <td>{{ fellow }}</td> + <td> + {% for expertise in fellow.contributor.expertises %} + <div class="single d-inline" data-specialization="{{expertise|lower}}" data-toggle="tooltip" data-placement="bottom" title="{{expertise|get_specialization_display}}">{{expertise|get_specialization_code}}</div> + {% endfor %} + </tr> + {% endfor %} + </tbody> + </table> + + <h3 class="mt-2">Plagiarism report</h3> + {% if submission.plagiarism_report %} + <a href="{% url 'submissions:plagiarism' submission.arxiv_identifier_w_vn_nr %}">Update plagiarism report</a> + <br> + <table> + <tr> + <td style="min-width: 150px;">iThenticate document</td> + <td>{{submission.plagiarism_report.doc_id}}</td> + </tr> + <tr> + <td>Percent match</td> + <td>{{ submission.plagiarism_report.percent_match|default:'?' }}%</td> + </tr> + <tr> + <td>Processed</td> + <td>{{submission.plagiarism_report.processed_time}}</td> + </tr> + <tr> + <td>Uploaded</td> + <td>{{submission.plagiarism_report.uploaded_time}}</td> + </tr> + <tr> + <td>Latest update</td> + <td>{{submission.plagiarism_report.latest_activity}}</td> + </tr> + </table> + <br> + {% else %} + <p>No Plagiarism Report found. <a href="{% url 'submissions:plagiarism' submission.arxiv_identifier_w_vn_nr %}">Run plagiarism check</a>.</p> + {% endif %} + + <h3>Take decision on pre-screening</h3> + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input type="submit" class="btn btn-primary" value="Submit"> + </form> + +{% endblock content %} diff --git a/submissions/templates/submissions/pool/editorial_assignment.html b/submissions/templates/submissions/pool/editorial_assignment.html new file mode 100644 index 0000000000000000000000000000000000000000..1ab6d603f10fdded4501eaf5672422ba1bd4425b --- /dev/null +++ b/submissions/templates/submissions/pool/editorial_assignment.html @@ -0,0 +1,65 @@ +{% extends 'submissions/pool/base.html' %} + +{% load bootstrap %} +{% load guardian_tags %} +{% load scipost_extras %} +{% load submissions_extras %} + +{% block breadcrumb_items %} + {{ block.super }} + <span class="breadcrumb-item">Editorial Assignment</span> +{% endblock %} + +{% block pagetitle %}: Editorial Assignment{% endblock pagetitle %} + +{% block content %} + +<h1 class="highlight">Editorial Assignment</h1> + +{% if form.instance.id %} + <h4 class="pt-0">We have received a Submission to SciPost for which we would like you to consider becoming Editor-in-charge.</h4> +{% endif %} + <h4 class="pt-0">Can you act as Editor-in-charge?{% if form.instance.id %} (see below to accept/decline){% endif %}</h4> +<br> + +<h3>Submission details</h3> +{% include 'partials/submissions/submission_summary.html' with submission=submission %} + +<br> +{% if form.instance.id %} + <h2 class="highlight">Accept or decline this Editorial Assignment</h2> +{% else %} + <h2 class="highlight">Volunteer to become Editor-in-charge</h2> +{% endif %} +<h4 class="mb-2">By accepting, you will be required to start a refereeing round on the next screen.</h4> + +<form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input class="btn btn-primary" type="submit" value="Submit" /> + <p class="border p-2 mt-3"> + <strong>Clarification</strong> + <br> + If you choose the <em>Normal refereeing cycle</em>, you will be redirected to the Editorial Page to proceed further. The Submission will be publicly available and the authors will be informed that the refereeing process has started. + <br> + If you choose to <em>directly formulate an Editorial Recommendation for rejection</em>, the Submission will not become publicly available. After formulation of the Editorial Recommendation, it will be put forward for voting as normal. + </p> +</form> + + +<script> + $(function() { + $('[name="decision"]').on('change', function() { + var val = $('[name="decision"]:checked').val(); + if(val == 'decline') { + $('[name="refusal_reason"]').closest('.form-group').show(); + $('[name="refereeing_cycle"]').closest('.form-group').hide(); + } else { + $('[name="refusal_reason"]').closest('.form-group').hide(); + $('[name="refereeing_cycle"]').closest('.form-group').show(); + } + }).trigger('change'); + }); +</script> + +{% endblock %} diff --git a/submissions/templates/submissions/pool/editorial_page.html b/submissions/templates/submissions/pool/editorial_page.html index e250a4966dfad5f5828bb9718fcd5e940836d630..0830ad21c54f6b9874307e1b8721563a3ee9af52 100644 --- a/submissions/templates/submissions/pool/editorial_page.html +++ b/submissions/templates/submissions/pool/editorial_page.html @@ -22,7 +22,7 @@ <h3>by {{submission.author_list}}</h3> <div class="ml-2 mt-2"> - <h3>- Go to the <a href="{% url 'submissions:submission' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}">Submissions Page</a> to view Reports and Comments</h3> + <h3>- Go to the <a href="{% url 'submissions:submission' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}">Submission Page</a> to view Reports and Comments</h3> </div> <h3 class="mt-4">Submission summary</h3> @@ -67,32 +67,107 @@ </div> </div> - -<div class="card card-grey my-4"> - <div class="card-body"> - <h2 class="card-title">Editorial Workflow</h2> - <a href="{% url 'submissions:editorial_workflow' %}">How-to guide: summary of the editorial workflow</a> - </div> +<div class="py-2 mb-2"> + <h2 class="highlight">Editorial Workflow</h2> + <a href="{% url 'submissions:editorial_workflow' %}">How-to guide: summary of the editorial workflow</a> </div> - <div class="row"><!-- Status --> - <div class="col-md-12"> - {% include 'partials/submissions/submission_status.html' with submission=submission %} - {% if submission.plagiarism_report %} - <h4>Plagiarism report status: {% if submission.plagiarism_report.percent_match %}<b>{{submission.plagiarism_report.percent_match}}%</b>{% else %}<em>Scan in progress</em>{% endif %}</h4> - {% endif %} + <div class="col"> + <h3>Editorial status:</h3> + <table class="table table-borderless"> + <tr> + <td>Submission status:</td> + <td><span class="label label-secondary">{{ submission.get_status_display }}</span></td> + </tr> + <tr> + <td>Recommendation status:</td> + <td> + {% if submission.eicrecommendations.active.first %} + <span class="label label-secondary">{{ submission.eicrecommendations.active.first.get_status_display }}</span> + {% else %} + <span class="label label-secondary mb-1">No Editorial Recommendation is formulated yet.</span> + {% if submission.eic_recommendation_required %} + <br><a href="{% url 'submissions:eic_recommendation' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}">Formulate an Editorial Recommendation here.</a> + {% endif %} + {% endif %} + </td> + </tr> + <tr> + <td>Refereeing cycle:</td> + <td>{{ submission.get_refereeing_cycle_display }}</td> + </tr> + <tr> + <td>Publicly available:</td> + <td> + {% if submission.visible_public %} + <i class="fa fa-check-circle text-success" aria-hidden="true"></i> + <span class="text-muted">Available in public pages and search results.</span> + {% else %} + <i class="fa fa-times-circle text-danger" aria-hidden="true"></i> + <span class="text-muted">Only available for editors and authors.</span> + {% endif %} + </td> + </tr> + <tr> + <td>Plagiarism report:</td> + <td> + {% if submission.plagiarism_report %} + {% if submission.plagiarism_report.percent_match %} + <b>{{ submission.plagiarism_report.percent_match }}%</b> + {% else %} + <em>Scan in progress</em> + {% if perms.scipost.can_do_plagiarism_checks %} + <br> + <a href="{% url 'submissions:plagiarism' submission.arxiv_identifier_w_vn_nr %}">Update plagiarism score</a> + {% endif %} + {% endif %} + {% else %} + <em>No plagiarism report found.</em> + {% if perms.scipost.can_do_plagiarism_checks %} + <br> + <a href="{% url 'submissions:plagiarism' submission.arxiv_identifier_w_vn_nr %}">Run plagiarism check</a> + {% endif %} + {% endif %} + </td> + </tr> + <tr> + <td>Open for commenting:</td> + <td> + {% if submission.open_for_commenting %} + <i class="fa fa-check-circle text-success" aria-hidden="true"></i> + <span class="text-muted">Open for commenting.</span> + {% else %} + <i class="fa fa-times-circle text-danger" aria-hidden="true"></i> + <span class="text-muted">Commenting closed.</span> + {% endif %} + </td> + </tr> + <tr> + <td>Open for refereeing:</td> + <td> + {% if submission.is_open_for_reporting %} + <i class="fa fa-check-circle text-success" aria-hidden="true"></i> + <span class="text-muted">Open for refereeing. Deadline: {{ submission.reporting_deadline|date:"SHORT_DATE_FORMAT" }}</span> + {% else %} + <i class="fa fa-times-circle text-danger" aria-hidden="true"></i> + <span class="text-muted"> + Refereeing closed. + <br> + <em>Invited referees may still submit a Report, as long as their invitation is not finished nor cancelled.</em> + </span> + {% endif %} + </td> + </tr> + </table> </div> -</div><!-- End status --> - -{% if full_access %} - <div class="row"> - <div class="col-md-10 col-lg-8"> + {% if full_access %} + <div class="col-12"> {% include 'partials/submissions/pool/required_actions_block.html' with submission=submission %} </div> - </div> -{% endif %} + {% endif %} +</div><!-- End status --> -{% if submission.status == 'resubmitted_incoming' %} +{% if not submission.refereeing_cycle %} {% if full_access %} <div class="row"> <div class="col-12"> @@ -111,7 +186,9 @@ <div class="row"> <div class="col-12"> <h3>Refereeing status summary:</h3> + {% include 'partials/submissions/pool/referee_invitations_status.html' with submission=submission %} + <a href="#reports-summary">View Reports and Comments on this Submission</a> </div> </div> @@ -174,7 +251,7 @@ </form> </li> - {% if not submission.reporting_deadline_has_passed %} + {% if submission.is_open_for_reporting %} <li><a href="{% url 'submissions:close_refereeing_round' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}">Close the refereeing round</a> (deactivates submission of new Reports and Comments)</li> {% endif %} {% endif %} @@ -212,7 +289,7 @@ {% if submission.eicrecommendations.last %} <a href="{% url 'submissions:reformulate_eic_recommendation' submission.arxiv_identifier_w_vn_nr %}">Reformulate Editorial Recommendation</a> {% else %} - <a href="{% url 'submissions:eic_recommendation' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}">Formulate an Editorial Recommendation</a> + <a href="{% url 'submissions:eic_recommendation' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}">Formulate an Editorial Recommendation.</a> {% endif %} <p> If you recommend revisions, this will be communicated directly to the Authors, who will be asked to resubmit. @@ -232,7 +309,13 @@ {% endif %} {% if full_access %} - <h2 class="mt-3">Communications</h2> + <h3 class="mt-3 mb-2" id="reports-summary">Reports</h3> + {% include 'partials/submissions/pool/submission_reports_summary_table.html' with submission=submission %} + + <h3 class="mt-3 mb-2">Comments</h3> + {% include 'partials/submissions/pool/submission_comments_summary_table.html' with submission=submission %} + + <h3 class="mt-3">Communications</h3> <ul> {% if submission.editor_in_charge == request.user.contributor %} <li><a href="{% url 'submissions:communication' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr comtype='EtoA' %}">Draft and send a communication with the submitting Author</a></li> @@ -257,8 +340,9 @@ </div> </div> - <h2 class="mt-3">Events</h2> + <h3 class="mt-3">Events</h3> {% include 'partials/submissions/submission_events.html' with events=submission.events.for_eic %} + {% endif %} <div class="mb-5"></div> diff --git a/submissions/templates/submissions/pool/pool.html b/submissions/templates/submissions/pool/pool.html index 92ae237be3333eb98e753ecaac6f0c2d873c06d5..92af9fba50a8aa088f68cd946aefecec453c8bde 100644 --- a/submissions/templates/submissions/pool/pool.html +++ b/submissions/templates/submissions/pool/pool.html @@ -30,11 +30,24 @@ <div class="quote-border"> <h2 class="text-primary">Administrative Tasks</h2> + {% if pre_screening_subs %} + <h3>Submissions in pre-screening phase <i class="fa fa-exclamation-circle text-warning"></i></h3> + <ul> + {% for submission in pre_screening_subs %} + <li> + {{ submission }}<br> + <a href="{% url 'submissions:do_prescreening' submission.arxiv_identifier_w_vn_nr %}">Do pre-screening</a> + </li> + {% endfor %} + </ul> + {% endif %} + {% if recommendations.voting_in_preparation %} <h3>Recommendations to prepare for voting <i class="fa fa-exclamation-circle text-warning"></i></h3> <ul> {% for recommendation in recommendations.voting_in_preparation %} - <li>On Editorial Recommendation: {{ recommendation }}<br> + <li> + On Editorial Recommendation: {{ recommendation }}<br> <a href="{% url 'submissions:prepare_for_voting' rec_id=recommendation.id %}">Prepare for voting</a> </li> {% endfor %} @@ -45,7 +58,8 @@ <h3>Recommendations undergoing voting <i class="fa fa-exclamation-circle text-warning"></i></h3> <ul class="fa-ul"> {% for recommendation in recommendations.put_to_voting %} - <li>{% include 'partials/submissions/admin/recommendation_tooltip.html' with classes='fa-li' recommendation=recommendation %} + <li> + {% include 'partials/submissions/admin/recommendation_tooltip.html' with classes='fa-li' recommendation=recommendation %} On Editorial Recommendation: {{ recommendation }}<br> <a href="{% url 'submissions:eic_recommendation_detail' recommendation.submission.arxiv_identifier_w_vn_nr recommendation.id %}">See Editorial Recommendation</a> </li> @@ -99,8 +113,8 @@ <h3>All Submissions with status: <span class="text-primary">{{ search_form.status_verbose }}</span></h3> {% include 'partials/submissions/pool/submissions_list.html' with submissions=submissions %} {% else %} - <h3>Submissions currently in pre-screening</h3> - {% include 'partials/submissions/pool/submissions_list.html' with submissions=submissions.prescreening %} + <h3>Submissions currently unassigned</h3> + {% include 'partials/submissions/pool/submissions_list.html' with submissions=submissions.unassigned %} <h3>Submissions currently in active refereeing phase</h3> {% include 'partials/submissions/pool/submissions_list.html' with submissions=submissions.actively_refereeing %} diff --git a/submissions/templates/submissions/pool/recommendation.html b/submissions/templates/submissions/pool/recommendation.html index 6a15dfd082c466208210e6e3bd4137f8abe36c53..7077d4970894e9e3102bf37762a98acb6a054d39 100644 --- a/submissions/templates/submissions/pool/recommendation.html +++ b/submissions/templates/submissions/pool/recommendation.html @@ -79,19 +79,27 @@ <div class="card-footer bg-light py-3"> <h3 class="card-title">Administrative actions on recommendations undergoing voting:</h3> <ul class="mb-0"> - <li>To send an email reminder to each Fellow with at least one voting duty: <a href="{% url 'submissions:remind_Fellows_to_vote' %}">click here</a></li> - <li>To fix the College decision and follow the Editorial Recommendation as is: <a href="{% url 'submissions:fix_College_decision' rec_id=recommendation.id %}">click here</a></li> + <li>To send an email reminder to each Fellow with at least one voting duty: <a href="{% url 'submissions:remind_Fellows_to_vote' %}">click here</a>.</li> + <li> + The current Editorial Recommendation: {{ recommendation.get_recommendation_display }}. + <br> + <form method="post" action="{% url 'submissions:eic_recommendation_detail' recommendation.submission.arxiv_identifier_w_vn_nr recommendation.id %}" class="d-inline-block my-2"> + {% csrf_token %} + <button type="submit" name="action" value="fix" class="btn btn-primary btn-sm mr-2">Fix College decision</button> + <button type="submit" name="action" value="deprecate" class="btn btn-danger btn-sm text-white">Deprecate College decision</button> + </form> + </li> <li>To request a modification of the Recommendation to request for revision: click here</li> </ul> </div> {% endif %} </div> - {% if form %} + {% if voting_form %} <h3 class="mt-4">Your position on this recommendation</h3> <form action="{% url 'submissions:vote_on_rec' rec_id=recommendation.id %}" method="post"> {% csrf_token %} - {{ form|bootstrap:'0,12' }} + {{ voting_form|bootstrap:'0,12' }} <input type="submit" name="submit" value="Cast your vote" class="btn btn-primary" id="submit-id-submit"> </form> {% endif %} diff --git a/submissions/templates/submissions/report_form.html b/submissions/templates/submissions/report_form.html index 84b69c692cf125c1b5ab90effb2b5580cf4847a2..12d790a0d920db64a377954e9da297a6d3ed6276 100644 --- a/submissions/templates/submissions/report_form.html +++ b/submissions/templates/submissions/report_form.html @@ -91,7 +91,7 @@ <div class="row"> <div class="col-md-6"> <br> - <form action="{% url 'submissions:submit_report' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}" method="post"> + <form action="{% url 'submissions:submit_report' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}" method="post" enctype="multipart/form-data"> {% csrf_token %} {{ form|bootstrap:'12,12' }} <div class="anonymous-alert" style="display: none;"> diff --git a/submissions/templates/submissions/submission_detail.html b/submissions/templates/submissions/submission_detail.html index 1c27d127c1bfbeb5d0239821f8e7e4a1766f4453..37e4b2fff9bd4540d7e76176e33173718c4f23c1 100644 --- a/submissions/templates/submissions/submission_detail.html +++ b/submissions/templates/submissions/submission_detail.html @@ -20,14 +20,35 @@ {% block content %} <div class="row"> + {% if is_author_unchecked %} + <div class="col-12"> + <div class="border border-warning py-2 px-3 mb-3"> + <h3><i class="fa fa-exclamation-circle text-warning"></i> Please advise</h3> + The system flagged you as a potential author of this Submission. Please <a href="{% url 'scipost:claim_authorships' %}">clarify this here</a>. Particular actions and information may be blocked until your authorship has been verified. + </div> + </div> + {% endif %} + {% if unfinished_report_for_user %} + <div class="col-12"> + <div class="border border-warning py-2 px-3 mb-3"> + <i class="fa fa-exclamation-circle text-warning"></i> 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></h3> + </div> + </div> + {% endif %} <div class="col-md-8"> <h2>SciPost Submission Page</h2> <h1 class="text-primary">{{submission.title}}</h1> <h3 class="mb-3">by {{submission.author_list}}</h3> - <div class="pl-2"> + <div class="pl-2 mb-1"> {% if submission.publication and submission.publication.is_published %} - <h3>- Published as <a href="{{submission.publication.get_absolute_url}}">{{submission.publication.in_issue.in_volume.in_journal.abbreviation_citation}} <strong>{{submission.publication.in_issue.in_volume.number}}</strong>, {{submission.publication.get_paper_nr}} ({{submission.publication.publication_date|date:'Y'}})</a></h3> + <h3>- Published as <a href="{{submission.publication.get_absolute_url}}"> + {% if submission.publication.in_issue %} + {{submission.publication.in_issue.in_volume.in_journal.abbreviation_citation}} <strong>{{submission.publication.in_issue.in_volume.number}}</strong>, {{submission.publication.get_paper_nr}} + {% else %} + {{submission.publication.in_journal.abbreviation_citation}}, {{submission.publication.paper_nr}} + {% endif %} + ({{submission.publication.publication_date|date:'Y'}})</a></h3> {% endif %} @@ -35,10 +56,6 @@ <h3>- You are the Editor-in-charge, go to the <a href="{% url 'submissions:editorial_page' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}">Editorial Page</a> to take editorial actions</h3> {% endif %} - {% if unfinished_report_for_user %} - <h3>- <span class="circle text-danger border-danger">!</span> 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></h3> - {% endif %} - {% if not submission.is_current %} <h3><span class="text-danger">- This is not the current version.</span></h3> {% endif %} @@ -74,67 +91,39 @@ </div> -{% if is_author or user|is_in_group:'Editorial College' or user|is_in_group:'Editorial Administrators' %} - {% for recommendation in recommendations %} - {% if user|is_in_group:'Editorial College' or user|is_in_group:'Editorial Administrators' or recommendation|is_viewable_by_authors %} - {% include 'partials/submissions/recommendation_author_content.html' with recommendation=recommendation %} - {% endif %} - {% endfor %} - - <div class="mb-4"> - <h3>Events</h3> - <div id="eventslist"> - {% include 'partials/submissions/submission_events.html' with events=submission.events.for_author %} - </div> - </div> +{% if is_author %} + {% include 'partials/submissions/submission_author_information.html' with submission=submission %} {% endif %} -{% if is_author or user|is_in_group:'Editorial Administrators' %} - {% if submission.production_stream.proofs.for_authors.exists %} - <div class="mb-4" id="proofsslist"> - <h3>Proofs</h3> - <ul> - {% for proofs in submission.production_stream.proofs.for_authors %} - <li> - <a href="{{ proofs.get_absolute_url }}" target="_blank">Download version {{ proofs.version }}</a> · uploaded: {{ proofs.created|date:"DATE_FORMAT" }} · - status: <span class="label label-secondary label-sm">{{ proofs.get_status_display }}</span> - {% if proofs.status == 'accepted_sup' or proofs.status == 'sent' %} - {% if proofs_decision_form and is_author %} - <h3 class="mb-0 mt-2">Please advise the Production Team on your findings on Proofs version {{ proofs.version }}</h3> - <form method="post" enctype="multipart/form-data" action="{% url 'production:author_decision' proofs.slug %}" class="my-2"> - {% csrf_token %} - {{ proofs_decision_form|bootstrap }} - <input class="btn btn-primary btn-sm" type="submit" value="Submit"> - </form> - {% endif %} - {% endif %} - </li> - {% endfor %} - </ul> - </div> - {% endif %} +{% if can_read_editorial_information %} + {% include 'partials/submissions/submission_editorial_information.html' with submission=submission %} {% endif %} -{% if user.is_authenticated and user|is_in_group:'Registered Contributors' %} +{% if perms.scipost.can_submit_comments %} <div class="row"> <div class="col-12"> - <h2 class="highlight">Actions</h2> + <h3 class="highlight">Actions</h3> <ul> - {% if submission.open_for_reporting %} - {% if perms.scipost.can_referee and not is_author and not is_author_unchecked %} + {% if submission.is_open_for_reporting or 1 and perms.scipost.can_referee %} + {% if 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 %}">{% 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 %} <li> - <h3 class="text-blue">Contribute a Report [deactivated]</h3> - <div>The system flagged you as a potential author of this Submission. - Please go to your <a href="{% url 'scipost:personal_page' %}">personal page</a> - under the Submissions tab to clarify this.</div> + <h3><a href="javascript:;">Contribute a Report</a> <small><span class="text-danger">[deactivated]</span></small></h3> + <div class="border border-warning py-2 px-3 mb-2"> + The system flagged you as a potential author of this Submission. Please <a href="{% url 'scipost:claim_authorships' %}">clarify this here</a>. You are not allowed to contributor a Report until your authorship has been verified. + </div> + </li> + {% elif is_author %} + <li> + <a href="javascript:;" disabled>Contribute a Report</a> <br><span class="text-danger">You are a verified author. Therefore, you can not submit a Report.</span>. </li> {% endif %} + {% elif unfinished_report_for_user %} + <li><i class="fa fa-exclamation"></i> You have an unfinished report for this submission. You can <a href="{% url 'submissions:submit_report' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}">finish your report here</a>.</li> {% else %} <li>Reporting for this Submission is closed.</li> {% endif %} @@ -145,15 +134,15 @@ {% else %} <li>Commenting on this Submission is closed.</li> {% endif %} - </ul> - {% if perms.scipost.can_manage_reports %} - <h3>Admin Actions</h3> - <ul> + {% if submission.editor_in_charge == request.user.contributor %} + <li><a href="{% url 'submissions:editorial_page' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}">Go to the Editorial Page</a></li> + {% endif %} + {% if perms.scipost.can_manage_reports %} <li> <a href="{% url 'submissions:treated_submission_pdf_compile' submission.arxiv_identifier_w_vn_nr %}">Update the Refereeing Package pdf</a> </li> - <ul> - {% endif %} + {% endif %} + <ul> </div> </div> {% endif %} @@ -184,17 +173,11 @@ {% if contributed_reports %} <hr class="divider"> -<div class="row"> - <div class="col-12"> - <div class="card card-grey"> - <div class="card-body"> - <h2 class="card-title mb-0">Contributed Reports on this Submission</h2> - <a href="javascript:;" data-toggle="toggle" data-target="#contributedreportslist">Toggle contributed reports view</a> - </div> - </div> - </div> -</div> +<div class="mb-3"> + <h2 class="highlight">Contributed Reports on this Submission</h2> + <a href="javascript:;" data-toggle="toggle" data-target="#contributedreportslist">Show/hide contributed reports</a> +</div> <div id="contributedreportslist"> {% for report in contributed_reports %} {% include 'partials/submissions/report_public.html' with report=report user=request.user perms=perms %} @@ -218,10 +201,12 @@ {# This is an apparent redundant logic block; however, it makes sure the "login to ..." links wouldn't be shown twice! #} -{% if not user.is_authenticated and submission.comments.vetted.exists %} - {% include 'comments/new_comment.html' with object_id=submission.id type_of_object='submission' open_for_commenting=submission.open_for_commenting user_is_referee=submission|user_is_referee:request.user %} -{% elif user.is_authenticated %} - {% include 'comments/new_comment.html' with object_id=submission.id type_of_object='submission' open_for_commenting=submission.open_for_commenting user_is_referee=submission|user_is_referee:request.user %} +{% if comment_form %} + {% if not user.is_authenticated and submission.comments.vetted.exists %} + {% include 'comments/new_comment.html' with form=comment_form object_id=submission.id type_of_object='submission' open_for_commenting=submission.open_for_commenting user_is_referee=submission|user_is_referee:request.user %} + {% elif user.is_authenticated %} + {% include 'comments/new_comment.html' with form=comment_form object_id=submission.id type_of_object='submission' open_for_commenting=submission.open_for_commenting user_is_referee=submission|user_is_referee:request.user %} + {% endif %} {% endif %} {% endblock content %} diff --git a/submissions/templatetags/lookup.py b/submissions/templatetags/lookup.py index 50faacc39c96bf261f033821cb099abe4021925a..2a051035679255992d82da4a3ba2c0334dc925b4 100644 --- a/submissions/templatetags/lookup.py +++ b/submissions/templatetags/lookup.py @@ -12,7 +12,7 @@ class SubmissionLookup(LookupChannel): def get_query(self, q, request): return (self.model.objects - .public_unlisted() + .public_listed() .order_by('-submission_date') .filter(title__icontains=q) .prefetch_related('publication')[:10]) diff --git a/submissions/templatetags/submissions_extras.py b/submissions/templatetags/submissions_extras.py index 24cac6d7af407eb9b7c0884b547d7f710da15b02..15cd2988f9e5fb68dcf8a7a2cf0f3e34fcfcfe15 100644 --- a/submissions/templatetags/submissions_extras.py +++ b/submissions/templatetags/submissions_extras.py @@ -4,30 +4,44 @@ __license__ = "AGPL v3" from django import template -from submissions.models import Submission +from ..constants import DECISION_FIXED +from ..models import Submission, EICRecommendation register = template.Library() -@register.filter(name='is_not_author_of_submission') -def is_not_author_of_submission(user, arxiv_identifier_w_vn_nr): - submission = Submission.objects.get(arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) - return (user.contributor not in submission.authors.all() - and - (user.last_name not in submission.author_list - or - user.contributor in submission.authors_false_claims.all())) +@register.filter +def is_possible_author_of_submission(user, submission): + """Check if User may be related to the Submission as author.""" + if not isinstance(submission, Submission): + return False + + if not user.is_authenticated: + return False + if submission.authors.filter(user=user).exists(): + # User explicitly assigned author. + return True -@register.filter(name='is_viewable_by_authors') + if submission.authors_false_claims.filter(user=user).exists(): + # User explicitly dissociated from the Submission. + return False + + # Last resort: last name check + return user.last_name in submission.author_list + + +@register.filter def is_viewable_by_authors(recommendation): - return recommendation.submission.status in ['revision_requested', 'resubmitted', - 'accepted', 'rejected', - 'published', 'withdrawn'] + """Check if the EICRecommendation is viewable by the authors of the Submission.""" + if not isinstance(recommendation, EICRecommendation): + return False + return recommendation.status == DECISION_FIXED @register.filter def user_is_referee(submission, user): + """Check if the User is invited to be Referee of the Submission.""" if not user.is_authenticated: return False return submission.referee_invitations.filter(referee__user=user).exists() @@ -35,6 +49,7 @@ def user_is_referee(submission, user): @register.filter def is_voting_fellow(submission, user): + """Check if the User is a voting-Fellow of the Submission.""" if not user.is_authenticated: return False return submission.voting_fellows.filter(contributor__user=user).exists() diff --git a/submissions/templatetags/submissions_pool.py b/submissions/templatetags/submissions_pool.py new file mode 100644 index 0000000000000000000000000000000000000000..d27a642d7822ac9d30a2661f14b1457dac537ed1 --- /dev/null +++ b/submissions/templatetags/submissions_pool.py @@ -0,0 +1,17 @@ +__copyright__ = "Copyright 2016-2018, Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django import template + +from ..models import EditorialAssignment + +register = template.Library() + + +@register.simple_tag +def get_editor_invitations(submission, user): + """Check if the User invited to become EIC for Submission.""" + if not user.is_authenticated or not hasattr(user, 'contributor'): + return EditorialAssignment.objects.none() + return EditorialAssignment.objects.filter(to__user=user, submission=submission).open() diff --git a/submissions/test_utils.py b/submissions/test_utils.py index 541986edd40d1443fcc397b2fabb37d9841b9514..7ca6d0792384210e48ef098f1a825f3effb17f67 100644 --- a/submissions/test_utils.py +++ b/submissions/test_utils.py @@ -10,21 +10,18 @@ from common.helpers.test import add_groups_and_permissions from scipost.factories import ContributorFactory from scipost.models import Contributor -from .constants import STATUS_UNASSIGNED, STATUS_RESUBMISSION_INCOMING, STATUS_AWAITING_ED_REC,\ - STATUS_EIC_ASSIGNED, CYCLE_DEFAULT, CYCLE_DIRECT_REC +from .constants import ( + STATUS_UNASSIGNED, STATUS_INCOMING, STATUS_EIC_ASSIGNED, CYCLE_DEFAULT, CYCLE_DIRECT_REC) from .exceptions import CycleUpdateDeadlineError from .factories import UnassignedSubmissionFactory, ResubmissionFactory from .utils import GeneralSubmissionCycle class TestDefaultSubmissionCycle(TestCase): - ''' - This TestCase should act as a master test to check all steps in the - submission's cycle: default. - ''' + """Test all steps in the Submission default cycle.""" def setUp(self): - """Basics for all tests""" + """Set up basics for all tests.""" self.submission_date = datetime.date.today() add_groups_and_permissions() ContributorFactory.create_batch(5) @@ -80,7 +77,7 @@ class TestResubmissionSubmissionCycle(TestCase): @tag('cycle', 'core') def test_init_resubmission_factory_is_valid(self): """Ensure valid fields for the factory.""" - self.assertEqual(self.submission.status, STATUS_RESUBMISSION_INCOMING) + self.assertEqual(self.submission.status, STATUS_INCOMING) self.assertIsInstance(self.submission.editor_in_charge, Contributor) self.assertTrue(self.submission.is_current) self.assertTrue(self.submission.is_resubmission) @@ -120,7 +117,7 @@ class TestResubmissionDirectSubmissionCycle(TestCase): @tag('cycle', 'core') def test_init_resubmission_factory_is_valid(self): """Ensure valid fields for the factory.""" - self.assertEqual(self.submission.status, STATUS_RESUBMISSION_INCOMING) + self.assertEqual(self.submission.status, STATUS_INCOMING) self.assertIsInstance(self.submission.editor_in_charge, Contributor) self.assertTrue(self.submission.is_current) self.assertTrue(self.submission.is_resubmission) @@ -138,4 +135,4 @@ class TestResubmissionDirectSubmissionCycle(TestCase): # Update status for default cycle to check new status self.submission.cycle.update_status() - self.assertEqual(self.submission.status, STATUS_AWAITING_ED_REC) + self.assertEqual(self.submission.status, STATUS_EIC_ASSIGNED) diff --git a/submissions/urls.py b/submissions/urls.py index 8116f9240d5951a283fe80fad35451ae181b43d4..9a708ef1c00f59bd31a110619d7c0a12ee43f752 100644 --- a/submissions/urls.py +++ b/submissions/urls.py @@ -28,11 +28,15 @@ urlpatterns = [ views.submission_detail, name='submission'), url(r'^{regex}/reports/(?P<report_nr>[0-9]+)/pdf$'.format(regex=SUBMISSIONS_COMPLETE_REGEX), views.report_detail_pdf, name='report_detail_pdf'), + url(r'^{regex}/reports/(?P<report_nr>[0-9]+)/attachment$'.format(regex=SUBMISSIONS_COMPLETE_REGEX), + views.report_attachment, name='report_attachment'), url(r'^{regex}/reports/pdf$'.format(regex=SUBMISSIONS_COMPLETE_REGEX), views.submission_refereeing_package_pdf, name='refereeing_package_pdf'), # Editorial Administration url(r'^admin/treated$', views.treated_submissions_list, name='treated_submissions_list'), + url(r'^admin/{regex}/prescreening$'.format(regex=SUBMISSIONS_COMPLETE_REGEX), + views.PreScreeningView.as_view(), name='do_prescreening'), url(r'^admin/{regex}/reports/compile$'.format(regex=SUBMISSIONS_COMPLETE_REGEX), views.treated_submission_pdf_compile, name='treated_submission_pdf_compile'), url(r'^admin/{regex}/plagiarism$'.format(regex=SUBMISSIONS_COMPLETE_REGEX), @@ -60,6 +64,12 @@ urlpatterns = [ views.assign_submission, name='assign_submission'), url(r'^pool/assignment_request/(?P<assignment_id>[0-9]+)$', views.assignment_request, name='assignment_request'), + url(r'^pool/{regex}/editorial_assignment/$'.format( + regex=SUBMISSIONS_COMPLETE_REGEX), views.editorial_assignment, + name='editorial_assignment'), + url(r'^pool/{regex}/editorial_assignment/(?P<assignment_id>[0-9]+)/$'.format( + regex=SUBMISSIONS_COMPLETE_REGEX), views.editorial_assignment, + name='editorial_assignment'), url(r'^volunteer_as_EIC/{regex}$'.format(regex=SUBMISSIONS_COMPLETE_REGEX), views.volunteer_as_EIC, name='volunteer_as_EIC'), url(r'^assignment_failed/{regex}$'.format(regex=SUBMISSIONS_COMPLETE_REGEX), @@ -122,7 +132,4 @@ urlpatterns = [ url(r'^vote_on_rec/(?P<rec_id>[0-9]+)$', views.vote_on_rec, name='vote_on_rec'), url(r'^remind_Fellows_to_vote$', views.remind_Fellows_to_vote, name='remind_Fellows_to_vote'), - # Editorial Administration - url(r'fix_College_decision/(?P<rec_id>[0-9]+)$', views.fix_College_decision, - name='fix_College_decision'), ] diff --git a/submissions/utils.py b/submissions/utils.py index 5098fbc6b3e7dc5dad7a6b5315bd1bcf9439b55e..987fccf16b9bde6417803f405895d7ed8f7b052a 100644 --- a/submissions/utils.py +++ b/submissions/utils.py @@ -9,9 +9,8 @@ from django.template import Context, Template from django.utils import timezone 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) + NO_REQUIRED_ACTION_STATUSES, STATUS_VETTED, STATUS_UNCLEAR, STATUS_INCORRECT, STATUS_INCOMING, + STATUS_NOT_USEFUL, STATUS_NOT_ACADEMIC, STATUS_EIC_ASSIGNED) from scipost.utils import EMAIL_FOOTER from common.utils import BaseMailUtil @@ -40,37 +39,37 @@ class BaseSubmissionCycle: 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. - """ + """Create the list of actions for the current submission.""" self.required_actions = [] if self.submission.status in NO_REQUIRED_ACTION_STATUSES: - '''Submission does not appear in the pool, no action required.''' + # 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''' + if self.submission.revision_requested: + # Editor-in-charge has requested revision. return False - if self.submission.eicrecommendations.exists(): - '''A Editorial Recommendation has already been submitted. Cycle done.''' + if not self.submission.plagiarism_report: + # No plagiarism report is known yet. + self.required_actions.append(( + 'plagiarism_report', + 'No plagiarism report found. Please run the plagiarism check.')) + + if self.submission.eicrecommendations.active().exists(): + # An Editorial Recommendation has already been submitted. Cycle done. return False - if self.submission.status == STATUS_RESUBMISSION_INCOMING: - """ - 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.',)) + if not self.submission.refereeing_cycle: + # Submission is a resubmission: EIC 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.''' + # There are comments on the submission awaiting vetting. if comments_to_vet > 1: - text = '%i Comment\'s have' % comments_to_vet + text = '%i Comments have' % comments_to_vet else: text = 'One Comment has' text += ' been delivered but is not yet vetted. Please vet it.' @@ -78,10 +77,8 @@ class BaseSubmissionCycle: 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. - """ + # 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 @@ -89,14 +86,13 @@ class BaseSubmissionCycle: reports_awaiting_vetting = self.submission.reports.awaiting_vetting().count() if reports_awaiting_vetting > 0: - '''There are reports on the submission awaiting vetting.''' + # There are reports on the submission awaiting vetting. if reports_awaiting_vetting > 1: text = '%i Reports have' % reports_awaiting_vetting else: text = 'One Report has' text += ' been delivered but is not yet vetted. Please vet it.' self.required_actions.append(('vet_reports', text,)) - return True def reinvite_referees(self, referees, request=None): @@ -158,7 +154,7 @@ class BaseRefereeSubmissionCycle(BaseSubmissionCycle): that require referees to be invited. """ def update_status(self): - if self.submission.status == STATUS_RESUBMISSION_INCOMING: + if self.submission.status == STATUS_INCOMING and self.submission.is_resubmission: from .models import Submission Submission.objects.filter(id=self.submission.id).update(status=STATUS_EIC_ASSIGNED) @@ -235,9 +231,10 @@ class DirectRecommendationSubmissionCycle(BaseSubmissionCycle): minimum_referees = 0 def update_status(self): - if self.submission.status == STATUS_RESUBMISSION_INCOMING: + if self.submission.status == STATUS_INCOMING and self.submission.is_resubmission: from .models import Submission - Submission.objects.filter(id=self.submission.id).update(status=STATUS_AWAITING_ED_REC) + Submission.objects.filter(id=self.submission.id).update(status=STATUS_EIC_ASSIGNED) + # TODO: Generate draft-EICRecommendation. def _update_actions(self): continue_update = super()._update_actions() @@ -255,18 +252,6 @@ class SubmissionUtils(BaseMailUtil): mail_sender = 'submissions@scipost.org' mail_sender_title = 'SciPost Editorial Admin' - @classmethod - def deprecate_other_assignments(cls): - """ - Called when a Fellow has accepted or volunteered to become EIC. - """ - # Import here due to circular import error - from .models import EditorialAssignment - - EditorialAssignment.objects.filter(submission=cls.assignment.submission, accepted=None)\ - .exclude(to=cls.assignment.to)\ - .update(deprecated=True) - @classmethod def deprecate_all_assignments(cls): """ @@ -430,6 +415,7 @@ class SubmissionUtils(BaseMailUtil): @classmethod def send_EIC_appointment_email(cls): """ Requires loading 'assignment' attribute. """ + r = cls.assignment email_text = ('Dear ' + cls.assignment.to.get_title_display() + ' ' + cls.assignment.to.user.last_name + ', \n\nThank you for accepting to become Editor-in-charge ' diff --git a/submissions/views.py b/submissions/views.py index aa1258d74b556832418179048ed338e77729318b..f1f6021107ff9e7bf8c47299259b104d4194995c 100644 --- a/submissions/views.py +++ b/submissions/views.py @@ -8,7 +8,6 @@ import strings from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required -from django.contrib.auth.models import Group from django.core.exceptions import PermissionDenied from django.core.urlresolvers import reverse, reverse_lazy from django.db import transaction, IntegrityError @@ -18,27 +17,24 @@ from django.template import Template, Context from django.utils import timezone from django.utils.decorators import method_decorator from django.views.generic.base import RedirectView -from django.views.generic.detail import DetailView, SingleObjectMixin +from django.views.generic.detail import SingleObjectMixin from django.views.generic.edit import CreateView, UpdateView from django.views.generic.list import ListView -from guardian.shortcuts import assign_perm - -from .constants import STATUS_VETTED, STATUS_EIC_ASSIGNED,\ - SUBMISSION_STATUS_PUBLICLY_INVISIBLE, SUBMISSION_STATUS, STATUS_ASSIGNMENT_FAILED,\ - STATUS_DRAFT, CYCLE_DIRECT_REC, STATUS_VOTING_IN_PREPARATION,\ - STATUS_PUT_TO_EC_VOTING -from .models import Submission, EICRecommendation, EditorialAssignment,\ - RefereeInvitation, Report, SubmissionEvent +from .constants import ( + STATUS_VETTED, STATUS_EIC_ASSIGNED, SUBMISSION_STATUS, STATUS_ASSIGNMENT_FAILED, + STATUS_DRAFT, CYCLE_DIRECT_REC) +from .helpers import check_verified_author, check_unverified_author +from .models import ( + Submission, EICRecommendation, EditorialAssignment, RefereeInvitation, Report, SubmissionEvent) from .mixins import SubmissionAdminViewMixin -from .forms import SubmissionIdentifierForm, RequestSubmissionForm, SubmissionSearchForm,\ - RecommendationVoteForm, ConsiderAssignmentForm, EditorialAssignmentForm,\ - SetRefereeingDeadlineForm, RefereeSelectForm, RefereeRecruitmentForm,\ - ConsiderRefereeInvitationForm, EditorialCommunicationForm,\ - EICRecommendationForm, ReportForm, VetReportForm, VotingEligibilityForm,\ - SubmissionCycleChoiceForm, ReportPDFForm, SubmissionReportsForm,\ - iThenticateReportForm, SubmissionPoolFilterForm -from .signals import notify_manuscript_accepted +from .forms import ( + SubmissionIdentifierForm, RequestSubmissionForm, SubmissionSearchForm, RecommendationVoteForm, + ConsiderAssignmentForm, InviteEditorialAssignmentForm, EditorialAssignmentForm, VetReportForm, + SetRefereeingDeadlineForm, RefereeSelectForm, iThenticateReportForm, VotingEligibilityForm, + RefereeRecruitmentForm, ConsiderRefereeInvitationForm, EditorialCommunicationForm, ReportForm, + SubmissionCycleChoiceForm, ReportPDFForm, SubmissionReportsForm, EICRecommendationForm, + SubmissionPoolFilterForm, FixCollegeDecisionForm, SubmissionPrescreeningForm) from .utils import SubmissionUtils from colleges.permissions import fellowship_required, fellowship_or_admin_required @@ -46,8 +42,7 @@ from comments.forms import CommentForm from journals.models import Journal from mails.views import MailEditingSubView from production.forms import ProofsDecisionForm -from production.models import ProductionStream -from scipost.forms import ModifyPersonalMessageForm, RemarkForm +from scipost.forms import RemarkForm from scipost.mixins import PaginationMixin from scipost.models import Contributor, Remark @@ -60,20 +55,25 @@ from scipost.models import Contributor, Remark @method_decorator(permission_required('scipost.can_submit_manuscript', raise_exception=True), name='dispatch') class RequestSubmission(CreateView): + """Formview to submit a new manuscript (Submission).""" + success_url = reverse_lazy('scipost:personal_page') form_class = RequestSubmissionForm template_name = 'submissions/submission_form.html' def get(self, request): + """Redirect to the arXiv prefill form if arXiv ID is not known.""" return redirect('submissions:prefill_using_identifier') def get_form_kwargs(self): + """Form requires extra kwargs.""" kwargs = super().get_form_kwargs() kwargs['requested_by'] = self.request.user return kwargs @transaction.atomic def form_valid(self, form): + """Redirect and send out mails if all data is valid.""" submission = form.save() submission.add_general_event('The manuscript has been submitted to %s.' % submission.get_submitted_to_journal_display()) @@ -94,6 +94,7 @@ class RequestSubmission(CreateView): return HttpResponseRedirect(self.success_url) def form_invalid(self, form): + """Add warnings as messages to make those more explicit.""" for error_messages in form.errors.values(): messages.warning(self.request, *error_messages) return super().form_invalid(form) @@ -102,6 +103,7 @@ class RequestSubmission(CreateView): @login_required @permission_required('scipost.can_submit_manuscript', raise_exception=True) def prefill_using_arxiv_identifier(request): + """Form view asking for the arXiv ID related to the new Submission to submit.""" query_form = SubmissionIdentifierForm(request.POST or None, initial=request.GET or None, requested_by=request.user) if query_form.is_valid(): @@ -129,12 +131,15 @@ def prefill_using_arxiv_identifier(request): class SubmissionListView(PaginationMixin, ListView): + """List all publicly available Submissions.""" + model = Submission form = SubmissionSearchForm submission_search_list = [] paginate_by = 10 def get_queryset(self): + """Return queryset, filtered with GET request data if given.""" queryset = Submission.objects.public_newest() self.form = self.form(self.request.GET) if 'to_journal' in self.request.GET: @@ -155,7 +160,7 @@ class SubmissionListView(PaginationMixin, ListView): return queryset.order_by('-submission_date') def get_context_data(self, **kwargs): - # Call the base implementation first to get a context + """Save data related to GET request if found.""" context = super().get_context_data(**kwargs) # Form into the context! @@ -179,52 +184,63 @@ class SubmissionListView(PaginationMixin, ListView): def submission_detail_wo_vn_nr(request, arxiv_identifier_wo_vn_nr): + """Redirect to the latest Submission's detail page.""" submission = get_object_or_404(Submission, arxiv_identifier_wo_vn_nr=arxiv_identifier_wo_vn_nr, is_current=True) - return(submission_detail(request, submission.arxiv_identifier_w_vn_nr)) + return submission_detail(request, submission.arxiv_identifier_w_vn_nr) def submission_detail(request, arxiv_identifier_w_vn_nr): + """Public detail page of Submission.""" submission = get_object_or_404(Submission, arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) - context = {} - try: - is_author = request.user.contributor in submission.authors.all() - 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 + context = { + 'can_read_editorial_information': False + } + + # Check if Contributor is author of the Submission + is_author = check_verified_author(submission, request.user) + is_author_unchecked = check_unverified_author(submission, request.user) + + if not submission.visible_public and not is_author: + if not request.user.is_authenticated: + raise Http404 + elif not request.user.has_perm( + 'scipost.can_assign_submissions') and not submission.fellows.filter( + contributor__user=request.user).exists(): + raise Http404 + if is_author: context['proofs_decision_form'] = ProofsDecisionForm() - 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', - 'Editorial College']).exists() - and not is_author): - raise Http404 - form = CommentForm() + if submission.open_for_commenting and request.user.has_perms('scipost.can_submit_comments'): + context['comment_form'] = CommentForm() - 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')) + invited_reports = submission.reports.accepted().invited() + contributed_reports = submission.reports.accepted().contributed() + comments = submission.comments.vetted().regular_comments().order_by('-date_submitted') + author_replies = submission.comments.vetted().author_replies().order_by('-date_submitted') # User is referee for the Submission if request.user.is_authenticated: - invitations = submission.referee_invitations.filter(referee__user=request.user) - else: - invitations = None - if invitations: + context['unfinished_report_for_user'] = submission.reports.in_draft().filter( + author__user=request.user).first() + context['invitations'] = submission.referee_invitations.filter(referee__user=request.user) + + if is_author or is_author_unchecked: + # Authors are not allowed to read all editorial info! Whatever + # their permission level is. + context['can_read_editorial_information'] = False + else: + # User may read eg. Editorial Recommendations if he/she is in the Pool. + context['can_read_editorial_information'] = submission.fellows.filter( + contributor__user=request.user).exists() + + # User may also read eg. Editorial Recommendations if he/she is editorial administrator. + if not context['can_read_editorial_information']: + context['can_read_editorial_information'] = request.user.has_perm( + 'can_oversee_refereeing') + + if 'invitations' in context and context['invitations']: context['communication'] = submission.editorial_communications.for_referees().filter( referee__user=request.user) @@ -236,20 +252,28 @@ 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, 'is_author_unchecked': is_author_unchecked, - 'invitations': invitations, }) return render(request, 'submissions/submission_detail.html', context) +def report_attachment(request, arxiv_identifier_w_vn_nr, report_nr): + """Download the attachment of a Report if available.""" + report = get_object_or_404(Report.objects.accepted(), + submission__arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr, + file_attachment__isnull=False, report_nr=report_nr) + response = HttpResponse(report.file_attachment.read(), content_type='application/pdf') + filename = '{}_report_attachment-{}.pdf'.format( + report.submission.arxiv_identifier_w_vn_nr, + report.report_nr) + response['Content-Disposition'] = ('filename=' + filename) + return response + + def report_detail_pdf(request, arxiv_identifier_w_vn_nr, report_nr): - """ - Download the PDF of a Report if available. - """ + """Download the PDF of a Report if available.""" report = get_object_or_404(Report.objects.accepted(), submission__arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr, pdf_report__isnull=False, report_nr=report_nr) @@ -260,7 +284,8 @@ def report_detail_pdf(request, arxiv_identifier_w_vn_nr, report_nr): def submission_refereeing_package_pdf(request, arxiv_identifier_w_vn_nr): - """ + """Down the refereeing package PDF. + This view let's the user download all Report PDF's in a single merged PDF. The merging takes places every time its downloaded to make sure all available report PDF's are included and the EdColAdmin doesn't have to compile the package every time again. @@ -275,12 +300,12 @@ def submission_refereeing_package_pdf(request, arxiv_identifier_w_vn_nr): @permission_required('scipost.can_manage_reports', raise_exception=True) def reports_accepted_list(request): + """List all accepted Reports. + + This gives an overview of Report that need a PDF update/compilation. """ - This view lists all accepted Reports. This shows if Report needs a PDF update/compile - in a convenient way. - """ - reports = (Report.objects.accepted() - .order_by('pdf_report', 'submission').prefetch_related('submission')) + reports = Report.objects.accepted().order_by( + 'pdf_report', 'submission').prefetch_related('submission') if request.GET.get('submission'): reports = reports.filter(submission__arxiv_identifier_w_vn_nr=request.GET.get('submission')) @@ -292,6 +317,7 @@ def reports_accepted_list(request): @permission_required('scipost.can_manage_reports', raise_exception=True) def report_pdf_compile(request, report_id): + """Form view to receive a auto-generated LaTeX code and submit a pdf version of the Report.""" report = get_object_or_404(Report.objects.accepted(), id=report_id) form = ReportPDFForm(request.POST or None, request.FILES or None, instance=report) if form.is_valid(): @@ -307,11 +333,12 @@ def report_pdf_compile(request, report_id): @permission_required('scipost.can_manage_reports', raise_exception=True) def treated_submissions_list(request): + """List all treated Submissions. + + This gives an overview of Submissions that need a PDF update/compilation of their Reports. """ - This view lists all accepted Reports. This shows if Report needs a PDF update/compile - in a convenient way. - """ - submissions = Submission.objects.treated().order_by('pdf_refereeing_pack', '-acceptance_date') + submissions = Submission.objects.treated().public().order_by( + 'pdf_refereeing_pack', '-acceptance_date') context = { 'submissions': submissions } @@ -320,6 +347,7 @@ def treated_submissions_list(request): @permission_required('scipost.can_manage_reports', raise_exception=True) def treated_submission_pdf_compile(request, arxiv_identifier_w_vn_nr): + """Form view to receive a auto-generated LaTeX code and submit a pdf version of the Reports.""" submission = get_object_or_404(Submission.objects.treated(), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) form = SubmissionReportsForm(request.POST or None, request.FILES or None, instance=submission) @@ -341,7 +369,8 @@ def treated_submission_pdf_compile(request, arxiv_identifier_w_vn_nr): @login_required @fellowship_or_admin_required() def editorial_workflow(request): - """ + """Information page for Editorial Fellows. + Summary page for Editorial Fellows, containing a digest of the actions to take to handle Submissions. """ @@ -351,7 +380,8 @@ def editorial_workflow(request): @login_required @fellowship_or_admin_required() def pool(request, arxiv_identifier_w_vn_nr=None): - """ + """List page of Submissions in refereeing. + The Submissions pool contains all submissions which are undergoing the editorial process, from submission to publication acceptance or rejection. @@ -365,8 +395,8 @@ def pool(request, arxiv_identifier_w_vn_nr=None): # Mainly as fallback for the old-pool while in test phase. submissions = Submission.objects.pool(request.user) - recommendations = EICRecommendation.objects.active().filter(submission__in=submissions) - recs_to_vote_on = recommendations.user_may_vote_on(request.user) + recs_to_vote_on = EICRecommendation.objects.user_may_vote_on(request.user).filter( + submission__in=submissions) assignments_to_consider = EditorialAssignment.objects.open().filter( to=request.user.contributor) @@ -379,7 +409,6 @@ def pool(request, arxiv_identifier_w_vn_nr=None): 'submissions': submissions.order_by('status', '-submission_date'), 'search_form': search_form, 'submission_status': SUBMISSION_STATUS, - 'recommendations': recommendations, 'assignments_to_consider': assignments_to_consider, 'consider_assignment_form': consider_assignment_form, 'recs_to_vote_on': recs_to_vote_on, @@ -391,16 +420,21 @@ def pool(request, arxiv_identifier_w_vn_nr=None): context['submission'] = None if arxiv_identifier_w_vn_nr: try: - context['submission'] = Submission.objects.pool_full(request.user).get( + context['submission'] = Submission.objects.pool_editable(request.user).get( arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) except Submission.DoesNotExist: pass # EdColAdmin related variables if request.user.has_perm('scipost.can_oversee_refereeing'): + context['recommendations'] = EICRecommendation.objects.active().filter( + submission__in=submissions) context['latest_submission_events'] = SubmissionEvent.objects.for_eic().last_hours()\ .filter(submission__in=context['submissions']) + if request.user.has_perm('scipost.can_run_pre_screening'): + context['pre_screening_subs'] = Submission.objects.prescreening() + # Pool gets Submission details via ajax request if context['submission'] and request.is_ajax(): template = 'partials/submissions/pool/submission_details.html' @@ -412,7 +446,8 @@ def pool(request, arxiv_identifier_w_vn_nr=None): @login_required @fellowship_or_admin_required() def add_remark(request, arxiv_identifier_w_vn_nr): - """ + """Form view to add a Remark to a Submission. + With this method, an Editorial Fellow or Board Member is adding a remark on a Submission. """ @@ -441,7 +476,7 @@ def assign_submission(request, arxiv_identifier_w_vn_nr): """ submission = get_object_or_404(Submission.objects.pool_editable(request.user), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) - form = EditorialAssignmentForm(request.POST or None, submission=submission) + form = InviteEditorialAssignmentForm(request.POST or None, submission=submission) if form.is_valid(): ed_assignment = form.save() @@ -464,109 +499,122 @@ def assign_submission(request, arxiv_identifier_w_vn_nr): @login_required @fellowship_required() @transaction.atomic -def assignment_request(request, assignment_id): - """Process EditorialAssignment acceptance/rejection form or show if not submitted.""" - assignment = get_object_or_404(EditorialAssignment.objects.open(), - to=request.user.contributor, pk=assignment_id) - - errormessage = None - if assignment.submission.status == 'assignment_failed': - errormessage = 'This Submission has failed pre-screening and has been rejected.' +def editorial_assignment(request, arxiv_identifier_w_vn_nr, assignment_id=None): + """Editorial Assignment form view.""" + submission = get_object_or_404(Submission.objects.pool_editable(request.user), + arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) - elif assignment.submission.editor_in_charge: - errormessage = (assignment.submission.editor_in_charge.get_title_display() + ' ' + - assignment.submission.editor_in_charge.user.last_name + - ' has already agreed to be Editor-in-charge of this Submission.') + # Check if Submission is still valid for a new assignment. + if submission.editor_in_charge: + messages.success( + request, '{} {} has already agreed to be Editor-in-charge of this Submission.'.format( + submission.editor_in_charge.get_title_display(), + submission.editor_in_charge.user.last_name)) + return redirect('submissions:pool') + elif submission.status == STATUS_ASSIGNMENT_FAILED: + messages.success( + request, ('Thank you for considering.' + ' This Submission has failed pre-screening and has been rejected.')) + return redirect('submissions:pool') - if errormessage: - # Assignments can get stuck here, - # if errormessage is given the contributor can't close the assignment!! - messages.warning(request, errormessage) - return redirect(reverse('submissions:pool')) + if assignment_id: + # Process existing EditorialAssignment. + assignment = get_object_or_404( + submission.editorial_assignments.open(), to=request.user.contributor, pk=assignment_id) + else: + # Get or create EditorialAssignment for user. + try: + assignment = submission.editorial_assignments.open().filter( + to__user=request.user).first() + except EditorialAssignment.DoesNotExist: + assignment = EditorialAssignment() - form = ConsiderAssignmentForm(request.POST or None) + form = EditorialAssignmentForm( + request.POST or None, submission=submission, instance=assignment, request=request) if form.is_valid(): - assignment.date_answered = timezone.now() - if form.cleaned_data['accept'] == 'True': - assignment.accepted = True - assignment.to = request.user.contributor - assignment.submission.status = STATUS_EIC_ASSIGNED - assignment.submission.editor_in_charge = request.user.contributor - assignment.submission.open_for_reporting = True - deadline = timezone.now() + datetime.timedelta(days=28) # for papers - if assignment.submission.submitted_to_journal == 'SciPostPhysLectNotes': - deadline += datetime.timedelta(days=28) - assignment.submission.reporting_deadline = deadline - assignment.submission.open_for_commenting = True - assignment.submission.latest_activity = timezone.now() - # Save assignment and submission - assignment.save() - assignment.submission.save() - + assignment = form.save() + if form.has_accepted_invite(): + # Fellow accepted to do a normal refereeing cycle. SubmissionUtils.load({'assignment': assignment}) - SubmissionUtils.deprecate_other_assignments() SubmissionUtils.send_EIC_appointment_email() - SubmissionUtils.send_author_prescreening_passed_email() - # Add SubmissionEvents - assignment.submission.add_general_event('The Editor-in-charge has been assigned.') + if form.is_normal_cycle(): + # Inform authors about new status. + SubmissionUtils.send_author_prescreening_passed_email() + + submission.add_general_event('The Editor-in-charge has been assigned.') msg = 'Thank you for becoming Editor-in-charge of this submission.' - url = reverse('submissions:editorial_page', - args=(assignment.submission.arxiv_identifier_w_vn_nr,)) + url = reverse('submissions:editorial_page', args=(submission.arxiv_identifier_w_vn_nr,)) else: - assignment.accepted = False - assignment.refusal_reason = form.cleaned_data['refusal_reason'] - assignment.submission.status = 'unassigned' - - # Save assignment and submission - assignment.save() - assignment.submission.save() + # Fellow declined the invitation. msg = 'Thank you for considering' url = reverse('submissions:pool') - # Form submitted, redirect user + # Form submitted; redirect user messages.success(request, msg) return redirect(url) + return redirect('submissions:pool') + context = { + 'form': form, + 'submission': submission, 'assignment': assignment, - 'form': form } - return render(request, 'submissions/pool/assignment_request.html', context) + return render(request, 'submissions/pool/editorial_assignment.html', context) + + +@login_required +@fellowship_required() +def assignment_request(request, assignment_id): + """Redirect to Editorial Assignment form view. + + Exists for historical reasons; email are send with this url construction. + """ + assignment = get_object_or_404(EditorialAssignment.objects.open(), + to=request.user.contributor, pk=assignment_id) + return redirect(reverse('submissions:editorial_assignment', kwargs={ + 'arxiv_identifier_w_vn_nr': assignment.submission.arxiv_identifier_w_vn_nr, + 'assignment_id': assignment.id + })) @login_required @fellowship_required() @transaction.atomic def volunteer_as_EIC(request, arxiv_identifier_w_vn_nr): - """ + """Single click action to take charge of a Submission. + Called when a Fellow volunteers while perusing the submissions pool. This is an adapted version of the assignment_request method. """ submission = get_object_or_404(Submission.objects.pool(request.user), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) errormessage = None - if submission.status == 'assignment_failed': + if submission.status == STATUS_ASSIGNMENT_FAILED: errormessage = '<h3>Thank you for considering.</h3>' errormessage += 'This Submission has failed pre-screening and has been rejected.' - messages.warning(request, errormessage) - return redirect(reverse('submissions:pool')) - if submission.editor_in_charge: + elif submission.editor_in_charge: errormessage = '<h3>Thank you for considering.</h3>' errormessage += (submission.editor_in_charge.get_title_display() + ' ' + submission.editor_in_charge.user.last_name + ' has already agreed to be Editor-in-charge of this Submission.') + elif not hasattr(request.user, 'contributor'): + errormessage = ( + 'You do not have an activated Contributor account. Therefore, you cannot take charge.') + + if errormessage: messages.warning(request, errormessage) return redirect(reverse('submissions:pool')) - contributor = Contributor.objects.get(user=request.user) + contributor = request.user.contributor # The Contributor may already have an EditorialAssignment due to an earlier invitation. assignment, __ = EditorialAssignment.objects.get_or_create( submission=submission, to=contributor) - assignment.accepted = True - assignment.date_answered = timezone.now() - assignment.save() + # Explicitly update afterwards, since update_or_create does not properly do the job. + EditorialAssignment.objects.filter(id=assignment.id).update( + accepted=True, date_answered=timezone.now()) # Set deadlines deadline = timezone.now() + datetime.timedelta(days=28) # for papers @@ -587,7 +635,6 @@ def volunteer_as_EIC(request, arxiv_identifier_w_vn_nr): # Send emails to EIC and authors regarding the EIC assignment. SubmissionUtils.load({'assignment': assignment}) - SubmissionUtils.deprecate_other_assignments() SubmissionUtils.send_EIC_appointment_email() SubmissionUtils.send_author_prescreening_passed_email() @@ -608,7 +655,7 @@ def assignment_failed(request, arxiv_identifier_w_vn_nr): No Editorial Fellow has accepted or volunteered to become Editor-in-charge., hence the Submission is rejected. An Editorial Administrator can access this view from the Pool. """ - submission = get_object_or_404(Submission.objects.pool(request.user).prescreening(), + submission = get_object_or_404(Submission.objects.pool(request.user).unassigned(), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) mail_request = MailEditingSubView( @@ -620,7 +667,8 @@ def assignment_failed(request, arxiv_identifier_w_vn_nr): # Update status of Submission submission.touch() - Submission.objects.filter(id=submission.id).update(status=STATUS_ASSIGNMENT_FAILED) + Submission.objects.filter(id=submission.id).update( + status=STATUS_ASSIGNMENT_FAILED, visible_pool=False, visible_public=False) messages.success( request, 'Submission {arxiv} has failed pre-screening and been rejected.'.format( @@ -642,11 +690,9 @@ def assignments(request): """ assignments = EditorialAssignment.objects.filter( to=request.user.contributor).order_by('-date_created') - assignments_to_consider = assignments.filter(accepted=None, - deprecated=False) - current_assignments = assignments.filter(accepted=True, - deprecated=False, - completed=False) + assignments_to_consider = assignments.open() + current_assignments = assignments.ongoing() + context = { 'assignments_to_consider': assignments_to_consider, 'current_assignments': current_assignments, @@ -662,7 +708,7 @@ def editorial_page(request, arxiv_identifier_w_vn_nr): The central page for the Editor-in-charge to manage all its Editorial duties. It's accessible for both the Editor-in-charge of the Submission and the Editorial Administration. """ - submission = get_object_or_404(Submission.objects.pool_full(request.user), + submission = get_object_or_404(Submission.objects.pool_editable(request.user), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) full_access = True @@ -686,7 +732,8 @@ def editorial_page(request, arxiv_identifier_w_vn_nr): @login_required @fellowship_or_admin_required() def cycle_form_submit(request, arxiv_identifier_w_vn_nr): - """ + """Form view to choose refereeing cycle. + If Submission is `resubmission_incoming` the EIC should first choose what refereeing cycle to choose. @@ -806,7 +853,8 @@ def recruit_referee(request, arxiv_identifier_w_vn_nr): @fellowship_or_admin_required() @transaction.atomic def send_refereeing_invitation(request, arxiv_identifier_w_vn_nr, contributor_id): - """ + """Send RefereeInvitation to a registered Contributor. + This method is called by the EIC from the submission's editorial_page, in the case where the referee is an identified Contributor. For a referee who isn't a Contributor yet, the method recruit_referee above @@ -822,16 +870,18 @@ def send_refereeing_invitation(request, arxiv_identifier_w_vn_nr, contributor_id errormessage = ('This Contributor is marked as currently unavailable. ' 'Please go back and select another referee.') return render(request, 'scipost/error.html', {'errormessage': errormessage}) - invitation = RefereeInvitation(submission=submission, - referee=contributor, - title=contributor.title, - first_name=contributor.user.first_name, - last_name=contributor.user.last_name, - email_address=contributor.user.email, - # the key is only used for inviting unregistered users - invitation_key='notused', - date_invited=timezone.now(), - invited_by=request.user.contributor) + + invitation = RefereeInvitation( + submission=submission, + referee=contributor, + title=contributor.title, + first_name=contributor.user.first_name, + last_name=contributor.user.last_name, + email_address=contributor.user.email, + # the key is only used for inviting unregistered users + invitation_key='notused', + date_invited=timezone.now(), + invited_by=request.user.contributor) mail_request = MailEditingSubView(request, mail_code='referees/invite_contributor_to_referee', invitation=invitation) @@ -850,7 +900,8 @@ def send_refereeing_invitation(request, arxiv_identifier_w_vn_nr, contributor_id @login_required @fellowship_or_admin_required() def ref_invitation_reminder(request, arxiv_identifier_w_vn_nr, invitation_id): - """ + """Send reminder email to pending RefereeInvitations. + This method is used by the Editor-in-charge from the editorial_page when a referee has been invited but hasn't answered yet. It can be used for registered as well as unregistered referees. @@ -876,7 +927,8 @@ def ref_invitation_reminder(request, arxiv_identifier_w_vn_nr, invitation_id): @login_required @permission_required('scipost.can_referee', raise_exception=True) def accept_or_decline_ref_invitations(request, invitation_id=None): - """ + """Decide on RefereeInvitation. + RefereeInvitations need to be either accepted or declined by the invited user using this view. The decision will be taken one invitation at a time. """ @@ -935,6 +987,7 @@ def accept_or_decline_ref_invitations(request, invitation_id=None): def decline_ref_invitation(request, invitation_key): + """Decline a RefereeInvitation.""" invitation = get_object_or_404(RefereeInvitation, invitation_key=invitation_key, accepted__isnull=True) @@ -967,12 +1020,12 @@ def decline_ref_invitation(request, invitation_key): @login_required def cancel_ref_invitation(request, arxiv_identifier_w_vn_nr, invitation_id): - """ - This method is used by the Editor-in-charge from the editorial_page - to remove a referee for the list of invited ones. - It can be used for registered as well as unregistered referees. + """Cancel a RefereeInvitation. - Accessible for: Editor-in-charge and Editorial Administration + This method is used by the Editor-in-charge from the editorial_page to remove a referee + from the list of invited ones. It can be used for registered as well as unregistered referees. + + Accessible for: Editor-in-charge and Editorial Administration. """ try: submissions = Submission.objects.filter_for_eic(request.user) @@ -1007,7 +1060,7 @@ def extend_refereeing_deadline(request, arxiv_identifier_w_vn_nr, days): submission.reporting_deadline += datetime.timedelta(days=int(days)) submission.open_for_reporting = True submission.open_for_commenting = True - submission.status = 'EICassigned' + submission.status = STATUS_EIC_ASSIGNED submission.latest_activity = timezone.now() submission.save() @@ -1018,9 +1071,7 @@ def extend_refereeing_deadline(request, arxiv_identifier_w_vn_nr, days): @login_required def set_refereeing_deadline(request, arxiv_identifier_w_vn_nr): - """ - Set Refereeing deadline for Submission and open reporting and commenting if - the new date is in the future. + """Set Refereeing deadline for Submission and open reporting and commenting. Accessible for: Editor-in-charge and Editorial Administration """ @@ -1033,8 +1084,8 @@ def set_refereeing_deadline(request, arxiv_identifier_w_vn_nr): if form.cleaned_data['deadline'] > timezone.now().date(): submission.open_for_reporting = True submission.open_for_commenting = True - submission.status = 'EICassigned' - submission.latest_activity = timezone.now() + submission.latest_activity = timezone.now() + # submission.status = STATUS_EIC_ASSIGNED # This is dangerous as shit. submission.save() submission.add_general_event('A new refereeing deadline is set.') messages.success(request, 'New reporting deadline set.') @@ -1047,26 +1098,24 @@ def set_refereeing_deadline(request, arxiv_identifier_w_vn_nr): @login_required def close_refereeing_round(request, arxiv_identifier_w_vn_nr): - """ - Called by the Editor-in-charge when a satisfactory number of - reports have been gathered. - Automatically emails the authors to ask them if they want to - round off any replies to reports or comments before the - editorial recommendation is formulated. + """Close Submission for refereeing. - Accessible for: Editor-in-charge and Editorial Administration + Called by the Editor-in-charge when a satisfactory number of reports have been gathered. + Automatically emails the authors to ask them if they want to round off any replies to + reports or comments before the editorial recommendation is formulated. + + Accessible for: Editor-in-charge and Editorial Administration. """ submission = get_object_or_404(Submission.objects.filter_for_eic(request.user), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) - submission.open_for_reporting = False - submission.open_for_commenting = False - if submission.status == 'EICassigned': # only close if currently undergoing refereeing - submission.status = 'review_closed' - submission.reporting_deadline = timezone.now() - submission.latest_activity = timezone.now() - submission.save() + Submission.objects.filter(id=submission.id).update( + open_for_reporting=False, + open_for_commenting=False, + reporting_deadline=timezone.now(), + latest_activity=timezone.now()) submission.add_general_event('Refereeing round has been closed.') + messages.success(request, 'Refereeing round closed.') return redirect(reverse('submissions:editorial_page', kwargs={'arxiv_identifier_w_vn_nr': arxiv_identifier_w_vn_nr})) @@ -1074,19 +1123,18 @@ 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 - .pool_editable(request.user) - .filter(status=STATUS_EIC_ASSIGNED) - .order_by('submission_date')) + """List all Submissions undergoing active Refereeing.""" + submissions_under_refereeing = Submission.objects.pool_editable( + request.user).actively_refereeing().order_by('submission_date') context = {'submissions_under_refereeing': submissions_under_refereeing} return render(request, 'submissions/admin/refereeing_overview.html', context) @login_required def communication(request, arxiv_identifier_w_vn_nr, comtype, referee_id=None): - """ - Communication between editor-in-charge, author or referee - occurring during the submission refereeing. + """Send refereeing related communication. + + Communication may be between two of: editor-in-charge, author and referee. """ referee = None if comtype in ['EtoA', 'EtoR', 'EtoS']: @@ -1157,8 +1205,7 @@ def communication(request, arxiv_identifier_w_vn_nr, comtype, referee_id=None): @fellowship_or_admin_required() @transaction.atomic def eic_recommendation(request, arxiv_identifier_w_vn_nr): - """ - Write EIC Recommendation. + """Write EIC Recommendation. Accessible for: Editor-in-charge and Editorial Administration """ @@ -1207,22 +1254,23 @@ def eic_recommendation(request, arxiv_identifier_w_vn_nr): @fellowship_or_admin_required() @transaction.atomic def reformulate_eic_recommendation(request, arxiv_identifier_w_vn_nr): - """ - Reformulate EIC Recommendation. + """Reformulate EIC Recommendation form view. - Accessible for: Editor-in-charge and Editorial Administration + Accessible for: Editor-in-charge and Editorial Administration. """ submission = get_object_or_404(Submission.objects.filter_for_eic(request.user), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) + recommendation = submission.eicrecommendations.first() + if not recommendation: + raise Http404('No EICRecommendation formulated yet.') - if submission.status not in [STATUS_EIC_ASSIGNED, STATUS_VOTING_IN_PREPARATION, STATUS_PUT_TO_EC_VOTING]: - messages.warning(request, ('With the current status of the Submission you are not ' + if not recommendation.may_be_reformulated: + messages.warning(request, ('With the current status of the EICRecommendation you are not ' 'allowed to reformulate the Editorial Recommendation')) return redirect(reverse('submissions:editorial_page', args=[submission.arxiv_identifier_w_vn_nr])) form = EICRecommendationForm(request.POST or None, submission=submission, reformulate=True) - if form.is_valid(): recommendation = form.save() if form.revision_requested(): @@ -1251,25 +1299,29 @@ def reformulate_eic_recommendation(request, arxiv_identifier_w_vn_nr): @permission_required('scipost.can_referee', raise_exception=True) @transaction.atomic def submit_report(request, arxiv_identifier_w_vn_nr): - """ - A form to submit a report on a submission will be shown and processed here. + """Submit Report on a Submission. 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) + submission = get_object_or_404(Submission, arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) + # Check whether the user can submit a report: - current_contributor = request.user.contributor - is_author = current_contributor in submission.authors.all() - is_author_unchecked = (not is_author and not - (current_contributor in submission.authors_false_claims.all()) and - (request.user.last_name in submission.author_list)) - invitation = submission.referee_invitations.filter(referee=current_contributor).first() + is_author = check_verified_author(submission, request.user) + is_author_unchecked = check_unverified_author(submission, request.user) + invitation = submission.referee_invitations.filter( + fulfilled=False, cancelled=False, referee__user=request.user).first() errormessage = None - if not invitation: + if is_author: + errormessage = 'You are an author of this Submission and cannot submit a Report.' + elif is_author_unchecked: + errormessage = ('The system flagged you as a potential author of this Submission. ' + 'Please go to your personal page under the Submissions tab' + ' to clarify this.') + elif not invitation: + # User is going to contribute a Report. Check deadlines for doing so. if timezone.now() > submission.reporting_deadline + datetime.timedelta(days=1): errormessage = ('The reporting deadline has passed. You cannot submit' ' a Report anymore.') @@ -1279,27 +1331,20 @@ def submit_report(request, arxiv_identifier_w_vn_nr): 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: - errormessage = ('The system flagged you as a potential author of this Submission. ' - 'Please go to your personal page under the Submissions tab' - ' to clarify this.') + submission.reports.in_draft().filter(author__user=request.user).delete() if errormessage: messages.warning(request, errormessage) - return redirect(reverse('scipost:personal_page')) + return redirect(submission.get_absolute_url()) # Find and fill earlier version of report try: - report_in_draft = submission.reports.in_draft().get(author=current_contributor) + report_in_draft = submission.reports.in_draft().get(author__user=request.user) except Report.DoesNotExist: - report_in_draft = Report(author=current_contributor, submission=submission) - form = ReportForm(request.POST or None, instance=report_in_draft, submission=submission) + report_in_draft = Report(author=request.user.contributor, submission=submission) + form = ReportForm( + request.POST or None, request.FILES or None, instance=report_in_draft, + submission=submission) # Check if data sent is valid if form.is_valid(): @@ -1309,7 +1354,8 @@ def submit_report(request, arxiv_identifier_w_vn_nr): 'You may carry on working on it,' ' or leave the page and finish it later.')) context = {'submission': submission, 'form': form} - return render(request, 'submissions/report_form.html', context) + return redirect(reverse('submissions:submit_report', kwargs={ + 'arxiv_identifier_w_vn_nr': arxiv_identifier_w_vn_nr})) # Send mails if report is submitted SubmissionUtils.load({'report': newreport}, request) @@ -1330,9 +1376,7 @@ def submit_report(request, arxiv_identifier_w_vn_nr): @login_required @fellowship_or_admin_required() def vet_submitted_reports_list(request): - """ - Reports with status `unvetted` will be shown (oldest first). - """ + """List Reports with status `unvetted`.""" submissions = get_list_or_404(Submission.objects.filter_for_eic(request.user)) reports_to_vet = Report.objects.filter( submission__in=submissions).awaiting_vetting().order_by('date_submitted') @@ -1344,16 +1388,21 @@ def vet_submitted_reports_list(request): @fellowship_or_admin_required() @transaction.atomic def vet_submitted_report(request, report_id): - """ - Report with status `unvetted` will be shown. A user may only vet reports of submissions - he/she is EIC of or if he/she is SciPost Admin or Vetting Editor. + """List Reports with status `unvetted` for vetting purposes. + + A user may only vet reports of submissions he/she is EIC of or if he/she is + SciPost Administratoror Vetting Editor. After vetting an email is sent to the report author, bcc EIC. If report has not been refused, the submission author is also mailed. """ - submissions = Submission.objects.filter_for_eic(request.user) - report = get_object_or_404(Report.objects.filter( - submission__in=submissions).awaiting_vetting(), id=report_id) + if request.user.has_perms('scipost.can_vet_submitted_reports'): + # Vetting Editors may vote on everything. + report = get_object_or_404(Report.objects.awaiting_vetting(), id=report_id) + else: + submissions = Submission.objects.filter_for_eic(request.user) + report = get_object_or_404(Report.objects.filter( + submission__in=submissions).awaiting_vetting(), id=report_id) form = VetReportForm(request.POST or None, initial={'report': report}) if form.is_valid(): @@ -1374,9 +1423,9 @@ def vet_submitted_report(request, report_id): # Add SubmissionEvent to tell the author about the new report report.submission.add_event_for_author('A new Report has been submitted.') - message = 'Submitted Report vetted for <a href="%s">%s</a>.' % ( - report.submission.get_absolute_url(), - report.submission.arxiv_identifier_w_vn_nr) + message = 'Submitted Report vetted for <a href="{url}">{arxiv}</a>.'.format( + url=report.submission.get_absolute_url(), + arxiv=report.submission.arxiv_identifier_w_vn_nr) messages.success(request, message) if report.submission.editor_in_charge == request.user.contributor: @@ -1392,9 +1441,11 @@ def vet_submitted_report(request, report_id): @permission_required('scipost.can_prepare_recommendations_for_voting', raise_exception=True) @transaction.atomic def prepare_for_voting(request, rec_id): + """Form view to prepare a EICRecommendation for voting.""" submissions = Submission.objects.pool_editable(request.user) recommendation = get_object_or_404( - EICRecommendation.objects.active().filter(submission__in=submissions), id=rec_id) + EICRecommendation.objects.voting_in_preparation().filter(submission__in=submissions), + id=rec_id) eligibility_form = VotingEligibilityForm(request.POST or None, instance=recommendation) if eligibility_form.is_valid(): @@ -1425,9 +1476,11 @@ def prepare_for_voting(request, rec_id): @fellowship_or_admin_required() @transaction.atomic def vote_on_rec(request, rec_id): + """Form view for Fellows to cast their vote on EICRecommendation.""" submissions = Submission.objects.pool_editable(request.user) recommendation = get_object_or_404( - EICRecommendation.objects.active().filter(submission__in=submissions), id=rec_id) + EICRecommendation.objects.user_may_vote_on( + request.user).filter(submission__in=submissions), id=rec_id) form = RecommendationVoteForm(request.POST or None) if form.is_valid(): @@ -1467,18 +1520,18 @@ def vote_on_rec(request, rec_id): context = { 'recommendation': recommendation, - 'form': form + 'voting_form': form } return render(request, 'submissions/pool/recommendation.html', context) @permission_required('scipost.can_prepare_recommendations_for_voting', raise_exception=True) def remind_Fellows_to_vote(request): - """ - This method sends an email to all Fellow with pending voting duties. + """Send an email to all Fellow with pending voting duties. + It must be called by and Editorial Administrator. - TODO: This reminder function doesn't filter per submission?! + Possible TODO: This reminder function doesn't filter per submission?! """ submissions = Submission.objects.pool_editable(request.user) recommendations = EICRecommendation.objects.active().filter( @@ -1507,94 +1560,85 @@ def remind_Fellows_to_vote(request): return render(request, 'scipost/acknowledgement.html', context) -@permission_required('scipost.can_fix_College_decision', raise_exception=True) -@transaction.atomic -def fix_College_decision(request, rec_id): - """ - Terminates the voting on a Recommendation. - Called by an Editorial Administrator. - - # TODO - 2 bugs: +class PreScreeningView(SubmissionAdminViewMixin, UpdateView): + """Do pre-screening of new incoming Submissions.""" - TO FIX: If multiple recommendations are submitted; decisions may be overruled unexpectedly. - TO FIX: A college decision can be fixed multiple times, there is no already-fixed mechanism!!! - """ - submissions = Submission.objects.pool_full(request.user) - recommendation = get_object_or_404(EICRecommendation.objects.filter( - submission__in=submissions), id=rec_id) - - submission = recommendation.submission - if recommendation.recommendation in [1, 2, 3]: - # Publish as Tier I, II or III - submission.status = 'accepted' - submission.acceptance_date = datetime.date.today() - - # Create a ProductionStream object - prodstream = ProductionStream(submission=submission) - prodstream.save() - ed_admins = Group.objects.get(name='Editorial Administrators') - assign_perm('can_perform_supervisory_actions', ed_admins, prodstream) - assign_perm('can_work_for_stream', ed_admins, prodstream) - - # Add SubmissionEvent for authors - # Do not write a new event for minor/major modification: already done at moment of - # creation. - notify_manuscript_accepted(request.user, submission, False) - submission.add_event_for_author('An Editorial Recommendation has been formulated: %s.' - % recommendation.get_recommendation_display()) - elif recommendation.recommendation == -3: - # Reject + update-reject other versions of submission - submission.status = 'rejected' - for sub in submission.other_versions: - sub.status = 'resubmitted_rejected' - sub.save() - - # Add SubmissionEvent for authors - # Do not write a new event for minor/major modification: already done at moment of - # creation. - submission.add_event_for_author('An Editorial Recommendation has been formulated: %s.' - % recommendation.get_recommendation_display()) - - # Add SubmissionEvent for EIC - submission.add_event_for_eic('The Editorial College\'s decision has been fixed: %s.' - % recommendation.get_recommendation_display()) + permission_required = 'scipost.can_run_pre_screening' + queryset = Submission.objects.prescreening() + template_name = 'submissions/admin/submission_prescreening.html' + form_class = SubmissionPrescreeningForm + editorial_page = True + success_url = reverse_lazy('submissions:pool') - submission.save() - SubmissionUtils.load({'submission': submission, 'recommendation': recommendation}) - SubmissionUtils.send_author_College_decision_email() - messages.success(request, 'The Editorial College\'s decision has been fixed.') - return redirect(reverse('submissions:pool')) +class EICRecommendationView(SubmissionAdminViewMixin, UpdateView): + """EICRecommendation detail view.""" -class EICRecommendationView(SubmissionAdminViewMixin, DetailView): permission_required = 'scipost.can_fix_College_decision' template_name = 'submissions/pool/recommendation.html' editorial_page = True + form_class = FixCollegeDecisionForm + success_url = reverse_lazy('submissions:pool') + + def get_object(self): + """Get EICRecommendation.""" + submission = super().get_object() + return get_object_or_404( + submission.eicrecommendations.put_to_voting(), id=self.kwargs['rec_id']) + + def get_form_kwargs(self): + """Form accepts request as argument.""" + kwargs = super().get_form_kwargs() + kwargs['request'] = self.request + return kwargs def get_context_data(self, *args, **kwargs): - """ Get the EICRecommendation as a submission-related instance. """ + """Get the EICRecommendation as a submission-related instance.""" ctx = super().get_context_data(*args, **kwargs) - ctx['recommendation'] = get_object_or_404( - ctx['submission'].eicrecommendations.all(), id=self.kwargs['rec_id']) + ctx['recommendation'] = ctx['object'] return ctx + @transaction.atomic + def form_valid(self, form): + """Redirect and send out mails if decision is fixed.""" + recommendation = form.save() + if form.is_fixed(): + submission = recommendation.submission + + # Temporary: Update submission instance for utils email func. + # Won't be needed in new mail construct. + submission = Submission.objects.get(id=recommendation.submission.id) + SubmissionUtils.load({'submission': submission, 'recommendation': recommendation}) + SubmissionUtils.send_author_College_decision_email() + messages.success(self.request, 'The Editorial College\'s decision has been fixed.') + else: + messages.success( + self.request, 'The Editorial College\'s decision has been deprecated.') + return HttpResponseRedirect(self.success_url) + class PlagiarismView(SubmissionAdminViewMixin, UpdateView): + """Administration detail page of Plagiarism report.""" + permission_required = 'scipost.can_do_plagiarism_checks' template_name = 'submissions/admin/plagiarism_report.html' editorial_page = True form_class = iThenticateReportForm def get_object(self): + """Get the plagiarism_report as a linked object from the Submission.""" submission = super().get_object() return submission.plagiarism_report class PlagiarismReportPDFView(SubmissionAdminViewMixin, SingleObjectMixin, RedirectView): + """Redirect to Plagiarism report PDF at iThenticate.""" + permission_required = 'scipost.can_do_plagiarism_checks' editorial_page = True def get_redirect_url(self, *args, **kwargs): + """Get the temporary url provided by the iThenticate API.""" submission = self.get_object() if not submission.plagiarism_report: raise Http404