diff --git a/.bootstraprc b/.bootstraprc index 0507aae30727298d2d6f86ed22a6d8e2df4ec385..f02ac6f2dbbc5fcc9fdb13a3ae9119cf053e20a3 100644 --- a/.bootstraprc +++ b/.bootstraprc @@ -9,6 +9,7 @@ ], "extractStyles": true, "styles": { + "alert": true, "mixins": true, "normalize": true, "reboot": true, @@ -26,6 +27,7 @@ "transitions": true, }, "scripts": { + "alert": true, "collapse": true, "util": true, } diff --git a/.gitignore b/.gitignore index 64ad90a0775214cd762d423e2f24ed70e94dcd52..3fac94e23995b2ce0d7727525f2014868de41002 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,8 @@ SCIPOST_JOURNALS UPLOADS docs/_build +local_files +static/ local_files/ static/ diff --git a/README.md b/README.md index 85b4385bf5438c74d0e747b94a5b02999718a2c0..1ebdbdcb35812fa89ac24d46b31d27a810349794 100644 --- a/README.md +++ b/README.md @@ -118,10 +118,10 @@ In order to use the admin site, you'll need a superuser. ``` ### Create groups and permissions -Groups and their respective permissions are created using the management command. Since users depend on the *Contributor* object to work properly, setup the first (admin) user using the `-u` and `-a` arguments. +Groups and their respective permissions are created using the management command. ```shell -(scipostenv) $ ./manage.py add_groups_and_permissions -u=<username> -a +(scipostenv) $ ./manage.py add_groups_and_permissions ``` ### Run development server diff --git a/SciPost_v1/settings.py b/SciPost_v1/settings.py index 4e6a5fc96a19b7b16c793cfa9f3ea3948db188d6..c8b6bf5e07241682fcabb72a6a9356aeaee0f41c 100644 --- a/SciPost_v1/settings.py +++ b/SciPost_v1/settings.py @@ -193,7 +193,7 @@ STATICFILES_DIRS = ( WEBPACK_LOADER = { 'DEFAULT': { 'CACHE': not DEBUG, - 'BUNDLE_DIR_NAME': 'static/bundles/', + 'BUNDLE_DIR_NAME': host_settings["STATIC_ROOT"] + 'bundles/', 'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats.json'), 'POLL_INTERVAL': 0.1, 'TIMEOUT': None, diff --git a/commentaries/migrations/0014_auto_20170201_1243.py b/commentaries/migrations/0014_auto_20170201_1243.py new file mode 100644 index 0000000000000000000000000000000000000000..060845cb8a82fa26f888bdc2cbadbf30b0541526 --- /dev/null +++ b/commentaries/migrations/0014_auto_20170201_1243.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-02-01 11:43 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone +import scipost.db.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('commentaries', '0013_auto_20161213_2328'), + ] + + operations = [ + migrations.AlterField( + model_name='commentary', + name='created', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='commentary', + name='latest_activity', + field=scipost.db.fields.AutoDateTimeField(blank=True, default=django.utils.timezone.now, editable=False), + ), + ] diff --git a/commentaries/models.py b/commentaries/models.py index 0da8b2bce3415abcf394799c535f3c324702829a..d64ea58e36c671ef90467009434f1627d75f81e2 100644 --- a/commentaries/models.py +++ b/commentaries/models.py @@ -52,7 +52,7 @@ class Commentary(TimeStampedModel): blank=True) metadata = JSONField(default={}, blank=True, null=True) arxiv_or_DOI_string = models.CharField( - max_length=100, + max_length=100, default='', verbose_name='string form of arxiv nr or DOI for commentary url') author_list = models.CharField(max_length=1000) diff --git a/comments/factories.py b/comments/factories.py new file mode 100644 index 0000000000000000000000000000000000000000..aaf4cc9698021923b66ba04444a4e612d0650240 --- /dev/null +++ b/comments/factories.py @@ -0,0 +1,14 @@ +import factory + +from django.utils import timezone + +from scipost.factories import ContributorFactory +from .models import Comment + + +class CommentFactory(factory.django.DjangoModelFactory): + class Meta: + model = Comment + + comment_text = factory.Faker('text') + date_submitted = timezone.now() diff --git a/comments/migrations/0004_auto_20161213_1208.py b/comments/migrations/0004_auto_20161213_1208.py deleted file mode 100644 index 95ab361f1066178c7588b6664106dae891cb902c..0000000000000000000000000000000000000000 --- a/comments/migrations/0004_auto_20161213_1208.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.3 on 2016-12-13 11:08 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('comments', '0003_auto_20160404_2150'), - ] - - operations = [ - migrations.AlterField( - model_name='comment', - name='in_agreement', - field=models.ManyToManyField(blank=True, related_name='in_agreement', to='scipost.Contributor'), - ), - migrations.AlterField( - model_name='comment', - name='in_disagreement', - field=models.ManyToManyField(blank=True, related_name='in_disagreement', to='scipost.Contributor'), - ), - migrations.AlterField( - model_name='comment', - name='in_notsure', - field=models.ManyToManyField(blank=True, related_name='in_notsure', to='scipost.Contributor'), - ), - migrations.AlterField( - model_name='comment', - name='is_cor', - field=models.BooleanField(default=False, verbose_name='correction/erratum'), - ), - ] diff --git a/comments/migrations/0005_auto_20170131_2141.py b/comments/migrations/0005_auto_20170131_2141.py new file mode 100644 index 0000000000000000000000000000000000000000..d8e0fbd5a430e6e648519375336ee4dd89921939 --- /dev/null +++ b/comments/migrations/0005_auto_20170131_2141.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-01-31 20:41 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('comments', '0004_auto_20161212_1931'), + ] + + operations = [ + migrations.AddField( + model_name='comment', + name='created', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='comment', + name='latest_activity', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/comments/migrations/0005_merge_20161219_2126.py b/comments/migrations/0005_merge_20161219_2126.py deleted file mode 100644 index f604a8605fd90bc48a48ababc797a758ed29c2a3..0000000000000000000000000000000000000000 --- a/comments/migrations/0005_merge_20161219_2126.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.3 on 2016-12-19 20:26 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('comments', '0004_auto_20161212_1931'), - ('comments', '0004_auto_20161213_1208'), - ] - - operations = [ - ] diff --git a/comments/migrations/0006_auto_20170201_1243.py b/comments/migrations/0006_auto_20170201_1243.py new file mode 100644 index 0000000000000000000000000000000000000000..f13cde452317daf5ba4f231ef446d5d8311727c6 --- /dev/null +++ b/comments/migrations/0006_auto_20170201_1243.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-02-01 11:43 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone +import scipost.db.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('comments', '0005_auto_20170131_2141'), + ] + + operations = [ + migrations.AlterField( + model_name='comment', + name='created', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='comment', + name='latest_activity', + field=scipost.db.fields.AutoDateTimeField(blank=True, default=django.utils.timezone.now, editable=False), + ), + ] diff --git a/comments/models.py b/comments/models.py index 1e6c1c5d1da23a916f201a3e664b24e001ab0d7d..0c43a91ac3751dcd52e860481d7fca09dc30f12d 100644 --- a/comments/models.py +++ b/comments/models.py @@ -8,7 +8,7 @@ from django.utils.safestring import mark_safe from .models import * from commentaries.models import Commentary -from scipost.models import Contributor +from scipost.models import TimeStampedModel, Contributor from submissions.models import Submission, Report from theses.models import ThesisLink @@ -34,7 +34,7 @@ COMMENT_STATUS = ( comment_status_dict = dict(COMMENT_STATUS) -class Comment(models.Model): +class Comment(TimeStampedModel): """ A Comment is an unsollicited note, submitted by a Contributor, on a particular publication or in reply to an earlier Comment. """ @@ -42,7 +42,7 @@ class Comment(models.Model): vetted_by = models.ForeignKey(Contributor, blank=True, null=True, on_delete=models.CASCADE, related_name='comment_vetted_by') - # a Comment is either for a Commentary or Submission + # a Comment is either for a Commentary or Submission or a ThesisLink. commentary = models.ForeignKey(Commentary, blank=True, null=True, on_delete=models.CASCADE) submission = models.ForeignKey(Submission, blank=True, null=True, on_delete=models.CASCADE) thesislink = models.ForeignKey(ThesisLink, blank=True, null=True, on_delete=models.CASCADE) diff --git a/comments/templates/comments/reply_to_comment.html b/comments/templates/comments/reply_to_comment.html index 19cdddfbe573d4e6bbf83580e3ecf70311ed9652..89f725b9a82535c7f15bb1ecd4b86219741e4d21 100644 --- a/comments/templates/comments/reply_to_comment.html +++ b/comments/templates/comments/reply_to_comment.html @@ -32,21 +32,21 @@ {% if comment.commentary %} <h1>The Commentary concerned:</h1> {{ commentary.header_as_table }} - + <h3>Abstract:</h3> <p>{{ commentary.pub_abstract }}</p> {% endif %} {% if comment.submission %} <h1>The Submission concerned:</h1> {{ submission.header_as_table }} - + <h3>Abstract:</h3> <p>{{ submission.abstract }}</p> {% endif %} {% if comment.thesislink %} <h1>The Thesis concerned:</h1> - {{ thesislink.header_as_table }} - + {% include "theses/_header_as_table.html" with thesislink=thesislink %} + <h3>Abstract:</h3> <p>{{ thesislink.abstract }}</p> {% endif %} diff --git a/common/helpers/__init__.py b/common/helpers/__init__.py index a29cff832e8239e0ffc0cf1db98d51745c4c88c8..bf886509ae9e9ca883b2d8c5591811e5d355dbc1 100644 --- a/common/helpers/__init__.py +++ b/common/helpers/__init__.py @@ -27,4 +27,5 @@ def model_form_data(model, form_class): def filter_keys(dictionary, keys_to_keep): - return {key: dictionary[key] for key in 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/forms.py b/journals/forms.py index 8bbb70cc12ee333901d3525b0cb25849001b83dd..6df90b1adad9b4a9980ef1160749142dcc5be20e 100644 --- a/journals/forms.py +++ b/journals/forms.py @@ -1,14 +1,10 @@ from django import forms from django.utils import timezone -from .models import SCIPOST_JOURNALS from .models import UnregisteredAuthor, Issue, Publication from submissions.models import Submission -from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, Div, Field, HTML, Submit - class InitiatePublicationForm(forms.Form): accepted_submission = forms.ModelChoiceField( @@ -25,65 +21,13 @@ class InitiatePublicationForm(forms.Form): self.fields['acceptance_date'].widget.attrs.update( {'placeholder': 'YYYY-MM-DD'}) -# class InitiatePublicationForm(forms.ModelForm): -# class Meta: -# model = Publication -# fields = ['accepted_submission', -# #'in_journal', 'volume', 'issue', -# 'in_issue', 'paper_nr', -# 'pdf_file', -# 'submission_date', 'acceptance_date', -# ] -# # accepted_submission = forms.ModelChoiceField( -# # queryset=Submission.objects.filter(status='accepted')) -# # in_journal = forms.ChoiceField(choices=SCIPOST_JOURNALS,) -# # volume = forms.IntegerField() -# # issue = forms.IntegerField() - -# def __init__(self, *args, **kwargs): -# super(InitiatePublicationForm, self).__init__(*args, **kwargs) -# self.fields['accepted_submission'] = forms.ModelChoiceField( -# queryset=Submission.objects.filter(status='accepted')) -# self.fields['accepted_submission'].label='' -# #self.fields['in_journal'].label='' -# #self.fields['journal'].label='' -# #self.fields['volume'].label='' -# #self.fields['issue'].label='' -# self.fields['in_issue'].label='' -# self.fields['paper_nr'].label='' -# self.fields['acceptance_date'].label='' -# self.fields['submission_date'].label='' -# self.helper = FormHelper() -# self.helper.layout = Layout( -# Div(HTML('<h3>Which Submission is ready for publishing?</h3>'), -# Field('accepted_submission')), -# # Div(HTML('<h3>In which Journal?</h3>'), -# # #Field('in_journal')), -# # Field('journal')), -# # Div(HTML('<h3>Which Volume?</h3>'), -# # Field('volume')), -# # Div(HTML('<h3>Which issue?</h3>'), -# # Field('issue')), -# Div(HTML('<h3>Which Journal/Volume/Issue?</h3>'), -# Field('in_issue')), -# Div(HTML('<h3>Which paper number?</h3>'), -# Field('paper_nr')), -# Div(HTML('<h3>pdf file (post-proof stage):</h3>'), -# Field('pdf_file')), -# Div(HTML('<h3>When was the paper originally submitted?</h3>'), -# Field('submission_date')), -# Div(HTML('<h3>When was the paper accepted?</h3>'), -# Field('acceptance_date')), -# Submit('submit', 'Initiate publication'), -# ) - class ValidatePublicationForm(forms.ModelForm): class Meta: model = Publication exclude = ['authors', 'authors_claims', 'authors_false_claims', 'metadata', 'metadata_xml', - 'latest_activity',] + 'latest_activity', ] class UnregisteredAuthorForm(forms.ModelForm): diff --git a/journals/models.py b/journals/models.py index f5b65028034176f1c7e97d1ee01d00958df8defc..fe7ea251f2451258975ae7ecc607e8166154e63c 100644 --- a/journals/models.py +++ b/journals/models.py @@ -1,10 +1,10 @@ -from django.contrib.postgres.fields import ArrayField, JSONField +from django.contrib.postgres.fields import JSONField from django.db import models from django.template import Template, Context from django.utils import timezone -from scipost.constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS, subject_areas_dict -from scipost.models import ChoiceArrayField, Contributor, TITLE_CHOICES +from scipost.constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS +from scipost.models import ChoiceArrayField, Contributor class UnregisteredAuthor(models.Model): @@ -30,6 +30,7 @@ class JournalNameError(Exception): def __str__(self): return self.name + def journal_name_abbrev_citation(journal_name): if journal_name == 'SciPost Physics': return 'SciPost Phys.' @@ -40,6 +41,7 @@ def journal_name_abbrev_citation(journal_name): else: raise JournalNameError(journal_name) + def journal_name_abbrev_doi(journal_name): if journal_name == 'SciPost Physics': return 'SciPostPhys' @@ -50,12 +52,15 @@ def journal_name_abbrev_doi(journal_name): else: raise JournalNameError(journal_name) + class PaperNumberError(Exception): def __init__(self, nr): self.nr = nr + def __str__(self): return self.nr + def paper_nr_string(nr): if nr < 10: return '00' + str(nr) @@ -66,13 +71,16 @@ def paper_nr_string(nr): else: raise PaperNumberError(nr) + class PaperNumberingError(Exception): def __init__(self, nr): self.nr = nr + def __str__(self): return self.nr -SCIPOST_JOURNALS_SUBMIT = ( # Same as SCIPOST_JOURNALS, but SP Select deactivated + +SCIPOST_JOURNALS_SUBMIT = ( # Same as SCIPOST_JOURNALS, but SP Select deactivated ('SciPost Physics', 'SciPost Physics'), ('SciPost Physics Lecture Notes', 'SciPost Physics Lecture Notes'), ) @@ -104,7 +112,6 @@ SCIPOST_JOURNALS_SPECIALIZATIONS = ( journals_spec_dict = dict(SCIPOST_JOURNALS_SPECIALIZATIONS) - class Journal(models.Model): name = models.CharField(max_length=100, choices=SCIPOST_JOURNALS, unique=True) @@ -137,8 +144,6 @@ class Issue(models.Model): def __str__(self): text = str(self.in_volume) + ' issue ' + str(self.number) - #if self.until_date >= timezone.now().date(): - # text += ' (in progress)' if self.start_date.month == self.until_date.month: text += ' (' + self.until_date.strftime('%B') + ' ' + self.until_date.strftime('%Y') + ')' else: @@ -146,7 +151,7 @@ class Issue(models.Model): ' ' + self.until_date.strftime('%Y') + ')') return text - def period (self): + def period(self): text = 'up to {{ until_month }} {{ year }}' template = Template(text) context = Context({'until_month': self.start_date.strftime('%B'), @@ -168,23 +173,23 @@ class Publication(models.Model): title = models.CharField(max_length=300) author_list = models.CharField(max_length=1000, verbose_name="author list") # Authors which have been mapped to contributors: - authors = models.ManyToManyField (Contributor, blank=True, related_name='authors_pub') - authors_unregistered = models.ManyToManyField (UnregisteredAuthor, blank=True, - related_name='authors_unregistered') - first_author = models.ForeignKey (Contributor, blank=True, null=True, on_delete=models.CASCADE) - first_author_unregistered = models.ForeignKey (UnregisteredAuthor, blank=True, null=True, - on_delete=models.CASCADE, - related_name='first_author_unregistered') - authors_claims = models.ManyToManyField (Contributor, blank=True, - related_name='authors_pub_claims') - authors_false_claims = models.ManyToManyField (Contributor, blank=True, - related_name='authors_pub_false_claims') + authors = models.ManyToManyField(Contributor, blank=True, related_name='authors_pub') + authors_unregistered = models.ManyToManyField(UnregisteredAuthor, blank=True, + related_name='authors_unregistered') + first_author = models.ForeignKey(Contributor, blank=True, null=True, on_delete=models.CASCADE) + first_author_unregistered = models.ForeignKey(UnregisteredAuthor, blank=True, null=True, + on_delete=models.CASCADE, + related_name='first_author_unregistered') + authors_claims = models.ManyToManyField(Contributor, blank=True, + related_name='authors_pub_claims') + authors_false_claims = models.ManyToManyField(Contributor, blank=True, + related_name='authors_pub_false_claims') abstract = models.TextField() pdf_file = models.FileField(upload_to='UPLOADS/PUBLICATIONS/%Y/%m/', max_length=200) metadata = JSONField(default={}, blank=True, null=True) - metadata_xml = models.TextField(blank=True, null=True) # for Crossref deposit + metadata_xml = models.TextField(blank=True, null=True) # for Crossref deposit BiBTeX_entry = models.TextField(blank=True, null=True) - doi_label = models.CharField(max_length=200, blank=True, null=True) # Used for file name + doi_label = models.CharField(max_length=200, blank=True, null=True) # Used for file name doi_string = models.CharField(max_length=200, blank=True, null=True) submission_date = models.DateField(verbose_name='submission date') acceptance_date = models.DateField(verbose_name='acceptance date') @@ -192,22 +197,20 @@ class Publication(models.Model): latest_activity = models.DateTimeField(default=timezone.now) citedby = JSONField(default={}, blank=True, null=True) - def __str__ (self): + def __str__(self): header = (self.citation() + ', ' + self.title[:30] + ' by ' + self.author_list[:30] + ', published ' + self.publication_date.strftime('%Y-%m-%d')) return header - def citation (self): + def citation(self): return (journal_name_abbrev_citation(self.in_issue.in_volume.in_journal.name) + ' ' + str(self.in_issue.in_volume.number) - #+ '(' + str(self.in_issue.number) + ')' + ', ' + paper_nr_string(self.paper_nr) - + ' (' + self.publication_date.strftime('%Y') + ')' ) + + ' (' + self.publication_date.strftime('%Y') + ')') - def citation_for_web (self): + def citation_for_web(self): citation = ('{{ abbrev }} <strong>{{ volume_nr }}</strong>' - #'({{ issue_nr }})' ', {{ paper_nr }} ({{ year }})') template = Template(citation) context = Context( @@ -215,13 +218,12 @@ class Publication(models.Model): 'volume_nr': str(self.in_issue.in_volume.number), 'issue_nr': str(self.in_issue.number), 'paper_nr': paper_nr_string(self.paper_nr), - 'year': self.publication_date.strftime('%Y'),}) + 'year': self.publication_date.strftime('%Y'), }) return template.render(context) - def citation_for_web_linked (self): + def citation_for_web_linked(self): citation = ('<a href="{% url \'scipost:publication_detail\' doi_string=doi_string %}">' '{{ abbrev }} <strong>{{ volume_nr }}</strong>' - #'({{ issue_nr }})' ', {{ paper_nr }} ({{ year }})') template = Template(citation) context = Context( @@ -230,20 +232,10 @@ class Publication(models.Model): 'volume_nr': str(self.in_issue.in_volume.number), 'issue_nr': str(self.in_issue.number), 'paper_nr': paper_nr_string(self.paper_nr), - 'year': self.publication_date.strftime('%Y'),}) + 'year': self.publication_date.strftime('%Y'), }) return template.render(context) - - # def doi_label_as_str(self): - # label = ( - # journal_name_abbrev_doi(self.in_issue.in_volume.in_journal.name) - # + '.' + str(self.in_issue.in_volume.number) - # + '.' + str(self.in_issue.number) - # + '.' + paper_nr_string(self.paper_nr) ) - # return label - - - def header_as_li (self): + def header_as_li(self): header = ('<li class="publicationHeader">' '<p class="publicationTitle"><a href="{% url \'scipost:publication_detail\' doi_string=doi_string %}">{{ title }}</a></p>' '<p class="publicationAuthors">{{ author_list }}</p>' @@ -256,17 +248,17 @@ class Publication(models.Model): '</ul>' '</li>') template = Template(header) - context = Context({'doi_string': self.doi_string, - 'title': self.title, - 'author_list': self.author_list, - 'citation': self.citation, - 'pub_date': self.publication_date.strftime('%d %B %Y'), - 'abstract': self.abstract, - }) + context = Context({ + 'doi_string': self.doi_string, + 'title': self.title, + 'author_list': self.author_list, + 'citation': self.citation, + 'pub_date': self.publication_date.strftime('%d %B %Y'), + 'abstract': self.abstract, + }) return template.render(context) - - def details (self): + def details(self): """ This method is called from the publication_detail template. It provides all the details for a publication. @@ -292,15 +284,16 @@ class Publication(models.Model): '<h2>BiBTeX</h2><p>{{ BiBTeX|linebreaks }}</p></div></div>' ) template = Template(pub_details) - context = Context({'title': self.title, - 'author_list': self.author_list, - 'citation': self.citation_for_web, - 'pub_date': self.publication_date.strftime('%d %B %Y'), - 'abstract': self.abstract, - 'doi_string': self.doi_string, - 'BiBTeX': self.BiBTeX_entry, - 'arxiv_identifier_w_vn_nr': self.accepted_submission.arxiv_identifier_w_vn_nr - }) + context = Context({ + 'title': self.title, + 'author_list': self.author_list, + 'citation': self.citation_for_web, + 'pub_date': self.publication_date.strftime('%d %B %Y'), + 'abstract': self.abstract, + 'doi_string': self.doi_string, + 'BiBTeX': self.BiBTeX_entry, + 'arxiv_identifier_w_vn_nr': self.accepted_submission.arxiv_identifier_w_vn_nr + }) return template.render(context) def citations_as_ul(self): @@ -314,11 +307,13 @@ class Publication(models.Model): if cit['multiauthors']: output += ' <em>et al.</em>' output += (', <em>{{ title_' + str(nr) + ' }}</em>, <br/>' - '{{ journal_abbrev_' + str(nr) + ' }} ' - '<strong>{{ volume_' + str(nr) + ' }}</strong>, ') + '{{ journal_abbrev_' + str(nr) + ' }}') context['title_' + str(nr)] = cit['article_title'] context['journal_abbrev_' + str(nr)] = cit['journal_abbreviation'] - context['volume_' + str(nr)] = cit['volume'] + if cit['volume']: + context['volume_' + str(nr)] = cit['volume'] + output += ' <strong>{{ volume_' + str(nr) + ' }}</strong>' + output += ', ' if cit['first_page']: output += '{{ first_page_' + str(nr) + ' }}' context['first_page_' + str(nr)] = cit['first_page'] @@ -350,5 +345,5 @@ class Deposit(models.Model): deposition_date = models.DateTimeField(default=timezone.now) def __str__(self): - return (deposition_date.strftime('%Y-%m-%D') + - ' for ' + publication.doi_string) + return (self.deposition_date.strftime('%Y-%m-%D') + + ' for ' + self.publication.doi_string) diff --git a/journals/templates/journals/journals_terms_and_conditions.html b/journals/templates/journals/journals_terms_and_conditions.html index 83e6074c226fb6ad5807706ed5b5e25935c59037..b6b316c7d3a75c9f3dc8e316613d172cf111809d 100644 --- a/journals/templates/journals/journals_terms_and_conditions.html +++ b/journals/templates/journals/journals_terms_and_conditions.html @@ -19,7 +19,8 @@ <br/> <hr class="hr12"/> <h2>General</h2> - SciPost expects the following from submitters: + + <h3>SciPost expects the following from submitters:</h3> <ul> <li>The manuscript which is submitted for publication has not been published before except in the form of an abstract or electronic preprint or other similar formats which have not undergone @@ -58,9 +59,15 @@ respect to the material contained herein. Any opinions expressed in SciPost journals are the views of the authors and are not the views of SciPost.</li> </ul> + <br/> + <hr class="hr12"/> + + <h2>Open Access policy</h2> + <p>All SciPost Journals are Open Access which means that all content is freely available without charge to the user or his/her institution. Users are allowed to read, download, copy, distribute, print, search, or link to the full texts of the articles, or use them for any other lawful purpose, without asking prior permission from the publisher or the author. This is in accordance with the BOAI definition of Open Access.</p> <br/> <hr class="hr12"/> + <h2 id="license_and_copyright_agreement">License and copyright agreement</h2> <p>The following license and copyright agreement is valid for any article published in any SciPost journal and web portal.</p> diff --git a/journals/utils.py b/journals/utils.py index 0e5845e8d4a2f99b380dde570ef96df0e8886e20..71d5cae1cd91cb6373334c5fd1d76b607bdb32bf 100644 --- a/journals/utils.py +++ b/journals/utils.py @@ -2,6 +2,7 @@ from django.core.mail import EmailMessage from scipost.models import title_dict + class JournalUtils(object): @classmethod @@ -9,7 +10,6 @@ class JournalUtils(object): for var_name in dict: setattr(cls, var_name, dict[var_name]) - @classmethod def send_authors_paper_published_email(cls): """ Requires loading 'publication' attribute. """ @@ -38,7 +38,6 @@ class JournalUtils(object): reply_to=['admin@scipost.org']) emailmessage.send(fail_silently=False) - @classmethod def generate_metadata_xml_file(cls): """ Requires loading 'publication' attribute. """ diff --git a/journals/views.py b/journals/views.py index e3ba969601240c8255c1caa91a267b871f3e1eb1..9c2225f27be5d5af83244795bd41bc3b54fbd4fe 100644 --- a/journals/views.py +++ b/journals/views.py @@ -1,4 +1,3 @@ -import datetime import hashlib import os import random @@ -7,72 +6,35 @@ import string import xml.etree.ElementTree as ET from django.conf import settings -#from django.core import serializers from django.utils import timezone from django.shortcuts import get_object_or_404, render, redirect -from django.contrib.auth import authenticate, login, logout -from django.contrib.auth.models import User from django.core.files import File -from django.core.mail import EmailMessage from django.core.urlresolvers import reverse from django.db import transaction -from django.http import HttpResponse, HttpResponseRedirect -from django.views.decorators.csrf import csrf_protect -from django.db.models import Avg +from django.http import HttpResponse -from .models import * -from .forms import * +from .models import Issue, Publication, PaperNumberingError,\ + journal_name_abbrev_doi, paper_nr_string, journal_name_abbrev_citation,\ + UnregisteredAuthor +from .forms import FundingInfoForm, InitiatePublicationForm, ValidatePublicationForm,\ + UnregisteredAuthorForm, CreateMetadataXMLForm, CitationListBibitemsForm +from .utils import JournalUtils -from journals.utils import JournalUtils - -from submissions.models import SUBMISSION_STATUS_PUBLICLY_UNLISTED from submissions.models import Submission +from scipost.models import Contributor from guardian.decorators import permission_required -from guardian.decorators import permission_required_or_403 -from guardian.shortcuts import assign_perm - - -# from requests.adapters import HTTPAdapter -# from requests.packages.urllib3.poolmanager import PoolManager -# import ssl - -# class MyAdapter(HTTPAdapter): -# def init_poolmanager(self, connections, maxsize, block=False): -# self.poolmanager = PoolManager(num_pools=connections, -# maxsize=maxsize, -# block=block, -# ssl_version=ssl.PROTOCOL_TLSv1) ############ # Journals ############ - -# Utilities - - -# @permission_required('scipost.can_publish_accepted_submission', return_403=True) -# @transaction.atomic -# def open_new_issue(request): -# """ -# For a Journal/Volume, creates a new issue. -# """ - -# settings.JOURNALS_DIR -# + journal_name_abbrev_doi(publication.in_issue.in_volume.in_journal.name) -# + '/' + str(publication.in_issue.in_volume.number) -# + '/' + str(publication.in_issue.number) - - def journals(request): return render(request, 'journals/journals.html') def scipost_physics(request): - #issues = Issue.objects.filter( - # in_volume__in_journal__name='SciPost Physics').order_by('-until_date') current_issue = Issue.objects.filter( in_volume__in_journal__name='SciPost Physics', start_date__lte=timezone.now(), @@ -80,23 +42,9 @@ def scipost_physics(request): latest_issue = Issue.objects.filter( in_volume__in_journal__name='SciPost Physics', until_date__lte=timezone.now()).order_by('-until_date').first() - #recent_papers = Publication.objects.filter( - # #in_issue=latest_issue).order_by('paper_nr') - # in_issue__in_volume__in_journal__name='SciPost Physics').order_by('-publication_date')[:20] - #accepted_SP_submissions = Submission.objects.filter( - # submitted_to_journal='SciPost Physics', status='accepted' - #).order_by('-latest_activity') - #current_SP_submissions = Submission.objects.filter( - # submitted_to_journal='SciPost Physics' - # ).exclude(status__in=SUBMISSION_STATUS_PUBLICLY_UNLISTED - # ).order_by('-submission_date') context = { - #'issues': issues, 'current_issue': current_issue, - 'latest_issue': latest_issue, - #'recent_papers': recent_papers, - #'accepted_SP_submissions': accepted_SP_submissions, - #'current_SP_submissions': current_SP_submissions, + 'latest_issue': latest_issue } return render(request, 'journals/scipost_physics.html', context) @@ -104,7 +52,7 @@ def scipost_physics(request): def scipost_physics_issues(request): issues = Issue.objects.filter( in_volume__in_journal__name='SciPost Physics').order_by('-until_date') - context = {'issues': issues,} + context = {'issues': issues, } return render(request, 'journals/scipost_physics_issues.html', context) @@ -112,13 +60,10 @@ def scipost_physics_recent(request): """ Display page for the most recent 20 publications in SciPost Physics. """ - #latest_issue = Issue.objects.filter( - # in_volume__in_journal__name='SciPost Physics').order_by('-until_date').first() + recent_papers = Publication.objects.filter( - #in_issue=latest_issue).order_by('-publication_date') in_issue__in_volume__in_journal__name='SciPost Physics').order_by('-publication_date')[:20] - context = {#'latest_issue': latest_issue, - 'recent_papers': recent_papers} + context = {'recent_papers': recent_papers} return render(request, 'journals/scipost_physics_recent.html', context) @@ -134,19 +79,6 @@ def scipost_physics_accepted(request): return render(request, 'journals/scipost_physics_accepted.html', context) -# def scipost_physics_submissions(request): -# """ -# Display page for submissions to SciPost Physics which -# have been accepted but are not yet published. -# """ -# current_SP_submissions = Submission.objects.filter( -# submitted_to_journal='SciPost Physics' -# ).exclude(status__in=SUBMISSION_STATUS_PUBLICLY_UNLISTED -# ).order_by('-submission_date') -# context = {'current_SP_submissions': current_SP_submissions} -# return render(request, 'journals/scipost_physics_submissions.html', context) - - def scipost_physics_info_for_authors(request): return render(request, 'journals/scipost_physics_info_for_authors.html') @@ -155,16 +87,14 @@ def scipost_physics_about(request): return render(request, 'journals/scipost_physics_about.html') - def scipost_physics_issue_detail(request, volume_nr, issue_nr): - issue = get_object_or_404 (Issue, in_volume__in_journal__name='SciPost Physics', - number=issue_nr) + issue = get_object_or_404(Issue, in_volume__in_journal__name='SciPost Physics', + number=issue_nr) papers = issue.publication_set.order_by('paper_nr') context = {'issue': issue, 'papers': papers} return render(request, 'journals/scipost_physics_issue_detail.html', context) - ####################### # Publication process # ####################### @@ -187,6 +117,7 @@ def upload_proofs(request): """ return render(request, 'journals/upload_proofs.html') + @permission_required('scipost.can_publish_accepted_submission', return_403=True) @transaction.atomic def initiate_publication(request): @@ -199,16 +130,15 @@ def initiate_publication(request): if request.method == 'POST': initiate_publication_form = InitiatePublicationForm(request.POST) if initiate_publication_form.is_valid(): - submission = get_object_or_404(Submission, - pk=initiate_publication_form.cleaned_data['accepted_submission'].id) - current_issue = get_object_or_404(Issue, - pk=initiate_publication_form.cleaned_data['to_be_issued_in'].id) + submission = get_object_or_404(Submission, pk=initiate_publication_form.cleaned_data[ + 'accepted_submission'].id) + current_issue = get_object_or_404(Issue, pk=initiate_publication_form.cleaned_data[ + 'to_be_issued_in'].id) + # Determine next available paper number: - #papers_in_current_issue = Publication.objects.filter(in_issue=current_issue) papers_in_current_volume = Publication.objects.filter( in_issue__in_volume=current_issue.in_volume) paper_nr = 1 - #while papers_in_current_issue.filter(paper_nr=paper_nr).exists(): while papers_in_current_volume.filter(paper_nr=paper_nr).exists(): paper_nr += 1 if paper_nr > 999: @@ -218,7 +148,7 @@ def initiate_publication(request): + '.' + str(current_issue.in_volume.number) + '.' + str(current_issue.number) + '.' + paper_nr_string(paper_nr) ) - doi_string='10.21468/' + doi_label + doi_string = '10.21468/' + doi_label BiBTeX_entry = ( '@Article{' + doi_label + ',\n' '\ttitle={{' + submission.title + '}},\n' @@ -255,7 +185,7 @@ def initiate_publication(request): 'latest_activity': timezone.now(), } validate_publication_form = ValidatePublicationForm(initial=initial) - context = {'validate_publication_form': validate_publication_form,} + context = {'validate_publication_form': validate_publication_form, } return render(request, 'journals/validate_publication.html', context) else: errormessage = 'The form was not filled validly.' @@ -292,8 +222,8 @@ def validate_publication(request): publication.save() # Move file to final location initial_path = publication.pdf_file.path - new_dir = (publication.in_issue.path + '/' - + paper_nr_string(publication.paper_nr)) + new_dir = (publication.in_issue.path + '/' + + paper_nr_string(publication.paper_nr)) new_path = new_dir + '/' + publication.doi_label.replace('.', '_') + '.pdf' os.makedirs(new_dir) os.rename(initial_path, new_path) @@ -308,7 +238,7 @@ def validate_publication(request): JournalUtils.load({'publication': publication}) JournalUtils.send_authors_paper_published_email() ack_header = 'The publication has been validated.' - context = {'ack_header': ack_header,} + context = {'ack_header': ack_header, } return render(request, 'scipost/acknowledgement.html', context) else: errormessage = 'The form was invalid.' @@ -331,7 +261,7 @@ def mark_first_author(request, publication_id, contributor_id): publication.first_author_unregistered = None publication.save() return redirect(reverse('scipost:publication_detail', - kwargs={'doi_string': publication.doi_string,})) + kwargs={'doi_string': publication.doi_string, })) @permission_required('scipost.can_publish_accepted_submission', return_403=True) @@ -343,7 +273,7 @@ def mark_first_author_unregistered(request, publication_id, unregistered_author_ publication.first_author_unregistered = unregistered_author publication.save() return redirect(reverse('scipost:publication_detail', - kwargs={'doi_string': publication.doi_string,})) + kwargs={'doi_string': publication.doi_string, })) @permission_required('scipost.can_publish_accepted_submission', return_403=True) @@ -361,7 +291,7 @@ def add_author(request, publication_id, contributor_id=None, unregistered_author publication.authors.add(contributor) publication.save() return redirect(reverse('scipost:publication_detail', - kwargs={'doi_string': publication.doi_string,})) + kwargs={'doi_string': publication.doi_string, })) if request.method == 'POST': form = UnregisteredAuthorForm(request.POST) if form.is_valid(): @@ -371,7 +301,7 @@ def add_author(request, publication_id, contributor_id=None, unregistered_author last_name__icontains=form.cleaned_data['last_name']) new_unreg_author_form = UnregisteredAuthorForm( initial={'first_name': form.cleaned_data['first_name'], - 'last_name': form.cleaned_data['last_name'],}) + 'last_name': form.cleaned_data['last_name'], }) else: errormessage = 'Please fill in the form properly' return render(request, 'scipost/error.html', context={'errormessage': errormessage}) @@ -384,7 +314,7 @@ def add_author(request, publication_id, contributor_id=None, unregistered_author 'contributors_found': contributors_found, 'unregistered_authors_found': unregistered_authors_found, 'form': form, - 'new_unreg_author_form': new_unreg_author_form,} + 'new_unreg_author_form': new_unreg_author_form, } return render(request, 'journals/add_author.html', context) @@ -396,7 +326,7 @@ def add_unregistered_author(request, publication_id, unregistered_author_id): publication.unregistered_authors.add(unregistered_author) publication.save() return redirect(reverse('scipost:publication_detail', - kwargs={'doi_string': publication.doi_string,})) + kwargs={'doi_string': publication.doi_string, })) @permission_required('scipost.can_publish_accepted_submission', return_403=True) @@ -407,12 +337,12 @@ def add_new_unreg_author(request, publication_id): new_unreg_author_form = UnregisteredAuthorForm(request.POST) if new_unreg_author_form.is_valid(): new_unreg_author = UnregisteredAuthor( - first_name = new_unreg_author_form.cleaned_data['first_name'], - last_name = new_unreg_author_form.cleaned_data['last_name'],) + first_name=new_unreg_author_form.cleaned_data['first_name'], + last_name=new_unreg_author_form.cleaned_data['last_name'],) new_unreg_author.save() publication.authors_unregistered.add(new_unreg_author) return redirect(reverse('scipost:publication_detail', - kwargs={'doi_string': publication.doi_string,})) + kwargs={'doi_string': publication.doi_string, })) errormessage = 'Method add_new_unreg_author can only be called with POST.' return render(request, 'scipost/error.html', context={'errormessage': errormessage}) @@ -432,16 +362,17 @@ def create_citation_list_metadata(request, doi_string): publication.metadata['citation_list'] = [] entries_list = bibitems_form.cleaned_data['latex_bibitems'].split('\doi{') nentries = 1 - for entry in entries_list[1:]: # drop first bit before first \doi{ + for entry in entries_list[1:]: # drop first bit before first \doi{ publication.metadata['citation_list'].append( {'key': 'ref' + str(nentries), - 'doi': entry.partition('}')[0],} + 'doi': entry.partition('}')[0], } ) nentries += 1 publication.save() bibitems_form = CitationListBibitemsForm() - context = {'publication': publication, - 'bibitems_form': bibitems_form, + context = { + 'publication': publication, + 'bibitems_form': bibitems_form, } if request.method == 'POST': context['citation_list'] = publication.metadata['citation_list'] @@ -460,10 +391,11 @@ def create_funding_info_metadata(request, doi_string): if request.method == 'POST': funding_info_form = FundingInfoForm(request.POST) if funding_info_form.is_valid(): - publication.metadata['funding_statement'] = funding_info_form.cleaned_data['funding_statement'] + publication.metadata['funding_statement'] = funding_info_form.cleaned_data[ + 'funding_statement'] publication.save() - initial = {'funding_statement': '',} + initial = {'funding_statement': '', } funding_statement = '' try: initial['funding_statement'] = publication.metadata['funding_statement'] @@ -472,7 +404,7 @@ def create_funding_info_metadata(request, doi_string): pass context = {'publication': publication, 'funding_info_form': FundingInfoForm(initial=initial), - 'funding_statement': funding_statement,} + 'funding_statement': funding_statement, } return render(request, 'journals/create_funding_info_metadata.html', context) @@ -494,7 +426,7 @@ def create_metadata_xml(request, doi_string): publication.metadata_xml = create_metadata_xml_form.cleaned_data['metadata_xml'] publication.save() return redirect(reverse('scipost:publication_detail', - kwargs={'doi_string': publication.doi_string,})) + kwargs={'doi_string': publication.doi_string, })) # create a doi_batch_id salt = "" @@ -505,7 +437,6 @@ def create_metadata_xml(request, doi_string): idsalt = idsalt.encode('utf8') doi_batch_id = hashlib.sha1(salt+idsalt).hexdigest() - #publication.metadata_xml = ( initial = {'metadata_xml': ''} initial['metadata_xml'] += ( '<?xml version="1.0" encoding="UTF-8"?>\n' @@ -553,44 +484,37 @@ def create_metadata_xml(request, doi_string): # this to be checked by EdAdmin before publishing. for author in publication.authors.all(): if author == publication.first_author: - #publication.metadata_xml += ( initial['metadata_xml'] += ( '<person_name sequence=\'first\' contributor_role=\'author\'> ' '<given_name>' + author.user.first_name + '</given_name> ' '<surname>' + author.user.last_name + '</surname> ' ) else: - #publication.metadata_xml += ( initial['metadata_xml'] += ( '<person_name sequence=\'additional\' contributor_role=\'author\'> ' '<given_name>' + author.user.first_name + '</given_name> ' '<surname>' + author.user.last_name + '</surname> ' ) if author.orcid_id: - #publication.metadata_xml += '<ORCID>http://orcid.org' + author.orcid_id + '</ORCID>' initial['metadata_xml'] += '<ORCID>http://orcid.org/' + author.orcid_id + '</ORCID>' initial['metadata_xml'] += '</person_name>\n' for author_unreg in publication.authors_unregistered.all(): if author_unreg == publication.first_author_unregistered: - #publication.metadata_xml += ( initial['metadata_xml'] += ( '<person_name sequence=\'first\' contributor_role=\'author\'> ' '<given_name>' + author_unreg.first_name + '</given_name> ' '<surname>' + author_unreg.last_name + '</surname> ' ) else: - #publication.metadata_xml += ( initial['metadata_xml'] += ( '<person_name sequence=\'additional\' contributor_role=\'author\'> ' '<given_name>' + author_unreg.first_name + '</given_name> ' '<surname>' + author_unreg.last_name + '</surname> ' ) initial['metadata_xml'] += '</person_name>\n' - #publication.metadata_xml += '</contributors>\n' initial['metadata_xml'] += '</contributors>\n' - #publication.metadata_xml += ( initial['metadata_xml'] += ( '<publication_date media_type=\'online\'>\n' '<month>' + publication.publication_date.strftime('%m') + '</month>' @@ -612,29 +536,22 @@ def create_metadata_xml(request, doi_string): ) try: if publication.metadata['citation_list']: - #publication.metadata_xml += '<citation_list>\n' initial['metadata_xml'] += '<citation_list>\n' for ref in publication.metadata['citation_list']: - #publication.metadata_xml += ( initial['metadata_xml'] += ( '<citation key="' + ref['key'] + '">' '<doi>' + ref['doi'] + '</doi>' '</citation>\n' ) - #publication.metadata_xml += '</citation_list>\n' initial['metadata_xml'] += '</citation_list>\n' except KeyError: pass - #publication.metadata_xml += ( initial['metadata_xml'] += ( '</journal_article>\n' '</journal>\n' ) - #publication.metadata_xml += '</body>\n</doi_batch>' initial['metadata_xml'] += '</body>\n</doi_batch>' publication.save() - #else: - # errormessage = 'The form was invalidly filled.' context = {'publication': publication, 'create_metadata_xml_form': CreateMetadataXMLForm(initial=initial), @@ -642,41 +559,6 @@ def create_metadata_xml(request, doi_string): return render(request, 'journals/create_metadata_xml.html', context) -# @permission_required('scipost.can_publish_accepted_submission', return_403=True) -# @transaction.atomic -# def test_metadata_xml_deposit(request, doi_string): -# """ -# Prior to the actual Crossref metadata deposit, -# test the metadata_xml using the Crossref test server. -# Makes use of the python requests module. -# """ -# publication = get_object_or_404 (Publication, doi_string=doi_string) -# url = 'http://test.crossref.org/servlet/deposit' -# #headers = {'Content-type': 'multipart/form-data'} -# params = {'operation': 'doMDUpload', -# 'login_id': settings.CROSSREF_LOGIN_ID, -# 'login_passwd': settings.CROSSREF_LOGIN_PASSWORD, -# } -# #files = {'fname': ('metadata.xml', publication.metadata_xml, 'multipart/form-data', {'Expires': '0'})} -# files = {'fname': ('metadata.xml', publication.metadata_xml, 'multipart/form-data')} -# r = requests.post(url, -# params=params, -# files=files, -# #verify=settings.CERTFILE, -# #verify=False, -# ) -# #s = requests.Session() -# #s.mount('https://', MyAdapter()) -# #r = s.post(url, params=params, files=files) -# response_headers = r.headers -# response_text = r.text -# context = {'publication': publication, -# 'response_headers': response_headers, -# 'response_text': response_text, -# } -# return render(request, 'journals/test_metadata_xml_deposit.html', context) - - @permission_required('scipost.can_publish_accepted_submission', return_403=True) @transaction.atomic def metadata_xml_deposit(request, doi_string, option='test'): @@ -685,30 +567,32 @@ def metadata_xml_deposit(request, doi_string, option='test'): If test==True, test the metadata_xml using the Crossref test server. Makes use of the python requests module. """ - publication = get_object_or_404 (Publication, doi_string=doi_string) - if option=='deposit': + publication = get_object_or_404(Publication, doi_string=doi_string) + if option == 'deposit': url = 'http://doi.crossref.org/servlet/deposit' - elif option=='test': + elif option == 'test': url = 'http://test.crossref.org/servlet/deposit' else: - {'errormessage': 'metadata_xml_deposit can only be called with options test or deposit',} + errormessage = 'metadata_xml_deposit can only be called with options test or deposit' return render(request, 'scipost/error.html', context={'errormessage': errormessage}) - params = {'operation': 'doMDUpload', - 'login_id': settings.CROSSREF_LOGIN_ID, - 'login_passwd': settings.CROSSREF_LOGIN_PASSWORD, - } + params = { + 'operation': 'doMDUpload', + 'login_id': settings.CROSSREF_LOGIN_ID, + 'login_passwd': settings.CROSSREF_LOGIN_PASSWORD, + } files = {'fname': ('metadata.xml', publication.metadata_xml, 'multipart/form-data')} r = requests.post(url, params=params, files=files, - ) + ) response_headers = r.headers response_text = r.text - context = {'option': option, - 'publication': publication, - 'response_headers': response_headers, - 'response_text': response_text, + context = { + 'option': option, + 'publication': publication, + 'response_headers': response_headers, + 'response_text': response_text, } return render(request, 'journals/metadata_xml_deposit.html', context) @@ -744,10 +628,9 @@ def harvest_citedby_links(request, doi_string): params = {'usr': settings.CROSSREF_LOGIN_ID, 'pwd': settings.CROSSREF_LOGIN_PASSWORD, 'qdata': query_xml, - 'doi': publication.doi_string,} + 'doi': publication.doi_string, } r = requests.post(url, params=params,) response_headers = r.headers - #response_text = bytes(r.text, 'utf-8').decode('unicode_escape') response_text = r.text response_deserialized = ET.fromstring(r.text) prefix = '{http://www.crossref.org/qrschema/2.0}' @@ -757,7 +640,10 @@ def harvest_citedby_links(request, doi_string): article_title = link.find(prefix + 'journal_cite').find(prefix + 'article_title').text journal_abbreviation = link.find(prefix + 'journal_cite').find( prefix + 'journal_abbreviation').text - volume = link.find(prefix + 'journal_cite').find(prefix + 'volume').text + try: + volume = link.find(prefix + 'journal_cite').find(prefix + 'volume').text + except AttributeError: + volume = None try: first_page = link.find(prefix + 'journal_cite').find(prefix + 'first_page').text except: @@ -783,31 +669,31 @@ def harvest_citedby_links(request, doi_string): 'volume': volume, 'first_page': first_page, 'item_number': item_number, - 'year': year,}) + 'year': year, }) publication.citedby = citations publication.save() - context = {'publication': publication, - 'response_headers': response_headers, - 'response_text': response_text, - 'response_deserialized': response_deserialized, - 'citations': citations, + context = { + 'publication': publication, + 'response_headers': response_headers, + 'response_text': response_text, + 'response_deserialized': response_deserialized, + 'citations': citations, } return render(request, 'journals/harvest_citedby_links.html', context) - ########### # Viewing # ########### def publication_detail(request, doi_string): - publication = get_object_or_404 (Publication, doi_string=doi_string) - context = {'publication': publication,} + publication = get_object_or_404(Publication, doi_string=doi_string) + context = {'publication': publication, } return render(request, 'journals/publication_detail.html', context) def publication_pdf(request, doi_string): - publication = get_object_or_404 (Publication, doi_string=doi_string) + publication = get_object_or_404(Publication, doi_string=doi_string) pdf = File(publication.pdf_file) response = HttpResponse(pdf, content_type='application/pdf') response['Content-Disposition'] = ('filename=' @@ -816,13 +702,13 @@ def publication_pdf(request, doi_string): def publication_detail_from_doi_label(request, doi_label): - publication = get_object_or_404 (Publication, doi_label=doi_label) - context = {'publication': publication,} + publication = get_object_or_404(Publication, doi_label=doi_label) + context = {'publication': publication, } return render(request, 'journals/publication_detail.html', context) def publication_pdf_from_doi_label(request, doi_label): - publication = get_object_or_404 (Publication, doi_label=doi_label) + publication = get_object_or_404(Publication, doi_label=doi_label) pdf = File(publication.pdf_file) response = HttpResponse(pdf, content_type='application/pdf') response['Content-Disposition'] = ('filename=' diff --git a/scipost/admin.py b/scipost/admin.py index 3c7650cc21f679532b38d44ebae768826dcbde6e..799517e9b63c9541937e8669a9945ea2361b77f9 100644 --- a/scipost/admin.py +++ b/scipost/admin.py @@ -20,6 +20,30 @@ admin.site.unregister(User) admin.site.register(User, UserAdmin) +class VGMAdmin(admin.ModelAdmin): + search_fields = ['start_date'] + +admin.site.register(VGM, VGMAdmin) + + +class FeedbackAdmin(admin.ModelAdmin): + search_fields = ['feedback', 'by'] + +admin.site.register(Feedback, FeedbackAdmin) + + +class NominationAdmin(admin.ModelAdmin): + search_fields = ['last_name', 'first_name', 'by'] + +admin.site.register(Nomination, NominationAdmin) + + +class MotionAdmin(admin.ModelAdmin): + search_fields = ['background', 'motion', 'put_forward_by'] + +admin.site.register(Motion, MotionAdmin) + + class RemarkAdmin(admin.ModelAdmin): search_fields = ['contributor', 'remark'] diff --git a/scipost/db/fields.py b/scipost/db/fields.py new file mode 100644 index 0000000000000000000000000000000000000000..ebbd6e74100d71dbb5985cb9692f76c10fd13b8d --- /dev/null +++ b/scipost/db/fields.py @@ -0,0 +1,14 @@ +from django.db import models +from django.utils import timezone + + +class AutoDateTimeField(models.DateTimeField): + '''Create an auto_now DateTimeField instead of auto_now.''' + + def __init__(self, *args, **kwargs): + kwargs['editable'] = False + kwargs['blank'] = True + super(AutoDateTimeField, self).__init__(*args, **kwargs) + + def pre_save(self, model_instance, add): + return timezone.now() diff --git a/scipost/factories.py b/scipost/factories.py index ea308dfa5fa1bdbbade9f17ccaf2420028c36b1f..27ebc93ef459df1de461cf65af874680d0ecbb48 100644 --- a/scipost/factories.py +++ b/scipost/factories.py @@ -16,6 +16,14 @@ class ContributorFactory(factory.django.DjangoModelFactory): vetted_by = factory.SubFactory('scipost.factories.ContributorFactory', vetted_by=None) +class VettingEditorFactory(ContributorFactory): + @factory.post_generation + def add_to_vetting_editors(self, create, extracted, **kwargs): + if not create: + return + self.user.groups.add(Group.objects.get(name="Vetting Editors")) + + class UserFactory(factory.django.DjangoModelFactory): class Meta: model = get_user_model() diff --git a/scipost/forms.py b/scipost/forms.py index d58a27352b45f3827e88aef7ae39e47e43ab7514..b4d7422a13dda2bc52cc11d3d2c9a4e228c00234 100644 --- a/scipost/forms.py +++ b/scipost/forms.py @@ -1,5 +1,6 @@ from django import forms +from django.contrib.auth.models import User, Group from django.db.models import Q from django_countries import countries @@ -8,10 +9,15 @@ from django_countries.fields import LazyTypedChoiceField from captcha.fields import CaptchaField from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, Div, Field, Fieldset, HTML, Submit +from crispy_forms.layout import Layout, Div, Field, HTML, Submit -from .models import * -from .constants import SCIPOST_DISCIPLINES +from .constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS +from .models import TITLE_CHOICES, SCIPOST_FROM_ADDRESSES, ARC_LENGTHS,\ + Contributor, DraftInvitation, RegistrationInvitation,\ + SupportingPartner, SPBMembershipAgreement,\ + UnavailabilityPeriod, PrecookedEmail,\ + List, Team, Graph, Node,\ + Feedback, Nomination, Motion from journals.models import Publication from submissions.models import SUBMISSION_STATUS_PUBLICLY_UNLISTED @@ -26,6 +32,7 @@ REGISTRATION_REFUSAL_CHOICES = ( ) reg_ref_dict = dict(REGISTRATION_REFUSAL_CHOICES) + class RegistrationForm(forms.Form): title = forms.ChoiceField(choices=TITLE_CHOICES, label='* Title') first_name = forms.CharField(label='* First name', max_length=100) @@ -38,7 +45,9 @@ class RegistrationForm(forms.Form): discipline = forms.ChoiceField(choices=SCIPOST_DISCIPLINES, label='* Main discipline') country_of_employment = LazyTypedChoiceField( choices=countries, label='* Country of employment', initial='NL', - widget=CountrySelectWidget(layout='{widget}<img class="country-select-flag" id="{flag_id}" style="margin: 6px 4px 0" src="{country.flag}">')) + widget=CountrySelectWidget(layout=( + '{widget}<img class="country-select-flag" id="{flag_id}"' + ' style="margin: 6px 4px 0" src="{country.flag}">'))) affiliation = forms.CharField(label='* Affiliation', max_length=300) address = forms.CharField( label='Address', max_length=1000, @@ -60,7 +69,7 @@ class DraftInvitationForm(forms.ModelForm): fields = ['title', 'first_name', 'last_name', 'email', 'invitation_type', 'cited_in_submission', 'cited_in_publication' - ] + ] def __init__(self, *args, **kwargs): super(DraftInvitationForm, self).__init__(*args, **kwargs) @@ -94,12 +103,13 @@ class RegistrationInvitationForm(forms.ModelForm): 'invitation_type', 'cited_in_submission', 'cited_in_publication', 'message_style', 'personal_message' - ] + ] def __init__(self, *args, **kwargs): super(RegistrationInvitationForm, self).__init__(*args, **kwargs) self.fields['personal_message'].widget.attrs.update( - {'placeholder': 'NOTE: a personal phrase or two. The bulk of the text will be auto-generated.'}) + {'placeholder': ('NOTE: a personal phrase or two.' + ' The bulk of the text will be auto-generated.')}) self.fields['cited_in_submission'] = forms.ModelChoiceField( queryset=Submission.objects.all().exclude( status__in=SUBMISSION_STATUS_PUBLICLY_UNLISTED).order_by('-submission_date'), @@ -124,6 +134,7 @@ class RegistrationInvitationForm(forms.ModelForm): Div(Field('cited_in_publication'),), ) + class ModifyPersonalMessageForm(forms.Form): personal_message = forms.CharField(widget=forms.Textarea()) @@ -133,37 +144,44 @@ class UpdateUserDataForm(forms.ModelForm): model = User fields = ['email', 'first_name', 'last_name'] + class UpdatePersonalDataForm(forms.ModelForm): class Meta: model = Contributor fields = ['title', 'discipline', 'expertises', 'orcid_id', 'country_of_employment', 'affiliation', 'address', 'personalwebpage', 'accepts_SciPost_emails' - ] + ] widgets = {'country_of_employment': CountrySelectWidget()} + class VetRegistrationForm(forms.Form): - promote_to_registered_contributor = forms.BooleanField(required=False, label='Accept registration') + promote_to_registered_contributor = forms.BooleanField(required=False, + label='Accept registration') refuse = forms.BooleanField(required=False) refusal_reason = forms.ChoiceField(choices=REGISTRATION_REFUSAL_CHOICES, required=False) email_response_field = forms.CharField(widget=forms.Textarea(), label='Justification (optional)', required=False) + class AuthenticationForm(forms.Form): username = forms.CharField(label='Username', max_length=100) password = forms.CharField(label='Password', widget=forms.PasswordInput()) + class PasswordChangeForm(forms.Form): password_prev = forms.CharField(label='Existing password', widget=forms.PasswordInput()) password_new = forms.CharField(label='New password', widget=forms.PasswordInput()) password_verif = forms.CharField(label='Reenter new password', widget=forms.PasswordInput()) + AUTHORSHIP_CLAIM_CHOICES = ( ('-', '-'), ('True', 'I am an author'), ('False', 'I am not an author'), ) + class AuthorshipClaimForm(forms.Form): claim = forms.ChoiceField(choices=AUTHORSHIP_CLAIM_CHOICES, required=False) @@ -185,12 +203,14 @@ class RemarkForm(forms.Form): def __init__(self, *args, **kwargs): super(RemarkForm, self).__init__(*args, **kwargs) self.fields['remark'].widget.attrs.update( - {'rows': 3, 'cols': 40, 'placeholder': 'Enter your remarks here. You can use LaTeX in $...$ or \[ \].'}) + {'rows': 3, 'cols': 40, + 'placeholder': 'Enter your remarks here. You can use LaTeX in $...$ or \[ \].'}) class SearchForm(forms.Form): query = forms.CharField(max_length=100, label='', - widget=forms.TextInput(attrs={'class': 'form-control mr-0 mb-2 mr-lg-2 mb-lg-0'})) + widget=forms.TextInput(attrs={ + 'class': 'form-control mr-0 mb-2 mr-lg-2 mb-lg-0'})) class EmailGroupMembersForm(forms.Form): @@ -290,7 +310,7 @@ class ManageTeamsForm(forms.Form): def __init__(self, *args, **kwargs): contributor = kwargs.pop('contributor') super(ManageTeamsForm, self).__init__(*args, **kwargs) - self.fields['teams_with_access'].queryset=Team.objects.filter( + self.fields['teams_with_access'].queryset = Team.objects.filter( Q(leader=contributor) | Q(members__in=[contributor])) self.fields['teams_with_access'].widget.attrs.update( {'placeholder': 'Team(s) to be given access rights:'}) @@ -314,7 +334,6 @@ class CreateArcForm(forms.Form): self.fields['target'].queryset = Node.objects.filter(graph=graph) - ############################# # Supporting Partners Board # ############################# @@ -325,13 +344,13 @@ class SupportingPartnerForm(forms.ModelForm): fields = ['partner_type', 'institution', 'institution_acronym', 'institution_address', 'consortium_members' - ] + ] def __init__(self, *args, **kwargs): super(SupportingPartnerForm, self).__init__(*args, **kwargs) - self.fields['institution_address'].widget = forms.Textarea({'rows': 8,}) + self.fields['institution_address'].widget = forms.Textarea({'rows': 8, }) self.fields['consortium_members'].widget.attrs.update( - {'placeholder': 'Please list the names of the institutions within the consortium',}) + {'placeholder': 'Please list the names of the institutions within the consortium', }) self.helper = FormHelper() self.helper.layout = Layout( Div( @@ -347,6 +366,7 @@ class SupportingPartnerForm(forms.ModelForm): css_class='row') ) + class SPBMembershipForm(forms.ModelForm): class Meta: model = SPBMembershipAgreement @@ -371,3 +391,60 @@ class SPBMembershipForm(forms.ModelForm): css_class="col-4"), css_class="row"), ) + + +################# +# VGMs, Motions # +################# + +class FeedbackForm(forms.ModelForm): + class Meta: + model = Feedback + fields = ['feedback'] + + +class NominationForm(forms.ModelForm): + class Meta: + model = Nomination + fields = ['first_name', 'last_name', + 'discipline', 'expertises', 'webpage'] + + def __init__(self, *args, **kwargs): + super(NominationForm, self).__init__(*args, **kwargs) + self.fields['expertises'].widget = forms.SelectMultiple(choices=SCIPOST_SUBJECT_AREAS) + + +class MotionForm(forms.ModelForm): + class Meta: + model = Motion + fields = ['category', 'background', 'motion'] + + def __init__(self, *args, **kwargs): + super(MotionForm, self).__init__(*args, **kwargs) + self.fields['background'].label = '' + self.fields['background'].widget.attrs.update( + {'rows': 8, 'cols': 100, + 'placeholder': 'Provide useful background information on your Motion.'}) + self.fields['motion'].label = '' + self.fields['motion'].widget.attrs.update( + {'rows': 8, 'cols': 100, + 'placeholder': 'Phrase your Motion as clearly and succinctly as possible.'}) + self.helper = FormHelper() + self.helper.layout = Layout( + Field('category'), + Div( + Div(HTML('<p>Background:</p>'), + css_class="col-2"), + Div( + Field('background'), + css_class="col-10"), + css_class="row"), + Div( + Div(HTML('<p>Motion:</p>'), + css_class="col-2"), + Div( + Field('motion'), + css_class="col-10"), + css_class="row"), + Submit('submit', 'Submit'), + ) diff --git a/scipost/global_methods.py b/scipost/global_methods.py index 1115ac33f4b96fe12074bb93def181aad67ebee1..3510aef9ea26803d370bc4035445190599249f0e 100644 --- a/scipost/global_methods.py +++ b/scipost/global_methods.py @@ -1,9 +1,10 @@ -from django.contrib.auth.models import User +from .models import Contributor -from .models import * class Global(object): + ''' Is this thing really being used?''' @classmethod def get_contributor(cls, request): + '''This should be fixed within the user model itself?''' Contributor.objects.get(user=request.user) diff --git a/scipost/management/commands/add_groups_and_permissions.py b/scipost/management/commands/add_groups_and_permissions.py index 7dbe007f4d9199739afb4ca4256122dc8ff9d178..e3977e696ae23584a4b2f846d1e5a10834fe08c2 100644 --- a/scipost/management/commands/add_groups_and_permissions.py +++ b/scipost/management/commands/add_groups_and_permissions.py @@ -1,6 +1,6 @@ -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand -from django.contrib.auth.models import Group, Permission, User +from django.contrib.auth.models import Group, Permission from django.contrib.contenttypes.models import ContentType from scipost.models import Contributor @@ -9,15 +9,6 @@ from scipost.models import Contributor class Command(BaseCommand): help = 'Defines groups and permissions' - def add_arguments(self, parser): - """Append arguments optionally for setup of Contributor roles.""" - parser.add_argument('-u', '--setup-user', metavar='<username>', type=str, required=False, - help='Username to make registered contributor') - parser.add_argument('-a', '--make-admin', required=False, action='store_true', - help='Grant admin permissions to user (superuser only)') - parser.add_argument('-t', '--make-tester', required=False, action='store_true', - help='Grant test permissions to user') - def handle(self, *args, **options): """Append all user Groups and setup a Contributor roles to user.""" @@ -27,11 +18,13 @@ class Command(BaseCommand): EditorialAdmin, created = Group.objects.get_or_create(name='Editorial Administrators') EditorialCollege, created = Group.objects.get_or_create(name='Editorial College') VettingEditors, created = Group.objects.get_or_create(name='Vetting Editors') - RegisteredContributors, created = Group.objects.get_or_create(name='Registered Contributors') + RegisteredContributors, created = Group.objects.get_or_create( + name='Registered Contributors') Developers, created = Group.objects.get_or_create(name='Developers') Testers, created = Group.objects.get_or_create(name='Testers') Ambassadors, created = Group.objects.get_or_create(name='Ambassadors') JuniorAmbassadors, created = Group.objects.get_or_create(name='Junior Ambassadors') + ProductionOfficers, created = Group.objects.get_or_create(name='Production Officers') # Create Permissions content_type = ContentType.objects.get_for_model(Contributor) @@ -69,6 +62,10 @@ class Command(BaseCommand): codename='view_bylaws', name='Can view By-laws of Editorial College', content_type=content_type) + can_attend_VGMs, created = Permission.objects.get_or_create( + codename='can_attend_VGMs', + name='Can attend Virtual General Meetings', + content_type=content_type) # Contributions (not related to submissions) can_submit_comments, created = Permission.objects.get_or_create( @@ -158,7 +155,6 @@ class Command(BaseCommand): name='Can view docs: scipost', content_type=content_type) - # Assign permissions to groups SciPostAdmin.permissions.add( can_manage_registration_invitations, @@ -173,9 +169,11 @@ class Command(BaseCommand): can_assign_submissions, can_prepare_recommendations_for_voting, can_fix_College_decision, + can_attend_VGMs, ) AdvisoryBoard.permissions.add( can_manage_registration_invitations, + can_attend_VGMs, ) EditorialAdmin.permissions.add( can_view_pool, @@ -183,12 +181,14 @@ class Command(BaseCommand): can_prepare_recommendations_for_voting, can_fix_College_decision, can_publish_accepted_submission, + can_attend_VGMs, ) EditorialCollege.permissions.add( can_view_pool, can_take_charge_of_submissions, can_vet_submitted_reports, view_bylaws, + can_attend_VGMs, ) VettingEditors.permissions.add( can_vet_commentary_requests, @@ -213,29 +213,8 @@ class Command(BaseCommand): JuniorAmbassadors.permissions.add( can_draft_registration_invitations, ) + ProductionOfficers.permissions.add( + can_view_docs_scipost, + ) self.stdout.write(self.style.SUCCESS('Successfully created groups and permissions.')) - - if options['setup_user']: - # Username is given, check options - try: - user = User.objects.get(username=str(options['setup_user'])) - except User.DoesNotExist: - self.stdout.write(self.style.WARNING('User <%s> not found.' % options['update_user'])) - return None - - user.groups.add(RegisteredContributors) - self.stdout.write(self.style.SUCCESS('Successfully setup %s as contributor.' % user)) - - if user.is_superuser and options['make_admin']: - # Setup admin contributor - user.groups.add(SciPostAdmin) - self.stdout.write(self.style.SUCCESS('Successfully made %s admin.' % user)) - elif options['make_admin']: - # Make admin failed, user not a superuser - self.stdout.write(self.style.WARNING('User %s is not a superuser.' % user)) - - if options['make_tester']: - # Setup test contributor - user.groups.add(Testers) - self.stdout.write(self.style.SUCCESS('Successfully made %s tester.' % user)) diff --git a/scipost/migrations/0030_auto_20170118_1406.py b/scipost/migrations/0030_auto_20170118_1406.py new file mode 100644 index 0000000000000000000000000000000000000000..c938c3a399e751dec47bdabb829fed2c6a0cbc88 --- /dev/null +++ b/scipost/migrations/0030_auto_20170118_1406.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-01-18 13:06 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0029_remark_submission'), + ] + + operations = [ + migrations.CreateModel( + name='Motion', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('background', models.TextField()), + ('motion', models.TextField()), + ('date', models.DateField()), + ('nr_A', models.PositiveIntegerField(default=0)), + ('nr_N', models.PositiveIntegerField(default=0)), + ('nr_D', models.PositiveIntegerField(default=0)), + ('voting_deadline', models.DateTimeField(default=django.utils.timezone.now, verbose_name='voting deadline')), + ('accepted', models.NullBooleanField()), + ], + ), + migrations.CreateModel( + name='VGM', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('start_date', models.DateField()), + ('end_date', models.DateField()), + ], + ), + migrations.AddField( + model_name='motion', + name='VGM', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='scipost.VGM'), + ), + migrations.AddField( + model_name='motion', + name='in_agreement', + field=models.ManyToManyField(blank=True, related_name='in_agreement_with_motion', to='scipost.Contributor'), + ), + migrations.AddField( + model_name='motion', + name='in_disagreement', + field=models.ManyToManyField(blank=True, related_name='in_disagreement_with_motion', to='scipost.Contributor'), + ), + migrations.AddField( + model_name='motion', + name='in_notsure', + field=models.ManyToManyField(blank=True, related_name='in_notsure_with_motion', to='scipost.Contributor'), + ), + migrations.AddField( + model_name='motion', + name='put_forward_by', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='scipost.Contributor'), + ), + ] diff --git a/scipost/migrations/0031_remark_motion.py b/scipost/migrations/0031_remark_motion.py new file mode 100644 index 0000000000000000000000000000000000000000..cd4f9fc8bc5659a59d3794d4031f3de1c31c76b3 --- /dev/null +++ b/scipost/migrations/0031_remark_motion.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-01-18 16:45 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0030_auto_20170118_1406'), + ] + + operations = [ + migrations.AddField( + model_name='remark', + name='motion', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='scipost.Motion'), + ), + ] diff --git a/scipost/migrations/0032_auto_20170121_1032.py b/scipost/migrations/0032_auto_20170121_1032.py new file mode 100644 index 0000000000000000000000000000000000000000..0ed75e0e513916b1b25223e3aea4a3d11e62339a --- /dev/null +++ b/scipost/migrations/0032_auto_20170121_1032.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-01-21 09:32 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import scipost.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0031_remark_motion'), + ] + + operations = [ + migrations.CreateModel( + name='Nomination', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField()), + ('first_name', models.CharField(default='', max_length=30)), + ('last_name', models.CharField(default='', max_length=30)), + ('discipline', models.CharField(choices=[('physics', 'Physics'), ('astrophysics', 'Astrophysics'), ('mathematics', 'Mathematics'), ('computerscience', 'Computer Science')], default='physics', max_length=20, verbose_name='Main discipline')), + ('expertises', scipost.models.ChoiceArrayField(base_field=models.CharField(choices=[('Physics', (('Phys:AE', 'Atomic, Molecular and Optical Physics - Experiment'), ('Phys:AT', 'Atomic, Molecular and Optical Physics - Theory'), ('Phys:BI', 'Biophysics'), ('Phys:CE', 'Condensed Matter Physics - Experiment'), ('Phys:CT', 'Condensed Matter Physics - Theory'), ('Phys:FD', 'Fluid Dynamics'), ('Phys:GR', 'Gravitation, Cosmology and Astroparticle Physics'), ('Phys:HE', 'High-Energy Physics - Experiment'), ('Phys:HT', 'High-Energy Physics- Theory'), ('Phys:HP', 'High-Energy Physics - Phenomenology'), ('Phys:MP', 'Mathematical Physics'), ('Phys:NE', 'Nuclear Physics - Experiment'), ('Phys:NT', 'Nuclear Physics - Theory'), ('Phys:QP', 'Quantum Physics'), ('Phys:SM', 'Statistical and Soft Matter Physics'))), ('Astrophysics', (('Astro:GA', 'Astrophysics of Galaxies'), ('Astro:CO', 'Cosmology and Nongalactic Astrophysics'), ('Astro:EP', 'Earth and Planetary Astrophysics'), ('Astro:HE', 'High Energy Astrophysical Phenomena'), ('Astro:IM', 'Instrumentation and Methods for Astrophysics'), ('Astro:SR', 'Solar and Stellar Astrophysics'))), ('Mathematics', (('Math:AG', 'Algebraic Geometry'), ('Math:AT', 'Algebraic Topology'), ('Math:AP', 'Analysis of PDEs'), ('Math:CT', 'Category Theory'), ('Math:CA', 'Classical Analysis and ODEs'), ('Math:CO', 'Combinatorics'), ('Math:AC', 'Commutative Algebra'), ('Math:CV', 'Complex Variables'), ('Math:DG', 'Differential Geometry'), ('Math:DS', 'Dynamical Systems'), ('Math:FA', 'Functional Analysis'), ('Math:GM', 'General Mathematics'), ('Math:GN', 'General Topology'), ('Math:GT', 'Geometric Topology'), ('Math:GR', 'Group Theory'), ('Math:HO', 'History and Overview'), ('Math:IT', 'Information Theory'), ('Math:KT', 'K-Theory and Homology'), ('Math:LO', 'Logic'), ('Math:MP', 'Mathematical Physics'), ('Math:MG', 'Metric Geometry'), ('Math:NT', 'Number Theory'), ('Math:NA', 'Numerical Analysis'), ('Math:OA', 'Operator Algebras'), ('Math:OC', 'Optimization and Control'), ('Math:PR', 'Probability'), ('Math:QA', 'Quantum Algebra'), ('Math:RT', 'Representation Theory'), ('Math:RA', 'Rings and Algebras'), ('Math:SP', 'Spectral Theory'), ('Math:ST', 'Statistics Theory'), ('Math:SG', 'Symplectic Geometry'))), ('Computer Science', (('Comp:AI', 'Artificial Intelligence'), ('Comp:CC', 'Computational Complexity'), ('Comp:CE', 'Computational Engineering, Finance, and Science'), ('Comp:CG', 'Computational Geometry'), ('Comp:GT', 'Computer Science and Game Theory'), ('Comp:CV', 'Computer Vision and Pattern Recognition'), ('Comp:CY', 'Computers and Society'), ('Comp:CR', 'Cryptography and Security'), ('Comp:DS', 'Data Structures and Algorithms'), ('Comp:DB', 'Databases'), ('Comp:DL', 'Digital Libraries'), ('Comp:DM', 'Discrete Mathematics'), ('Comp:DC', 'Distributed, Parallel, and Cluster Computing'), ('Comp:ET', 'Emerging Technologies'), ('Comp:FL', 'Formal Languages and Automata Theory'), ('Comp:GL', 'General Literature'), ('Comp:GR', 'Graphics'), ('Comp:AR', 'Hardware Architecture'), ('Comp:HC', 'Human-Computer Interaction'), ('Comp:IR', 'Information Retrieval'), ('Comp:IT', 'Information Theory'), ('Comp:LG', 'Learning'), ('Comp:LO', 'Logic in Computer Science'), ('Comp:MS', 'Mathematical Software'), ('Comp:MA', 'Multiagent Systems'), ('Comp:MM', 'Multimedia'), ('Comp:NI', 'Networking and Internet Architecture'), ('Comp:NE', 'Neural and Evolutionary Computing'), ('Comp:NA', 'Numerical Analysis'), ('Comp:OS', 'Operating Systems'), ('Comp:OH', 'Other Computer Science'), ('Comp:PF', 'Performance'), ('Comp:PL', 'Programming Languages'), ('Comp:RO', 'Robotics'), ('Comp:SI', 'Social and Information Networks'), ('Comp:SE', 'Software Engineering'), ('Comp:SD', 'Sound'), ('Comp:SC', 'Symbolic Computation'), ('Comp:SY', 'Systems and Control')))], max_length=10), blank=True, null=True, size=None)), + ('nr_A', models.PositiveIntegerField(default=0)), + ('nr_N', models.PositiveIntegerField(default=0)), + ('nr_D', models.PositiveIntegerField(default=0)), + ('voting_deadline', models.DateTimeField(default=django.utils.timezone.now, verbose_name='voting deadline')), + ('accepted', models.NullBooleanField()), + ('by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='scipost.Contributor')), + ('in_agreement', models.ManyToManyField(blank=True, related_name='in_agreement_with_nomination', to='scipost.Contributor')), + ('in_disagreement', models.ManyToManyField(blank=True, related_name='in_disagreement_with_nomination', to='scipost.Contributor')), + ('in_notsure', models.ManyToManyField(blank=True, related_name='in_notsure_with_nomination', to='scipost.Contributor')), + ], + ), + migrations.AddField( + model_name='remark', + name='nomination', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='scipost.Nomination'), + ), + ] diff --git a/scipost/migrations/0033_nomination_vgm.py b/scipost/migrations/0033_nomination_vgm.py new file mode 100644 index 0000000000000000000000000000000000000000..ea1713d6ae70acbcc619b6140f7e8b12c2000168 --- /dev/null +++ b/scipost/migrations/0033_nomination_vgm.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-01-21 09:53 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0032_auto_20170121_1032'), + ] + + operations = [ + migrations.AddField( + model_name='nomination', + name='VGM', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='scipost.VGM'), + ), + ] diff --git a/scipost/migrations/0034_motion_category.py b/scipost/migrations/0034_motion_category.py new file mode 100644 index 0000000000000000000000000000000000000000..2f3ab3c6657ecc2fc6ecced854612b826fb79b74 --- /dev/null +++ b/scipost/migrations/0034_motion_category.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-01-21 10:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0033_nomination_vgm'), + ] + + operations = [ + migrations.AddField( + model_name='motion', + name='category', + field=models.CharField(choices=[('ByLawAmend', 'Amendments to by-laws'), ('Workflow', 'Editorial workflow improvements'), ('General', 'General')], default='General', max_length=10), + ), + ] diff --git a/scipost/migrations/0035_vgm_information.py b/scipost/migrations/0035_vgm_information.py new file mode 100644 index 0000000000000000000000000000000000000000..9037d41378cc0095783a212485b4700f42c8fb24 --- /dev/null +++ b/scipost/migrations/0035_vgm_information.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-01-21 13:19 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0034_motion_category'), + ] + + operations = [ + migrations.AddField( + model_name='vgm', + name='information', + field=models.TextField(default=''), + ), + ] diff --git a/scipost/migrations/0036_feedback.py b/scipost/migrations/0036_feedback.py new file mode 100644 index 0000000000000000000000000000000000000000..ed0da2b20125fbfd2c9d6c083adcf7983e6db1b6 --- /dev/null +++ b/scipost/migrations/0036_feedback.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-01-21 14:17 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0035_vgm_information'), + ] + + operations = [ + migrations.CreateModel( + name='Feedback', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField()), + ('feedback', models.TextField()), + ('VGM', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='scipost.VGM')), + ('by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='scipost.Contributor')), + ], + ), + ] diff --git a/scipost/migrations/0037_remark_feedback.py b/scipost/migrations/0037_remark_feedback.py new file mode 100644 index 0000000000000000000000000000000000000000..2c07b7f723a2b95fffed6f1d1903369939e56f0b --- /dev/null +++ b/scipost/migrations/0037_remark_feedback.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-01-21 18:22 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0036_feedback'), + ] + + operations = [ + migrations.AddField( + model_name='remark', + name='feedback', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='scipost.Feedback'), + ), + ] diff --git a/scipost/migrations/0038_nomination_webpage.py b/scipost/migrations/0038_nomination_webpage.py new file mode 100644 index 0000000000000000000000000000000000000000..f53718c7133b45b12ba7d8969630b31e5c6004ee --- /dev/null +++ b/scipost/migrations/0038_nomination_webpage.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-01-24 09:18 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0037_remark_feedback'), + ] + + operations = [ + migrations.AddField( + model_name='nomination', + name='webpage', + field=models.URLField(default=''), + ), + ] diff --git a/scipost/models.py b/scipost/models.py index 85f5b085086c85eb376ba8b7886131d78cddeb56..1a4ca8bc79b79dc9b85fbf1d9c4e060e18141750 100644 --- a/scipost/models.py +++ b/scipost/models.py @@ -1,9 +1,10 @@ import datetime from django import forms -from django.contrib.auth.models import User, Group -from django.contrib.postgres.fields import ArrayField, JSONField +from django.contrib.auth.models import User +from django.contrib.postgres.fields import ArrayField from django.db import models +from django.shortcuts import get_object_or_404 from django.template import Template, Context from django.utils import timezone from django.utils.safestring import mark_safe @@ -12,8 +13,7 @@ from django_countries.fields import CountryField from .constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS,\ disciplines_dict, subject_areas_dict - -from scipost.models import * +from .db.fields import AutoDateTimeField class ChoiceArrayField(ArrayField): @@ -66,8 +66,8 @@ class TimeStampedModel(models.Model): This will ensure the creation of created and modified timestamps in the objects. """ - created = models.DateTimeField(auto_now_add=True) - latest_activity = models.DateTimeField(auto_now=True) + created = models.DateTimeField(default=timezone.now) + latest_activity = AutoDateTimeField(default=timezone.now) class Meta: abstract = True @@ -151,7 +151,6 @@ class Contributor(models.Model): }) return template.render(context) - def public_info_as_table(self): """Prints out all publicly-accessible info as a table.""" @@ -185,11 +184,19 @@ class Contributor(models.Model): def expertises_as_ul(self): output = '<ul>' - for exp in self.expertises: - output += '<li>%s</li>' % subject_areas_dict[exp] + if self.expertises: + for exp in self.expertises: + output += '<li>%s</li>' % subject_areas_dict[exp] output += '</ul>' return mark_safe(output) + def expertises_as_string(self): + output = '' + if self.expertises: + for exp in self.expertises: + output += subject_areas_dict[exp] + ', ' + return output + def assignments_summary_as_td(self): assignments = self.editorialassignment_set.all() nr_ongoing = assignments.filter(accepted=True, completed=False).count() @@ -244,6 +251,12 @@ class UnavailabilityPeriod(models.Model): class Remark(models.Model): contributor = models.ForeignKey(Contributor, on_delete=models.CASCADE) + feedback = models.ForeignKey('scipost.Feedback', on_delete=models.CASCADE, + blank=True, null=True) + nomination = models.ForeignKey('scipost.Nomination', on_delete=models.CASCADE, + blank=True, null=True) + motion = models.ForeignKey('scipost.Motion', on_delete=models.CASCADE, + blank=True, null=True) submission = models.ForeignKey('submissions.Submission', on_delete=models.CASCADE, blank=True, null=True) @@ -254,22 +267,21 @@ class Remark(models.Model): remark = models.TextField() def __str__(self): - return (title_dict[self.contributor.title] + ' ' - + self.contributor.user.first_name + ' ' + return (self.contributor.user.first_name + ' ' + self.contributor.user.last_name + ' on ' + self.date.strftime("%Y-%m-%d")) def as_li(self): - output = '<li>{{ by }}<p>{{ remark }}</p>' + output = '<li><em>{{ by }}</em><p>{{ remark }}</p>' context = Context({'by': str(self), 'remark': self.remark}) template = Template(output) return template.render(context) -################## -## Invitations ### -################## +############### +# Invitations # +############### INVITATION_TYPE = ( ('F', 'Editorial Fellow'), @@ -284,6 +296,7 @@ INVITATION_STYLE = ( ('P', 'personal'), ) + class DraftInvitation(models.Model): """ Draft of an invitation, filled in by an officer. @@ -305,7 +318,7 @@ class DraftInvitation(models.Model): date_drafted = models.DateTimeField(default=timezone.now) processed = models.BooleanField(default=False) - def __str__ (self): + def __str__(self): return (self.invitation_type + ' ' + self.first_name + ' ' + self.last_name) @@ -337,7 +350,7 @@ class RegistrationInvitation(models.Model): responded = models.BooleanField(default=False) declined = models.BooleanField(default=False) - def __str__ (self): + def __str__(self): return (self.invitation_type + ' ' + self.first_name + ' ' + self.last_name + ' on ' + self.date_sent.strftime("%Y-%m-%d")) @@ -362,12 +375,14 @@ class CitationNotification(models.Model): text += ' (processed)' return text + AUTHORSHIP_CLAIM_STATUS = ( (1, 'accepted'), (0, 'not yet vetted (pending)'), (-1, 'rejected'), ) + class AuthorshipClaim(models.Model): claimant = models.ForeignKey(Contributor, on_delete=models.CASCADE, @@ -381,9 +396,9 @@ class AuthorshipClaim(models.Model): thesislink = models.ForeignKey('theses.ThesisLink', on_delete=models.CASCADE, blank=True, null=True) - vetted_by = models.ForeignKey (Contributor, - on_delete=models.CASCADE, - blank=True, null=True) + vetted_by = models.ForeignKey(Contributor, + on_delete=models.CASCADE, + blank=True, null=True) status = models.SmallIntegerField(choices=AUTHORSHIP_CLAIM_STATUS, default=0) @@ -433,10 +448,10 @@ class NewsItem(models.Model): '<h3 class="NewsHeadline">{{ headline }}</h3>' '<p>{{ date }}</p>' '<p>{{ blurb }}</p>' - ) + ) context = Context({'headline': self.headline, 'date': self.date.strftime('%Y-%m-%d'), - 'blurb': self.blurb,}) + 'blurb': self.blurb, }) if self.followup_link: descriptor += '<p><a href="{{ followup_link }}">{{ followup_link_text }}</a></p>' context['followup_link'] = self.followup_link @@ -445,16 +460,15 @@ class NewsItem(models.Model): template = Template(descriptor) return template.render(context) - def descriptor_small(self): """ For index page. """ descriptor = ('<h3 class="NewsHeadline">{{ headline }}</h3>' '<p>{{ date }}</p>' '<p>{{ blurb }}</p>' - ) + ) context = Context({'headline': self.headline, 'date': self.date.strftime('%Y-%m-%d'), - 'blurb': self.blurb,}) + 'blurb': self.blurb, }) if self.followup_link: descriptor += '<p><a href="{{ followup_link }}">{{ followup_link_text }}</a></p>' context['followup_link'] = self.followup_link @@ -463,6 +477,219 @@ class NewsItem(models.Model): return template.render(context) +##################################### +# Virtual General Meetings, Motions # +##################################### + +class VGM(models.Model): + """ + Each year, a Virtual General Meeting is held during which operations at + SciPost are discussed. A VGM can be attended by Administrators, + Advisory Board members and Editorial Fellows. + """ + start_date = models.DateField() + end_date = models.DateField() + information = models.TextField(default='') + + def __str__(self): + return 'From %s to %s' % (self.start_date.strftime('%Y-%m-%d'), + self.end_date.strftime('%Y-%m-%d')) + + +class Feedback(models.Model): + """ + Feedback, suggestion or criticism on any aspect of SciPost. + """ + VGM = models.ForeignKey(VGM, blank=True, null=True) + by = models.ForeignKey(Contributor) + date = models.DateField() + feedback = models.TextField() + + def __str__(self): + return '%s: %s' % (self.by, self.feedback[:50]) + + def as_li(self): + html = ('<div class="Feedback">' + '<h3><em>by {{ by }}</em></h3>' + '<p>{{ feedback|linebreaks }}</p>' + '</div>') + context = Context({ + 'feedback': self.feedback, + 'by': '%s %s' % (self.by.user.first_name, + self.by.user.last_name)}) + template = Template(html) + return template.render(context) + + +class Nomination(models.Model): + """ + Nomination to an Editorial Fellowship. + """ + VGM = models.ForeignKey(VGM, blank=True, null=True) + by = models.ForeignKey(Contributor) + date = models.DateField() + first_name = models.CharField(max_length=30, default='') + last_name = models.CharField(max_length=30, default='') + discipline = models.CharField(max_length=20, choices=SCIPOST_DISCIPLINES, + default='physics', verbose_name='Main discipline') + expertises = ChoiceArrayField( + models.CharField(max_length=10, choices=SCIPOST_SUBJECT_AREAS), + blank=True, null=True) + webpage = models.URLField(default='') + nr_A = models.PositiveIntegerField(default=0) + in_agreement = models.ManyToManyField(Contributor, + related_name='in_agreement_with_nomination', blank=True) + nr_N = models.PositiveIntegerField(default=0) + in_notsure = models.ManyToManyField(Contributor, + related_name='in_notsure_with_nomination', blank=True) + nr_D = models.PositiveIntegerField(default=0) + in_disagreement = models.ManyToManyField(Contributor, + related_name='in_disagreement_with_nomination', + blank=True) + voting_deadline = models.DateTimeField('voting deadline', default=timezone.now) + accepted = models.NullBooleanField() + + def __str__(self): + return '%s %s (nominated by %s)' % (self.first_name, + self.last_name, + self.by) + + def as_li(self): + html = ('<div class="Nomination" id="nomination_id{{ nomination_id }}" ' + 'style="background-color: #eeeeee;">' + '<div class="row">' + '<div class="col-4">' + '<h3><em> {{ name }}</em></h3>' + '<p>Nominated by {{ proposer }}</p>' + '</div>' + '<div class="col-4">' + '<p><a href="{{ webpage }}">Webpage</a></p>' + '<p>Discipline: {{ discipline }}</p></div>' + '<div class="col-4"><p>expertise:<ul>') + for exp in self.expertises: + html += '<li>%s</li>' % subject_areas_dict[exp] + html += '</ul></div></div></div>' + context = Context({ + 'nomination_id': self.id, + 'proposer': '%s %s' % (self.by.user.first_name, + self.by.user.last_name), + 'name': self.first_name + ' ' + self.last_name, + 'discipline': disciplines_dict[self.discipline], + 'webpage': self.webpage, + }) + template = Template(html) + return template.render(context) + + def votes_as_ul(self): + template = Template(''' + <ul class="opinionsDisplay"> + <li style="background-color: #000099">Agree {{ nr_A }}</li> + <li style="background-color: #555555">Abstain {{ nr_N }}</li> + <li style="background-color: #990000">Disagree {{ nr_D }}</li> + </ul> + ''') + context = Context({'nr_A': self.nr_A, 'nr_N': self.nr_N, 'nr_D': self.nr_D}) + return template.render(context) + + def update_votes(self, contributor_id, vote): + contributor = get_object_or_404(Contributor, pk=contributor_id) + self.in_agreement.remove(contributor) + self.in_notsure.remove(contributor) + self.in_disagreement.remove(contributor) + if vote == 'A': + self.in_agreement.add(contributor) + elif vote == 'N': + self.in_notsure.add(contributor) + elif vote == 'D': + self.in_disagreement.add(contributor) + self.nr_A = self.in_agreement.count() + self.nr_N = self.in_notsure.count() + self.nr_D = self.in_disagreement.count() + self.save() + + +MOTION_CATEGORIES = ( + ('ByLawAmend', 'Amendments to by-laws'), + ('Workflow', 'Editorial workflow improvements'), + ('General', 'General'), +) +motion_categories_dict = dict(MOTION_CATEGORIES) + + +class Motion(models.Model): + """ + Motion instances are put forward to the Advisory Board and Editorial College + and detail suggested changes to rules, procedures etc. + They are meant to be voted on at the annual VGM. + """ + category = models.CharField(max_length=10, choices=MOTION_CATEGORIES, + default='General') + VGM = models.ForeignKey(VGM, blank=True, null=True) + background = models.TextField() + motion = models.TextField() + put_forward_by = models.ForeignKey(Contributor) + date = models.DateField() + nr_A = models.PositiveIntegerField(default=0) + in_agreement = models.ManyToManyField(Contributor, + related_name='in_agreement_with_motion', blank=True) + nr_N = models.PositiveIntegerField(default=0) + in_notsure = models.ManyToManyField(Contributor, + related_name='in_notsure_with_motion', blank=True) + nr_D = models.PositiveIntegerField(default=0) + in_disagreement = models.ManyToManyField(Contributor, + related_name='in_disagreement_with_motion', + blank=True) + voting_deadline = models.DateTimeField('voting deadline', default=timezone.now) + accepted = models.NullBooleanField() + + def __str__(self): + return self.motion[:32] + + def as_li(self): + html = ('<div class="Motion" id="motion_id{{ motion_id }}">' + '<h3><em>Motion {{ motion_id }}, put forward by {{ proposer }}</em></h3>' + '<h3>Background:</h3><p>{{ background|linebreaks }}</p>' + '<h3>Motion:</h3>' + '<div class="flex-container"><div class="flex-greybox">' + '<p style="background-color: #eeeeee;">{{ motion|linebreaks }}</p>' + '</div></div>' + '</div>') + context = Context({ + 'motion_id': self.id, + 'proposer': '%s %s' % (self.put_forward_by.user.first_name, + self.put_forward_by.user.last_name), + 'background': self.background, + 'motion': self.motion, }) + template = Template(html) + return template.render(context) + + def votes_as_ul(self): + template = Template(''' + <ul class="opinionsDisplay"> + <li style="background-color: #000099">Agree {{ nr_A }}</li> + <li style="background-color: #555555">Abstain {{ nr_N }}</li> + <li style="background-color: #990000">Disagree {{ nr_D }}</li> + </ul> + ''') + context = Context({'nr_A': self.nr_A, 'nr_N': self.nr_N, 'nr_D': self.nr_D}) + return template.render(context) + + def update_votes(self, contributor_id, vote): + contributor = get_object_or_404(Contributor, pk=contributor_id) + self.in_agreement.remove(contributor) + self.in_notsure.remove(contributor) + self.in_disagreement.remove(contributor) + if vote == 'A': + self.in_agreement.add(contributor) + elif vote == 'N': + self.in_notsure.add(contributor) + elif vote == 'D': + self.in_disagreement.add(contributor) + self.nr_A = self.in_agreement.count() + self.nr_N = self.in_notsure.count() + self.nr_D = self.in_disagreement.count() + self.save() + ######### # Lists # @@ -491,10 +718,9 @@ class List(models.Model): class Meta: default_permissions = ['add', 'view', 'change', 'delete'] - def __str__(self): - return '%s (owner: %s %s)' % (self.title[:30], self.owner.user.first_name, self.owner.user.last_name) - + return '%s (owner: %s %s)' % (self.title[:30], + self.owner.user.first_name, self.owner.user.last_name) def header(self): context = Context({'id': self.id, 'title': self.title, @@ -506,7 +732,6 @@ class List(models.Model): ''') return template.render(context) - def header_as_li(self): context = Context({'id': self.id, 'title': self.title, 'first_name': self.owner.user.first_name, @@ -517,7 +742,6 @@ class List(models.Model): ''') return template.render(context) - def contents(self): context = Context({}) output = '<p>' + self.description + '</p>' @@ -569,19 +793,18 @@ class Team(models.Model): class Meta: default_permissions = ['add', 'view', 'change', 'delete'] - def __str__(self): return (self.name + ' (led by ' + self.leader.user.first_name + ' ' + self.leader.user.last_name + ')') def header_as_li(self): - context = Context({'name': self.name,}) + context = Context({'name': self.name, }) output = ('<li><p>Team {{ name }}, led by ' + self.leader.user.first_name + ' ' + self.leader.user.last_name + '</p>') output += '<p>Members: ' if not self.members.all(): output += '(none yet, except for the leader)' - else : + else: for member in self.members.all(): output += member.user.first_name + ' ' + member.user.last_name + ', ' output += '</p></li>' @@ -609,9 +832,9 @@ class Graph(models.Model): class Meta: default_permissions = ['add', 'view', 'change', 'delete'] - def __str__(self): - return '%s (owner: %s %s)' % (self.title[:30], self.owner.user.first_name, self.owner.user.last_name) + return '%s (owner: %s %s)' % (self.title[:30], + self.owner.user.first_name, self.owner.user.last_name) def header_as_li(self): context = Context({'id': self.id, 'title': self.title, @@ -651,7 +874,6 @@ class Node(models.Model): class Meta: default_permissions = ['add', 'view', 'change', 'delete'] - def __str__(self): return self.graph.title[:20] + ': ' + self.name[:20] @@ -678,10 +900,11 @@ class Node(models.Model): ARC_LENGTHS = [ -# (4, '4'), (8, '8'), (16, '16'), (32, '32'), (64, '64'), (128, '128') + # (4, '4'), (8, '8'), (16, '16'), (32, '32'), (64, '64'), (128, '128') (1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, '5'), (6, '6'), (7, '7'), (8, '8'), ] + class Arc(models.Model): """ Arc of a graph, linking two nodes. @@ -695,7 +918,6 @@ class Arc(models.Model): length = models.PositiveSmallIntegerField(choices=ARC_LENGTHS, default=32) - ####################### # Affiliation Objects # ####################### @@ -745,6 +967,7 @@ class SupportingPartner(models.Model): def __str__(self): return self.institution_acronym + ' (' + partner_status_dict[self.status] + ')' + SPB_MEMBERSHIP_AGREEMENT_STATUS = ( ('Submitted', 'Request submitted by Partner'), ('Pending', 'Sent to Partner, response pending'), @@ -762,6 +985,7 @@ SPB_MEMBERSHIP_DURATION = ( ) spb_membership_duration_dict = dict(SPB_MEMBERSHIP_DURATION) + class SPBMembershipAgreement(models.Model): """ Agreement for membership of the Supporting Partners Board. diff --git a/scipost/static/scipost/assets/css/_buttons.scss b/scipost/static/scipost/assets/css/_buttons.scss new file mode 100644 index 0000000000000000000000000000000000000000..90781a20da4ef3d3fb0a51dc4953bef2e104771a --- /dev/null +++ b/scipost/static/scipost/assets/css/_buttons.scss @@ -0,0 +1,8 @@ +/** + * Buttons + * + */ +.btn { + cursor: pointer; + font-family: inherit; +} diff --git a/scipost/static/scipost/assets/css/_form.scss b/scipost/static/scipost/assets/css/_form.scss new file mode 100644 index 0000000000000000000000000000000000000000..6b48198edc22fde86f80ed2667fa689e563b8cfc --- /dev/null +++ b/scipost/static/scipost/assets/css/_form.scss @@ -0,0 +1,17 @@ +/** + * Form + * + */ +.form-control { + font-family: inherit; +} + +.has-error .form-control { + border-color: #d9534f; +} + +.form-control + .help-block { + margin-top: 3px; + display: inline-block; + font-weight: 600; +} diff --git a/scipost/static/scipost/assets/css/_messages.scss b/scipost/static/scipost/assets/css/_messages.scss new file mode 100644 index 0000000000000000000000000000000000000000..581731faf9432e8b67daa00b02ae7be7169f7fe9 --- /dev/null +++ b/scipost/static/scipost/assets/css/_messages.scss @@ -0,0 +1,17 @@ +.alert-fixed-container { + padding-left: 10px; + padding-right: 10px; + position: fixed; + bottom: 0px; + left: 0px; + width: 100%; + z-index: 9999; +} + +.alert .close { + box-shadow: none; + + @include hover-focus() { + box-shadow: none; + } +} diff --git a/scipost/static/scipost/assets/css/_navbar.scss b/scipost/static/scipost/assets/css/_navbar.scss new file mode 100644 index 0000000000000000000000000000000000000000..7691f7a5b7d410edbc522377611f23ac0f9777c9 --- /dev/null +++ b/scipost/static/scipost/assets/css/_navbar.scss @@ -0,0 +1,44 @@ +/** + * Navbar + * + */ +.navbar { + margin-bottom: 0.75rem; + + .nav-link { + padding-right: 1rem; + padding-left: 1rem; + border: 1px solid transparent; + border-radius: 0.1rem; + -webkit-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; + + @include hover-focus { + background-color: $white; + } + } + + .active > .nav-link { + border-color: $scipost-darkblue; + } + + .highlighted > .nav-link { + background-color: rgba(255, 255, 255, 0.6); + } + + .nav-item { + margin-right: 0.5rem; + } +} +.navbar-brand { + height: 38px; + margin: 0; +} +.navbar-toggler-icon { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 43, 73, 1.0)"); +} +.panel { + padding: 0.75rem; + background-color: #f4f4f4; +} diff --git a/scipost/static/scipost/assets/css/_page_header.scss b/scipost/static/scipost/assets/css/_page_header.scss new file mode 100644 index 0000000000000000000000000000000000000000..61042dabdecf156180cca0a650c8073d549620af --- /dev/null +++ b/scipost/static/scipost/assets/css/_page_header.scss @@ -0,0 +1,4 @@ +.page-header { + background-color: #f4f4f4; + padding: 10px; +} diff --git a/scipost/static/scipost/assets/css/_type.scss b/scipost/static/scipost/assets/css/_type.scss new file mode 100644 index 0000000000000000000000000000000000000000..86cc6ed02019476a59aae42e379c9251082c7779 --- /dev/null +++ b/scipost/static/scipost/assets/css/_type.scss @@ -0,0 +1,18 @@ +/** + * Type + * + */ +a { + color: $scipost-lightblue; + text-decoration: none; +} +a:hover { + color: $scipost-darkblue; + text-decoration: underline; +} +h1, h2, h3, h4, h5, h6 { + margin: 0; + padding: 5px 0; + text-shadow: none; + font-weight: 500; +} diff --git a/scipost/static/scipost/assets/css/style.scss b/scipost/static/scipost/assets/css/style.scss index 936fe0a4706e8cd24ad509a6ce70a47cc77d4e8e..3b7db7f8bc6232a4d49cca20749123d9fba32d30 100644 --- a/scipost/static/scipost/assets/css/style.scss +++ b/scipost/static/scipost/assets/css/style.scss @@ -1,91 +1,18 @@ @import "node_modules/bootstrap/scss/_variables"; @import "node_modules/bootstrap/scss/_mixins"; @import "./../config/preconfig.scss"; + /** * Make variables defined in preconfig.scss accessable in this file, * plus default variables and mixins of Bootstrap@4 - * - SciPost custom sass - * - */ - - -/** - * Buttons - * - */ -.btn { - cursor: pointer; - font-family: inherit; -} - -/** - * Type - * - */ -a { - color: $scipost-lightblue; - text-decoration: none; -} -a:hover { - color: $scipost-darkblue; - text-decoration: underline; -} -h1, h2, h3, h4, h5, h6 { - margin: 0; - padding: 5px 0; - text-shadow: none; - font-weight: 500; -} - -/** - * Form * - */ -.form-control { - font-family: inherit; -} - -/** - * Navbar + * Do not write styles in this file. Get or create a file in this folder + * and make sure its imported underneath! * */ -.navbar { - margin-bottom: 0.75rem; - - .nav-link { - padding-right: 1rem; - padding-left: 1rem; - border: 1px solid transparent; - border-radius: 0.1rem; - -webkit-transition: all 0.2s ease-in-out; - -o-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; - - @include hover-focus { - background-color: $white; - } - } - - .active > .nav-link { - border-color: $scipost-darkblue; - } - - .highlighted > .nav-link { - background-color: rgba(255, 255, 255, 0.6); - } - - .nav-item { - margin-right: 0.5rem; - } -} -.navbar-brand { - height: 38px; - margin: 0; -} -.navbar-toggler-icon { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 43, 73, 1.0)"); -} -.panel { - padding: 0.75rem; - background-color: #f4f4f4; -} +@import "buttons"; +@import "form"; +@import "messages"; +@import "navbar"; +@import "page_header"; +@import "type"; diff --git a/scipost/static/scipost/assets/js/scripts.js b/scipost/static/scipost/assets/js/scripts.js new file mode 100644 index 0000000000000000000000000000000000000000..b79dcd2ae4a00411aa3862b5c5d10622f52c20e3 --- /dev/null +++ b/scipost/static/scipost/assets/js/scripts.js @@ -0,0 +1,9 @@ +function hide_all_alerts() { + $(".alert").fadeOut(300); +} + + +$(function(){ + // Remove all alerts in screen automatically after 4sec. + setTimeout(function() {hide_all_alerts()}, 4000); +}); diff --git a/scipost/templates/scipost/SPB_membership_request.html b/scipost/templates/scipost/SPB_membership_request.html index 9be4a6c3ed76c2f0c532f86da736a52c798bc0d6..455ce5b04347aeaae62171bcc03432c5f58adc77 100644 --- a/scipost/templates/scipost/SPB_membership_request.html +++ b/scipost/templates/scipost/SPB_membership_request.html @@ -32,24 +32,31 @@ $(document).ready(function(){ </div> </div> - <p>You can hereby initiate the process to become one of our Supporting Partners.</p> - <p>After filling this form, SciPost Administration will contact you with a Partnership - Agreement offer.</p> - <p><em>Note: you will automatically be considered as the contact person for this Partner.</em></p> - - {% if errormessage %} - <p style="color: red;">{{ errormessage }}</p> - {% endif %} - - <form action="{% url 'scipost:SPB_membership_request' %}" method="post"> - {% csrf_token %} - <h3>Partner details:</h3> - {% load crispy_forms_tags %} - {% crispy SP_form %} - <h3>Agreement terms:</h3> - {% crispy membership_form %} - <input type="submit" value="Submit"/> - </form> + <div class="flex-container"> + <div class="flex-whitebox"> + <p>You can hereby initiate the process to become one of our Supporting Partners.</p> + <p>Filling in this form does not yet constitute a binding agreement.</p> + <p>It simply expresses your interest in considering joining our Supporting Partners Board.</p> + <p>After filling this form, SciPost Administration will contact you with a Partnership + Agreement offer.</p> + <p><em>Note: you will automatically be considered as the contact person for this Partner.</em></p> + + {% if errormessage %} + <p style="color: red;">{{ errormessage }}</p> + {% endif %} + + <form action="{% url 'scipost:SPB_membership_request' %}" method="post"> + {% csrf_token %} + <h3>Partner details:</h3> + {% load crispy_forms_tags %} + {% crispy SP_form %} + <h3>Agreement terms:</h3> + {% crispy membership_form %} + <input type="submit" value="Submit"/> + </form> + + </div> + </div> </section> diff --git a/scipost/templates/scipost/VGM_detail.html b/scipost/templates/scipost/VGM_detail.html new file mode 100644 index 0000000000000000000000000000000000000000..69b5a224772da1497dd1b1040cb97344f42e3f5b --- /dev/null +++ b/scipost/templates/scipost/VGM_detail.html @@ -0,0 +1,352 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: VGM detail{% endblock pagetitle %} + +{% load staticfiles %} + +{% block bodysup %} + +<script> +$(document).ready(function(){ + + $("#submitFeedbackForm").hide(); + $("#submitFeedbackButton").click( function() { + $(this).next("form").toggle(); + }); + + $("#FellowshipListing").hide(); + $("#FellowshipListingButton").click( function() { + $("#FellowshipListing").toggle(); + }); + + $("#submitNominationForm").hide(); + $("#submitNominationButton").click( function() { + $(this).next("form").toggle(); + }); + + $("#submitMotionForm").hide(); + $("#submitMotionButton").click( function() { + $(this).next("form").toggle(); + }); + + $(".submitRemarkForm").hide(); + + $(".submitRemarkButton").click( function() { + $(this).next("div").toggle(); + }); + }); + +</script> + +<section> + <div class="flex-container"> + <div class="flex-greybox"> + <h1>SciPost Virtual General Meeting</h1> + </div> + </div> + <div class="flex-container"> + <div class="flex-whitebox"> + <h2>On this page:</h2> + <ul> + <li><a href="#Information">Information message</a></li> + <li><a href="#Feedback">Feedback</a></li> + <li><a href="#Nominations">Nominations</a></li> + <li><a href="#Motions">Motions</a></li> + </ul> + </div> + </div> + <hr class="hr12"/> +</section> + + +<section id="Information"> + <div class="flex-container"> + <div class="flex-greybox"> + <h2>Information message from SciPost Administration</h2> + </div> + </div> + <div class="flex-whitebox"> + {{ VGM_information }} + </div> + <br/> + <div class="flex-whitebox"> + <h3>Quick bullet points:</h3> + <ul> + <li>This VGM is scheduled from {{ VGM.start_date|date:'Y-m-d' }} to {{ VGM.end_date|date:'Y-m-d' }}.</li> + <li>Your feedback/suggestions/criticisms on any aspect of SciPost are greatly valued. Provide them by filling the <a href="#FeedbackBox">feedback form</a>.</li> + <li>Your nominations to the Editorial College are welcome. Simply fill the <a href="#NominationBox">nomination form</a>, and cast your vote on current nominations.</li> + <li>For substantial changes, for example to the by-laws, new Motions can be put forward until the end of the meeting using the <a href="#MotionBox">form</a>.</li> + <li>Voting on Motions is open until one week after the meeting.</li> + <li>You a referred to the <a href="{% url 'scipost:EdCol_by-laws' %}">by-laws</a>, section 2 for further details about the procedures.</li> + </ul> + </div> + <br/> + <hr class="hr12"/> +</section> + +<section id="Feedback"> + <div class="flex-container"> + <div class="flex-greybox" id="FeedbackBox"> + <h2>Feedback on SciPost</h2> + <button id="submitFeedbackButton">Provide feedback</button> + <form id="submitFeedbackForm" action="{% url 'scipost:feedback' VGM_id=VGM.id %}" method="post"> + {% csrf_token %} + {{ feedback_form.as_p }} + <input type="submit" value="Submit"/> + </form> + </div> + </div> + <div class="flex-container"> + <div class="flex-greybox"> + <h2>General Feedback provided</h2> + </div> + </div> + <div class="row"> + <div class="col-1"></div> + <div class="col-10"> + <ul> + {% for feedback in feedback_received %} + <li>{{ feedback.as_li }}</li> + <button class="submitRemarkButton" id="remarkButton{{ nomination.id }}">Add a remark on this Feedback</button> + <div class="submitRemarkForm" id="remarkForm{{ feedback.id }}"> + <form action="{% url 'scipost:add_remark_on_feedback' VGM_id=VGM.id feedback_id=feedback.id %}" method="post"> + {% csrf_token %} + {{ remark_form.as_p }} + <input type="submit" value="Submit" /> + </form> + </div> + {% if feedback.remark_set.all %} + <h3>Remarks on this feedback:</h3> + <ul> + {% for rem in feedback.remark_set.all %} + {{ rem.as_li }} + {% endfor %} + </ul> + {% endif %} + {% endfor %} + </ul> + </div> + </div> + <hr class="hr12"/> +</section> + +<section id="Nominations"> + <div class="flex-container"> + <div class="flex-greybox" id="NominationBox"> + <h2>Nominations to the Editorial College</h2> + <button id="submitNominationButton">Nominate an Editorial Fellow candidate</button> + <form id="submitNominationForm" action="{% url 'scipost:nominate_Fellow' VGM_id=VGM.id %}" method="post"> + {% csrf_token %} + {{ nomination_form.as_p }} + <input type="submit" value="Submit"/> + </form> + </div> + </div> + <button id="FellowshipListingButton">View/hide Fellows and Invitations listings</button> + <div class="row" id="FellowshipListing"> + <div class="col-6"> + <div class="flex-container"> + <div class="flex-greybox"> + <h3>Current Fellows</h3> + </div> + </div> + <div class="flex-container"> + <div class="flex-whitebox"> + <table class="tableofInviteesResponded"> + {% for Fellow in current_Fellows %} + <tr><td>{{ Fellow }}</td><td>{{ Fellow.discipline_as_string }}</td> + <td>{{ Fellow.expertises_as_string }}</td></tr> + {% endfor %} + </table> + </div> + </div> + </div> + <div class="col-6"> + <div class="flex-container"> + <div class="flex-greybox"> + <h3>Invitations currently outstanding</h3> + </div> + </div> + <div class="flex-container"> + <div class="flex-whitebox"> + <table class="tableofInvitees"> + {% for invitee in pending_inv_Fellows %} + <tr><td>{{ invitee.first_name }} {{ invitee.last_name }}</td></tr> + {% endfor %} + </table> + </div> + </div> + <div class="flex-container"> + <div class="flex-greybox"> + <h3>Invitations which have been turned down</h3> + </div> + </div> + <div class="flex-container"> + <div class="flex-whitebox"> + <table class="tableofInviteesDeclined"> + {% for invitee in declined_inv_Fellows %} + <tr><td>{{ invitee.first_name }} {{ invitee.last_name }}</td></tr> + {% endfor %} + </table> + </div> + </div> + </div> + </div> + + {% if nominations %} + <div class="row"> + <div class="flex-container"> + <div class="flex-greybox"> + <h2>Nominations under consideration</h2> + </div> + </div> + </div> + <div class="row"> + <div class="col-1"></div> + <div class="col-10"> + <ul style="list-style-type: none;"> + {% for nomination in nominations %} + <li> + {{ nomination.as_li }} + <br/> + <div class="opinionsDisplay"> + <h4>Your opinion on this Nomination (voting deadline: {{ nomination.voting_deadline|date:'y-m-d' }}):</h4> + <form action="{% url 'scipost:vote_on_nomination' nomination_id=nomination.id vote='A' %}" method="post"> + {% csrf_token %} + <input type="submit" class="agree" value="Agree {{ nomination.nr_A }} "/> + </form> + <form action="{% url 'scipost:vote_on_nomination' nomination_id=nomination.id vote='N' %}" method="post"> + {% csrf_token %} + <input type="submit" class="notsure" value="Not sure {{ nomination.nr_N }}"/> + </form> + <form action="{% url 'scipost:vote_on_nomination' nomination_id=nomination.id vote='D'%}" method="post"> + {% csrf_token %} + <input type="submit" class="disagree" value="Disagree {{ nomination.nr_D }}"/> + </form> + {% if request.user.contributor in nomination.in_agreement.all %} + <strong>(you have voted: Agreed)</strong> + {% elif request.user.contributor in nomination.in_notsure.all %} + <strong>(you have voted: Not sure)</strong> + {% elif request.user.contributor in nomination.in_disagreement.all %} + <strong>(you have voted: Disagree)</strong> + {% endif %} + </div> + <br/><br/> + <button class="submitRemarkButton" id="remarkButton{{ nomination.id }}">Add a remark on this Nomination</button> + <div class="submitRemarkForm" id="remarkForm{{ nomination.id }}"> + <form action="{% url 'scipost:add_remark_on_nomination' VGM_id=VGM.id nomination_id=nomination.id %}" method="post"> + {% csrf_token %} + {{ remark_form.as_p }} + <input type="submit" value="Submit" /> + </form> + </div> + {% if nomination.remark_set.all %} + <h3>Remarks on this nomination:</h3> + <ul> + {% for rem in nomination.remark_set.all %} + {{ rem.as_li }} + {% endfor %} + </ul> + {% endif %} + <hr class="hr6"/> + <br/> + </li> + {% endfor %} + </ul> + </div> + </div> + {% endif %} + + <hr class="hr12"/> + +</section> + +<section id="Motions"> + <div class="flex-container"> + <div class="flex-greybox" id="MotionBox"> + <h2>Submit a new Motion</h2> + <button id="submitMotionButton">Put a new Motion forward</button> + <form id="submitMotionForm" action="{% url 'scipost:put_motion_forward' VGM_id=VGM.id %}" method="post"> + {% csrf_token %} + {% load crispy_forms_tags %} + {% crispy motion_form %} + </form> + </div> + </div> + + <div class="row"> + <div class="flex-container"> + <div class="flex-greybox"> + <h2>Motions under consideration</h2> + </div> + </div> + </div> + {% for key, val in motion_categories_dict.items %} + <div class="row"> + <div class="col-1"></div> + <div class="flex-container"> + <div class="flex-greybox"> + <h3>{{ val }}:</h3> + </div> + </div> + <div class="col-1"></div> + <div class="col-10"> + <ul> + {% for motion in VGM.motion_set.all %} + {% if motion.category == key %} + <li> + {{ motion.as_li }} + <br/> + <div class="opinionsDisplay"> + <h4>Your opinion on this Motion (voting deadline: {{ motion.voting_deadline|date:'y-m-d' }}):</h4> + <form action="{% url 'scipost:vote_on_motion' motion_id=motion.id vote='A' %}" method="post"> + {% csrf_token %} + <input type="submit" class="agree" value="Agree {{ motion.nr_A }} "/> + </form> + <form action="{% url 'scipost:vote_on_motion' motion_id=motion.id vote='N' %}" method="post"> + {% csrf_token %} + <input type="submit" class="notsure" value="Not sure {{ motion.nr_N }}"/> + </form> + <form action="{% url 'scipost:vote_on_motion' motion_id=motion.id vote='D'%}" method="post"> + {% csrf_token %} + <input type="submit" class="disagree" value="Disagree {{ motion.nr_D }}"/> + </form> + {% if request.user.contributor in motion.in_agreement.all %} + <strong>(you have voted: Agreed)</strong> + {% elif request.user.contributor in motion.in_notsure.all %} + <strong>(you have voted: Not sure)</strong> + {% elif request.user.contributor in motion.in_disagreement.all %} + <strong>(you have voted: Disagree)</strong> + {% endif %} + </div> + <br/><br/> + <button class="submitRemarkButton" id="remarkButton{{ motion.id }}">Add a remark on this Motion</button> + <div class="submitRemarkForm" id="remarkForm{{ motion.id }}"> + <form action="{% url 'scipost:add_remark_on_motion' motion_id=motion.id %}" method="post"> + {% csrf_token %} + {{ remark_form.as_p }} + <input type="submit" value="Submit" /> + </form> + </div> + {% if motion.remark_set.all %} + <h3>Remarks on this motion:</h3> + <ul> + {% for rem in motion.remark_set.all %} + {{ rem.as_li }} + {% endfor %} + </ul> + {% endif %} + <hr class="hr6"/> + <br/> + </li> + {% endif %} + {% endfor %} + </ul> + </div> + </div> + {% endfor %} + +</section> + + +{% endblock bodysup %} diff --git a/scipost/templates/scipost/VGMs.html b/scipost/templates/scipost/VGMs.html new file mode 100644 index 0000000000000000000000000000000000000000..493eefde5df67e226514048c5410da811aae0b57 --- /dev/null +++ b/scipost/templates/scipost/VGMs.html @@ -0,0 +1,26 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: VGMs{% endblock pagetitle %} + +{% load staticfiles %} + +{% block bodysup %} + + +<section> + <div class="flex-container"> + <div class="flex-greybox"> + <h1>SciPost Virtual General Meetings</h1> + </div> + </div> + + <ul> + {% for VGM in VGM_list %} + <li><a href="{% url 'scipost:VGM_detail' VGM_id=VGM.id %}">{{ VGM }}</a></li> + {% endfor %} + </ul> + +</section> + + +{% endblock bodysup %} diff --git a/scipost/templates/scipost/about.html b/scipost/templates/scipost/about.html index 6b89228b362007b8563070cd70cde2770c69569b..415843cfa14a607a891f3a627577f736e9bfbaef 100644 --- a/scipost/templates/scipost/about.html +++ b/scipost/templates/scipost/about.html @@ -118,7 +118,7 @@ <div class="flex-container"> <div class="flex-whitebox"> <ul> - <li>Prof. <a href="http://www.nwo.nl/en/about-nwo/organisation/governing+board/Jos+Engelen">J. J. Engelen</a><br/>(Chairman NWO;<br/> U. van Amsterdam)</li> + <li>Prof. <a href="http://www.nikhef.nl/~h02/">J. J. Engelen</a><br/>(U. van Amsterdam)</li> <li>Prof. <a href="https://www.asc.ox.ac.uk/person/18">P. Fendley</a><br/>(Oxford; <a href="https://www.asc.ox.ac.uk/all-souls-college-oxford">All Souls College</a>)</li> <li>Prof. <a href="http://www.ru.nl/highenergyphysics/ehep/persons/sijbrand_de_jong/">S. J. de Jong</a><br/>(Radboud Univ. Nijmegen,<br/>President CERN Council)</li> </ul> diff --git a/scipost/templates/scipost/base.html b/scipost/templates/scipost/base.html index 2bedef9de0857fbfb823f74ed24c21783a37cd06..4a3105f7a1ef8dc2fa23b523e0415cbd2438dd72 100644 --- a/scipost/templates/scipost/base.html +++ b/scipost/templates/scipost/base.html @@ -28,12 +28,16 @@ {% include 'scipost/navbar.html' %} {% include 'scipost/messages.html' %} + + <div class="container"> + {% block page_header %}{% endblock page_header %} + + {% block content %}{% endblock content %} + </div> + {% block bodysup %} {% endblock bodysup %} - {% block content %} - {% endblock content %} - {% include 'scipost/footer.html' %} <script type="text/x-mathjax-config"> diff --git a/scipost/templates/scipost/messages.html b/scipost/templates/scipost/messages.html index ac8c6dff8c12a6bea16bcdaca31738d52cd55336..53699fac2a6b73508d8df505760b9924b1a25b87 100644 --- a/scipost/templates/scipost/messages.html +++ b/scipost/templates/scipost/messages.html @@ -1,8 +1,10 @@ -{% for message in messages %} - <div class="alert {{ message.tags }} alert-dismissible" role="alert"> - <button type="button" class="close" data-dismiss="alert" aria-label="Close"> - <span aria-hidden="true">×</span> - </button> - {{ message }} - </div> -{% endfor %} +<div class="alert-fixed-container"> + {% for message in messages %} + <div class="alert alert-{{ message.tags }} alert-dismissible" role="alert"> + <button type="button" class="close" data-dismiss="alert" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + {{ message }} + </div> + {% endfor %} +</div> diff --git a/scipost/templates/scipost/personal_page.html b/scipost/templates/scipost/personal_page.html index d2645698fd4982108e18cd393f6bad8a3bee8bed..72627b79c85afc445d8f0f003afd6e17a5c1bc11 100644 --- a/scipost/templates/scipost/personal_page.html +++ b/scipost/templates/scipost/personal_page.html @@ -273,7 +273,7 @@ <li><a href="{% url 'comments:vet_submitted_comments' %}">Vet submitted Comments</a> ({{ nr_comments_to_vet }})</li> {% endif %} {% if perms.scipost.can_vet_thesislink_requests %} - <li><a href="{% url 'theses:vet_thesislink_requests' %}">Vet Thesis Link Requests</a> ({{ nr_thesislink_requests_to_vet }})</li> + <li><a href="{% url 'theses:unvetted_thesislinks' %}">Vet Thesis Link Requests</a> ({{ nr_thesislink_requests_to_vet }})</li> {% endif %} {% if perms.scipost.can_vet_authorship_claims %} <li><a href="{% url 'scipost:vet_authorship_claims' %}">Vet Authorship Claims</a> ({{ nr_authorship_claims_to_vet }})</li> @@ -282,6 +282,12 @@ <li><a href="{% url 'submissions:vet_submitted_reports' %}">Vet submitted Reports</a> ({{ nr_reports_to_vet }})</li> {% endif %} </ul> + {% if perms.scipost.can_attend_VGMs %} + <h3>Virtual General Meetings</h3> + <ul> + <li><a href="{% url 'scipost:VGMs' %}">List of VGMs</a></li> + </ul> + {% endif %} </div> {% if request.user|is_in_group:'Editorial Administrators' or request.user|is_in_group:'Editorial College' %} @@ -293,6 +299,7 @@ <h3>Submissions assignments</h3> <ul> {% if perms.scipost.can_view_pool %} + <li><a href="{% url 'submissions:assignments' %}">Your assignments</a></li> <li><a href="{% url 'scipost:Fellow_activity_overview' %}">View assignments overview</a></li> {% endif %} {% if perms.scipost.can_assign_submissions %} diff --git a/scipost/templates/scipost/supporting_partners.html b/scipost/templates/scipost/supporting_partners.html index 4c7ab5bf73a1735fbcf66cdf450bba5420d57ab9..f5ced193e5cf7364cf6501cbbdf892a32e365e58 100644 --- a/scipost/templates/scipost/supporting_partners.html +++ b/scipost/templates/scipost/supporting_partners.html @@ -4,22 +4,133 @@ {% load staticfiles %} +{% load scipost_extras %} + {% block bodysup %} <section> + <div class="flex-container"> + <div class="flex-greybox"> + <h1>SciPost Supporting Partners</h1> + </div> + </div> + + <div class="flex-container"> + <div class="flex-whitebox"> + + <h3>Openness at strictly minimal cost: the role of professional academics</h3> + <p>A fundamental principle underlining all of SciPost’s activities is openness. This means in particular that SciPost guarantees free online access to all publications in all its Journals (free for readers; all articles are published under the terms of a Creative Commons license (most commonly CC-BY)), and does not charge any article processing fees for publishing (free for authors). All editorial activities are performed by professional academics as part of their normal professional duties: contributors to SciPost are demonstrably dedicated to serving their community, and to realizing the dream of true openness in scientific publishing. SciPost thus achieves the highest possible standards of academic quality and open accessibility while maintaining costs at the lowest achievable level.</p> + + <h3>Financing and sustainability: the role of institutional backers</h3> + <p>Besides relying on the dedicated service of professional scientists for many of its day-to-day operations, SciPost must additionally rely on institutional backers for sustaining its professional-level publishing facilities and other activities. This backing is sought primarily from the organizations which are positively impacted by its activities, directly or indirectly: (inter)national funding agencies, universities, national/university/research libraries, academic consortia, governments and ministries, foundations, benefactors and further interested parties. This financial backing cannot take the form of article processing charges or journal subscriptions, due to SciPost’s operating principles: rather, Supporting Partners provide operating funds to SciPost, these funds being pooled in order to cover maintenance and operation of the web infrastructure at SciPost.org, administration of all site-related activities, production of publications in SciPost Journals and development of new publishing-related services for the international scientific community.</p> + + </div> + </div> + + + <div class="flex-container"> <div class="flex-greybox"> <h1>Supporting Partners Board</h1> </div> </div> + <div class="flex-container"> + <div class="flex-whitebox"> + + <p>We hereby cordially invite interested parties who are supportive of SciPost's mission to join the SciPost Supporting Partners Board by signing a <a href="{% static 'scipost/SPB/SciPost_Supporting_Partner_Agreement.pdf' %}">Partner Agreement</a>.</p> + + <p>Prospective partners can initiate the process leading to Membership by filling the <a href="{% url 'scipost:SPB_membership_request' %}">online request form</a>.</p> + + <br/> + <p>The <a href="{% static 'scipost/SPB/SciPost_Supporting_Partner_Agreement.pdf' %}">Partner Agreement</a> itself contains a detailed presentation of the Foundation, its activities and financial aspects. What follows is a summary of the most important points.</p> + + <br/> <p>The set of all Supporting Partners forms the SciPost Supporting Partners Board (SPB). Acting as a representative body, the SPB’s purpose is to provide the main financial backing without which SciPost could not continue carrying out its mission, to counsel it in all its operations and to help determine the initiative’s development priorities.</p> - <p>Interested parties can join the SciPost Supporting Partners Board by signing a <a href="{% static 'scipost/SPB/SciPost_Supporting_Partner_Agreement.pdf' %}">Partner Agreement</a>.</p> + <p>The SPB has a yearly virtual general meeting, organized and chaired by a representative of the SciPost Foundation. During this meeting, SPB-related topics are discussed, recommendations to the SciPost Foundation can be put forward and voted on by Partners, and general issues on any of SciPost’s activities can be discussed.</p> + + + <h3>Types of Partners</h3> + <p>Supporting Partners can for example be: + <ul> + <li>International/national funding agencies</li> + <li>National/university/research libraries</li> + <li>Consortia (of e.g. universities or libraries)</li> + <li>Government (through e.g. education/science ministries)</li> + <li>Foundations</li> + <li>Benefactors of any other type.</li> + </ul> + </p> + + <h3>Partnership benefits</h3> + <p>All funds originating from Supporting Partners are used to provide services to the academic community as a whole: SciPost operates in a completely open fashion, and the fulfillment of the Foundation’s mission benefits academics worldwide, without any distinction.</p> + <p>Partnership nonetheless provides Partners with a number of additional benefits as compared to non-Partner parties. SciPost agrees to provide its Partners with: + <ol> + <li>A seat on the SciPost Supporting Partners Board, including access to yearly meetings, with voting rights proportional to financial contribution (up to a maximum of 20% of all votes for any individual Partner).</li> + <li>Inclusion in the online listing of Supporting Partners on the SciPost website.</li> + <li>Exclusive ability to feed the Partner’s institutional theses database to the SciPost Theses database, for inclusion in the Theses part of the portal.</li> + <li>Access to the SciPost metadata API (providing among others means to generate yearly metadata summaries relating to SciPost publications for each Contributor employed by the Partner).</li> + <li>Exclusive access to further online tools planned by SciPost during their development stage. The SPB as a whole can provide feedback and request specific features.</li> + </ol> + </p> - <p>Membership can be requested by filling the <a href="{% url 'scipost:SPB_membership_request' %}">online request form</a>.</p> + <h3>Financial contribution</h3> + <p>For the financial year 2017, the contributions are set to yearly amounts of: + <table> + <tr> + <td>(Inter)national funding agency:</td><td> </td> + <td>size-dependent tailored agreement</td> + </tr> + <tr> + <td>University/library:</td><td> </td> + <td>€1000 (base; more is greatly appreciated)</td> + </tr> + <tr> + <td>National consortium of universities/libraries:</td><td> </td> + <td>10% bulk discount on the above<br/>(e.g. €3600 for 4 universities)</td> + </tr> + <tr> + <td>Foundations/benefactors:</td><td> </td> + <td>(Partner-set amount of at least €500)</td> + </tr> + </table> + <p>Note that if the consortium itself is not a legal entity, each individual member must sign a separate Agreement.</p> + <p>All amounts are exclusive of VAT, which will be payable by the Partner where applicable.</p> + + <p><strong>Sustainability - </strong> This norm allows sustainable functioning of SciPost under the expectation that individual institutions be associated to between two and three full publications per year on average (computed using authorship fractions). A Partner who is associated to more authorships and/or who generally recognizes the value of SciPost’s activities, is of course welcome to contribute more.</p> + <p><strong>Donations - </strong>Contributions of less than €500 per year are treated as incidental donations and do not lead to the obtention of Partner benefits.</p> + <p>Note that SciPost has been designated as a Public Benefit Organisation (PBO; in Dutch: Algemeen Nut Beogende Instelling, ANBI) by the Dutch Tax Administration. Natural and legal persons making donations to a PBO may deduct their gifts from their Dutch income tax or corporate income tax (see the PBO page of the Dutch Tax Administration).</p> + + <h3>Activation procedure</h3> + <p>In order to become a Supporting Partner, one must: + <ul> + <li>Fill in the online <a href="{% url 'scipost:SPB_membership_request' %}">membership request form</a> (the form must be filled in by a registered Contributor, employed by or associated to the prospective Partner and acting as an authorized agent for the latter; personal contact details of this person will be treated confidentially).</li> + <li>Wait for the email response from the SciPost administration, containing a Partnership Agreement offer including detailed terms (start date, duration, financial contribution).</li> + <li>Email a scan of the signed copy of the Partnership Agreement to SciPost.</li> + <li>Proceed with the payment of the financial contribution, following invoicing from the SciPost Foundation.</li> + </ul> + </p> + + </div> + </div> +</section> + +{% if request.user|is_in_group:'Editorial Administrators' %} +<section> + <div class="flex-container"> + <div class="flex-greybox"> + <h1>Prospective Partners</h1> + </div> + </div> + <ul> + {% for agreement in prospective_agreements %} + <li>{{ agreement }}</li> + {% endfor %} + </ul> </section> +{% endif %} {% endblock bodysup %} diff --git a/scipost/urls.py b/scipost/urls.py index facaec8388b3c35a219c831f1d02871fb6cd4338..e3b8c573147bbafd6e2541cce29bbeee3d68279e 100644 --- a/scipost/urls.py +++ b/scipost/urls.py @@ -172,6 +172,28 @@ urlpatterns = [ views.Fellow_activity_overview, name='Fellow_activity_overview'), + ############################ + # Virtual General Meetings # + ############################ + url(r'^VGMs$', views.VGMs, name='VGMs'), + url(r'^VGM/(?P<VGM_id>[0-9]+)/$', views.VGM_detail, name='VGM_detail'), + url(r'^feedback/(?P<VGM_id>[0-9]+)$', + views.feedback, name='feedback'), + url(r'^add_remark_on_feedback/(?P<VGM_id>[0-9]+)/(?P<feedback_id>[0-9]+)$', + views.add_remark_on_feedback, name='add_remark_on_feedback'), + url(r'^nominate_Fellow/(?P<VGM_id>[0-9]+)$', + views.nominate_Fellow, name='nominate_Fellow'), + url(r'^add_remark_on_nomination/(?P<VGM_id>[0-9]+)/(?P<nomination_id>[0-9]+)$', + views.add_remark_on_nomination, name='add_remark_on_nomination'), + url(r'^vote_on_nomination/(?P<nomination_id>[0-9]+)/(?P<vote>[AND])$', + views.vote_on_nomination, name='vote_on_nomination'), + url(r'^put_motion_forward/(?P<VGM_id>[0-9]+)$', + views.put_motion_forward, name='put_motion_forward'), + url(r'^add_remark_on_motion/(?P<motion_id>[0-9]+)$', + views.add_remark_on_motion, name='add_remark_on_motion'), + url(r'^vote_on_motion/(?P<motion_id>[0-9]+)/(?P<vote>[AND])$', + views.vote_on_motion, name='vote_on_motion'), + ################ # Publications # ################ diff --git a/scipost/utils.py b/scipost/utils.py index 0c7a0a6d25ecf1b8f805f2a85b08263306c7b779..bac564998baf16da8d187141d2d65d5c12214b30 100644 --- a/scipost/utils.py +++ b/scipost/utils.py @@ -4,11 +4,11 @@ import random import string from django.contrib.auth.models import User -from django.core.mail import EmailMessage, EmailMultiAlternatives +from django.core.mail import EmailMultiAlternatives from django.template import Context, Template from django.utils import timezone -from .models import * +from .models import Contributor, DraftInvitation, RegistrationInvitation, title_dict SCIPOST_SUMMARY_FOOTER = ( @@ -74,8 +74,8 @@ EMAIL_UNSUBSCRIBE_LINK_HTML = ( '<a href="https://scipost.org/update_personal_data">updating your personal data</a>.</p>' ) -class Utils(object): +class Utils(object): @classmethod def load(cls, dict): for var_name in dict: @@ -118,25 +118,25 @@ class Utils(object): @classmethod def create_and_save_contributor(cls, invitation_key): - user = User.objects.create_user ( - first_name = cls.form.cleaned_data['first_name'], - last_name = cls.form.cleaned_data['last_name'], - email = cls.form.cleaned_data['email'], - username = cls.form.cleaned_data['username'], - password = cls.form.cleaned_data['password'] + user = User.objects.create_user( + first_name=cls.form.cleaned_data['first_name'], + last_name=cls.form.cleaned_data['last_name'], + email=cls.form.cleaned_data['email'], + username=cls.form.cleaned_data['username'], + password=cls.form.cleaned_data['password'] ) # Set to inactive until activation via email link user.is_active = False user.save() - contributor = Contributor ( + contributor = Contributor( user=user, invitation_key=invitation_key, - title = cls.form.cleaned_data['title'], - orcid_id = cls.form.cleaned_data['orcid_id'], - country_of_employment = cls.form.cleaned_data['country_of_employment'], - address = cls.form.cleaned_data['address'], - affiliation = cls.form.cleaned_data['affiliation'], - personalwebpage = cls.form.cleaned_data['personalwebpage'], + title=cls.form.cleaned_data['title'], + orcid_id=cls.form.cleaned_data['orcid_id'], + country_of_employment=cls.form.cleaned_data['country_of_employment'], + address=cls.form.cleaned_data['address'], + affiliation=cls.form.cleaned_data['affiliation'], + personalwebpage=cls.form.cleaned_data['personalwebpage'], ) contributor.save() Utils.load({'contributor': contributor}) @@ -179,7 +179,6 @@ class Utils(object): email_text_html += '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) html_version = html_template.render(email_context) - #emailmessage = EmailMessage( emailmessage = EmailMultiAlternatives( 'SciPost registration request received', email_text, 'SciPost registration <registration@scipost.org>', @@ -191,31 +190,31 @@ class Utils(object): @classmethod def create_draft_invitation(cls): - invitation = DraftInvitation ( - title = cls.form.cleaned_data['title'], - first_name = cls.form.cleaned_data['first_name'], - last_name = cls.form.cleaned_data['last_name'], - email = cls.form.cleaned_data['email'], - invitation_type = cls.form.cleaned_data['invitation_type'], - cited_in_submission = cls.form.cleaned_data['cited_in_submission'], - cited_in_publication = cls.form.cleaned_data['cited_in_publication'], - drafted_by = cls.contributor, + invitation = DraftInvitation( + title=cls.form.cleaned_data['title'], + first_name=cls.form.cleaned_data['first_name'], + last_name=cls.form.cleaned_data['last_name'], + email=cls.form.cleaned_data['email'], + invitation_type=cls.form.cleaned_data['invitation_type'], + cited_in_submission=cls.form.cleaned_data['cited_in_submission'], + cited_in_publication=cls.form.cleaned_data['cited_in_publication'], + drafted_by=cls.contributor, ) invitation.save() @classmethod def create_invitation(cls): - invitation = RegistrationInvitation ( - title = cls.form.cleaned_data['title'], - first_name = cls.form.cleaned_data['first_name'], - last_name = cls.form.cleaned_data['last_name'], - email = cls.form.cleaned_data['email'], - invitation_type = cls.form.cleaned_data['invitation_type'], - cited_in_submission = cls.form.cleaned_data['cited_in_submission'], - cited_in_publication = cls.form.cleaned_data['cited_in_publication'], - invited_by = cls.contributor, - message_style = cls.form.cleaned_data['message_style'], - personal_message = cls.form.cleaned_data['personal_message'], + invitation = RegistrationInvitation( + title=cls.form.cleaned_data['title'], + first_name=cls.form.cleaned_data['first_name'], + last_name=cls.form.cleaned_data['last_name'], + email=cls.form.cleaned_data['email'], + invitation_type=cls.form.cleaned_data['invitation_type'], + cited_in_submission=cls.form.cleaned_data['cited_in_submission'], + cited_in_publication=cls.form.cleaned_data['cited_in_publication'], + invited_by=cls.contributor, + message_style=cls.form.cleaned_data['message_style'], + personal_message=cls.form.cleaned_data['personal_message'], ) Utils.load({'invitation': invitation}) @@ -246,7 +245,7 @@ class Utils(object): email_text += ('Reminder: Invitation to SciPost\n' '-------------------------------\n\n') email_text_html += ('<strong>Reminder: Invitation to SciPost</strong>' - '<br/><hr/><br/>') + '<br/><hr/><br/>') if cls.invitation.invitation_type == 'F': email_text += 'RE: Invitation to join the Editorial College of SciPost\n\n' email_text_html += ('<strong>RE: Invitation to join the Editorial College ' @@ -262,7 +261,7 @@ class Utils(object): email_text += cls.invitation.first_name email_text_html += '{{ first_name }}' email_context['first_name'] = cls.invitation.first_name - email_text += ',\n\n' + email_text += ',\n\n' email_text_html += ',<br/>' if len(cls.invitation.personal_message) > 3: email_text += cls.invitation.personal_message + '\n\n' @@ -296,11 +295,6 @@ class Utils(object): 'useful to your work as a professional scientist.' '\n\nMany thanks in advance for taking a few minutes to look into it,' '\n\nOn behalf of the SciPost Foundation,\n\n' - #'Prof. dr Jean-Sébastien Caux\n---------------------------------------------' - #'\nInstitute for Theoretical Physics\nUniversity of Amsterdam\nScience Park 904' - #'\n1098 XH Amsterdam\nThe Netherlands\n' - #'---------------------------------------------\ntel.: +31 (0)20 5255775' - #'\nfax: +31 (0)20 5255778\n---------------------------------------------' + signature + '\n' ) @@ -378,7 +372,6 @@ class Utils(object): email_text_html += '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) html_version = html_template.render(email_context) - #emailmessage = EmailMessage( emailmessage = EmailMultiAlternatives( 'SciPost: refereeing request (and registration invitation)', email_text, 'SciPost Registration <registration@scipost.org>', @@ -417,10 +410,8 @@ class Utils(object): email_text_html += '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) html_version = html_template.render(email_context) - #emailmessage = EmailMessage( emailmessage = EmailMultiAlternatives( 'SciPost: invitation', email_text, - #'J.-S. Caux <jscaux@scipost.org>', 'SciPost registration <registration@scipost.org>', [cls.invitation.email], cc=[cls.invitation.invited_by.user.email], @@ -456,10 +447,8 @@ class Utils(object): email_text_html += '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) html_version = html_template.render(email_context) - #emailmessage = EmailMessage( emailmessage = EmailMultiAlternatives( 'SciPost: invitation', email_text, - #'J.-S. Caux <jscaux@scipost.org>', 'SciPost registration <registration@scipost.org>', [cls.invitation.email], cc=[cls.invitation.invited_by.user.email], @@ -479,10 +468,8 @@ class Utils(object): email_text_html += summary_text_html + '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) html_version = html_template.render(email_context) - #emailmessage = EmailMessage( emailmessage = EmailMultiAlternatives( 'SciPost: invitation', email_text, - #'J.-S. Caux <jscaux@scipost.org>', 'SciPost registration <registration@scipost.org>', [cls.invitation.email], cc=[cls.invitation.invited_by.user.email], @@ -640,7 +627,6 @@ class Utils(object): email_text_html += '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) html_version = html_template.render(email_context) - #emailmessage = EmailMessage( emailmessage = EmailMultiAlternatives( 'SciPost registration invitation', email_text, 'J-S Caux <jscaux@scipost.org>', @@ -650,11 +636,9 @@ class Utils(object): reply_to=['registration@scipost.org']) emailmessage.attach_alternative(html_version, 'text/html') - # This function is now for all invitation types: emailmessage.send(fail_silently=False) - @classmethod def send_citation_notification_email(cls): """ @@ -666,7 +650,7 @@ class Utils(object): email_text_html = 'Dear {{ title }} {{ last_name }}' email_context['title'] = title_dict[cls.notification.contributor.title] email_context['last_name'] = cls.notification.contributor.user.last_name - email_text += ',\n\n' + email_text += ',\n\n' email_text_html += ',<br/>' if cls.notification.cited_in_publication: email_text += ( diff --git a/scipost/views.py b/scipost/views.py index c3029a670b2f78cd617e4f5e1b086f4b79e6213e..78635541dc0746e2cb539ba7308640f21686acfa 100644 --- a/scipost/views.py +++ b/scipost/views.py @@ -8,29 +8,41 @@ from django.utils import timezone from django.shortcuts import get_object_or_404, render from django.contrib.auth import authenticate, login, logout from django.contrib.auth.decorators import login_required -from django.contrib.auth.models import User, Group, Permission +from django.contrib.auth.models import Group from django.contrib.auth.views import password_reset, password_reset_confirm from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist, PermissionDenied from django.core import mail from django.core.mail import EmailMessage, EmailMultiAlternatives from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.urlresolvers import reverse -from django.http import HttpResponse, HttpResponseRedirect, JsonResponse +from django.db.models import Q +from django.http import HttpResponseRedirect, JsonResponse from django.shortcuts import redirect -from django.template import Context, RequestContext, Template +from django.template import Context, Template from django.utils.http import is_safe_url -from django.views.decorators.csrf import csrf_protect -from django.db.models import Avg from guardian.decorators import permission_required from guardian.decorators import permission_required_or_403 from guardian.shortcuts import assign_perm -from .models import * -from .forms import * - -from .global_methods import * -from .utils import * +from .constants import SCIPOST_SUBJECT_AREAS +from .models import Contributor, CitationNotification, UnavailabilityPeriod,\ + DraftInvitation, RegistrationInvitation, NewsItem,\ + List, Team, Graph, Node, Arc,\ + title_dict, SciPost_from_addresses_dict,\ + AuthorshipClaim, SupportingPartner, SPBMembershipAgreement,\ + VGM, Feedback, Nomination, Remark, Motion, motion_categories_dict +from .forms import AuthenticationForm, DraftInvitationForm, UnavailabilityPeriodForm,\ + RegistrationForm, RegistrationInvitationForm, AuthorshipClaimForm,\ + ModifyPersonalMessageForm, SearchForm, VetRegistrationForm, reg_ref_dict,\ + UpdatePersonalDataForm, UpdateUserDataForm, PasswordChangeForm,\ + EmailGroupMembersForm, EmailParticularForm, SendPrecookedEmailForm,\ + CreateListForm, CreateTeamForm,\ + AddTeamMemberForm, CreateGraphForm,\ + ManageTeamsForm, CreateNodeForm, CreateArcForm,\ + SupportingPartnerForm, SPBMembershipForm,\ + FeedbackForm, MotionForm, NominationForm, RemarkForm +from .utils import Utils, EMAIL_FOOTER, SCIPOST_SUMMARY_FOOTER, SCIPOST_SUMMARY_FOOTER_HTML from commentaries.models import Commentary from commentaries.forms import CommentarySearchForm @@ -51,12 +63,15 @@ from theses.forms import ThesisLinkSearchForm def is_registered(user): return user.groups.filter(name='Registered Contributors').exists() + def is_SP_Admin(user): return user.groups.filter(name='SciPost Administrators').exists() + def is_MEC(user): return user.groups.filter(name='Editorial College').exists() + def is_VE(user): return user.groups.filter(name='Vetting Editors').exists() @@ -69,6 +84,7 @@ def normalize_query(query_string, """ Splits a query string in individual keywords, keeping quoted words together. """ return [normspace(' ', (t[0] or t[1]).strip()) for t in findterms(query_string)] + def get_query(query_string, search_fields): """ Returns a query, namely a combination of Q objects. """ query = None @@ -110,14 +126,12 @@ def documentsSearchResults(query): publication_query, ).order_by('-publication_date') commentary_search_queryset = Commentary.objects.filter( - #commentary_search_list = Commentary.objects.filter( commentary_query, vetted=True, ).order_by('-pub_date') submission_search_queryset = Submission.objects.filter( - #submission_search_list = Submission.objects.filter( submission_query, - ).exclude(status__in=SUBMISSION_STATUS_PUBLICLY_UNLISTED + ).exclude(status__in=SUBMISSION_STATUS_PUBLICLY_UNLISTED, ).order_by('-submission_date') thesislink_search_list = ThesisLink.objects.filter( thesislink_query, @@ -129,9 +143,7 @@ def documentsSearchResults(query): ).order_by('-date_submitted') context = {'publication_search_queryset': publication_search_queryset, 'commentary_search_queryset': commentary_search_queryset, - #'commentary_search_list': commentary_search_list, 'submission_search_queryset': submission_search_queryset, - #'submission_search_list': submission_search_list, 'thesislink_search_list': thesislink_search_list, 'comment_search_list': comment_search_list} return context @@ -152,7 +164,7 @@ def search(request): context = {} if 'publication_search_queryset' in context: - publication_search_list_paginator = Paginator (context['publication_search_queryset'], 10) + publication_search_list_paginator = Paginator(context['publication_search_queryset'], 10) publication_search_list_page = request.GET.get('publication_search_list_page') try: publication_search_list = publication_search_list_paginator.page( @@ -165,7 +177,7 @@ def search(request): context['publication_search_list'] = publication_search_list if 'commentary_search_queryset' in context: - commentary_search_list_paginator = Paginator (context['commentary_search_queryset'], 10) + commentary_search_list_paginator = Paginator(context['commentary_search_queryset'], 10) commentary_search_list_page = request.GET.get('commentary_search_list_page') try: commentary_search_list = commentary_search_list_paginator.page( @@ -178,7 +190,7 @@ def search(request): context['commentary_search_list'] = commentary_search_list if 'submission_search_queryset' in context: - submission_search_list_paginator = Paginator (context['submission_search_queryset'], 10) + submission_search_list_paginator = Paginator(context['submission_search_queryset'], 10) submission_search_list_page = request.GET.get('submission_search_list_page') try: submission_search_list = submission_search_list_paginator.page( @@ -210,6 +222,7 @@ def index(request): } return render(request, 'scipost/index.html', context) + ############### # Information ############### @@ -218,11 +231,13 @@ def base(request): """ Skeleton for pages, used in template inheritance """ return render(request, 'scipost/base.html') + def news(request): newsitems = NewsItem.objects.all().order_by('-date') context = {'newsitems': newsitems} return render(request, 'scipost/news.html', context) + def feeds(request): context = {'subject_areas_physics': SCIPOST_SUBJECT_AREAS[0][1]} return render(request, 'scipost/feeds.html', context) @@ -247,7 +262,8 @@ def register(request): {'form': form, 'errormessage': 'This username is already in use'}) if Utils.email_already_taken(): return render(request, 'scipost/register.html', - {'form': form, 'errormessage': 'This email address is already in use'}) + {'form': form, + 'errormessage': 'This email address is already in use'}) Utils.create_and_save_contributor('') Utils.send_registration_email() # If this email was associated to an invitation, mark it as responded to @@ -265,7 +281,8 @@ def register(request): invitation_to_delete.delete() context = {'ack_header': 'Thanks for registering to SciPost.', 'ack_message': ('You will receive an email with a link to verify ' - 'your email address. Please visit this link within 48 hours. ' + 'your email address. ' + 'Please visit this link within 48 hours. ' 'Your credentials will thereafter be verified. ' 'If your registration is vetted through by the ' 'administrators, you will be enabled to contribute.'), @@ -291,9 +308,9 @@ def invitation(request, key): Utils.load({'form': form}) if form.is_valid(): if Utils.password_mismatch(): - return render(request, 'scipost/register.html', - {'form': form, 'invited': True, 'key': key, - 'errormessage': 'Your passwords must match'}) + return render(request, 'scipost/register.html', { + 'form': form, 'invited': True, 'key': key, + 'errormessage': 'Your passwords must match'}) if Utils.username_already_taken(): return render(request, 'scipost/register.html', {'form': form, 'invited': True, 'key': key, @@ -308,7 +325,8 @@ def invitation(request, key): Utils.send_registration_email() context = {'ack_header': 'Thanks for registering to SciPost.', 'ack_message': ('You will receive an email with a link to verify ' - 'your email address. Please visit this link within 48 hours. ' + 'your email address. ' + 'Please visit this link within 48 hours. ' 'Your credentials will thereafter be verified. ' 'If your registration is vetted through by the ' 'administrators, you will be enabled to contribute.'), @@ -329,30 +347,27 @@ def invitation(request, key): welcome_message = ('Welcome, ' + title_dict[invitation.title] + ' ' + invitation.last_name + ', and thanks in advance for ' 'registering (by completing this form)') - return render(request, 'scipost/register.html', - {'form': form, 'invited': True, 'key': key, - 'errormessage': errormessage, 'welcome_message': welcome_message}) + return render(request, 'scipost/register.html', { + 'form': form, 'invited': True, 'key': key, + 'errormessage': errormessage, 'welcome_message': welcome_message}) context = {'errormessage': errormessage} return render(request, 'scipost/accept_invitation_error.html', context) - def activation(request, key): """ After registration, an email verification link is sent. Once clicked, the account is activated. """ contributor = get_object_or_404(Contributor, activation_key=key) - if contributor.user.is_active == False: + if not contributor.user.is_active: if timezone.now() > contributor.key_expires: - id_user = contributor.user.id context = {'oldkey': key} return render(request, 'scipost/request_new_activation_link.html', context) else: contributor.user.is_active = True contributor.user.save() - #return render(request, 'scipost/activation_ack.html') context = {'ack_header': 'Your email address has been confirmed.', 'ack_message': ('Your SciPost account will soon be vetted. ' 'You will soon receive an email from us.'), @@ -368,7 +383,7 @@ def request_new_activation_link(request, oldkey): salt = "" for i in range(5): salt = salt + random.choice(string.ascii_letters) - #salt = hashlib.sha1(str(random.random()).encode('utf8')).hexdigest()[:5] + salt = salt.encode('utf8') usernamesalt = contributor.user.username usernamesalt = usernamesalt.encode('utf8') @@ -389,11 +404,11 @@ def request_new_activation_link(request, oldkey): [contributor.user.email, 'registration@scipost.org'], reply_to=['registration@scipost.org']) emailmessage.send(fail_silently=False) - #return render (request, 'scipost/request_new_activation_link_ack.html') - context = {'ack_header': 'We have emailed you a new activation link.', - 'ack_message': ('Please acknowledge it within its 48 hours validity ' - 'window if you want us to proceed with vetting your registraion.'), - } + context = { + 'ack_header': 'We have emailed you a new activation link.', + 'ack_message': ('Please acknowledge it within its 48 hours validity ' + 'window if you want us to proceed with vetting your registraion.'), + } return render(request, 'scipost/acknowledgement.html', context) @@ -405,12 +420,13 @@ def unsubscribe(request, key): want to receive any non-essential email notifications from SciPost. """ contributor = get_object_or_404(Contributor, activation_key=key) - context = {'contributor': contributor,} + context = {'contributor': contributor, } return render(request, 'scipost/unsubscribe.html', context) + def unsubscribe_confirm(request, key): contributor = get_object_or_404(Contributor, activation_key=key) - contributor.accepts_SciPost_emails=False + contributor.accepts_SciPost_emails = False contributor.save() context = {'ack_header': 'Unsubscribe', 'followup_message': ('We have recorded your preference: you will ' @@ -423,15 +439,14 @@ def unsubscribe_confirm(request, key): @permission_required('scipost.can_vet_registration_requests', return_403=True) def vet_registration_requests(request): - contributor = Contributor.objects.get(user=request.user) contributors_to_vet = (Contributor.objects .filter(user__is_active=True, status=0) .order_by('key_expires')) - reg_cont_group = Group.objects.get(name='Registered Contributors') # TODO: remove this line? form = VetRegistrationForm() - context = {'contributors_to_vet': contributors_to_vet, 'form': form } + context = {'contributors_to_vet': contributors_to_vet, 'form': form} return render(request, 'scipost/vet_registration_requests.html', context) + @permission_required('scipost.can_vet_registration_requests', return_403=True) def vet_registration_request_ack(request, contributor_id): # process the form @@ -495,8 +510,6 @@ def vet_registration_request_ack(request, contributor_id): contributor.status = form.cleaned_data['refusal_reason'] contributor.save() - #context = {} - #return render(request, 'scipost/vet_registration_request_ack.html', context) context = {'ack_header': 'SciPost Registration request vetted.', 'followup_message': 'Back to ', 'followup_link': reverse('scipost:vet_registration_requests'), @@ -577,30 +590,31 @@ def draft_registration_invitation(request): 'user__first_name', 'user__last_name') existing_drafts = DraftInvitation.objects.filter(processed=False).order_by('last_name') - context = {'draft_inv_form': draft_inv_form, 'errormessage': errormessage, - 'sent_reg_inv_fellows': sent_reg_inv_fellows, - 'nr_sent_reg_inv_fellows': nr_sent_reg_inv_fellows, - 'sent_reg_inv_contrib': sent_reg_inv_contrib, - 'nr_sent_reg_inv_contrib': nr_sent_reg_inv_contrib, - 'sent_reg_inv_ref': sent_reg_inv_ref, - 'nr_sent_reg_inv_ref': nr_sent_reg_inv_ref, - 'sent_reg_inv_cited_sub': sent_reg_inv_cited_sub, - 'nr_sent_reg_inv_cited_sub': nr_sent_reg_inv_cited_sub, - 'sent_reg_inv_cited_pub': sent_reg_inv_cited_pub, - 'nr_sent_reg_inv_cited_pub': nr_sent_reg_inv_cited_pub, - 'resp_reg_inv_fellows': resp_reg_inv_fellows, - 'nr_resp_reg_inv_fellows': nr_resp_reg_inv_fellows, - 'resp_reg_inv_contrib': resp_reg_inv_contrib, - 'nr_resp_reg_inv_contrib': nr_resp_reg_inv_contrib, - 'resp_reg_inv_ref': resp_reg_inv_ref, - 'nr_resp_reg_inv_ref': nr_resp_reg_inv_ref, - 'resp_reg_inv_cited_sub': resp_reg_inv_cited_sub, - 'nr_resp_reg_inv_cited_sub': nr_resp_reg_inv_cited_sub, - 'resp_reg_inv_cited_pub': resp_reg_inv_cited_pub, - 'nr_resp_reg_inv_cited_pub': nr_resp_reg_inv_cited_pub, - 'decl_reg_inv': decl_reg_inv, - 'names_reg_contributors': names_reg_contributors, - 'existing_drafts': existing_drafts, + context = { + 'draft_inv_form': draft_inv_form, 'errormessage': errormessage, + 'sent_reg_inv_fellows': sent_reg_inv_fellows, + 'nr_sent_reg_inv_fellows': nr_sent_reg_inv_fellows, + 'sent_reg_inv_contrib': sent_reg_inv_contrib, + 'nr_sent_reg_inv_contrib': nr_sent_reg_inv_contrib, + 'sent_reg_inv_ref': sent_reg_inv_ref, + 'nr_sent_reg_inv_ref': nr_sent_reg_inv_ref, + 'sent_reg_inv_cited_sub': sent_reg_inv_cited_sub, + 'nr_sent_reg_inv_cited_sub': nr_sent_reg_inv_cited_sub, + 'sent_reg_inv_cited_pub': sent_reg_inv_cited_pub, + 'nr_sent_reg_inv_cited_pub': nr_sent_reg_inv_cited_pub, + 'resp_reg_inv_fellows': resp_reg_inv_fellows, + 'nr_resp_reg_inv_fellows': nr_resp_reg_inv_fellows, + 'resp_reg_inv_contrib': resp_reg_inv_contrib, + 'nr_resp_reg_inv_contrib': nr_resp_reg_inv_contrib, + 'resp_reg_inv_ref': resp_reg_inv_ref, + 'nr_resp_reg_inv_ref': nr_resp_reg_inv_ref, + 'resp_reg_inv_cited_sub': resp_reg_inv_cited_sub, + 'nr_resp_reg_inv_cited_sub': nr_resp_reg_inv_cited_sub, + 'resp_reg_inv_cited_pub': resp_reg_inv_cited_pub, + 'nr_resp_reg_inv_cited_pub': nr_resp_reg_inv_cited_pub, + 'decl_reg_inv': decl_reg_inv, + 'names_reg_contributors': names_reg_contributors, + 'existing_drafts': existing_drafts, } return render(request, 'scipost/draft_registration_invitation.html', context) @@ -624,7 +638,7 @@ def edit_draft_reg_inv(request, draft_id): draft_inv_form = DraftInvitationForm(instance=draft) context = {'draft_inv_form': draft_inv_form, 'draft': draft, - 'errormessage': errormessage,} + 'errormessage': errormessage, } return render(request, 'scipost/edit_draft_reg_inv.html', context) @@ -638,7 +652,6 @@ def map_draft_reg_inv_to_contributor(request, draft_id, contributor_id): """ draft = get_object_or_404(DraftInvitation, id=draft_id) contributor = get_object_or_404(Contributor, id=contributor_id) - errormessage = '' draft.processed = True draft.save() citation = CitationNotification( @@ -699,14 +712,15 @@ def registration_invitations(request, draft_id=None): draft = get_object_or_404(DraftInvitation, id=draft_id) associated_contributors = Contributor.objects.filter( user__last_name__icontains=draft.last_name) - initial = {'title': draft.title, - 'first_name': draft.first_name, - 'last_name': draft.last_name, - 'email': draft.email, - 'invitation_type': draft.invitation_type, - 'cited_in_submission': draft.cited_in_submission, - 'cited_in_publication': draft.cited_in_publication, - } + initial = { + 'title': draft.title, + 'first_name': draft.first_name, + 'last_name': draft.last_name, + 'email': draft.email, + 'invitation_type': draft.invitation_type, + 'cited_in_submission': draft.cited_in_submission, + 'cited_in_publication': draft.cited_in_publication, + } reg_inv_form = RegistrationInvitationForm(initial=initial) sent_reg_inv = RegistrationInvitation.objects.filter(responded=False, declined=False) @@ -740,31 +754,32 @@ def registration_invitations(request, draft_id=None): 'user__first_name', 'user__last_name') existing_drafts = DraftInvitation.objects.filter(processed=False).order_by('last_name') - context = {'reg_inv_form': reg_inv_form, 'errormessage': errormessage, - 'sent_reg_inv_fellows': sent_reg_inv_fellows, - 'nr_sent_reg_inv_fellows': nr_sent_reg_inv_fellows, - 'sent_reg_inv_contrib': sent_reg_inv_contrib, - 'nr_sent_reg_inv_contrib': nr_sent_reg_inv_contrib, - 'sent_reg_inv_ref': sent_reg_inv_ref, - 'nr_sent_reg_inv_ref': nr_sent_reg_inv_ref, - 'sent_reg_inv_cited_sub': sent_reg_inv_cited_sub, - 'nr_sent_reg_inv_cited_sub': nr_sent_reg_inv_cited_sub, - 'sent_reg_inv_cited_pub': sent_reg_inv_cited_pub, - 'nr_sent_reg_inv_cited_pub': nr_sent_reg_inv_cited_pub, - 'resp_reg_inv_fellows': resp_reg_inv_fellows, - 'nr_resp_reg_inv_fellows': nr_resp_reg_inv_fellows, - 'resp_reg_inv_contrib': resp_reg_inv_contrib, - 'nr_resp_reg_inv_contrib': nr_resp_reg_inv_contrib, - 'resp_reg_inv_ref': resp_reg_inv_ref, - 'nr_resp_reg_inv_ref': nr_resp_reg_inv_ref, - 'resp_reg_inv_cited_sub': resp_reg_inv_cited_sub, - 'nr_resp_reg_inv_cited_sub': nr_resp_reg_inv_cited_sub, - 'resp_reg_inv_cited_pub': resp_reg_inv_cited_pub, - 'nr_resp_reg_inv_cited_pub': nr_resp_reg_inv_cited_pub, - 'decl_reg_inv': decl_reg_inv, - 'names_reg_contributors': names_reg_contributors, - 'existing_drafts': existing_drafts, - 'associated_contributors': associated_contributors, + context = { + 'reg_inv_form': reg_inv_form, 'errormessage': errormessage, + 'sent_reg_inv_fellows': sent_reg_inv_fellows, + 'nr_sent_reg_inv_fellows': nr_sent_reg_inv_fellows, + 'sent_reg_inv_contrib': sent_reg_inv_contrib, + 'nr_sent_reg_inv_contrib': nr_sent_reg_inv_contrib, + 'sent_reg_inv_ref': sent_reg_inv_ref, + 'nr_sent_reg_inv_ref': nr_sent_reg_inv_ref, + 'sent_reg_inv_cited_sub': sent_reg_inv_cited_sub, + 'nr_sent_reg_inv_cited_sub': nr_sent_reg_inv_cited_sub, + 'sent_reg_inv_cited_pub': sent_reg_inv_cited_pub, + 'nr_sent_reg_inv_cited_pub': nr_sent_reg_inv_cited_pub, + 'resp_reg_inv_fellows': resp_reg_inv_fellows, + 'nr_resp_reg_inv_fellows': nr_resp_reg_inv_fellows, + 'resp_reg_inv_contrib': resp_reg_inv_contrib, + 'nr_resp_reg_inv_contrib': nr_resp_reg_inv_contrib, + 'resp_reg_inv_ref': resp_reg_inv_ref, + 'nr_resp_reg_inv_ref': nr_resp_reg_inv_ref, + 'resp_reg_inv_cited_sub': resp_reg_inv_cited_sub, + 'nr_resp_reg_inv_cited_sub': nr_resp_reg_inv_cited_sub, + 'resp_reg_inv_cited_pub': resp_reg_inv_cited_pub, + 'nr_resp_reg_inv_cited_pub': nr_resp_reg_inv_cited_pub, + 'decl_reg_inv': decl_reg_inv, + 'names_reg_contributors': names_reg_contributors, + 'existing_drafts': existing_drafts, + 'associated_contributors': associated_contributors, } return render(request, 'scipost/registration_invitations.html', context) @@ -806,9 +821,9 @@ def edit_invitation_personal_message(request, invitation_id): errormessage = 'The form was invalid.' else: form = ModifyPersonalMessageForm( - initial={'personal_message': invitation.personal_message,}) + initial={'personal_message': invitation.personal_message, }) context = {'invitation': invitation, - 'form': form, 'errormessage': errormessage,} + 'form': form, 'errormessage': errormessage, } return render(request, 'scipost/edit_invitation_personal_message.html', context) @@ -819,8 +834,8 @@ def renew_registration_invitation(request, invitation_id): """ invitation = get_object_or_404(RegistrationInvitation, pk=invitation_id) errormessage = None - if (invitation.invitation_type == 'F' - and not request.user.has_perm('scipost.can_invite_Fellows')): + if(invitation.invitation_type == 'F' + and not request.user.has_perm('scipost.can_invite_Fellows')): errormessage = ('You do not have the authorization to send a Fellow-type ' 'invitation. Consider Contributor, or cited (sub/pub). ') elif invitation.invitation_type == 'R': @@ -850,14 +865,14 @@ def mark_reg_inv_as_declined(request, invitation_id): def citation_notifications(request): unprocessed_notifications = CitationNotification.objects.filter( processed=False).order_by('contributor__user__last_name') - context = {'unprocessed_notifications': unprocessed_notifications,} + context = {'unprocessed_notifications': unprocessed_notifications, } return render(request, 'scipost/citation_notifications.html', context) @permission_required('scipost.can_manage_registration_invitations', return_403=True) def process_citation_notification(request, cn_id): notification = get_object_or_404(CitationNotification, id=cn_id) - notification.processed=True + notification.processed = True notification.save() if notification.contributor.accepts_SciPost_emails: Utils.load({'notification': notification}) @@ -886,8 +901,6 @@ def login_view(request): if user is not None and is_registered(user): if user.is_active: login(request, user) - contributor = Contributor.objects.get(user=request.user) - context = {'contributor': contributor } return redirect(redirect_to) else: return render(request, 'scipost/disabled_account.html') @@ -914,7 +927,8 @@ def mark_unavailable_period(request): elif unav_form.cleaned_data['end'] < now.date(): errormessage = 'You have entered an end date in the past.' if errormessage is not None: - return render(request, 'scipost/error.html', context={'errormessage': errormessage}) + return render(request, 'scipost/error.html', + context={'errormessage': errormessage}) else: unav = UnavailabilityPeriod( contributor=request.user.contributor, @@ -933,11 +947,13 @@ def personal_page(request): """ if request.user.is_authenticated(): contributor = Contributor.objects.get(user=request.user) + # Compile the unavailability periods: now = timezone.now() unavailabilities = UnavailabilityPeriod.objects.filter( contributor=contributor).exclude(end__lt=now).order_by('start') unavailability_form = UnavailabilityPeriodForm() + # if an editor, count the number of actions required: nr_reg_to_vet = 0 nr_reg_awaiting_validation = 0 @@ -945,6 +961,7 @@ def personal_page(request): nr_recommendations_to_prepare_for_voting = 0 if is_SP_Admin(request.user): intwodays = now + timezone.timedelta(days=2) + # count the number of pending registration requests nr_reg_to_vet = Contributor.objects.filter(user__is_active=True, status=0).count() nr_reg_awaiting_validation = Contributor.objects.filter( @@ -1005,10 +1022,10 @@ def personal_page(request): .exclude(author_false_claims__in=[contributor]) .count()) own_comments = (Comment.objects - .filter(author=contributor,is_author_reply=False) + .filter(author=contributor, is_author_reply=False) .order_by('-date_submitted')) own_authorreplies = (Comment.objects - .filter(author=contributor,is_author_reply=True) + .filter(author=contributor, is_author_reply=True) .order_by('-date_submitted')) lists_owned = List.objects.filter(owner=contributor) lists = List.objects.filter(teams_with_access__members__in=[contributor]) @@ -1018,37 +1035,38 @@ def personal_page(request): graphs_private = Graph.objects.filter(Q(teams_with_access__leader=contributor) | Q(teams_with_access__members__in=[contributor])) appellation = title_dict[contributor.title] + ' ' + contributor.user.last_name - context = {'contributor': contributor, - 'appellation': appellation, - 'unavailabilities': unavailabilities, - 'unavailability_form': unavailability_form, - 'nr_reg_to_vet': nr_reg_to_vet, - 'nr_reg_awaiting_validation': nr_reg_awaiting_validation, - 'nr_commentary_page_requests_to_vet': nr_commentary_page_requests_to_vet, - 'nr_comments_to_vet': nr_comments_to_vet, - 'nr_thesislink_requests_to_vet': nr_thesislink_requests_to_vet, - 'nr_authorship_claims_to_vet': nr_authorship_claims_to_vet, - 'nr_reports_to_vet': nr_reports_to_vet, - 'nr_submissions_to_assign': nr_submissions_to_assign, - 'nr_recommendations_to_prepare_for_voting': nr_recommendations_to_prepare_for_voting, - 'nr_assignments_to_consider': nr_assignments_to_consider, - 'active_assignments': active_assignments, - 'nr_submission_authorships_to_claim': nr_submission_authorships_to_claim, - 'nr_commentary_authorships_to_claim': nr_commentary_authorships_to_claim, - 'nr_thesis_authorships_to_claim': nr_thesis_authorships_to_claim, - 'nr_ref_inv_to_consider': nr_ref_inv_to_consider, - 'pending_ref_tasks': pending_ref_tasks, - 'own_submissions': own_submissions, - 'own_commentaries': own_commentaries, - 'own_thesislinks': own_thesislinks, - 'own_comments': own_comments, 'own_authorreplies': own_authorreplies, - 'lists_owned': lists_owned, - 'lists': lists, - 'teams_led': teams_led, - 'teams': teams, - 'graphs_owned': graphs_owned, - 'graphs_private': graphs_private, - } + context = { + 'contributor': contributor, + 'appellation': appellation, + 'unavailabilities': unavailabilities, + 'unavailability_form': unavailability_form, + 'nr_reg_to_vet': nr_reg_to_vet, + 'nr_reg_awaiting_validation': nr_reg_awaiting_validation, + 'nr_commentary_page_requests_to_vet': nr_commentary_page_requests_to_vet, + 'nr_comments_to_vet': nr_comments_to_vet, + 'nr_thesislink_requests_to_vet': nr_thesislink_requests_to_vet, + 'nr_authorship_claims_to_vet': nr_authorship_claims_to_vet, + 'nr_reports_to_vet': nr_reports_to_vet, + 'nr_submissions_to_assign': nr_submissions_to_assign, + 'nr_recommendations_to_prepare_for_voting': nr_recommendations_to_prepare_for_voting, + 'nr_assignments_to_consider': nr_assignments_to_consider, + 'active_assignments': active_assignments, + 'nr_submission_authorships_to_claim': nr_submission_authorships_to_claim, + 'nr_commentary_authorships_to_claim': nr_commentary_authorships_to_claim, + 'nr_thesis_authorships_to_claim': nr_thesis_authorships_to_claim, + 'nr_ref_inv_to_consider': nr_ref_inv_to_consider, + 'pending_ref_tasks': pending_ref_tasks, + 'own_submissions': own_submissions, + 'own_commentaries': own_commentaries, + 'own_thesislinks': own_thesislinks, + 'own_comments': own_comments, 'own_authorreplies': own_authorreplies, + 'lists_owned': lists_owned, + 'lists': lists, + 'teams_led': teams_led, + 'teams': teams, + 'graphs_owned': graphs_owned, + 'graphs_private': graphs_private, + } return render(request, 'scipost/personal_page.html', context) else: form = AuthenticationForm() @@ -1060,6 +1078,7 @@ def personal_page(request): def change_password(request): if request.method == 'POST': form = PasswordChangeForm(request.POST) + ack = False if form.is_valid(): if not request.user.check_password(form.cleaned_data['password_prev']): return render( @@ -1067,16 +1086,17 @@ def change_password(request): {'form': form, 'errormessage': 'The currently existing password you entered is incorrect'}) if form.cleaned_data['password_new'] != form.cleaned_data['password_verif']: - return render(request, 'scipost/change_password.html', - {'form': form, 'errormessage': 'Your new password entries must match'}) + return render(request, 'scipost/change_password.html', { + 'form': form, + 'errormessage': 'Your new password entries must match'}) request.user.set_password(form.cleaned_data['password_new']) request.user.save() ack = True - context = {'ack': True, 'form': form} + context = {'ack': ack, 'form': form} else: form = PasswordChangeForm() context = {'ack': False, 'form': form} - return render (request, 'scipost/change_password.html', context) + return render(request, 'scipost/change_password.html', context) def reset_password_confirm(request, uidb64=None, token=None): @@ -1084,11 +1104,12 @@ def reset_password_confirm(request, uidb64=None, token=None): uidb64=uidb64, token=token, post_reset_redirect=reverse('scipost:login')) + def reset_password(request): return password_reset(request, template_name='scipost/reset_password.html', - email_template_name='scipost/reset_password_email.html', - subject_template_name='scipost/reset_password_subject.txt', - post_reset_redirect=reverse('scipost:login')) + email_template_name='scipost/reset_password_email.html', + subject_template_name='scipost/reset_password_subject.txt', + post_reset_redirect=reverse('scipost:login')) @login_required @@ -1112,7 +1133,6 @@ def update_personal_data(request): request.user.contributor.accepts_SciPost_emails = cont_form.cleaned_data['accepts_SciPost_emails'] request.user.save() request.user.contributor.save() - #return render(request, 'scipost/update_personal_data_ack.html') context = {'ack_header': 'Your personal data has been updated.', 'followup_message': 'Return to your ', 'followup_link': reverse('scipost:personal_page'), @@ -1120,6 +1140,9 @@ def update_personal_data(request): return render(request, 'scipost/acknowledgement.html', context) else: user_form = UpdateUserDataForm(instance=contributor.user) + # Prevent exploit of gaining view on self-authored submissions by changing surname. + user_form.fields['last_name'].widget.attrs['disabled'] = True + # Surname can only be changed through the admin. cont_form = UpdatePersonalDataForm(instance=contributor) return render(request, 'scipost/update_personal_data.html', {'user_form': user_form, 'cont_form': cont_form}) @@ -1168,7 +1191,7 @@ def claim_authorships(request): def claim_sub_authorship(request, submission_id, claim): if request.method == 'POST': contributor = Contributor.objects.get(user=request.user) - submission = get_object_or_404(Submission,pk=submission_id) + submission = get_object_or_404(Submission, pk=submission_id) if claim == '1': submission.authors_claims.add(contributor) newclaim = AuthorshipClaim(claimant=contributor, submission=submission) @@ -1178,11 +1201,12 @@ def claim_sub_authorship(request, submission_id, claim): submission.save() return redirect('scipost:claim_authorships') + @login_required def claim_com_authorship(request, commentary_id, claim): if request.method == 'POST': contributor = Contributor.objects.get(user=request.user) - commentary = get_object_or_404(Commentary,pk=commentary_id) + commentary = get_object_or_404(Commentary, pk=commentary_id) if claim == '1': commentary.authors_claims.add(contributor) newclaim = AuthorshipClaim(claimant=contributor, commentary=commentary) @@ -1192,11 +1216,12 @@ def claim_com_authorship(request, commentary_id, claim): commentary.save() return redirect('scipost:claim_authorships') + @login_required def claim_thesis_authorship(request, thesis_id, claim): if request.method == 'POST': contributor = Contributor.objects.get(user=request.user) - thesislink = get_object_or_404(ThesisLink,pk=thesis_id) + thesislink = get_object_or_404(ThesisLink, pk=thesis_id) if claim == '1': thesislink.author_claims.add(contributor) newclaim = AuthorshipClaim(claimant=contributor, thesislink=thesislink) @@ -1213,6 +1238,7 @@ def vet_authorship_claims(request): context = {'claims_to_vet': claims_to_vet} return render(request, 'scipost/vet_authorship_claims.html', context) + @permission_required('scipost.can_vet_authorship_claims', return_403=True) def vet_authorship_claim(request, claim_id, claim): if request.method == 'POST': @@ -1322,8 +1348,8 @@ def email_group_members(request): email_text = '' email_text_html = '' if form.cleaned_data['personalize']: - email_text = ('Dear ' + title_dict[member.contributor.title] + ' ' + - member.last_name + ', \n\n') + email_text = ('Dear ' + title_dict[member.contributor.title] + + ' ' + member.last_name + ', \n\n') email_text_html = 'Dear {{ title }} {{ last_name }},<br/>' email_text += form.cleaned_data['email_text'] email_text_html += '{{ email_text|linebreaks }}' @@ -1347,9 +1373,6 @@ def email_group_members(request): }) html_template = Template(email_text_html) html_version = html_template.render(email_context) - # mail.EmailMessage(form.cleaned_data['email_subject'], - # email_text, 'SciPost Admin <admin@scipost.org>', - # [member.email], connection=connection).send() message = EmailMultiAlternatives( form.cleaned_data['email_subject'], email_text, 'SciPost Admin <admin@scipost.org>', @@ -1453,7 +1476,6 @@ def send_precooked_email(request): # Editorial College # ##################### - def EdCol_bylaws(request): return render(request, 'scipost/EdCol_by-laws.html') @@ -1462,7 +1484,7 @@ def EdCol_bylaws(request): def Fellow_activity_overview(request, Fellow_id=None): Fellows = Contributor.objects.filter( user__groups__name='Editorial College').order_by('user__last_name') - context = {'Fellows': Fellows,} + context = {'Fellows': Fellows, } if Fellow_id: Fellow = get_object_or_404(Contributor, pk=Fellow_id) context['Fellow'] = Fellow @@ -1472,6 +1494,241 @@ def Fellow_activity_overview(request, Fellow_id=None): return render(request, 'scipost/Fellow_activity_overview.html', context) +@login_required +@permission_required('scipost.can_attend_VGMs', return_403=True) +def VGMs(request): + VGM_list = VGM.objects.all().order_by('start_date') + context = {'VGM_list': VGM_list} + return render(request, 'scipost/VGMs.html', context) + + +@login_required +@permission_required('scipost.can_attend_VGMs', return_403=True) +def VGM_detail(request, VGM_id): + VGM_instance = get_object_or_404(VGM, id=VGM_id) + VGM_information = Template(VGM_instance.information).render(Context({})) + feedback_received = Feedback.objects.filter(VGM=VGM_instance).order_by('date') + feedback_form = FeedbackForm() + current_Fellows = Contributor.objects.filter( + user__groups__name='Editorial College').order_by('user__last_name') + sent_inv_Fellows = RegistrationInvitation.objects.filter( + invitation_type='F', responded=False) + pending_inv_Fellows = sent_inv_Fellows.filter(declined=False).order_by('last_name') + declined_inv_Fellows = sent_inv_Fellows.filter(declined=True).order_by('last_name') + nomination_form = NominationForm() + nominations = Nomination.objects.filter(accepted=None).order_by('last_name') + motion_form = MotionForm() + remark_form = RemarkForm() + context = { + 'VGM': VGM_instance, + 'VGM_information': VGM_information, + 'feedback_received': feedback_received, + 'feedback_form': feedback_form, + 'current_Fellows': current_Fellows, + 'pending_inv_Fellows': pending_inv_Fellows, + 'declined_inv_Fellows': declined_inv_Fellows, + 'nominations': nominations, + 'nomination_form': nomination_form, + 'motion_categories_dict': motion_categories_dict, + 'motion_form': motion_form, + 'remark_form': remark_form, + } + return render(request, 'scipost/VGM_detail.html', context) + + +@login_required +@permission_required('scipost.can_attend_VGMs', return_403=True) +def feedback(request, VGM_id=None): + if request.method == 'POST': + feedback_form = FeedbackForm(request.POST) + if feedback_form.is_valid(): + feedback = Feedback(by=request.user.contributor, + date=timezone.now().date(), + feedback=feedback_form.cleaned_data['feedback'],) + if VGM_id: + VGM_instance = get_object_or_404(VGM, id=VGM_id) + feedback.VGM = VGM_instance + feedback.save() + ack_message = 'Your feedback has been received.' + context = {'ack_message': ack_message} + if VGM_id: + context['followup_message'] = 'Return to the ' + context['followup_link'] = reverse('scipost:VGM_detail', + kwargs={'VGM_id': VGM_id}) + context['followup_link_label'] = 'VGM page' + return render(request, 'scipost/acknowledgement.html', context) + else: + errormessage = 'The form was not filled properly.' + return render(request, 'scipost/error.html', {'errormessage': errormessage}) + else: + errormessage = 'This view can only be posted to.' + return render(request, 'scipost/error.html', {'errormessage': errormessage}) + + +@login_required +@permission_required('scipost.can_attend_VGMs', raise_exception=True) +def add_remark_on_feedback(request, VGM_id, feedback_id): + # contributor = request.user.contributor + feedback = get_object_or_404(Feedback, pk=feedback_id) + if request.method == 'POST': + remark_form = RemarkForm(request.POST) + if remark_form.is_valid(): + remark = Remark(contributor=request.user.contributor, + feedback=feedback, + date=timezone.now(), + remark=remark_form.cleaned_data['remark']) + remark.save() + return HttpResponseRedirect('/VGM/' + str(VGM_id) + + '/#feedback_id' + str(feedback.id)) + else: + errormessage = 'The form was invalidly filled.' + return render(request, 'scipost/error.html', {'errormessage': errormessage}) + else: + errormessage = 'This view can only be posted to.' + return render(request, 'scipost/error.html', {'errormessage': errormessage}) + + +@login_required +@permission_required('scipost.can_attend_VGMs', return_403=True) +def nominate_Fellow(request, VGM_id): + VGM_instance = get_object_or_404(VGM, id=VGM_id) + if request.method == 'POST': + nomination_form = NominationForm(request.POST) + if nomination_form.is_valid(): + nomination = Nomination( + VGM=VGM_instance, + by=request.user.contributor, + date=timezone.now().date(), + first_name=nomination_form.cleaned_data['first_name'], + last_name=nomination_form.cleaned_data['last_name'], + discipline=nomination_form.cleaned_data['discipline'], + expertises=nomination_form.cleaned_data['expertises'], + webpage=nomination_form.cleaned_data['webpage'], + voting_deadline=VGM_instance.end_date + datetime.timedelta(days=7), + ) + nomination.save() + nomination.update_votes(request.user.contributor.id, 'A') + ack_message = 'The nomination has been registered.' + context = {'ack_message': ack_message, + 'followup_message': 'Return to the ', + 'followup_link': reverse('scipost:VGM_detail', kwargs={'VGM_id': VGM_id}), + 'followup_link_label': 'VGM page'} + return render(request, 'scipost/acknowledgement.html', context) + else: + errormessage = 'The form was not filled properly.' + return render(request, 'scipost/error.html', {'errormessage': errormessage}) + else: + errormessage = 'This view can only be posted to.' + return render(request, 'scipost/error.html', {'errormessage': errormessage}) + + +@login_required +@permission_required('scipost.can_attend_VGMs', raise_exception=True) +def add_remark_on_nomination(request, VGM_id, nomination_id): + # contributor = request.user.contributor + nomination = get_object_or_404(Nomination, pk=nomination_id) + if request.method == 'POST': + remark_form = RemarkForm(request.POST) + if remark_form.is_valid(): + remark = Remark(contributor=request.user.contributor, + nomination=nomination, + date=timezone.now(), + remark=remark_form.cleaned_data['remark']) + remark.save() + return HttpResponseRedirect('/VGM/' + str(VGM_id) + + '/#nomination_id' + str(nomination.id)) + else: + errormessage = 'The form was invalidly filled.' + return render(request, 'scipost/error.html', {'errormessage': errormessage}) + else: + errormessage = 'This view can only be posted to.' + return render(request, 'scipost/error.html', {'errormessage': errormessage}) + + +@login_required +@permission_required('scipost.can_attend_VGMs', raise_exception=True) +def vote_on_nomination(request, nomination_id, vote): + contributor = request.user.contributor + nomination = get_object_or_404(Nomination, pk=nomination_id) + if timezone.now() > nomination.voting_deadline: + errormessage = 'The voting deadline on this nomination has passed.' + return render(request, 'scipost/error.html', {'errormessage': errormessage}) + nomination.update_votes(contributor.id, vote) + return HttpResponseRedirect('/VGM/' + str(nomination.VGM.id) + + '/#nomination_id' + str(nomination.id)) + + +@login_required +@permission_required('scipost.can_attend_VGMs', return_403=True) +def put_motion_forward(request, VGM_id): + VGM_instance = get_object_or_404(VGM, id=VGM_id) + if timezone.now().date() > VGM_instance.end_date: + errormessage = 'This VGM has ended. No new motions can be put forward.' + return render(request, 'scipost/error.html', {'errormessage': errormessage}) + if request.method == 'POST': + motion_form = MotionForm(request.POST) + if motion_form.is_valid(): + motion = Motion( + category=motion_form.cleaned_data['category'], + VGM=VGM_instance, + background=motion_form.cleaned_data['background'], + motion=motion_form.cleaned_data['motion'], + put_forward_by=request.user.contributor, + date=timezone.now().date(), + voting_deadline=VGM_instance.end_date + datetime.timedelta(days=7), + ) + motion.save() + motion.update_votes(request.user.contributor.id, 'A') + ack_message = 'Your motion has been registered.' + context = {'ack_message': ack_message, + 'followup_message': 'Return to the ', + 'followup_link': reverse('scipost:VGM_detail', kwargs={'VGM_id': VGM_id}), + 'followup_link_label': 'VGM page'} + return render(request, 'scipost/acknowledgement.html', context) + else: + errormessage = 'The form was not filled properly.' + return render(request, 'scipost/error.html', {'errormessage': errormessage}) + else: + errormessage = 'This view can only be posted to.' + return render(request, 'scipost/error.html', {'errormessage': errormessage}) + + +@login_required +@permission_required('scipost.can_attend_VGMs', raise_exception=True) +def add_remark_on_motion(request, motion_id): + # contributor = request.user.contributor + motion = get_object_or_404(Motion, pk=motion_id) + if request.method == 'POST': + remark_form = RemarkForm(request.POST) + if remark_form.is_valid(): + remark = Remark(contributor=request.user.contributor, + motion=motion, + date=timezone.now(), + remark=remark_form.cleaned_data['remark']) + remark.save() + return HttpResponseRedirect('/VGM/' + str(motion.VGM.id) + + '/#motion_id' + str(motion.id)) + else: + errormessage = 'The form was invalidly filled.' + return render(request, 'scipost/error.html', {'errormessage': errormessage}) + else: + errormessage = 'This view can only be posted to.' + return render(request, 'scipost/error.html', {'errormessage': errormessage}) + + +@login_required +@permission_required('scipost.can_attend_VGMs', raise_exception=True) +def vote_on_motion(request, motion_id, vote): + contributor = request.user.contributor + motion = get_object_or_404(Motion, pk=motion_id) + if timezone.now() > motion.voting_deadline: + errormessage = 'The voting deadline on this motion has passed.' + return render(request, 'scipost/error.html', {'errormessage': errormessage}) + motion.update_votes(contributor.id, vote) + return HttpResponseRedirect('/VGM/' + str(motion.VGM.id) + + '/#motion_id' + str(motion.id)) + + ######### # Lists # ######### @@ -1493,7 +1750,7 @@ def create_list(request): assign_perm('scipost.change_list', request.user, newlist) assign_perm('scipost.view_list', request.user, newlist) assign_perm('scipost.delete_list', request.user, newlist) - message = 'List ' + create_list_form.cleaned_data['title'] + ' was successfully created.' + message = 'List %s was successfully created.' % create_list_form.cleaned_data['title'] else: create_list_form = CreateListForm() context = {'create_list_form': create_list_form, 'listcreated': listcreated, @@ -1575,6 +1832,7 @@ def create_team(request): 'add_team_member_form': add_team_member_form} return render(request, 'scipost/create_team.html', context) + @permission_required_or_403('scipost.change_team', (Team, 'id', 'team_id')) def add_team_member(request, team_id, contributor_id=None): team = get_object_or_404(Team, pk=team_id) @@ -1631,7 +1889,6 @@ def create_graph(request): def graph(request, graph_id): graph = get_object_or_404(Graph, pk=graph_id) nodes = Node.objects.filter(graph=graph) - arcs = Arc.objects.filter(graph=graph) if request.method == "POST": attach_teams_form = ManageTeamsForm(request.POST, contributor=request.user.contributor, @@ -1684,13 +1941,13 @@ def edit_graph_node(request, node_id): elif request.method == "POST": edit_node_form = CreateNodeForm(request.POST, instance=node) if edit_node_form.is_valid(): - node.name=edit_node_form.cleaned_data['name'] - node.description=edit_node_form.cleaned_data['description'] + node.name = edit_node_form.cleaned_data['name'] + node.description = edit_node_form.cleaned_data['description'] node.save() create_node_form = CreateNodeForm() create_arc_form = CreateArcForm(graph=node.graph) - context = {'create_node_form': create_node_form, - 'create_arc_form': create_arc_form} + context = {'create_node_form': create_node_form, + 'create_arc_form': create_arc_form} return redirect(reverse('scipost:graph', kwargs={'graph_id': node.graph.id}), context) else: edit_node_form = CreateNodeForm(instance=node) @@ -1701,7 +1958,6 @@ def edit_graph_node(request, node_id): def delete_graph_node(request, node_id): node = get_object_or_404(Node, pk=node_id) - errormessage = '' if not request.user.has_perm('scipost.change_graph', node.graph): raise PermissionDenied else: @@ -1721,11 +1977,10 @@ def api_graph(request, graph_id): arcs = Arc.objects.filter(graph=graph) nodesjson = [] arcsjson = [] + for node in nodes: nodesjson.append({'name': node.name, 'id': node.id}) -# for origin in node.arcs_in.all(): -# links.append({'source': origin.name, 'source_id': origin.id, -# 'target': node.name, 'target_id': node.id}) + for arc in arcs: arcsjson.append({'id': arc.id, 'source': arc.source.name, 'source_id': arc.source.id, @@ -1734,13 +1989,16 @@ def api_graph(request, graph_id): return JsonResponse({'nodes': nodesjson, 'arcs': arcsjson}, safe=False) - ############################# # Supporting Partners Board # ############################# def supporting_partners(request): - return render(request, 'scipost/supporting_partners.html') + prospective_agreements = SPBMembershipAgreement.objects.filter( + status='Submitted').order_by('date_requested') + context = {'prospective_partners': prospective_agreements, } + return render(request, 'scipost/supporting_partners.html', context) + @login_required def SPB_membership_request(request): @@ -1770,7 +2028,7 @@ def SPB_membership_request(request): ack_message = ('Thank you for your SPB Membership request. ' 'We will get back to you in the very near future ' 'with details of the proposed agreement.') - context = {'ack_message': ack_message,} + context = {'ack_message': ack_message, } return render(request, 'scipost/acknowledgement.html', context) else: errormessage = 'The form was not filled properly.' @@ -1780,5 +2038,5 @@ def SPB_membership_request(request): membership_form = SPBMembershipForm() context = {'errormessage': errormessage, 'SP_form': SP_form, - 'membership_form': membership_form,} + 'membership_form': membership_form, } return render(request, 'scipost/SPB_membership_request.html', context) diff --git a/static/scipost/SPB/SciPost_Supporting_Partner_Agreement.pdf b/static/scipost/SPB/SciPost_Supporting_Partner_Agreement.pdf new file mode 100644 index 0000000000000000000000000000000000000000..042b661a679115369f9ee152cf626744eb8f3eea Binary files /dev/null and b/static/scipost/SPB/SciPost_Supporting_Partner_Agreement.pdf differ diff --git a/strings/__init__.py b/strings/__init__.py index 82d957c9dde2ab9c1606aa3568f778e408e13a84..4aa362a9cb4689d7212cb18ff1284ed5cd7b6635 100644 --- a/strings/__init__.py +++ b/strings/__init__.py @@ -1 +1,5 @@ -acknowledge_request_thesis_link = "Thank you for your request for a Thesis Link. Your request will soon be handled by an editor" +acknowledge_request_thesis_link = ( + "Thank you for your request for a Thesis Link. Your request" + " will soon be handled by an editor." +) +acknowledge_vet_thesis_link = "Thesis Link request vetted." diff --git a/submissions/forms.py b/submissions/forms.py index e299e58cd601c661b44c9bcd3adb8148729ed554..bcac8cac71b9590b2e4c30d78aecfb018a45e53e 100644 --- a/submissions/forms.py +++ b/submissions/forms.py @@ -1,13 +1,14 @@ from django import forms from django.core.validators import RegexValidator -#from django.contrib.auth.models import User, Group -from .models import * +from .models import ASSIGNMENT_BOOL, ASSIGNMENT_REFUSAL_REASONS,\ + Submission, RefereeInvitation, Report, EICRecommendation -from scipost.models import SCIPOST_SUBJECT_AREAS +from scipost.constants import SCIPOST_SUBJECT_AREAS +from scipost.models import Contributor from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, Div, Field, Fieldset, HTML, Submit +from crispy_forms.layout import Layout, Div, Field, HTML, Submit class SubmissionSearchForm(forms.Form): @@ -37,7 +38,6 @@ class SubmissionIdentifierForm(forms.Form): ]) - class SubmissionForm(forms.ModelForm): class Meta: model = Submission @@ -73,8 +73,8 @@ class SubmissionForm(forms.ModelForm): 'placeholder': 'Optional: names of suggested referees', 'rows': 3}) self.fields['referees_flagged'].widget.attrs.update({ - 'placeholder': 'Optional: names of referees whose reports should be treated with caution (+ short reason)', - 'rows': 3}) + 'placeholder': 'Optional: names of referees whose reports should be treated with caution (+ short reason)', + 'rows': 3}) ###################### @@ -85,12 +85,10 @@ class AssignSubmissionForm(forms.Form): def __init__(self, *args, **kwargs): discipline = kwargs.pop('discipline') -# subject_area = kwargs.pop('subject_area') # Reactivate later on, once the Editorial College is large enough - super(AssignSubmissionForm,self).__init__(*args, **kwargs) + super(AssignSubmissionForm, self).__init__(*args, **kwargs) self.fields['editor_in_charge'] = forms.ModelChoiceField( queryset=Contributor.objects.filter(user__groups__name='Editorial College', user__contributor__discipline=discipline, -# user__contributor__expertises__contains=[subject_area,] # Reactivate later on, once the Editorial College is large enough ).order_by('user__last_name'), required=True, label='Select an Editor-in-charge') @@ -125,7 +123,7 @@ class RefereeRecruitmentForm(forms.ModelForm): Field('email_address'), Submit('submit', 'Send invitation', css_class="submitButton"), css_class="flex-whitebox320") - ) + ) class ConsiderRefereeInvitationForm(forms.Form): @@ -138,31 +136,33 @@ class SetRefereeingDeadlineForm(forms.Form): deadline = forms.DateField(required=False, label='', widget=forms.SelectDateWidget) + class VotingEligibilityForm(forms.Form): def __init__(self, *args, **kwargs): discipline = kwargs.pop('discipline') subject_area = kwargs.pop('subject_area') - super(VotingEligibilityForm,self).__init__(*args, **kwargs) + super(VotingEligibilityForm, self).__init__(*args, **kwargs) self.fields['eligible_Fellows'] = forms.ModelMultipleChoiceField( queryset=Contributor.objects.filter( user__groups__name__in=['Editorial College'], user__contributor__discipline=discipline, user__contributor__expertises__contains=[subject_area] ).order_by('user__last_name'), - widget = forms.CheckboxSelectMultiple({'checked': 'checked'}), + widget=forms.CheckboxSelectMultiple({'checked': 'checked'}), required=True, label='Eligible for voting', ) + ############ # Reports: ############ REPORT_ACTION_CHOICES = ( -# (0, 'modify'), + # (0, 'modify'), (1, 'accept'), (2, 'refuse'), - ) +) REPORT_REFUSAL_CHOICES = ( (0, '-'), @@ -170,8 +170,9 @@ REPORT_REFUSAL_CHOICES = ( (-2, 'not fully factually correct'), (-3, 'not useful for the authors'), (-4, 'not sufficiently academic in style'), - ) -report_refusal_choices_dict=dict(REPORT_REFUSAL_CHOICES) +) + +report_refusal_choices_dict = dict(REPORT_REFUSAL_CHOICES) class ReportForm(forms.ModelForm): @@ -180,6 +181,7 @@ class ReportForm(forms.ModelForm): fields = ['qualification', 'strengths', 'weaknesses', 'report', 'requested_changes', 'validity', 'significance', 'originality', 'clarity', 'formatting', 'grammar', 'recommendation', 'remarks_for_editors', 'anonymous'] + def __init__(self, *args, **kwargs): super(ReportForm, self).__init__(*args, **kwargs) self.fields['strengths'].widget.attrs.update( @@ -223,7 +225,6 @@ class EditorialCommunicationForm(forms.Form): {'rows': 5, 'cols': 50, 'placeholder': 'Write your message in this box.'}) - ###################### # EIC Recommendation # ###################### @@ -234,6 +235,7 @@ class EICRecommendationForm(forms.ModelForm): fields = ['recommendation', 'remarks_for_authors', 'requested_changes', 'remarks_for_editorial_college'] + def __init__(self, *args, **kwargs): super(EICRecommendationForm, self).__init__(*args, **kwargs) self.fields['remarks_for_authors'].widget.attrs.update( @@ -256,7 +258,7 @@ class RecommendationVoteForm(forms.Form): ('disagree', 'Disagree'), ('abstain', 'Abstain')], label='', - ) + ) remark = forms.CharField(widget=forms.Textarea(), label='', required=False) def __init__(self, *args, **kwargs): diff --git a/submissions/migrations/0029_auto_20170131_1425.py b/submissions/migrations/0029_auto_20170131_1425.py new file mode 100644 index 0000000000000000000000000000000000000000..5127b51d766b76880a954447ea02af0a45e513f0 --- /dev/null +++ b/submissions/migrations/0029_auto_20170131_1425.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-01-31 13:25 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0028_auto_20161212_1931'), + ] + + operations = [ + migrations.AlterField( + model_name='submission', + name='status', + field=models.CharField(choices=[('unassigned', 'Unassigned, undergoing pre-screening'), ('assignment_failed', 'Failed to assign Editor-in-charge; manuscript rejected'), ('EICassigned', 'Editor-in-charge assigned, manuscript under review'), ('review_closed', 'Review period closed, editorial recommendation pending'), ('revision_requested', 'Editor-in-charge has requested revision'), ('resubmitted', 'Has been resubmitted'), ('resubmitted_and_rejected', 'Has been resubmitted and subsequently rejected'), ('resubmitted_and_rejected_visible', 'Has been resubmitted and subsequently rejected (still publicly visible)'), ('voting_in_preparation', 'Voting in preparation (eligible Fellows being selected)'), ('put_to_EC_voting', 'Undergoing voting at the Editorial College'), ('EC_vote_completed', 'Editorial College voting rounded up'), ('accepted', 'Publication decision taken: accept'), ('rejected', 'Publication decision taken: reject'), ('rejected_visible', 'Publication decision taken: reject (still publicly visible)'), ('published', 'Published'), ('withdrawn', 'Withdrawn by the Authors')], default='unassigned', max_length=30), + ), + migrations.AlterField( + model_name='submission', + name='submission_date', + field=models.DateField(default=django.utils.timezone.now, verbose_name='submission date'), + ), + ] diff --git a/submissions/models.py b/submissions/models.py index fcb9237ac75aaa5288a37e888f93a2d3267af517..39b0e37d1183c05d49e37484f2d0444a53b8065a 100644 --- a/submissions/models.py +++ b/submissions/models.py @@ -1,18 +1,15 @@ +import datetime + from django.utils import timezone -from django.utils.safestring import mark_safe -from django.db import models -from django.contrib.auth.models import User -from django.contrib.postgres.fields import ArrayField, JSONField +from django.db import models, transaction +from django.contrib.postgres.fields import JSONField from django.template import Template, Context -from .models import * - -from scipost.models import ChoiceArrayField, Contributor, title_dict, Remark +from scipost.models import ChoiceArrayField, Contributor, title_dict from scipost.constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS, subject_areas_dict from scipost.models import TITLE_CHOICES from journals.models import SCIPOST_JOURNALS_SUBMIT, SCIPOST_JOURNALS_DOMAINS -from journals.models import SCIPOST_JOURNALS_SPECIALIZATIONS -from journals.models import journals_submit_dict, journals_domains_dict, journals_spec_dict +from journals.models import journals_submit_dict, journals_domains_dict from journals.models import Publication @@ -48,6 +45,8 @@ SUBMISSION_STATUS_OUT_OF_POOL = [ 'resubmitted', 'published', 'withdrawn', + 'rejected', + 'rejected_visible', ] # Submissions which should not appear in search lists @@ -68,7 +67,6 @@ SUBMISSION_STATUS_PUBLICLY_INVISIBLE = [ 'assignment_failed', 'resubmitted_rejected', 'rejected', - #'published', 'withdrawn', ] @@ -82,7 +80,7 @@ SUBMISSION_STATUS_VOTING_DEPRECATED = [ # SUBMISSION_ACTION_REQUIRED = ( # ('assign_EIC', 'Editor-in-charge to be assigned'), -# # ('Fellow_accepts_or_refuse_assignment', 'Fellow must accept or refuse assignment'), +# ('Fellow_accepts_or_refuse_assignment', 'Fellow must accept or refuse assignment'), # ('EIC_runs_refereeing_round', 'Editor-in-charge to run refereeing round (inviting referees)'), # ('EIC_closes_refereeing_round', 'Editor-in-charge to close refereeing round'), # ('EIC_invites_author_response', 'Editor-in-charge invites authors to complete their replies'), @@ -102,33 +100,33 @@ submission_type_dict = dict(SUBMISSION_TYPE) class Submission(models.Model): # Main submission fields - is_current = models.BooleanField(default=True) - is_resubmission = models.BooleanField(default=False) - submitted_by = models.ForeignKey(Contributor, on_delete=models.CASCADE) - editor_in_charge = models.ForeignKey(Contributor, related_name='EIC', blank=True, null=True, - on_delete=models.CASCADE) - submitted_to_journal = models.CharField(max_length=30, choices=SCIPOST_JOURNALS_SUBMIT, - verbose_name="Journal to be submitted to") - submission_type = models.CharField(max_length=10, choices=SUBMISSION_TYPE, - blank=True, null=True, default=None) + author_comments = models.TextField(blank=True, null=True) + author_list = models.CharField(max_length=1000, verbose_name="author list") discipline = models.CharField(max_length=20, choices=SCIPOST_DISCIPLINES, default='physics') domain = models.CharField(max_length=3, choices=SCIPOST_JOURNALS_DOMAINS) - subject_area = models.CharField(max_length=10, choices=SCIPOST_SUBJECT_AREAS, - verbose_name='Primary subject area', default='Phys:QP') - secondary_areas = ChoiceArrayField( - models.CharField(max_length=10, choices=SCIPOST_SUBJECT_AREAS), - blank=True, null=True) - status = models.CharField(max_length=30, choices=SUBMISSION_STATUS) # set by Editors - author_comments = models.TextField(blank=True, null=True) + editor_in_charge = models.ForeignKey(Contributor, related_name='EIC', blank=True, null=True, + on_delete=models.CASCADE) + is_current = models.BooleanField(default=True) + is_resubmission = models.BooleanField(default=False) list_of_changes = models.TextField(blank=True, null=True) - remarks_for_editors = models.TextField(blank=True, null=True) - referees_suggested = models.TextField(blank=True, null=True) - referees_flagged = models.TextField(blank=True, null=True) + open_for_commenting = models.BooleanField(default=False) open_for_reporting = models.BooleanField(default=False) + referees_flagged = models.TextField(blank=True, null=True) + referees_suggested = models.TextField(blank=True, null=True) + remarks_for_editors = models.TextField(blank=True, null=True) reporting_deadline = models.DateTimeField(default=timezone.now) - open_for_commenting = models.BooleanField(default=False) + secondary_areas = ChoiceArrayField( + models.CharField(max_length=10, choices=SCIPOST_SUBJECT_AREAS), + blank=True, null=True) + status = models.CharField(max_length=30, choices=SUBMISSION_STATUS, default='unassigned') # set by Editors + 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) + submitted_by = models.ForeignKey(Contributor, on_delete=models.CASCADE) + submitted_to_journal = models.CharField(max_length=30, choices=SCIPOST_JOURNALS_SUBMIT, + verbose_name="Journal to be submitted to") title = models.CharField(max_length=300) - author_list = models.CharField(max_length=1000, verbose_name="author list") # Authors which have been mapped to contributors: authors = models.ManyToManyField(Contributor, blank=True, related_name='authors_sub') @@ -146,7 +144,7 @@ class Submission(models.Model): # Metadata metadata = JSONField(default={}, blank=True, null=True) - submission_date = models.DateField(verbose_name='submission date') + submission_date = models.DateField(verbose_name='submission date', default=timezone.now) latest_activity = models.DateTimeField(default=timezone.now) class Meta: @@ -173,8 +171,64 @@ class Submission(models.Model): return True return False - - def header_as_table (self): + @transaction.atomic + def finish_submission(self): + if self.is_resubmission: + self.mark_other_versions_as_deprecated() + self.copy_authors_from_previous_version() + self.copy_EIC_from_previous_version() + self.set_resubmission_defaults() + else: + self.authors.add(self.submitted_by) + + self.save() + + def make_assignment(self): + assignment = EditorialAssignment( + submission=self, + to=self.editor_in_charge, + accepted=True, + date_created=timezone.now(), + date_answered=timezone.now(), + ) + assignment.save() + + def set_resubmission_defaults(self): + self.open_for_reporting = True + self.open_for_commenting = True + if self.other_versions()[0].submitted_to_journal == 'SciPost Physics Lecture Notes': + self.reporting_deadline = timezone.now() + datetime.timedelta(days=56) + else: + self.reporting_deadline = timezone.now() + datetime.timedelta(days=28) + + def copy_EIC_from_previous_version(self): + last_version = self.other_versions()[0] + self.editor_in_charge = last_version.editor_in_charge + self.status = 'EICassigned' + + def copy_authors_from_previous_version(self): + last_version = self.other_versions()[0] + + for author in last_version.authors.all(): + self.authors.add(author) + for author in last_version.authors_claims.all(): + self.authors_claims.add(author) + for author in last_version.authors_false_claims.all(): + self.authors_false_claims.add(author) + + def mark_other_versions_as_deprecated(self): + for sub in self.other_versions(): + sub.is_current = False + sub.open_for_reporting = False + sub.status = 'resubmitted' + sub.save() + + def other_versions(self): + return Submission.objects.filter( + arxiv_identifier_wo_vn_nr=self.arxiv_identifier_wo_vn_nr + ).exclude(pk=self.id).order_by('-arxiv_vn_nr') + + def header_as_table(self): # for Submission page header = '<table>' header += '<tr><td>Title: </td><td> </td><td>{{ title }}</td></tr>' @@ -195,26 +249,22 @@ class Submission(models.Model): '<tr><td>Submitted by: </td><td> </td><td>{{ submitted_by }}</td></tr>' '<tr><td>Submitted to: </td><td> </td><td>{{ to_journal }}</td></tr>' '<tr><td>Domain(s): </td><td> </td><td>{{ domain }}</td></tr>' -# '<tr><td>Specialization: </td><td> </td><td>{{ spec }}</td></tr>' '<tr><td>Subject area: </td><td> </td><td>{{ subject_area }}</td></tr>' '</table>') template = Template(header) - context = Context({'title': self.title, 'author_list': self.author_list, - 'arxiv_link': self.arxiv_link, 'submission_date': self.submission_date, - 'submitted_by': self.submitted_by, - 'to_journal': journals_submit_dict[self.submitted_to_journal], - 'domain': journals_domains_dict[self.domain], -# 'spec': journals_spec_dict[self.specialization], - 'subject_area': subject_areas_dict[self.subject_area], - }) + context = Context({ + 'title': self.title, 'author_list': self.author_list, + 'arxiv_link': self.arxiv_link, 'submission_date': self.submission_date, + 'submitted_by': self.submitted_by, + 'to_journal': journals_submit_dict[self.submitted_to_journal], + 'domain': journals_domains_dict[self.domain], + 'subject_area': subject_areas_dict[self.subject_area], + }) return template.render(context) - - def header_as_li (self): + def header_as_li(self): # for search lists header = ('<li>' - #'<div class="flex-container">' - #'<div class="flex-whitebox0">' '<p>' '<a href="/submission/{{ arxiv_identifier_w_vn_nr }}" ' 'class="pubtitleli">{{ title }}</a></p>' @@ -226,7 +276,6 @@ class Submission(models.Model): header += ' (deprecated version {{ arxiv_vn_nr }})' header += ('</p><p> Submitted {{ submission_date }} to {{ to_journal }}' ' - latest activity: {{ latest_activity }}</p>' - #'</div></div>' '</li>') context = Context({'arxiv_identifier_w_vn_nr': self.arxiv_identifier_w_vn_nr, 'arxiv_vn_nr': self.arxiv_vn_nr, @@ -237,12 +286,9 @@ class Submission(models.Model): template = Template(header) return template.render(context) - - def header_as_li_for_authors (self): + def header_as_li_for_authors(self): # includes status specification header = ('<li>' - #'<div class="flex-container">' - #'<div class="flex-whitebox0">' '<p><a href="/submission/{{ arxiv_identifier_w_vn_nr }}" ' 'class="pubtitleli">{{ title }}</a></p>' '<p>by {{ author_list }}</p>' @@ -254,20 +300,20 @@ class Submission(models.Model): header += ('</p><p>Submitted {{ submission_date }} to {{ to_journal }}' ' - latest activity: {{ latest_activity }}</p>' '<p>Status: {{ status }}</p>' - #'</div></div>' '</li>') - context = Context({'arxiv_identifier_w_vn_nr': self.arxiv_identifier_w_vn_nr, - 'arxiv_vn_nr': self.arxiv_vn_nr, - 'title': self.title, 'author_list': self.author_list, - 'submission_date': self.submission_date, - 'to_journal': journals_submit_dict[self.submitted_to_journal], - 'latest_activity': self.latest_activity.strftime('%Y-%m-%d %H:%M'), - 'status': submission_status_dict[self.status]}) + context = Context({ + 'arxiv_identifier_w_vn_nr': self.arxiv_identifier_w_vn_nr, + 'arxiv_vn_nr': self.arxiv_vn_nr, + 'title': self.title, 'author_list': self.author_list, + 'submission_date': self.submission_date, + 'to_journal': journals_submit_dict[self.submitted_to_journal], + 'latest_activity': self.latest_activity.strftime('%Y-%m-%d %H:%M'), + 'status': submission_status_dict[self.status] + }) template = Template(header) return template.render(context) - - def refereeing_status_as_p (self): + def refereeing_status_as_p(self): nr_ref_invited = RefereeInvitation.objects.filter(submission=self).count() nr_ref_accepted = RefereeInvitation.objects.filter(submission=self, accepted=True).count() nr_ref_declined = RefereeInvitation.objects.filter(submission=self, accepted=False).count() @@ -291,12 +337,9 @@ class Submission(models.Model): context = Context({}) return template.render(context) - - def header_as_li_for_Fellows (self): + def header_as_li_for_Fellows(self): # for submissions pool header = ('<li>' - #'<div class="flex-container">' - #'<div class="flex-whitebox0">' '<p><a href="/submission/{{ arxiv_identifier_w_vn_nr }}" ' 'class="pubtitleli">{{ title }}</a></p>' '<p>by {{ author_list }}</p>' @@ -306,7 +349,7 @@ class Submission(models.Model): else: header += ' (deprecated version {{ arxiv_vn_nr }})' header += ('</p><p> Submitted {{ submission_date }} to {{ to_journal }}' - ' - latest activity: {{ latest_activity }}</p>') + ' - latest activity: {{ latest_activity }}</p>') if self.status == 'unassigned': header += ('<p style="color: red">Status: {{ status }}.' ' You can volunteer to become Editor-in-charge by ' @@ -315,8 +358,7 @@ class Submission(models.Model): else: header += '<p>Editor-in-charge: {{ EIC }}</p><p>Status: {{ status }}</p>' header += self.refereeing_status_as_p() - header += (#'</div></div>' - '</li>') + header += '</li>' context = Context({'arxiv_identifier_w_vn_nr': self.arxiv_identifier_w_vn_nr, 'arxiv_vn_nr': self.arxiv_vn_nr, 'title': self.title, 'author_list': self.author_list, @@ -328,12 +370,9 @@ class Submission(models.Model): template = Template(header) return template.render(context) - - def simple_header_as_li (self): + def simple_header_as_li(self): # for Lists header = ('<li>' - #'<div class="flex-container">' - #'<div class="flex-whitebox0">' '<p>' '<a href="/submission/{{ arxiv_identifier_w_vn_nr }}" ' 'class="pubtitleli">{{ title }}</a></p>' @@ -344,7 +383,6 @@ class Submission(models.Model): else: header += ' (deprecated version {{ arxiv_vn_nr }})' header += ('</p>' - #'</div></div>' '</li>') context = Context({'arxiv_identifier_w_vn_nr': self.arxiv_identifier_w_vn_nr, 'arxiv_vn_nr': self.arxiv_vn_nr, @@ -352,11 +390,9 @@ class Submission(models.Model): template = Template(header) return template.render(context) - - def version_info_as_li (self): + def version_info_as_li(self): # for listing all versions of a Submission header = ('<li>' - #'<div class="flex-whitebox0">' '<p>' '<a href="/submission/{{ arxiv_identifier_w_vn_nr }}" ' 'class="pubtitleli">version {{ arxiv_vn_nr }}</a>') @@ -365,17 +401,15 @@ class Submission(models.Model): else: header += ' (deprecated version {{ arxiv_vn_nr }})' header += ('</p>' - #'</div>' '</li>') context = Context({'arxiv_identifier_w_vn_nr': self.arxiv_identifier_w_vn_nr, - 'arxiv_vn_nr': self.arxiv_vn_nr,}) + 'arxiv_vn_nr': self.arxiv_vn_nr, }) template = Template(header) return template.render(context) - - def status_info_as_table (self): + def status_info_as_table(self): header = '<table><tr><td>Current status: </td><td> </td><td>{{ status }}' - context = Context({'status': submission_status_dict[self.status],}) + context = Context({'status': submission_status_dict[self.status], }) try: context['citation'] = self.publication.citation_for_web_linked() header += ' as {{ citation }}' @@ -386,12 +420,10 @@ class Submission(models.Model): return template.render(context) - ###################### # Editorial workflow # ###################### - ASSIGNMENT_BOOL = ((True, 'Accept'), (False, 'Decline')) ASSIGNMENT_NULLBOOL = ((None, 'Response pending'), (True, 'Accept'), (False, 'Decline')) @@ -406,6 +438,7 @@ ASSIGNMENT_REFUSAL_REASONS = ( ) assignment_refusal_reasons_dict = dict(ASSIGNMENT_REFUSAL_REASONS) + class EditorialAssignment(models.Model): submission = models.ForeignKey(Submission, on_delete=models.CASCADE) to = models.ForeignKey(Contributor, on_delete=models.CASCADE) @@ -432,7 +465,7 @@ class EditorialAssignment(models.Model): info += ' style="color: green"' elif self.deprecated: info += ' style="color: purple"' - elif self.accepted == False: + elif not self.accepted: if self.refusal_reason == 'NIE' or self.refusal_reason == 'DNP': info += ' style="color: #CC0000"' else: @@ -453,8 +486,6 @@ class EditorialAssignment(models.Model): def header_as_li_for_eic(self): header = ('<li>' - #'<div class="flex-container">' - #'<div class="flex-whitebox0">' '<p><a href="/submission/{{ arxiv_identifier_w_vn_nr }}" ' 'class="pubtitleli">{{ title }}</a></p>' '<p>by {{ author_list }}</p>' @@ -463,7 +494,6 @@ class EditorialAssignment(models.Model): '<a href="/submissions/editorial_page/{{ arxiv_identifier_w_vn_nr }}">' 'Editorial Page</a>.' '</p>' - #'</div></div>' '</li>') template = Template(header) context = Context({'arxiv_identifier_w_vn_nr': self.submission.arxiv_identifier_w_vn_nr, @@ -477,23 +507,20 @@ class EditorialAssignment(models.Model): def header_as_li(self): """ Same as above, but without link to Editorial Page. """ header = ('<li>' - #'<div class="flex-container">' - #'<div class="flex-whitebox0">' '<p><a href="/submission/{{ arxiv_identifier_w_vn_nr }}" ' 'class="pubtitleli">{{ title }}</a></p>' '<p>by {{ author_list }}</p>' '<p> (submitted {{ date }} to {{ to_journal }})</p>' '<p>Status: {{ status }}</p>' - #'</div></div>' - '</li>' - ) + '</li>') template = Template(header) - context = Context({'arxiv_identifier_w_vn_nr': self.submission.arxiv_identifier_w_vn_nr, - 'title': self.submission.title, - 'author_list': self.submission.author_list, - 'date': self.submission.submission_date, - 'to_journal': journals_submit_dict[self.submission.submitted_to_journal], - 'status': submission_status_dict[self.submission.status]}) + context = Context({ + 'arxiv_identifier_w_vn_nr': self.submission.arxiv_identifier_w_vn_nr, + 'title': self.submission.title, + 'author_list': self.submission.author_list, + 'date': self.submission.submission_date, + 'to_journal': journals_submit_dict[self.submission.submitted_to_journal], + 'status': submission_status_dict[self.submission.status]}) return template.render(context) @@ -516,32 +543,14 @@ class RefereeInvitation(models.Model): date_responded = models.DateTimeField(blank=True, null=True) refusal_reason = models.CharField(max_length=3, choices=ASSIGNMENT_REFUSAL_REASONS, blank=True, null=True) - fulfilled = models.BooleanField(default=False) # True if a Report has been submitted - cancelled = models.BooleanField(default=False) # True if EIC has deactivated invitation + fulfilled = models.BooleanField(default=False) # True if a Report has been submitted + cancelled = models.BooleanField(default=False) # True if EIC has deactivated invitation def __str__(self): return (self.first_name + ' ' + self.last_name + ' to referee ' + self.submission.title[:30] + ' by ' + self.submission.author_list[:30] + ', invited on ' + self.date_invited.strftime('%Y-%m-%d')) - # def summary_as_li(self): - # context = Context({'first_name': self.first_name, 'last_name': self.last_name, - # 'date_invited': self.date_invited.strftime('%Y-%m-%d %H:%M')}) - # output = '<li>{{ first_name }} {{ last_name }}, invited {{ date_invited }}, ' - # if self.accepted is not None: - # if self.accepted: - # output += '<strong style="color: green">task accepted</strong> ' - # else: - # output += '<strong style="color: red">task declined</strong> ' - # output += '{{ date_responded }}' - # context['date_responded'] = self.date_responded.strftime('%Y-%m-%d %H:%M') - # else: - # output += 'response pending' - # if self.fulfilled: - # output += '; Report has been delivered' - # template = Template(output) - # return template.render(context) - def summary_as_tds(self): context = Context({'first_name': self.first_name, 'last_name': self.last_name, 'date_invited': self.date_invited.strftime('%Y-%m-%d %H:%M')}) @@ -591,7 +600,7 @@ quality_spec_dict = dict(QUALITY_SPEC) RANKING_CHOICES = ( - (101, '-'), # Only values between 0 and 100 are kept, anything outside those limits is discarded. + (101, '-'), # Only values between 0 and 100 are kept, anything outside those limits is discarded. (100, 'top'), (80, 'high'), (60, 'good'), (40, 'ok'), (20, 'low'), (0, 'poor') ) ranking_choices_dict = dict(RANKING_CHOICES) @@ -606,6 +615,7 @@ REPORT_REC = ( ) report_rec_dict = dict(REPORT_REC) + class Report(models.Model): """ Both types of reports, invited or contributed. """ # status: see forms.py:REPORT_REFUSAL_CHOICES @@ -648,7 +658,6 @@ class Report(models.Model): verbose_name='optional remarks for the Editors only') anonymous = models.BooleanField(default=True, verbose_name='Publish anonymously') - def __str__(self): return (self.author.user.first_name + ' ' + self.author.user.last_name + ' on ' + self.submission.title[:50] + ' by ' + self.submission.author_list[:50]) @@ -668,7 +677,6 @@ class Report(models.Model): template = Template(output) return template.render(context) - def print_contents(self): context = Context({'strengths': self.strengths, 'weaknesses': self.weaknesses, 'report': self.report, 'requested_changes': self.requested_changes}) @@ -692,14 +700,14 @@ class Report(models.Model): template = Template(output) return template.render(context) - def print_contents_for_editors(self): - context = Context({'id': self.id, 'author_id': self.author.id, - 'author_first_name': self.author.user.first_name, - 'author_last_name': self.author.user.last_name, - 'date_submitted': self.date_submitted.strftime("%Y-%m-%d"), - 'remarks_for_editors': self.remarks_for_editors, - }) + context = Context({ + 'id': self.id, 'author_id': self.author.id, + 'author_first_name': self.author.user.first_name, + 'author_last_name': self.author.user.last_name, + 'date_submitted': self.date_submitted.strftime("%Y-%m-%d"), + 'remarks_for_editors': self.remarks_for_editors, + }) output = '<div class="reportid">\n' output += '<h3><a id="report_id{{ id }}"></a>' if self.anonymous: @@ -731,6 +739,7 @@ ED_COMM_CHOICES = ( ) ed_comm_choices_dict = dict(ED_COMM_CHOICES) + class EditorialCommunication(models.Model): """ Each individual communication between Editor-in-charge @@ -743,7 +752,7 @@ class EditorialCommunication(models.Model): timestamp = models.DateTimeField(default=timezone.now) text = models.TextField() - def __str__ (self): + def __str__(self): output = self.comtype if self.referee is not None: output += ' ' + self.referee.user.first_name + ' ' + self.referee.user.last_name @@ -781,7 +790,6 @@ class EditorialCommunication(models.Model): return template.render(context) - ############################ # Editorial Recommendation # ############################ @@ -797,11 +805,11 @@ class EICRecommendation(models.Model): verbose_name='optional remarks for the Editorial College') recommendation = models.SmallIntegerField(choices=REPORT_REC) # Editorial Fellows who have assessed this recommendation: - eligible_to_vote = models.ManyToManyField (Contributor, blank=True, - related_name='eligible_to_vote') - voted_for = models.ManyToManyField (Contributor, blank=True, related_name='voted_for') - voted_against = models.ManyToManyField (Contributor, blank=True, related_name='voted_against') - voted_abstain = models.ManyToManyField (Contributor, blank=True, related_name='voted_abstain') + eligible_to_vote = models.ManyToManyField(Contributor, blank=True, + related_name='eligible_to_vote') + voted_for = models.ManyToManyField(Contributor, blank=True, related_name='voted_for') + voted_against = models.ManyToManyField(Contributor, blank=True, related_name='voted_against') + voted_abstain = models.ManyToManyField(Contributor, blank=True, related_name='voted_abstain') voting_deadline = models.DateTimeField('date submitted', default=timezone.now) def __str__(self): @@ -823,22 +831,23 @@ class EICRecommendation(models.Model): def print_for_authors(self): output = ('<h3>Date: {{ date_submitted }}</h3>' '<h3>Remarks for authors</h3>' - '<p>{{ remarks_for_authors }}</p>' + '<p>{{ remarks_for_authors }}</p>' '<h3>Requested changes</h3>' '<p>{{ requested_changes }}</p>' '<h3>Recommendation</h3>' '<p>{{ recommendation }}</p>') - context = Context({'date_submitted': self.date_submitted.strftime('%Y-%m-%d %H:%M'), - 'remarks_for_authors': self.remarks_for_authors, - 'requested_changes': self.requested_changes, - 'recommendation': report_rec_dict[self.recommendation],}) + context = Context({ + 'date_submitted': self.date_submitted.strftime('%Y-%m-%d %H:%M'), + 'remarks_for_authors': self.remarks_for_authors, + 'requested_changes': self.requested_changes, + 'recommendation': report_rec_dict[self.recommendation], }) template = Template(output) return template.render(context) def print_for_Fellows(self): output = ('<h3>By {{ Fellow }}, formulated on {{ date_submitted }}</h3>' '<h3>Remarks for authors</h3>' - '<p>{{ remarks_for_authors }}</p>' + '<p>{{ remarks_for_authors }}</p>' '<h3>Requested changes</h3>' '<p>{{ requested_changes }}</p>' '<h3>Remarks for Editorial College</h3>' @@ -853,6 +862,6 @@ class EICRecommendation(models.Model): 'remarks_for_authors': self.remarks_for_authors, 'requested_changes': self.requested_changes, 'remarks_for_editorial_college': self.remarks_for_editorial_college, - 'recommendation': report_rec_dict[self.recommendation],}) + 'recommendation': report_rec_dict[self.recommendation], }) template = Template(output) return template.render(context) diff --git a/submissions/services.py b/submissions/services.py index bd16fb2ae70960507034ca3e46f8b6780a85fe87..671a4326173d084fef01f656d625deb7ecfd4218 100644 --- a/submissions/services.py +++ b/submissions/services.py @@ -1,67 +1,130 @@ # Module for making external api calls as needed in the submissions cycle import feedparser +import requests +import pprint +import re +from io import BytesIO -from .models import * +from .models import Submission class ArxivCaller(): - def lookup_article(identifier): - # Pre-checks - if same_version_exists(identifier) - return False, "This preprint version has already been submitted to SciPost." + """ Performs an Arxiv article lookup for given identifier """ + + # State of the caller + isvalid = None + errorcode = '' + resubmission = False + previous_submissions = [] + arxiv_journal_ref = '' + arxiv_doi = '' + metadata = {} + query_base_url = 'http://export.arxiv.org/api/query?id_list=%s' + identifier_without_vn_nr = '' + identifier_with_vn_nr = '' + version_nr = None + + def __init__(self): + pass + + def is_valid(self): + if self.isvalid is None: + print("Run process() first") + return False + return self.isvalid + + def process(self, identifier): + # ============================= # + # Pre-checks # + # ============================= # + if self.same_version_exists(identifier): + self.errorcode = 'preprint_already_submitted' + self.isvalid = False + return # Split the given identifier in an article identifier and version number - identifier_without_vn_nr = identifier.rpartition('v')[0] - arxiv_vn_nr = int(identifier.rpartition('v')[2]) - - resubmission = False - if previous_submission_undergoing_refereeing(identifier): - errormessage = '<p>There exists a preprint with this arXiv identifier ' - 'but an earlier version number, which is still undergoing ' - 'peer refereeing.</p>' - '<p>A resubmission can only be performed after request ' - 'from the Editor-in-charge. Please wait until the ' - 'closing of the previous refereeing round and ' - 'formulation of the Editorial Recommendation ' - 'before proceeding with a resubmission.</p>' - return False, errormessage - - # Arxiv query - queryurl = ('http://export.arxiv.org/api/query?id_list=%s' - % identifier) - arxiv_response = feedparser.parse(queryurl) + if re.match("^[0-9]{4,}.[0-9]{4,5}v[0-9]{1,2}$", identifier) is None: + self.errorcode = 'bad_identifier' + self.isvalid = False + return + + self.identifier_without_vn_nr = identifier.rpartition('v')[0] + self.identifier_with_vn_nr = identifier + self.version_nr = int(identifier.rpartition('v')[2]) + + previous_submissions = self.different_versions(self.identifier_without_vn_nr) + if previous_submissions: + if previous_submissions[0].status == 'revision_requested': + self.resubmission = True + self.previous_submissions = previous_submissions + elif previous_submissions[0].status in ['rejected', 'rejected_visible']: + self.errorcode = 'previous_submissions_rejected' + self.isvalid = False + return + else: + self.errorcode = 'previous_submission_undergoing_refereeing' + self.isvalid = False + return + + # ============================= # + # Arxiv query # + # ============================= # + queryurl = (self.query_base_url % identifier) + + try: + req = requests.get(queryurl, timeout=4.0) + except requests.ReadTimeout: + self.errorcode = 'arxiv_timeout' + self.isvalid = False + return + except requests.ConnectionError: + self.errorcode = 'arxiv_timeout' + self.isvalid = False + return + + content = req.content + arxiv_response = feedparser.parse(content) # Check if response has at least one entry - if not 'entries' in arxiv_response - errormessage = 'Bad response from Arxiv.' - return False, errormessage + if req.status_code == 400 or 'entries' not in arxiv_response: + self.errorcode = 'arxiv_bad_request' + self.isvalid = False + return + + # arxiv_response['entries'][0]['title'] == 'Error' # Check if preprint exists - if not preprint_exists(arxiv_response) - errormessage = 'A preprint associated to this identifier does not exist.' - return False, errormessage + if not self.preprint_exists(arxiv_response): + self.errorcode = 'preprint_does_not_exist' + self.isvalid = False + return # Check via journal ref if already published - arxiv_journal_ref = published_journal_ref - if arxiv_journal_ref - errormessage = 'This paper has been published as ' + arxiv_journal_ref + - '. You cannot submit it to SciPost anymore.' - return False, resubmission + self.arxiv_journal_ref = self.published_journal_ref(arxiv_response) + if self.arxiv_journal_ref: + self.errorcode = 'paper_published_journal_ref' + self.isvalid = False + return # Check via DOI if already published - arxiv_doi = published_journal_ref - if arxiv_doi - errormessage = 'This paper has been published under DOI ' + arxiv_doi - + '. You cannot submit it to SciPost anymore.' - return False, errormessage - - return arxiv_response, "" + self.arxiv_doi = self.published_doi(arxiv_response) + if self.arxiv_doi: + self.errorcode = 'paper_published_doi' + self.isvalid = False + return + self.metadata = arxiv_response + self.isvalid = True + return - def same_version_exists(identifier): + def same_version_exists(self, identifier): return Submission.objects.filter(arxiv_identifier_w_vn_nr=identifier).exists() - def previous_submission_undergoing_refereeing(identifier): + def different_versions(self, identifier): + return Submission.objects.filter( + arxiv_identifier_wo_vn_nr=identifier).order_by('-arxiv_vn_nr') + + def check_previous_submissions(self, identifier): previous_submissions = Submission.objects.filter( arxiv_identifier_wo_vn_nr=identifier).order_by('-arxiv_vn_nr') @@ -70,17 +133,17 @@ class ArxivCaller(): else: return False - def preprint_exists(arxiv_response): + def preprint_exists(self, arxiv_response): return 'title' in arxiv_response['entries'][0] - def published_journal_ref(arxiv_response): - if 'arxiv_journal_ref' in arxiv_response['entries'][0] + def published_journal_ref(self, arxiv_response): + if 'arxiv_journal_ref' in arxiv_response['entries'][0]: return arxiv_response['entries'][0]['arxiv_journal_ref'] else: return False - def published_DOI(arxiv_response): - if 'arxiv_doi' in arxiv_response['entries'][0] + def published_doi(self, arxiv_response): + if 'arxiv_doi' in arxiv_response['entries'][0]: return arxiv_response['entries'][0]['arxiv_doi'] else: return False diff --git a/submissions/templates/submissions/assignments.html b/submissions/templates/submissions/assignments.html new file mode 100644 index 0000000000000000000000000000000000000000..9fa1e95e0cdad78833979b564e726cbe4311fa2b --- /dev/null +++ b/submissions/templates/submissions/assignments.html @@ -0,0 +1,107 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: Assignments{% endblock pagetitle %} + +{% block bodysup %} + +<script> +$(document).ready(function(){ + + $('#ref_reason').hide(); + + $('#id_accept').on('change', function() { + if ($('#id_accept_1').is(':checked')) { + $('#ref_reason').show(); + } + else { + $('#ref_reason').hide(); + } + }); + + $(".submitRemarkForm").hide(); + + $(".submitRemarkButton").click( function() { + $(this).next("div").toggle(); + }); + }); + +</script> + +{% load guardian_tags %} +{% load scipost_extras %} +{% load submissions_extras %} + + +{% if assignments_to_consider %} +<section> + + {% for assignment_to_consider in assignments_to_consider %} + + <div class="flex-greybox"> + <h1>Assignment request: can you act as Editor-in-charge? (see below to accept/decline):</h1> + </div> + <br> + <hr> + + {{ assignment_to_consider.submission.header_as_table }} + <br /> + <h4>Abstract:</h4> + <p>{{ assignment_to_consider.submission.abstract }}</p> + <br/> + + <hr> + <div class="flex-greybox"> + <h1>Accept or Decline this Assignment</h1> + </div> + <h3>By accepting, you will be required to start a refereeing round on the next screen.</h3> + <form action="{% url 'submissions:accept_or_decline_assignment_ack' assignment_id=assignment_to_consider.id %}" method="post"> + {% csrf_token %} + {{ consider_assignment_form.accept }} + <div id="ref_reason"> + <p>Please select a reason for declining this assignment:</p> + {{ consider_assignment_form.refusal_reason }} + </div> + <input type="submit" value="Submit" /> + </form> + + <hr class="hr6"/> + {% endfor %} + +</section> +<hr class="hr12"/> +{% endif %} + + +{% if current_assignments %} +<section> + <div class="flex-container"> + <div class="flex-greybox"> + <h1>Your current assignments:</h1> + </div> + </div> + <ul> + {% for assignment in current_assignments %} + {{ assignment.submission.header_as_li_for_Fellows }} + <div class="flex-container"> + <div class-"flex-whitebox" style="background-color: #ffaaaa;"> + <h3>Required actions:</h3> + <ul> + {% for todoitem in assignment.submission|required_actions %} + <li>{{ todoitem }}</li> + {% endfor %} + </ul> + </div> + </div> + <h4><a href="{% url 'submissions:editorial_page' arxiv_identifier_w_vn_nr=assignment.submission.arxiv_identifier_w_vn_nr %}"> + Go to this Submission's Editorial Page</a></h4> + {% endfor %} + </ul> +</section> +{% else %} +<section> + <p>You currently have no assignments to take care of.</p> +</section> +{% endif %} + + +{% endblock bodysup %} diff --git a/submissions/templates/submissions/editorial_page.html b/submissions/templates/submissions/editorial_page.html index e903d01779e7ac2a4fc009ec46718cef76451fde..989af1cedd256f936cf5fc3059bba6ad5655488e 100644 --- a/submissions/templates/submissions/editorial_page.html +++ b/submissions/templates/submissions/editorial_page.html @@ -5,6 +5,7 @@ {% block headsup %} {% load scipost_extras %} +{% load submissions_extras %} {% endblock headsup %} @@ -81,7 +82,20 @@ </div> <h3>Status:</h3> {{ submission.status_info_as_table }} + {% if submission|required_actions %} + <div class="flex-container"> + <div class-"flex-whitebox" style="background-color: #ffaaaa;"> + <h3>Required actions:</h3> + <ul> + {% for todoitem in submission|required_actions %} + <li>{{ todoitem }}</li> + {% endfor %} + </ul> + </div> + </div> + {% endif %} <hr/> + <h3>Refereeing status summary:</h3> {{ submission.refereeing_status_as_p }} diff --git a/submissions/templates/submissions/new_submission.html b/submissions/templates/submissions/new_submission.html new file mode 100644 index 0000000000000000000000000000000000000000..71fb9dcc12c2ea218e0749b1d1b2c3e578da911b --- /dev/null +++ b/submissions/templates/submissions/new_submission.html @@ -0,0 +1,85 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: submit manuscript{% endblock pagetitle %} + +{% block bodysup %} + + +<script> +$(document).ready(function(){ + $("#id_submission_type").closest('tr').hide() + + $('select#id_submitted_to_journal').on('change', function (){ + var selection = $(this).val(); + switch(selection){ + case "SciPost Physics": + $("#id_submission_type").closest('tr').show() + break; + default: + $("#id_submission_type").closest('tr').hide() + } +}); + + var isresub = $("#id_is_resubmission").val(); + switch(isresub){ + case "True": + $("#id_author_comments").closest('tr').show() + $("#id_list_of_changes").closest('tr').show() + break; + default: + $("#id_author_comments").closest('tr').hide() + $("#id_list_of_changes").closest('tr').hide() + } + +}); +</script> + +<section> + <div class="flex-greybox"> + <h1>Submit a manuscript to SciPost</h1> + </div> + + <p>Before submitting, make sure you agree with the + <a href="{% url 'journals:journals_terms_and_conditions' %}"> + SciPost Journals Terms and Conditions</a>.</p> + <p>You should also make sure you understand the + <a href="{% url 'submissions:sub_and_ref_procedure' %}#pwr"> + refereeing procedure</a> and its open aspect.</p> + <p>In particular, make sure you are familiar with the + <a href="{% url 'journals:journals_terms_and_conditions' %}#license_and_copyright_agreement"> + license and copyright agreement</a> + and the <a href="{% url 'journals:journals_terms_and_conditions' %}#author_obligations"> + author obligations</a>.</p> + <p>Please prepare your manuscript according to the + <a href="{% url 'submissions:author_guidelines' %}">author guidelines</a>.</p> + + + {% if perms.scipost.can_submit_manuscript %} + + {% if form.arxiv_link.value %} + <form id="full_submission_form" action="{% url 'submissions:submit_manuscript' %}" method="post"> + {% csrf_token %} + <table> + <ul> + {{ form.as_table }} + </ul> + </table> + <p>By clicking on Submit, you state that you have read and agree with + the <a href="{% url 'journals:journals_terms_and_conditions' %}"> + SciPost Journals Terms and Conditions</a>, the + <a href="{% url 'journals:journals_terms_and_conditions' %}#license_and_copyright_agreement"> + license and copyright agreement</a> + and the <a href="{% url 'journals:journals_terms_and_conditions' %}#author_obligations"> + author obligations</a>.</p> + <input type="submit" value="Submit"/> + </form> + {% endif %} + + {% else %} + <h3>You are currently not allowed to submit a manuscript.</h3> + {% endif %} + +</section> + + +{% endblock bodysup %} diff --git a/submissions/templates/submissions/pool.html b/submissions/templates/submissions/pool.html index d388a60efb4da5ba4ab747547bc9f106b30f0e62..6f7db2515e4d59490572adf53ae5429908a88898 100644 --- a/submissions/templates/submissions/pool.html +++ b/submissions/templates/submissions/pool.html @@ -34,6 +34,14 @@ $(document).ready(function(){ {% if request.user|is_in_group:'Editorial Administrators' and recommendations_undergoing_voting %} <section> + <div class="flex-container"> + <div class="flex-whitebox"> + <h3>Administrative actions on recommendations undergoing voting:</h3> + <ul> + <li>To send an email reminder to each Fellow with at least one voting duty, <a href="{% url 'submissions:remind_Fellows_to_vote' %}">click here</a></li> + </ul> + </div> + </div> <div class="flex-container"> <div class="flex-greybox"> <h1>Recommendations undergoing voting</h1> @@ -220,6 +228,15 @@ $(document).ready(function(){ </div> </div> + <h3>Submissions by status:</h3> + <ul> + {% for key, val in submission_status %} + <li><a href="{% url 'submissions:submissions_by_status' status=key %}">{{ val }}</a></li> + {% endfor %} + </ul> + <hr class="hr6"/> + + <ul> {% for sub in submissions_in_pool %} {% if request.user|is_not_author_of_submission:sub.arxiv_identifier_w_vn_nr %} @@ -243,6 +260,19 @@ $(document).ready(function(){ </div> {% get_obj_perms request.user for sub as "sub_perms" %} {% if "can_take_editorial_actions" in sub_perms or request.user|is_in_group:'Editorial Administrators' %} + <br/> + {% if sub|required_actions %} + <div class="flex-container"> + <div class-"flex-whitebox" style="background-color: #ffaaaa;"> + <h3>Required actions:</h3> + <ul> + {% for todoitem in sub|required_actions %} + <li>{{ todoitem }}</li> + {% endfor %} + </ul> + </div> + </div> + {% endif %} <h4><a href="{% url 'submissions:editorial_page' arxiv_identifier_w_vn_nr=sub.arxiv_identifier_w_vn_nr %}"> Go to this Submission's Editorial Page</a></h4> {% endif %} diff --git a/submissions/templates/submissions/prefill_using_identifier.html b/submissions/templates/submissions/prefill_using_identifier.html new file mode 100644 index 0000000000000000000000000000000000000000..41f3dd90c645969b1b5752d8c4166a787f00c905 --- /dev/null +++ b/submissions/templates/submissions/prefill_using_identifier.html @@ -0,0 +1,86 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: submit manuscript{% endblock pagetitle %} + +{% block bodysup %} + + +<script> +$(document).ready(function(){ + $("#id_submission_type").closest('tr').hide() + + $('select#id_submitted_to_journal').on('change', function (){ + var selection = $(this).val(); + switch(selection){ + case "SciPost Physics": + $("#id_submission_type").closest('tr').show() + break; + default: + $("#id_submission_type").closest('tr').hide() + } +}); + + var isresub = $("#id_is_resubmission").val(); + switch(isresub){ + case "True": + $("#id_author_comments").closest('tr').show() + $("#id_list_of_changes").closest('tr').show() + break; + default: + $("#id_author_comments").closest('tr').hide() + $("#id_list_of_changes").closest('tr').hide() + } + +}); +</script> + +<section> + <div class="flex-greybox"> + <h1>Submit a manuscript to SciPost</h1> + </div> + + <p>Before submitting, make sure you agree with the + <a href="{% url 'journals:journals_terms_and_conditions' %}"> + SciPost Journals Terms and Conditions</a>.</p> + <p>You should also make sure you understand the + <a href="{% url 'submissions:sub_and_ref_procedure' %}#pwr"> + refereeing procedure</a> and its open aspect.</p> + <p>In particular, make sure you are familiar with the + <a href="{% url 'journals:journals_terms_and_conditions' %}#license_and_copyright_agreement"> + license and copyright agreement</a> + and the <a href="{% url 'journals:journals_terms_and_conditions' %}#author_obligations"> + author obligations</a>.</p> + <p>Please prepare your manuscript according to the + <a href="{% url 'submissions:author_guidelines' %}">author guidelines</a>.</p> + + + {% if perms.scipost.can_submit_manuscript %} + + <div class="flex-greybox"> + <h3><em>Please provide the arXiv identifier for your Submission:</em></h3> + <p> + <em>(give the identifier without prefix but with version number, as per the placeholder)</em> + </p> + <form action="{% url 'submissions:prefill_using_identifier' %}" method="post"> + {% csrf_token %} + {{ form.as_p }} + <input type="submit" value="Query arXiv"/> + </form> + </div> + <br/> + <!-- {% if errormessage %} + <h3 style="color: red;">Error: {{ errormessage }}</h3> + {% endif %} --> + + {% if resubmessage %} + <h3 style="color: green;">{{ resubmessage }}</h3> + {% endif %} + + {% else %} + <h3>You are currently not allowed to submit a manuscript.</h3> + {% endif %} + +</section> + + +{% endblock bodysup %} diff --git a/submissions/templates/submissions/submissions_by_status.html b/submissions/templates/submissions/submissions_by_status.html new file mode 100644 index 0000000000000000000000000000000000000000..82a4babfa350f766ac8af7373d850a4815e0b7b7 --- /dev/null +++ b/submissions/templates/submissions/submissions_by_status.html @@ -0,0 +1,98 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: Submissions by status{% endblock pagetitle %} + +{% block bodysup %} + +<script> +$(document).ready(function(){ + + $('#ref_reason').hide(); + + $('#id_accept').on('change', function() { + if ($('#id_accept_1').is(':checked')) { + $('#ref_reason').show(); + } + else { + $('#ref_reason').hide(); + } + }); + + $(".submitRemarkForm").hide(); + + $(".submitRemarkButton").click( function() { + $(this).next("div").toggle(); + }); + }); + +</script> + +{% load guardian_tags %} +{% load scipost_extras %} +{% load submissions_extras %} + + +<section> + <div class="flex-container"> + <div class="flex-greybox"> + <h1>SciPost Submissions with status {{ status }}</h1> + </div> + </div> + + <ul> + {% for sub in submissions_of_status %} + {% if request.user|is_not_author_of_submission:sub.arxiv_identifier_w_vn_nr %} + <br/> + {{ sub.header_as_li_for_Fellows }} + {% if sub.remark_set.all %} + <h4>Remarks on this submission:</h4> + <ul> + {% for rem in sub.remark_set.all %} + {{ rem.as_li }} + {% endfor %} + </ul> + {% endif %} + <button class="submitRemarkButton" id="remarkButton{{ submission.id }}">Add a remark on this Submission</button> + <div class="submitRemarkForm" id="remarkForm{{ submission.id }}"> + <form action="{% url 'submissions:add_remark' arxiv_identifier_w_vn_nr=sub.arxiv_identifier_w_vn_nr %}" method="post"> + {% csrf_token %} + {{ remark_form.as_p }} + <input type="submit" value="Submit" /> + </form> + </div> + {% get_obj_perms request.user for sub as "sub_perms" %} + {% if "can_take_editorial_actions" in sub_perms or request.user|is_in_group:'Editorial Administrators' %} + <h4><a href="{% url 'submissions:editorial_page' arxiv_identifier_w_vn_nr=sub.arxiv_identifier_w_vn_nr %}"> + Go to this Submission's Editorial Page</a></h4> + {% endif %} + {% if perms.scipost.can_assign_submissions %} + {% if sub.editorialassignment_set.all %} + <h4>EIC Assignment requests:</h4> + <ul> + {% for assignment in sub.editorialassignment_set.all %} + {{ assignment.info_as_li }} + {% endfor %} + </ul> + {% endif %} + {% if sub.editor_in_charge == None %} + <h4>Actions:</h4> + <ul> + <li><a href="{% url 'submissions:assign_submission' arxiv_identifier_w_vn_nr=sub.arxiv_identifier_w_vn_nr %}">Send a new assignment request</a></li> + <li><a href="{% url 'submissions:assignment_failed' arxiv_identifier_w_vn_nr=sub.arxiv_identifier_w_vn_nr %}">Close pre-screening: failure to find EIC</a></li> + </ul> + {% endif %} + {% endif %} + {% if request.user|is_in_group:'Editorial Administrators' %} + <h4><a href="{% url 'submissions:communication' arxiv_identifier_w_vn_nr=sub.arxiv_identifier_w_vn_nr comtype='StoE' %}">Send a communication to the Editor-in-charge</a></h4> + {% if sub.status == 'accepted' %} + <h4>After proofs have been accepted, you can + <a href="{% url 'journals:initiate_publication' %}">initiate the publication process</a> (leads to the validation page)</h4> + {% endif %} + {% endif %} + {% endif %} + {% endfor %} + </ul> + +</section> + +{% endblock bodysup %} diff --git a/submissions/templates/submissions/submit_manuscript.html b/submissions/templates/submissions/submit_manuscript.html index 8bb92e95a355883202f2b3350d17ff5991680603..55a6f49aa898a9022d570434037288017dc4ad6d 100644 --- a/submissions/templates/submissions/submit_manuscript.html +++ b/submissions/templates/submissions/submit_manuscript.html @@ -68,9 +68,9 @@ $(document).ready(function(){ </form> </div> <br/> - {% if errormessage %} + <!-- {% if errormessage %} <h3 style="color: red;">Error: {{ errormessage }}</h3> - {% endif %} + {% endif %} --> {% if resubmessage %} <h3 style="color: green;">{{ resubmessage }}</h3> diff --git a/submissions/templatetags/submissions_extras.py b/submissions/templatetags/submissions_extras.py index 53fa07743aaf5d4cb3b08fa633fbf025d538611d..0ae2ea8d199f6414894e0256bc3a01955fba13eb 100644 --- a/submissions/templatetags/submissions_extras.py +++ b/submissions/templatetags/submissions_extras.py @@ -1,5 +1,10 @@ +import datetime + from django import template +from django.utils import timezone +from django.utils.timesince import timesince +from submissions.models import SUBMISSION_STATUS_OUT_OF_POOL from submissions.models import Submission register = template.Library() @@ -19,3 +24,57 @@ def is_viewable_by_authors(recommendation): return recommendation.submission.status in ['revision_requested', 'resubmitted', 'accepted', 'rejected', 'published', 'withdrawn'] + +@register.filter(name='required_actions') +def required_actions(submission): + """ + This method returns a list of required actions on a Submission. + Each list element is a textual statement. + """ + if (submission.status in SUBMISSION_STATUS_OUT_OF_POOL + or submission.status == 'revision_requested' + or submission.eicrecommendation_set.exists()): + return [] + todo = [] + for comment in submission.comment_set.all(): + if comment.status == 0: + todo.append('A Comment from %s has been delivered but is not yet vetted. ' + 'Please vet it.' % comment.author) + nr_ref_inv = submission.refereeinvitation_set.count() + if (submission.is_resubmission and nr_ref_inv == 0 + and not submission.eicrecommendation_set.exists()): + todo.append('This resubmission requires attention: either (re)invite referees ' + 'or formulate an Editorial Recommendation.') + if nr_ref_inv == 0 and not submission.is_resubmission: + todo.append('No Referees have yet been invited. ' + 'At least 3 should be.') + elif nr_ref_inv < 3 and not submission.is_resubmission: + todo.append('Only %s Referees have been invited. ' + 'At least 3 should be.' % nr_ref_inv) + for ref_inv in submission.refereeinvitation_set.all(): + refname = ref_inv.last_name + ', ' + ref_inv.first_name + if ref_inv.referee: + refname = str(ref_inv.referee) + timelapse = timezone.now() - ref_inv.date_invited + timeleft = submission.reporting_deadline - timezone.now() + if (ref_inv.accepted is None and not ref_inv.cancelled + and timelapse > datetime.timedelta(days=3)): + todo.append('Referee %s has not responded for %s days. ' + 'Consider sending a reminder ' + 'or cancelling the invitation.' % (refname, str(timelapse.days))) + if (ref_inv.accepted and not ref_inv.fulfilled and not ref_inv.cancelled + and timeleft < datetime.timedelta(days=7)): + todo.append('Referee %s has accepted to send a Report, ' + 'but not yet delivered it (with %s days left). ' + 'Consider sending a reminder or cancelling the invitation.' + % (refname, str(timeleft.days))) + if submission.reporting_deadline < timezone.now(): + todo.append('The refereeing deadline has passed. Please either extend it, ' + 'or formulate your Editorial Recommendation if at least ' + 'one Report has been received.') + reports = submission.report_set.all() + for report in reports: + if report.status == 0: + todo.append('The Report from %s has been delivered but is not yet vetted. ' + 'Please vet it.' % report.author) + return todo diff --git a/submissions/tests/test_models.py b/submissions/test_models.py similarity index 100% rename from submissions/tests/test_models.py rename to submissions/test_models.py diff --git a/submissions/test_services.py b/submissions/test_services.py new file mode 100644 index 0000000000000000000000000000000000000000..420411bdd0fc01cdeb2f0a3792be16b1d96e4f5c --- /dev/null +++ b/submissions/test_services.py @@ -0,0 +1,44 @@ +from django.test import TestCase +from .services import ArxivCaller +import pprint + + +class ArxivCallerTest(TestCase): + + def test_correct_lookup(self): + caller = ArxivCaller() + + caller.process('1611.09574v1') + + self.assertEqual(caller.is_valid(), True) + self.assertIn('entries', caller.metadata) + + def test_errorcode_for_non_existing_paper(self): + caller = ArxivCaller() + + caller.process('2611.09574v1') + self.assertEqual(caller.is_valid(), False) + self.assertEqual(caller.errorcode, 'preprint_does_not_exist') + + def test_errorcode_for_bad_request(self): + caller = ArxivCaller() + + caller.process('161109574v1') + self.assertEqual(caller.is_valid(), False) + self.assertEqual(caller.errorcode, 'arxiv_bad_request') + + def test_errorcode_for_already_published_journal_ref(self): + caller = ArxivCaller() + + caller.process('1412.0006v1') + self.assertEqual(caller.is_valid(), False) + self.assertEqual(caller.errorcode, 'paper_published_journal_ref') + self.assertNotEqual(caller.arxiv_journal_ref, '') + + def test_errorcode_no_version_nr(self): + # Should be already caught in form validation + caller = ArxivCaller() + + caller.process('1412.0006') + self.assertEqual(caller.is_valid(), False) + self.assertEqual(caller.errorcode, 'bad_identifier') diff --git a/submissions/test_views.py b/submissions/test_views.py new file mode 100644 index 0000000000000000000000000000000000000000..5eef315b06ebc633dbb967284d7d0336a8073e17 --- /dev/null +++ b/submissions/test_views.py @@ -0,0 +1,47 @@ +from django.test import TestCase +from django.test import Client +from submissions.views import * +import django.core.urlresolvers + + +class PrefillUsingIdentifierTest(TestCase): + fixtures = ['permissions', 'groups', 'contributors'] + + def test_retrieving_existing_arxiv_paper(self): + client = Client() + client.login(username="Test", password="testpw") + + response = client.post(reverse('submissions:prefill_using_identifier'), + {'identifier': '1512.00030v1'}) + + self.assertEqual(response.status_code, 200) + + def test_still_200_ok_if_identifier_is_wrong(self): + client = Client() + client.login(username="Test", password="testpw") + + response = client.post(reverse('submissions:prefill_using_identifier'), + {'identifier': '1512.00030'}) + + self.assertEqual(response.status_code, 200) + +class SubmitManuscriptTest(TestCase): + fixtures = ['permissions', 'groups', 'contributors'] + + def test_submit_correct_manuscript(self): + client = Client() + client.login(username="Test", password="testpw") + + response = client.post(reverse('submissions:prefill_using_identifier'), + {'identifier': '1512.00030v1'}) + + params = response.context['form'].initial + + extras = {'discipline': 'physics', + 'submitted_to_journal': 'SciPost Physics', + 'submission_type': 'Article', + 'domain': 'T'} + response = client.post(reverse('submissions:submit_manuscript'), + {**params, **extras}) + + self.assertEqual(response.status_code, 200) diff --git a/submissions/tests/test_views.py b/submissions/tests/test_views.py deleted file mode 100644 index f35d237bde6def6e0755612febe441d1d10d6d63..0000000000000000000000000000000000000000 --- a/submissions/tests/test_views.py +++ /dev/null @@ -1,27 +0,0 @@ -from django.test import TestCase -from django.test import Client - -from submissions.views import * - - - -class PrefillUsingIdentifierTest(TestCase): - fixtures = ['permissions', 'groups', 'contributors'] - - def test_retrieving_existing_arxiv_paper(self): - client = Client() - client.login(username="Test", password="testpw") - - response = client.post('/submissions/prefill_using_identifier', - {'identifier': '1512.00030v1'}) - - self.assertEqual(response.status_code, 200) - - def test_still_200_ok_if_identifier_is_wrong(self): - client = Client() - client.login(username="Test", password="testpw") - - response = client.post('/submissions/prefill_using_identifier', - {'identifier': '1512.00030'}) - - self.assertEqual(response.status_code, 200) diff --git a/submissions/urls.py b/submissions/urls.py index e8bd179cf48dd92a26c05fde6a5aa6a9740340a9..1b3ee7bbc3fad480b1f0ac5698a521abfeae6603 100644 --- a/submissions/urls.py +++ b/submissions/urls.py @@ -18,10 +18,15 @@ urlpatterns = [ name='submission_wo_vn_nr'), url(r'^(?P<arxiv_identifier_w_vn_nr>[0-9]{4,}.[0-9]{5,}v[0-9]{1,2})/$', views.submission_detail, name='submission'), + # url(r'^prefill_using_identifier$', + # views.prefill_using_identifier, name='prefill_using_identifier'), url(r'^prefill_using_identifier$', - views.prefill_using_identifier, name='prefill_using_identifier'), - url(r'^submit_manuscript$', views.submit_manuscript, name='submit_manuscript'), + views.PrefillUsingIdentifierView.as_view(), name='prefill_using_identifier'), + # url(r'^submit_manuscript$', views.submit_manuscript, name='submit_manuscript'), + url(r'^submit_manuscript$', views.SubmissionCreateView.as_view(), name='submit_manuscript'), url(r'^pool$', views.pool, name='pool'), + url(r'^submissions_by_status/(?P<status>[a-zA-Z_]+)$', + views.submissions_by_status, name='submissions_by_status'), url(r'^add_remark/(?P<arxiv_identifier_w_vn_nr>[0-9]{4,}.[0-9]{5,}v[0-9]{1,2})$', views.add_remark, name='add_remark'), # Assignment of Editor-in-charge @@ -37,6 +42,7 @@ urlpatterns = [ views.assignment_failed, name='assignment_failed'), # Editorial workflow and refereeing url(r'^editorial_workflow$', views.editorial_workflow, name='editorial_workflow'), + url(r'^assignments$', views.assignments, name='assignments'), url(r'^editorial_page/(?P<arxiv_identifier_w_vn_nr>[0-9]{4,}.[0-9]{5,}v[0-9]{1,2})$', views.editorial_page, name='editorial_page'), url(r'^select_referee/(?P<arxiv_identifier_w_vn_nr>[0-9]{4,}.[0-9]{5,}v[0-9]{1,2})$', @@ -76,6 +82,8 @@ urlpatterns = [ # Voting url(r'^prepare_for_voting/(?P<rec_id>[0-9]+)$', views.prepare_for_voting, name='prepare_for_voting'), url(r'^vote_on_rec/(?P<rec_id>[0-9]+)$', views.vote_on_rec, name='vote_on_rec'), + url(r'^remind_Fellows_to_vote$', views.remind_Fellows_to_vote, + name='remind_Fellows_to_vote'), # Editorial Administration url(r'fix_College_decision/(?P<rec_id>[0-9]+)$', views.fix_College_decision, name='fix_College_decision'), diff --git a/submissions/utils.py b/submissions/utils.py index 6d2e0c65d65a5cf5bf2b470d2210ecf6e9dc7492..20690867f50cfa64766cbb573b2ad2b38b591c59 100644 --- a/submissions/utils.py +++ b/submissions/utils.py @@ -19,7 +19,6 @@ class SubmissionUtils(object): for var_name in dict: setattr(cls, var_name, dict[var_name]) - @classmethod def deprecate_other_assignments(cls): """ @@ -44,7 +43,6 @@ class SubmissionUtils(object): atd.deprecated = True atd.save() - @classmethod def send_authors_submission_ack_email(cls): """ Requires loading 'submission' attribute. """ @@ -78,7 +76,6 @@ class SubmissionUtils(object): email_text_html += '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) html_version = html_template.render(email_context) - #emailmessage = EmailMessage( emailmessage = EmailMultiAlternatives( 'SciPost: Submission received', email_text, 'SciPost Editorial Admin <submissions@scipost.org>', @@ -88,7 +85,6 @@ class SubmissionUtils(object): emailmessage.attach_alternative(html_version, 'text/html') emailmessage.send(fail_silently=False) - @classmethod def send_authors_resubmission_ack_email(cls): """ Requires loading 'submission' attribute. """ @@ -119,7 +115,6 @@ class SubmissionUtils(object): email_text_html += '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) html_version = html_template.render(email_context) - #emailmessage = EmailMessage( emailmessage = EmailMultiAlternatives( 'SciPost: Resubmission received', email_text, 'SciPost Editorial Admin <submissions@scipost.org>', @@ -129,7 +124,6 @@ class SubmissionUtils(object): emailmessage.attach_alternative(html_version, 'text/html') emailmessage.send(fail_silently=False) - @classmethod def send_assignment_request_email(cls): """ Requires loading 'assignment' attribute. """ @@ -174,7 +168,6 @@ class SubmissionUtils(object): email_text_html += '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) html_version = html_template.render(email_context) - #emailmessage = EmailMessage( emailmessage = EmailMultiAlternatives( 'SciPost: potential Submission assignment', email_text, 'SciPost Editorial Admin <submissions@scipost.org>', @@ -184,7 +177,6 @@ class SubmissionUtils(object): emailmessage.attach_alternative(html_version, 'text/html') emailmessage.send(fail_silently=False) - @classmethod def send_EIC_appointment_email(cls): """ Requires loading 'assignment' attribute. """ @@ -199,8 +191,8 @@ class SubmissionUtils(object): + cls.assignment.submission.arxiv_identifier_w_vn_nr + ' (also accessible from your personal page ' 'https://scipost.org/personal_page under the Editorial Actions tab). ' - 'In particular, you should now invite at least 3 referees; you might want to ' - 'make sure you are aware of the ' + 'In particular, you should now invite at least 3 referees; you might want to' + ' make sure you are aware of the ' 'detailed procedure described in the Editorial College by-laws at ' 'https://scipost.org/EdCol_by-laws.' '\n\nMany thanks in advance for your collaboration,' @@ -233,7 +225,6 @@ class SubmissionUtils(object): email_text_html += '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) html_version = html_template.render(email_context) - #emailmessage = EmailMessage( emailmessage = EmailMultiAlternatives( 'SciPost: assignment as EIC', email_text, 'SciPost Editorial Admin <submissions@scipost.org>', @@ -243,7 +234,6 @@ class SubmissionUtils(object): emailmessage.attach_alternative(html_version, 'text/html') emailmessage.send(fail_silently=False) - @classmethod def send_EIC_reappointment_email(cls): """ Requires loading 'submission' attribute. """ @@ -306,7 +296,6 @@ class SubmissionUtils(object): emailmessage.attach_alternative(html_version, 'text/html') emailmessage.send(fail_silently=False) - @classmethod def send_author_prescreening_passed_email(cls): """ Requires loading 'assignment' attribute. """ @@ -376,7 +365,6 @@ class SubmissionUtils(object): email_text_html += '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) html_version = html_template.render(email_context) - #emailmessage = EmailMessage( emailmessage = EmailMultiAlternatives( 'SciPost: pre-screening passed', email_text, 'SciPost Editorial Admin <submissions@scipost.org>', @@ -386,7 +374,6 @@ class SubmissionUtils(object): emailmessage.attach_alternative(html_version, 'text/html') emailmessage.send(fail_silently=False) - @classmethod def assignment_failed_email_authors(cls): """ Requires loading 'submission' attribute. """ @@ -401,8 +388,8 @@ class SubmissionUtils(object): if len(cls.personal_message) > 3: email_text += '\n\n' + cls.personal_message email_text += ('\n\nWe nonetheless thank you very much for your contribution.' - '\n\nSincerely,' + - '\n\nThe SciPost Team.') + '\n\nSincerely,' + + '\n\nThe SciPost Team.') email_text_html = ( '<p>Dear {{ title }} {{ last_name }},</p>' '<p>Your recent Submission to SciPost,</p>' @@ -428,7 +415,6 @@ class SubmissionUtils(object): email_text_html += '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) html_version = html_template.render(email_context) - #emailmessage = EmailMessage( emailmessage = EmailMultiAlternatives( 'SciPost: pre-screening not passed', email_text, 'SciPost Editorial Admin <submissions@scipost.org>', @@ -438,7 +424,6 @@ class SubmissionUtils(object): emailmessage.attach_alternative(html_version, 'text/html') emailmessage.send(fail_silently=False) - @classmethod def send_refereeing_invitation_email(cls): """ @@ -514,7 +499,6 @@ class SubmissionUtils(object): email_text_html += '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) html_version = html_template.render(email_context) - #emailmessage = EmailMessage( emailmessage = EmailMultiAlternatives( 'SciPost: refereeing request', email_text, 'SciPost Editorial Admin <submissions@scipost.org>', @@ -525,7 +509,6 @@ class SubmissionUtils(object): emailmessage.attach_alternative(html_version, 'text/html') emailmessage.send(fail_silently=False) - @classmethod def send_ref_reminder_email(cls): """ @@ -615,7 +598,6 @@ class SubmissionUtils(object): email_text_html += '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) html_version = html_template.render(email_context) - #emailmessage = EmailMessage( emailmessage = EmailMultiAlternatives( 'SciPost: reminder (refereeing request and registration invitation)', email_text, 'SciPost Submissions <submissions@scipost.org>', @@ -626,7 +608,6 @@ class SubmissionUtils(object): emailmessage.attach_alternative(html_version, 'text/html') emailmessage.send(fail_silently=False) - @classmethod def send_ref_cancellation_email(cls): """ @@ -685,7 +666,6 @@ class SubmissionUtils(object): email_text_html += '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) html_version = html_template.render(email_context) - #emailmessage = EmailMessage( emailmessage = EmailMultiAlternatives( 'SciPost: report no longer needed', email_text, 'SciPost Submissions <submissions@scipost.org>', @@ -696,7 +676,6 @@ class SubmissionUtils(object): emailmessage.attach_alternative(html_version, 'text/html') emailmessage.send(fail_silently=False) - @classmethod def email_referee_response_to_EIC(cls): """ Requires loading 'invitation' attribute. """ @@ -712,7 +691,7 @@ class SubmissionUtils(object): email_text += 'accepted ' email_text_html += 'accepted ' email_subject = 'SciPost: referee accepts to review' - elif cls.invitation.accepted == False: + elif not cls.invitation.accepted: email_text += ('declined (due to reason: ' + assignment_refusal_reasons_dict[cls.invitation.refusal_reason] + ') ') email_text_html += 'declined (due to reason: {{ reason }}) ' @@ -723,7 +702,7 @@ class SubmissionUtils(object): email_text_html += ( 'to referee Submission</p>' '<p>{{ sub_title }}</p>\n<p>by {{ author_list }}.</p>') - if cls.invitation.accepted == False: + if not cls.invitation.accepted: email_text += ('\n\nPlease invite another referee from the Submission\'s editorial page ' 'at https://scipost.org/submissions/editorial_page/' + cls.invitation.submission.arxiv_identifier_w_vn_nr + '.') @@ -749,7 +728,6 @@ class SubmissionUtils(object): email_text_html += '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) html_version = html_template.render(email_context) - #emailmessage = EmailMessage( emailmessage = EmailMultiAlternatives( email_subject, email_text, 'SciPost Editorial Admin <submissions@scipost.org>', @@ -759,7 +737,6 @@ class SubmissionUtils(object): emailmessage.attach_alternative(html_version, 'text/html') emailmessage.send(fail_silently=False) - @classmethod def email_EIC_report_delivered(cls): """ Requires loading 'report' attribute. """ @@ -795,7 +772,6 @@ class SubmissionUtils(object): email_text_html += '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) html_version = html_template.render(email_context) - #emailmessage = EmailMessage( emailmessage = EmailMultiAlternatives( 'SciPost: Report delivered', email_text, 'SciPost Editorial Admin <submissions@scipost.org>', @@ -805,14 +781,13 @@ class SubmissionUtils(object): emailmessage.attach_alternative(html_version, 'text/html') emailmessage.send(fail_silently=False) - @classmethod def acknowledge_report_email(cls): """ Requires loading 'report' attribute. """ email_text = ('Dear ' + title_dict[cls.report.author.title] + ' ' + cls.report.author.user.last_name + ',' '\n\nMany thanks for your Report on Submission\n\n' + - cls.report.submission.title + ' by ' + cls.report.submission.title + ' by ' + cls.report.submission.author_list + '.') email_text_html = ( '<p>Dear {{ ref_title }} {{ ref_last_name }},</p>' @@ -878,7 +853,6 @@ class SubmissionUtils(object): email_text_html += '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) html_version = html_template.render(email_context) - #emailmessage = EmailMessage( emailmessage = EmailMultiAlternatives( 'SciPost: Report acknowledgement', email_text, 'SciPost Editorial Admin <submissions@scipost.org>', @@ -889,7 +863,6 @@ class SubmissionUtils(object): emailmessage.attach_alternative(html_version, 'text/html') emailmessage.send(fail_silently=False) - @classmethod def send_author_report_received_email(cls): """ Requires loading 'report' attribute. """ @@ -933,7 +906,6 @@ class SubmissionUtils(object): email_text_html += '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) html_version = html_template.render(email_context) - #emailmessage = EmailMessage( emailmessage = EmailMultiAlternatives( 'SciPost: Report received on your Submission', email_text, 'SciPost Editorial Admin <submissions@scipost.org>', @@ -943,7 +915,6 @@ class SubmissionUtils(object): emailmessage.attach_alternative(html_version, 'text/html') emailmessage.send(fail_silently=False) - @classmethod def send_author_comment_received_email(cls): """ Requires loading 'submission' attribute. """ @@ -977,7 +948,6 @@ class SubmissionUtils(object): email_text_html += '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) html_version = html_template.render(email_context) - #emailmessage = EmailMessage( emailmessage = EmailMultiAlternatives( 'SciPost: Comment received on your Submission', email_text, 'SciPost Editorial Admin <submissions@scipost.org>', @@ -988,7 +958,6 @@ class SubmissionUtils(object): emailmessage.attach_alternative(html_version, 'text/html') emailmessage.send(fail_silently=False) - @classmethod def send_communication_email(cls): """ @@ -1006,8 +975,6 @@ class SubmissionUtils(object): cls.communication.submission.editor_in_charge.user.last_name) further_action_page = ('https://scipost.org/submission/editorial_page/' + cls.communication.submission.arxiv_identifier_w_vn_nr) - #if cls.communication.comtype == 'AtoE': - # bcc_emails.append(cls.communication.submission.submitted_by.user.email) # BUG: must not happen! if cls.communication.comtype == 'RtoE': bcc_emails.append(cls.communication.referee.user.email) bcc_emails.append('submissions@scipost.org') @@ -1053,7 +1020,6 @@ class SubmissionUtils(object): reply_to=['submissions@scipost.org']) emailmessage.send(fail_silently=False) - @classmethod def send_author_revision_requested_email(cls): """ Requires loading 'submission' and 'recommendation' attributes. """ @@ -1074,8 +1040,8 @@ class SubmissionUtils(object): email_text += 'major' email_text_html += 'major' email_text += (' revision.' - '\n\nYou can view it at the Submission Page ' - 'https://scipost.org/submission/' + '\n\nYou can view it at the Submission Page ' + 'https://scipost.org/submission/' + cls.submission.arxiv_identifier_w_vn_nr + '. ' 'Note that the recommendation is viewable only by ' 'the registered authors of the submission.' @@ -1113,7 +1079,6 @@ class SubmissionUtils(object): email_text_html += '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) html_version = html_template.render(email_context) - #emailmessage = EmailMessage( emailmessage = EmailMultiAlternatives( 'SciPost: revision requested', email_text, 'SciPost Editorial Admin <submissions@scipost.org>', @@ -1124,7 +1089,6 @@ class SubmissionUtils(object): emailmessage.attach_alternative(html_version, 'text/html') emailmessage.send(fail_silently=False) - @classmethod def send_author_College_decision_email(cls): """ Requires loading 'submission' and 'recommendation' attributes. """ @@ -1147,7 +1111,7 @@ class SubmissionUtils(object): email_text_html += ( '<p>We are pleased to inform you that your Submission ' 'has been accepted for publication in <strong>{{ journal }}</strong>') - if cls.recommendation.recommendation == 1 and False: # Temporary deactivation of Select + if cls.recommendation.recommendation == 1 and False: # Temporary deactivation of Select email_text += (', with a promotion to Select. We warmly congratulate you ' 'on this achievement, which is reserved to papers deemed in ' 'the top ten percent of papers we publish.') @@ -1162,8 +1126,8 @@ class SubmissionUtils(object): 'production team, who will soon send you proofs ' 'to check before final publication.') email_text_html += ('\n<p>Your manuscript will now be taken charge of by our ' - 'production team, who will soon send you proofs ' - 'to check before final publication.</p>') + 'production team, who will soon send you proofs ' + 'to check before final publication.</p>') elif cls.recommendation.recommendation == -3: email_text += ('We are sorry to inform you that your Submission ' @@ -1173,12 +1137,9 @@ class SubmissionUtils(object): + cls.submission.arxiv_identifier_w_vn_nr + '. ' 'Note that these details are viewable only by ' 'the registered authors of the submission.' - #'\n\nUnless you explicitly request otherwise, we will deactivate your ' - #'Submission\'s Page within one week and remove it from public view.' '\n\nThis Submission Page has now been removed ' 'from general public view; if you wish, you can email us and ' - 'request to make it publicly visible again.' - ) + 'request to make it publicly visible again.') email_text_html += ( '<p>We are sorry to inform you that your Submission ' 'has not been accepted for publication.</p>' @@ -1187,8 +1148,6 @@ class SubmissionUtils(object): '{{ arxiv_identifier_w_vn_nr }}">Submission\'s Page</a>. ' 'Note that these details are viewable only by ' 'the registered authors of the submission.</p>' - #'<p>Unless you explicitly request otherwise, we will deactivate your ' - #'Submission\'s Page within one week and remove it from public view.</p>' '<p>This Submission Page has now been removed ' 'from general public view; if you wish, you can email us and ' 'request to make it publicly visible again.</p>' @@ -1210,7 +1169,6 @@ class SubmissionUtils(object): email_text_html += '<br/>' + EMAIL_FOOTER html_template = Template(email_text_html) html_version = html_template.render(email_context) - #emailmessage = EmailMessage( emailmessage = EmailMultiAlternatives( 'SciPost: College decision', email_text, 'SciPost Editorial Admin <submissions@scipost.org>', @@ -1220,3 +1178,39 @@ class SubmissionUtils(object): reply_to=['submissions@scipost.org']) emailmessage.attach_alternative(html_version, 'text/html') emailmessage.send(fail_silently=False) + + @classmethod + def send_Fellows_voting_reminder_email(cls): + """ + Requires loading 'Fellow_emails' attribute, which is a list of email addresses. + """ + email_text = ('Dear Fellow,' + '\n\nYou have pending voting duties in the SciPost ' + 'submissions pool at https://scipost.org/submissions/pool' + ' (also accessible from your personal page ' + 'https://scipost.org/personal_page under the Editorial Actions tab). ' + 'Could you please have a quick look within the next couple of days, ' + 'so we can finish processing these submissions?' + '\n\nMany thanks in advance,' + '\n\nThe SciPost Team.') + email_text_html = ( + '<p>Dear Fellow,</p>' + '<p>You have pending voting duties in the SciPost ' + 'submissions pool https://scipost.org/submissions/pool' + ' (also accessible from your personal page ' + 'https://scipost.org/personal_page under the Editorial Actions tab).</p>' + '<p>Could you please have a quick look within the next couple of days, ' + 'so we can finish processing these submissions?</p>' + '<p>Many thanks in advance,</p>' + '<p>The SciPost Team.</p><br/>' + EMAIL_FOOTER) + email_context = Context({}) + html_template = Template(email_text_html) + html_version = html_template.render(email_context) + emailmessage = EmailMultiAlternatives( + 'SciPost: voting duties', email_text, + 'SciPost Editorial Admin <admin@scipost.org>', + to=['admin@scipost.org'], + bcc=cls.Fellow_emails, + reply_to=['admin@scipost.org']) + emailmessage.attach_alternative(html_version, 'text/html') + emailmessage.send(fail_silently=False) diff --git a/submissions/views.py b/submissions/views.py index c44ca274fe39818c1a86f74a4ae527602fac06e7..1cd915072877aeed08fec352c19a4fec2ca8888e 100644 --- a/submissions/views.py +++ b/submissions/views.py @@ -1,28 +1,31 @@ import datetime import feedparser -import re -import requests -import sys -from django.contrib.auth import authenticate, login, logout from django.contrib.auth.decorators import login_required, permission_required -from django.contrib.auth.models import User, Group, Permission +from django.contrib.auth.models import Group from django.core.exceptions import PermissionDenied -from django.core.mail import EmailMessage from django.core.urlresolvers import reverse from django.db import transaction -from django.db.models import Avg -from django.http import HttpResponse, HttpResponseRedirect +from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, render, redirect +from django.template import Template, Context from django.utils import timezone -from django.utils.safestring import mark_safe -from django.views.decorators.csrf import csrf_protect from guardian.decorators import permission_required_or_403 from guardian.shortcuts import assign_perm -from .models import * -from .forms import * +# from .models import * +# from .forms import * +from .models import Submission, EICRecommendation, EditorialAssignment,\ + RefereeInvitation, Report, EditorialCommunication,\ + SUBMISSION_STATUS_PUBLICLY_UNLISTED, SUBMISSION_STATUS_VOTING_DEPRECATED,\ + SUBMISSION_STATUS_PUBLICLY_INVISIBLE, SUBMISSION_STATUS_OUT_OF_POOL,\ + SUBMISSION_STATUS, submission_status_dict, ed_comm_choices_dict +from .forms import SubmissionIdentifierForm, SubmissionForm, SubmissionSearchForm,\ + RecommendationVoteForm, ConsiderAssignmentForm, AssignSubmissionForm,\ + SetRefereeingDeadlineForm, RefereeSelectForm, RefereeRecruitmentForm,\ + ConsiderRefereeInvitationForm, EditorialCommunicationForm,\ + EICRecommendationForm, ReportForm, VetReportForm, VotingEligibilityForm from .utils import SubmissionUtils from comments.models import Comment @@ -34,100 +37,140 @@ from scipost.utils import Utils from comments.forms import CommentForm +from .services import ArxivCaller + +from django.views.generic.edit import CreateView, FormView ############### # SUBMISSIONS: ############### -@permission_required('scipost.can_submit_manuscript', raise_exception=True) -def prefill_using_identifier(request): - if request.method == "POST": +# @permission_required('scipost.can_submit_manuscript', raise_exception=True) +# def prefill_using_identifier(request): +# if request.method == "POST": +# identifierform = SubmissionIdentifierForm(request.POST) +# if identifierform.is_valid(): +# # Use the ArxivCaller class to make the API calls +# caller = ArxivCaller() +# caller.process(identifierform.cleaned_data['identifier']) +# +# if caller.is_valid(): +# # Arxiv response is valid and can be shown +# +# metadata = caller.metadata +# is_resubmission = caller.resubmission +# title = metadata['entries'][0]['title'] +# authorlist = metadata['entries'][0]['authors'][0]['name'] +# for author in metadata['entries'][0]['authors'][1:]: +# authorlist += ', ' + author['name'] +# arxiv_link = metadata['entries'][0]['id'] +# abstract = metadata['entries'][0]['summary'] +# initialdata = {'is_resubmission': is_resubmission, +# 'metadata': metadata, +# 'title': title, 'author_list': authorlist, +# 'arxiv_identifier_w_vn_nr': caller.identifier_with_vn_nr, +# 'arxiv_identifier_wo_vn_nr': caller.identifier_without_vn_nr, +# 'arxiv_vn_nr': caller.version_nr, +# 'arxiv_link': arxiv_link, 'abstract': abstract} +# if is_resubmission: +# resubmessage = ('There already exists a preprint with this arXiv identifier ' +# 'but a different version number. \nYour Submission will be ' +# 'handled as a resubmission.') +# initialdata['submitted_to_journal'] = previous_submissions[0].submitted_to_journal +# initialdata['submission_type'] = previous_submissions[0].submission_type +# initialdata['discipline'] = previous_submissions[0].discipline +# initialdata['domain'] = previous_submissions[0].domain +# initialdata['subject_area'] = previous_submissions[0].subject_area +# initialdata['secondary_areas'] = previous_submissions[0].secondary_areas +# initialdata['referees_suggested'] = previous_submissions[0].referees_suggested +# initialdata['referees_flagged'] = previous_submissions[0].referees_flagged +# else: +# resubmessage = '' +# +# form = SubmissionForm(initial=initialdata) +# context = {'identifierform': identifierform, +# 'form': form, +# 'resubmessage': resubmessage} +# return render(request, 'submissions/submit_manuscript.html', context) +# +# else: +# # Arxiv response is not valid +# errormessages = { +# 'preprint_does_not_exist': +# 'A preprint associated to this identifier does not exist.', +# 'paper_published_journal_ref': +# ('This paper has been published as ' + caller.arxiv_journal_ref + +# '. You cannot submit it to SciPost anymore.'), +# 'paper_published_doi': +# ('This paper has been published under DOI ' + caller.arxiv_doi + +# '. You cannot submit it to SciPost anymore.'), +# 'arxiv_timeout': 'Arxiv did not respond in time. Please try again later', +# 'arxiv_bad_request': +# ('There was an error with requesting identifier ' + +# caller.identifier_with_vn_nr + +# ' from Arxiv. Please check the identifier and try again.'), +# 'previous_submission_undergoing_refereeing': +# ('There exists a preprint with this arXiv identifier ' +# 'but an earlier version number, which is still undergoing ' +# 'peer refereeing.' +# 'A resubmission can only be performed after request ' +# 'from the Editor-in-charge. Please wait until the ' +# 'closing of the previous refereeing round and ' +# 'formulation of the Editorial Recommendation ' +# 'before proceeding with a resubmission.'), +# 'preprint_already_submitted': 'This preprint version has already been submitted to SciPost.' +# } +# +# identifierform.add_error(None, errormessages[caller.errorcode]) +# form = SubmissionForm() +# return render(request, 'submissions/submit_manuscript.html', +# {'identifierform': identifierform, 'form': form}) +# else: +# form = SubmissionForm() +# return render(request, 'submissions/submit_manuscript.html', +# {'identifierform': identifierform, 'form': form}) +# # return redirect(reverse('submissions:submit_manuscript')) +# form = SubmissionForm() +# identifierform = SubmissionIdentifierForm() +# return render(request, 'submissions/submit_manuscript.html', +# {'identifierform': identifierform, 'form': form}) + + +class PrefillUsingIdentifierView(FormView): + form_class = SubmissionIdentifierForm + template_name = 'submissions/prefill_using_identifier.html' + + def post(self, request): identifierform = SubmissionIdentifierForm(request.POST) if identifierform.is_valid(): - # Perform Arxiv query and check if results are OK for submission - metadata, errormessage = lookup_article(identifierform.cleaned_data['identifier']) - - if not metadata: - form = SubmissionForm() - return render(request, 'submissions/submit_manuscript.html', - {'identifierform': identifierform, 'form': form, - 'errormessage': errormessage}) - - is_resubmission = False - resubmessage = '' - previous_submissions = Submission.objects.filter( - arxiv_identifier_wo_vn_nr=identifier_without_vn_nr).order_by('-arxiv_vn_nr') - if previous_submissions.exists(): - if previous_submissions[0].status in ['rejected', 'rejected_visible',]: - errormessage = ('<p>This arXiv preprint has previously undergone refereeing ' - 'and has been rejected. Resubmission is only possible ' - 'if the manuscript has been substantially reworked into ' - 'a new arXiv submission with distinct identifier.</p>') - return render(request, 'scipost/error.html', - {'errormessage': mark_safe(errormessage)}) - # If the Editorial Recommendation hasn't been formulated, ask to wait - if previous_submissions[0].status != 'revision_requested': - errormessage = ('<p>There exists a preprint with this arXiv identifier ' - 'but an earlier version number, which is still undergoing ' - 'peer refereeing.</p>' - '<p>A resubmission can only be performed after request ' - 'from the Editor-in-charge. Please wait until the ' - 'closing of the previous refereeing round and ' - 'formulation of the Editorial Recommendation ' - 'before proceeding with a resubmission.</p>') - return render(request, 'scipost/error.html', - {'errormessage': mark_safe(errormessage)}) - is_resubmission = True - resubmessage = ('There already exists a preprint with this arXiv identifier ' - 'but a different version number. \nYour Submission will be ' - 'handled as a resubmission.') - try: - queryurl = ('http://export.arxiv.org/api/query?id_list=%s' - % identifierform.cleaned_data['identifier']) - arxivquery = feedparser.parse(queryurl) - # Flag error if preprint doesn't exist - try: - test = arxivquery['entries'][0]['title'] - except KeyError: - errormessage = 'A preprint associated to this identifier does not exist.' - except: - pass - - # If paper has been published, should comment on published version - try: - arxiv_journal_ref = arxivquery['entries'][0]['arxiv_journal_ref'] - errormessage = ('This paper has been published as ' + arxiv_journal_ref + - '. You cannot submit it to SciPost anymore.') - except: - pass - try: - arxiv_doi = arxivquery['entries'][0]['arxiv_doi'] - errormessage = ('This paper has been published under DOI ' + arxiv_DOI - + '. You cannot submit it to SciPost anymore.') - except: - pass - if errormessage != '': - form = SubmissionForm() - context = {'identifierform': identifierform, 'form': form, - 'errormessage': errormessage} - return render(request, 'submissions/submit_manuscript.html', context) - - metadata = arxivquery - title = arxivquery['entries'][0]['title'] - authorlist = arxivquery['entries'][0]['authors'][0]['name'] - for author in arxivquery['entries'][0]['authors'][1:]: + # Use the ArxivCaller class to make the API calls + caller = ArxivCaller() + caller.process(identifierform.cleaned_data['identifier']) + + if caller.is_valid(): + # Arxiv response is valid and can be shown + + metadata = caller.metadata + is_resubmission = caller.resubmission + title = metadata['entries'][0]['title'] + authorlist = metadata['entries'][0]['authors'][0]['name'] + for author in metadata['entries'][0]['authors'][1:]: authorlist += ', ' + author['name'] - arxiv_link = arxivquery['entries'][0]['id'] - abstract = arxivquery['entries'][0]['summary'] - initialdata={'is_resubmission': is_resubmission, - 'metadata': metadata, - 'title': title, 'author_list': authorlist, - 'arxiv_identifier_w_vn_nr': identifierform.cleaned_data['identifier'], - 'arxiv_identifier_wo_vn_nr': identifier_without_vn_nr, - 'arxiv_vn_nr': arxiv_vn_nr, - 'arxiv_link': arxiv_link, 'abstract': abstract} + arxiv_link = metadata['entries'][0]['id'] + abstract = metadata['entries'][0]['summary'] + initialdata = {'is_resubmission': is_resubmission, + 'metadata': metadata, + 'title': title, 'author_list': authorlist, + 'arxiv_identifier_w_vn_nr': caller.identifier_with_vn_nr, + 'arxiv_identifier_wo_vn_nr': caller.identifier_without_vn_nr, + 'arxiv_vn_nr': caller.version_nr, + 'arxiv_link': arxiv_link, 'abstract': abstract} if is_resubmission: + previous_submissions = caller.previous_submissions + resubmessage = ('There already exists a preprint with this arXiv identifier ' + 'but a different version number. \nYour Submission will be ' + 'handled as a resubmission.') initialdata['submitted_to_journal'] = previous_submissions[0].submitted_to_journal initialdata['submission_type'] = previous_submissions[0].submission_type initialdata['discipline'] = previous_submissions[0].discipline @@ -136,131 +179,239 @@ def prefill_using_identifier(request): initialdata['secondary_areas'] = previous_submissions[0].secondary_areas initialdata['referees_suggested'] = previous_submissions[0].referees_suggested initialdata['referees_flagged'] = previous_submissions[0].referees_flagged + else: + resubmessage = '' + form = SubmissionForm(initial=initialdata) context = {'identifierform': identifierform, 'form': form, 'resubmessage': resubmessage} - return render(request, 'submissions/submit_manuscript.html', context) - except: - print("Unexpected error in prefill_using_identifier:", sys.exc_info()[0]) - context = {'identifierform': identifierform, - 'form': SubmissionForm(), - 'errormessage': errormessage,} - return render(request, 'submissions/submit_manuscript.html', context) + return render(request, 'submissions/new_submission.html', context) + + else: + # Arxiv response is not valid + errormessages = { + 'preprint_does_not_exist': + 'A preprint associated to this identifier does not exist.', + 'paper_published_journal_ref': + ('This paper has been published as ' + caller.arxiv_journal_ref + + '. You cannot submit it to SciPost anymore.'), + 'paper_published_doi': + ('This paper has been published under DOI ' + caller.arxiv_doi + + '. You cannot submit it to SciPost anymore.'), + 'arxiv_timeout': 'Arxiv did not respond in time. Please try again later', + 'arxiv_bad_request': + ('There was an error with requesting identifier ' + + caller.identifier_with_vn_nr + + ' from Arxiv. Please check the identifier and try again.'), + 'previous_submission_undergoing_refereeing': + ('There exists a preprint with this arXiv identifier ' + 'but an earlier version number, which is still undergoing ' + 'peer refereeing.' + 'A resubmission can only be performed after request ' + 'from the Editor-in-charge. Please wait until the ' + 'closing of the previous refereeing round and ' + 'formulation of the Editorial Recommendation ' + 'before proceeding with a resubmission.'), + 'preprint_already_submitted': 'This preprint version has already been submitted to SciPost.', + 'previous_submissions_rejected': + ('This arXiv preprint has previously undergone refereeing ' + 'and has been rejected. Resubmission is only possible ' + 'if the manuscript has been substantially reworked into ' + 'a new arXiv submission with distinct identifier.') + } + + identifierform.add_error(None, errormessages[caller.errorcode]) + return render(request, 'submissions/prefill_using_identifier.html', + {'form': identifierform}) + else: + return render(request, 'submissions/prefill_using_identifier.html', + {'form': identifierform}) + + +class SubmissionCreateView(CreateView): + model = Submission + fields = [ + 'is_resubmission', + 'discipline', + 'submitted_to_journal', + 'submission_type', + 'domain', + 'subject_area', + 'secondary_areas', + 'title', + 'author_list', + 'abstract', + 'arxiv_identifier_w_vn_nr', + 'arxiv_identifier_wo_vn_nr', + 'arxiv_vn_nr', + 'arxiv_link', + 'metadata', + 'author_comments', + 'list_of_changes', + 'remarks_for_editors', + 'referees_suggested', + 'referees_flagged' + ] + + template_name = 'submissions/new_submission.html' + + def get(self, request): + # Only use prefilled forms + return redirect('submissions:prefill_using_identifier') + + @transaction.atomic + def form_valid(self, form): + submitted_by = Contributor.objects.get(user=self.request.user) + form.instance.submitted_by = submitted_by + + # Save all the information contained in the form + submission = form.save() + + # Perform all extra actions and set information not contained in the form + submission.finish_submission() + + if submission.is_resubmission: + # Assign permissions + assign_perm('can_take_editorial_actions', submission.editor_in_charge.user, submission) + ed_admins = Group.objects.get(name='Editorial Administrators') + assign_perm('can_take_editorial_actions', ed_admins, submission) + + # Send emails + SubmissionUtils.load({'submission': submission}) + SubmissionUtils.send_authors_resubmission_ack_email() + SubmissionUtils.send_EIC_reappointment_email() else: - form = SubmissionForm() - return render(request, 'submissions/submit_manuscript.html', - {'identifierform': identifierform, 'form': form}) - return redirect(reverse('submissions:submit_manuscript')) + # Send emails + SubmissionUtils.load({'submission': submission}) + SubmissionUtils.send_authors_submission_ack_email() + + context = {'ack_header': 'Thank you for your Submission to SciPost', + 'ack_message': 'Your Submission will soon be handled by an Editor. ', + 'followup_message': 'Return to your ', + 'followup_link': reverse('scipost:personal_page'), + 'followup_link_label': 'personal page'} + return render(self.request, 'scipost/acknowledgement.html', context) + + def mark_previous_submissions_as_deprecated(self, previous_submissions): + for sub in previous_submissions: + sub.is_current = False + sub.open_for_reporting = False + sub.status = 'resubmitted' + sub.save() + def previous_submissions(self, form): + return Submission.objects.filter( + arxiv_identifier_wo_vn_nr=form.cleaned_data['arxiv_identifier_wo_vn_nr'] + ) -@login_required -@permission_required('scipost.can_submit_manuscript', raise_exception=True) -@transaction.atomic -def submit_manuscript(request): - if request.method == 'POST': - form = SubmissionForm(request.POST) - if form.is_valid(): - submitted_by = Contributor.objects.get(user=request.user) - # Verify if submitter is among the authors - if not submitted_by.user.last_name in form.cleaned_data['author_list']: - errormessage = ('Your name does not match that of any of the authors. ' - 'You are not authorized to submit this preprint.') - identifierform = SubmissionIdentifierForm() - return render(request, 'submissions/submit_manuscript.html', - {'identifierform': identifierform, 'form': form, - 'errormessage': errormessage}) - submission = Submission( - is_current = True, - is_resubmission = form.cleaned_data['is_resubmission'], - submitted_by = submitted_by, - submitted_to_journal = form.cleaned_data['submitted_to_journal'], - submission_type = form.cleaned_data['submission_type'], - discipline = form.cleaned_data['discipline'], - domain = form.cleaned_data['domain'], -# specialization = form.cleaned_data['specialization'], - subject_area = form.cleaned_data['subject_area'], - secondary_areas = form.cleaned_data['secondary_areas'], - status = 'unassigned', - title = form.cleaned_data['title'], - author_list = form.cleaned_data['author_list'], - abstract = form.cleaned_data['abstract'], - arxiv_identifier_w_vn_nr = form.cleaned_data['arxiv_identifier_w_vn_nr'], - arxiv_identifier_wo_vn_nr = form.cleaned_data['arxiv_identifier_wo_vn_nr'], - arxiv_vn_nr = form.cleaned_data['arxiv_vn_nr'], - arxiv_link = form.cleaned_data['arxiv_link'], - metadata = form.cleaned_data['metadata'], - submission_date = timezone.now(), - remarks_for_editors = form.cleaned_data['remarks_for_editors'], - referees_suggested = form.cleaned_data['referees_suggested'], - referees_flagged = form.cleaned_data['referees_flagged'], - ) - submission.save() - submission.authors.add(submitted_by) # must be author to be able to submit - submission.save() - # If this is a resubmission, mark previous submissions as deprecated: - if form.cleaned_data['is_resubmission']: - previous_submissions = Submission.objects.filter( - arxiv_identifier_wo_vn_nr=form.cleaned_data['arxiv_identifier_wo_vn_nr'] - ).exclude(pk=submission.id).order_by('-arxiv_vn_nr') - for sub in previous_submissions: - sub.is_current = False - sub.open_for_reporting = False - sub.status = 'resubmitted' - sub.save() - # Handle this submission in same way as if assignment had been accepted - submission.open_for_reporting = True - deadline = timezone.now() + datetime.timedelta(days=28) # for papers - if submission.submitted_to_journal == 'SciPost Physics Lecture Notes': - deadline += datetime.timedelta(days=28) - submission.reporting_deadline = deadline - submission.open_for_commenting = True - submission.latest_activity = timezone.now() - # We keep the same (most recent) Editor-in-charge by default - submission.editor_in_charge = previous_submissions[0].editor_in_charge - submission.status = 'EICassigned' - # Keep the info about authors: - for author in previous_submissions[0].authors.all(): - submission.authors.add(author) - for author in previous_submissions[0].authors_claims.all(): - submission.authors_claims.add(author) - for author in previous_submissions[0].authors_false_claims.all(): - submission.authors_false_claims.add(author) - submission.author_comments = form.cleaned_data['author_comments'] - submission.list_of_changes = form.cleaned_data['list_of_changes'] - submission.save() - assignment = EditorialAssignment( - submission=submission, - to=submission.editor_in_charge, - accepted=True, - date_created=timezone.now(), - date_answered=timezone.now(), - ) - assignment.save() - SubmissionUtils.load({'submission': submission}) - SubmissionUtils.send_authors_resubmission_ack_email() - assign_perm('can_take_editorial_actions', submission.editor_in_charge.user, submission) - ed_admins = Group.objects.get(name='Editorial Administrators') - assign_perm('can_take_editorial_actions', ed_admins, submission) - SubmissionUtils.send_EIC_reappointment_email() - else: - SubmissionUtils.load({'submission': submission}) - SubmissionUtils.send_authors_submission_ack_email() - - #return HttpResponseRedirect(reverse('submissions:submit_manuscript_ack')) - context = {'ack_header': 'Thank you for your Submission to SciPost', - 'ack_message': 'Your Submission will soon be handled by an Editor. ', - 'followup_message': 'Return to your ', - 'followup_link': reverse('scipost:personal_page'), - 'followup_link_label': 'personal page'} - return render(request, 'scipost/acknowledgement.html', context) - else: # form is invalid - pass - else: - form = SubmissionForm() - identifierform = SubmissionIdentifierForm() - return render(request, 'submissions/submit_manuscript.html', - {'identifierform': identifierform, 'form': form}) + +# @login_required +# @permission_required('scipost.can_submit_manuscript', raise_exception=True) +# @transaction.atomic +# def submit_manuscript(request): +# if request.method == 'POST': +# form = SubmissionForm(request.POST) +# if form.is_valid(): +# submitted_by = Contributor.objects.get(user=request.user) +# # Verify if submitter is among the authors +# if submitted_by.user.last_name not in form.cleaned_data['author_list']: +# errormessage = ('Your name does not match that of any of the authors. ' +# 'You are not authorized to submit this preprint.') +# identifierform = SubmissionIdentifierForm() +# return render(request, 'submissions/submit_manuscript.html', +# {'identifierform': identifierform, 'form': form, +# 'errormessage': errormessage}) +# submission = Submission( +# is_current=True, +# is_resubmission=form.cleaned_data['is_resubmission'], +# submitted_by=submitted_by, +# submitted_to_journal=form.cleaned_data['submitted_to_journal'], +# submission_type=form.cleaned_data['submission_type'], +# discipline=form.cleaned_data['discipline'], +# domain=form.cleaned_data['domain'], +# subject_area=form.cleaned_data['subject_area'], +# secondary_areas=form.cleaned_data['secondary_areas'], +# status='unassigned', +# title=form.cleaned_data['title'], +# author_list=form.cleaned_data['author_list'], +# abstract=form.cleaned_data['abstract'], +# arxiv_identifier_w_vn_nr=form.cleaned_data['arxiv_identifier_w_vn_nr'], +# arxiv_identifier_wo_vn_nr=form.cleaned_data['arxiv_identifier_wo_vn_nr'], +# arxiv_vn_nr=form.cleaned_data['arxiv_vn_nr'], +# arxiv_link=form.cleaned_data['arxiv_link'], +# metadata=form.cleaned_data['metadata'], +# submission_date=timezone.now(), +# remarks_for_editors=form.cleaned_data['remarks_for_editors'], +# referees_suggested=form.cleaned_data['referees_suggested'], +# referees_flagged=form.cleaned_data['referees_flagged'], +# ) +# submission.save() +# submission.authors.add(submitted_by) # must be author to be able to submit +# submission.save() +# # If this is a resubmission, mark previous submissions as deprecated: +# if form.cleaned_data['is_resubmission']: +# previous_submissions = Submission.objects.filter( +# arxiv_identifier_wo_vn_nr=form.cleaned_data['arxiv_identifier_wo_vn_nr'] +# ).exclude(pk=submission.id).order_by('-arxiv_vn_nr') +# for sub in previous_submissions: +# sub.is_current = False +# sub.open_for_reporting = False +# sub.status = 'resubmitted' +# sub.save() +# # Handle this submission in same way as if assignment had been accepted +# submission.open_for_reporting = True +# deadline = timezone.now() + datetime.timedelta(days=28) # for papers +# if submission.submitted_to_journal == 'SciPost Physics Lecture Notes': +# deadline += datetime.timedelta(days=28) +# submission.reporting_deadline = deadline +# submission.open_for_commenting = True +# submission.latest_activity = timezone.now() +# # We keep the same (most recent) Editor-in-charge by default +# submission.editor_in_charge = previous_submissions[0].editor_in_charge +# submission.status = 'EICassigned' +# # Keep the info about authors: +# for author in previous_submissions[0].authors.all(): +# submission.authors.add(author) +# for author in previous_submissions[0].authors_claims.all(): +# submission.authors_claims.add(author) +# for author in previous_submissions[0].authors_false_claims.all(): +# submission.authors_false_claims.add(author) +# submission.author_comments = form.cleaned_data['author_comments'] +# submission.list_of_changes = form.cleaned_data['list_of_changes'] +# submission.save() +# assignment = EditorialAssignment( +# submission=submission, +# to=submission.editor_in_charge, +# accepted=True, +# date_created=timezone.now(), +# date_answered=timezone.now(), +# ) +# assignment.save() +# SubmissionUtils.load({'submission': submission}) +# SubmissionUtils.send_authors_resubmission_ack_email() +# assign_perm('can_take_editorial_actions', submission.editor_in_charge.user, submission) +# ed_admins = Group.objects.get(name='Editorial Administrators') +# assign_perm('can_take_editorial_actions', ed_admins, submission) +# SubmissionUtils.send_EIC_reappointment_email() +# else: +# SubmissionUtils.load({'submission': submission}) +# SubmissionUtils.send_authors_submission_ack_email() +# +# # return HttpResponseRedirect(reverse('submissions:submit_manuscript_ack')) +# context = {'ack_header': 'Thank you for your Submission to SciPost', +# 'ack_message': 'Your Submission will soon be handled by an Editor. ', +# 'followup_message': 'Return to your ', +# 'followup_link': reverse('scipost:personal_page'), +# 'followup_link_label': 'personal page'} +# return render(request, 'scipost/acknowledgement.html', context) +# else: # form is invalid +# pass +# else: +# form = SubmissionForm() +# identifierform = SubmissionIdentifierForm() +# return render(request, 'submissions/submit_manuscript.html', +# {'identifierform': identifierform, 'form': form}) def submissions(request, to_journal=None): @@ -274,8 +425,8 @@ def submissions(request, to_journal=None): title__icontains=form.cleaned_data['title_keyword'], author_list__icontains=form.cleaned_data['author'], abstract__icontains=form.cleaned_data['abstract_keyword'], - ).exclude(status__in=SUBMISSION_STATUS_PUBLICLY_UNLISTED, - ).order_by('-submission_date') + ).exclude(status__in=SUBMISSION_STATUS_PUBLICLY_UNLISTED, + ).order_by('-submission_date') else: submission_search_list = [] @@ -286,12 +437,12 @@ def submissions(request, to_journal=None): submission_recent_list = Submission.objects.filter( latest_activity__gte=timezone.now() + datetime.timedelta(days=-60) ).exclude(status__in=SUBMISSION_STATUS_PUBLICLY_UNLISTED - ).exclude(is_current=False).order_by('-submission_date') + ).exclude(is_current=False).order_by('-submission_date') # If doing a journal-specific listing: if to_journal is not None: submission_recent_list.filter(submitted_to_journal=to_journal) context = {'form': form, 'submission_search_list': submission_search_list, - 'submission_recent_list': submission_recent_list } + 'submission_recent_list': submission_recent_list} return render(request, 'submissions/submissions.html', context) @@ -303,21 +454,21 @@ def browse(request, discipline, nrweeksback): title__icontains=form.cleaned_data['title_keyword'], author_list__icontains=form.cleaned_data['author'], abstract__icontains=form.cleaned_data['abstract_keyword'], - ).exclude(status__in=SUBMISSION_STATUS_PUBLICLY_UNLISTED, - ).order_by('-submission_date') + ).exclude(status__in=SUBMISSION_STATUS_PUBLICLY_UNLISTED, + ).order_by('-submission_date') else: submission_search_list = [] - context = {'form': form, 'submission_search_list': submission_search_list } + context = {'form': form, 'submission_search_list': submission_search_list} return HttpResponseRedirect(request, 'submissions/submissions.html', context) else: form = SubmissionSearchForm() submission_browse_list = Submission.objects.filter( discipline=discipline, latest_activity__gte=timezone.now() + datetime.timedelta(weeks=-int(nrweeksback)) - ).exclude(status__in=SUBMISSION_STATUS_PUBLICLY_UNLISTED - ).exclude(is_current=False).order_by('-submission_date') + ).exclude(status__in=SUBMISSION_STATUS_PUBLICLY_UNLISTED + ).exclude(is_current=False).order_by('-submission_date') context = {'form': form, 'discipline': discipline, 'nrweeksback': nrweeksback, - 'submission_browse_list': submission_browse_list } + 'submission_browse_list': submission_browse_list} return render(request, 'submissions/submissions.html', context) @@ -337,8 +488,7 @@ def submission_detail(request, arxiv_identifier_w_vn_nr): and not request.user.groups.filter(name='SciPost Administrators').exists() and not request.user.groups.filter(name='Editorial Administrators').exists() and not request.user.groups.filter(name='Editorial College').exists() - and not is_author - ): + and not is_author): raise PermissionDenied other_versions = Submission.objects.filter( arxiv_identifier_wo_vn_nr=submission.arxiv_identifier_wo_vn_nr @@ -348,26 +498,24 @@ def submission_detail(request, arxiv_identifier_w_vn_nr): form = CommentForm(request.POST) if form.is_valid(): author = Contributor.objects.get(user=request.user) - newcomment = Comment ( - submission = submission, - author = author, - is_rem = form.cleaned_data['is_rem'], - is_que = form.cleaned_data['is_que'], - is_ans = form.cleaned_data['is_ans'], - is_obj = form.cleaned_data['is_obj'], - is_rep = form.cleaned_data['is_rep'], - is_val = form.cleaned_data['is_val'], - is_lit = form.cleaned_data['is_lit'], - is_sug = form.cleaned_data['is_sug'], - comment_text = form.cleaned_data['comment_text'], - remarks_for_editors = form.cleaned_data['remarks_for_editors'], - date_submitted = timezone.now(), - ) + newcomment = Comment( + submission=submission, + author=author, + is_rem=form.cleaned_data['is_rem'], + is_que=form.cleaned_data['is_que'], + is_ans=form.cleaned_data['is_ans'], + is_obj=form.cleaned_data['is_obj'], + is_rep=form.cleaned_data['is_rep'], + is_val=form.cleaned_data['is_val'], + is_lit=form.cleaned_data['is_lit'], + is_sug=form.cleaned_data['is_sug'], + comment_text=form.cleaned_data['comment_text'], + remarks_for_editors=form.cleaned_data['remarks_for_editors'], + date_submitted=timezone.now(), + ) newcomment.save() author.nr_comments = Comment.objects.filter(author=author).count() author.save() - #request.session['arxiv_identifier_w_vn_nr'] = submission.arxiv_identifier_w_vn_nr - #return HttpResponseRedirect(reverse('comments:comment_submission_ack')) context = {'ack_header': 'Thank you for contributing a Comment.', 'ack_message': 'It will soon be vetted by an Editor.', 'followup_message': 'Back to the ', @@ -376,7 +524,7 @@ def submission_detail(request, arxiv_identifier_w_vn_nr): kwargs={'arxiv_identifier_w_vn_nr': newcomment.submission.arxiv_identifier_w_vn_nr} ), 'followup_link_label': ' Submission page you came from' - } + } return render(request, 'scipost/acknowledgement.html', context) else: form = CommentForm() @@ -391,9 +539,9 @@ def submission_detail(request, arxiv_identifier_w_vn_nr): # To check in template whether the user can submit a report: try: is_author = request.user.contributor in submission.authors.all() - is_author_unchecked = (not is_author - and not (request.user.contributor in submission.authors_false_claims.all()) - and (request.user.last_name in submission.author_list)) + is_author_unchecked = (not is_author and not + (request.user.contributor in submission.authors_false_claims.all()) and + (request.user.last_name in submission.author_list)) except AttributeError: is_author = False is_author_unchecked = False @@ -436,10 +584,10 @@ def pool(request): to publication acceptance or rejection. All members of the Editorial College have access. """ - submissions_in_pool=(Submission.objects.all() - .exclude(status__in=SUBMISSION_STATUS_OUT_OF_POOL) - .exclude(is_current=False) - .order_by('-submission_date')) + submissions_in_pool = (Submission.objects.all() + .exclude(status__in=SUBMISSION_STATUS_OUT_OF_POOL) + .exclude(is_current=False) + .order_by('-submission_date')) recommendations_undergoing_voting = (EICRecommendation.objects.filter( submission__status__in=['put_to_EC_voting'])) recommendations_to_prepare_for_voting = (EICRecommendation.objects.filter( @@ -458,16 +606,30 @@ def pool(request): rec_vote_form = RecommendationVoteForm() remark_form = RemarkForm() context = {'submissions_in_pool': submissions_in_pool, + 'submission_status': SUBMISSION_STATUS, 'recommendations_undergoing_voting': recommendations_undergoing_voting, 'recommendations_to_prepare_for_voting': recommendations_to_prepare_for_voting, 'assignments_to_consider': assignments_to_consider, 'consider_assignment_form': consider_assignment_form, 'recs_to_vote_on': recs_to_vote_on, 'rec_vote_form': rec_vote_form, - 'remark_form': remark_form,} + 'remark_form': remark_form, } return render(request, 'submissions/pool.html', context) +@login_required +@permission_required('scipost.can_view_pool', raise_exception=True) +def submissions_by_status(request, status): + if status not in submission_status_dict.keys(): + errormessage = 'Unknown status.' + return render(request, 'scipost/error.html', {'errormessage': errormessage}) + submissions_of_status = Submission.objects.filter( + status=status).order_by('-submission_date') + context = {'status': submission_status_dict[status], + 'submissions_of_status': submissions_of_status, } + return render(request, 'submissions/submissions_by_status.html', context) + + @login_required @permission_required('scipost.can_view_pool', raise_exception=True) def add_remark(request, arxiv_identifier_w_vn_nr): @@ -494,13 +656,11 @@ def add_remark(request, arxiv_identifier_w_vn_nr): return render(request, 'scipost/error.html', {'errormessage': errormessage}) - @login_required @permission_required('scipost.can_assign_submissions', raise_exception=True) def assign_submission(request, arxiv_identifier_w_vn_nr): submission_to_assign = get_object_or_404(Submission, arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) - #form = AssignSubmissionForm(discipline=submission_to_assign.discipline, subject_area=submission_to_assign.subject_area) # reactivate later on form = AssignSubmissionForm(discipline=submission_to_assign.discipline) context = {'submission_to_assign': submission_to_assign, 'form': form} @@ -540,7 +700,7 @@ def assign_submission_ack(request, arxiv_identifier_w_vn_nr): @transaction.atomic def accept_or_decline_assignment_ack(request, assignment_id): contributor = Contributor.objects.get(user=request.user) - assignment = get_object_or_404 (EditorialAssignment, pk=assignment_id) + assignment = get_object_or_404(EditorialAssignment, pk=assignment_id) errormessage = None if assignment.submission.status == 'assignment_failed': errormessage = 'This Submission has failed pre-screening and has been rejected.' @@ -562,7 +722,7 @@ def accept_or_decline_assignment_ack(request, assignment_id): assignment.submission.status = 'EICassigned' assignment.submission.editor_in_charge = contributor assignment.submission.open_for_reporting = True - deadline = timezone.now() + datetime.timedelta(days=28) # for papers + deadline = timezone.now() + datetime.timedelta(days=28) # for papers if assignment.submission.submitted_to_journal == 'SciPost Physics Lecture Notes': deadline += datetime.timedelta(days=28) assignment.submission.reporting_deadline = deadline @@ -613,7 +773,7 @@ def volunteer_as_EIC(request, arxiv_identifier_w_vn_nr): accepted=True, date_created=timezone.now(), date_answered=timezone.now()) - deadline = timezone.now() + datetime.timedelta(days=28) # for papers + deadline = timezone.now() + datetime.timedelta(days=28) # for papers if submission.submitted_to_journal == 'SciPost Physics Lecture Notes': deadline += datetime.timedelta(days=28) submission.status = 'EICassigned' @@ -671,6 +831,29 @@ def assignment_failed(request, arxiv_identifier_w_vn_nr): return render(request, 'submissions/assignment_failed.html', context) +@login_required +@permission_required('scipost.can_take_charge_of_submissions', raise_exception=True) +def assignments(request): + """ + This page provides a Fellow with an explicit task list + of editorial actions which should be undertaken. + """ + assignments = EditorialAssignment.objects.filter( + to=request.user.contributor).order_by('-date_created') + assignments_to_consider = assignments.filter(accepted=None, + deprecated=False) + current_assignments = assignments.filter(accepted=True, + deprecated=False, + completed=False) + consider_assignment_form = ConsiderAssignmentForm() + context = { + 'assignments_to_consider': assignments_to_consider, + 'consider_assignment_form': consider_assignment_form, + 'current_assignments': current_assignments, + } + return render(request, 'submissions/assignments.html', context) + + @login_required @permission_required_or_403('can_take_editorial_actions', (Submission, 'arxiv_identifier_w_vn_nr', 'arxiv_identifier_w_vn_nr')) @@ -760,25 +943,25 @@ def recruit_referee(request, arxiv_identifier_w_vn_nr): last_name=ref_recruit_form.cleaned_data['last_name'], email_address=ref_recruit_form.cleaned_data['email_address'], date_invited=timezone.now(), - invited_by = request.user.contributor) + invited_by=request.user.contributor) ref_invitation.save() # Create and send a registration invitation ref_inv_message_head = ('On behalf of the Editor-in-charge ' + title_dict[submission.editor_in_charge.title] + ' ' + submission.editor_in_charge.user.last_name + - ', we would like to invite you to referee a Submission to ' - + journals_submit_dict[submission.submitted_to_journal] - + ', namely\n\n' + submission.title - + '\nby ' + submission.author_list + '.') - reg_invitation = RegistrationInvitation ( - title = ref_recruit_form.cleaned_data['title'], - first_name = ref_recruit_form.cleaned_data['first_name'], - last_name = ref_recruit_form.cleaned_data['last_name'], - email = ref_recruit_form.cleaned_data['email_address'], - invitation_type = 'R', - invited_by = request.user.contributor, - message_style = 'F', - personal_message = ref_inv_message_head, + ', we would like to invite you to referee a Submission to ' + + journals_submit_dict[submission.submitted_to_journal] + + ', namely\n\n' + submission.title + + '\nby ' + submission.author_list + '.') + reg_invitation = RegistrationInvitation( + title=ref_recruit_form.cleaned_data['title'], + first_name=ref_recruit_form.cleaned_data['first_name'], + last_name=ref_recruit_form.cleaned_data['last_name'], + email=ref_recruit_form.cleaned_data['email_address'], + invitation_type='R', + invited_by=request.user.contributor, + message_style='F', + personal_message=ref_inv_message_head, ) reg_invitation.save() Utils.load({'invitation': reg_invitation}) @@ -834,7 +1017,7 @@ def ref_invitation_reminder(request, arxiv_identifier_w_vn_nr, invitation_id): when a referee has been invited but hasn't answered yet. It can be used for registered as well as unregistered referees. """ - invitation = get_object_or_404 (RefereeInvitation, pk=invitation_id) + invitation = get_object_or_404(RefereeInvitation, pk=invitation_id) invitation.nr_reminders += 1 invitation.date_last_reminded = timezone.now() invitation.save() @@ -857,8 +1040,7 @@ def accept_or_decline_ref_invitations(request): @login_required @permission_required('scipost.can_referee', raise_exception=True) def accept_or_decline_ref_invitation_ack(request, invitation_id): - contributor = Contributor.objects.get(user=request.user) - invitation = get_object_or_404 (RefereeInvitation, pk=invitation_id) + invitation = get_object_or_404(RefereeInvitation, pk=invitation_id) if request.method == 'POST': form = ConsiderRefereeInvitationForm(request.POST) if form.is_valid(): @@ -876,7 +1058,6 @@ def accept_or_decline_ref_invitation_ack(request, invitation_id): return render(request, 'submissions/accept_or_decline_ref_invitation_ack.html', context) - @login_required @permission_required_or_403('can_take_editorial_actions', (Submission, 'arxiv_identifier_w_vn_nr', 'arxiv_identifier_w_vn_nr')) @@ -886,8 +1067,8 @@ def cancel_ref_invitation(request, arxiv_identifier_w_vn_nr, invitation_id): to remove a referee for the list of invited ones. It can be used for registered as well as unregistered referees. """ - invitation = get_object_or_404 (RefereeInvitation, pk=invitation_id) - invitation.cancelled=True + invitation = get_object_or_404(RefereeInvitation, pk=invitation_id) + invitation.cancelled = True invitation.save() SubmissionUtils.load({'invitation': invitation}) SubmissionUtils.send_ref_cancellation_email() @@ -899,7 +1080,7 @@ def cancel_ref_invitation(request, arxiv_identifier_w_vn_nr, invitation_id): @permission_required_or_403('can_take_editorial_actions', (Submission, 'arxiv_identifier_w_vn_nr', 'arxiv_identifier_w_vn_nr')) def extend_refereeing_deadline(request, arxiv_identifier_w_vn_nr, days): - submission = get_object_or_404 (Submission, arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) + submission = get_object_or_404(Submission, arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) submission.reporting_deadline += datetime.timedelta(days=int(days)) submission.open_for_reporting = True submission.open_for_commenting = True @@ -914,7 +1095,7 @@ def extend_refereeing_deadline(request, arxiv_identifier_w_vn_nr, days): @permission_required_or_403('can_take_editorial_actions', (Submission, 'arxiv_identifier_w_vn_nr', 'arxiv_identifier_w_vn_nr')) def set_refereeing_deadline(request, arxiv_identifier_w_vn_nr): - submission = get_object_or_404 (Submission, arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) + submission = get_object_or_404(Submission, arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) if request.method == 'POST': form = SetRefereeingDeadlineForm(request.POST) if form.is_valid(): @@ -950,10 +1131,10 @@ def close_refereeing_round(request, arxiv_identifier_w_vn_nr): round off any replies to reports or comments before the editorial recommendation is formulated. """ - submission = get_object_or_404 (Submission, arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) + submission = get_object_or_404(Submission, arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) submission.open_for_reporting = False submission.open_for_commenting = False - if submission.status == 'EICassigned': # only close if currently undergoing refereeing + if submission.status == 'EICassigned': # only close if currently undergoing refereeing submission.status = 'review_closed' submission.reporting_deadline = timezone.now() submission.latest_activity = timezone.now() @@ -968,9 +1149,9 @@ def communication(request, arxiv_identifier_w_vn_nr, comtype, referee_id=None): Communication between editor-in-charge, author or referee occurring during the submission refereeing. """ - submission = get_object_or_404 (Submission, arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) + submission = get_object_or_404(Submission, arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) errormessage = None - if not comtype in ed_comm_choices_dict.keys(): + if comtype not in ed_comm_choices_dict.keys(): errormessage = 'Unknown type of cummunication.' # TODO: Verify that this is requested by an authorized contributor (eic, ref, author) elif (comtype in ['EtoA', 'EtoR', 'EtoS'] and @@ -1021,37 +1202,36 @@ def communication(request, arxiv_identifier_w_vn_nr, comtype, referee_id=None): (Submission, 'arxiv_identifier_w_vn_nr', 'arxiv_identifier_w_vn_nr')) @transaction.atomic def eic_recommendation(request, arxiv_identifier_w_vn_nr): - submission = get_object_or_404 (Submission, arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) + submission = get_object_or_404(Submission, arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) if submission.status not in ['EICassigned', 'review_closed']: - errormessage = ('This submission\'s current status is: ' - + submission_status_dict[submission.status] + '. ' + errormessage = ('This submission\'s current status is: ' + + submission_status_dict[submission.status] + '. ' 'An Editorial Recommendation is not required.') return render(request, 'scipost/error.html', {'errormessage': errormessage}) if request.method == 'POST': form = EICRecommendationForm(request.POST) if form.is_valid(): - #recommendation = form.save() recommendation = EICRecommendation( - submission = submission, - date_submitted = timezone.now(), - remarks_for_authors = form.cleaned_data['remarks_for_authors'], - requested_changes = form.cleaned_data['requested_changes'], - remarks_for_editorial_college = form.cleaned_data['remarks_for_editorial_college'], - recommendation = form.cleaned_data['recommendation'], - voting_deadline = timezone.now() + datetime.timedelta(days=7), + submission=submission, + date_submitted=timezone.now(), + remarks_for_authors=form.cleaned_data['remarks_for_authors'], + requested_changes=form.cleaned_data['requested_changes'], + remarks_for_editorial_college=form.cleaned_data['remarks_for_editorial_college'], + recommendation=form.cleaned_data['recommendation'], + voting_deadline=timezone.now() + datetime.timedelta(days=7), ) recommendation.save() # If recommendation is to accept or reject, # it is forwarded to the Editorial College for voting # If it is to carry out minor or major revisions, # it is returned to the Author who is asked to resubmit - if (recommendation.recommendation == 1 - or recommendation.recommendation == 2 - or recommendation.recommendation == 3 - or recommendation.recommendation == -3): + if (recommendation.recommendation == 1 or + recommendation.recommendation == 2 or + recommendation.recommendation == 3 or + recommendation.recommendation == -3): submission.status = 'voting_in_preparation' - elif (recommendation.recommendation == -1 - or recommendation.recommendation == -2): + elif (recommendation.recommendation == -1 or + recommendation.recommendation == -2): submission.status = 'revision_requested' SubmissionUtils.load({'submission': submission, 'recommendation': recommendation}) @@ -1081,12 +1261,12 @@ def eic_recommendation(request, arxiv_identifier_w_vn_nr): @permission_required('scipost.can_referee', raise_exception=True) @transaction.atomic def submit_report(request, arxiv_identifier_w_vn_nr): - submission = get_object_or_404 (Submission, arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) + submission = get_object_or_404(Submission, arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) # Check whether the user can submit a report: is_author = request.user.contributor in submission.authors.all() - is_author_unchecked = (not is_author - and not (request.user.contributor in submission.authors_false_claims.all()) - and (request.user.last_name in submission.author_list)) + is_author_unchecked = (not is_author and not + (request.user.contributor in submission.authors_false_claims.all()) and + (request.user.last_name in submission.author_list)) invited = RefereeInvitation.objects.filter(submission=submission, referee=request.user.contributor).exists() errormessage = None @@ -1115,27 +1295,27 @@ def submit_report(request, arxiv_identifier_w_vn_nr): if submission.referees_flagged is not None: if author.user.last_name in submission.referees_flagged: flagged = True - newreport = Report ( - submission = submission, - author = author, - invited = invited, - flagged = flagged, - qualification = form.cleaned_data['qualification'], - strengths = form.cleaned_data['strengths'], - weaknesses = form.cleaned_data['weaknesses'], - report = form.cleaned_data['report'], - requested_changes = form.cleaned_data['requested_changes'], - validity = form.cleaned_data['validity'], - significance = form.cleaned_data['significance'], - originality = form.cleaned_data['originality'], - clarity = form.cleaned_data['clarity'], - formatting = form.cleaned_data['formatting'], - grammar = form.cleaned_data['grammar'], - recommendation = form.cleaned_data['recommendation'], - remarks_for_editors = form.cleaned_data['remarks_for_editors'], - anonymous = form.cleaned_data['anonymous'], - date_submitted = timezone.now(), - ) + newreport = Report( + submission=submission, + author=author, + invited=invited, + flagged=flagged, + qualification=form.cleaned_data['qualification'], + strengths=form.cleaned_data['strengths'], + weaknesses=form.cleaned_data['weaknesses'], + report=form.cleaned_data['report'], + requested_changes=form.cleaned_data['requested_changes'], + validity=form.cleaned_data['validity'], + significance=form.cleaned_data['significance'], + originality=form.cleaned_data['originality'], + clarity=form.cleaned_data['clarity'], + formatting=form.cleaned_data['formatting'], + grammar=form.cleaned_data['grammar'], + recommendation=form.cleaned_data['recommendation'], + remarks_for_editors=form.cleaned_data['remarks_for_editors'], + anonymous=form.cleaned_data['anonymous'], + date_submitted=timezone.now(), + ) newreport.save() author.nr_reports = Report.objects.filter(author=author).count() author.save() @@ -1146,7 +1326,7 @@ def submit_report(request, arxiv_identifier_w_vn_nr): else: form = ReportForm() - context = {'submission': submission, 'form': form } + context = {'submission': submission, 'form': form} return render(request, 'submissions/submit_report.html', context) @@ -1157,7 +1337,7 @@ def vet_submitted_reports(request): report_to_vet = Report.objects.filter(status=0, submission__editor_in_charge=contributor).first() form = VetReportForm() - context = {'contributor': contributor, 'report_to_vet': report_to_vet, 'form': form } + context = {'contributor': contributor, 'report_to_vet': report_to_vet, 'form': form} return(render(request, 'submissions/vet_submitted_reports.html', context)) @@ -1182,11 +1362,9 @@ def vet_submitted_report_ack(request, report_id): # email report author SubmissionUtils.load({'report': report, 'email_response': form.cleaned_data['email_response_field']}) - SubmissionUtils.acknowledge_report_email() # email report author, bcc EIC + SubmissionUtils.acknowledge_report_email() # email report author, bcc EIC if report.status == 1: SubmissionUtils.send_author_report_received_email() - #context = {'submission': report.submission} - #return render(request, 'submissions/vet_submitted_report_ack.html', context) context = {'ack_header': 'Submitted Report vetted.', 'followup_message': 'Return to the ', 'followup_link': reverse('submissions:editorial_page', @@ -1213,10 +1391,10 @@ def prepare_for_voting(request, rec_id): recommendation.eligible_to_vote = eligibility_form.cleaned_data['eligible_Fellows'] recommendation.voted_for.add(recommendation.submission.editor_in_charge) recommendation.save() - recommendation.submission.status='put_to_EC_voting' + recommendation.submission.status = 'put_to_EC_voting' recommendation.submission.save() - return render (request, 'scipost/acknowledgement.html', - context={'ack_message': 'We have registered your selection.'}) + return render(request, 'scipost/acknowledgement.html', + context={'ack_message': 'We have registered your selection.'}) else: # Identify possible co-authorships in last 3 years, disqualifying Fellow from voting: if recommendation.submission.metadata is not None: @@ -1280,6 +1458,37 @@ def vote_on_rec(request, rec_id): return redirect(reverse('submissions:pool')) +@permission_required('scipost.can_prepare_recommendations_for_voting', raise_exception=True) +def remind_Fellows_to_vote(request): + """ + This method sends an email to all Fellow with pending voting duties. + It must be called by and Editorial Administrator. + """ + recommendations_undergoing_voting = (EICRecommendation.objects.filter( + submission__status__in=['put_to_EC_voting'])) + Fellow_emails = [] + Fellow_names = [] + for rec in recommendations_undergoing_voting: + for Fellow in rec.eligible_to_vote.all(): + if (Fellow not in rec.voted_for.all() + and Fellow not in rec.voted_against.all() + and Fellow not in rec.voted_abstain.all() + and Fellow.user.email not in Fellow_emails): + Fellow_emails.append(Fellow.user.email) + Fellow_names.append(str(Fellow)) + SubmissionUtils.load({'Fellow_emails': Fellow_emails}) + SubmissionUtils.send_Fellows_voting_reminder_email() + ack_message = 'Email reminders have been sent to: <ul>' + for name in sorted(Fellow_names): + ack_message += '<li>' + name + '</li>' + ack_message += '</ul>' + context = {'ack_message': Template(ack_message).render(Context({})), + 'followup_message': 'Return to the ', + 'followup_link': reverse('submissions:pool'), + 'followup_link_label': 'Submissions pool'} + return render(request, 'scipost/acknowledgement.html', context) + + @permission_required('scipost.can_fix_College_decision', raise_exception=True) @transaction.atomic def fix_College_decision(request, rec_id): @@ -1288,18 +1497,18 @@ def fix_College_decision(request, rec_id): Called by an Editorial Administrator. """ recommendation = get_object_or_404(EICRecommendation, pk=rec_id) - if recommendation.recommendation==1: + if recommendation.recommendation == 1: # Publish as Tier I (top 10%) - recommendation.submission.status='accepted' - elif recommendation.recommendation==2: + recommendation.submission.status = 'accepted' + elif recommendation.recommendation == 2: # Publish as Tier II (top 50%) - recommendation.submission.status='accepted' - elif recommendation.recommendation==3: + recommendation.submission.status = 'accepted' + elif recommendation.recommendation == 3: # Publish as Tier III (meets criteria) - recommendation.submission.status='accepted' - elif recommendation.recommendation==-3: + recommendation.submission.status = 'accepted' + elif recommendation.recommendation == -3: # Reject - recommendation.submission.status='rejected' + recommendation.submission.status = 'rejected' previous_submissions = Submission.objects.filter( arxiv_identifier_wo_vn_nr=recommendation.submission.arxiv_identifier_wo_vn_nr ).exclude(pk=recommendation.submission.id) @@ -1312,5 +1521,5 @@ def fix_College_decision(request, rec_id): 'recommendation': recommendation}) SubmissionUtils.send_author_College_decision_email() ack_message = 'The Editorial College\'s decision has been fixed.' - return render (request, 'scipost/acknowledgement.html', - context={'ack_message': ack_message}) + return render(request, 'scipost/acknowledgement.html', + context={'ack_message': ack_message}) diff --git a/theses/factories.py b/theses/factories.py index bc57863b350e82579b8725bd27884bdeb1382c24..550d5a73e004b69b8e40ac52ed2dc4618e9309c9 100644 --- a/theses/factories.py +++ b/theses/factories.py @@ -18,7 +18,7 @@ class ThesisLinkFactory(factory.django.DjangoModelFactory): author = factory.Faker('name') supervisor = factory.Faker('name') institution = factory.Faker('company') - defense_date = factory.Faker('date_time_this_century') + defense_date = factory.Faker('date') abstract = factory.Faker('text') domain = 'ET' @@ -28,4 +28,3 @@ class VetThesisLinkFormFactory(FormFactory): model = VetThesisLinkForm action_option = VetThesisLinkForm.ACCEPT - # justification = factory.Faker('lorem') diff --git a/theses/forms.py b/theses/forms.py index 05963cb3221ca5cadcc553f653090d316395470b..ae40b907a28b886f25f9f6dc9b593e5755b46cec 100644 --- a/theses/forms.py +++ b/theses/forms.py @@ -1,8 +1,11 @@ from django import forms +from django.core.mail import EmailMessage -from .models import * +from .models import ThesisLink from .helpers import past_years +from scipost.models import Contributor, title_dict + class RequestThesisLinkForm(forms.ModelForm): class Meta: @@ -16,7 +19,7 @@ class RequestThesisLinkForm(forms.ModelForm): } -class VetThesisLinkForm(forms.Form): +class VetThesisLinkForm(RequestThesisLinkForm): MODIFY = 0 ACCEPT = 1 REFUSE = 2 @@ -41,10 +44,66 @@ class VetThesisLinkForm(forms.Form): justification = forms.CharField(widget=forms.Textarea( attrs={'rows': 5, 'cols': 40}), label='Justification (optional)', required=False) - def vet_request(self, thesis_link): - print(self.cleaned_data) - if self.cleaned_data['action_option'] == VetThesisLinkForm.ACCEPT: - print('hoi') + def __init__(self, *args, **kwargs): + super(VetThesisLinkForm, self).__init__(*args, **kwargs) + self.order_fields(['action_option', 'refusal_reason', 'justification']) + + def vet_request(self, thesislink, user): + if int(self.cleaned_data['action_option']) == VetThesisLinkForm.ACCEPT: + thesislink.vetted = True + thesislink.vetted_by = Contributor.objects.get(user=user) + thesislink.save() + + email_text = ('Dear ' + title_dict[thesislink.requested_by.title] + ' ' + + thesislink.requested_by.user.last_name + + ', \n\nThe Thesis Link you have requested, concerning thesis with' + + ' title ' + thesislink.title + ' by ' + thesislink.author + + ', has been activated at https://scipost.org/thesis/' + + str(thesislink.id) + '.' + + '\n\nThank you for your contribution, \nThe SciPost Team.') + emailmessage = EmailMessage('SciPost Thesis Link activated', email_text, + 'SciPost Theses <theses@scipost.org>', + [thesislink.requested_by.user.email], + ['theses@scipost.org'], + reply_to=['theses@scipost.org']) + emailmessage.send(fail_silently=False) + elif int(self.cleaned_data['action_option']) == VetThesisLinkForm.REFUSE: + email_text = ('Dear ' + title_dict[thesislink.requested_by.title] + ' ' + + thesislink.requested_by.user.last_name + + ', \n\nThe Thesis Link you have requested, concerning thesis with' + + ' title ' + thesislink.title + ' by ' + thesislink.author + + ', has not been activated for the following reason: ' + + self.cleaned_data['refusal_reason'] + + '.\n\nThank you for your interest, \nThe SciPost Team.') + if self.cleaned_data['justification']: + email_text += '\n\nFurther explanations: ' + \ + self.cleaned_data['justification'] + emailmessage = EmailMessage('SciPost Thesis Link', email_text, + 'SciPost Theses <theses@scipost.org>', + [thesislink.requested_by.user.email], + ['theses@scipost.org'], + reply_to=['theses@scipost.org']) + emailmessage.send(fail_silently=False) + thesislink.delete() + + elif int(self.cleaned_data['action_option']) == VetThesisLinkForm.MODIFY: + thesislink.vetted = True + thesislink.vetted_by = Contributor.objects.get(user=user) + thesislink.save() + email_text = ('Dear ' + title_dict[thesislink.requested_by.title] + ' ' + + thesislink.requested_by.user.last_name + + ', \n\nThe Thesis Link you have requested, concerning thesis with' + + ' title ' + thesislink.title + ' by ' + thesislink.author + + ', has been activated ' + '(with slight modifications to your submitted details) at ' + 'https://scipost.org/thesis/' + str(thesislink.id) + '.' + '\n\nThank you for your contribution, \nThe SciPost Team.') + emailmessage = EmailMessage('SciPost Thesis Link activated', email_text, + 'SciPost Theses <theses@scipost.org>', + [thesislink.requested_by.user.email], + ['theses@scipost.org'], + reply_to=['theses@scipost.org']) + emailmessage.send(fail_silently=False) class ThesisLinkSearchForm(forms.Form): diff --git a/theses/models.py b/theses/models.py index 50ec6b1f47548e9f43d6f53d31d091883f4ed091..a804930e1eac31b2255ce455bcd65586fa8a55ff 100644 --- a/theses/models.py +++ b/theses/models.py @@ -1,13 +1,11 @@ from django.utils import timezone from django.db import models -from django.contrib.auth.models import User from django.template import Template, Context -from .models import * - -from journals.models import * -from scipost.constants import SCIPOST_DISCIPLINES, subject_areas_dict, disciplines_dict -from scipost.models import * +from journals.models import SCIPOST_JOURNALS_DOMAINS, journals_domains_dict +from scipost.constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS,\ + subject_areas_dict, disciplines_dict +from scipost.models import Contributor class ThesisLink(models.Model): @@ -69,53 +67,13 @@ class ThesisLink(models.Model): def __str__(self): return self.title - def header_as_table(self): - context = Context({ - 'title': self.title, 'author': self.author, - 'pub_link': self.pub_link, 'institution': self.institution, - 'supervisor': self.supervisor, 'defense_date': self.defense_date}) - header = ( - '<table>' - '<tr><td>Title: </td><td> </td><td>{{ title }}</td></tr>' - '<tr><td>Author: </td><td> </td><td>{{ author }}</td></tr>' - '<tr><td>As Contributor: </td><td> </td>') - if self.author_as_cont.all(): - for auth in self.author_as_cont.all(): - header += ( - '<td><a href="/contributor/' + str(auth.id) + '">' + - auth.user.first_name + ' ' + auth.user.last_name + - '</a></td>') - else: - header += '<td>(not claimed)</td>' - header += ( - '</tr>' - '<tr><td>Type: </td><td></td><td>' + self.THESIS_TYPES_DICT[self.type] + - '</td></tr>' - '<tr><td>Discipline: </td><td></td><td>' + - disciplines_dict[self.discipline] + '</td></tr>' - '<tr><td>Domain: </td><td></td><td>' + - journals_domains_dict[self.domain] + '</td></tr>' - '<tr><td>Subject area: </td><td></td><td>' + - subject_areas_dict[self.subject_area] + '</td></tr>' - '<tr><td>URL: </td><td> </td><td><a href="{{ pub_link }}" ' - 'target="_blank">{{ pub_link }}</a></td></tr>' - '<tr><td>Degree granting institution: </td><td> </td>' - '<td>{{ institution }}</td></tr>' - '<tr><td>Supervisor(s): </td><td></td><td>{{ supervisor }}' - '</td></tr>' '<tr><td>Defense date: </td><td> </td>' - '<td>{{ defense_date }}</td></tr>' - '</table>') - template = Template(header) - return template.render(context) - def header_as_li(self): context = Context({ 'id': self.id, 'title': self.title, 'author': self.author, 'pub_link': self.pub_link, 'institution': self.institution, 'supervisor': self.supervisor, 'defense_date': self.defense_date, 'latest_activity': self.latest_activity.strftime('%Y-%m-%d %H:%M')}) - print(subject_areas_dict) - print(self.subject_area in subject_areas_dict) + header = ( '<li><div class="flex-container">' '<div class="flex-whitebox0"><p><a href="/thesis/{{ id }}" ' diff --git a/theses/templates/theses/_header_as_table.html b/theses/templates/theses/_header_as_table.html new file mode 100644 index 0000000000000000000000000000000000000000..2a23414649882e3f50f6d2c1e7788ad254de5099 --- /dev/null +++ b/theses/templates/theses/_header_as_table.html @@ -0,0 +1,46 @@ +{% load theses_extras %} + +<table> + <tr> + <td>Title: </td><td> </td><td>{{ thesislink.title }}</td> + </tr> + <tr> + <td>Author: </td><td> </td><td>{{ thesislink.author }}</td> + </tr> + <tr> + <td>As Contributor: </td><td> </td> + {% if thesislink.author_as_cont.all %} + {% for author in thesislink.author_as_cont.all %} + <td><a href= {% url 'scipost:contributor_info' author.id %}> + author.user.first_name author.user.last_name + </a></td> + {% endfor %} + {% else %} + <td>(not claimed)</td> + {% endif %} + </tr> + <tr> + <td>Type: </td><td></td><td> {{ thesislink|type }}</td> + </tr> + <tr> + <td>Discipline: </td><td></td><td>{{ thesislink|discipline }}</td> + </tr> + <tr> + <td>Domain: </td><td></td><td>{{ thesislink|domain }}</td> + </tr> + <tr> + <td>Subject area: </td><td></td><td> {{ thesislink|subject_area }} </td> + </tr> + <tr> + <td>URL: </td><td> </td><td><a href="{{ pub_link }}" target="_blank">{{ thesislink.pub_link }}</a></td> + </tr> + <tr> + <td>Degree granting institution: </td><td> </td><td>{{ thesislink.institution }}</td> + </tr> + <tr> + <td>Supervisor(s): </td><td></td><td>{{ thesislink.supervisor }}</td> + </tr> + <tr> + <td>Defense date: </td><td> </td><td>{{ thesislink.defense_date }}</td> + </tr> +</table> diff --git a/theses/templates/theses/request_thesislink.html b/theses/templates/theses/request_thesislink.html index 1ccedd0e6e21411c33b8ca7974a86528e7948652..6795e8065bb6f42c8dc05719cae224e87306339e 100644 --- a/theses/templates/theses/request_thesislink.html +++ b/theses/templates/theses/request_thesislink.html @@ -1,20 +1,28 @@ {% extends 'scipost/base.html' %} +{% load bootstrap %} + {% block pagetitle %}: request Thesis Link{% endblock pagetitle %} -{% block bodysup %} +{% block page_header %} +<div class="row"> + <div class="col-12"> + <h1 class="page-header">Request Activation of a Thesis Link</h1> + </div> +</div> +{% endblock page_header %} -<section> - <div class="flex-greybox"> - <h1>Request Activation of a Thesis Link:</h1> +{% block content %} +<div class="row"> + <div class="col-12 col-md-8"> + <form action="{% url 'theses:request_thesislink' %}" method="post"> + {% csrf_token %} + <table> + {{ form|bootstrap }} + </table> + <input class="btn btn-primary" type="submit" value="Submit"/> + </form> </div> - <form action="{% url 'theses:request_thesislink' %}" method="post"> - {% csrf_token %} - <table> - {{ form.as_table }} - </table> - <input type="submit" value="Submit"/> - </form> -</section> +</div> -{% endblock bodysup %} +{% endblock content %} diff --git a/theses/templates/theses/thesis_detail.html b/theses/templates/theses/thesis_detail.html index 541f934e1009668a521c0839c0a54fa77f9748af..45b16160beaa66b2f6f1041d0255a65c99642f2a 100644 --- a/theses/templates/theses/thesis_detail.html +++ b/theses/templates/theses/thesis_detail.html @@ -44,7 +44,8 @@ <h2>Thesis information: </h2> </div> </div> - {{ thesislink.header_as_table }} + {% include "./_header_as_table.html" with thesislink=thesislink %} + {# {{ thesislink.header_as_table }}#} <h3>Abstract:</h3> <p>{{ thesislink.abstract }}</p> diff --git a/theses/templates/theses/unvetted_thesislinks.html b/theses/templates/theses/unvetted_thesislinks.html new file mode 100644 index 0000000000000000000000000000000000000000..6846720a4d81155ee3d888a68a48187bc4df8ad8 --- /dev/null +++ b/theses/templates/theses/unvetted_thesislinks.html @@ -0,0 +1,28 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: Unvetted Thesis Links{% endblock pagetitle %} + +{% block headsup %} + +{% endblock headsup %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1>Unvetted Thesis Links</h1> + <ul> + {% for thesislink in thesislinks %} + <li> + {{ thesislink.author }} - {{ thesislink.title }} - + <a href = "{% url 'theses:vet_thesislink' pk=thesislink.id %}">vet</a> + </li> + {% empty %} + <li> + No unvetted thesis links. + </li> + {% endfor %} + </ul> + </div> +</div> +{% endblock content %} diff --git a/theses/templates/theses/vet_thesislink.html b/theses/templates/theses/vet_thesislink.html new file mode 100644 index 0000000000000000000000000000000000000000..8822dfff11c61f69a0a0ded2b8e5ea12b0004f33 --- /dev/null +++ b/theses/templates/theses/vet_thesislink.html @@ -0,0 +1,22 @@ +{% extends 'scipost/base.html' %} + +{% load bootstrap %} + +{% block pagetitle %}: Unvetted Thesis Links{% endblock pagetitle %} + +{% block headsup %} + +{% endblock headsup %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <form action="" method="post">{% csrf_token %} + {{ form|bootstrap }} + <input class="btn btn-primary" type="submit" value="Update" /> + </form> + </div> +</div> + +{% endblock content %} diff --git a/theses/templates/theses/vet_thesislink_requests.html b/theses/templates/theses/vet_thesislink_requests.html index c49008776f23329e746b62c1e0d84c9da1f3a178..49172307273f713a950bb4355acb90c6ab8387ff 100644 --- a/theses/templates/theses/vet_thesislink_requests.html +++ b/theses/templates/theses/vet_thesislink_requests.html @@ -16,13 +16,13 @@ <hr> <div class="row"> <div class="col-8"> - {{ thesislink_to_vet.header_as_table }} + {% include "./_header_as_table.html" with thesislink=thesislink_to_vet %} <br /> <h4>Abstract:</h4> <p>{{ thesislink_to_vet.abstract }}</p> </div> <div class="col-4"> - <form method="post"> + <form action= {% url 'theses:vet_thesislink_request' thesislink_to_vet.id %} method="post"> {% csrf_token %} {{ form.as_ul }} <input type="submit" value="Submit" /> diff --git a/theses/templatetags/__init__.py b/theses/templatetags/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/theses/templatetags/theses_extras.py b/theses/templatetags/theses_extras.py new file mode 100644 index 0000000000000000000000000000000000000000..f3b441f41f64e3bb5a7ad23f852eb3e627fe9dec --- /dev/null +++ b/theses/templatetags/theses_extras.py @@ -0,0 +1,26 @@ +from django import template + +from scipost.constants import SCIPOST_DISCIPLINES, subject_areas_dict, disciplines_dict +from journals.models import journals_domains_dict + +register = template.Library() + + +@register.filter +def type(thesislink): + return thesislink.THESIS_TYPES_DICT[thesislink.type] + + +@register.filter +def discipline(thesislink): + return disciplines_dict[thesislink.discipline] + + +@register.filter +def domain(thesislink): + return journals_domains_dict[thesislink.domain] + + +@register.filter +def subject_area(thesislink): + return subject_areas_dict[thesislink.subject_area] diff --git a/theses/test_forms.py b/theses/test_forms.py index 1456184593dd64e0f63f12574fb5c59b87576e6a..73d294c75e3c7f0908828be27e06deb5920ef005 100644 --- a/theses/test_forms.py +++ b/theses/test_forms.py @@ -24,21 +24,3 @@ class TestRequestThesisLink(TestCase): form = RequestThesisLinkForm(form_data) form.is_valid() self.assertEqual(form.errors['domain'], ['This field is required.']) - - -class TestVetThesisLinkRequests(TestCase): - fixtures = ['permissions', 'groups'] - - def test_thesislink_gets_vetted_when_accepted(self): - thesis_link = ThesisLinkFactory() - form = VetThesisLinkFormFactory() - form.is_valid() - form.vet_request(thesis_link) - self.assertTrue(thesis_link.vetted) - - def test_thesislink_is_not_vetted_when_refused(self): - thesis_link = ThesisLinkFactory() - form = VetThesisLinkFormFactory(action_option=VetThesisLinkForm.REFUSE) - form.is_valid() - form.vet_request(thesis_link) - self.assertFalse(thesis_link.vetted) diff --git a/theses/test_models.py b/theses/test_models.py index 4308ab25f1d22c56e33cb5047cb6f571fe080bf7..9cda6172906f7feeb4a8789f014fb4a09f5f7bf9 100644 --- a/theses/test_models.py +++ b/theses/test_models.py @@ -8,6 +8,8 @@ from .factories import ThesisLinkFactory class ThesisLinkTestCase(TestCase): + fixtures = ['permissions', 'groups'] + def test_domain_cannot_be_blank(self): thesis_link = ThesisLinkFactory() thesis_link.domain = "" diff --git a/theses/test_views.py b/theses/test_views.py index faa7f14d5d621096441faa542af7984e8e629573..c3f1244b1865f5576fab264d50d7ab95ae428a69 100644 --- a/theses/test_views.py +++ b/theses/test_views.py @@ -1,27 +1,53 @@ import re +from django.core import mail from django.core.exceptions import PermissionDenied from django.test import TestCase, RequestFactory from django.test.client import Client from django.contrib.auth.models import Group -from django.urls import reverse +from django.urls import reverse, reverse_lazy +from django.contrib.messages.storage.fallback import FallbackStorage -from .views import RequestThesisLink, VetThesisLinkRequests from scipost.factories import UserFactory, ContributorFactory +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, VetThesisLinkFormFactory from .models import ThesisLink +from .forms import VetThesisLinkForm +from common.helpers import model_form_data class TestThesisDetail(TestCase): fixtures = ['groups', 'permissions'] def test_visits_valid_thesis_detail(self): + """ A visitor does not have to be logged in to view a thesis link. """ thesis_link = ThesisLinkFactory() client = Client() target = reverse('theses:thesis', kwargs={'thesislink_id': thesis_link.id}) response = client.post(target) self.assertEqual(response.status_code, 200) + def test_submitting_comment_creates_comment(self): + """ Valid Comment gets saved """ + + contributor = ContributorFactory() + thesislink = ThesisLinkFactory() + valid_comment_data = model_form_data(CommentFactory.build(), CommentForm) + target = reverse('theses:thesis', kwargs={'thesislink_id': thesislink.id}) + + comment_count = Comment.objects.filter(author=contributor).count() + self.assertEqual(comment_count, 0) + + request = RequestFactory().post(target, valid_comment_data) + request.user = contributor.user + response = thesis_detail(request, thesislink_id=thesislink.id) + + comment_count = Comment.objects.filter(author=contributor).count() + self.assertEqual(comment_count, 1) + class TestRequestThesisLink(TestCase): fixtures = ['groups', 'permissions'] @@ -47,7 +73,8 @@ class TestVetThesisLinkRequests(TestCase): def setUp(self): self.client = Client() - self.target = reverse('theses:vet_thesislink_requests') + self.thesislink = ThesisLinkFactory() + self.target = reverse('theses:vet_thesislink', kwargs={'pk': self.thesislink.id}) def test_response_when_not_logged_in(self): response = self.client.get(self.target) @@ -58,33 +85,59 @@ class TestVetThesisLinkRequests(TestCase): A Contributor needs to be in the Vetting Editors group to be able to vet submitted thesis links. ''' - # Create ThesisLink to vet. - ThesisLinkFactory() request = RequestFactory().get(self.target) user = UserFactory() request.user = user self.assertRaises( - PermissionDenied, VetThesisLinkRequests.as_view(), request) + PermissionDenied, VetThesisLink.as_view(), request, pk=self.thesislink.id) def test_response_vetting_editor(self): - # Create ThesisLink to vet. - ThesisLinkFactory() request = RequestFactory().get(self.target) user = UserFactory() user.groups.add(Group.objects.get(name="Vetting Editors")) request.user = user - response = VetThesisLinkRequests.as_view()(request) + response = VetThesisLink.as_view()(request, pk=self.thesislink.id) self.assertEqual(response.status_code, 200) - def test_thesislink_is_vetted_by_correct_contributor(self): - # TODO: how to make sure we are vetting the right thesis link? + def test_thesislink_is_vetted_by_correct_contributor_and_mail_is_sent(self): + contributor = ContributorFactory() + contributor.user.groups.add(Group.objects.get(name="Vetting Editors")) + post_data = model_form_data(ThesisLinkFactory(), VetThesisLinkForm) + post_data["action_option"] = VetThesisLinkForm.ACCEPT + target = reverse('theses:vet_thesislink', kwargs={'pk': self.thesislink.id}) + + request = RequestFactory().post(target, post_data) + request.user = contributor.user + + # I don't know what the following three lines do, but they help make a RequestFactory + # work with the messages middleware + setattr(request, 'session', 'session') + messages = FallbackStorage(request) + setattr(request, '_messages', messages) + + response = VetThesisLink.as_view()(request, pk=self.thesislink.id) + self.thesislink.refresh_from_db() + self.assertEqual(self.thesislink.vetted_by, contributor) + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].subject, 'SciPost Thesis Link activated') + + def test_thesislink_that_is_refused_is_deleted_and_mail_is_sent(self): contributor = ContributorFactory() contributor.user.groups.add(Group.objects.get(name="Vetting Editors")) - post_data = VetThesisLinkFormFactory().data + post_data = model_form_data(ThesisLinkFactory(), VetThesisLinkForm) + post_data["action_option"] = VetThesisLinkForm.REFUSE + target = reverse('theses:vet_thesislink', kwargs={'pk': self.thesislink.id}) - request = RequestFactory().post(self.target, post_data) + request = RequestFactory().post(target, post_data) request.user = contributor.user - response = VetThesisLinkRequests.as_view()(request) + # I don't know what the following three lines do, but they help make a RequestFactory + # work with the messages middleware + setattr(request, 'session', 'session') + messages = FallbackStorage(request) + setattr(request, '_messages', messages) - self.assertTrue(False) + response = VetThesisLink.as_view()(request, pk=self.thesislink.id) + self.assertEqual(ThesisLink.objects.filter(id=self.thesislink.id).count(), 0) + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].subject, 'SciPost Thesis Link') diff --git a/theses/urls.py b/theses/urls.py index 3d3c530e497a0ca6794259b9626530d6a8e8fbc9..a0734fdb52195d0ffaa5864a8ed649fe7129630d 100644 --- a/theses/urls.py +++ b/theses/urls.py @@ -9,8 +9,6 @@ urlpatterns = [ url(r'^browse/(?P<discipline>[a-z]+)/(?P<nrweeksback>[0-9]+)/$', views.browse, name='browse'), url(r'^(?P<thesislink_id>[0-9]+)/$', views.thesis_detail, name='thesis'), url(r'^request_thesislink$', views.RequestThesisLink.as_view(), name='request_thesislink'), - url(r'^vet_thesislink_requests$', views.VetThesisLinkRequests.as_view(), - name='vet_thesislink_requests'), - url(r'^vet_thesislink_request_ack/(?P<thesislink_id>[0-9]+)$', - views.vet_thesislink_request_ack, name='vet_thesislink_request_ack'), + url(r'^unvetted_thesislinks$', views.UnvettedThesisLinks.as_view(), name='unvetted_thesislinks'), + url(r'^vet_thesislink/(?P<pk>[0-9]+)/$', views.VetThesisLink.as_view(), name='vet_thesislink'), ] diff --git a/theses/views.py b/theses/views.py index 7e782a4b88b20052e4c52217c4544106c246ced6..9915a691373689516e5561639c331979b5b4f775 100644 --- a/theses/views.py +++ b/theses/views.py @@ -2,33 +2,30 @@ import datetime from django.utils import timezone from django.shortcuts import get_object_or_404, render -from django.contrib.auth import authenticate, login, logout -from django.contrib.auth.decorators import login_required, permission_required -from django.contrib.auth.models import User +from django.contrib.auth.decorators import permission_required from django.contrib import messages -from django.core.mail import EmailMessage from django.core.urlresolvers import reverse, reverse_lazy -from django.http import HttpResponse, HttpResponseRedirect -from django.views.decorators.csrf import csrf_protect -from django.db.models import Avg -from django.views.generic.edit import CreateView, FormView +from django.http import HttpResponseRedirect +from django.views.generic.edit import CreateView, UpdateView +from django.views.generic.list import ListView from django.utils.decorators import method_decorator -from .models import * -from .forms import * +from .models import ThesisLink +from .forms import RequestThesisLinkForm, ThesisLinkSearchForm, VetThesisLinkForm from comments.models import Comment from comments.forms import CommentForm -from scipost.forms import TITLE_CHOICES, AuthenticationForm - +from scipost.forms import TITLE_CHOICES +from scipost.models import Contributor +import strings title_dict = dict(TITLE_CHOICES) # Convert titles for use in emails + ################ # Theses ################ - @method_decorator(permission_required( 'scipost.can_request_thesislinks', raise_exception=True), name='dispatch') class RequestThesisLink(CreateView): @@ -37,6 +34,7 @@ class RequestThesisLink(CreateView): success_url = reverse_lazy('scipost:personal_page') def form_valid(self, form): + form.instance.requested_by = self.request.user.contributor messages.add_message(self.request, messages.SUCCESS, strings.acknowledge_request_thesis_link) return super(RequestThesisLink, self).form_valid(form) @@ -44,108 +42,39 @@ class RequestThesisLink(CreateView): @method_decorator(permission_required( 'scipost.can_vet_thesislink_requests', raise_exception=True), name='dispatch') -class VetThesisLinkRequests(FormView): - form_class = VetThesisLinkForm - template_name = 'theses/vet_thesislink_requests.html' - # TODO: not right yet - success_url = reverse_lazy('theses:vet_thesislink_requests') +class UnvettedThesisLinks(ListView): + model = ThesisLink + template_name = 'theses/unvetted_thesislinks.html' + context_object_name = 'thesislinks' + queryset = ThesisLink.objects.filter(vetted=False) - def get_context_data(self, **kwargs): - context = super(VetThesisLinkRequests, self).get_context_data(**kwargs) - context['thesislink_to_vet'] = self.thesislink_to_vet() - return context - def thesislink_to_vet(self): - return ThesisLink.objects.filter(vetted=False).first() +@method_decorator(permission_required( + 'scipost.can_vet_thesislink_requests', raise_exception=True), name='dispatch') +class VetThesisLink(UpdateView): + model = ThesisLink + form_class = VetThesisLinkForm + template_name = "theses/vet_thesislink.html" + success_url = reverse_lazy('theses:unvetted_thesislinks') def form_valid(self, form): - form.vet_request(self.thesislink_to_vet()) - return super(VetThesisLinkRequests, self).form_valid(form) - - -# @permission_required('scipost.can_vet_thesislink_requests', raise_exception=True) -# def vet_thesislink_requests(request): -# contributor = Contributor.objects.get(user=request.user) -# thesislink_to_vet = ThesisLink.objects.filter( -# vetted=False).first() # only handle one at a time -# form = VetThesisLinkForm() -# context = {'contributor': contributor, 'thesislink_to_vet': thesislink_to_vet, 'form': form} -# return render(request, 'theses/vet_thesislink_requests.html', context) + # I totally override the form_valid method. I do not call super. + # This is because, by default, an UpdateView saves the object as instance, + # which it builds from the form data. So, the changes (by whom the thesis link was + # vetted, etc.) would be lost. Instead, we need the form to save with commit=False, + # then modify the vetting fields, and then save. + # Builds model that reflects changes made during update. Does not yet save. + self.object = form.save(commit=False) + # Process vetting actions (object already gets saved.) + form.vet_request(self.object, self.request.user) + # Save again. + self.object.save() -@permission_required('scipost.can_vet_thesislink_requests', raise_exception=True) -def vet_thesislink_request_ack(request, thesislink_id): - if request.method == 'POST': - form = VetThesisLinkForm(request.POST) - thesislink = ThesisLink.objects.get(pk=thesislink_id) - if form.is_valid(): - if form.cleaned_data['action_option'] == '1': - thesislink.vetted = True - thesislink.vetted_by = Contributor.objects.get(user=request.user) - thesislink.save() - email_text = ('Dear ' + title_dict[thesislink.requested_by.title] + ' ' - + thesislink.requested_by.user.last_name - + ', \n\nThe Thesis Link you have requested, concerning thesis with title ' - + thesislink.title + ' by ' + thesislink.author - + ', has been activated at https://scipost.org/thesis/' - + str(thesislink.id) + '.' - + '\n\nThank you for your contribution, \nThe SciPost Team.') - emailmessage = EmailMessage('SciPost Thesis Link activated', email_text, - 'SciPost Theses <theses@scipost.org>', - [thesislink.requested_by.user.email], - ['theses@scipost.org'], - reply_to=['theses@scipost.org']) - emailmessage.send(fail_silently=False) - elif form.cleaned_data['action_option'] == '0': - # re-edit the form starting from the data provided - form2 = RequestThesisLinkForm(initial={'title': thesislink.pub_title, - 'pub_ink': thesislink.pub_link, - 'author': thesislink.author, - 'institution': thesislink.institution, - 'defense_date': thesislink.defense_date, - 'abstract': thesislink.abstract}) - thesislink.delete() - email_text = ('Dear ' + title_dict[thesislink.requested_by.title] + ' ' - + thesislink.requested_by.user.last_name - + ', \n\nThe Thesis Link you have requested, concerning thesis with title ' - + thesislink.title + ' by ' + thesislink.author_list - + ', has been activated ' - '(with slight modifications to your submitted details) at ' - 'https://scipost.org/thesis/' + str(thesislink.id) + '.' - '\n\nThank you for your contribution, \nThe SciPost Team.') - emailmessage = EmailMessage('SciPost Thesis Link activated', email_text, - 'SciPost Theses <theses@scipost.org>', - [thesislink.requested_by.user.email], - ['theses@scipost.org'], - reply_to=['theses@scipost.org']) - # Don't send email yet... only when option 1 has succeeded! - # emailmessage.send(fail_silently=False) - context = {'form': form2} - return render(request, 'theses/request_thesislink.html', context) - elif form.cleaned_data['action_option'] == '2': - email_text = ('Dear ' + title_dict[thesislink.requested_by.title] + ' ' - + thesislink.requested_by.user.last_name - + ', \n\nThe Thesis Link you have requested, concerning thesis with title ' - + thesislink.title + ' by ' + thesislink.author - + ', has not been activated for the following reason: ' - + form.cleaned_data['refusal_reason'] - + '.\n\nThank you for your interest, \nThe SciPost Team.') - if form.cleaned_data['justification']: - email_text += '\n\nFurther explanations: ' + \ - form.cleaned_data['justification'] - emailmessage = EmailMessage('SciPost Thesis Link', email_text, - 'SciPost Theses <theses@scipost.org>', - [thesislink.requested_by.user.email], - ['theses@scipost.org'], - reply_to=['theses@scipost.org']) - emailmessage.send(fail_silently=False) - thesislink.delete() - - context = {'ack_header': 'Thesis Link request vetted.', - 'followup_message': 'Return to the ', - 'followup_link': reverse('theses:vet_thesislink_requests'), - 'followup_link_label': 'Thesis Link requests page'} - return render(request, 'scipost/acknowledgement.html', context) + messages.add_message( + self.request, messages.SUCCESS, + strings.acknowledge_vet_thesis_link) + return HttpResponseRedirect(self.get_success_url()) def theses(request): @@ -169,7 +98,8 @@ def theses(request): thesislink_recent_list = (ThesisLink.objects .filter(vetted=True, - latest_activity__gte=timezone.now() + datetime.timedelta(days=-7))) + latest_activity__gte=timezone.now() + datetime.timedelta( + days=-7))) context = {'form': form, 'thesislink_search_list': thesislink_search_list, 'thesislink_recent_list': thesislink_recent_list} return render(request, 'theses/theses.html', context) diff --git a/webpack.config.js b/webpack.config.js index b41c163e9113774a6f928bbeaf4293722637c5ee..2b29fc5ea351cb1b77f543f2d3f053c5982c49be 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -8,7 +8,10 @@ var path_bundles = __dirname + '/static_bundles/bundles'; module.exports = { context: __dirname, entry: { - main: glob.sync("./scipost/static/scipost/assets/**/*.@(js|css|scss)"), + main: [ + "./scipost/static/scipost/assets/js/scripts.js", + "./scipost/static/scipost/assets/css/style.scss" + ], bootstrap: 'bootstrap-loader' }, output: { @@ -48,7 +51,7 @@ module.exports = { "window.jQuery": "jquery", Tether: "tether", "window.Tether": "tether", - // Alert: "exports-loader?Alert!bootstrap/js/dist/alert", + Alert: "exports-loader?Alert!bootstrap/js/dist/alert", // Button: "exports-loader?Button!bootstrap/js/dist/button", // Carousel: "exports-loader?Carousel!bootstrap/js/dist/carousel", Collapse: "exports-loader?Collapse!bootstrap/js/dist/collapse",