diff --git a/submissions/exceptions.py b/submissions/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..19c5684bd004f175ffe4169cc2938d312d66edf0 --- /dev/null +++ b/submissions/exceptions.py @@ -0,0 +1,6 @@ +class CycleUpdateDeadlineError(Exception): + def __init__(self, name): + self.name = name + + def __str__(self): + return self.name diff --git a/submissions/factories.py b/submissions/factories.py index cdf905fb56b2672019ee707381672388a939eba3..da936a2e7a8fcbe4d2d9225a6d4a83f2b4f812da 100644 --- a/submissions/factories.py +++ b/submissions/factories.py @@ -1,16 +1,14 @@ import factory -import datetime import pytz from django.utils import timezone -from scipost.factories import ContributorFactory from scipost.models import Contributor from journals.constants import SCIPOST_JOURNALS_DOMAINS from common.helpers import random_arxiv_identifier_without_version_number, random_scipost_journal from .constants import STATUS_UNASSIGNED, STATUS_EIC_ASSIGNED, STATUS_RESUBMISSION_INCOMING,\ - STATUS_PUBLISHED + STATUS_PUBLISHED, SUBMISSION_TYPE from .models import Submission from faker import Faker @@ -21,7 +19,7 @@ class SubmissionFactory(factory.django.DjangoModelFactory): model = Submission author_list = factory.Faker('name') - submitted_by = factory.SubFactory(ContributorFactory) + submitted_by = Contributor.objects.first() submitted_to_journal = factory.Sequence(lambda n: random_scipost_journal()) title = factory.lazy_attribute(lambda x: Faker().sentence()) abstract = factory.lazy_attribute(lambda x: Faker().paragraph()) @@ -32,7 +30,8 @@ class SubmissionFactory(factory.django.DjangoModelFactory): abstract = Faker().paragraph() author_comments = Faker().paragraph() remarks_for_editors = Faker().paragraph() - submission_type = 'Letter' + submission_type = factory.Iterator(SUBMISSION_TYPE, getter=lambda c: c[0]) + is_current = True @factory.post_generation def fill_arxiv_fields(self, create, extracted, **kwargs): @@ -43,11 +42,13 @@ class SubmissionFactory(factory.django.DjangoModelFactory): @factory.post_generation def contributors(self, create, extracted, **kwargs): - contributors = list(Contributor.objects.order_by('?') - .exclude(pk=self.submitted_by.pk).all()[:3]) + contributors = list(Contributor.objects.order_by('?')[:4]) + + # Auto-add the submitter as an author + self.submitted_by = contributors.pop() + if not create: return - # Auto-add the submitter as an author self.authors.add(self.submitted_by) # Add three random authors @@ -60,11 +61,13 @@ class SubmissionFactory(factory.django.DjangoModelFactory): timezone.now() if kwargs.get('submission', False): self.submission_date = kwargs['submission'] - else: - self.submission_date = Faker().date_time_between(start_date="-3y", end_date="now", - tzinfo=pytz.UTC) + self.cycle.update_deadline() + return + self.submission_date = Faker().date_time_between(start_date="-3y", end_date="now", + tzinfo=pytz.UTC).date() self.latest_activity = Faker().date_time_between(start_date=self.submission_date, end_date="now", tzinfo=pytz.UTC) + self.cycle.update_deadline() class UnassignedSubmissionFactory(SubmissionFactory): @@ -79,24 +82,53 @@ class EICassignedSubmissionFactory(SubmissionFactory): open_for_commenting = True open_for_reporting = True - @factory.post_generation - def report_dates(self, create, extracted, **kwargs): - self.reporting_deadline = self.latest_activity + datetime.timedelta(weeks=2) - @factory.post_generation def eic(self, create, extracted, **kwargs): + '''Assign an EIC to submission.''' author_ids = list(self.authors.values_list('id', flat=True)) self.editor_in_charge = (Contributor.objects.order_by('?') .exclude(pk=self.submitted_by.pk) .exclude(pk__in=author_ids).first()) -class ResubmittedScreeningSubmissionFactory(SubmissionFactory): +class ResubmittedSubmissionFactory(SubmissionFactory): + ''' + This Submission is a newer version of a Submission which is + already known by the SciPost database. + ''' status = STATUS_RESUBMISSION_INCOMING + open_for_commenting = True + open_for_reporting = True + is_resubmission = True + + @factory.post_generation + def alter_arxiv_fields(self, create, extracted, **kwargs): + '''Alter arxiv fields to save as version 2.''' + self.arxiv_identifier_w_vn_nr = '%sv2' % self.arxiv_identifier_wo_vn_nr + self.arxiv_vn_nr = 2 + + @factory.post_generation + def eic(self, create, extracted, **kwargs): + '''Assign an EIC to submission.''' + author_ids = list(self.authors.values_list('id', flat=True)) + self.editor_in_charge = (Contributor.objects.order_by('?') + .exclude(pk=self.submitted_by.pk) + .exclude(pk__in=author_ids).first()) + + @factory.post_generation + def dates(self, create, extracted, **kwargs): + """Overwrite the parent `dates` method to skip the update_deadline call.""" + timezone.now() + if kwargs.get('submission', False): + self.submission_date = kwargs['submission'] + return + self.submission_date = Faker().date_time_between(start_date="-3y", end_date="now", + tzinfo=pytz.UTC).date() + self.latest_activity = Faker().date_time_between(start_date=self.submission_date, + end_date="now", tzinfo=pytz.UTC) class PublishedSubmissionFactory(SubmissionFactory): status = STATUS_PUBLISHED open_for_commenting = False open_for_reporting = False - is_current = True diff --git a/submissions/migrations/0042_auto_20170511_2321.py b/submissions/migrations/0042_auto_20170511_2321.py new file mode 100644 index 0000000000000000000000000000000000000000..20b3a653e5fc906c41d3159a46c85278dba8b182 --- /dev/null +++ b/submissions/migrations/0042_auto_20170511_2321.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-05-11 21:21 +from __future__ import unicode_literals + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0041_auto_20170418_1022'), + ] + + operations = [ + migrations.AlterField( + model_name='submission', + name='submission_date', + field=models.DateField(default=datetime.date.today, verbose_name='submission date'), + ), + ] diff --git a/submissions/models.py b/submissions/models.py index 0a2b82f9ae8f5dfb2a42b6a9df8c0a3978d772e3..787754332f0ecdd4c01c8b9685b499e84333b448 100644 --- a/submissions/models.py +++ b/submissions/models.py @@ -78,7 +78,7 @@ class Submission(ArxivCallable, models.Model): # Metadata metadata = JSONField(default={}, blank=True, null=True) - submission_date = models.DateField(verbose_name='submission date', default=timezone.now) + submission_date = models.DateField(verbose_name='submission date', default=datetime.date.today) latest_activity = models.DateTimeField(default=timezone.now) objects = SubmissionManager() diff --git a/submissions/test_utils.py b/submissions/test_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..36f9a5ca7a3727542c52ff5345ea96dd197c8a3f --- /dev/null +++ b/submissions/test_utils.py @@ -0,0 +1,137 @@ +import datetime + +from django.test import TestCase, tag + +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 .exceptions import CycleUpdateDeadlineError +from .factories import UnassignedSubmissionFactory, ResubmittedSubmissionFactory +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. + ''' + + def setUp(self): + """Basics for all tests""" + self.submission_date = datetime.date.today() + add_groups_and_permissions() + ContributorFactory.create_batch(5) + self.new_submission = UnassignedSubmissionFactory( + dates__submission=self.submission_date + ) + + @tag('cycle', 'core') + def test_init_submission_factory_is_valid(self): + """Ensure valid fields for the factory.""" + self.assertEqual(self.new_submission.status, STATUS_UNASSIGNED) + self.assertIsNone(self.new_submission.editor_in_charge) + self.assertTrue(self.new_submission.is_current) + self.assertFalse(self.new_submission.is_resubmission) + self.assertIsNot(self.new_submission.title, '') + self.assertIsInstance(self.new_submission.submitted_by, Contributor) + self.assertFalse(self.new_submission.open_for_commenting) + self.assertFalse(self.new_submission.open_for_reporting) + self.assertEqual(self.new_submission.submission_date, self.submission_date) + + @tag('cycle', 'core') + def test_initial_cycle_required_actions_and_deadline(self): + """Test valid required actions for default cycle.""" + self.assertIsInstance(self.new_submission.cycle, GeneralSubmissionCycle) + + # Explicit: No actions required if no EIC is assigned yet + self.assertFalse(self.new_submission.cycle.get_required_actions()) + + # Two weeks deadline check + self.new_submission.cycle.update_deadline() + real_report_deadline = self.submission_date + datetime.timedelta(days=28) + self.assertEqual(self.new_submission.reporting_deadline.day, real_report_deadline.day) + self.assertEqual(self.new_submission.reporting_deadline.month, real_report_deadline.month) + self.assertEqual(self.new_submission.reporting_deadline.year, real_report_deadline.year) + self.assertIsInstance(self.new_submission.reporting_deadline, datetime.datetime) + + +class TestResubmissionSubmissionCycle(TestCase): + ''' + This TestCase should act as a master test to check all steps in the + submission's cycle: resubmission. + ''' + + def setUp(self): + """Basics for all tests""" + self.submission_date = datetime.date.today() + add_groups_and_permissions() + ContributorFactory.create_batch(5) + self.submission = ResubmittedSubmissionFactory( + dates__submission=self.submission_date + ) + + @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.assertIsInstance(self.submission.editor_in_charge, Contributor) + self.assertTrue(self.submission.is_current) + self.assertTrue(self.submission.is_resubmission) + self.assertIsNot(self.submission.title, '') + self.assertIsInstance(self.submission.submitted_by, Contributor) + self.assertTrue(self.submission.open_for_commenting) + self.assertTrue(self.submission.open_for_reporting) + self.assertEqual(self.submission.submission_date, self.submission_date) + self.assertEqual(self.submission.refereeing_cycle, CYCLE_DEFAULT) + + @tag('cycle', 'core') + def test_initial_cycle_required_actions_and_deadline(self): + """Test valid required actions for default cycle.""" + self.assertRaises(CycleUpdateDeadlineError, self.submission.cycle.update_deadline) + + # Update status for default cycle to check new status + self.submission.cycle.update_status() + self.assertEqual(self.submission.status, STATUS_EIC_ASSIGNED) + + +class TestResubmissionDirectSubmissionCycle(TestCase): + ''' + This TestCase should act as a master test to check all steps in the + submission's cycle: resubmission (cycle: DIRECT_RECOMMENDATION). + ''' + + def setUp(self): + """Basics for all tests""" + self.submission_date = datetime.date.today() + add_groups_and_permissions() + ContributorFactory.create_batch(5) + self.submission = ResubmittedSubmissionFactory( + dates__submission=self.submission_date, + refereeing_cycle=CYCLE_DIRECT_REC + ) + + @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.assertIsInstance(self.submission.editor_in_charge, Contributor) + self.assertTrue(self.submission.is_current) + self.assertTrue(self.submission.is_resubmission) + self.assertIsNot(self.submission.title, '') + self.assertIsInstance(self.submission.submitted_by, Contributor) + self.assertTrue(self.submission.open_for_commenting) + self.assertTrue(self.submission.open_for_reporting) + self.assertEqual(self.submission.submission_date, self.submission_date) + self.assertEqual(self.submission.refereeing_cycle, CYCLE_DIRECT_REC) + + @tag('cycle', 'core') + def test_initial_cycle_required_actions_and_deadline(self): + """Test valid required actions for default cycle.""" + self.assertRaises(CycleUpdateDeadlineError, self.submission.cycle.update_deadline) + + # Update status for default cycle to check new status + self.submission.cycle.update_status() + self.assertEqual(self.submission.status, STATUS_AWAITING_ED_REC) diff --git a/submissions/utils.py b/submissions/utils.py index d713236f3f2b66dab3fd04300962da4ece33e9a6..29ed7e85be4febc089c51bd4b476b66f3db4c21a 100644 --- a/submissions/utils.py +++ b/submissions/utils.py @@ -7,6 +7,7 @@ from django.utils import timezone from .constants import NO_REQUIRED_ACTION_STATUSES,\ STATUS_REVISION_REQUESTED, STATUS_EIC_ASSIGNED,\ STATUS_RESUBMISSION_INCOMING, STATUS_AWAITING_ED_REC +from .exceptions import CycleUpdateDeadlineError from scipost.utils import EMAIL_FOOTER from common.utils import BaseMailUtil @@ -112,11 +113,19 @@ class BaseSubmissionCycle: SubmissionUtils.reinvite_referees_email() def update_deadline(self, period=None): + """ + Reset the reporting deadline according to current datetime and default cycle length. + New reporting deadline may be explicitly given as datetime instance. + """ + if self.submission.status == STATUS_RESUBMISSION_INCOMING: + raise CycleUpdateDeadlineError('Submission has invalid status: %s' + % self.submission.status) delta_d = period or self.default_days deadline = timezone.now() + datetime.timedelta(days=delta_d) self.submission.reporting_deadline = deadline self.submission.save() + def get_required_actions(self): '''Return list of the submission its required actions''' if not self.updated_action: