diff --git a/SciPost_v1/settings/base.py b/SciPost_v1/settings/base.py index 0bc456e6fc11f3f29c79da458918f705935d5c44..30aa73cfcfdecf33260ffd7431f7e954c3f07d0e 100644 --- a/SciPost_v1/settings/base.py +++ b/SciPost_v1/settings/base.py @@ -138,9 +138,9 @@ SHELL_PLUS_POST_IMPORTS = ( ('submissions.factories', ('SubmissionFactory', 'EICassignedSubmissionFactory')), ('commentaries.factories', ('EmptyCommentaryFactory', - 'VettedCommentaryFactory', + 'CommentaryFactory', 'UnvettedCommentaryFactory', - 'UnpublishedVettedCommentaryFactory',)), + 'UnpublishedCommentaryFactory',)), ('scipost.factories', ('ContributorFactory')), ) diff --git a/SciPost_v1/settings/local_jorran.py b/SciPost_v1/settings/local_jorran.py index 0ca80ebb49d60854a148e3619d6d79bae6e0d078..bb65bb29a62b13b3f42c541dd6a4906b01924e71 100644 --- a/SciPost_v1/settings/local_jorran.py +++ b/SciPost_v1/settings/local_jorran.py @@ -13,23 +13,23 @@ MIDDLEWARE += ( INTERNAL_IPS = ['127.0.0.1', '::1'] # Static and media -STATIC_ROOT = '/Users/jorranwit/Develop/SciPost/scipost_v1/local_files/static/' -MEDIA_ROOT = '/Users/jorranwit/Develop/SciPost/scipost_v1/local_files/media/' +STATIC_ROOT = '/Users/jorrandewit/Documents/Develop/SciPost/scipost_v1/local_files/static/' +MEDIA_ROOT = '/Users/jorrandewit/Documents/Develop/SciPost/scipost_v1/local_files/media/' WEBPACK_LOADER['DEFAULT']['BUNDLE_DIR_NAME'] =\ - '/Users/jorranwit/Develop/SciPost/scipost_v1/local_files/static/bundles/' + '/Users/jorrandewit/Documents/Develop/SciPost/scipost_v1/local_files/static/bundles/' MAILCHIMP_API_USER = get_secret("MAILCHIMP_API_USER") MAILCHIMP_API_KEY = get_secret("MAILCHIMP_API_KEY") -DATABASES['default']['PORT'] = '5433' +DATABASES['default']['PORT'] = '5432' # iThenticate ITHENTICATE_USERNAME = get_secret('ITHENTICATE_USERNAME') ITHENTICATE_PASSWORD = get_secret('ITHENTICATE_PASSWORD') # Logging -LOGGING['handlers']['scipost_file_arxiv']['filename'] = '/Users/jorranwit/Develop/SciPost/SciPost_v1/logs/arxiv.log' -LOGGING['handlers']['scipost_file_doi']['filename'] = '/Users/jorranwit/Develop/SciPost/SciPost_v1/logs/doi.log' +LOGGING['handlers']['scipost_file_arxiv']['filename'] = '/Users/jorrandewit/Documents/Develop/SciPost/SciPost_v1/logs/arxiv.log' +LOGGING['handlers']['scipost_file_doi']['filename'] = '/Users/jorrandewit/Documents/Develop/SciPost/SciPost_v1/logs/doi.log' # Other CROSSREF_LOGIN_ID = get_secret("CROSSREF_LOGIN_ID") diff --git a/affiliations/factories.py b/affiliations/factories.py new file mode 100644 index 0000000000000000000000000000000000000000..c01e316b1598c30d0bd732341c65464dbd1a64f1 --- /dev/null +++ b/affiliations/factories.py @@ -0,0 +1,26 @@ +import factory + +from .constants import INSTITUTION_TYPES +from .models import Institution, Affiliation + + +class InstitutionFactory(factory.django.DjangoModelFactory): + name = factory.Faker('company') + acronym = factory.lazy_attribute(lambda o: o.name[:16]) + country = factory.Faker('country_code') + type = factory.Iterator(INSTITUTION_TYPES, getter=lambda c: c[0]) + + class Meta: + model = Institution + django_get_or_create = ('name',) + + +class AffiliationFactory(factory.django.DjangoModelFactory): + institution = factory.SubFactory('affiliations.factories.InstitutionFactory') + contributor = factory.SubFactory('scipost.factories.ContributorFactory') + begin_date = factory.Faker('date_this_decade') + end_date = factory.Faker('future_date', end_date="+2y") + + class Meta: + model = Affiliation + django_get_or_create = ('institution', 'contributor') diff --git a/affiliations/forms.py b/affiliations/forms.py index 774a2e6a7895455701b1fbc7652c79cc6c4aa205..b46b37472ecbe4b35b7817c730bb02194d06b11d 100644 --- a/affiliations/forms.py +++ b/affiliations/forms.py @@ -1,6 +1,5 @@ from django import forms from django.forms import BaseModelFormSet, modelformset_factory -# from django.db.models import F from django_countries import countries from django_countries.fields import LazyTypedChoiceField diff --git a/colleges/factories.py b/colleges/factories.py new file mode 100644 index 0000000000000000000000000000000000000000..dc1a829b8427c6ffb2358776aabfb5e2bae15f67 --- /dev/null +++ b/colleges/factories.py @@ -0,0 +1,22 @@ +import factory + +from scipost.models import Contributor + +from .models import Fellowship + + +class BaseFellowshipFactory(factory.django.DjangoModelFactory): + contributor = factory.Iterator(Contributor.objects.all()) + start_date = factory.Faker('date_this_year') + until_date = factory.Faker('date_between', start_date="now", end_date="+2y") + + guest = factory.Faker('boolean', chance_of_getting_true=10) + + class Meta: + model = Fellowship + django_get_or_create = ('contributor', 'start_date') + abstract = True + + +class FellowshipFactory(BaseFellowshipFactory): + pass diff --git a/colleges/management/__init__.py b/colleges/management/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/colleges/management/commands/__init__.py b/colleges/management/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/colleges/management/commands/create_fellowships.py b/colleges/management/commands/create_fellowships.py new file mode 100644 index 0000000000000000000000000000000000000000..b28ad5069950659c47e69e2794859309079bed2b --- /dev/null +++ b/colleges/management/commands/create_fellowships.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand + +from colleges import factories + + +class Command(BaseCommand): + help = 'Create random Fellowships objects using the factories.' + + def add_arguments(self, parser): + parser.add_argument( + 'number', action='store', default=0, type=int, + help='Number of Fellowships to add') + + def handle(self, *args, **kwargs): + self.create_fellowships(kwargs['number']) + + def create_fellowships(self, n): + factories.FellowshipFactory.create_batch(n) + self.stdout.write(self.style.SUCCESS('Successfully created {n} Fellowships.'.format(n=n))) diff --git a/commentaries/admin.py b/commentaries/admin.py index 93aee6375bdb1b36584042765d5e5220d7d1ca0e..f303ebcfefa5c47f18b939ac1f180aff8412aed4 100644 --- a/commentaries/admin.py +++ b/commentaries/admin.py @@ -29,4 +29,5 @@ class CommentaryAdmin(admin.ModelAdmin): date_hierarchy = 'latest_activity' form = CommentaryAdminForm + admin.site.register(Commentary, CommentaryAdmin) diff --git a/commentaries/factories.py b/commentaries/factories.py index c908621441bff8715a7bbaf5cce7cd3833994445..0e0f2df751417dc915d3e88002a2cb8e085d4a0c 100644 --- a/commentaries/factories.py +++ b/commentaries/factories.py @@ -8,31 +8,31 @@ from common.helpers import random_arxiv_identifier_with_version_number, random_e from .constants import COMMENTARY_TYPES from .models import Commentary -from faker import Faker - -class CommentaryFactory(factory.django.DjangoModelFactory): +class BaseCommentaryFactory(factory.django.DjangoModelFactory): class Meta: model = Commentary + django_get_or_create = ('pub_DOI', 'arxiv_identifier') + abstract = True requested_by = factory.Iterator(Contributor.objects.all()) + vetted = True + vetted_by = factory.Iterator(Contributor.objects.all()) type = factory.Iterator(COMMENTARY_TYPES, getter=lambda c: c[0]) discipline = factory.Iterator(SCIPOST_DISCIPLINES, getter=lambda c: c[0]) domain = factory.Iterator(SCIPOST_JOURNALS_DOMAINS, getter=lambda c: c[0]) subject_area = factory.Iterator(SCIPOST_SUBJECT_AREAS[0][1], getter=lambda c: c[0]) - title = factory.Faker('text') + title = factory.Faker('sentence') pub_DOI = factory.Sequence(lambda n: random_external_doi()) - arxiv_identifier = factory.Sequence(lambda n: random_arxiv_identifier_with_version_number()) + arxiv_identifier = factory.Sequence(lambda n: random_arxiv_identifier_with_version_number('1')) author_list = factory.Faker('name') pub_abstract = factory.Faker('text') - pub_date = factory.Faker('date') - arxiv_link = factory.Faker('uri') - pub_abstract = factory.lazy_attribute(lambda x: Faker().paragraph()) + pub_date = factory.Faker('date_this_decade') + pub_abstract = factory.Faker('paragraph') - @factory.post_generation - def arxiv_link(self, create, extracted, **kwargs): - self.arxiv_link = 'https://arxiv.org/abs/%s' % self.arxiv_identifier - self.arxiv_or_DOI_string = self.arxiv_identifier + arxiv_link = factory.lazy_attribute(lambda o: 'https://arxiv.org/abs/%s' % o.arxiv_identifier) + arxiv_or_DOI_string = factory.lazy_attribute(lambda o: ( + o.arxiv_identifier if o.arxiv_identifier else o.pub_DOI)) @factory.post_generation def create_urls(self, create, extracted, **kwargs): @@ -40,27 +40,45 @@ class CommentaryFactory(factory.django.DjangoModelFactory): @factory.post_generation def add_authors(self, create, extracted, **kwargs): - contributors = list(Contributor.objects.order_by('?') - .exclude(pk=self.requested_by.pk).all()[:4]) + contributors = Contributor.objects.order_by('?').exclude(pk=self.requested_by.pk)[:4] self.author_list = ', '.join( - ['%s %s' % (contrib.user.first_name, - contrib.user.last_name) for contrib in contributors]) - self.authors.add(*contributors) + ['%s %s' % (contrib.user.first_name, contrib.user.last_name) + for contrib in contributors]) + if create: + self.authors.add(*contributors) -class VettedCommentaryFactory(CommentaryFactory): - vetted = True - vetted_by = factory.Iterator(Contributor.objects.all()) + @factory.post_generation + def set_journal_data(self, create, extracted, **kwargs): + if not self.pub_DOI: + return + data = self.pub_DOI.split('/')[1].split('.') + self.journal = data[0] + self.volume = data[1] + self.pages = data[2] -class UnpublishedVettedCommentaryFactory(VettedCommentaryFactory): - pub_DOI = '' +class CommentaryFactory(BaseCommentaryFactory): + pass -class UnvettedCommentaryFactory(CommentaryFactory): + +class UnvettedCommentaryFactory(BaseCommentaryFactory): vetted = False + vetted_by = None -class UnvettedArxivPreprintCommentaryFactory(CommentaryFactory): - vetted = False +class UnpublishedCommentaryFactory(BaseCommentaryFactory): pub_DOI = '' + pub_date = None + + +class UnvettedUnpublishedCommentaryFactory(UnpublishedCommentaryFactory): + vetted = False + vetted_by = None + + +class PublishedCommentaryFactory(BaseCommentaryFactory): + arxiv_identifier = '' + arxiv_link = '' + arxiv_or_DOI_string = factory.lazy_attribute(lambda o: o.pub_DOI) diff --git a/commentaries/management/__init__.py b/commentaries/management/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/commentaries/management/commands/__init__.py b/commentaries/management/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/commentaries/management/commands/create_commentaries.py b/commentaries/management/commands/create_commentaries.py new file mode 100644 index 0000000000000000000000000000000000000000..1353a86dd1794882a4587d0f916e98f7ddd9ad33 --- /dev/null +++ b/commentaries/management/commands/create_commentaries.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand + +from commentaries import factories + + +class Command(BaseCommand): + help = 'Create random Commentaries objects using the factories.' + + def add_arguments(self, parser): + parser.add_argument( + 'number', action='store', default=0, type=int, + help='Number of Commentaries to add') + + def handle(self, *args, **kwargs): + self.create_commentaries(kwargs['number']) + + def create_commentaries(self, n): + factories.CommentaryFactory.create_batch(n) + self.stdout.write(self.style.SUCCESS('Successfully created {n} Commentaries.'.format(n=n))) diff --git a/commentaries/templates/commentaries/_commentary_card_content.html b/commentaries/templates/commentaries/_commentary_card_content.html index 7c5954005dbba4b8a85a7312f0684392da50c232..ff4e0df85e17167d59b0716583737f07df316d0a 100644 --- a/commentaries/templates/commentaries/_commentary_card_content.html +++ b/commentaries/templates/commentaries/_commentary_card_content.html @@ -1,6 +1,6 @@ <div class="card-body"> <h3 class="card-title"> - <a href="{% url 'commentaries:commentary' commentary.arxiv_or_DOI_string %}">{{ commentary.title }}</a> + <a href="{{ commentary.get_absolute_url }}">{{ commentary.title }}</a> </h3> <p class="mt-0 mb-3"> by {{ commentary.author_list }}{% if commentary.type == 'published' %}, {{ commentary.journal }} {{ commentary.volume }}, {{ commentary.pages }}{% elif commentary.type == 'preprint' %} · <a href="{{ commentary.arxiv_link }}">{{ commentary.arxiv_link }}</a>{% endif %} diff --git a/commentaries/test_forms.py b/commentaries/test_forms.py index 35e65abb64b3b50ed5f3aca96a9a65dbefce65a5..a24e9785c060b0da78756622c57ccec9e15d02f5 100644 --- a/commentaries/test_forms.py +++ b/commentaries/test_forms.py @@ -5,8 +5,8 @@ from django.test import TestCase from common.helpers import model_form_data from scipost.factories import UserFactory, ContributorFactory -from .factories import VettedCommentaryFactory, UnvettedCommentaryFactory,\ - UnvettedArxivPreprintCommentaryFactory +from .factories import CommentaryFactory, UnvettedCommentaryFactory,\ + UnvettedUnpublishedCommentaryFactory from .forms import RequestPublishedArticleForm, VetCommentaryForm, DOIToQueryForm,\ ArxivQueryForm, RequestArxivPreprintForm from .models import Commentary @@ -189,7 +189,7 @@ class TestRequestArxivPreprintForm(TestCase): def setUp(self): add_groups_and_permissions() ContributorFactory.create_batch(5) - factory_instance = UnvettedArxivPreprintCommentaryFactory.build() + factory_instance = UnvettedUnpublishedCommentaryFactory.build() self.user = UserFactory() self.valid_form_data = model_form_data(factory_instance, RequestPublishedArticleForm) self.valid_form_data['arxiv_identifier'] = factory_instance.arxiv_identifier @@ -199,7 +199,7 @@ class TestRequestArxivPreprintForm(TestCase): self.assertTrue(form.is_valid()) def test_identifier_that_already_has_commentary_page_is_invalid(self): - commentary = UnvettedArxivPreprintCommentaryFactory() + commentary = UnvettedUnpublishedCommentaryFactory() invalid_data = {**self.valid_form_data, **{'arxiv_identifier': commentary.arxiv_identifier}} form = RequestArxivPreprintForm(invalid_data) self.assertEqual(form.is_valid(), False) diff --git a/commentaries/test_views.py b/commentaries/test_views.py index 24f46518623509e53016c7939f564ceee26d53cd..bddad132390fe8ecc387899c3fa7c46f7da2621c 100644 --- a/commentaries/test_views.py +++ b/commentaries/test_views.py @@ -5,8 +5,8 @@ from django.test import TestCase, Client, RequestFactory from scipost.models import Contributor from scipost.factories import ContributorFactory, UserFactory -from .factories import UnvettedCommentaryFactory, VettedCommentaryFactory, UnpublishedVettedCommentaryFactory, \ - UnvettedArxivPreprintCommentaryFactory +from .factories import UnvettedCommentaryFactory, CommentaryFactory, UnpublishedCommentaryFactory, \ + UnvettedUnpublishedCommentaryFactory from .forms import CommentarySearchForm, RequestPublishedArticleForm from .models import Commentary from .views import RequestPublishedArticle, prefill_using_DOI, RequestArxivPreprint @@ -57,7 +57,7 @@ class RequestArxivPreprintTest(TestCase): add_groups_and_permissions() self.target = reverse('commentaries:request_arxiv_preprint') self.contributor = ContributorFactory() - self.commentary_instance = UnvettedArxivPreprintCommentaryFactory.build(requested_by=self.contributor) + self.commentary_instance = UnvettedUnpublishedCommentaryFactory.build(requested_by=self.contributor) self.valid_form_data = model_form_data(self.commentary_instance, RequestPublishedArticleForm) # The form field is called 'identifier', while the model field is called 'arxiv_identifier', # so model_form_data doesn't include it. @@ -76,6 +76,7 @@ class RequestArxivPreprintTest(TestCase): self.assertEqual(commentary.arxiv_or_DOI_string, "arXiv:" + self.commentary_instance.arxiv_identifier) self.assertEqual(commentary.requested_by, self.contributor) + class VetCommentaryRequestsTest(TestCase): """Test cases for `vet_commentary_requests` view method""" @@ -119,7 +120,7 @@ class VetCommentaryRequestsTest(TestCase): # Only vetted Commentaries exist! # ContributorFactory.create_batch(5) - VettedCommentaryFactory(requested_by=ContributorFactory(), vetted_by=ContributorFactory()) + CommentaryFactory(requested_by=ContributorFactory(), vetted_by=ContributorFactory()) response = self.client.get(self.view_url) self.assertEquals(response.context['commentary_to_vet'], None) @@ -134,7 +135,7 @@ class BrowseCommentariesTest(TestCase): def setUp(self): add_groups_and_permissions() - VettedCommentaryFactory(discipline='physics', requested_by=ContributorFactory()) + CommentaryFactory(discipline='physics', requested_by=ContributorFactory()) self.view_url = reverse('commentaries:browse', kwargs={ 'discipline': 'physics', 'nrweeksback': '1' @@ -155,7 +156,7 @@ class CommentaryDetailTest(TestCase): def setUp(self): add_groups_and_permissions() self.client = Client() - self.commentary = UnpublishedVettedCommentaryFactory( + self.commentary = UnpublishedCommentaryFactory( requested_by=ContributorFactory(), vetted_by=ContributorFactory()) self.target = reverse( 'commentaries:commentary', diff --git a/commentaries/views.py b/commentaries/views.py index 015fb814d1a87c8739186b1c1001ecdad39df58c..532c8b2cfc14fbe56868959ca4c8f61a7b22f7de 100644 --- a/commentaries/views.py +++ b/commentaries/views.py @@ -233,13 +233,7 @@ def commentary_detail(request, arxiv_or_DOI_string): arxiv_or_DOI_string=arxiv_or_DOI_string) form = CommentForm() - try: - author_replies = Comment.objects.filter( - commentary=commentary, is_author_reply=True, status__gte=1) - except Comment.DoesNotExist: - author_replies = () - context = {'commentary': commentary, - 'author_replies': author_replies, 'form': form} + context = {'commentary': commentary, 'form': form} return render(request, 'commentaries/commentary_detail.html', context) diff --git a/comments/factories.py b/comments/factories.py index 1f76cf6a460001cb60077d1d5e73bdf075dd2c6a..bd6a3c784d52457cf1ae506921b27bb73176df37 100644 --- a/comments/factories.py +++ b/comments/factories.py @@ -1,29 +1,37 @@ +import random import factory -import pytz -from django.utils import timezone - -from commentaries.factories import VettedCommentaryFactory +from commentaries.models import Commentary from scipost.models import Contributor -from submissions.factories import EICassignedSubmissionFactory -from theses.factories import VettedThesisLinkFactory +from submissions.models import Submission, Report +from theses.models import ThesisLink from .constants import STATUS_VETTED from .models import Comment from faker import Faker -timezone.now() - class CommentFactory(factory.django.DjangoModelFactory): + status = STATUS_VETTED + vetted_by = factory.Iterator(Contributor.objects.all()) + author = factory.Iterator(Contributor.objects.all()) - comment_text = factory.lazy_attribute(lambda x: Faker().paragraph()) - remarks_for_editors = factory.lazy_attribute(lambda x: Faker().paragraph()) + comment_text = factory.Faker('paragraph') + remarks_for_editors = factory.Faker('paragraph') file_attachment = Faker().file_name(extension='pdf') - status = STATUS_VETTED # All comments will have status vetted! - vetted_by = factory.Iterator(Contributor.objects.all()) - date_submitted = Faker().date_time_between(start_date="-3y", end_date="now", tzinfo=pytz.UTC) + date_submitted = factory.Faker('date_this_decade') + + # Categories + is_cor = factory.Faker('boolean', chance_of_getting_true=20) + is_rem = factory.Faker('boolean', chance_of_getting_true=20) + is_que = factory.Faker('boolean', chance_of_getting_true=20) + is_ans = factory.Faker('boolean', chance_of_getting_true=20) + is_obj = factory.Faker('boolean', chance_of_getting_true=20) + is_rep = factory.Faker('boolean', chance_of_getting_true=20) + is_val = factory.Faker('boolean', chance_of_getting_true=20) + is_lit = factory.Faker('boolean', chance_of_getting_true=20) + is_sug = factory.Faker('boolean', chance_of_getting_true=20) class Meta: model = Comment @@ -31,16 +39,27 @@ class CommentFactory(factory.django.DjangoModelFactory): class CommentaryCommentFactory(CommentFactory): - content_object = factory.SubFactory(VettedCommentaryFactory) + content_object = factory.Iterator(Commentary.objects.all()) class SubmissionCommentFactory(CommentFactory): - content_object = factory.SubFactory(EICassignedSubmissionFactory) + content_object = factory.Iterator(Submission.objects.all()) + + @factory.post_generation + def replies(self, create, extracted, **kwargs): + if create: + for i in range(random.randint(0, 2)): + ReplyCommentFactory(content_object=self) + + +class ReplyCommentFactory(CommentFactory): + content_object = factory.SubFactory(SubmissionCommentFactory, replies=False) + is_author_reply = factory.Faker('boolean') class ThesislinkCommentFactory(CommentFactory): - content_object = factory.SubFactory(VettedThesisLinkFactory) + content_object = factory.Iterator(ThesisLink.objects.all()) -class ReplyCommentFactory(CommentFactory): - content_object = factory.SubFactory(SubmissionCommentFactory) +class ReportCommentFactory(CommentFactory): + content_object = factory.Iterator(Report.objects.all()) diff --git a/comments/migrations/0003_auto_20180314_1502.py b/comments/migrations/0003_auto_20180314_1502.py new file mode 100644 index 0000000000000000000000000000000000000000..585e4360923d274cd2c0204c18b1c5c9c4be9552 --- /dev/null +++ b/comments/migrations/0003_auto_20180314_1502.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-03-14 14:02 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('comments', '0002_auto_20171229_1435'), + ] + + operations = [ + migrations.RemoveField( + model_name='comment', + name='commentary', + ), + migrations.RemoveField( + model_name='comment', + name='in_reply_to_comment', + ), + migrations.RemoveField( + model_name='comment', + name='in_reply_to_report', + ), + migrations.RemoveField( + model_name='comment', + name='submission', + ), + migrations.RemoveField( + model_name='comment', + name='thesislink', + ), + ] diff --git a/comments/models.py b/comments/models.py index e138ca39ad2b9a7fefc8d3f7606207bdfc176df5..960419d6cb51ad72ad873c0ba903523e4df52025 100644 --- a/comments/models.py +++ b/comments/models.py @@ -17,7 +17,8 @@ from .constants import COMMENT_STATUS, STATUS_PENDING from .managers import CommentQuerySet -WARNING_TEXT = 'Warning: Rather use/edit `content_object` instead or be 100% sure you know what you are doing!' +WARNING_TEXT = ('Warning: Rather use/edit `content_object` instead or be 100% sure you' + ' know what you are doing!') US_NOTICE = 'Warning: This field is out of service and will be removed in the future.' @@ -28,9 +29,9 @@ class Comment(TimeStampedModel): status = models.SmallIntegerField(default=STATUS_PENDING, choices=COMMENT_STATUS) vetted_by = models.ForeignKey('scipost.Contributor', blank=True, null=True, on_delete=models.CASCADE, related_name='comment_vetted_by') - file_attachment = models.FileField(upload_to='uploads/comments/%Y/%m/%d/', blank=True, - validators=[validate_file_extension, validate_max_file_size] - ) + file_attachment = models.FileField( + upload_to='uploads/comments/%Y/%m/%d/', blank=True, + validators=[validate_file_extension, validate_max_file_size]) # A Comment is always related to another model # This construction implicitly has property: `on_delete=models.CASCADE` @@ -40,23 +41,6 @@ class Comment(TimeStampedModel): nested_comments = GenericRelation('comments.Comment', related_query_name='comments') - # -- U/S - # These fields will be removed in the future. - # They still exists only to prevent possible data loss. - commentary = models.ForeignKey('commentaries.Commentary', blank=True, null=True, - on_delete=models.CASCADE, help_text=US_NOTICE) - submission = models.ForeignKey('submissions.Submission', blank=True, null=True, - on_delete=models.CASCADE, related_name='comments_old', - help_text=US_NOTICE) - thesislink = models.ForeignKey('theses.ThesisLink', blank=True, null=True, - on_delete=models.CASCADE, help_text=US_NOTICE) - in_reply_to_comment = models.ForeignKey('self', blank=True, null=True, - related_name="nested_comments_old", - on_delete=models.CASCADE, help_text=US_NOTICE) - in_reply_to_report = models.ForeignKey('submissions.Report', blank=True, null=True, - on_delete=models.CASCADE, help_text=US_NOTICE) - # -- End U/S - # Author info is_author_reply = models.BooleanField(default=False) author = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE, @@ -77,6 +61,7 @@ class Comment(TimeStampedModel): remarks_for_editors = models.TextField(blank=True, verbose_name='optional remarks for the Editors only') date_submitted = models.DateTimeField('date submitted', default=timezone.now) + # Opinions nr_A = models.PositiveIntegerField(default=0) in_agreement = models.ManyToManyField('scipost.Contributor', related_name='in_agreement', diff --git a/comments/templates/comments/_comment_identifier.html b/comments/templates/comments/_comment_identifier.html index 5eb681610a2fd2e657a5d71fcea0a9d1656e16f3..9b7ac2da98169b92f313d3ddd9fa041ce0bc3d40 100644 --- a/comments/templates/comments/_comment_identifier.html +++ b/comments/templates/comments/_comment_identifier.html @@ -7,9 +7,8 @@ <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 %} - <h3>{% 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> + {% 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' }} - </h3> {% elif comment.anonymous %} Anonymous on {{comment.date_submitted|date:'Y-m-d'}} {% else %} @@ -17,7 +16,7 @@ <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 %} - {% if comment.doi_string %} <small>{{ comment|citation }}</small>{% endif %} + {% if comment.doi_string %} <small>{{ comment|citation }}</small>{% endif %} </h3> diff --git a/comments/templates/comments/_single_comment.html b/comments/templates/comments/_single_comment.html index 665503b06852f4686845577dde1c1356898f831a..5c1137c2e4223106ef50268e61fc51e1a94b724d 100644 --- a/comments/templates/comments/_single_comment.html +++ b/comments/templates/comments/_single_comment.html @@ -15,24 +15,24 @@ <p class="my-3 pb-2"> {{ comment.comment_text|linebreaksbr }} - - {% if comment.file_attachment %} - <h3>Attachment:</h3> - <p> - <a target="_blank" href="{{ comment.get_attachment_url }}"> - {% if comment.file_attachment|is_image %} - <img class="attachment attachment-comment" src="{{ comment.get_attachment_url }}"> - {% else %} - {{ comment.file_attachment|filename }}<br><small>{{ comment.file_attachment.size|filesizeformat }}</small> - {% endif %} - </a> - </p> - {% endif %} </p> + {% if comment.file_attachment %} + <h3>Attachment:</h3> + <p> + <a target="_blank" href="{{ comment.get_attachment_url }}"> + {% if comment.file_attachment|is_image %} + <img class="attachment attachment-comment" src="{{ comment.get_attachment_url }}"> + {% else %} + {{ comment.file_attachment|filename }} + {% endif %} + </a> + </p> + {% endif %} + {% if is_editorial_college or is_edcol_admin %} {% if comment.remarks_for_editors %} <h3>Remarks for editors:</h3> - <p>{{ comment.remarks_for_editors|linebreaks }}</p> + <p>{{ comment.remarks_for_editors|linebreaksbr }}</p> {% endif %} {% endif %} diff --git a/comments/test_views.py b/comments/test_views.py index c16e256953ec7be3ea87ce46c9e96f16763cb15c..16d91d5d54adf61e629689bf455898b6acbc3448 100644 --- a/comments/test_views.py +++ b/comments/test_views.py @@ -6,7 +6,7 @@ from django.http import Http404 from scipost.factories import ContributorFactory from theses.factories import ThesisLinkFactory from submissions.factories import EICassignedSubmissionFactory -from commentaries.factories import UnpublishedVettedCommentaryFactory +from commentaries.factories import UnpublishedCommentaryFactory from .factories import CommentFactory from .forms import CommentForm @@ -84,7 +84,7 @@ class TestNewComment(TestCase): """ Valid Comment gets saved """ contributor = ContributorFactory() - commentary = UnpublishedVettedCommentaryFactory() + commentary = UnpublishedCommentaryFactory() valid_comment_data = model_form_data(CommentFactory, CommentForm) target = reverse('comments:new_comment', kwargs={'object_id': commentary.id, 'type_of_object': 'commentary'}) diff --git a/common/helpers/__init__.py b/common/helpers/__init__.py index abe97df91f6222671cdcf4338f5ac1a39d0073ba..88f0fb9216d67f08929dc56e73245ed3da5fc8ee 100644 --- a/common/helpers/__init__.py +++ b/common/helpers/__init__.py @@ -32,8 +32,8 @@ def model_form_data(model, form_class, form_kwargs={}): return filter_keys(model_data, form_fields) -def random_arxiv_identifier_with_version_number(): - return random_arxiv_identifier_without_version_number() + "v0" +def random_arxiv_identifier_with_version_number(version_nr='0'): + return random_arxiv_identifier_without_version_number() + 'v' + str(version_nr) def random_arxiv_identifier_without_version_number(): @@ -44,15 +44,25 @@ def random_scipost_journal(): return random.choice(SCIPOST_JOURNALS_SUBMIT)[0] -def random_external_journal(): +def random_external_journal_abbrev(): return random.choice(( - 'PhysRevA.', - 'PhysRevB.', - 'PhysRevC.', - 'nature.' - 'S0550-3213(01)', - '1742-5468/', - '0550-3213(96)' + 'Ann. Phys.', + 'Phys. Rev. A', + 'Phys. Rev. B', + 'Phys. Rev. C', + 'Phys. Rev. Lett.', + 'Europhys. Lett.', + 'J. Math. Anal. Appl.', + 'Nat. Phys.' + 'J. Phys. A', + 'J. Stat. Phys.', + 'J. Stat. Mech.', + 'J. Math. Phys.', + 'Lett. Math. Phys.', + 'Sov. Phys. JETP', + 'Sov. Phys. JETP', + 'Nucl. Phys. B', + 'Adv. Phys.' )) @@ -64,14 +74,41 @@ def random_scipost_doi(): return '10.21468/%s.%s' % (random_scipost_journal(), random_pub_number()) +def random_scipost_report_doi_label(): + return 'SciPost.Report.%s' % random_digits(4) + + def random_external_doi(): - return '10.%s/%s%s' % (random_digits(5), random_external_journal(), random_pub_number()) + """ + Return a fake/random doi as if all journal abbrev and pub_number are separated by `.`, which + can be helpfull for testing purposes. + """ + journal = random.choice(( + 'PhysRevA', + 'PhysRevB', + 'PhysRevC', + 'PhysRevLett', + 'nature' + 'S0550-3213(01)', + '1742-5468', + '0550-3213(96)' + )) + return '10.%s/%s.%s' % (random_digits(5), journal, random_pub_number()) def random_digits(n): return "".join(random.choice(string.digits) for _ in range(n)) +def generate_orcid(): + return '{}-{}-{}-{}'.format( + random_digits(4), + random_digits(4), + random_digits(4), + random_digits(4), + ) + + def filter_keys(dictionary, keys_to_keep): # Field is empty if not on model. return {key: dictionary.get(key, "") for key in keys_to_keep} diff --git a/journals/admin.py b/journals/admin.py index 955fef5eddbb325a49a02e51f3796c06b4c4d0f8..956df25fea384d0aa0c737c6ab49740e2a2779f2 100644 --- a/journals/admin.py +++ b/journals/admin.py @@ -55,6 +55,7 @@ class PublicationAdminForm(forms.ModelForm): class ReferenceInline(admin.TabularInline): model = Reference + extra = 0 class AuthorsInline(admin.TabularInline): diff --git a/journals/factories.py b/journals/factories.py index e6b9ef92328909e0a8826e93e4cd34113c8a0989..a49839f1e1b195f3e5822ba6f14ecd3a8b1553b6 100644 --- a/journals/factories.py +++ b/journals/factories.py @@ -1,18 +1,37 @@ import factory import datetime import pytz +import random -from django.utils import timezone - -from common.helpers import random_digits -from journals.constants import SCIPOST_JOURNALS +from common.helpers import random_digits, random_external_doi, random_external_journal_abbrev +from journals.constants import SCIPOST_JOURNALS, SCIPOST_JOURNAL_PHYSICS_LECTURE_NOTES,\ + ISSUES_AND_VOLUMES, INDIVIDUAL_PUBLCATIONS, PUBLICATION_PUBLISHED from submissions.factories import PublishedSubmissionFactory -from .models import Journal, Volume, Issue, Publication +from .models import Journal, Volume, Issue, Publication, Reference from faker import Faker +class ReferenceFactory(factory.django.DjangoModelFactory): + reference_number = factory.LazyAttribute(lambda o: o.publication.references.count() + 1) + identifier = factory.lazy_attribute(lambda n: random_external_doi()) + link = factory.Faker('uri') + + class Meta: + model = Reference + + @factory.lazy_attribute + def citation(self): + faker = Faker() + return '<em>{}</em> {} <b>{}</b>, {} ({})'.format( + faker.sentence(), + random_external_journal_abbrev(), + random.randint(1, 100), + random.randint(1, 100), + faker.year()) + + class JournalFactory(factory.django.DjangoModelFactory): name = factory.Iterator(SCIPOST_JOURNALS, getter=lambda c: c[0]) doi_label = factory.Iterator(SCIPOST_JOURNALS, getter=lambda c: c[0]) @@ -20,25 +39,21 @@ class JournalFactory(factory.django.DjangoModelFactory): class Meta: model = Journal - django_get_or_create = ('name', 'doi_label',) + django_get_or_create = ('name',) + + @factory.lazy_attribute + def structure(self): + if self.name == SCIPOST_JOURNAL_PHYSICS_LECTURE_NOTES: + return INDIVIDUAL_PUBLCATIONS + return ISSUES_AND_VOLUMES class VolumeFactory(factory.django.DjangoModelFactory): in_journal = factory.SubFactory(JournalFactory) - number = 9999 - doi_label = factory.Faker('md5') - - @factory.post_generation - def doi(self, create, extracted, **kwargs): - self.number = self.in_journal.volume_set.count() - self.doi_label = self.in_journal.doi_label + '.' + str(self.number) - - @factory.post_generation - def dates(self, create, extracted, **kwargs): - timezone.now() - self.start_date = Faker().date_time_between(start_date="-3y", end_date="now", - tzinfo=pytz.UTC) - self.until_date = self.start_date + datetime.timedelta(weeks=26) + doi_label = factory.lazy_attribute(lambda o: '%s.%i' % (o.in_journal.doi_label, o.number)) + number = factory.lazy_attribute(lambda o: o.in_journal.volumes.count() + 1) + start_date = factory.Faker('date_time_this_decade') + until_date = factory.lazy_attribute(lambda o: o.start_date + datetime.timedelta(weeks=26)) class Meta: model = Volume @@ -47,21 +62,12 @@ class VolumeFactory(factory.django.DjangoModelFactory): class IssueFactory(factory.django.DjangoModelFactory): in_volume = factory.Iterator(Volume.objects.all()) - number = 9999 - doi_label = factory.Faker('md5') + number = factory.LazyAttribute(lambda o: o.in_volume.issues.count() + 1) + doi_label = factory.LazyAttribute(lambda o: '%s.%i' % (o.in_volume.doi_label, o.number)) - @factory.post_generation - def doi(self, create, extracted, **kwargs): - self.number = self.in_volume.issue_set.count() - self.doi_label = self.in_volume.doi_label + '.' + str(self.number) - - @factory.post_generation - def dates(self, create, extracted, **kwargs): - timezone.now() - self.start_date = Faker().date_time_between(start_date=self.in_volume.start_date, - end_date=self.in_volume.until_date, - tzinfo=pytz.UTC) - self.until_date = self.start_date + datetime.timedelta(weeks=4) + start_date = factory.LazyAttribute(lambda o: Faker().date_time_between( + start_date=o.in_volume.start_date, end_date=o.in_volume.until_date, tzinfo=pytz.UTC)) + until_date = factory.LazyAttribute(lambda o: o.start_date + datetime.timedelta(weeks=4)) class Meta: model = Issue @@ -69,42 +75,99 @@ class IssueFactory(factory.django.DjangoModelFactory): class PublicationFactory(factory.django.DjangoModelFactory): - accepted_submission = factory.SubFactory(PublishedSubmissionFactory) + accepted_submission = factory.SubFactory( + PublishedSubmissionFactory, generate_publication=False) paper_nr = 9999 - pdf_file = Faker().file_name(extension='pdf') - in_issue = factory.Iterator(Issue.objects.all()) - submission_date = factory.Faker('date') - acceptance_date = factory.Faker('date') - publication_date = factory.Faker('date') - doi_label = factory.Faker('md5') + pdf_file = factory.Faker('file_name', extension='pdf') + status = PUBLICATION_PUBLISHED + submission_date = factory.Faker('date_this_year') + acceptance_date = factory.Faker('date_this_year') + publication_date = factory.Faker('date_this_year') + + discipline = factory.LazyAttribute(lambda o: o.accepted_submission.discipline) + domain = factory.LazyAttribute(lambda o: o.accepted_submission.domain) + subject_area = factory.LazyAttribute(lambda o: o.accepted_submission.subject_area) + title = factory.LazyAttribute(lambda o: o.accepted_submission.title) + abstract = factory.LazyAttribute(lambda o: o.accepted_submission.abstract) + + # Dates + submission_date = factory.LazyAttribute(lambda o: o.accepted_submission.submission_date) + acceptance_date = factory.LazyAttribute(lambda o: o.accepted_submission.latest_activity) + publication_date = factory.LazyAttribute(lambda o: o.accepted_submission.latest_activity) + latest_activity = factory.LazyAttribute(lambda o: o.accepted_submission.latest_activity) + + # Authors + author_list = factory.LazyAttribute(lambda o: o.accepted_submission.author_list) + + class Meta: + model = Publication + django_get_or_create = ('accepted_submission', ) + + class Params: + journal = None + + @factory.lazy_attribute + def in_issue(self): + # Make sure Issues, Journals and doi are correct. + if self.journal: + journal = Journal.objects.get(name=self.journal) + else: + journal = Journal.objects.order_by('?').first() + + if journal.has_issues: + return Issue.objects.for_journal(journal.name).order_by('?').first() + return None + + @factory.lazy_attribute + def in_journal(self): + # Make sure Issues, Journals and doi are correct. + if self.journal: + journal = Journal.objects.get(name=self.journal) + elif not self.in_issue: + journal = Journal.objects.has_individual_publications().order_by('?').first() + else: + return None + + if not journal.has_issues: + # Keep this logic in case self.journal is set. + return journal + return None + + @factory.lazy_attribute + def paper_nr(self): + if self.in_issue: + return self.in_issue.publications.count() + 1 + elif self.in_journal: + return self.in_journal.publications.count() + 1 + + @factory.lazy_attribute + def doi_label(self): + if self.in_issue: + return self.in_issue.doi_label + '.' + str(self.paper_nr).rjust(3, '0') + elif self.in_journal: + return '%s.%i' % (self.in_journal.doi_label, self.paper_nr) @factory.post_generation - def doi(self, create, extracted, **kwargs): - paper_nr = self.in_issue.publications.count() - self.paper_nr = paper_nr - self.doi_label = self.in_issue.doi_label + '.' + str(paper_nr).rjust(3, '0') + def generate_publication(self, create, extracted, **kwargs): + if create and extracted is not False: + return + + from journals.factories import PublicationFactory + factory.RelatedFactory( + PublicationFactory, 'accepted_submission', + title=self.title, author_list=self.author_list) @factory.post_generation - def submission_data(self, create, extracted, **kwargs): - # Content - self.discipline = self.accepted_submission.discipline - self.domain = self.accepted_submission.domain - self.subject_area = self.accepted_submission.subject_area - self.title = self.accepted_submission.title - self.abstract = self.accepted_submission.abstract - - # Authors - self.author_list = self.accepted_submission.author_list - # self.authors.add(*self.accepted_submission.authors.all()) - self.authors_claims.add(*self.accepted_submission.authors_claims.all()) - self.authors_false_claims.add(*self.accepted_submission.authors_false_claims.all()) + def author_relations(self, create, extracted, **kwargs): + if not create: + return - # Dates - self.submission_date = self.accepted_submission.latest_activity - self.acceptance_date = self.accepted_submission.latest_activity - self.publication_date = self.accepted_submission.latest_activity - self.latest_activity = self.accepted_submission.latest_activity + # Append references + for i in range(5): + ReferenceFactory(publication=self) - class Meta: - model = Publication - django_get_or_create = ('accepted_submission', ) + # Copy author data from Submission + for author in self.accepted_submission.authors.all(): + self.authors.create(publication=self, contributor=author) + self.authors_claims.add(*self.accepted_submission.authors_claims.all()) + self.authors_false_claims.add(*self.accepted_submission.authors_false_claims.all()) diff --git a/journals/forms.py b/journals/forms.py index 4464d567a65e5ad49d7d0870b78f1f807c35e853..0c6ea15bcd423d55e652191e2e2041357cafd240 100644 --- a/journals/forms.py +++ b/journals/forms.py @@ -16,6 +16,7 @@ from .constants import STATUS_DRAFT, PUBLICATION_PREPUBLISHED, PUBLICATION_PUBLI from .exceptions import PaperNumberingError from .models import Issue, Publication, Reference, UnregisteredAuthor, PublicationAuthorsTable from .utils import JournalUtils +from .signals import notify_manuscript_published from funders.models import Grant, Funder @@ -83,6 +84,20 @@ class FundingInfoForm(forms.ModelForm): return super().save(*args, **kwargs) +class BasePublicationAuthorsTableFormSet(BaseModelFormSet): + def save(self, *args, **kwargs): + objects = super().save(*args, **kwargs) + for form in self.ordered_forms: + form.instance.order = form.cleaned_data['ORDER'] + form.instance.save() + return objects + + +PublicationAuthorOrderingFormSet = modelformset_factory( + PublicationAuthorsTable, fields=(), can_order=True, extra=0, + formset=BasePublicationAuthorsTableFormSet) + + class CreateMetadataXMLForm(forms.ModelForm): class Meta: model = Publication @@ -638,4 +653,6 @@ class PublicationPublishForm(RequestFormMixin, forms.ModelForm): # Email authors JournalUtils.load({'publication': self.instance}) JournalUtils.send_authors_paper_published_email() + notify_manuscript_published(self.request.user, self.instance, False) + return self.instance diff --git a/journals/management/__init__.py b/journals/management/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/journals/management/commands/__init__.py b/journals/management/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/journals/management/commands/create_issues.py b/journals/management/commands/create_issues.py new file mode 100644 index 0000000000000000000000000000000000000000..ea07e6ac7ca831da3148b22110b9ca7e4a28576f --- /dev/null +++ b/journals/management/commands/create_issues.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand + +from journals import factories + + +class Command(BaseCommand): + help = 'Create Issue objects using the factories.' + + def add_arguments(self, parser): + parser.add_argument( + 'number', action='store', default=0, type=int, + help='Number of Issues to add') + + def handle(self, *args, **kwargs): + self.create_issues(kwargs['number']) + + def create_issues(self, n): + factories.IssueFactory.create_batch(n) + self.stdout.write(self.style.SUCCESS('Successfully created {n} Issues.'.format(n=n))) diff --git a/journals/management/commands/create_journals.py b/journals/management/commands/create_journals.py new file mode 100644 index 0000000000000000000000000000000000000000..eaaa425050f9016ea0ed2ca9dde902b553bad262 --- /dev/null +++ b/journals/management/commands/create_journals.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand + +from journals import factories + + +class Command(BaseCommand): + help = 'Create Journal objects using the factories.' + + def add_arguments(self, parser): + parser.add_argument( + 'number', action='store', default=0, type=int, + help='Number of Journals to add') + + def handle(self, *args, **kwargs): + self.create_journals(kwargs['number']) + + def create_journals(self, n): + factories.JournalFactory.create_batch(n) + self.stdout.write(self.style.SUCCESS('Successfully created {n} Journals.'.format(n=n))) diff --git a/journals/management/commands/create_publications.py b/journals/management/commands/create_publications.py new file mode 100644 index 0000000000000000000000000000000000000000..24c2f0480db625b119939ba394fb73d7f11901ab --- /dev/null +++ b/journals/management/commands/create_publications.py @@ -0,0 +1,30 @@ +from django.core.management.base import BaseCommand + +from journals.constants import SCIPOST_JOURNALS_SUBMIT +from journals.factories import PublicationFactory + + +class Command(BaseCommand): + help = 'Create random Publication objects by using the factories.' + + def add_arguments(self, parser): + parser.add_argument( + 'number', action='store', default=0, type=int, + help='Number of publications to add', + ) + parser.add_argument( + '--journal', choices=[i[0] for i in SCIPOST_JOURNALS_SUBMIT], + action='store', dest='journal', + help='The name of the specific Journal to add the Publications to', + ) + + def handle(self, *args, **kwargs): + if kwargs['number'] > 0: + journal = None + if kwargs.get('journal'): + journal = kwargs['journal'] + self.create_publications(kwargs['number'], journal=journal) + + def create_publications(self, n, journal=None): + PublicationFactory.create_batch(n, journal=journal) + self.stdout.write(self.style.SUCCESS('Successfully created {n} Publications.'.format(n=n))) diff --git a/journals/management/commands/create_volumes.py b/journals/management/commands/create_volumes.py new file mode 100644 index 0000000000000000000000000000000000000000..29dce2f400ace4310f7996774503965260707425 --- /dev/null +++ b/journals/management/commands/create_volumes.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand + +from journals import factories + + +class Command(BaseCommand): + help = 'Create Volume objects using the factories.' + + def add_arguments(self, parser): + parser.add_argument( + 'number', action='store', default=0, type=int, + help='Number of Volumes to add') + + def handle(self, *args, **kwargs): + self.create_volumes(kwargs['number']) + + def create_volumes(self, n): + factories.VolumeFactory.create_batch(n) + self.stdout.write(self.style.SUCCESS('Successfully created {n} Volumes.'.format(n=n))) diff --git a/journals/migrations/0025_auto_20180314_1637.py b/journals/migrations/0025_auto_20180314_1637.py new file mode 100644 index 0000000000000000000000000000000000000000..35b306e63723cd7b0b0ec6db28a0d0c5317b7c59 --- /dev/null +++ b/journals/migrations/0025_auto_20180314_1637.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-03-14 15:37 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0024_auto_20180310_1740'), + ] + + operations = [ + migrations.AlterField( + model_name='issue', + name='in_journal', + field=models.ForeignKey(blank=True, help_text='Assign either an Volume or Journal to the Issue', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='issues', to='journals.Journal'), + ), + migrations.AlterField( + model_name='issue', + name='in_volume', + field=models.ForeignKey(blank=True, help_text='Assign either an Volume or Journal to the Issue', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='issues', to='journals.Volume'), + ), + migrations.AlterField( + model_name='publication', + name='in_issue', + field=models.ForeignKey(blank=True, help_text='Assign either an Issue or Journal to the Publication', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='publications', to='journals.Issue'), + ), + migrations.AlterField( + model_name='publication', + name='in_journal', + field=models.ForeignKey(blank=True, help_text='Assign either an Issue or Journal to the Publication', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='publications', to='journals.Journal'), + ), + ] diff --git a/journals/models.py b/journals/models.py index 5d0ec94f68761b269f33a91843cc3a9474fad4e3..a7c846e42bba6ecf7c0a37b00cbbe046ece1eaae 100644 --- a/journals/models.py +++ b/journals/models.py @@ -224,10 +224,10 @@ class Issue(models.Model): An Issue may be used as a subgroup of Publications related to a specific Journal object. """ in_journal = models.ForeignKey( - 'journals.Journal', on_delete=models.PROTECT, null=True, blank=True, + 'journals.Journal', on_delete=models.CASCADE, null=True, blank=True, help_text='Assign either an Volume or Journal to the Issue') in_volume = models.ForeignKey( - 'journals.Volume', on_delete=models.PROTECT, null=True, blank=True, + 'journals.Volume', on_delete=models.CASCADE, null=True, blank=True, help_text='Assign either an Volume or Journal to the Issue') number = models.PositiveSmallIntegerField() start_date = models.DateField(default=timezone.now) @@ -341,10 +341,10 @@ class Publication(models.Model): accepted_submission = models.OneToOneField('submissions.Submission', on_delete=models.CASCADE, related_name='publication') in_issue = models.ForeignKey( - 'journals.Issue', on_delete=models.PROTECT, null=True, blank=True, + 'journals.Issue', on_delete=models.CASCADE, null=True, blank=True, help_text='Assign either an Issue or Journal to the Publication') in_journal = models.ForeignKey( - 'journals.Journal', on_delete=models.PROTECT, null=True, blank=True, + 'journals.Journal', on_delete=models.CASCADE, null=True, blank=True, help_text='Assign either an Issue or Journal to the Publication') paper_nr = models.PositiveSmallIntegerField() status = models.CharField(max_length=8, diff --git a/journals/signals.py b/journals/signals.py new file mode 100644 index 0000000000000000000000000000000000000000..01787e588d96919588b5c32ed3f9dbaf4256b46f --- /dev/null +++ b/journals/signals.py @@ -0,0 +1,17 @@ +from django.contrib.auth.models import User, Group + +from notifications.signals import notify + + +def notify_manuscript_published(sender, instance, created, **kwargs): + """ + Notify the authors about their new Publication. + + instance -- Publication instance + """ + if instance.is_published: + authors = User.objects.filter(contributor__publications=instance) + editorial_administration = Group.objects.get(name='Editorial Administrators') + for user in authors: + notify.send(sender=sender, recipient=user, actor=editorial_administration, + verb=' published your manuscript.', target=instance) diff --git a/journals/templates/journals/manage_metadata.html b/journals/templates/journals/manage_metadata.html index 08ae88c8079ddfdbc104961abab2b010b75adc7d..8b0554e06f17e37f8d007205bf55727f44c11bc2 100644 --- a/journals/templates/journals/manage_metadata.html +++ b/journals/templates/journals/manage_metadata.html @@ -103,16 +103,8 @@ event: "focusin" <div class="col-md-6"> <h2 class="ml-3">Actions</h2> <ul> - <li>Mark the first author - <ul class="list-unstyled pl-4"> - {% for author in publication.authors.all %} - <li> - {{ author.order }}. <a href="{% url 'journals:mark_first_author' doi_label=publication.doi_label author_object_id=author.id %}">{{ author }}</a> - </li> - {% endfor %} - </ul> - </li> <li><a href="{% url 'journals:add_author' doi_label=publication.doi_label %}">Add a missing author</a></li> + <li><a href="{% url 'journals:update_author_ordering' doi_label=publication.doi_label %}">Update Author ordering</a></li> <li><a href="{% url 'journals:create_citation_list_metadata' publication.doi_label %}">Create/update citation list metadata</a></li> <li><a href="{% url 'journals:create_funding_info_metadata' publication.doi_label %}">Create/update funding info metadata</a></li> diff --git a/journals/templates/journals/publication_authors_form.html b/journals/templates/journals/publication_authors_form.html new file mode 100644 index 0000000000000000000000000000000000000000..861f712b9e0e4a275ba9b436757658c8352346ac --- /dev/null +++ b/journals/templates/journals/publication_authors_form.html @@ -0,0 +1,47 @@ +{% extends 'scipost/base.html' %} + +{% load bootstrap %} + +{% block pagetitle %}: Publication Authors{% endblock pagetitle %} + +{% block breadcrumb %} + <div class="container-outside header"> + <div class="container"> + <nav class="breadcrumb hidden-sm-down"> + <a href="{% url 'journals:journals' %}" class="breadcrumb-item">Journals</a> + <a href="{{publication.get_absolute_url}}" class="breadcrumb-item">{{publication.citation}}</a> + <span class="breadcrumb-item active">Author ordering</span> + + </nav> + </div> + </div> +{% endblock %} + +{% block content %} + + +<h1 class="highlight">Author Ordering</h1> + +<div class="mb-4"> + {% include 'partials/journals/publication_li_content.html' with publication=publication %} +</div> +<a href="{% url 'journals:add_author' publication.doi_label %}">Add missing author</a> +<h3 class="highlight">Ordering</h3> + +<form method="post" enctype="multipart/form-data"> + {% csrf_token %} + {{ formset.management_form }} + <ul class="fa-ul sortable-list d-inline-block"> + {% for form in formset %} + <li> + <i class="fa fa-sort"></i> + {{ form.instance.first_name }} {{ form.instance.last_name }} + <div class="d-none">{{ form }}</div> + </li> + {% endfor %} + </ul> + <br> + <input type="submit" class="btn btn-primary" value="Save ordering"> +</form> + +{% endblock %} diff --git a/journals/templates/journals/publication_detail.html b/journals/templates/journals/publication_detail.html index 20f08ba694ec77c6fc6d9d491eaf3333a274596e..c2f1e27c069cdc2a85fc35af99714f296fe7b0d3 100644 --- a/journals/templates/journals/publication_detail.html +++ b/journals/templates/journals/publication_detail.html @@ -172,17 +172,8 @@ <div class="col-12"> <h3>Editorial Administration tools</h3> <ul class="mb-0"> - <li> - Mark the first author - <ul class="list-unstyled pl-4"> - {% for author in publication.authors.all %} - <li> - {{ author.order }}. <a href="{% url 'journals:mark_first_author' doi_label=publication.doi_label author_object_id=author.id %}">{{ author }}</a> - </li> - {% endfor %} - </ul> - </li> <li><a href="{% url 'journals:add_author' doi_label=publication.doi_label %}">Add a missing author</a></li> + <li><a href="{% url 'journals:update_author_ordering' doi_label=publication.doi_label %}">Update Author ordering</a></li> <li><a href="{% url 'journals:create_citation_list_metadata' publication.doi_label %}">Create/update citation list metadata</a></li> <li><a href="{% url 'journals:create_funding_info_metadata' publication.doi_label %}">Create/update funding info metadata</a></li> <li><a href="{% url 'journals:create_metadata_xml' publication.doi_label %}">Create/update the XML metadata</a></li> diff --git a/journals/urls/general.py b/journals/urls/general.py index 1e3442302505823c1c8aee16753532eb21cb75cf..7d4430e1b5def101440e9dc01e32a73490959222 100644 --- a/journals/urls/general.py +++ b/journals/urls/general.py @@ -32,6 +32,10 @@ urlpatterns = [ regex=PUBLICATION_DOI_REGEX), journals_views.DraftPublicationApprovalView.as_view(), name='send_publication_for_approval'), + url(r'^admin/publications/(?P<doi_label>{regex})/authors$'.format(regex=PUBLICATION_DOI_REGEX), + # journals_views.PublicationAuthorOrderingView.as_view(), + journals_views.publication_authors_ordering, + name='update_author_ordering'), url(r'^admin/publications/(?P<doi_label>{regex})/grants$'.format(regex=PUBLICATION_DOI_REGEX), journals_views.PublicationGrantsView.as_view(), name='update_grants'), @@ -48,10 +52,6 @@ urlpatterns = [ url(r'^admin/(?P<doi_label>{regex})/authors/add$'.format(regex=PUBLICATION_DOI_REGEX), journals_views.add_author, name='add_author'), - url(r'^admin/(?P<doi_label>{regex})/authors/mark_first/(?P<author_object_id>[0-9]+)$'.format( - regex=PUBLICATION_DOI_REGEX), - journals_views.mark_first_author, - name='mark_first_author'), url(r'^admin/(?P<doi_label>{regex})/manage_metadata$'.format(regex=PUBLICATION_DOI_REGEX), journals_views.manage_metadata, name='manage_metadata'), diff --git a/journals/views.py b/journals/views.py index 465dedd616866150242c10137931f2ad1243d366..f4a338d5d836be9a6ae9f748dd209bb1b818c5fc 100644 --- a/journals/views.py +++ b/journals/views.py @@ -30,7 +30,8 @@ from .models import Journal, Issue, Publication, Deposit, DOAJDeposit,\ from .forms import FundingInfoForm,\ UnregisteredAuthorForm, CreateMetadataXMLForm, CitationListBibitemsForm,\ ReferenceFormSet, CreateMetadataDOAJForm, DraftPublicationForm,\ - PublicationGrantsForm, DraftPublicationApprovalForm, PublicationPublishForm + PublicationGrantsForm, DraftPublicationApprovalForm, PublicationPublishForm,\ + PublicationAuthorOrderingFormSet from .mixins import PublicationMixin, ProdSupervisorPublicationPermissionMixin from .utils import JournalUtils @@ -212,6 +213,22 @@ class PublicationGrantsRemovalView(PermissionsMixin, DetailView): return redirect(reverse('journals:update_grants', args=(self.object.doi_label,))) +@permission_required('scipost.can_publish_accepted_submission', raise_exception=True) +def publication_authors_ordering(request, doi_label): + publication = get_object_or_404(Publication, doi_label=doi_label) + formset = PublicationAuthorOrderingFormSet( + request.POST or None, queryset=publication.authors.order_by('order')) + if formset.is_valid(): + formset.save() + messages.success(request, 'Author ordering updated') + return redirect(publication.get_absolute_url()) + context = { + 'formset': formset, + 'publication': publication, + } + return render(request, 'journals/publication_authors_form.html', context) + + class DraftPublicationUpdateView(PermissionsMixin, UpdateView): """ Any Production Officer or Administrator can draft a new publication without publishing here. @@ -306,25 +323,6 @@ def manage_metadata(request, doi_label=None, issue_doi_label=None, journal_doi_l return render(request, 'journals/manage_metadata.html', context) -@permission_required('scipost.can_publish_accepted_submission', return_403=True) -def mark_first_author(request, publication_id, author_object_id): - publication = get_object_or_404(Publication, id=publication_id) - author_object = get_object_or_404(publication.authors, id=author_object_id) - - # Redo ordering - author_object.order = 1 - author_object.save() - author_objects = publication.authors.exclude(id=author_object.id) - count = 2 - for author in author_objects: - author.order = count - author.save() - count += 1 - messages.success(request, 'Marked {} first author'.format(author_object)) - return redirect(reverse('journals:manage_metadata', - kwargs={'doi_label': publication.doi_label})) - - @permission_required('scipost.can_draft_publication', return_403=True) @transaction.atomic def add_author(request, doi_label, contributor_id=None, unregistered_author_id=None): diff --git a/news/factories.py b/news/factories.py index 74dcad74481d0c0de0e9dfeaabba153673a29723..a17229133a154ccd27117e135fac715a522ab54d 100644 --- a/news/factories.py +++ b/news/factories.py @@ -7,8 +7,8 @@ class NewsItemFactory(factory.django.DjangoModelFactory): class Meta: model = NewsItem - date = factory.Faker('date_time') - headline = factory.Faker('sentence', nb_words=6) - blurb = factory.Faker('text', max_nb_chars=200) - followup_link = factory.Faker('url') + date = factory.Faker('date_this_year') + headline = factory.Faker('sentence') + blurb = factory.Faker('paragraph', nb_sentences=8) + followup_link = factory.Faker('uri') followup_link_text = factory.Faker('sentence', nb_words=4) diff --git a/news/management/__init__.py b/news/management/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/news/management/commands/__init__.py b/news/management/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/news/management/commands/create_news.py b/news/management/commands/create_news.py new file mode 100644 index 0000000000000000000000000000000000000000..96e170762e48e6a2b57aa4e741786bff6b7247b2 --- /dev/null +++ b/news/management/commands/create_news.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand + +from news import factories + + +class Command(BaseCommand): + help = 'Create random News Item objects using the factories.' + + def add_arguments(self, parser): + parser.add_argument( + 'number', action='store', default=0, type=int, + help='Number of News items to add') + + def handle(self, *args, **kwargs): + self.create_news_items(kwargs['number']) + + def create_news_items(self, n): + factories.NewsItemFactory.create_batch(n) + self.stdout.write(self.style.SUCCESS('Successfully created {n} News Items.'.format(n=n))) diff --git a/notifications/models.py b/notifications/models.py index e05b53710ade32063ef292107119e6f9b0d4835c..f6dfb21ea25c0250531b17f3a074eb617904d1ac 100644 --- a/notifications/models.py +++ b/notifications/models.py @@ -3,7 +3,6 @@ from django.core.urlresolvers import reverse from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey -from django.utils import timezone from .constants import NOTIFICATION_TYPES from .managers import NotificationQuerySet diff --git a/notifications/templates/notifications/partials/notification_list_popover.html b/notifications/templates/notifications/partials/notification_list_popover.html new file mode 100644 index 0000000000000000000000000000000000000000..9ce9cd38d7f56fd8e789542e662ea487d19f10d3 --- /dev/null +++ b/notifications/templates/notifications/partials/notification_list_popover.html @@ -0,0 +1,52 @@ +{% load request_filters %} + +<div class="popover-template popover"> + <div class="popover notifications" role="tooltip"> + <div class="arrow"></div> + {% if user.contributor %} + <div class="header"> + <h3>{{ user.contributor.get_title_display }} {{ user.first_name }} {{ user.last_name }}</h3> + <a class="item" href="{% url 'scipost:update_personal_data' %}"><i class="fa fa-gear"></i> Update personal data</a> + </div> + {% if not user.contributor.is_currently_available %} + <div class="unavailable"> + <div class="head">You are currently unavailable</div> + <div class="text">Check your availability in your personal page if this should not be the case.</div> + </div> + {% endif %} + {% else %} + <div class="header"> + <h3>{{ user.first_name }} {{ user.last_name }}</h3> + </div> + {% endif %} + + <div class="links"> + <a class="item {% active 'scipost:personal_page' %}" href="{% url 'scipost:personal_page' %}">Personal Page</a> + {% if user.partner_contact or perms.scipost.can_read_partner_page %} + <a class="item {% active 'partners:dashboard' %}" href="{% url 'partners:dashboard' %}">Partner Page</a> + {% endif %} + + {% if perms.scipost.can_view_timesheets %} + <a class="item {% active 'finances:finance' %}" href="{% url 'finances:finance' %}">Financial Administration</a> + {% endif %} + + {% if perms.scipost.can_view_all_funding_info %} + <a class="item {% active 'funders:funders' %}" href="{% url 'funders:funders' %}">Funders</a> + {% endif %} + + {% if perms.scipost.can_view_production %} + <a class="item {% active 'production:production' %}" href="{% url 'production:production' %}">Production</a> + {% endif %} + + {% if perms.scipost.can_view_pool %} + <a class="item {% active 'submissions:pool' %}" href="{% url 'submissions:pool' %}">Submissions Pool</a> + {% endif %} + + <a class="item" href="{% url 'scipost:logout' %}">Logout</a> + </div> + + <h4 class="inbox-header">Inbox</h4> + <div class="live_notify_list"></div> + </div> + <div class="popover-body"></div> +</div> diff --git a/notifications/templatetags/notifications_tags.py b/notifications/templatetags/notifications_tags.py index 8d8bccd2402cd1f213000b4597ba82bc67390833..a89cafee93cf6d8501328fe71fb7958e3557c623 100644 --- a/notifications/templatetags/notifications_tags.py +++ b/notifications/templatetags/notifications_tags.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from django.core.urlresolvers import reverse from django.template import Library +from django.template.loader import render_to_string from django.utils.html import format_html register = Library() @@ -18,44 +18,12 @@ def live_notify_list(context): if not user: return '' - html = '<div class="popover-template popover">' - html += '<div class="popover notifications" role="tooltip">' - - # User default links - html += '<h6 class="header">Welcome {first_name} {last_name}</h6>'.format( - first_name=user.first_name, last_name=user.last_name) - - if hasattr(user, 'contributor'): - html += '<a class="item" href="{url}">Personal Page</a>'.format( - url=reverse('scipost:personal_page')) - - # User specific links - if user.has_perm('scipost.can_read_partner_page'): - html += '<a class="item" href="{url}">Partner Page</a>'.format( - url=reverse('partners:dashboard')) - if user.has_perm('scipost.can_view_timesheets'): - html += '<a class="item" href="{url}">Financial Administration</a>'.format( - url=reverse('finances:finance')) - if user.has_perm('scipost.can_view_all_funding_info'): - html += '<a class="item" href="{url}">Funders</a>'.format( - url=reverse('funders:funders')) - if user.has_perm('scipost.can_view_production'): - html += '<a class="item" href="{url}">Production</a>'.format( - url=reverse('production:production')) - if user.has_perm('scipost.can_view_pool'): - html += '<a class="item" href="{url}">Submission Pool</a>'.format( - url=reverse('submissions:pool')) - - # Logout links - html += '<div class="divider"></div>' - html += '<a class="item" href="{url}">Logout</a>'.format( - url=reverse('scipost:logout')) - - # Notifications - html += '<div class="divider"></div><h6 class="header">Inbox</h6>' - html += '<div class="live_notify_list"></div></div>' - html += '<div class="popover-body"></div></div>' - return format_html(html) + request = context['request'] + context = { + 'user': user, + } + return render_to_string('notifications/partials/notification_list_popover.html', + context, request=request) def user_context(context): diff --git a/notifications/views.py b/notifications/views.py index b36eaab4806253da91d5c18cfc1aa033983249b9..8728b683a0639b1230570b53d6139098809764f9 100644 --- a/notifications/views.py +++ b/notifications/views.py @@ -60,7 +60,7 @@ def live_notification_list(request): try: # Default to 5 as a max number of notifications - num_to_fetch = max(int(request.GET.get('max', 5)), 1) + num_to_fetch = max(int(request.GET.get('max', 10)), 1) num_to_fetch = min(num_to_fetch, 100) except ValueError: num_to_fetch = 5 diff --git a/package.json b/package.json index 9e3d508dcb107b4bbfa981744c683f1089a08a11..37b40615f359601885e9a941ce88f79c1691d868 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "extract-text-webpack-plugin": "^3.0.0", "file-loader": "^0.11.2", "imports-loader": "^0.7.1", - "jquery": "^2.2.0", + "jquery": "^3.3.1", + "jquery-ui": "^1.12.1", "node-loader": "^0.6.0", "node-sass": "^4.4.0", "popper.js": "^1.11.1", diff --git a/requirements.txt b/requirements.txt index 21531ce520116fba8000b6c04f2201a7bb7ee953..0328daa6b14b1f1ef889a4db82427391e2cb817f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,9 +32,8 @@ sphinx-rtd-theme==0.1.9 # Sphinx theme # Testing -factory-boy==2.9.2 -Faker==0.7.18 -fake-factory==0.7.2 # Old version of Faker package +factory-boy==2.10.0 +Faker==0.8.12 # Django Utils diff --git a/scipost/factories.py b/scipost/factories.py index 67cb4e2df31821db68e780bbf44fef72d83636c2..0e3f561745d184e402192cf4de0c5d5a1f88ad28 100644 --- a/scipost/factories.py +++ b/scipost/factories.py @@ -4,31 +4,36 @@ import random from django.contrib.auth import get_user_model from django.contrib.auth.models import Group +from common.helpers import generate_orcid from submissions.models import Submission from .models import Contributor, EditorialCollege, EditorialCollegeFellowship, Remark from .constants import TITLE_CHOICES, SCIPOST_SUBJECT_AREAS -from django_countries.data import COUNTRIES -from faker import Faker - class ContributorFactory(factory.django.DjangoModelFactory): - title = random.choice(list(dict(TITLE_CHOICES).keys())) + title = factory.Iterator(TITLE_CHOICES, getter=lambda c: c[0]) user = factory.SubFactory('scipost.factories.UserFactory', contributor=None) status = 1 # normal user - vetted_by = factory.SubFactory('scipost.factories.ContributorFactory', vetted_by=None) - personalwebpage = factory.Faker('url') - country_of_employment = factory.Iterator(list(COUNTRIES)) - affiliation = factory.Faker('company') + vetted_by = factory.Iterator(Contributor.objects.all()) + personalwebpage = factory.Faker('uri') expertises = factory.Iterator(SCIPOST_SUBJECT_AREAS[0][1], getter=lambda c: [c[0]]) - personalwebpage = factory.Faker('domain_name') + orcid_id = factory.lazy_attribute(lambda n: generate_orcid()) address = factory.Faker('address') + invitation_key = factory.Faker('md5') + activation_key = factory.Faker('md5') + key_expires = factory.Faker('future_datetime') class Meta: model = Contributor django_get_or_create = ('user',) + @factory.post_generation + def add_to_vetting_editors(self, create, extracted, **kwargs): + if create: + from affiliations.factories import AffiliationFactory + AffiliationFactory(contributor=self) + class VettingEditorFactory(ContributorFactory): @factory.post_generation @@ -69,7 +74,7 @@ class EditorialCollegeFactory(factory.django.DjangoModelFactory): class Meta: model = EditorialCollege - django_get_or_create = ('discipline', ) + django_get_or_create = ('discipline',) class EditorialCollegeFellowshipFactory(factory.django.DjangoModelFactory): @@ -85,7 +90,7 @@ class SubmissionRemarkFactory(factory.django.DjangoModelFactory): contributor = factory.Iterator(Contributor.objects.all()) submission = factory.Iterator(Submission.objects.all()) date = factory.Faker('date_time_this_decade') - remark = factory.lazy_attribute(lambda x: Faker().paragraph()) + remark = factory.Faker('paragraph') class Meta: model = Remark diff --git a/scipost/management/commands/create_contributors.py b/scipost/management/commands/create_contributors.py new file mode 100644 index 0000000000000000000000000000000000000000..025513c74d49b6f82a77268706cf84d314a950bb --- /dev/null +++ b/scipost/management/commands/create_contributors.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand + +from scipost import factories + + +class Command(BaseCommand): + help = 'Create random Contributor objects using the factories.' + + def add_arguments(self, parser): + parser.add_argument( + 'number', action='store', default=0, type=int, + help='Number of Contributors to add') + + def handle(self, *args, **kwargs): + self.create_contributors(kwargs['number']) + + def create_contributors(self, n): + factories.ContributorFactory.create_batch(n) + self.stdout.write(self.style.SUCCESS('Successfully created {n} Contributors.'.format(n=n))) diff --git a/scipost/management/commands/create_remarks.py b/scipost/management/commands/create_remarks.py new file mode 100644 index 0000000000000000000000000000000000000000..19ebe3e92c5098192ae4880b149f5822018528f5 --- /dev/null +++ b/scipost/management/commands/create_remarks.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand + +from scipost import factories + + +class Command(BaseCommand): + help = 'Create random Remark objects (related to a Submission) using the factories.' + + def add_arguments(self, parser): + parser.add_argument( + 'number', action='store', default=0, type=int, + help='Number of Remarks to add') + + def handle(self, *args, **kwargs): + self.create_remarks(kwargs['number']) + + def create_remarks(self, n): + factories.SubmissionRemarkFactory.create_batch(n) + self.stdout.write(self.style.SUCCESS('Successfully created {n} Remarks.'.format(n=n))) diff --git a/scipost/management/commands/models.py b/scipost/management/commands/models.py deleted file mode 100644 index 0447660e2a189556e389b610e0c5d6bc797218aa..0000000000000000000000000000000000000000 --- a/scipost/management/commands/models.py +++ /dev/null @@ -1,188 +0,0 @@ -import datetime -import hashlib -import random -import string - -from django.db import models, IntegrityError -from django.conf import settings -from django.utils import timezone - -from . import constants -from .managers import RegistrationInvitationQuerySet, CitationNotificationQuerySet - -from scipost.constants import TITLE_CHOICES - - -class RegistrationInvitation(models.Model): - """ - Invitation to particular persons for registration - """ - title = models.CharField(max_length=4, choices=TITLE_CHOICES) - first_name = models.CharField(max_length=30) - last_name = models.CharField(max_length=150) - email = models.EmailField() - status = models.CharField(max_length=8, choices=constants.REGISTATION_INVITATION_STATUSES, - default=constants.STATUS_DRAFT) - - # Text content - message_style = models.CharField(max_length=1, choices=constants.INVITATION_STYLE, - default=constants.INVITATION_FORMAL) - personal_message = models.TextField(blank=True) - invited_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, - blank=True, null=True, related_name='invitations_sent') - created_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='invitations_created') - - # Related to objects - invitation_type = models.CharField(max_length=2, choices=constants.INVITATION_TYPE, - default=constants.INVITATION_CONTRIBUTOR) - - # Response keys - invitation_key = models.CharField(max_length=40, unique=True) - key_expires = models.DateTimeField(default=timezone.now) - - # Timestamps - date_sent_first = models.DateTimeField(null=True, blank=True) - date_sent_last = models.DateTimeField(null=True, blank=True) - times_sent = models.PositiveSmallIntegerField(default=0) - created = models.DateTimeField(auto_now_add=True) - modified = models.DateTimeField(auto_now=True) - - objects = RegistrationInvitationQuerySet.as_manager() - - class Meta: - ordering = ['last_name'] - - def __str__(self): - return '{} {} on {}'.format(self.first_name, self.last_name, - self.created.strftime("%Y-%m-%d")) - - def save(self, *args, **kwargs): - self.refresh_keys(commit=False) - return super().save(*args, **kwargs) - - def refresh_keys(self, force_new_key=False, commit=True): - # Generate email activation key and link - if not self.invitation_key or force_new_key: - # TODO: Replace this all by the `secrets` package available from python 3.6(!) - salt = '' - for i in range(5): - salt += random.choice(string.ascii_letters) - salt = salt.encode('utf8') - invitationsalt = self.last_name.encode('utf8') - self.invitation_key = hashlib.sha1(salt + invitationsalt).hexdigest() - self.key_expires = timezone.now() + datetime.timedelta(days=365) - if commit: - self.save() - - def mail_sent(self, user=None): - """ - Update instance fields as if a new invitation mail has been sent out. - """ - if self.status == constants.STATUS_DRAFT: - self.status = constants.STATUS_SENT - if not self.date_sent_first: - self.date_sent_first = timezone.now() - self.date_sent_last = timezone.now() - self.invited_by = user or self.created_by - self.times_sent += 1 - self.citation_notifications.update(processed=True) - self.save() - - @property - def has_responded(self): - return self.status in [constants.STATUS_DECLINED, constants.STATUS_REGISTERED] - - -class CitationNotification(models.Model): - invitation = models.ForeignKey('invitations.RegistrationInvitation', - on_delete=models.SET_NULL, - null=True, blank=True) - contributor = models.ForeignKey('scipost.Contributor', - on_delete=models.CASCADE, - null=True, blank=True, - related_name='+') - - # Content - submission = models.ForeignKey('submissions.Submission', null=True, blank=True, - related_name='+') - publication = models.ForeignKey('journals.Publication', null=True, blank=True, - related_name='+') - processed = models.BooleanField(default=False) - - # Meta info - created_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='notifications_created') - date_sent = models.DateTimeField(null=True, blank=True) - created = models.DateTimeField(auto_now_add=True) - modified = models.DateTimeField(auto_now=True) - - objects = CitationNotificationQuerySet.as_manager() - - class Meta: - default_related_name = 'citation_notifications' - unique_together = ( - ('invitation', 'submission'), - ('invitation', 'publication'), - ('contributor', 'submission'), - ('contributor', 'publication'), - ) - - def __str__(self): - _str = 'Citation for ' - if self.invitation: - _str += ' Invitation ({} {})'.format( - self.invitation.first_name, - self.invitation.last_name, - ) - elif self.contributor: - _str += ' Contributor ({})'.format(self.contributor) - - _str += ' on ' - if self.submission: - _str += 'Submission ({})'.format(self.submission.arxiv_identifier_w_vn_nr) - elif self.publication: - _str += 'Publication ({})'.format(self.publication.doi_label) - return _str - - def save(self, *args, **kwargs): - if not self.submission and not self.publication: - raise IntegrityError(('CitationNotification needs to be related to either a ' - 'Submission or Publication object.')) - return super().save(*args, **kwargs) - - def mail_sent(self): - """ - Update instance fields as if a new citation notification mail has been sent out. - """ - self.processed = True - if not self.date_sent: - # Don't overwrite by accident... - self.date_sent = timezone.now() - self.save() - - def related_notifications(self): - return CitationNotification.objects.unprocessed().filter( - models.Q(contributor=self.contributor) | models.Q(invitation=self.invitation)) - - def get_first_related_contributor(self): - return self.related_notifications().filter(contributor__isnull=False).first() - - @property - def email(self): - if self.invitation: - return self.invitation.email - elif self.contributor: - return self.contributor.user.email - - @property - def last_name(self): - if self.invitation: - return self.invitation.last_name - elif self.contributor: - return self.contributor.user.last_name - - @property - def get_title(self): - if self.invitation: - return self.invitation.get_title_display() - elif self.contributor: - return self.contributor.get_title_display() diff --git a/scipost/management/commands/populate_db.py b/scipost/management/commands/populate_db.py index 4efe19d85887e5a79782a2b8defb1919286e049f..4812d71e6fa2958a98c7792ff5c97579aa94b83e 100644 --- a/scipost/management/commands/populate_db.py +++ b/scipost/management/commands/populate_db.py @@ -1,34 +1,11 @@ from django.core.management.base import BaseCommand -from commentaries.factories import VettedCommentaryFactory -from comments.factories import CommentaryCommentFactory, SubmissionCommentFactory,\ - ThesislinkCommentFactory -from scipost.factories import SubmissionRemarkFactory -from journals.factories import JournalFactory, VolumeFactory, IssueFactory, PublicationFactory -from news.factories import NewsItemFactory -from submissions.factories import EICassignedSubmissionFactory -from theses.factories import VettedThesisLinkFactory - -from ...factories import ContributorFactory, EditorialCollegeFactory,\ - EditorialCollegeFellowshipFactory +from comments.factories import CommentaryCommentFactory,\ + ThesislinkCommentFactory, ReplyCommentFactory class Command(BaseCommand): def add_arguments(self, parser): - parser.add_argument( - '--news', - action='store_true', - dest='news', - default=False, - help='Add NewsItems', - ) - parser.add_argument( - '--commentaries', - action='store_true', - dest='commentaries', - default=False, - help='Add 5 Commentaries', - ) parser.add_argument( '--comments', action='store_true', @@ -36,147 +13,13 @@ class Command(BaseCommand): default=False, help='Add 10 Comments', ) - parser.add_argument( - '--contributor', - action='store_true', - dest='contributor', - default=False, - help='Add 5 Contributors', - ) - parser.add_argument( - '--college', - action='store_true', - dest='editorial-college', - default=False, - help='Add 5 Editorial College and Fellows (Contributors required)', - ) - parser.add_argument( - '--pubset', - action='store_true', - dest='pubset', - default=False, - help='Add 5 Issues, Volumes and Journals', - ) - parser.add_argument( - '--issues', - action='store_true', - dest='issues', - default=False, - help='Add 5 Issues', - ) - parser.add_argument( - '--submissions', - action='store_true', - dest='submissions', - default=False, - help='Add 5 new submissions status EIC assigned', - ) - parser.add_argument( - '--publications', - action='store_true', - dest='publications', - default=False, - help='Add 5 Publications (includes --issues action)', - ) - parser.add_argument( - '--remarks', - action='store_true', - dest='remarks', - default=False, - help='Add 5 new Remarks linked to Submissions', - ) - parser.add_argument( - '--theses', - action='store_true', - dest='theses', - default=False, - help='Add 5 ThesisLinks', - ) - parser.add_argument( - '--all', - action='store_true', - dest='all', - default=False, - help='Add all available', - ) def handle(self, *args, **kwargs): - if kwargs['contributor'] or kwargs['all']: - n = 5 - if kwargs['all']: - n += 10 - self.create_contributors(n) - if kwargs['commentaries'] or kwargs['all']: - self.create_commentaries() - if kwargs['comments'] or kwargs['all']: + if kwargs['comments']: self.create_comments() - if kwargs['editorial-college'] or kwargs['all']: - self.create_editorial_college() - self.create_editorial_college_fellows() - if kwargs['news'] or kwargs['all']: - self.create_news_items() - if kwargs['submissions'] or kwargs['all']: - self.create_submissions() - if kwargs['pubset'] or kwargs['all']: - self.create_pubset() - if kwargs['issues'] or kwargs['all']: - self.create_issues() - if kwargs['publications'] or kwargs['all']: - self.create_publications() - if kwargs['remarks'] or kwargs['all']: - self.create_remarks() - if kwargs['theses'] or kwargs['all']: - self.create_theses() - - def create_contributors(self, n=5): - ContributorFactory.create_batch(n) - self.stdout.write(self.style.SUCCESS('Successfully created %i Contributors.' % n)) - - def create_commentaries(self): - VettedCommentaryFactory.create_batch(5) - self.stdout.write(self.style.SUCCESS('Successfully created 5 Commentaries.')) def create_comments(self): CommentaryCommentFactory.create_batch(3) - SubmissionCommentFactory.create_batch(4) + ReplyCommentFactory.create_batch(2) ThesislinkCommentFactory.create_batch(3) self.stdout.write(self.style.SUCCESS('Successfully created 10 Comments.')) - - def create_editorial_college(self): - EditorialCollegeFactory.create_batch(5) - self.stdout.write(self.style.SUCCESS('Successfully created 5 Editorial College\'s.')) - - def create_editorial_college_fellows(self): - EditorialCollegeFellowshipFactory.create_batch(5) - self.stdout.write(self.style.SUCCESS('Successfully created 5 Editorial College Fellows.')) - - def create_news_items(self): - NewsItemFactory.create_batch(5) - self.stdout.write(self.style.SUCCESS('Successfully created 5 News items.')) - - def create_submissions(self): - EICassignedSubmissionFactory.create_batch(5) - self.stdout.write(self.style.SUCCESS('Successfully created 5 Submissions.')) - - def create_pubset(self): - VolumeFactory.create_batch(5) - IssueFactory.create_batch(5) - self.stdout.write(self.style.SUCCESS( - 'Successfully created 5x {Journal, Volume and Issue}.')) - - def create_issues(self): - IssueFactory.create_batch(5) - self.stdout.write(self.style.SUCCESS( - 'Successfully created 5 Issue.')) - - def create_publications(self): - PublicationFactory.create_batch(5) - self.stdout.write(self.style.SUCCESS('Successfully created 5 Publications.')) - - def create_remarks(self): - SubmissionRemarkFactory.create_batch(5) - self.stdout.write(self.style.SUCCESS('Successfully created 5 Remarks.')) - - def create_theses(self): - VettedThesisLinkFactory.create_batch(5) - self.stdout.write(self.style.SUCCESS('Successfully created 5 ThesisLinks.')) diff --git a/scipost/migrations/0007_auto_20180314_1502.py b/scipost/migrations/0007_auto_20180314_1502.py new file mode 100644 index 0000000000000000000000000000000000000000..6f5d6e3e9487a5660baf324f47590992396fcddc --- /dev/null +++ b/scipost/migrations/0007_auto_20180314_1502.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-03-14 14:02 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0006_auto_20180220_2120'), + ] + + operations = [ + migrations.AlterField( + model_name='contributor', + name='orcid_id', + field=models.CharField(blank=True, max_length=20, validators=[django.core.validators.RegexValidator('^[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{4}$', 'Please follow the ORCID format, e.g.: 0000-0001-2345-6789')], verbose_name='ORCID id'), + ), + ] diff --git a/scipost/static/scipost/SciPost.css b/scipost/static/scipost/SciPost.css index 44e1b4c27a9f5f124ba0bfed164a4ffca84c4685..616f2a6bcd0f597b9a0146c8ab314a7dcee8ef5b 100644 --- a/scipost/static/scipost/SciPost.css +++ b/scipost/static/scipost/SciPost.css @@ -666,22 +666,6 @@ p.publicationAuthors { padding: 1rem; border-radius: 1.4px; } -#preview-strengths { - border: 1px solid black; - white-space: pre-wrap; -} -#preview-weaknesses { - border: 1px solid black; - white-space: pre-wrap; -} -#preview-report { - border: 1px solid black; - white-space: pre-wrap; -} -#preview-requested_changes { - border: 1px solid black; - white-space: pre-wrap; -} /* Styling of sphinxdoc-generated docs */ .pagination-top { diff --git a/scipost/static/scipost/assets/config/preconfig.scss b/scipost/static/scipost/assets/config/preconfig.scss index 86ec1f0f209ca547d73c19b50d2896fa6f16be36..0721df1ff8ee2a1c88d3afe50d431ab6a9b3984e 100644 --- a/scipost/static/scipost/assets/config/preconfig.scss +++ b/scipost/static/scipost/assets/config/preconfig.scss @@ -34,6 +34,7 @@ $white: #fff; $blue: $scipost-lightblue; // Primary $green: #6ebb6e; $cyan: $scipost-lightestblue; +$orange: $scipost-orange; $yellow: $scipost-orange; $gray-100: $scipost-white; $gray-200: #e5e5e5; diff --git a/scipost/static/scipost/assets/css/_list_group.scss b/scipost/static/scipost/assets/css/_list_group.scss index c2313938bd328c3d3fa2c09ed704792f1f706da6..b92386962d3936d208b009cd8c3b4fabc11fbb00 100644 --- a/scipost/static/scipost/assets/css/_list_group.scss +++ b/scipost/static/scipost/assets/css/_list_group.scss @@ -49,3 +49,17 @@ ul.references { ul.links > li.active a { font-weight: 700; } + +ul.sortable-list { + li { + background-color: $white; + border: 1px solid $scipost-darkblue; + padding: 0.5rem; + margin-bottom: -1px; + cursor: move; + + &:hover { + background-color: $gray-200; + } + } +} diff --git a/scipost/static/scipost/assets/css/_notifications.scss b/scipost/static/scipost/assets/css/_notifications.scss index 2338ec63b5217d12980d3c1b0ef5c2d4e70fb368..5f980c52dac31f65b2bf1b79065d8dda0013e521 100644 --- a/scipost/static/scipost/assets/css/_notifications.scss +++ b/scipost/static/scipost/assets/css/_notifications.scss @@ -30,20 +30,48 @@ } } -.popover { - top: 9px !important; -} + .popover-template { display: none; } .notifications { padding: 0; - min-width: 500px; + min-width: 450px; + border-color: $gray-600; + border-radius: 1px; + margin-top: 17px !important; + .inbox-header, .header { - padding: 1rem 1rem 0.5rem 1rem; - background-color: #f9f9f9; + padding: 0.5rem 0; + border-bottom: 1px solid $gray-600; + margin: 0 1rem; + } + + .inbox-header { + border-top: 1px solid $gray-600; + border-bottom: 0; + margin-top: 0.25rem; + } + + .header { + padding-top: 1rem; + padding-bottom: 1rem; + margin-bottom: 0.25rem; + + h3, + .item { + border: 0; + line-height: 20px; + display: inline-block; + } + + .item { + float: right; + padding-left: 0; + padding-right: 0; + } } li.item { @@ -55,7 +83,7 @@ .item { padding: 0.4rem 1rem; border-radius: 0; - border-top: 1px solid #fff; + border-top: 1px solid $white; border-left: 0; border-right: 0; flex-direction: row; @@ -80,6 +108,30 @@ } } + .live_notify_list { + max-height: 250px; + overflow: scroll; + + &::after { + bottom: 0; + content: ''; + display: block; + width: 100%; + position: absolute; + height: 20px; + box-shadow: inset 0 -3px 9px 0px $gray-600; + } + + .meta { + font-size: 90%; + margin-top: 0.5rem; + } + + .item { + border-color: $gray-600; + } + } + a.item, .item a { color: $scipost-lightblue; @@ -114,7 +166,48 @@ } } + + .links .item { + &.active, + &.active[href]:hover { + background-color: transparent; + } + + &.active { + font-weight: 600; + text-decoration: underline; + } + } + .item:hover .actions { opacity: 1.0; } + + .unavailable { + margin: 0 1rem 0.25rem; + padding: 0.5rem 0 0.75rem; + border-bottom: 1px solid $gray-600; + + .head { + color: $orange; + font-weight: 600; + } + } + + .arrow { + top: -5px !important; + + &::before { + border-bottom-color: $gray-600 !important; + border-width: 12px; + left: -2px; + top: -7px !important; + } + + &::after { + border-bottom-color: $white; + border-width: 10px; + // left: -2px; + } + } } diff --git a/scipost/static/scipost/assets/css/_reports.scss b/scipost/static/scipost/assets/css/_reports.scss index 871bb5accdaf88fc863fe537b54214286203a04a..70bba0b47549c5f8cccf4d28a647276183bd4e9a 100644 --- a/scipost/static/scipost/assets/css/_reports.scss +++ b/scipost/static/scipost/assets/css/_reports.scss @@ -40,3 +40,22 @@ } } } + +.anonymous-alert { + margin-bottom: 0.5rem; + + .anonymous-yes { + color: $red; + } + .anonymous-no { + color: $green; + } +} + +.report-preview { + .latex-preview { + border: 1px solid $scipost-darkblue; + padding: 0.5rem 0.75rem; + white-space: pre-wrap; + } +} diff --git a/scipost/static/scipost/assets/js/notifications.js b/scipost/static/scipost/assets/js/notifications.js index 12f6b849bbcea9efbd0ee7b546e9ea0995cc00f3..eb7e8e671224ce41ef3a8feb1741da6118fe6434 100644 --- a/scipost/static/scipost/assets/js/notifications.js +++ b/scipost/static/scipost/assets/js/notifications.js @@ -63,11 +63,11 @@ function update_list_callback(data, args) { } } if(typeof item.timesince !== 'undefined'){ - message += "<br><small>"; + message += "<div class='meta'>"; if(typeof item.forward_link !== 'undefined') { message += " <a href='" + item.forward_link + "'>Direct link</a> · "; } - message += "<span class='text-muted'>" + item.timesince + " ago</span></small>"; + message += "<span class='text-muted'>" + item.timesince + " ago</span></div>"; } // Notification actions @@ -127,7 +127,7 @@ var badge_timer = setInterval(trigger_badge, 60000); function initiate_popover() { var template = $('.notifications_container .popover-template').html(); $('.notifications_container a[data-toggle="popover"]').popover({ - trigger: 'focus', + // trigger: 'focus', template: template, placement: 'bottom', title: 'empty-on-purpose' diff --git a/scipost/static/scipost/assets/js/scripts.js b/scipost/static/scipost/assets/js/scripts.js index 6a451fe5e5b1bed6004c85e7da4fb15275f42a5f..8adbae645179b5aea816d04c6e9f86a7f06e52b5 100644 --- a/scipost/static/scipost/assets/js/scripts.js +++ b/scipost/static/scipost/assets/js/scripts.js @@ -1,3 +1,6 @@ +require('jquery-ui/ui/widgets/sortable'); +require('jquery-ui/ui/disable-selection'); + import notifications from './notifications.js'; function hide_all_alerts() { @@ -12,6 +15,19 @@ var activate_tooltip = function() { }); } + +var sort_form_list = function(list_el) { + $(list_el).sortable({ + update: function(event, ui) { + $.each($(list_el + ' li'), function(index, el) { + $(el).find('input[name$=ORDER]').val(index + 1); + }); + } + }); +}; + + + var getUrlParameter = function getUrlParameter(sParam) { var sPageURL = decodeURIComponent(window.location.search.substring(1)), sURLVariables = sPageURL.split('&'), @@ -53,6 +69,7 @@ function init_page() { }); activate_tooltip(); + sort_form_list('form ul.sortable-list'); } $(function(){ diff --git a/scipost/templates/scipost/navbar.html b/scipost/templates/scipost/navbar.html index 2ea2260148733e03bedea67e360d526066216c6a..43fcc97810c0547f2097623df562ae982837bccf 100644 --- a/scipost/templates/scipost/navbar.html +++ b/scipost/templates/scipost/navbar.html @@ -70,10 +70,6 @@ </li> {% endif %} - <li class="nav-item search-item"> - <a class="nav-link" href="{% url 'scipost:search' %}">Search</a> - </li> - </ul> <form action="{% url 'scipost:search' %}" method="get" class="form-inline search-nav-form"> <input class="form-control mr-sm-2" id="id_q" maxlength="100" name="q" type="text" aria-label="Search" value="{{ search_query|default:'' }}"> diff --git a/scipost/templatetags/filename.py b/scipost/templatetags/filename.py index 6a20f1384cb29b5befd3d6dbf9c8c4a95335c3a1..2bff32b21130e34881c7fbccc41b9084dda4aa3f 100644 --- a/scipost/templatetags/filename.py +++ b/scipost/templatetags/filename.py @@ -8,4 +8,7 @@ register = template.Library() @register.filter def filename(value): - return os.path.basename(value.file.name) + try: + return os.path.basename(value.file.name) + except OSError: + return 'Error: File not found' diff --git a/scipost/test_views.py b/scipost/test_views.py index 22291f792c19630af0cc6b5192667fb3b4c4d8b4..3c6a15c1d384bc65bd1588bd90fd4ec78030f8e7 100644 --- a/scipost/test_views.py +++ b/scipost/test_views.py @@ -2,8 +2,8 @@ from django.core.urlresolvers import reverse from django.contrib.auth.models import Group from django.test import TestCase, Client, tag -from commentaries.factories import UnvettedCommentaryFactory, VettedCommentaryFactory,\ - UnpublishedVettedCommentaryFactory +from commentaries.factories import UnvettedCommentaryFactory, CommentaryFactory,\ + UnpublishedCommentaryFactory from commentaries.forms import CommentarySearchForm from commentaries.models import Commentary @@ -81,7 +81,7 @@ class VetCommentaryRequestsTest(TestCase): self.assertEquals(response.context['commentary_to_vet'], None) # Only vetted Commentaries exist! - VettedCommentaryFactory() + CommentaryFactory() response = self.client.get(self.view_url) self.assertEquals(response.context['commentary_to_vet'], None) @@ -96,7 +96,7 @@ class BrowseCommentariesTest(TestCase): fixtures = ['groups', 'permissions'] def setUp(self): - VettedCommentaryFactory(discipline='physics') + CommentaryFactory(discipline='physics') self.view_url = reverse('commentaries:browse', kwargs={ 'discipline': 'physics', 'nrweeksback': '1' @@ -118,7 +118,7 @@ class CommentaryDetailTest(TestCase): def setUp(self): self.client = Client() - self.commentary = UnpublishedVettedCommentaryFactory() + self.commentary = UnpublishedCommentaryFactory() self.target = reverse( 'commentaries:commentary', kwargs={'arxiv_or_DOI_string': self.commentary.arxiv_or_DOI_string} diff --git a/submissions/factories.py b/submissions/factories.py index d9c32c745fd079caed4ab17d02fd3511c0605ff9..6231ec36adcb4cd8b0ea9cd86c4155ffbc482898 100644 --- a/submissions/factories.py +++ b/submissions/factories.py @@ -1,108 +1,175 @@ import factory import pytz +import random -from django.utils import timezone - +from comments.factories import SubmissionCommentFactory from scipost.constants import SCIPOST_SUBJECT_AREAS 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 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 .models import Submission, Report, RefereeInvitation +from .models import Submission, Report, RefereeInvitation, EICRecommendation, EditorialAssignment from faker import Faker class SubmissionFactory(factory.django.DjangoModelFactory): - class Meta: - model = Submission - author_list = factory.Faker('name') submitted_by = factory.Iterator(Contributor.objects.all()) + submission_type = factory.Iterator(SUBMISSION_TYPE, getter=lambda c: c[0]) 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()) + title = factory.Faker('sentence') + abstract = factory.Faker('paragraph', nb_sentences=10) arxiv_link = factory.Faker('uri') arxiv_identifier_wo_vn_nr = factory.Sequence( - lambda n: random_arxiv_identifier_without_version_number()) + lambda n: random_arxiv_identifier_without_version_number()) subject_area = factory.Iterator(SCIPOST_SUBJECT_AREAS[0][1], getter=lambda c: c[0]) domain = factory.Iterator(SCIPOST_JOURNALS_DOMAINS, getter=lambda c: c[0]) - abstract = Faker().paragraph() - author_comments = Faker().paragraph() - remarks_for_editors = Faker().paragraph() - submission_type = factory.Iterator(SUBMISSION_TYPE, getter=lambda c: c[0]) + abstract = factory.Faker('paragraph') + author_comments = factory.Faker('paragraph') + remarks_for_editors = factory.Faker('paragraph') is_current = True + arxiv_vn_nr = 1 + arxiv_link = factory.lazy_attribute(lambda o: ( + 'https://arxiv.org/abs/%s' % o.arxiv_identifier_wo_vn_nr)) + arxiv_identifier_w_vn_nr = factory.lazy_attribute(lambda o: '%sv%i' % ( + o.arxiv_identifier_wo_vn_nr, o.arxiv_vn_nr)) + submission_date = factory.Faker('date_this_decade') + latest_activity = factory.LazyAttribute(lambda o: Faker().date_time_between( + start_date=o.submission_date, end_date="now", tzinfo=pytz.UTC)) - @factory.post_generation - def fill_arxiv_fields(self, create, extracted, **kwargs): - '''Fill empty arxiv fields.''' - self.arxiv_link = 'https://arxiv.org/abs/%s' % self.arxiv_identifier_wo_vn_nr - self.arxiv_identifier_w_vn_nr = '%sv1' % self.arxiv_identifier_wo_vn_nr - self.arxiv_vn_nr = kwargs.get('arxiv_vn_nr', 1) + class Meta: + model = Submission @factory.post_generation def contributors(self, create, extracted, **kwargs): - contributors = list(Contributor.objects.order_by('?')[:4]) + contributors = Contributor.objects.all() + if self.editor_in_charge: + contributors = contributors.exclude(id=self.editor_in_charge.id) + contributors = contributors.order_by('?')[:random.randint(1, 6)] # Auto-add the submitter as an author - self.submitted_by = contributors.pop() + self.submitted_by = contributors[0] + self.author_list = ', '.join([ + '%s %s' % (c.user.first_name, c.user.last_name) for c in contributors]) if not create: return - self.authors.add(self.submitted_by) # Add three random authors - for contrib in contributors: - self.authors.add(contrib) - self.author_list += ', %s %s' % (contrib.user.first_name, contrib.user.last_name) - - @factory.post_generation - def dates(self, create, extracted, **kwargs): - timezone.now() - if kwargs.get('submission', False): - self.submission_date = kwargs['submission'] - 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.authors.add(*contributors) self.cycle.update_deadline() class UnassignedSubmissionFactory(SubmissionFactory): - '''This Submission is a 'new request' by a Contributor for its Submission.''' + """ + A new incoming Submission without any EIC assigned. + """ status = STATUS_UNASSIGNED class EICassignedSubmissionFactory(SubmissionFactory): + """ + A Submission with an EIC assigned, visible in the pool and refereeing in process. + """ status = STATUS_EIC_ASSIGNED open_for_commenting = True open_for_reporting = True + @factory.lazy_attribute + def editor_in_charge(self): + return Contributor.objects.order_by('?').first() + @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()) + def eic_assignment(self, create, extracted, **kwargs): + if create: + EditorialAssignmentFactory(submission=self, to=self.editor_in_charge) + + @factory.post_generation + def referee_invites(self, create, extracted, **kwargs): + for i in range(random.randint(1, 3)): + RefereeInvitationFactory(submission=self) + + for i in range(random.randint(0, 2)): + AcceptedRefereeInvitationFactory(submission=self) + for i in range(random.randint(0, 2)): + FulfilledRefereeInvitationFactory(submission=self) -class ResubmittedSubmissionFactory(SubmissionFactory): - '''This Submission is a `resubmitted` version.''' + @factory.post_generation + def comments(self, create, extracted, **kwargs): + if create: + for i in range(random.randint(0, 3)): + SubmissionCommentFactory(content_object=self) + + @factory.post_generation + def eic_recommendation(self, create, extracted, **kwargs): + if create: + EICRecommendationFactory(submission=self) + + +class ResubmittedSubmissionFactory(EICassignedSubmissionFactory): + """ + A Submission that has a newer Submission version in the database + with a successive version number. + """ status = STATUS_RESUBMITTED open_for_commenting = False open_for_reporting = False is_current = False is_resubmission = False + @factory.post_generation + def successive_submission(self, create, extracted, **kwargs): + """ + Generate a second Submission that's the successive version of the resubmitted Submission + """ + if create and extracted is not False: + # Prevent infinite loops by checking the extracted argument + ResubmissionFactory(arxiv_identifier_wo_vn_nr=self.arxiv_identifier_wo_vn_nr, + previous_submission=False) -class ResubmissionFactory(SubmissionFactory): + @factory.post_generation + def gather_successor_data(self, create, extracted, **kwargs): + """ + Gather some data from Submission with same arxiv id such that this Submission + more or less looks like any regular real resubmission. + """ + submission = Submission.objects.filter( + arxiv_identifier_wo_vn_nr=self.arxiv_identifier_wo_vn_nr).exclude( + arxiv_vn_nr=self.arxiv_vn_nr).first() + if not submission: + return + + self.author_list = submission.author_list + self.submitted_by = submission.submitted_by + self.editor_in_charge = submission.editor_in_charge + self.submission_type = submission.submission_type + self.submitted_to_journal = submission.submitted_to_journal + self.title = submission.title + self.subject_area = submission.subject_area + self.domain = submission.domain + self.title = submission.title + self.authors.set(self.authors.all()) + + @factory.post_generation + def referee_invites(self, create, extracted, **kwargs): + """ + This Submission is deactivated for refereeing. + """ + for i in range(random.randint(0, 2)): + FulfilledRefereeInvitationFactory(submission=self) + + for i in range(random.randint(1, 3)): + CancelledRefereeInvitationFactory(submission=self) + + +class ResubmissionFactory(EICassignedSubmissionFactory): """ This Submission is a newer version of a Submission which is already known by the SciPost database. @@ -111,63 +178,99 @@ class ResubmissionFactory(SubmissionFactory): open_for_commenting = True open_for_reporting = True is_resubmission = True + arxiv_vn_nr = 2 @factory.post_generation - def fill_arxiv_fields(self, create, extracted, **kwargs): - '''Fill empty arxiv fields.''' - self.arxiv_link = 'https://arxiv.org/abs/%s' % self.arxiv_identifier_wo_vn_nr - self.arxiv_identifier_w_vn_nr = '%sv2' % self.arxiv_identifier_wo_vn_nr - self.arxiv_vn_nr = 2 + def previous_submission(self, create, extracted, **kwargs): + if create and extracted is not False: + # Prevent infinite loops by checking the extracted argument + ResubmittedSubmissionFactory(arxiv_identifier_wo_vn_nr=self.arxiv_identifier_wo_vn_nr, + successive_submission=False) @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()) + def gather_predecessor_data(self, create, extracted, **kwargs): + """ + Gather some data from Submission with same arxiv id such that this Submission + more or less looks like any regular real resubmission. + """ + submission = Submission.objects.filter( + arxiv_identifier_wo_vn_nr=self.arxiv_identifier_wo_vn_nr).exclude( + arxiv_vn_nr=self.arxiv_vn_nr).first() + if not submission: + return + + self.author_list = submission.author_list + self.submitted_by = submission.submitted_by + self.editor_in_charge = submission.editor_in_charge + self.submission_type = submission.submission_type + self.submitted_to_journal = submission.submitted_to_journal + self.title = submission.title + self.subject_area = submission.subject_area + self.domain = submission.domain + self.title = submission.title + self.authors.set(self.authors.all()) @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) + def referee_invites(self, create, extracted, **kwargs): + """ + Referees for resubmissions are invited once the cycle has been chosen. + """ + pass -class PublishedSubmissionFactory(SubmissionFactory): +class PublishedSubmissionFactory(EICassignedSubmissionFactory): status = STATUS_PUBLISHED open_for_commenting = False open_for_reporting = False + @factory.post_generation + def generate_publication(self, create, extracted, **kwargs): + if create and extracted is not False: + from journals.factories import PublicationFactory + PublicationFactory( + journal=self.submitted_to_journal, + accepted_submission=self, title=self.title, author_list=self.author_list) -class ReportFactory(factory.django.DjangoModelFactory): - class Meta: - model = Report + @factory.post_generation + def eic_assignment(self, create, extracted, **kwargs): + if create: + EditorialAssignmentFactory(submission=self, to=self.editor_in_charge, completed=True) + @factory.post_generation + def referee_invites(self, create, extracted, **kwargs): + for i in range(random.randint(2, 4)): + FulfilledRefereeInvitationFactory(submission=self) + + for i in range(random.randint(0, 2)): + CancelledRefereeInvitationFactory(submission=self) + + +class ReportFactory(factory.django.DjangoModelFactory): status = factory.Iterator(REPORT_STATUSES, getter=lambda c: c[0]) submission = factory.Iterator(Submission.objects.all()) - date_submitted = Faker().date_time_between(start_date="-3y", end_date="now", tzinfo=pytz.UTC) + date_submitted = factory.Faker('date_time_this_decade') vetted_by = factory.Iterator(Contributor.objects.all()) author = factory.Iterator(Contributor.objects.all()) - qualification = factory.Iterator(REFEREE_QUALIFICATION, getter=lambda c: c[0]) - strengths = Faker().paragraph() - weaknesses = Faker().paragraph() - report = Faker().paragraph() - requested_changes = Faker().paragraph() - validity = factory.Iterator(RANKING_CHOICES, getter=lambda c: c[0]) - significance = factory.Iterator(RANKING_CHOICES, getter=lambda c: c[0]) - originality = factory.Iterator(RANKING_CHOICES, getter=lambda c: c[0]) - clarity = factory.Iterator(RANKING_CHOICES, getter=lambda c: c[0]) - formatting = factory.Iterator(QUALITY_SPEC, getter=lambda c: c[0]) - grammar = factory.Iterator(QUALITY_SPEC, getter=lambda c: c[0]) - recommendation = factory.Iterator(REPORT_REC, getter=lambda c: c[0]) - remarks_for_editors = Faker().paragraph() + strengths = factory.Faker('paragraph') + weaknesses = factory.Faker('paragraph') + report = factory.Faker('paragraph') + requested_changes = factory.Faker('paragraph') + + qualification = factory.Iterator(REFEREE_QUALIFICATION[1:], getter=lambda c: c[0]) + validity = factory.Iterator(RANKING_CHOICES[1:], getter=lambda c: c[0]) + significance = factory.Iterator(RANKING_CHOICES[1:], getter=lambda c: c[0]) + originality = factory.Iterator(RANKING_CHOICES[1:], getter=lambda c: c[0]) + clarity = factory.Iterator(RANKING_CHOICES[1:], getter=lambda c: c[0]) + formatting = factory.Iterator(QUALITY_SPEC[1:], getter=lambda c: c[0]) + grammar = factory.Iterator(QUALITY_SPEC[1:], getter=lambda c: c[0]) + recommendation = factory.Iterator(REPORT_REC[1:], getter=lambda c: c[0]) + + remarks_for_editors = factory.Faker('paragraph') + flagged = factory.Faker('boolean', chance_of_getting_true=10) + anonymous = factory.Faker('boolean', chance_of_getting_true=75) + + class Meta: + model = Report class DraftReportFactory(ReportFactory): @@ -182,26 +285,88 @@ class UnVettedReportFactory(ReportFactory): class VettedReportFactory(ReportFactory): status = STATUS_VETTED + needs_doi = True + doideposit_needs_updating = factory.Faker('boolean') + doi_label = factory.lazy_attribute(lambda n: random_scipost_report_doi_label()) + pdf_report = factory.Faker('file_name', extension='pdf') class RefereeInvitationFactory(factory.django.DjangoModelFactory): + submission = factory.SubFactory('submissions.factories.SubmissionFactory') + referee = factory.lazy_attribute(lambda o: Contributor.objects.exclude( + id__in=o.submission.authors.all()).order_by('?').first()) + + title = factory.lazy_attribute(lambda o: o.referee.title) + first_name = factory.lazy_attribute(lambda o: o.referee.user.first_name) + last_name = factory.lazy_attribute(lambda o: o.referee.user.last_name) + email_address = factory.lazy_attribute(lambda o: o.referee.user.email) + date_invited = factory.lazy_attribute(lambda o: o.submission.latest_activity) + nr_reminders = factory.lazy_attribute(lambda o: random.randint(0, 4)) + date_last_reminded = factory.lazy_attribute(lambda o: o.submission.latest_activity) + + invitation_key = factory.Faker('md5') + invited_by = factory.lazy_attribute(lambda o: o.submission.editor_in_charge) + class Meta: model = RefereeInvitation - submission = factory.SubFactory('submissions.factories.SubmissionFactory') - referee = factory.Iterator(Contributor.objects.all()) - invitation_key = factory.Faker('md5') - invited_by = factory.Iterator(Contributor.objects.all()) +class AcceptedRefereeInvitationFactory(RefereeInvitationFactory): + accepted = True + date_responded = factory.lazy_attribute(lambda o: Faker().date_time_between( + start_date=o.date_invited, end_date="now", tzinfo=pytz.UTC)) @factory.post_generation - def contributor_fields(self, create, extracted, **kwargs): - self.title = self.referee.title - self.first_name = self.referee.user.first_name - self.last_name = self.referee.user.last_name - self.email_address = self.referee.user.email + def report(self, create, extracted, **kwargs): + if create: + VettedReportFactory(submission=self.submission, author=self.referee) -class AcceptedRefereeInvitationFactory(RefereeInvitationFactory): +class FulfilledRefereeInvitationFactory(AcceptedRefereeInvitationFactory): + fulfilled = True + date_responded = factory.lazy_attribute(lambda o: Faker().date_time_between( + start_date=o.date_invited, end_date="now", tzinfo=pytz.UTC)) + + @factory.post_generation + def report(self, create, extracted, **kwargs): + if create: + VettedReportFactory(submission=self.submission, author=self.referee) + + +class CancelledRefereeInvitationFactory(AcceptedRefereeInvitationFactory): + fulfilled = False + cancelled = True + date_responded = factory.lazy_attribute(lambda o: Faker().date_time_between( + start_date=o.date_invited, end_date="now", tzinfo=pytz.UTC)) + + +class EICRecommendationFactory(factory.django.DjangoModelFactory): + submission = factory.Iterator(Submission.objects.all()) + date_submitted = factory.lazy_attribute(lambda o: Faker().date_time_between( + start_date=o.submission.submission_date, end_date="now", tzinfo=pytz.UTC)) + remarks_for_authors = factory.Faker('paragraph') + requested_changes = factory.Faker('paragraph') + remarks_for_editorial_college = factory.Faker('paragraph') + recommendation = factory.Iterator(REPORT_REC[1:], getter=lambda c: c[0]) + version = 1 + active = True + + class Meta: + model = EICRecommendation + + +class EditorialAssignmentFactory(factory.django.DjangoModelFactory): + """ + A EditorialAssignmentFactory should always have a `submission` explicitly assigned. This will + mostly be done using the post_generation hook in any SubmissionFactory. + """ + submission = None + to = factory.Iterator(Contributor.objects.all()) accepted = True - date_responded = Faker().date_time_between(start_date="-1y", end_date="now", tzinfo=pytz.UTC) + deprecated = False + completed = False + date_created = factory.lazy_attribute(lambda o: o.submission.latest_activity) + date_answered = factory.lazy_attribute(lambda o: o.submission.latest_activity) + + class Meta: + model = EditorialAssignment diff --git a/submissions/management/commands/create_submissions.py b/submissions/management/commands/create_submissions.py new file mode 100644 index 0000000000000000000000000000000000000000..575f01ec60414c1293e1c5dd342d606be7a07b4c --- /dev/null +++ b/submissions/management/commands/create_submissions.py @@ -0,0 +1,36 @@ +from django.core.management.base import BaseCommand + +from submissions import factories + + +class Command(BaseCommand): + help = 'Create random Submission objects by using the factories.' + + def add_arguments(self, parser): + parser.add_argument( + 'number', action='store', default=0, type=int, + help='Number of submissions to add', + ) + parser.add_argument( + '-s', '--status', + choices=['unassigned', 'assigned', 'resubmitted', 'resubmission', 'published'], + action='store', dest='status', default='assigned', + help='Current status of the Submission', + ) + + def handle(self, *args, **kwargs): + if kwargs['number']: + self.create_submissions(kwargs['number'], status=kwargs['status']) + + def create_submissions(self, n, status='assigned'): + if status == 'unassigned': + factories.UnassignedSubmissionFactory.create_batch(n) + elif status == 'assigned': + factories.EICassignedSubmissionFactory.create_batch(n) + elif status == 'resubmitted': + factories.ResubmittedSubmissionFactory.create_batch(n) + elif status == 'resubmission': + factories.ResubmissionFactory.create_batch(n) + elif status == 'published': + factories.PublishedSubmissionFactory.create_batch(n) + self.stdout.write(self.style.SUCCESS('Successfully created {n} Submissions.'.format(n=n))) diff --git a/submissions/migrations/0010_auto_20180314_1607.py b/submissions/migrations/0010_auto_20180314_1607.py new file mode 100644 index 0000000000000000000000000000000000000000..7bbd633195f6d1b3e4dfb44a9552f18d9586275f --- /dev/null +++ b/submissions/migrations/0010_auto_20180314_1607.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-03-14 15:07 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0009_auto_20180220_2120'), + ] + + operations = [ + migrations.AlterField( + model_name='submission', + name='submission_type', + field=models.CharField(choices=[('Letter', 'Letter (broad-interest breakthrough results)'), ('Article', 'Article (in-depth reports on specialized research)'), ('Review', 'Review (candid snapshot of current research in a given area)')], default='', max_length=10), + preserve_default=False, + ), + ] diff --git a/submissions/models.py b/submissions/models.py index f9cdcfd6f731c28c9ef43b839e2b76e04d9e12ba..95c4c2df27f360282c3f8cc176d3a4ab1268ed64 100644 --- a/submissions/models.py +++ b/submissions/models.py @@ -66,8 +66,7 @@ class Submission(models.Model): related_name='pool') 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, - blank=True, null=True, default=None) + submission_type = models.CharField(max_length=10, choices=SUBMISSION_TYPE) submitted_by = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE, related_name='submitted_submissions') voting_fellows = models.ManyToManyField('colleges.Fellowship', blank=True, diff --git a/submissions/signals.py b/submissions/signals.py index be833cb833d351eab88e1d0d384e638ef4992762..9bb75251f1999445672a0cbfcaff2a5a5242c16b 100644 --- a/submissions/signals.py +++ b/submissions/signals.py @@ -85,3 +85,17 @@ def notify_invitation_overdue(sender, instance, created, **kwargs): verb=(' would like to remind you that your Refereeing Task is overdue, ' 'please submit your Report'), target=instance.submission, type=NOTIFICATION_REFEREE_OVERDUE) + + +def notify_manuscript_accepted(sender, instance, created, **kwargs): + """ + Notify authors about their manuscript decision. + + instance --- Submission + """ + college = Group.objects.get(name='Editorial College') + authors = User.objects.filter(contributor__submissions=instance) + for user in authors: + notify.send(sender=sender, recipient=user, actor=college, + verb=' has accepted your manuscript for publication.', + target=instance) diff --git a/submissions/templates/partials/submissions/submission_status.html b/submissions/templates/partials/submissions/submission_status.html index 82438028e92314b6c417d505f29cc6b90da17efc..fb727c41e9c6865bfc23117c7ff1a69bf9b30559 100644 --- a/submissions/templates/partials/submissions/submission_status.html +++ b/submissions/templates/partials/submissions/submission_status.html @@ -2,6 +2,12 @@ <div class="d-inline"> <span class="label label-secondary">{{submission.get_status_display}}</span> {% if submission.publication and submission.publication.is_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> + 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> {% endif %} </div> diff --git a/submissions/templates/submissions/report_form.html b/submissions/templates/submissions/report_form.html index 5c6c813aefffc2db5a0df7c883b9dfd8b90beb7b..bb7a3639093272957da2538408c4245801735b3f 100644 --- a/submissions/templates/submissions/report_form.html +++ b/submissions/templates/submissions/report_form.html @@ -10,56 +10,27 @@ {% block pagetitle %}: submit report{% endblock pagetitle %} {% block content %} - -<script> - $(document).ready(function(){ - - var strengths_input = $("#id_strengths"); - function set_strengths(value) { - $("#preview-strengths").text(value) - } - set_strengths(strengths_input.val()) - strengths_input.keyup(function(){ - var new_text = $(this).val() - set_strengths(new_text) - MathJax.Hub.Queue(["Typeset",MathJax.Hub]); - }) - - var weaknesses_input = $("#id_weaknesses"); - function set_weaknesses(value) { - $("#preview-weaknesses").text(value) - } - set_weaknesses(weaknesses_input.val()) - weaknesses_input.keyup(function(){ - var new_text = $(this).val() - set_weaknesses(new_text) - MathJax.Hub.Queue(["Typeset",MathJax.Hub]); - }) - - var report_input = $("#id_report"); - function set_report(value) { - $("#preview-report").text(value) - } - set_report(report_input.val()) - report_input.keyup(function(){ - var new_text = $(this).val() - set_report(new_text) - MathJax.Hub.Queue(["Typeset",MathJax.Hub]); - }) - - var requested_changes_input = $("#id_requested_changes"); - function set_requested_changes(value) { - $("#preview-requested_changes").text(value) - } - set_requested_changes(requested_changes_input.val()) - requested_changes_input.keyup(function(){ - var new_text = $(this).val() - set_requested_changes(new_text) - MathJax.Hub.Queue(["Typeset",MathJax.Hub]); - }) - - }); -</script> + <script> + $(function(){ + function set_preview(el) { + $('#preview-' + $(el).attr('id')).text($(el).val()) + } + $('#id_weaknesses, #id_strengths, #id_report, #id_requested_changes').on('keyup', function(){ + set_preview(this) + MathJax.Hub.Queue(["Typeset",MathJax.Hub]); + }) + + $('input[name$="anonymous"]').on('change', function() { + $('.anonymous-alert').show() + .children('h3').hide() + if ($(this).prop('checked')) { + $('.anonymous-yes').show(); + } else { + $('.anonymous-no').show(); + } + }).trigger('change'); + }); + </script> {% if user.is_authenticated %} @@ -76,7 +47,7 @@ </div> </div> - <hr> + <hr class="divider"> <div class="row"> <div class="col-12"> <div class="card card-grey"> @@ -97,9 +68,14 @@ </div> </div> {% endif %} + <br> <form action="{% url 'submissions:submit_report' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}" method="post"> {% csrf_token %} {{ form|bootstrap:'3,9' }} + <div class="anonymous-alert" style="display: none;"> + <h3 class="anonymous-yes">Your Report will remain anonymous.</h3> + <h3 class="anonymous-no">Your Report will be signed. Thank you very much!</h3> + </div> <p>Any fields with an asterisk (*) are required.</p> <input class="btn btn-primary" type="submit" name="save_submit" value="Submit your report"/> <input class="btn btn-secondary ml-2" type="submit" name="save_draft" value="Save your report as draft"/> @@ -118,20 +94,20 @@ <hr> <div class="row"> - <div class="col-12"> + <div class="col-12 report-preview"> <h3>Preview of your report (text areas only):</h3> <h4>Strengths:</h4> - <p class="p-2" id="preview-strengths"></p> + <p class="latex-preview" id="preview-id_strengths"></p> <h4>Weaknesses:</h4> - <p class="p-2" id="preview-weaknesses"></p> + <p class="latex-preview" id="preview-id_weaknesses"></p> <h4>Report:</h4> - <p class="p-2" id="preview-report"></p> + <p class="latex-preview" id="preview-id_report"></p> <h4>Requested changes:</h4> - <p class="p-2" id="preview-requested_changes"></p> + <p class="latex-preview" id="preview-id_requested_changes"></p> </div> </div> diff --git a/submissions/utils.py b/submissions/utils.py index 0610215599b89987167ca1540b4a2c06b0db2849..dcf46c20424e7e761a3cbbe88d7a8a9d7f5b680f 100644 --- a/submissions/utils.py +++ b/submissions/utils.py @@ -118,9 +118,6 @@ class BaseSubmissionCycle: 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 diff --git a/submissions/views.py b/submissions/views.py index 3df758bca45b6ff60eeed2fce5ed7112228d29ea..da7983022760b0242c0cfd6655d18ab2ed22e821 100644 --- a/submissions/views.py +++ b/submissions/views.py @@ -34,6 +34,7 @@ from .forms import SubmissionIdentifierForm, RequestSubmissionForm, SubmissionSe EICRecommendationForm, ReportForm, VetReportForm, VotingEligibilityForm,\ SubmissionCycleChoiceForm, ReportPDFForm, SubmissionReportsForm,\ iThenticateReportForm, SubmissionPoolFilterForm +from .signals import notify_manuscript_accepted from .utils import SubmissionUtils from colleges.permissions import fellowship_required, fellowship_or_admin_required @@ -1573,6 +1574,7 @@ def fix_College_decision(request, rec_id): # 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: diff --git a/theses/factories.py b/theses/factories.py index c5461a1a6160fe74397f854ed00309966ed6c06c..91e4e2edce654395ed1be66847ba6ad83459f3c3 100644 --- a/theses/factories.py +++ b/theses/factories.py @@ -1,7 +1,5 @@ import factory -from django.utils import timezone - from common.helpers.factories import FormFactory from journals.constants import SCIPOST_JOURNALS_DOMAINS from scipost.constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS @@ -11,32 +9,45 @@ from .models import ThesisLink from .forms import VetThesisLinkForm from .constants import THESIS_TYPES -from faker import Faker - -timezone.now() - -class ThesisLinkFactory(factory.django.DjangoModelFactory): +class BaseThesisLinkFactory(factory.django.DjangoModelFactory): class Meta: model = ThesisLink + abstract = True requested_by = factory.Iterator(Contributor.objects.all()) + vetted_by = factory.Iterator(Contributor.objects.all()) + vetted = True + type = factory.Iterator(THESIS_TYPES, getter=lambda c: c[0]) domain = factory.Iterator(SCIPOST_JOURNALS_DOMAINS, getter=lambda c: c[0]) discipline = factory.Iterator(SCIPOST_DISCIPLINES, getter=lambda c: c[0]) subject_area = factory.Iterator(SCIPOST_SUBJECT_AREAS[0][1], getter=lambda c: c[0]) - title = factory.Faker('text') + title = factory.Faker('sentence') pub_link = factory.Faker('uri') author = factory.Faker('name') supervisor = factory.Faker('name') institution = factory.Faker('company') - defense_date = factory.Faker('date') - abstract = factory.lazy_attribute(lambda x: Faker().paragraph()) - - -class VettedThesisLinkFactory(ThesisLinkFactory): - vetted_by = factory.Iterator(Contributor.objects.all()) - vetted = True + defense_date = factory.Faker('date_this_decade') + abstract = factory.Faker('paragraph') + + @factory.post_generation + def author_as_cont(self, create, extracted, **kwargs): + if not create: + # Simple build, do nothing. + return + + if extracted: + # A list of groups were passed in, use them + for contributor in extracted: + self.author_as_cont.add(contributor) + elif factory.Faker('boolean'): + contributor = Contributor.objects.order_by('?').first() + self.author_as_cont.add(contributor) + + +class ThesisLinkFactory(BaseThesisLinkFactory): + pass class VetThesisLinkFormFactory(FormFactory): diff --git a/theses/management/__init__.py b/theses/management/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/theses/management/commands/__init__.py b/theses/management/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/theses/management/commands/create_theses.py b/theses/management/commands/create_theses.py new file mode 100644 index 0000000000000000000000000000000000000000..8c98134929bcf3ff3c5aecc8be334ba2df9c900e --- /dev/null +++ b/theses/management/commands/create_theses.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand + +from theses import factories + + +class Command(BaseCommand): + help = 'Create random Thesis objects using the factories.' + + def add_arguments(self, parser): + parser.add_argument( + 'number', action='store', default=0, type=int, + help='Number of Theses to add') + + def handle(self, *args, **kwargs): + self.create_theses(kwargs['number']) + + def create_theses(self, n): + factories.ThesisLinkFactory.create_batch(n) + self.stdout.write(self.style.SUCCESS('Successfully created {n} Theses.'.format(n=n))) diff --git a/theses/test_views.py b/theses/test_views.py index 91e37fc3c6b157d72e479765f8dc7d3c5ba6344e..33d8eed56faa8bd9fa7cafd75a8ff150f0eed209 100644 --- a/theses/test_views.py +++ b/theses/test_views.py @@ -13,7 +13,7 @@ from comments.factories import CommentFactory from comments.forms import CommentForm from comments.models import Comment from .views import RequestThesisLink, VetThesisLink, thesis_detail -from .factories import ThesisLinkFactory, VettedThesisLinkFactory, VetThesisLinkFormFactory +from .factories import ThesisLinkFactory, ThesisLinkFactory, VetThesisLinkFormFactory from .models import ThesisLink from .forms import VetThesisLinkForm from common.helpers import model_form_data @@ -163,14 +163,14 @@ class TestTheses(TestCase): self.target = reverse('theses:theses') def test_empty_search_query(self): - thesislink = VettedThesisLinkFactory() + thesislink = ThesisLinkFactory() response = self.client.get(self.target) search_results = response.context["object_list"] self.assertTrue(thesislink in search_results) def test_search_query_on_author(self): - thesislink = VettedThesisLinkFactory() - other_thesislink = VettedThesisLinkFactory() + thesislink = ThesisLinkFactory() + other_thesislink = ThesisLinkFactory() form_data = {'author': thesislink.author} response = self.client.get(self.target, form_data) search_results = response.context['object_list']