diff --git a/SciPost_v1/settings/staging.py b/SciPost_v1/settings/staging.py index 48120c079d0f89189a545a27d9afafd9d4053968..62c0b2932720cb11fc62559d50d1bb798f20b1af 100644 --- a/SciPost_v1/settings/staging.py +++ b/SciPost_v1/settings/staging.py @@ -26,3 +26,6 @@ LOGGING['handlers']['scipost_file_doi']['filename'] = '/home/scipoststg/webapps/ SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True +# Email +EMAIL_BACKEND = 'mails.backends.filebased.ModelEmailBackend' +EMAIL_BACKEND_ORIGINAL = 'django.core.mail.backends.dummy.EmailBackend' # Disable real processing diff --git a/affiliations/managers.py b/affiliations/managers.py index e6a0872050e865cd4a2e7fdf3efcab9fae156f2e..11d16f9ecedba5af4c15825c04fd3a594c28193b 100644 --- a/affiliations/managers.py +++ b/affiliations/managers.py @@ -16,3 +16,7 @@ class AffiliationQuerySet(models.QuerySet): Q(begin_date__isnull=True, end_date__gte=today) | Q(begin_date__lte=today, end_date__gte=today) | Q(begin_date__isnull=True, end_date__isnull=True)) + +class InstitutionQuerySet(models.QuerySet): + def has_publications(self): + return self.filter(publications__isnull=False) diff --git a/affiliations/models.py b/affiliations/models.py index 40f41195d423482d132f86ceb33536c7fb26e7ea..faf52284684048e97d7729cce9e03c55e87862d8 100644 --- a/affiliations/models.py +++ b/affiliations/models.py @@ -10,18 +10,19 @@ from django_countries.fields import CountryField from scipost.models import Contributor from .constants import INSTITUTION_TYPES, TYPE_UNIVERSITY -from .managers import AffiliationQuerySet +from .managers import AffiliationQuerySet, InstitutionQuerySet class Institution(models.Model): - """ - Any (scientific) Institution in the world should ideally have a SciPost registration. - """ + """Any (scientific) Institution with a SciPost registration.""" + name = models.CharField(max_length=255) acronym = models.CharField(max_length=16, blank=True) country = CountryField() type = models.CharField(max_length=16, choices=INSTITUTION_TYPES, default=TYPE_UNIVERSITY) + objects = InstitutionQuerySet.as_manager() + class Meta: default_related_name = 'institutions' ordering = ['country'] @@ -30,9 +31,11 @@ class Institution(models.Model): return '{name} ({country})'.format(name=self.name, country=self.get_country_display()) def get_absolute_url(self): + """Return the Institution detail page.""" return reverse('affiliations:institution_details', args=(self.id,)) def contributors(self): + """All Contributor instances related to the Institution.""" return Contributor.objects.filter(affiliations__institution=self) diff --git a/affiliations/templates/affiliations/base.html b/affiliations/templates/affiliations/base.html new file mode 100644 index 0000000000000000000000000000000000000000..6e6300f5ffebd97a0a4a9cc57c02baa65e50dccf --- /dev/null +++ b/affiliations/templates/affiliations/base.html @@ -0,0 +1,13 @@ +{% extends 'scipost/base.html' %} + +{% block breadcrumb %} + <div class="container-outside header"> + <div class="container"> + <nav class="breadcrumb hidden-sm-down"> + {% block breadcrumb_items %} + <a href="{% url 'affiliations:institutions' %}" class="breadcrumb-item">Institutions</a> + {% endblock %} + </nav> + </div> + </div> +{% endblock %} diff --git a/affiliations/templates/affiliations/institution_detail.html b/affiliations/templates/affiliations/institution_detail.html new file mode 100644 index 0000000000000000000000000000000000000000..641f629292c7e8d000698fcb847373ea3e9af177 --- /dev/null +++ b/affiliations/templates/affiliations/institution_detail.html @@ -0,0 +1,26 @@ +{% extends 'affiliations/base.html' %} + +{% load bootstrap %} + +{% block pagetitle %}: Institution details{% endblock pagetitle %} + +{% block breadcrumb_items %} + {{ block.super }} + <span class="breadcrumb-item">{{ institution }}</span> +{% endblock %} + +{% block content %} + +<h1 class="highlight">Institution {{ institution }}</h1> + +<ul> + {% for publication in institution.publications.all %} + <li> + <a href="{{ publication.get_absolute_url }}">{{ publication.title }}</a> + <br>by {{ publication.author_list }}, + <br>{{ publication.citation }} + </li> + {% endfor %} +</ul> + +{% endblock content %} diff --git a/affiliations/templates/affiliations/institution_list.html b/affiliations/templates/affiliations/institution_list.html index 50d0b8ea36fd872feb75a211c4cf652822c9bd47..a76c137dd6e2ba85102ca14a1f173535d6fab87e 100644 --- a/affiliations/templates/affiliations/institution_list.html +++ b/affiliations/templates/affiliations/institution_list.html @@ -1,23 +1,29 @@ -{% extends 'scipost/_personal_page_base.html' %} +{% extends 'affiliations/base.html' %} {% block pagetitle %}: Institutions{% endblock pagetitle %} + {% block breadcrumb_items %} - {{ block.super }} <span class="breadcrumb-item">Institutions</span> {% endblock %} {% block content %} -<h1>All Institutions in the database</h1> +<h1 class="highlight">Institutions</h1> + +<h3>All Institutions with a SciPost publication</h3> {% if is_paginated %} {% include 'partials/pagination.html' with page_obj=page_obj %} {% endif %} + <ul> {% for institution in object_list %} - <li><a href="{% url 'affiliations:institution_details' institution.id %}">{{ institution }}</a></li> + <li> + <a href="{{ institution.get_absolute_url }}">{{ institution }}</a> + {% if perms.scipost.can_manage_affiliations %} · <a href="{% url 'affiliations:institution_edit' institution.id %}"><i class="fa fa-pencil"></i></a>{% endif %} + </li> {% empty %} <li><em>There are no Institutions known yet.</em><li> {% endfor %} diff --git a/affiliations/urls.py b/affiliations/urls.py index 0b839a4656c5434cacde2f183db38203ed1182fc..22b6fc62eb3081d47741c2169c86467015116523 100644 --- a/affiliations/urls.py +++ b/affiliations/urls.py @@ -8,8 +8,10 @@ from . import views urlpatterns = [ url(r'^$', views.InstitutionListView.as_view(), name='institutions'), - url(r'^(?P<institution_id>[0-9]+)/$', views.InstitutionUpdateView.as_view(), + url(r'^(?P<institution_id>[0-9]+)/$', views.InstitutionDetailView.as_view(), name='institution_details'), + url(r'^(?P<institution_id>[0-9]+)/edit', views.InstitutionUpdateView.as_view(), + name='institution_edit'), url(r'^(?P<institution_id>[0-9]+)/merge$', views.merge_institutions, name='merge_institutions'), ] diff --git a/affiliations/views.py b/affiliations/views.py index 83e94a2c94c16ab8d865b246803bdc04c6d9ab47..9de5a4d024652bae71457646716549a6560fe995 100644 --- a/affiliations/views.py +++ b/affiliations/views.py @@ -7,6 +7,7 @@ from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.urls import reverse from django.utils.decorators import method_decorator +from django.views.generic.detail import DetailView from django.views.generic.edit import UpdateView from django.views.generic.list import ListView from django.shortcuts import get_object_or_404 @@ -15,10 +16,14 @@ from .forms import InstitutionMergeForm from .models import Institution -@method_decorator(permission_required('scipost.can_manage_affiliations'), name='dispatch') class InstitutionListView(ListView): + queryset = Institution.objects.has_publications() + paginate_by = 20 + + +class InstitutionDetailView(DetailView): model = Institution - paginate_by = 100 + pk_url_kwarg = 'institution_id' @method_decorator(permission_required('scipost.can_manage_affiliations'), name='dispatch') @@ -53,4 +58,4 @@ def merge_institutions(request, institution_id): messages.success(request, 'Institution {a} merged into {b}'.format( a=form.cleaned_data.get('institution', '?'), b=institution)) - return redirect(reverse('affiliations:institution_details', args=(institution.id,))) + return redirect(reverse('affiliations:institution_edit', args=(institution.id,))) diff --git a/colleges/models.py b/colleges/models.py index 8f4f8ac110f51050320e5a738ea5d8c652f4f3bd..312021771eb5501790b86f011e264e206628d2c9 100644 --- a/colleges/models.py +++ b/colleges/models.py @@ -13,13 +13,15 @@ from .managers import FellowQuerySet class Fellowship(TimeStampedModel): - """ - Editorial College Fellowship connecting Editorial College and Contributors, - possibly with a limiting start/until date. + """A Fellowship gives access to the Submission Pool to Contributors. + + Editorial College Fellowship connects the Editorial College and Contributors, + possibly with a limiting start/until date and/or a Proceedings event. The date range will effectively be used while determining 'the pool' for a specific Submission, so it has a direct effect on the submission date. """ + contributor = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE, related_name='fellowships') start_date = models.DateField(null=True, blank=True) @@ -39,15 +41,15 @@ class Fellowship(TimeStampedModel): return _str def get_absolute_url(self): + """Return the admin fellowship page.""" return reverse('colleges:fellowship', args=(self.id,)) def sibling_fellowships(self): - """ - Return all Fellowships that are directly related to the Fellow of this Fellowship. - """ + """Return all Fellowships that are directly related to the Fellow of this Fellowship.""" return self.contributor.fellowships.all() def is_active(self): + """Check if the instance is within start and until date.""" today = datetime.date.today() if not self.start_date: if not self.until_date: diff --git a/colleges/views.py b/colleges/views.py index 14e4765853905caf5136d68b349e40d26eaf769b..babe8a45304bc2f92231d82ffa63a6c7a3f2ecec 100644 --- a/colleges/views.py +++ b/colleges/views.py @@ -19,9 +19,7 @@ from .models import Fellowship @login_required @permission_required('scipost.can_manage_college_composition', raise_exception=True) def fellowships(request): - """ - List all fellowships to be able to edit them, or create new ones. - """ + """List all fellowships to be able to edit them, or create new ones.""" fellowships = Fellowship.objects.active() context = { @@ -33,9 +31,7 @@ def fellowships(request): @login_required @permission_required('scipost.can_manage_college_composition', raise_exception=True) def fellowship_detail(request, id): - """ - View details of a specific fellowship - """ + """View details of a specific fellowship.""" fellowship = get_object_or_404(Fellowship, id=id) context = { diff --git a/comments/managers.py b/comments/managers.py index c66d7f93225adf6d9e3d1883e32a118bd69f5505..dc094232df4abcdc60b907f36d6fc894af72cf17 100644 --- a/comments/managers.py +++ b/comments/managers.py @@ -19,3 +19,6 @@ class CommentQuerySet(models.QuerySet): def author_replies(self): return self.filter(is_author_reply=True) + + def publicly_visible(self): + return self.filter(anonymous=False, status__gte=1) diff --git a/comments/models.py b/comments/models.py index fda1b74ae3fab6b4abb779b9b6bf09748c36fb82..23ceb52188aef09e7d162499b8e09e2a88786b4f 100644 --- a/comments/models.py +++ b/comments/models.py @@ -27,8 +27,10 @@ US_NOTICE = 'Warning: This field is out of service and will be removed in the fu class Comment(TimeStampedModel): - """ A Comment is an unsollicited note, submitted by a Contributor, - on a particular publication or in reply to an earlier Comment. """ + """ A Comment is an unsollicited note, submitted by a Contributor. + + A Comment is pointed to a particular publication or in reply to an earlier Comment. It + may be l""" status = models.SmallIntegerField(default=STATUS_PENDING, choices=COMMENT_STATUS) vetted_by = models.ForeignKey('scipost.Contributor', blank=True, null=True, @@ -151,16 +153,16 @@ class Comment(TimeStampedModel): assign_perm('comments.can_vet_comments', to_object.editor_in_charge.user, self) def get_author(self): - '''Get author, if and only if comment is not anonymous!!!''' + """Return Contributor instance of object if not anonymous.""" if not self.anonymous: return self.author return None def get_author_str(self): - '''Get author string, if and only if comment is not anonymous!!!''' + """Return author string if not anonymous.""" author = self.get_author() if author: - return author.user.first_name + ' ' + author.user.last_name + return '{} {}'.format(author.get_title_display(), author.user.last_name) return 'Anonymous' def update_opinions(self, contributor_id, opinion): diff --git a/comments/templates/comments/_comment_card_content.html b/comments/templates/comments/_comment_card_content.html index fb7dff65a51631ad07bb1211b5751b85ffdb217a..6fa4f459bd55ef6e161accab805c8689b5e973b3 100644 --- a/comments/templates/comments/_comment_card_content.html +++ b/comments/templates/comments/_comment_card_content.html @@ -2,10 +2,11 @@ {% block card_block_header %}{% endblock %} <p class="card-text"> {% if comment.anonymous %} - Anonymous: + Anonymous: {% else %} - <a href="{{comment.author.get_absolute_url}}">{{comment.author.user.first_name}} {{comment.author.user.last_name}}</a>: + <a href="{{ comment.author.get_absolute_url }}">{{ comment.get_author_str }}</a>: {% endif %} + <a href="{{comment.get_absolute_url}}"> "{{comment.comment_text|slice:'30'}}{% if comment.comment_text|length > 30 %}...{% endif %}" </a> diff --git a/comments/templates/comments/_comment_card_extended_for_author.html b/comments/templates/comments/_comment_card_extended_for_author.html index 51c26e39304d86a3b916dce310b43132f9792d83..5fa9fa5f645c02426aa4aecdc37530cb93c9b441 100644 --- a/comments/templates/comments/_comment_card_extended_for_author.html +++ b/comments/templates/comments/_comment_card_extended_for_author.html @@ -5,7 +5,11 @@ </div> <p>"{{comment.comment_text|linebreaksbr}}"</p> - <p class="card-text">by <a href="{{comment.author.get_absolute_url}}">{{comment.author.user.first_name}} {{comment.author.user.last_name}}</a> in {{comment.content_type|capfirst}} on <a href="{{comment.content_object.get_absolute_url}}" class="pubtitleli">{{comment.title}}</a> {% if comment.content_object.author_list %} <span class="text-muted">by {{comment.content_object.author_list}}</span>{% endif %}</p> + {% if comment.anonymous %} + <p class="card-text">by Anonymous in {{comment.content_type|capfirst}} on <a href="{{comment.content_object.get_absolute_url}}" class="pubtitleli">{{comment.title}}</a> {% if comment.content_object.author_list %} <span class="text-muted">by {{comment.content_object.author_list}}</span>{% endif %}</p> + {% else %} + <p class="card-text">by <a href="{{comment.author.get_absolute_url}}">{{comment.author.user.first_name}} {{comment.author.user.last_name}}</a> in {{comment.content_type|capfirst}} on <a href="{{comment.content_object.get_absolute_url}}" class="pubtitleli">{{comment.title}}</a> {% if comment.content_object.author_list %} <span class="text-muted">by {{comment.content_object.author_list}}</span>{% endif %}</p> + {% endif %} {% comment %} Using 'by xxx' on non-submission comments here would be ambigious. Does the `by xxx` apply to the diff --git a/comments/templates/comments/_comment_tex_template.html b/comments/templates/comments/_comment_tex_template.html index cb39aa0d3c78f50d5fe9f2d75f618abadc9ce8a5..05d5e1479ade5d44ab1b41c9d15647940f1773d3 100644 --- a/comments/templates/comments/_comment_tex_template.html +++ b/comments/templates/comments/_comment_tex_template.html @@ -4,8 +4,8 @@ Received {{comment.date_submitted|date:'d-m-Y'}}\ \\ {% endspaceless %} {% for subcomment in comment.nested_comments.vetted %} - \addcontentsline{toc}{subsection}{\protect\numberline{}{% if subcomment.is_author_reply %}Author Reply{% else %}Comment{% endif %} {{forloop.counter}} by {{subcomment.author.user.first_name}} {{subcomment.author.user.last_name}} } + \addcontentsline{toc}{subsection}{\protect\numberline{}{% if subcomment.is_author_reply %}Author Reply{% else %}Comment{% endif %} {{forloop.counter}} by {% if subcomment.anonymous %}Anonymous{% else %}{{subcomment.author.user.first_name}} {{subcomment.author.user.last_name}}{% endif %} } - \subsection*{ {% if subcomment.is_author_reply %}Author Reply{% else %}Comment{% endif %} {{forloop.parentloop.counter}}.{{forloop.counter}} by {{subcomment.author.user.first_name}} {{subcomment.author.user.last_name}} } + \subsection*{ {% if subcomment.is_author_reply %}Author Reply{% else %}Comment{% endif %} {{forloop.parentloop.counter}}.{{forloop.counter}} by {% if subcomment.anonymous %}Anonymous{% else %}{{subcomment.author.user.first_name}} {{subcomment.author.user.last_name}}{% endif %} } {% include 'comments/_comment_tex_template.html' with comment=subcomment %} {% endfor %} diff --git a/comments/templates/partials/comments/comments_list.html b/comments/templates/partials/comments/comments_list.html index ea3e9ac08e398d5ad273a886e34f42cd4e4f6f0d..6f35351e2a6b0e59a50c0c394113cc4040648c71 100644 --- a/comments/templates/partials/comments/comments_list.html +++ b/comments/templates/partials/comments/comments_list.html @@ -1,7 +1,7 @@ {% if comments %} <ul class="{{ css_class|default:'' }}"> {% for comment in comments %} - <li><a href="{{ comment.get_absolute_url }}"{% if target_blank %} target="_blank"{% endif %}>{% if comment.is_author_reply %}Author Reply{% else %}Comment{% endif %} by {{ comment.author.get_title_display }} {{ comment.author.user.last_name }} on {{ comment.date_submitted|date:'DATE_FORMAT' }}</a></li> + <li><a href="{{ comment.get_absolute_url }}"{% if target_blank %} target="_blank"{% endif %}>{% if comment.is_author_reply %}Author Reply{% else %}Comment{% endif %} by {{ comment.get_author_str }} on {{ comment.date_submitted|date:'DATE_FORMAT' }}</a></li> {% include 'partials/comments/comments_list.html' with comments=comment.nested_comments.vetted css_class='m-0 pl-4' %} {% endfor %} </ul> diff --git a/comments/utils.py b/comments/utils.py index 8ffa2e4fcf1b09c19ee53965ef0eae64baaa71e0..8737a8e0d20b60b23fc0f2d9e5efe4bb5a4f313c 100644 --- a/comments/utils.py +++ b/comments/utils.py @@ -8,7 +8,7 @@ from common.utils import BaseMailUtil def validate_file_extention(value, allowed_extentions): - '''Check if a filefield (value) has allowed extentions.''' + """Check if a filefield (value) has allowed extentions.""" ext = os.path.splitext(value.name)[1] # [0] returns path+filename return ext.lower() in allowed_extentions @@ -19,20 +19,30 @@ class CommentUtils(BaseMailUtil): @classmethod def email_comment_vet_accepted_to_author(cls): - """ - Send mail after Comment is vetted: `Accept` + """Send mail after Comment is vetted: `Accept`. Requires loading: comment -- Comment """ + from submissions.models import Submission, Report + + comment = cls._context['comment'] + send_mail = True + if isinstance(comment.content_object, Submission): + send_mail = comment.author not in comment.content_object.authors.all() + elif isinstance(comment.content_object, Report): + send_mail = comment.author not in comment.content_object.submission.authors.all() + + if not send_mail: + return + cls._send_mail(cls, 'comment_vet_accepted', - [cls._context['comment'].author.user.email], + [comment.author.user.email], 'SciPost Comment published') @classmethod def email_comment_vet_rejected_to_author(cls, email_response=''): - """ - Send mail after Comment is vetted: `Reject` + """Send mail after Comment is vetted: `Reject`. Requires loading: comment -- Comment diff --git a/comments/views.py b/comments/views.py index f916794d000fccb427ea44366310c4a58e7b11ed..69f65864535d70908d3b47e16dbfd6d06b08f6c1 100644 --- a/comments/views.py +++ b/comments/views.py @@ -76,10 +76,6 @@ def vet_submitted_comment(request, comment_id): comment.vetted_by = request.user.contributor comment.save() - # Send emails - CommentUtils.load({'comment': comment}) - CommentUtils.email_comment_vet_accepted_to_author() - # Update `latest_activity` fields content_object = comment.content_object content_object.latest_activity = timezone.now() @@ -100,6 +96,10 @@ def vet_submitted_comment(request, comment_id): SubmissionUtils.load({'submission': content_object.submission}) SubmissionUtils.send_author_comment_received_email() + # Send emails + CommentUtils.load({'comment': comment}) + CommentUtils.email_comment_vet_accepted_to_author() + elif form.cleaned_data['action_option'] == '2': # The comment request is simply rejected comment.status = int(form.cleaned_data['refusal_reason']) diff --git a/funders/managers.py b/funders/managers.py new file mode 100644 index 0000000000000000000000000000000000000000..12d206fbfbd2615bfca021d1b008fe64ed7197b0 --- /dev/null +++ b/funders/managers.py @@ -0,0 +1,12 @@ +__copyright__ = "Copyright 2016-2018, Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django.db import models + + +class FunderQuerySet(models.QuerySet): + def has_publications(self): + """Return those Funder instances related to any Publication instance.""" + return self.filter( + models.Q(publications__isnull=False) | models.Q(grants__publications__isnull=False)) diff --git a/funders/migrations/0003_auto_20180425_2146.py b/funders/migrations/0003_auto_20180425_2146.py new file mode 100644 index 0000000000000000000000000000000000000000..b1f2aa55e96a7df2a9d7018efa6b6298785147d7 --- /dev/null +++ b/funders/migrations/0003_auto_20180425_2146.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-25 19:46 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('funders', '0002_auto_20171229_1435'), + ] + + operations = [ + migrations.AlterField( + model_name='funder', + name='acronym', + field=models.CharField(blank=True, default='', max_length=32), + ), + ] diff --git a/funders/migrations/0004_auto_20180425_2146.py b/funders/migrations/0004_auto_20180425_2146.py new file mode 100644 index 0000000000000000000000000000000000000000..a929cb51fd082773ae15c7221046270fa83be19d --- /dev/null +++ b/funders/migrations/0004_auto_20180425_2146.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-25 19:46 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('funders', '0003_auto_20180425_2146'), + ] + + operations = [ + migrations.AlterField( + model_name='funder', + name='acronym', + field=models.CharField(blank=True, max_length=32), + ), + ] diff --git a/funders/migrations/0005_auto_20180425_2211.py b/funders/migrations/0005_auto_20180425_2211.py new file mode 100644 index 0000000000000000000000000000000000000000..0ed56330406865e3630eec772710cefc37bc016b --- /dev/null +++ b/funders/migrations/0005_auto_20180425_2211.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-25 20:11 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('funders', '0004_auto_20180425_2146'), + ] + + operations = [ + migrations.AlterField( + model_name='grant', + name='funder', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='grants', to='funders.Funder'), + ), + migrations.AlterField( + model_name='grant', + name='further_details', + field=models.CharField(blank=True, default='', max_length=256), + ), + migrations.AlterField( + model_name='grant', + name='recipient', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='grants', to='scipost.Contributor'), + ), + migrations.AlterField( + model_name='grant', + name='recipient_name', + field=models.CharField(blank=True, default='', max_length=64), + ), + ] diff --git a/funders/migrations/0006_auto_20180425_2212.py b/funders/migrations/0006_auto_20180425_2212.py new file mode 100644 index 0000000000000000000000000000000000000000..bc846c3962a333dce84a0d59ed9f0e99a72b681f --- /dev/null +++ b/funders/migrations/0006_auto_20180425_2212.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-25 20:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('funders', '0005_auto_20180425_2211'), + ] + + operations = [ + migrations.AlterField( + model_name='grant', + name='further_details', + field=models.CharField(blank=True, max_length=256), + ), + migrations.AlterField( + model_name='grant', + name='recipient_name', + field=models.CharField(blank=True, max_length=64), + ), + ] diff --git a/funders/models.py b/funders/models.py index 93410a0fe07f5e281309e0bcb32e8c84cfe761b6..ceefe67b5ec091fbfd4240e11ab90b604279bafa 100644 --- a/funders/models.py +++ b/funders/models.py @@ -8,16 +8,21 @@ from django.urls import reverse from journals.models import Publication +from .managers import FunderQuerySet + class Funder(models.Model): - """ + """Funder is a Fundref regsitry. + Funding info metadata is linked to funders from Crossref's - Fundref registry. """ + name = models.CharField(max_length=256) - acronym = models.CharField(max_length=32, blank=True, null=True) + acronym = models.CharField(max_length=32, blank=True) identifier = models.CharField(max_length=200, unique=True) + objects = FunderQuerySet.as_manager() + class Meta: ordering = ['name', 'acronym'] @@ -28,27 +33,31 @@ class Funder(models.Model): return result def get_absolute_url(self): + """Return the Funder detail page.""" return reverse('funders:funder_publications', args=(self.id,)) def all_related_publications(self): + """Return all Publication objects linked to this Funder.""" return Publication.objects.filter( Q(funders_generic=self) | Q(grants__funder=self)).distinct() class Grant(models.Model): - """ - An instance of a grant, award or other funding. + """An instance of a grant, award or other funding. + In a Publication's metadata, all grants are listed in the Crossmark part of the metadata. """ + funder = models.ForeignKey('funders.Funder', on_delete=models.CASCADE) number = models.CharField(max_length=64) - recipient_name = models.CharField(max_length=64, blank=True, null=True) + recipient_name = models.CharField(max_length=64, blank=True) recipient = models.ForeignKey('scipost.Contributor', blank=True, null=True, on_delete=models.CASCADE) - further_details = models.CharField(max_length=256, blank=True, null=True) + further_details = models.CharField(max_length=256, blank=True) class Meta: + default_related_name = 'grants' ordering = ['funder', 'recipient', 'recipient_name', 'number'] unique_together = ('funder', 'number') diff --git a/funders/templates/funders/base.html b/funders/templates/funders/base.html new file mode 100644 index 0000000000000000000000000000000000000000..b36e0fc49f13e65cb6024b6183a05889f0f4c8fa --- /dev/null +++ b/funders/templates/funders/base.html @@ -0,0 +1,13 @@ +{% extends 'scipost/base.html' %} + +{% block breadcrumb %} + <div class="container-outside header"> + <div class="container"> + <nav class="breadcrumb hidden-sm-down"> + {% block breadcrumb_items %} + <a href="{% url 'funders:funders' %}" class="breadcrumb-item">Funders</a> + {% endblock %} + </nav> + </div> + </div> +{% endblock %} diff --git a/funders/templates/funders/funder_details.html b/funders/templates/funders/funder_details.html index 382cb69bb5ec80832975e65ba26e9bf61f000bd5..9fd9b51b26bfd08ea977e54263ef1a86d481b053 100644 --- a/funders/templates/funders/funder_details.html +++ b/funders/templates/funders/funder_details.html @@ -1,16 +1,25 @@ -{% extends 'scipost/base.html' %} +{% extends 'funders/base.html' %} {% block pagetitle %}: Funder details{% endblock pagetitle %} +{% block breadcrumb_items %} + {{ block.super }} + <span class="breadcrumb-item">{{ funder.name }}</span> +{% endblock %} + {% block content %} <h1 class="highlight">Funder {{ funder.name }}</h1> -<h3>All Publications related to this Funder</h3> +<h3>All Publications related to {{ funder.name }}</h3> <ul> {% for publication in funder.all_related_publications %} - <li><a href="{{ publication.get_absolute_url }}">{{ publication }}</a></li> + <li> + <a href="{{ publication.get_absolute_url }}">{{ publication.title }}</a> + <br>by {{ publication.author_list }}, + <br>{{ publication.citation }} + </li> {% empty %} <li>No publications</li> {% endfor %} diff --git a/funders/templates/funders/funder_list.html b/funders/templates/funders/funder_list.html new file mode 100644 index 0000000000000000000000000000000000000000..ac4b9599470ee692c81d348970595963f9d90198 --- /dev/null +++ b/funders/templates/funders/funder_list.html @@ -0,0 +1,29 @@ +{% extends 'funders/base.html' %} + +{% block pagetitle %}: Funders list{% endblock pagetitle %} + +{% block breadcrumb_items %} + <span class="breadcrumb-item">Funders</span> +{% endblock %} + +{% block content %} + +<h1 class="highlight">Funders</h1> + +{% if perms.scipost.can_view_all_funding_info %} + <a href="{% url 'funders:funders_dashboard' %}">Go to dashboard</a> + <br><br> +{% endif %} + +<h3>All Funders with a SciPost publication</h3> + +<ul> + {% for funder in funders %} + <li><a href="{{ funder.get_absolute_url }}">{{ funder }}</a></li> + {% empty %} + <li>No funders</li> + {% endfor %} +</ul> + + +{% endblock content %} diff --git a/funders/templates/funders/funders.html b/funders/templates/funders/funders_dashboard.html similarity index 94% rename from funders/templates/funders/funders.html rename to funders/templates/funders/funders_dashboard.html index 0ab70cf7f9e3769407ee16b5fbf14ef0765ac7d9..16ea8e04a2b36caa4767ce575c9196d6126a1492 100644 --- a/funders/templates/funders/funders.html +++ b/funders/templates/funders/funders_dashboard.html @@ -1,6 +1,11 @@ -{% extends 'scipost/base.html' %} +{% extends 'funders/base.html' %} -{% block pagetitle %}: Funders{% endblock pagetitle %} +{% block pagetitle %}: Funders dashboard{% endblock pagetitle %} + +{% block breadcrumb_items %} + {{ block.super }} + <span class="breadcrumb-item">Dashboard</span> +{% endblock %} {% load bootstrap %} diff --git a/funders/urls.py b/funders/urls.py index 8ec6892cddbbc9a46582790535020b16c2f04190..48f99d288374cd7faded49940a731a2604568833 100644 --- a/funders/urls.py +++ b/funders/urls.py @@ -8,10 +8,10 @@ from . import views urlpatterns = [ url(r'^$', views.funders, name='funders'), + url(r'^dashboard$', views.funders_dashboard, name='funders_dashboard'), url(r'^query_crossref_for_funder$', views.query_crossref_for_funder, name='query_crossref_for_funder'), url(r'^add$', views.add_funder, name='add_funder'), - url(r'^(?P<funder_id>[0-9]+)/$', views.funder_publications, - name='funder_publications'), + url(r'^(?P<funder_id>[0-9]+)/$', views.funder_publications, name='funder_publications'), url(r'^grants/add$', views.CreateGrantView.as_view(), name='add_grant'), ] diff --git a/funders/views.py b/funders/views.py index 5e04d384fe374d608c3fe868ce0255d17307527d..f6b2d5b56af91fda9bcfe07df1596e76cfc660cb 100644 --- a/funders/views.py +++ b/funders/views.py @@ -20,14 +20,15 @@ from scipost.mixins import PermissionsMixin @permission_required('scipost.can_view_all_funding_info', raise_exception=True) -def funders(request): +def funders_dashboard(request): + """Administration of Funders and Grants.""" funders = Funder.objects.all() form = FunderRegistrySearchForm() grants = Grant.objects.all() grant_form = GrantForm(request=request) context = {'form': form, 'funders': funders, 'grants': grants, 'grant_form': grant_form} - return render(request, 'funders/funders.html', context) + return render(request, 'funders/funders_dashboard.html', context) @permission_required('scipost.can_view_all_funding_info', raise_exception=True) @@ -59,13 +60,20 @@ def add_funder(request): str(funder)) elif form.has_changed(): messages.warning(request, 'The form was invalidly filled.') - return redirect(reverse('funders:funders')) + return redirect(reverse('funders:funders_dashboard')) + + +def funders(request): + """List page of Funders.""" + funders = Funder.objects.has_publications().distinct() + context = { + 'funders': funders + } + return render(request, 'funders/funder_list.html', context) def funder_publications(request, funder_id): - """ - See details of specific Funder (publicly accessible). - """ + """Detail page of a specific Funder (publicly accessible).""" funder = get_object_or_404(Funder, id=funder_id) context = {'funder': funder} return render(request, 'funders/funder_details.html', context) @@ -91,4 +99,4 @@ class CreateGrantView(PermissionsMixin, HttpRefererMixin, CreateView): permission_required = 'scipost.can_create_grants' model = Grant form_class = GrantForm - success_url = reverse_lazy('funders:funders') + success_url = reverse_lazy('funders:funders_dashboard') diff --git a/invitations/admin.py b/invitations/admin.py index 59092e3c025f2d394d3a28100190ff2402eedb37..068ed5f3598d9def5645964aa8ae308ec7d23f64 100644 --- a/invitations/admin.py +++ b/invitations/admin.py @@ -20,7 +20,7 @@ admin.site.register(RegistrationInvitation, RegistrationInvitationAdmin) class CitationNotificationAdmin(admin.ModelAdmin): date_hierarchy = 'date_sent' search_fields = ['invitation__first_name', 'invitation__last_name', - 'contributor__first_name', 'contributor__last_name'] + 'contributor__user__first_name', 'contributor__user__last_name'] list_display = ['__str__', 'created_by', 'date_sent', 'processed'] list_filter = ['processed'] diff --git a/invitations/forms.py b/invitations/forms.py index 98da5c53684c3ff8be40188af9990244f8331d8b..d3e299992bed5d5fbc0147f778647042e8389e41 100644 --- a/invitations/forms.py +++ b/invitations/forms.py @@ -126,6 +126,66 @@ class RegistrationInvitationAddCitationForm(AcceptRequestMixin, forms.ModelForm) return self.instance +class RegistrationInvitationMergeForm(AcceptRequestMixin, forms.ModelForm): + """Merge RegistrationInvitations. + + This form will merge the instance with any other RegistrationInvitation selected + into a single RegistrationInvitation. + """ + + invitation = forms.ModelChoiceField(queryset=RegistrationInvitation.objects.none(), + label="Invitation to merge with") + + class Meta: + model = RegistrationInvitation + fields = () + + def __init__(self, *args, **kwargs): + """Update queryset according to the passed instance.""" + super().__init__(*args, **kwargs) + self.fields['invitation'].queryset = RegistrationInvitation.objects.no_response().filter( + last_name__icontains=self.instance.last_name).exclude(id=self.instance.id) + + def save(self, *args, **kwargs): + """Merge the two RegistationInvitations into one.""" + if kwargs.get('commit', True): + # Pick the right Invitation, with the most up-to-date invitation_key + selected_invitation = self.cleaned_data['invitation'] + if not selected_invitation.date_sent_last: + # Selected Invitation has never been sent yet. + leading_invitation = self.instance + deprecated_invitation = selected_invitation + elif not self.instance.date_sent_last: + # Instance has never been sent yet. + leading_invitation = selected_invitation + deprecated_invitation = self.instance + elif selected_invitation.date_sent_last > self.instance.date_sent_last: + # Lastest reminder: selected Invitation + leading_invitation = selected_invitation + deprecated_invitation = self.instance + else: + # Lastest reminder: instance + leading_invitation = self.instance + deprecated_invitation = selected_invitation + + # Move CitationNotification to the new leading Invitation + deprecated_invitation.citation_notifications.update(invitation=leading_invitation) + leading_invitation.times_sent += deprecated_invitation.times_sent # Update counts + leading_invitation.save() + + qs_contributor = deprecated_invitation.citation_notifications.filter( + contributor__isnull=False).values_list('contributor', flat=True) + if qs_contributor: + if not leading_invitation.citation_notifications.filter(contributor__isnull=False): + # Contributor is already assigned in "old" RegistrationInvitation, copy it. + leading_invitation.citation_notifications.filter(contributor=qs_contributor[0]) + + # Magic. + deprecated_invitation.delete() + return self.instance + + + class RegistrationInvitationForm(AcceptRequestMixin, forms.ModelForm): cited_in_submissions = AutoCompleteSelectMultipleField('submissions_lookup', required=False) cited_in_publications = AutoCompleteSelectMultipleField('publication_lookup', required=False) diff --git a/invitations/migrations/0013_auto_20180414_2053.py b/invitations/migrations/0013_auto_20180414_2053.py new file mode 100644 index 0000000000000000000000000000000000000000..8537b33a834a5aba7e52c4f69a5f36901b36828d --- /dev/null +++ b/invitations/migrations/0013_auto_20180414_2053.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-14 18:53 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('invitations', '0012_auto_20180220_2120'), + ] + + operations = [ + migrations.AlterField( + model_name='citationnotification', + name='invitation', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='citation_notifications', to='invitations.RegistrationInvitation'), + ), + ] diff --git a/invitations/models.py b/invitations/models.py index fc42000e0066051c1b2372bc44f7f923fa8839e0..9d1a8b8b5f687cab32aef07255f54f3bec428db1 100644 --- a/invitations/models.py +++ b/invitations/models.py @@ -98,7 +98,7 @@ class RegistrationInvitation(models.Model): class CitationNotification(models.Model): invitation = models.ForeignKey('invitations.RegistrationInvitation', - on_delete=models.SET_NULL, + on_delete=models.CASCADE, null=True, blank=True) contributor = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE, diff --git a/invitations/templates/invitations/registrationinvitation_form_merge.html b/invitations/templates/invitations/registrationinvitation_form_merge.html new file mode 100644 index 0000000000000000000000000000000000000000..3c284b5102cc9c3a5a7c25e9feeba03adf52da0c --- /dev/null +++ b/invitations/templates/invitations/registrationinvitation_form_merge.html @@ -0,0 +1,34 @@ +{% extends 'scipost/_personal_page_base.html' %} + +{% block pagetitle %}: Edit Registration Invitation{% endblock pagetitle %} + +{% load scipost_extras %} +{% load bootstrap %} + +{% block breadcrumb_items %} + {{block.super}} + <a href="{% url 'invitations:list' %}" class="breadcrumb-item">Registration Invitations</a> + <span class="breadcrumb-item">Edit</span> +{% endblock %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Registration Invitation {{ object.id }}</h1> + {% include 'partials/invitations/registrationinvitation_summary.html' with invitation=object %} + + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <button type="submit" class="btn btn-primary" name="save" value="save">Merge</button> + </form> + </div> +</div> + +{% endblock %} + +{% block footer_script %} + {{ block.super }} + {{ form.media }} +{% endblock footer_script %} diff --git a/invitations/templates/partials/invitations/registrationinvitation_table.html b/invitations/templates/partials/invitations/registrationinvitation_table.html index 1e85df48e5d4567cac19a3196e23f34ea2d32f2b..86ac4b72bca1963997f2cb59f82c325da7eb702d 100644 --- a/invitations/templates/partials/invitations/registrationinvitation_table.html +++ b/invitations/templates/partials/invitations/registrationinvitation_table.html @@ -53,6 +53,7 @@ </li> {% endfor %} <li><a href="{% url 'invitations:add_citation' invitation.id %}">Add new Citation to Invitation</a></li> + <li><a href="{% url 'invitations:merge' invitation.id %}">Merge this Invitation</a></li> </ul> </td> {% else %} diff --git a/invitations/urls.py b/invitations/urls.py index 5f69269f97c6686a4b19c097d58a38c5eac72124..3761e3a75cde19c91c7fa17c2b9fdf5adf48a072 100644 --- a/invitations/urls.py +++ b/invitations/urls.py @@ -8,7 +8,7 @@ from . import views urlpatterns = [ url(r'^$', views.RegistrationInvitationsView.as_view(), name='list'), - url(r'^sent$', views.RegistrationInvitationsSentView.as_view(), name='list_sent'), + url(r'^sent$', views.RegistrationInvitationsSendView.as_view(), name='list_sent'), url(r'^fellows$', views.RegistrationInvitationsFellowView.as_view(), name='list_fellows'), url(r'^new$', views.create_registration_invitation_or_citation, name='new'), url(r'^(?P<pk>[0-9]+)/$', views.RegistrationInvitationsUpdateView.as_view(), name='update'), @@ -16,6 +16,8 @@ urlpatterns = [ name='add_citation'), url(r'^(?P<pk>[0-9]+)/delete$', views.RegistrationInvitationsDeleteView.as_view(), name='delete'), + url(r'^(?P<pk>[0-9]+)/merge$', views.RegistrationInvitationsMergeView.as_view(), + name='merge'), url(r'^(?P<pk>[0-9]+)/mark/(?P<label>sent)$', views.RegistrationInvitationsMarkView.as_view(), name='mark'), url(r'^(?P<pk>[0-9]+)/map_to_contributor/(?P<contributor_id>[0-9]+)/$', diff --git a/invitations/views.py b/invitations/views.py index 3caa687f791cdd794f04df1d860c29ebef9f7fa0..d54c002300f5b2aa41fa581f72cb5a44303bface 100644 --- a/invitations/views.py +++ b/invitations/views.py @@ -13,7 +13,8 @@ from django.views.generic.edit import UpdateView, DeleteView from .forms import RegistrationInvitationForm, RegistrationInvitationReminderForm,\ RegistrationInvitationMarkForm, RegistrationInvitationMapToContributorForm,\ CitationNotificationForm, SuggestionSearchForm, RegistrationInvitationFilterForm,\ - CitationNotificationProcessForm, RegistrationInvitationAddCitationForm + CitationNotificationProcessForm, RegistrationInvitationAddCitationForm,\ + RegistrationInvitationMergeForm from .mixins import RequestArgumentMixin, SaveAndSendFormMixin, SendMailFormMixin from .models import RegistrationInvitation, CitationNotification @@ -43,8 +44,8 @@ class RegistrationInvitationsView(PaginationMixin, PermissionsMixin, ListView): return context -class RegistrationInvitationsSentView(RegistrationInvitationsView): - permission_required = 'scipost.can_create_registration_invitations' +class RegistrationInvitationsSendView(RegistrationInvitationsView): + permission_required = 'scipost.can_manage_registration_invitations' queryset = RegistrationInvitation.objects.sent().not_for_fellows() template_name = 'invitations/registrationinvitation_list_sent.html' @@ -162,6 +163,14 @@ class RegistrationInvitationsUpdateView(RequestArgumentMixin, PermissionsMixin, return qs +class RegistrationInvitationsMergeView(RequestArgumentMixin, PermissionsMixin, UpdateView): + permission_required = 'scipost.can_manage_registration_invitations' + queryset = RegistrationInvitation.objects.no_response() + form_class = RegistrationInvitationMergeForm + template_name = 'invitations/registrationinvitation_form_merge.html' + success_url = reverse_lazy('invitations:list') + + class RegistrationInvitationsAddCitationView(RequestArgumentMixin, PermissionsMixin, UpdateView): permission_required = 'scipost.can_create_registration_invitations' form_class = RegistrationInvitationAddCitationForm diff --git a/journals/migrations/0027_auto_20180414_2053.py b/journals/migrations/0027_auto_20180414_2053.py new file mode 100644 index 0000000000000000000000000000000000000000..165078a172a03bde9e214369a8d6f04226dcd2ef --- /dev/null +++ b/journals/migrations/0027_auto_20180414_2053.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-14 18:53 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0026_auto_20180327_1937'), + ] + + operations = [ + migrations.AlterField( + model_name='publication', + name='doi_label', + field=models.CharField(db_index=True, max_length=200, unique=True, validators=[django.core.validators.RegexValidator('^(SciPostPhysProc|SciPostPhysSel|SciPostPhysLectNotes|SciPostPhys).[0-9]+(.[0-9]+.[0-9]{3,})?$', 'Only valid DOI expressions are allowed (`[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,}` or `[a-zA-Z]+.[0-9]+`)')]), + ), + ] diff --git a/journals/templates/journals/journals.html b/journals/templates/journals/journals.html index a696b5cd0184ce4be0100e0afae9a8c1dbcf23cd..2dad808af08ed6a0f3266302465db2b77a04aab4 100644 --- a/journals/templates/journals/journals.html +++ b/journals/templates/journals/journals.html @@ -69,13 +69,6 @@ </div> <div class="row"> - <div class="col-md-6"> - <h1 class="banner">SciPost Physics Select</h1> - <div class="py-2"> - <p>SciPost Physics Select publishes articles of superlative quality in the field of Physics.</p> - <p>Authors cannot submit directly to this Journal; SPS papers are editorially selected from the most outstanding Submissions to SciPost Physics.</p> - </div> - </div> <div class="col-md-6"> <h1 class="banner"><a href="{% url 'scipost:landing_page' 'SciPostPhysLectNotes' %}">SciPost Physics Lecture Notes</a></h1> <div class="py-2"> diff --git a/journals/templates/journals/manage_metadata.html b/journals/templates/journals/manage_metadata.html index bcaa4e0c9daaf8e0c5ddaf74f95195bfdb1f4327..0d240c96c2169ffb44cecf3df8ee4ade5f474373 100644 --- a/journals/templates/journals/manage_metadata.html +++ b/journals/templates/journals/manage_metadata.html @@ -150,7 +150,7 @@ event: "focusin" <br/> <h3>Other funding-related actions:</h3> <ul> - <li><a href="{% url 'funders:funders' %}" target="_blank">go to the Funders page to add a Funder and/or Grant instance</a></li> + <li><a href="{% url 'funders:funders_dashboard' %}" target="_blank">go to the Funders page to add a Funder and/or Grant instance</a></li> </ul> </div> </div> diff --git a/journals/templates/journals/publication_detail.html b/journals/templates/journals/publication_detail.html index 67892330e77d5b979ec472bfd0b120cc4136fc55..9566a22849031cf93121803010d500cfd1808d0e 100644 --- a/journals/templates/journals/publication_detail.html +++ b/journals/templates/journals/publication_detail.html @@ -107,30 +107,22 @@ {% endfor %} </ul> + {% if publication.funders_generic.all %} + <h3>Funder{{ publication.funders_generic.count|pluralize }} for this publication</h3> + <ul> + {% for funder in publication.funders_generic.all %} + <li><a href="{{ funder.get_absolute_url }}">{{ funder }}</a></li> + {% endfor %} + </ul> + {% endif %} - - {% if is_edcol_admin %} - {# This function is not available for public yet! #} - <em>The following is not available for the public yet:</em> - {% include 'partials/journals/references.html' with publication=publication %} - - {% if publication.funders_generic.exists %} - <h3>Funder{{ publication.funders_generic.count|pluralize }} for this publication:</h3> - <ul> - {% for funder in publication.funders_generic.all %} - <li><a href="{{ funder.get_absolute_url }}">{{ funder }}</a></li> - {% endfor %} - </ul> - {% endif %} - - {% if publication.institutions.exists %} - <h3>Institution{{ publication.institutions.count|pluralize }} related to this Publication:</h3> - <ul> - {% for institution in publication.institutions.all %} - <li>{{ institution }}</li> - {% endfor %} - </ul> - {% endif %} + {% if publication.institutions.all %} + <h3>Institution{{ publication.institutions.count|pluralize }} related to this Publication</h3> + <ul> + {% for institution in publication.institutions.all %} + <li><a href="{{ institution.get_absolute_url }}">{{ institution }}</a></li> + {% endfor %} + </ul> {% endif %} </div> </div> diff --git a/journals/views.py b/journals/views.py index a0aec70a3a39d85dec00399cedb61fea24bed4e5..a5fee852fb4deb606f67f47e472de14bb46f6a17 100644 --- a/journals/views.py +++ b/journals/views.py @@ -1152,6 +1152,6 @@ def arxiv_doi_feed(request, doi_label): for publication in publications: feedxml += ('\n<article preprint_id="%s" doi="%s" journal_ref="%s" />' % ( publication.accepted_submission.arxiv_identifier_wo_vn_nr, publication.doi_string, - publication.citation())) + publication.citation)) feedxml += '\n</preprint>' return HttpResponse(feedxml, content_type='text/xml') diff --git a/mailing_lists/models.py b/mailing_lists/models.py index 5d6bdfe0d80e987b1ade7a7afa459288cc54f5d5..e9ccc0b32da6c800d3179cda24d3a3adb32cc709 100644 --- a/mailing_lists/models.py +++ b/mailing_lists/models.py @@ -17,7 +17,7 @@ from .constants import MAIL_LIST_STATUSES, MAIL_LIST_STATUS_ACTIVE,\ from .managers import MailListManager from scipost.behaviors import TimeStampedModel -from scipost.constants import CONTRIBUTOR_NORMAL +from scipost.constants import NORMAL_CONTRIBUTOR from scipost.models import Contributor @@ -88,7 +88,7 @@ class MailchimpList(TimeStampedModel): # are not in the list yet. db_subscribers = (User.objects .filter(contributor__isnull=False) - .filter(is_active=True, contributor__status=CONTRIBUTOR_NORMAL) + .filter(is_active=True, contributor__status=NORMAL_CONTRIBUTOR) .filter(contributor__accepts_SciPost_emails=True, groups__in=self.allowed_groups.all(), email__isnull=False, diff --git a/mails/templates/mail_templates/registration_invitation.html b/mails/templates/mail_templates/registration_invitation.html index fceaa5f9956115a983731e522ee8a74e28fb4f16..beae41fdc428089bb93649de69e6512c273eeac2 100644 --- a/mails/templates/mail_templates/registration_invitation.html +++ b/mails/templates/mail_templates/registration_invitation.html @@ -80,7 +80,7 @@ Dear {% if invitation.message_style == 'F' %}{{ invitation.get_title_display }} </ul> {% endif %} <p> - I would hereby like to use this opportunity to quickly introduce you to the SciPost initiative, and to invite you to become an active Contributor. + We would hereby like to use this opportunity to quickly introduce you to the SciPost initiative, and to invite you to become an active Contributor. </p> <p> In summary, SciPost.org is a publication portal managed by @@ -115,10 +115,12 @@ Dear {% if invitation.message_style == 'F' %}{{ invitation.get_title_display }} <p> Many thanks in advance for taking a few minutes to look into it, <br> - On behalf of the SciPost Foundation, - <br> - <br> - {{ invitation.invited_by.contributor.get_title_display }} {{ invitation.invited_by.first_name }} {{ invitation.invited_by.last_name }} + <br>SciPost Foundation + <br>Institute for Theoretial Physics + <br>University of Amsterdam + <br>Science Park 904 + <br>1098 XH Amsterdam + <br>The Netherlands </p> {% elif invitation.invitation_type == 'F' %} {# Fellow invite #} diff --git a/mails/templates/mail_templates/submissions_assignment_failed.html b/mails/templates/mail_templates/submissions_assignment_failed.html new file mode 100644 index 0000000000000000000000000000000000000000..382b4217405c28280daa788d592d42fab903fd3c --- /dev/null +++ b/mails/templates/mail_templates/submissions_assignment_failed.html @@ -0,0 +1,17 @@ +<p>Dear {{ submission.submitted_by.get_title_display }} {{ submission.submitted_by.user.last_name }},</p> +<p>Your recent Submission to SciPost,</p> +<p>{{ submission.title }}</p> +<p>by {{ submission.author_list }}</p> +<p> + has unfortunately not passed the pre-screening stage. + We therefore regret to inform you that we will not + process your paper further towards publication, and that you + are now free to send your manuscript to an alternative journal. +</p> + + +<p>We nonetheless thank you very much for your contribution.</p> +<p>Sincerely,</p> +<p>The SciPost Team.</p> + +{% include 'email/_footer.html' %} diff --git a/mails/templates/mail_templates/submissions_assignment_failed.json b/mails/templates/mail_templates/submissions_assignment_failed.json new file mode 100644 index 0000000000000000000000000000000000000000..9b1051a0bc9cdfdbf1e524a93326f6e2f45b55fd --- /dev/null +++ b/mails/templates/mail_templates/submissions_assignment_failed.json @@ -0,0 +1,8 @@ +{ + "subject": "SciPost: pre-screening not passed", + "to_address": "submitted_by.user.email", + "bcc_to": "submissions@scipost.org", + "from_address_name": "SciPost Editorial Admin", + "from_address": "submissions@scipost.org", + "context_object": "submission" +} diff --git a/mails/templates/mails/mail_form.html b/mails/templates/mails/mail_form.html index f1afc1a879536a84a3fe69de45cb7930ce025d19..6bae596d97c8f88e000f6f83829e5c554c974e64 100644 --- a/mails/templates/mails/mail_form.html +++ b/mails/templates/mails/mail_form.html @@ -6,7 +6,13 @@ {% block content %} - <h1>Complete and send mail</h1> + {% if header_template %} + {% include header_template with object=object %} + <hr class="divider"> + <h2 class="highlight">Complete and send mail</h2> + {% else %} + <h1 class="highlight">Complete and send mail</h1> + {% endif %} <h3 class="mb-4">You may edit the mail before sending it.</h3> <form enctype="multipart/form-data" method="post"> diff --git a/mails/views.py b/mails/views.py index 6132d392342310e7dff63ef38295450998d5f0f7..36578a74a7baa349e13f3a40723c0ff63ae61cc4 100644 --- a/mails/views.py +++ b/mails/views.py @@ -15,6 +15,7 @@ class MailEditingSubView(object): self.request = request self.context = kwargs.get('context', {}) self.template_name = kwargs.get('template', 'mails/mail_form.html') + self.header_template = kwargs.get('header_template', '') self.mail_form = EmailTemplateForm(request.POST or None, mail_code=mail_code, **kwargs) @property @@ -38,6 +39,11 @@ class MailEditingSubView(object): def return_render(self): self.context['form'] = self.mail_form + self.context['header_template'] = self.header_template + if hasattr(self.mail_form, 'instance') and self.mail_form.instance: + self.context['object'] = self.mail_form.instance + else: + self.context['object'] = None return render(self.request, self.template_name, self.context) diff --git a/mails/widgets.py b/mails/widgets.py index f9cfaf114b35723693da4d54e6d187cd2967829a..d13bcf86bc4f2a7111d0cfc96f5d0e5af5ab4f53 100644 --- a/mails/widgets.py +++ b/mails/widgets.py @@ -52,9 +52,8 @@ class SummernoteEditor(widgets.Textarea): def trigger_summernote(self, el_id, options): str = """ <script type='text/javascript'> - var $ = jQuery; - $(document).ready(function() { - $('#%s').summernote(%s) + $(function() { + $('#%s').summernote(%s); }); </script>""" % (el_id, options) return str @@ -66,7 +65,7 @@ class SummernoteEditor(widgets.Textarea): } js = ('//cdnjs.cloudflare.com/ajax/libs/summernote/0.8.8/summernote-bs4.js',) - if self.include_jquery: - js = ('//cdnjs.cloudflare.com/ajax/libs/jquery/1.11.0/jquery.min.js',) + js + # if self.include_jquery: + # js = ('//cdnjs.cloudflare.com/ajax/libs/jquery/1.11.0/jquery.min.js',) + js return Media(css=css, js=js) diff --git a/notifications/templates/notifications/partials/notification_list_popover.html b/notifications/templates/notifications/partials/notification_list_popover.html index 89f08094fdb87affd7f7d7f830c5695c988ffe0a..3f16c462650a4c636afde7c90ebd8d78bf609d74 100644 --- a/notifications/templates/notifications/partials/notification_list_popover.html +++ b/notifications/templates/notifications/partials/notification_list_popover.html @@ -31,7 +31,7 @@ {% endif %} {% if perms.scipost.can_view_all_funding_info %} - <a class="item {% active 'funders:funders' %}" href="{% url 'funders:funders' %}">Funders</a> + <a class="item {% active 'funders:funders_dashboard' %}" href="{% url 'funders:funders_dashboard' %}">Funders</a> {% endif %} {% if perms.scipost.can_view_production %} diff --git a/partners/models.py b/partners/models.py index 966a782317001e39eb221f954b9a3b4e03110657..d8c0b21da83faa3fade57d4fcbbf8ffd902c07f7 100644 --- a/partners/models.py +++ b/partners/models.py @@ -14,22 +14,20 @@ from django.urls import reverse from django_countries.fields import CountryField -from .constants import PARTNER_KINDS, PARTNER_STATUS, CONSORTIUM_STATUS, MEMBERSHIP_DURATION,\ - PROSPECTIVE_PARTNER_STATUS, PROSPECTIVE_PARTNER_EVENTS, PARTNER_EVENTS,\ - MEMBERSHIP_AGREEMENT_STATUS, PROSPECTIVE_PARTNER_ADDED,\ - PARTNER_KIND_UNI_LIBRARY -from .constants import PROSPECTIVE_PARTNER_EVENT_EMAIL_SENT,\ - PROSPECTIVE_PARTNER_APPROACHED,\ - PROSPECTIVE_PARTNER_EVENT_INITIATE_NEGOTIATION,\ - PROSPECTIVE_PARTNER_NEGOTIATING,\ - PROSPECTIVE_PARTNER_EVENT_MARKED_AS_UNINTERESTED,\ - PROSPECTIVE_PARTNER_UNINTERESTED,\ - PROSPECTIVE_PARTNER_EVENT_PROMOTED,\ - PROSPECTIVE_PARTNER_PROCESSED, CONTACT_TYPES,\ - PARTNER_INITIATED, REQUEST_STATUSES, REQUEST_INITIATED - -from .managers import MembershipAgreementManager, ProspectivePartnerManager, PartnerManager,\ - ContactRequestManager, PartnersAttachmentManager +from .constants import ( + PARTNER_KINDS, PARTNER_STATUS, CONSORTIUM_STATUS, MEMBERSHIP_DURATION, PARTNER_EVENTS, + PROSPECTIVE_PARTNER_STATUS, PROSPECTIVE_PARTNER_EVENTS, MEMBERSHIP_AGREEMENT_STATUS, + PROSPECTIVE_PARTNER_ADDED, PARTNER_KIND_UNI_LIBRARY) +from .constants import ( + PROSPECTIVE_PARTNER_EVENT_EMAIL_SENT, PROSPECTIVE_PARTNER_APPROACHED, PARTNER_INITIATED, + PROSPECTIVE_PARTNER_EVENT_INITIATE_NEGOTIATION, PROSPECTIVE_PARTNER_PROCESSED, CONTACT_TYPES, + PROSPECTIVE_PARTNER_NEGOTIATING, PROSPECTIVE_PARTNER_EVENT_MARKED_AS_UNINTERESTED, + REQUEST_STATUSES, PROSPECTIVE_PARTNER_UNINTERESTED, PROSPECTIVE_PARTNER_EVENT_PROMOTED, + REQUEST_INITIATED) + +from .managers import ( + MembershipAgreementManager, ProspectivePartnerManager, PartnerManager, ContactRequestManager, + PartnersAttachmentManager) from scipost.constants import TITLE_CHOICES from scipost.fields import ChoiceArrayField @@ -44,9 +42,8 @@ now = timezone.now() ######################## class ProspectivePartner(models.Model): - """ - Created from the membership_request page, after submitting a query form. - """ + """A prospect Partner is a Partner without explicit contract with SciPost yet.""" + kind = models.CharField(max_length=32, choices=PARTNER_KINDS, default=PARTNER_KIND_UNI_LIBRARY) institution_name = models.CharField(max_length=256) country = CountryField() @@ -62,6 +59,11 @@ class ProspectivePartner(models.Model): self.date_received.strftime("%Y-%m-%d"), self.get_status_display()) + @property + def is_promoted_to_partner(self): + """Check if Prospect is already known to be a Partner.""" + return self.status == PROSPECTIVE_PARTNER_PROCESSED + def update_status_from_event(self, event): if event == PROSPECTIVE_PARTNER_EVENT_EMAIL_SENT: self.status = PROSPECTIVE_PARTNER_APPROACHED diff --git a/partners/templates/partners/_prospective_partner_card.html b/partners/templates/partners/_prospective_partner_card.html index 48c903504c317ae88d07ef63fdbd60c621bbd770..9a56f10cd941f5f431d31d4735cacc6ce8ea2402 100644 --- a/partners/templates/partners/_prospective_partner_card.html +++ b/partners/templates/partners/_prospective_partner_card.html @@ -70,10 +70,12 @@ <input type="submit" name="submit" value="Submit" class="btn btn-outline-secondary"> </form> - <h3>Partner status</h3> - <ul> - <li><a href="{% url 'partners:promote_prospartner' pp.id %}">Upgrade prospect to partner</a></li> - </ul> + {% if not pp.is_promoted_to_partner %} + <h3>Partner status</h3> + <ul> + <li><a href="{% url 'partners:promote_prospartner' pp.id %}">Upgrade prospect to partner</a></li> + </ul> + {% endif %} </div> </div> </div> diff --git a/partners/views.py b/partners/views.py index a17405729386aea2af58cb53b24854e185c9e4d0..9c5bf531d0eddab20e19d08ebb22f4480caff132 100644 --- a/partners/views.py +++ b/partners/views.py @@ -47,10 +47,11 @@ def supporting_partners(request): @login_required @permission_required('scipost.can_read_partner_page', return_403=True) def dashboard(request): - ''' + """Administration page for Partners and Prospective Partners. + This page is meant as a personal page for Partners, where they will for example be able to read their personal data and agreements. - ''' + """ context = {} try: context['personal_agreements'] = (MembershipAgreement.objects.open_to_partner() @@ -62,8 +63,8 @@ def dashboard(request): context['contact_requests_count'] = ContactRequest.objects.awaiting_processing().count() context['inactivate_contacts_count'] = Contact.objects.filter(user__is_active=False).count() context['partners'] = Partner.objects.all() - context['prospective_partners'] = (ProspectivePartner.objects.not_yet_partner() - .order_by('country', 'institution_name')) + context['prospective_partners'] = ProspectivePartner.objects.order_by( + 'country', 'institution_name') context['ppevent_form'] = ProspectivePartnerEventForm() context['agreements'] = MembershipAgreement.objects.order_by('date_requested') return render(request, 'partners/dashboard.html', context) diff --git a/scipost/constants.py b/scipost/constants.py index 44c02c3b232db385cc9015db6a4c691585d2c322..dd7a72d42083715dfc7d0fa3db8fbca06b4c86f1 100644 --- a/scipost/constants.py +++ b/scipost/constants.py @@ -29,8 +29,7 @@ SCIPOST_SUBJECT_AREAS = ( ('Phys:NE', 'Nuclear Physics - Experiment'), ('Phys:NT', 'Nuclear Physics - Theory'), ('Phys:QP', 'Quantum Physics'), - ('Phys:SM', 'Statistical and Soft Matter Physics'), - ) + ('Phys:SM', 'Statistical and Soft Matter Physics')) ), ('Astrophysics', ( ('Astro:GA', 'Astrophysics of Galaxies'), @@ -38,8 +37,7 @@ SCIPOST_SUBJECT_AREAS = ( ('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'), - ) + ('Astro:SR', 'Solar and Stellar Astrophysics')) ), ('Mathematics', ( ('Math:AG', 'Algebraic Geometry'), @@ -73,8 +71,7 @@ SCIPOST_SUBJECT_AREAS = ( ('Math:RA', 'Rings and Algebras'), ('Math:SP', 'Spectral Theory'), ('Math:ST', 'Statistics Theory'), - ('Math:SG', 'Symplectic Geometry'), - ) + ('Math:SG', 'Symplectic Geometry')) ), ('Computer Science', ( ('Comp:AI', 'Artificial Intelligence'), @@ -115,9 +112,8 @@ SCIPOST_SUBJECT_AREAS = ( ('Comp:SE', 'Software Engineering'), ('Comp:SD', 'Sound'), ('Comp:SC', 'Symbolic Computation'), - ('Comp:SY', 'Systems and Control'), - ) - ), + ('Comp:SY', 'Systems and Control')) + ) ) subject_areas_raw_dict = dict(SCIPOST_SUBJECT_AREAS) @@ -126,25 +122,20 @@ subject_areas_dict = {} for k in subject_areas_raw_dict.keys(): subject_areas_dict.update(dict(subject_areas_raw_dict[k])) -CONTRIBUTOR_NEWLY_REGISTERED = 0 -CONTRIBUTOR_NORMAL = 1 -CONTRIBUTOR_STATUS = ( - # status determine the type of Contributor: - # 0: newly registered (unverified; not allowed to submit, comment or vote) - # 1: contributor has been vetted through - # - # Negative status denotes rejected requests or: - # -1: not a professional scientist (>= PhD student in known university) - # -2: other account already exists for this person - # -3: barred from SciPost (abusive behaviour) - # -4: disabled account (deceased) - (CONTRIBUTOR_NEWLY_REGISTERED, 'newly registered'), - (CONTRIBUTOR_NORMAL, 'normal user'), - (-1, 'not a professional scientist'), # Soon to be deprecated - (-2, 'other account already exists'), - (-3, 'barred from SciPost'), - (-4, 'account disabled'), - ) +# Contributor types +NEWLY_REGISTERED, NORMAL_CONTRIBUTOR = 'newly_registered', 'normal' +NO_SCIENTIST, DOUBLE_ACCOUNT, OUT_OF_ACADEMIA = 'no_scientist', 'double_account', 'out_of_academia' +BARRED, DISABLED, DECEASED = 'barred', 'disabled', 'deceased' +CONTRIBUTOR_STATUSES = ( + (NEWLY_REGISTERED, 'Newly registered'), + (NORMAL_CONTRIBUTOR, 'Normal user'), + (NO_SCIENTIST, 'Not a professional scientist'), + (DOUBLE_ACCOUNT, 'Other account already exists'), + (OUT_OF_ACADEMIA, 'Out of academia'), + (BARRED, 'Barred from SciPost'), + (DISABLED, 'Account disabled'), + (DECEASED, 'Person deceased') +) TITLE_CHOICES = ( ('PR', 'Prof.'), diff --git a/scipost/decorators.py b/scipost/decorators.py index cebaccc0221244949c78412b12bd918903266fda..fea4dc361ee19a9b8c0909cdea45f6be1b3074ff 100644 --- a/scipost/decorators.py +++ b/scipost/decorators.py @@ -2,13 +2,24 @@ __copyright__ = "Copyright 2016-2018, Stichting SciPost (SciPost Foundation)" __license__ = "AGPL v3" +from django.contrib.auth.decorators import user_passes_test + from .models import Contributor def has_contributor(user): - """Requires user to be related to any Contributor.""" + """Require user to be related to any Contributor.""" try: user.contributor return True except Contributor.DoesNotExist: return False + + +def is_contributor_user(): + """Dceorator checking if user is related to any Contributor.""" + def test(u): + if u.is_authenticated(): + return has_contributor(u) + return False + return user_passes_test(test) diff --git a/scipost/factories.py b/scipost/factories.py index 4c7e91d45a9430c899f093416fc7a36e69b5e57f..d058e515701c72e7a0229d2e79af31867d51fd55 100644 --- a/scipost/factories.py +++ b/scipost/factories.py @@ -18,7 +18,7 @@ from .constants import TITLE_CHOICES, SCIPOST_SUBJECT_AREAS class ContributorFactory(factory.django.DjangoModelFactory): title = factory.Iterator(TITLE_CHOICES, getter=lambda c: c[0]) user = factory.SubFactory('scipost.factories.UserFactory', contributor=None) - status = 1 # normal user + status = 'normal' # normal user vetted_by = factory.Iterator(Contributor.objects.all()) personalwebpage = factory.Faker('uri') expertises = factory.Iterator(SCIPOST_SUBJECT_AREAS[0][1], getter=lambda c: [c[0]]) diff --git a/scipost/feeds.py b/scipost/feeds.py index 8417bbdc3c5dce05ba8aab4441d1bcbfc97d5912..e5426b9bf44f00ca6709654d9637da739cc56dea 100644 --- a/scipost/feeds.py +++ b/scipost/feeds.py @@ -11,11 +11,13 @@ from django.core.urlresolvers import reverse from django.db.models import Q from comments.models import Comment +from commentaries.models import Commentary from journals.models import Publication from news.models import NewsItem from scipost.models import subject_areas_dict from submissions.constants import SUBMISSION_STATUS_PUBLICLY_INVISIBLE from submissions.models import Submission +from theses.models import ThesisLink class LatestCommentsFeedRSS(Feed): @@ -24,7 +26,7 @@ class LatestCommentsFeedRSS(Feed): link = "/comments/" def items(self): - return Comment.objects.filter(status__gte=0).order_by('-date_submitted')[:10] + return Comment.objects.vetted().order_by('-date_submitted')[:10] def item_title(self, item): return item.comment_text[:50] @@ -33,14 +35,14 @@ class LatestCommentsFeedRSS(Feed): return item.comment_text[:50] def item_link(self, item): - if item.commentary: + if isinstance(item.content_object, Commentary): return reverse('commentaries:commentary', - kwargs={'arxiv_or_DOI_string': item.commentary.arxiv_or_DOI_string}) - elif item.submission: + kwargs={'arxiv_or_DOI_string': item.content_object.arxiv_or_DOI_string}) + elif isinstance(item.content_object, Submission): return reverse('submissions:submission', kwargs={'arxiv_identifier_w_vn_nr': - item.submission.arxiv_identifier_w_vn_nr}) - elif item.thesislink: + item.content_object.arxiv_identifier_w_vn_nr}) + elif isinstance(item.content_object, ThesisLink): return reverse('theses:thesis', kwargs={'thesislink_id': item.thesislink.id}) else: diff --git a/scipost/forms.py b/scipost/forms.py index 73a2a142d803ee568904776fb34bd6349caab0a7..7beb0185ee5b3c26ef020df792e04b290ff2effd 100644 --- a/scipost/forms.py +++ b/scipost/forms.py @@ -24,10 +24,11 @@ from ajax_select.fields import AutoCompleteSelectField from haystack.forms import ModelSearchForm as HayStackSearchForm from .behaviors import orcid_validator -from .constants import SCIPOST_DISCIPLINES, TITLE_CHOICES, SCIPOST_FROM_ADDRESSES +from .constants import ( + SCIPOST_DISCIPLINES, TITLE_CHOICES, SCIPOST_FROM_ADDRESSES, NO_SCIENTIST, DOUBLE_ACCOUNT, + BARRED) from .decorators import has_contributor -from .models import Contributor, DraftInvitation,\ - UnavailabilityPeriod, PrecookedEmail +from .models import Contributor, DraftInvitation, UnavailabilityPeriod, PrecookedEmail from affiliations.models import Affiliation, Institution from common.forms import MonthYearWidget @@ -39,11 +40,11 @@ from submissions.models import Report REGISTRATION_REFUSAL_CHOICES = ( - (0, '-'), - (-1, 'not a professional scientist (>= PhD student)'), - (-2, 'another account already exists for this person'), - (-3, 'barred from SciPost (abusive behaviour)'), - ) + (None, '-'), + (NO_SCIENTIST, 'not a professional scientist (>= PhD student)'), + (DOUBLE_ACCOUNT, 'another account already exists for this person'), + (BARRED, 'barred from SciPost (abusive behaviour)') +) reg_ref_dict = dict(REGISTRATION_REFUSAL_CHOICES) @@ -80,10 +81,10 @@ class RegistrationForm(forms.Form): last_name = forms.CharField(label='* Last name', max_length=100) email = forms.EmailField(label='* Email address') invitation_key = forms.CharField(max_length=40, widget=forms.HiddenInput(), required=False) - orcid_id = forms.CharField(label="ORCID id", max_length=20, required=False, - validators=[orcid_validator], - widget=forms.TextInput( - {'placeholder': 'Recommended. Get one at orcid.org'})) + orcid_id = forms.CharField( + label="ORCID id", max_length=20, required=False, validators=[orcid_validator], + widget=forms.TextInput({ + 'placeholder': 'Recommended. Get one at orcid.org'})) discipline = forms.ChoiceField(choices=SCIPOST_DISCIPLINES, label='* Main discipline') country_of_employment = LazyTypedChoiceField( choices=countries, label='* Country of employment', initial='NL', @@ -93,12 +94,10 @@ class RegistrationForm(forms.Form): affiliation = forms.CharField(label='* Affiliation', max_length=300) address = forms.CharField( label='Address', max_length=1000, - widget=forms.TextInput({'placeholder': 'For postal correspondence'}), - required=False) + widget=forms.TextInput({'placeholder': 'For postal correspondence'}), required=False) personalwebpage = forms.URLField( - label='Personal web page', - widget=forms.TextInput({'placeholder': 'full URL, e.g. http://www.[yourpage].com'}), - required=False) + label='Personal web page', required=False, + widget=forms.TextInput({'placeholder': 'full URL, e.g. http://www.[yourpage].com'})) username = forms.CharField(label='* Username', max_length=100) password = forms.CharField(label='* Password', widget=forms.PasswordInput()) password_verif = forms.CharField(label='* Verify password', widget=forms.PasswordInput(), @@ -283,8 +282,9 @@ class AuthenticationForm(forms.Form): next = forms.CharField(widget=forms.HiddenInput(), required=False) def user_is_inactive(self): - """ - Check if the User is active but only if the password is valid, to prevent any + """Check if the User is active only if the password is valid. + + Only check to prevent any possible clue (?) of the password. """ username = self.cleaned_data['username'] diff --git a/scipost/management/commands/setup_contributor.py b/scipost/management/commands/setup_contributor.py index c4798f122ef1db1b8029c14139b0a52541e95ffd..f4b98c3abb3404ebc1730601a1417ecc482dfc82 100644 --- a/scipost/management/commands/setup_contributor.py +++ b/scipost/management/commands/setup_contributor.py @@ -5,6 +5,7 @@ __license__ = "AGPL v3" from django.core.management.base import BaseCommand from django.contrib.auth.models import User +from ...constants import NORMAL_CONTRIBUTOR from ...models import Contributor @@ -16,7 +17,7 @@ class Command(BaseCommand): def create_contributor(self, username): user = User.objects.get(username=username) - contributor = Contributor(user=user, status=1, title="MR") + contributor = Contributor(user=user, status=NORMAL_CONTRIBUTOR, title="MR") contributor.vetted_by = contributor contributor.save() diff --git a/scipost/managers.py b/scipost/managers.py index b552f3adff613cfc6d807ce8c9e817339e7ff171..df5a60d44e538eeee8c77149556af1af3f07822e 100644 --- a/scipost/managers.py +++ b/scipost/managers.py @@ -6,7 +6,7 @@ from django.db import models from django.db.models import Q from django.utils import timezone -from .constants import CONTRIBUTOR_NORMAL, CONTRIBUTOR_NEWLY_REGISTERED, AUTHORSHIP_CLAIM_PENDING +from .constants import NORMAL_CONTRIBUTOR, NEWLY_REGISTERED, AUTHORSHIP_CLAIM_PENDING today = timezone.now().date() @@ -23,7 +23,7 @@ class FellowManager(models.Manager): class ContributorQuerySet(models.QuerySet): def active(self): - return self.filter(user__is_active=True, status=CONTRIBUTOR_NORMAL) + return self.filter(user__is_active=True, status=NORMAL_CONTRIBUTOR) def available(self): return self.exclude( @@ -31,10 +31,10 @@ class ContributorQuerySet(models.QuerySet): unavailability_periods__end__gte=today) def awaiting_validation(self): - return self.filter(user__is_active=False, status=CONTRIBUTOR_NEWLY_REGISTERED) + return self.filter(user__is_active=False, status=NEWLY_REGISTERED) def awaiting_vetting(self): - return self.filter(user__is_active=True, status=CONTRIBUTOR_NEWLY_REGISTERED) + return self.filter(user__is_active=True, status=NEWLY_REGISTERED) def fellows(self): return self.filter(user__groups__name='Editorial College') diff --git a/scipost/migrations/0011_contributor_new_status.py b/scipost/migrations/0011_contributor_new_status.py new file mode 100644 index 0000000000000000000000000000000000000000..7ad51995a44018ac139414bc2d66744c5dfd611b --- /dev/null +++ b/scipost/migrations/0011_contributor_new_status.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-14 20:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0010_merge_20180327_2022'), + ] + + operations = [ + migrations.AddField( + model_name='contributor', + name='new_status', + field=models.CharField(choices=[('newly_registered', 'Newly registered'), ('normal', 'Normal user'), ('no_scientist', 'Not a professional scientist'), ('double_account', 'Other account already exists'), ('out_of_academia', 'Out of academia'), ('barred', 'Barred from SciPost'), ('disabled', 'Account disabled'), ('deceased', 'Person deceased')], default='newly_registered', max_length=16), + ), + ] diff --git a/scipost/migrations/0012_auto_20180414_2212.py b/scipost/migrations/0012_auto_20180414_2212.py new file mode 100644 index 0000000000000000000000000000000000000000..c3faed8fd33b8ffbd9a04432e42a627949b3b9ab --- /dev/null +++ b/scipost/migrations/0012_auto_20180414_2212.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-14 20:12 +from __future__ import unicode_literals + +from django.db import migrations + +def update_contributor_status_field(apps, schema_editor): + Contributor = apps.get_model('scipost', 'Contributor') + + Contributor.objects.filter(status=-4).update(new_status='disabled') + Contributor.objects.filter(status=-3).update(new_status='barred') + Contributor.objects.filter(status=-2).update(new_status='double_account') + Contributor.objects.filter(status=-1).update(new_status='no_scientist') + Contributor.objects.filter(status=0).update(new_status='newly_registered') + Contributor.objects.filter(status=1).update(new_status='normal') + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0011_contributor_new_status'), + ] + + operations = [ + migrations.RunPython(update_contributor_status_field) + ] diff --git a/scipost/migrations/0013_remove_contributor_status.py b/scipost/migrations/0013_remove_contributor_status.py new file mode 100644 index 0000000000000000000000000000000000000000..7dfb7cc212142c45870d0fbea11d73a5f972f7cc --- /dev/null +++ b/scipost/migrations/0013_remove_contributor_status.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-14 20:18 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0012_auto_20180414_2212'), + ] + + operations = [ + migrations.RemoveField( + model_name='contributor', + name='status', + ), + ] diff --git a/scipost/migrations/0014_auto_20180414_2218.py b/scipost/migrations/0014_auto_20180414_2218.py new file mode 100644 index 0000000000000000000000000000000000000000..d2782391c11e94bc712aa0c98de6c7a3ee192a3c --- /dev/null +++ b/scipost/migrations/0014_auto_20180414_2218.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-14 20:18 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0013_remove_contributor_status'), + ] + + operations = [ + migrations.RenameField( + model_name='contributor', + old_name='new_status', + new_name='status', + ), + ] diff --git a/scipost/models.py b/scipost/models.py index 9f358b52c9bac2ee022a4ce26f65cb76523c76d6..d5bc608c42a3a3d4eca6b37aab37128958c259d9 100644 --- a/scipost/models.py +++ b/scipost/models.py @@ -15,40 +15,39 @@ from django.db import models from django.utils import timezone from .behaviors import TimeStampedModel, orcid_validator -from .constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS,\ - subject_areas_dict, CONTRIBUTOR_STATUS, TITLE_CHOICES,\ - INVITATION_STYLE, INVITATION_TYPE,\ - INVITATION_CONTRIBUTOR, INVITATION_FORMAL,\ - AUTHORSHIP_CLAIM_PENDING, AUTHORSHIP_CLAIM_STATUS,\ - CONTRIBUTOR_NEWLY_REGISTERED +from .constants import ( + SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS, subject_areas_dict, DISABLED, + TITLE_CHOICES, INVITATION_STYLE, INVITATION_TYPE, INVITATION_CONTRIBUTOR, INVITATION_FORMAL, + AUTHORSHIP_CLAIM_PENDING, AUTHORSHIP_CLAIM_STATUS, CONTRIBUTOR_STATUSES, NEWLY_REGISTERED) from .fields import ChoiceArrayField -from .managers import FellowManager, ContributorQuerySet,\ - UnavailabilityPeriodManager, AuthorshipClaimQuerySet +from .managers import ( + FellowManager, ContributorQuerySet, UnavailabilityPeriodManager, AuthorshipClaimQuerySet) today = timezone.now().date() def get_sentinel_user(): - ''' - Temporary fix: eventually the 'to-be-removed-Contributor' should be - status: "deactivated" and anonymized. + """Temporary fix to be able to delete Contributor instances. + + Eventually the 'to-be-removed-Contributor' should be status: "deactivated" and anonymized. Fallback user for models relying on Contributor that is being deleted. - ''' + """ user, __ = get_user_model().objects.get_or_create(username='deleted') - return Contributor.objects.get_or_create(status=-4, user=user)[0] + return Contributor.objects.get_or_create(status=DISABLED, user=user)[0] class Contributor(models.Model): - """ + """A Contributor is an academic extention of the User model. + All *science* users of SciPost are Contributors. username, password, email, first_name and last_name are inherited from User. """ + user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, unique=True) invitation_key = models.CharField(max_length=40, blank=True) activation_key = models.CharField(max_length=40, blank=True) key_expires = models.DateTimeField(default=timezone.now) - status = models.SmallIntegerField(default=CONTRIBUTOR_NEWLY_REGISTERED, - choices=CONTRIBUTOR_STATUS) + status = models.CharField(max_length=16, choices=CONTRIBUTOR_STATUSES, default=NEWLY_REGISTERED) title = models.CharField(max_length=4, choices=TITLE_CHOICES) discipline = models.CharField(max_length=20, choices=SCIPOST_DISCIPLINES, default='physics', verbose_name='Main discipline') @@ -57,16 +56,12 @@ class Contributor(models.Model): blank=True, null=True) orcid_id = models.CharField(max_length=20, verbose_name="ORCID id", blank=True, validators=[orcid_validator]) - address = models.CharField(max_length=1000, verbose_name="address", - blank=True) - personalwebpage = models.URLField(verbose_name='personal web page', - blank=True) + address = models.CharField(max_length=1000, verbose_name="address", blank=True) + personalwebpage = models.URLField(verbose_name='personal web page', blank=True) vetted_by = models.ForeignKey('self', on_delete=models.SET(get_sentinel_user), - related_name="contrib_vetted_by", - blank=True, null=True) + related_name="contrib_vetted_by", blank=True, null=True) accepts_SciPost_emails = models.BooleanField( - default=True, - verbose_name="I accept to receive SciPost emails") + default=True, verbose_name="I accept to receive SciPost emails") objects = ContributorQuerySet.as_manager() @@ -74,48 +69,50 @@ class Contributor(models.Model): return '%s, %s' % (self.user.last_name, self.user.first_name) def save(self, *args, **kwargs): + """Generate new activitation key if not set.""" if not self.activation_key: self.generate_key() return super().save(*args, **kwargs) def get_absolute_url(self): + """Return public information page url.""" return reverse('scipost:contributor_info', args=(self.id,)) - @property - def get_formal_display(self): - return '%s %s %s' % (self.get_title_display(), self.user.first_name, self.user.last_name) - @property def is_currently_available(self): + """Check if Contributor is currently not marked as unavailable.""" return not self.unavailability_periods.today().exists() def is_EdCol_Admin(self): + """Check if Contributor is an Editorial Administrator.""" return (self.user.groups.filter(name='Editorial Administrators').exists() or self.user.is_superuser) def is_SP_Admin(self): + """Check if Contributor is a SciPost Administrator.""" return (self.user.groups.filter(name='SciPost Administrators').exists() or self.user.is_superuser) def is_MEC(self): + """Check if Contributor is a member of the Editorial College.""" return self.fellowships.active().exists() or self.user.is_superuser def is_VE(self): + """Check if Contributor is a Vetting Editor.""" return (self.user.groups.filter(name='Vetting Editors').exists() or self.user.is_superuser) def generate_key(self, feed=''): - """ - Generate and save a new activation_key for the contributor, given a certain feed. - """ + """Generate a new activation_key for the contributor, given a certain feed.""" for i in range(5): feed += random.choice(string.ascii_letters) feed = feed.encode('utf8') salt = self.user.username.encode('utf8') - self.activation_key = hashlib.sha1(salt+salt).hexdigest() + self.activation_key = hashlib.sha1(salt + salt).hexdigest() self.key_expires = datetime.datetime.now() + datetime.timedelta(days=2) def expertises_as_string(self): + """Return joined expertises.""" if self.expertises: return ', '.join([subject_areas_dict[exp].lower() for exp in self.expertises]) return '' diff --git a/scipost/static/scipost/images/FJN-logo-long.png b/scipost/static/scipost/images/FJN-logo-long.png new file mode 100755 index 0000000000000000000000000000000000000000..93860c5a2f5c1644e9c4858c398949ec7f0d6390 Binary files /dev/null and b/scipost/static/scipost/images/FJN-logo-long.png differ diff --git a/scipost/static/scipost/info/AnnualReports/AnnualReport_2015.pdf b/scipost/static/scipost/info/AnnualReports/AnnualReport_2015.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6c6473508a5e182cf496f627afd2e966ecbc0b5b Binary files /dev/null and b/scipost/static/scipost/info/AnnualReports/AnnualReport_2015.pdf differ diff --git a/scipost/static/scipost/info/AnnualReports/AnnualReport_2016.pdf b/scipost/static/scipost/info/AnnualReports/AnnualReport_2016.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6bd811577587c7a1d60dcf8827fb20ba29a5b824 Binary files /dev/null and b/scipost/static/scipost/info/AnnualReports/AnnualReport_2016.pdf differ diff --git a/scipost/static/scipost/info/AnnualReports/AnnualReport_2017.pdf b/scipost/static/scipost/info/AnnualReports/AnnualReport_2017.pdf new file mode 100644 index 0000000000000000000000000000000000000000..43863e00252e7fb34366a408d0836ec17821fa3d Binary files /dev/null and b/scipost/static/scipost/info/AnnualReports/AnnualReport_2017.pdf differ diff --git a/scipost/templates/partials/scipost/personal_page/account.html b/scipost/templates/partials/scipost/personal_page/account.html index bfda86979026a266428a272d69dc8e4e5b2b9d1e..43e350ba7295fd8f3707fe43756b793d40a18520 100644 --- a/scipost/templates/partials/scipost/personal_page/account.html +++ b/scipost/templates/partials/scipost/personal_page/account.html @@ -102,6 +102,15 @@ {% if fellowship.guest %} (Guest Fellowship) + <br> + Your Proceedings: + <ul> + {% for proc in fellowship.proceedings.all %} + <li>{{ proc }}</li> + {% empty %} + <li><em>No proceedings assigned yet.</em></li> + {% endfor %} + </ul> {% else %} (Regular Fellowship) {% endif %} diff --git a/scipost/templates/scipost/bare_base.html b/scipost/templates/scipost/bare_base.html index fbfb7b288ed8f36a1e814836c0eb22981202e85e..5efb4b3aed589d63858c7c6cde5cadd1d0a8d2bf 100644 --- a/scipost/templates/scipost/bare_base.html +++ b/scipost/templates/scipost/bare_base.html @@ -9,7 +9,7 @@ <link href="https://fonts.googleapis.com/css?family=Merriweather+Sans:300,400,700" rel="stylesheet"> <link rel="stylesheet" type="text/css" href="{% static 'scipost/SciPost.css' %}" /> <link rel="stylesheet" type="text/css" href="{% static 'fa/css/font-awesome.min.css' %}" /> - <script async src="https://code.jquery.com/jquery-2.2.0.min.js"></script> + <script async src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script> <script async src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script> {% render_bundle 'main' 'css' %} diff --git a/scipost/templates/scipost/contributor_info.html b/scipost/templates/scipost/contributor_info.html index 26423a9502582d3579c1ea106e3e1e9752c701fa..d65751ed7db2636235ff7754b3725448eed99fb8 100644 --- a/scipost/templates/scipost/contributor_info.html +++ b/scipost/templates/scipost/contributor_info.html @@ -4,7 +4,7 @@ {% block content %} -<h1 class="highlight mb-4">Contributor info: {{ contributor.get_formal_display }}</h1> +<h1 class="highlight mb-4">Contributor info: {{ contributor.get_title_display }} {{ contributor.user.first_name }} {{ contributor.user.last_name }}</h1> {% include "scipost/_public_info_as_table.html" with contributor=contributor %} diff --git a/scipost/templates/scipost/foundation.html b/scipost/templates/scipost/foundation.html index 75430efccb88624bb861a053398ca155faa504e5..d541dee897dfa5f9fb7b39264c77ca27a7186349 100644 --- a/scipost/templates/scipost/foundation.html +++ b/scipost/templates/scipost/foundation.html @@ -71,16 +71,6 @@ </div> </div> - <div class="card bg-white border-0"> - <div class="card-body"> - <h2 class="highlight">Registration</h2> - <p class="px-2"> - Dutch Chamber of Commerce nr 65280083.</br> - RSIN 856049487.<br> - <a href="{% static 'scipost/info/uittreksel_Stichting_SciPost.pdf' %}">Registration extract</a>. - </p> - </div> - </div> </div> </div> </div> @@ -88,23 +78,33 @@ <div class="row"> <div class="col-12"> <div class="card-deck"> +<!-- <div class="card bg-white border-0"> <div class="card-body"> - <h2 class="highlight">Registration</h2> + <h2 class="highlight">Yearly Reports</h2> + <p class="px-2">2016 (to be published)</p> </div> </div> - +--> <div class="card bg-white border-0"> <div class="card-body"> - <h2 class="highlight">Yearly Reports</h2> - <p class="px-2">2016 (to be published)</p> + <h2 class="highlight">Registration</h2> + <p class="px-2"> + Dutch Chamber of Commerce nr 65280083.</br> + RSIN 856049487.<br> + <a href="{% static 'scipost/info/uittreksel_Stichting_SciPost.pdf' %}">Registration extract</a>. + </p> </div> </div> <div class="card bg-white border-0"> <div class="card-body"> <h2 class="highlight">Financial Reports</h2> - <p class="px-2">2016-7 (to be published)</p> + <ul> + <li><a href="{% static 'scipost/info/AnnualReports/AnnualReport_2015.pdf' %}">Annual Report 2015</a></li> + <li><a href="{% static 'scipost/info/AnnualReports/AnnualReport_2016.pdf' %}">Annual Report 2016</a></li> + <li><a href="{% static 'scipost/info/AnnualReports/AnnualReport_2017.pdf' %}">Annual Report 2017</a></li> + </ul> </div> </div> </div> diff --git a/scipost/templates/scipost/index.html b/scipost/templates/scipost/index.html index 5546719426f347d435abd1155666200f886a4429..58b56563081658effeb20a2c355c1705e1637b41 100644 --- a/scipost/templates/scipost/index.html +++ b/scipost/templates/scipost/index.html @@ -195,6 +195,7 @@ <a href="//www.doaj.org" target="_blank"><img src="{% static 'scipost/images/doaj_logo_200.jpg' %}" width="90" alt="DOAJ logo"></a> <a href="//www.clockss.org" target="_blank"><img src="{% static 'scipost/images/clockss_original_logo_boxed_ai-cropped-90.png' %}" width="80" alt="Clockss logo"></a> <a href="//i4oc.org/" target="_blank"><img width="100" src="{% static 'scipost/images/I4OC.png' %}"></a> + <a href="//freejournals.org" target="_blank"><img width="100" src="{% static 'scipost/images/FJN-logo-long.png' %}"></a> </div> </div> <div class="row"> diff --git a/scipost/views.py b/scipost/views.py index 9c9ba8c786e6f0739759f8682df5cd4c3db2a51f..915df08f3fa3e0bfdc34fbef5b55fdda1e969eb5 100644 --- a/scipost/views.py +++ b/scipost/views.py @@ -9,7 +9,7 @@ from django.shortcuts import get_object_or_404, render from django.conf import settings from django.contrib import messages from django.contrib.auth import login, logout, update_session_auth_hash -from django.contrib.auth.decorators import login_required, user_passes_test +from django.contrib.auth.decorators import login_required from django.contrib.auth.models import Group from django.contrib.auth.views import password_reset, password_reset_confirm from django.core import mail @@ -29,16 +29,16 @@ from django.views.static import serve from guardian.decorators import permission_required from haystack.generic_views import SearchView -from .constants import SCIPOST_SUBJECT_AREAS, subject_areas_raw_dict, SciPost_from_addresses_dict,\ - CONTRIBUTOR_NORMAL -from .decorators import has_contributor -from .models import Contributor, UnavailabilityPeriod,\ - AuthorshipClaim, EditorialCollege, EditorialCollegeFellowship -from .forms import AuthenticationForm, UnavailabilityPeriodForm,\ - RegistrationForm, AuthorshipClaimForm,\ - SearchForm, VetRegistrationForm, reg_ref_dict,\ - UpdatePersonalDataForm, UpdateUserDataForm, PasswordChangeForm,\ - EmailGroupMembersForm, EmailParticularForm, SendPrecookedEmailForm +from .constants import ( + SCIPOST_SUBJECT_AREAS, subject_areas_raw_dict, SciPost_from_addresses_dict, NORMAL_CONTRIBUTOR) +from .decorators import has_contributor, is_contributor_user +from .models import ( + Contributor, UnavailabilityPeriod, AuthorshipClaim, EditorialCollege, + EditorialCollegeFellowship) +from .forms import ( + AuthenticationForm, UnavailabilityPeriodForm, RegistrationForm, AuthorshipClaimForm, + SearchForm, VetRegistrationForm, reg_ref_dict, UpdatePersonalDataForm, UpdateUserDataForm, + PasswordChangeForm, EmailGroupMembersForm, EmailParticularForm, SendPrecookedEmailForm) from .utils import Utils, EMAIL_FOOTER, SCIPOST_SUMMARY_FOOTER, SCIPOST_SUMMARY_FOOTER_HTML from affiliations.forms import AffiliationsFormset @@ -49,8 +49,7 @@ from invitations.constants import STATUS_REGISTERED from invitations.models import RegistrationInvitation from journals.models import Publication, Journal, PublicationAuthorsTable from news.models import NewsItem -from submissions.models import Submission, RefereeInvitation,\ - Report, EICRecommendation +from submissions.models import Submission, RefereeInvitation, Report, EICRecommendation from partners.models import MembershipAgreement from theses.models import ThesisLink @@ -60,18 +59,18 @@ from theses.models import ThesisLink ############## def is_registered(user): - """ - This method checks if user is activated assuming an validated user - has at least one permission group (`Registered Contributor` or `Partner Accounts`). - """ + """Check if user is a validated user; has at least one permission group.""" return user.groups.exists() class SearchView(SearchView): + """Search CBV inherited from Haystack.""" + template_name = 'search/search.html' form_class = SearchForm def get_context_data(self, *args, **kwargs): + """Update context with some additional information.""" ctx = super().get_context_data(*args, **kwargs) ctx['search_query'] = self.request.GET.get('q') ctx['results_count'] = kwargs['object_list'].count() @@ -83,7 +82,7 @@ class SearchView(SearchView): ############# def index(request): - '''Main page.''' + """Homepage view of SciPost.""" context = { 'latest_newsitem': NewsItem.objects.filter(on_homepage=True).order_by('-date').first(), 'submissions': Submission.objects.public().order_by('-submission_date')[:3], @@ -96,7 +95,8 @@ def index(request): def protected_serve(request, path, show_indexes=False): - """ + """Serve media files from outside the public MEDIA_ROOT folder. + Serve files that are saved outside the default MEDIA_ROOT folder for superusers only! This will be usefull eg. in the admin pages. """ @@ -112,6 +112,7 @@ def protected_serve(request, path, show_indexes=False): ############### def feeds(request): + """Information page for RSS and Atom feeds.""" context = {'subject_areas_physics': SCIPOST_SUBJECT_AREAS[0][1]} return render(request, 'scipost/feeds.html', context) @@ -121,7 +122,8 @@ def feeds(request): ################ def register(request): - """ + """Contributor registration form page. + This public registration view shows and processes the form that will create new user account requests. After registration the Contributor will need to activate its account via the mail @@ -155,7 +157,8 @@ def register(request): def invitation(request, key): - """ + """Registration Invitation reception page. + If a scientist has recieved an invitation (RegistrationInvitation) he/she will finish it's invitation via still view which will prefill the default registration form. @@ -244,9 +247,8 @@ def unsubscribe(request, contributor_id, key): @permission_required('scipost.can_vet_registration_requests', return_403=True) def vet_registration_requests(request): - contributors_to_vet = (Contributor.objects - .awaiting_vetting() - .order_by('key_expires')) + """List of new Registration requests to vet.""" + contributors_to_vet = Contributor.objects.awaiting_vetting().order_by('key_expires') form = VetRegistrationForm() context = {'contributors_to_vet': contributors_to_vet, 'form': form} return render(request, 'scipost/vet_registration_requests.html', context) @@ -254,12 +256,12 @@ def vet_registration_requests(request): @permission_required('scipost.can_vet_registration_requests', return_403=True) def vet_registration_request_ack(request, contributor_id): - # process the form + """Form view to vet new Registration requests.""" form = VetRegistrationForm(request.POST or None) contributor = Contributor.objects.get(pk=contributor_id) if form.is_valid(): if form.promote_to_registered_contributor(): - contributor.status = 1 + contributor.status = NORMAL_CONTRIBUTOR contributor.vetted_by = request.user.contributor contributor.save() group = Group.objects.get(name='Registered Contributors') @@ -274,11 +276,10 @@ def vet_registration_request_ack(request, contributor_id): except RefereeInvitation.DoesNotExist: pending_ref_inv_exists = False - email_text = ('Dear ' + contributor.get_title_display() + ' ' - + contributor.user.last_name + - ', \n\nYour registration to the SciPost publication portal ' - 'has been accepted. ' - 'You can now login at https://scipost.org and contribute. \n\n') + email_text = ( + 'Dear ' + contributor.get_title_display() + ' ' + contributor.user.last_name + + ', \n\nYour registration to the SciPost publication portal has been accepted. ' + 'You can now login at https://scipost.org and contribute. \n\n') if pending_ref_inv_exists: email_text += ( 'Note that you have pending refereeing invitations; please navigate to ' @@ -292,18 +293,16 @@ def vet_registration_request_ack(request, contributor_id): reply_to=['registration@scipost.org']) emailmessage.send(fail_silently=False) else: - ref_reason = int(form.cleaned_data['refusal_reason']) - email_text = ('Dear ' + contributor.get_title_display() + ' ' - + contributor.user.last_name + - ', \n\nYour registration to the SciPost publication portal ' - 'has been turned down, the reason being: ' - + reg_ref_dict[ref_reason] + '. You can however still view ' - 'all SciPost contents, just not submit papers, ' - 'comments or votes. We nonetheless thank you for your interest.' - '\n\nThe SciPost Team.') + ref_reason = form.cleaned_data['refusal_reason'] + email_text = ( + 'Dear ' + contributor.get_title_display() + ' ' + contributor.user.last_name + + ', \n\nYour registration to the SciPost publication portal has been turned down,' + ' the reason being: ' + reg_ref_dict[ref_reason] + '. You can however still view ' + 'all SciPost contents, just not submit papers, comments or votes. We nonetheless ' + 'thank you for your interest.\n\nThe SciPost Team.') if form.cleaned_data['email_response_field']: - email_text += ('\n\nFurther explanations: ' - + form.cleaned_data['email_response_field']) + email_text += ( + '\n\nFurther explanations: ' + form.cleaned_data['email_response_field']) emailmessage = EmailMessage('SciPost registration: unsuccessful', email_text, 'SciPost registration <registration@scipost.org>', @@ -337,9 +336,7 @@ def registration_requests(request): @require_POST @permission_required('scipost.can_resend_registration_requests', return_403=True) def registration_requests_reset(request, contributor_id): - ''' - Reset specific activation_key for Contributor and resend activation mail. - ''' + """Reset specific activation_key for Contributor and resend activation mail.""" contributor = get_object_or_404(Contributor.objects.awaiting_validation(), id=contributor_id) contributor.generate_key() contributor.save() @@ -351,15 +348,7 @@ def registration_requests_reset(request, contributor_id): def login_view(request): - """ - This view shows and processes a user's login session. - - The function based method login() is deprecated from - Django 1.11 and replaced by Class Based Views. - - See: - https://docs.djangoproject.com/en/1.11/releases/1.11/#django-contrib-auth - """ + """Login form page.""" form = AuthenticationForm(request.POST or None, initial=request.GET) if form.is_valid(): user = form.authenticate() @@ -381,13 +370,7 @@ def login_view(request): def logout_view(request): - """ - The function based method logout() is deprecated from - Django 1.11 and replaced by Class Based Views. - - See: - https://docs.djangoproject.com/en/1.11/releases/1.11/#django-contrib-auth - """ + """Logout form page.""" logout(request) messages.success(request, ('<h3>Keep contributing!</h3>' 'You are now logged out of SciPost.')) @@ -395,11 +378,9 @@ def logout_view(request): @login_required -@user_passes_test(has_contributor) +@is_contributor_user() def mark_unavailable_period(request): - ''' - Mark period unavailable for Contributor using this view. - ''' + """Form view to mark period unavailable for Contributor.""" unav_form = UnavailabilityPeriodForm(request.POST or None) if unav_form.is_valid(): unav = unav_form.save(commit=False) @@ -415,11 +396,9 @@ def mark_unavailable_period(request): @require_POST @login_required -@user_passes_test(has_contributor) +@is_contributor_user() def delete_unavailable_period(request, period_id): - ''' - Delete period unavailable registered. - ''' + """Delete period unavailable registered.""" unav = get_object_or_404(UnavailabilityPeriod, contributor=request.user.contributor, id=int(period_id)) unav.delete() @@ -428,11 +407,9 @@ def delete_unavailable_period(request, period_id): @login_required -@user_passes_test(has_contributor) +@is_contributor_user() def _personal_page_editorial_account(request): - """ - The Personal Page tab: Account - """ + """Personal Page tab: Account.""" contributor = request.user.contributor context = { 'contributor': contributor, @@ -442,11 +419,9 @@ def _personal_page_editorial_account(request): return render(request, 'partials/scipost/personal_page/account.html', context) -@user_passes_test(has_contributor) +@is_contributor_user() def _personal_page_editorial_actions(request): - """ - The Personal Page tab: Editorial Actions - """ + """Personal Page tab: Editorial Actions.""" permission = request.user.groups.filter(name__in=[ 'Ambassadors', 'Advisory Board', @@ -491,11 +466,9 @@ def _personal_page_editorial_actions(request): @permission_required('scipost.can_referee', return_403=True) -@user_passes_test(has_contributor) +@is_contributor_user() def _personal_page_refereeing(request): - """ - The Personal Page tab: Refereeing - """ + """Personal Page tab: Refereeing.""" context = { 'contributor': request.user.contributor } @@ -503,11 +476,9 @@ def _personal_page_refereeing(request): @login_required -@user_passes_test(has_contributor) +@is_contributor_user() def _personal_page_publications(request): - """ - The Personal Page tab: Publications - """ + """Personal Page tab: Publications.""" contributor = request.user.contributor context = { 'contributor': contributor, @@ -522,11 +493,9 @@ def _personal_page_publications(request): @login_required -@user_passes_test(has_contributor) +@is_contributor_user() def _personal_page_submissions(request): - """ - The Personal Page tab: Submissions - """ + """Personal Page tab: Submissions.""" contributor = request.user.contributor context = {'contributor': contributor} @@ -541,11 +510,9 @@ def _personal_page_submissions(request): @login_required -@user_passes_test(has_contributor) +@is_contributor_user() def _personal_page_commentaries(request): - """ - The Personal Page tab: Commentaries - """ + """Personal Page tab: Commentaries.""" contributor = request.user.contributor context = {'contributor': contributor} @@ -559,11 +526,9 @@ def _personal_page_commentaries(request): @login_required -@user_passes_test(has_contributor) +@is_contributor_user() def _personal_page_theses(request): - """ - The Personal Page tab: Theses - """ + """Personal Page tab: Theses.""" contributor = request.user.contributor context = {'contributor': contributor} @@ -577,11 +542,9 @@ def _personal_page_theses(request): @login_required -@user_passes_test(has_contributor) +@is_contributor_user() def _personal_page_comments(request): - """ - The Personal Page tab: Comments - """ + """Personal Page tab: Comments.""" contributor = request.user.contributor context = { 'contributor': contributor, @@ -592,11 +555,9 @@ def _personal_page_comments(request): @login_required -@user_passes_test(has_contributor) +@is_contributor_user() def _personal_page_author_replies(request): - """ - The Personal Page tab: Author Replies - """ + """Personal Page tab: Author Replies.""" contributor = request.user.contributor context = { 'contributor': contributor, @@ -608,9 +569,7 @@ def _personal_page_author_replies(request): @login_required def personal_page(request, tab='account'): - """ - The Personal Page is the main view for accessing user functions. - """ + """Personal Page is the main view for accessing user functions.""" if request.is_ajax(): if tab == 'account': return _personal_page_editorial_account(request) @@ -640,7 +599,7 @@ def personal_page(request, tab='account'): try: contributor = Contributor.objects.select_related('user').get(user=request.user) - context['needs_validation'] = contributor.status != CONTRIBUTOR_NORMAL + context['needs_validation'] = contributor.status != NORMAL_CONTRIBUTOR except Contributor.DoesNotExist: contributor = None @@ -659,6 +618,7 @@ def personal_page(request, tab='account'): @login_required def change_password(request): + """Change password form view.""" form = PasswordChangeForm(request.POST or None, current_user=request.user) if form.is_valid(): form.save_new_password() @@ -729,7 +689,7 @@ def update_personal_data(request): @login_required -@user_passes_test(has_contributor) +@is_contributor_user() def claim_authorships(request): """ The system auto-detects potential authorships (of submissions, @@ -778,7 +738,7 @@ def claim_authorships(request): @login_required -@user_passes_test(has_contributor) +@is_contributor_user() def claim_pub_authorship(request, publication_id, claim): if request.method == 'POST': contributor = Contributor.objects.get(user=request.user) @@ -794,7 +754,7 @@ def claim_pub_authorship(request, publication_id, claim): @login_required -@user_passes_test(has_contributor) +@is_contributor_user() def claim_sub_authorship(request, submission_id, claim): if request.method == 'POST': contributor = Contributor.objects.get(user=request.user) @@ -810,7 +770,7 @@ def claim_sub_authorship(request, submission_id, claim): @login_required -@user_passes_test(has_contributor) +@is_contributor_user() def claim_com_authorship(request, commentary_id, claim): if request.method == 'POST': contributor = Contributor.objects.get(user=request.user) @@ -826,7 +786,7 @@ def claim_com_authorship(request, commentary_id, claim): @login_required -@user_passes_test(has_contributor) +@is_contributor_user() def claim_thesis_authorship(request, thesis_id, claim): if request.method == 'POST': contributor = Contributor.objects.get(user=request.user) @@ -908,10 +868,10 @@ def contributor_info(request, contributor_id): contributor_submissions = Submission.objects.public_unlisted().filter(authors=contributor) contributor_commentaries = Commentary.objects.filter(authors=contributor) contributor_theses = ThesisLink.objects.vetted().filter(author_as_cont=contributor) - contributor_comments = (Comment.objects.vetted() + contributor_comments = (Comment.objects.vetted().publicly_visible() .filter(author=contributor, is_author_reply=False) .order_by('-date_submitted')) - contributor_authorreplies = (Comment.objects.vetted() + contributor_authorreplies = (Comment.objects.vetted().publicly_visible() .filter(author=contributor, is_author_reply=True) .order_by('-date_submitted')) context = {'contributor': contributor, diff --git a/submissions/forms.py b/submissions/forms.py index c07f76b1fe3bd5a4ad07c51f5b3455930140ee98..7aa45b91c4a90757b431f9743a3f1e89715d7c64 100644 --- a/submissions/forms.py +++ b/submissions/forms.py @@ -24,7 +24,7 @@ from .models import ( from common.helpers import get_new_secrets_key from colleges.models import Fellowship from invitations.models import RegistrationInvitation -from journals.constants import SCIPOST_JOURNAL_PHYSICS_PROC +from journals.constants import SCIPOST_JOURNAL_PHYSICS_PROC, SCIPOST_JOURNAL_PHYSICS from scipost.constants import SCIPOST_SUBJECT_AREAS, INVITATION_REFEREEING from scipost.services import ArxivCaller from scipost.models import Contributor @@ -290,6 +290,7 @@ class RequestSubmissionForm(SubmissionChecks, forms.ModelForm): del self.fields['proceedings'] # Update placeholder for the other fields + self.fields['submission_type'].required = False self.fields['arxiv_link'].widget.attrs.update({ 'placeholder': 'ex.: arxiv.org/abs/1234.56789v1'}) self.fields['abstract'].widget.attrs.update({'cols': 100}) @@ -322,9 +323,8 @@ class RequestSubmissionForm(SubmissionChecks, forms.ModelForm): return cleaned_data def clean_author_list(self): - """ - Important check! - + """Check if author list matches the Contributor submitting. + The submitting user must be an author of the submission. Also possibly may be extended to check permissions and give ultimate submission power to certain user groups. @@ -336,12 +336,20 @@ class RequestSubmissionForm(SubmissionChecks, forms.ModelForm): raise forms.ValidationError(error_message, code='not_an_author') return author_list + def clean_submission_type(self): + """Validate Submission type. + + The SciPost Physics journal requires a Submission type to be specified. + """ + submission_type = self.cleaned_data['submission_type'] + journal = self.cleaned_data['submitted_to_journal'] + if journal == SCIPOST_JOURNAL_PHYSICS and not submission_type: + self.add_error('submission_type', 'Please specify the submission type.') + return submission_type + @transaction.atomic def copy_and_save_data_from_resubmission(self, submission): - """ - Fill given Submission with data coming from last_submission in the SubmissionChecks - blueprint. - """ + """Fill given Submission with data coming from last_submission.""" if not self.last_submission: raise Submission.DoesNotExist diff --git a/submissions/models.py b/submissions/models.py index 4f704ce629d484af7b342e8bdde8c4cd4b915d0c..0b4b43423c3132d5183ad1addb73fa6ca3fabed4 100644 --- a/submissions/models.py +++ b/submissions/models.py @@ -3,6 +3,7 @@ __license__ = "AGPL v3" import datetime +import feedparser from django.contrib.postgres.fields import JSONField from django.contrib.contenttypes.fields import GenericRelation @@ -74,6 +75,8 @@ class Submission(models.Model): fellows = models.ManyToManyField('colleges.Fellowship', blank=True, related_name='pool') + # visible_pool = models.BooleanField(default=True) + # visible_public = models.BooleanField(default=False) subject_area = models.CharField(max_length=10, choices=SCIPOST_SUBJECT_AREAS, verbose_name='Primary subject area', default='Phys:QP') submission_type = models.CharField(max_length=10, choices=SUBMISSION_TYPE) @@ -277,6 +280,32 @@ class Submission(models.Model): ) event.save() + """ + Identify coauthorships from arXiv, using author surname matching. + """ + def flag_coauthorships_arxiv(self, fellows): + coauthorships = {} + if self.metadata and 'entries' in self.metadata: + author_last_names = [] + for author in self.metadata['entries'][0]['authors']: + # Gather author data to do conflict-of-interest queries with + author_last_names.append(author['name'].split()[-1]) + authors_last_names_str = '+OR+'.join(author_last_names) + + for fellow in fellows: + # For each fellow found, so a query with the authors to check for conflicts + search_query = 'au:({fellow}+AND+({authors}))'.format( + fellow=fellow.contributor.user.last_name, + authors=authors_last_names_str) + queryurl = 'https://export.arxiv.org/api/query?search_query={sq}'.format( + sq=search_query) + queryurl += '&sortBy=submittedDate&sortOrder=descending&max_results=5' + queryurl = queryurl.replace(' ', '+') # Fallback for some last names with spaces + queryresults = feedparser.parse(queryurl) + if queryresults.entries: + coauthorships[fellow.contributor.user.last_name] = queryresults.entries + return coauthorships + class SubmissionEvent(SubmissionRelatedObjectMixin, TimeStampedModel): """Private message directly related to a Submission. diff --git a/submissions/templates/partials/submissions/admin/editorial_assignment_failed.html b/submissions/templates/partials/submissions/admin/editorial_assignment_failed.html new file mode 100644 index 0000000000000000000000000000000000000000..09bd150d83dac7dbc9371018115fe3498d284cc6 --- /dev/null +++ b/submissions/templates/partials/submissions/admin/editorial_assignment_failed.html @@ -0,0 +1,13 @@ +<h1 class="highlight">Assignment has failed for Submission</h1> +<h3>Submission details</h3> +{% include 'partials/submissions/submission_summary.html' with submission=object %} + +<br> +<h3>Current EIC assignment requests:</h3> +<ul> + {% for assignment in object.editorial_assignments.all %} + {% include 'partials/submissions/pool/assignment_info.html' with assignment=assignment %} + {% empty %} + <li>No assignment requests have been sent</li> + {% endfor %} +</ul> diff --git a/submissions/templates/partials/submissions/arxiv_queryresult.html b/submissions/templates/partials/submissions/arxiv_queryresult.html index 5f2ad667f742f9c0ea66a2915602b3fde1172c26..e469a0320a9387051903a8d4dbccebba92d00d64 100644 --- a/submissions/templates/partials/submissions/arxiv_queryresult.html +++ b/submissions/templates/partials/submissions/arxiv_queryresult.html @@ -1,10 +1,12 @@ <div class="card-body"> <h3 class="card-title">{{ item.title }}</h3> <div class="card-text"> - {% for author in item.authors %} - {{ author.name }}{% if not forloop.last %},{% endif %} - {% endfor %} - - <a href="{{ item.id }}" target="_blank">{{ item.id }}</a> + <a href="javascript:;" data-toggle="toggle" data-target="#arxiv_authors_{{ id }}_{{ id2 }}">Toggle authors</a> · <a href="{{ item.link }}" target="_blank">{{ item.id }}</a> + <div class="authors mt-2" id="arxiv_authors_{{ id }}_{{ id2 }}" style="display: none;"> + {% for author in item.authors %} + {{ author.name }}{% if not forloop.last %},{% endif %} + {% endfor %} + </div> </div> <p class="card-text text-muted">Published: {{ item.published }}</p> </div> diff --git a/submissions/templates/submissions/admin/editorial_assignment_failed.html b/submissions/templates/submissions/admin/editorial_assignment_failed.html deleted file mode 100644 index cd2931bea482477f5fd6ce8777d9a2cb50db2a52..0000000000000000000000000000000000000000 --- a/submissions/templates/submissions/admin/editorial_assignment_failed.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends 'scipost/base.html' %} - -{% load bootstrap %} - -{% block pagetitle %}: assignment failed (ack){% endblock pagetitle %} - -{% block content %} - -<h1>Assignment has failed for Submission</h1> -<p>{{ submission.title }} by {{ submission.author_list }}.</p> -<p>Please add comments on the Submission in this box.</p> -<form action="{% url 'submissions:assignment_failed' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}" method="POST"> - {% csrf_token %} - {{ form|bootstrap }} - <input type="submit" name="Submit" class="btn btn-primary" /> -</form> - -{% endblock content %} diff --git a/submissions/templates/submissions/admin/editorial_assignment_form.html b/submissions/templates/submissions/admin/editorial_assignment_form.html index df473a80d47ac289a9f7303c1965dff8c4344d05..a437401f49b0a43daad1775e9280f065fb82cef9 100644 --- a/submissions/templates/submissions/admin/editorial_assignment_form.html +++ b/submissions/templates/submissions/admin/editorial_assignment_form.html @@ -62,4 +62,38 @@ </div> </div> + +<div class="row"> + <div class="col-12"> + {% if coauthorships %} + <div class="card border-danger"> + <div class="card-body"> + <h3 class="card-title text-danger">The system identified the following potential coauthorships (from arXiv database)</h3> + <p class="card-text text-danger">(only up to 5 most recent shown; if within the last 3 years, referee is disqualified):</p> + </div> + <div class="card-body"> + <ul class="list-group list-group-flush"> + {% for author, entries in coauthorships.items %} + <li class="list-group-item pt-3"> + <div class="card-content"> + <h3>For Fellow: {{ author }}</h3> + </div>{{ value}} + </li> + {% for entry in entries %} + <li class="list-group-item"> + {% include 'partials/submissions/arxiv_queryresult.html' with item=entry id=forloop.counter id2=forloop.parentloop.counter %} + </li> + {% endfor %} + {% endfor %} + </ul> + </div> + </div> + {% else %} + <h3 class="text-success">The system has not identified any coauthorships (from arXiv database)</h3> + {% endif %} + </div> +</div> + + + {% endblock %} diff --git a/submissions/templates/submissions/admin/recommendation_prepare_for_voting.html b/submissions/templates/submissions/admin/recommendation_prepare_for_voting.html index 67c157592a55c34617d064ac1bcb1f052b409060..b7ff99ca46377deb652e6f999885ea51cf5e2f5a 100644 --- a/submissions/templates/submissions/admin/recommendation_prepare_for_voting.html +++ b/submissions/templates/submissions/admin/recommendation_prepare_for_voting.html @@ -49,22 +49,22 @@ <div class="row"> <div class="col-12"> {% if coauthorships %} - <div class="card card-outline-danger"> + <div class="card border-danger"> <div class="card-body"> <h3 class="card-title text-danger">The system identified the following potential coauthorships (from arXiv database)</h3> <p class="card-text text-danger">(only up to 5 most recent shown; if within the last 3 years, referee is disqualified):</p> </div> <div class="card-body"> <ul class="list-group list-group-flush"> - {% for key, value in coauthorships.items %} + {% for author, entries in coauthorships.items %} <li class="list-group-item pt-3"> <div class="card-content"> - <h3>For Fellow {{key}}:</h3> + <h3>For Fellow: {{ author }}</h3> </div> </li> - {% for entry in value.entries %} + {% for entry in entries %} <li class="list-group-item"> - {% include 'partials/submissions/arxiv_queryresult.html' with item=entry %} + {% include 'partials/submissions/arxiv_queryresult.html' with item=entry id=forloop.counter id2=forloop.parentloop.counter %} </li> {% endfor %} {% endfor %} diff --git a/submissions/templates/submissions/referee_form.html b/submissions/templates/submissions/referee_form.html index 8fae92385d4bde0416a0f6bffbb356bec371811f..9935f079af18f9a31efc4747dceeb75c49d1f2e9 100644 --- a/submissions/templates/submissions/referee_form.html +++ b/submissions/templates/submissions/referee_form.html @@ -52,16 +52,16 @@ {% if queryresults.entries %} <div class="row"> <div class="col-12"> - <div class="card card-outline-danger"> + <div class="card border-danger"> <div class="card-body"> <h3 class="card-title text-danger">The system identified the following potential coauthorships (from arXiv database)</h3> <p class="card-text text-danger">(only up to 5 most recent shown; if within the last 3 years, referee is disqualified):</p> </div> <div class="card-body"> - <ul class="list-group list-group-flush"> + <ul class="list-group list-group-flush px-0"> {% for entry in queryresults.entries %} <li class="list-group-item"> - {% include 'partials/submissions/arxiv_queryresult.html' with item=entry %} + {% include 'partials/submissions/arxiv_queryresult.html' with item=entry id=forloop.counter id2=0 %} </li> {% endfor %} </ul> diff --git a/submissions/utils.py b/submissions/utils.py index 44115064ba9bfc12aced5a0b3e6d217b9f1b7404..5098fbc6b3e7dc5dad7a6b5315bd1bcf9439b55e 100644 --- a/submissions/utils.py +++ b/submissions/utils.py @@ -569,68 +569,6 @@ class SubmissionUtils(BaseMailUtil): emailmessage.attach_alternative(html_version, 'text/html') emailmessage.send(fail_silently=False) - @classmethod - def assignment_failed_email_authors(cls): - """ Requires loading 'submission' attribute. """ - email_text = ('Dear ' + cls.submission.submitted_by.get_title_display() + ' ' - + cls.submission.submitted_by.user.last_name - + ', \n\nYour recent Submission to SciPost,\n\n' - + cls.submission.title + ' by ' + cls.submission.author_list - + '\n\nhas unfortunately not passed the pre-screening stage. ' - 'We therefore regret to inform you that we will not ' - 'process your paper further towards publication, and that you ' - 'are now free to send your manuscript to an alternative journal.') - 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.') - email_text_html = ( - '<p>Dear {{ title }} {{ last_name }},</p>' - '<p>Your recent Submission to SciPost,</p>' - '<p>{{ sub_title }}</p>' - '\n<p>by {{ author_list }}</p>' - '\n<p>has unfortunately not passed the pre-screening stage. ' - 'We therefore regret to inform you that we will not ' - 'process your paper further towards publication, and that you ' - 'are now free to send your manuscript to an alternative journal.</p>') - if len(cls.personal_message) > 3: - email_text_html += '{{ personal_message|linebreaks }}' - email_text_html += ( - '<p>We nonetheless thank you very much for your contribution.</p>' - '<p>Sincerely,</p>' - '<p>The SciPost Team.</p>') - email_context = { - 'title': cls.submission.submitted_by.get_title_display(), - 'last_name': cls.submission.submitted_by.user.last_name, - 'sub_title': cls.submission.title, - 'author_list': cls.submission.author_list, - 'personal_message': cls.personal_message, - } - email_text_html += '<br/>' + EMAIL_FOOTER - html_template = Template(email_text_html) - html_version = html_template.render(Context(email_context)) - emailmessage = EmailMultiAlternatives( - 'SciPost: pre-screening not passed', email_text, - 'SciPost Editorial Admin <submissions@scipost.org>', - [cls.submission.submitted_by.user.email], - bcc=['submissions@scipost.org'], - reply_to=['submissions@scipost.org']) - emailmessage.attach_alternative(html_version, 'text/html') - emailmessage.send(fail_silently=False) - - @classmethod - def send_refereeing_invitation_email(cls): - """ - This method is called by send_refereeing_invitation in submissions/views. - It is used when the referee is already a registered contributor. - If a referee is not yet registered, the method recruit_referee is used - instead, which calls the send_registration_email method in scipost/utils. - Requires loading 'invitation' attribute. - """ - raise DeprecationWarning(('Use new mails.views.MailEditingSubView() with code' - ' `submission_referee_invite` instead')) - @classmethod def send_unreg_ref_reminder_email(cls): """ diff --git a/submissions/views.py b/submissions/views.py index 30a3e980fcb3a8d4555bc4c3f1ab35ada22b6882..3c1c2e08f13e5656a35d70ce0adb45d460857489 100644 --- a/submissions/views.py +++ b/submissions/views.py @@ -1,4 +1,4 @@ -__copyright__ = "Copyright 2016-2018, Stichting SciPost (SciPost Foundation)" +ip__copyright__ = "Copyright 2016-2018, Stichting SciPost (SciPost Foundation)" __license__ = "AGPL v3" @@ -25,7 +25,7 @@ from django.views.generic.list import ListView from guardian.shortcuts import assign_perm from .constants import STATUS_VETTED, STATUS_EIC_ASSIGNED,\ - SUBMISSION_STATUS_PUBLICLY_INVISIBLE, SUBMISSION_STATUS,\ + SUBMISSION_STATUS_PUBLICLY_INVISIBLE, SUBMISSION_STATUS, STATUS_ASSIGNMENT_FAILED,\ STATUS_DRAFT, CYCLE_DIRECT_REC, STATUS_VOTING_IN_PREPARATION,\ STATUS_PUT_TO_EC_VOTING from .models import Submission, EICRecommendation, EditorialAssignment,\ @@ -43,8 +43,6 @@ from .utils import SubmissionUtils from colleges.permissions import fellowship_required, fellowship_or_admin_required from comments.forms import CommentForm -from invitations.constants import INVITATION_REFEREEING -from invitations.models import RegistrationInvitation from journals.models import Journal from mails.views import MailEditingSubView from production.forms import ProofsDecisionForm @@ -451,8 +449,13 @@ def assign_submission(request, arxiv_identifier_w_vn_nr): SubmissionUtils.send_assignment_request_email() messages.success(request, 'Your assignment request has been sent successfully.') return redirect('submissions:pool') + + fellows_with_expertise = submission.fellows.all() + coauthorships = submission.flag_coauthorships_arxiv(fellows_with_expertise) + context = { 'submission_to_assign': submission, + 'coauthorships': coauthorships, 'form': form } return render(request, 'submissions/admin/editorial_assignment_form.html', context) @@ -571,14 +574,18 @@ def volunteer_as_EIC(request, arxiv_identifier_w_vn_nr): deadline += datetime.timedelta(days=28) # Update Submission data - submission.status = STATUS_EIC_ASSIGNED - submission.editor_in_charge = contributor - submission.open_for_reporting = True - submission.reporting_deadline = deadline - submission.open_for_commenting = True - submission.save() - submission.touch() - + Submission.objects.filter(id=submission.id).update( + status=STATUS_EIC_ASSIGNED, + editor_in_charge=contributor, + open_for_reporting=True, + reporting_deadline=deadline, + open_for_commenting=True, + latest_activity=timezone.now()) + + # Deprecate old Editorial Assignments + EditorialAssignment.objects.filter(submission=submission).open().update(deprecated=True) + + # Send emails to EIC and authors regarding the EIC assignment. SubmissionUtils.load({'assignment': assignment}) SubmissionUtils.deprecate_other_assignments() SubmissionUtils.send_EIC_appointment_email() @@ -596,41 +603,40 @@ def volunteer_as_EIC(request, arxiv_identifier_w_vn_nr): @permission_required('scipost.can_assign_submissions', raise_exception=True) @transaction.atomic def assignment_failed(request, arxiv_identifier_w_vn_nr): + """Reject a Submission in pre-screening. + + No Editorial Fellow has accepted or volunteered to become Editor-in-charge., hence the + Submission is rejected. An Editorial Administrator can access this view from the Pool. """ - No Editorial Fellow has accepted or volunteered to become Editor-in-charge. - The submission is rejected. - This method is called from pool.html by an Editorial Administrator. - """ - submission = get_object_or_404(Submission.objects.pool(request.user), + submission = get_object_or_404(Submission.objects.pool(request.user).prescreening(), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) - if request.method == 'POST': - form = ModifyPersonalMessageForm(request.POST) - if form.is_valid(): - submission.status = 'assignment_failed' - submission.latest_activity = timezone.now() - submission.save() - SubmissionUtils.load({'submission': submission, - 'personal_message': form.cleaned_data['personal_message']}) - SubmissionUtils.deprecate_all_assignments() - SubmissionUtils.assignment_failed_email_authors() - context = {'ack_header': ('Submission ' + submission.arxiv_identifier_w_vn_nr + - ' has failed pre-screening and been rejected. ' - 'Authors have been informed by email.'), - 'followup_message': 'Return to the ', - 'followup_link': reverse('submissions:pool'), - 'followup_link_label': 'Submissions pool'} - return render(request, 'scipost/acknowledgement.html', context) + + mail_request = MailEditingSubView( + request, mail_code='submissions_assignment_failed', instance=submission, + header_template='partials/submissions/admin/editorial_assignment_failed.html') + if mail_request.is_valid(): + # Deprecate old Editorial Assignments + EditorialAssignment.objects.filter(submission=submission).open().update(deprecated=True) + + # Update status of Submission + submission.touch() + Submission.objects.filter(id=submission.id).update(status=STATUS_ASSIGNMENT_FAILED) + + messages.success( + request, 'Submission {arxiv} has failed pre-screening and been rejected.'.format( + arxiv=submission.arxiv_identifier_w_vn_nr)) + messages.success(request, 'Authors have been informed by email.') + mail_request.send() + return redirect(reverse('submissions:pool')) else: - form = ModifyPersonalMessageForm() - context = {'submission': submission, - 'form': form} - return render(request, 'submissions/admin/editorial_assignment_failed.html', context) + return mail_request.return_render() @login_required @fellowship_required() def assignments(request): - """ + """List editorial tasks for a Fellow. + This page provides a Fellow with an explicit task list of editorial actions which should be undertaken. """ @@ -651,10 +657,10 @@ def assignments(request): @login_required @fellowship_or_admin_required() def editorial_page(request, arxiv_identifier_w_vn_nr): - """ - The central page for the EIC to manage all its Editorial duties. + """Detail page of a Submission its editorial tasks. - Accessible for: Editor-in-charge and Editorial Administration + The central page for the Editor-in-charge to manage all its Editorial duties. It's accessible + for both the Editor-in-charge of the Submission and the Editorial Administration. """ submission = get_object_or_404(Submission.objects.pool_full(request.user), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) @@ -736,7 +742,7 @@ def select_referee(request, arxiv_identifier_w_vn_nr): sub_auth_boolean_str += '+OR+' + author['name'].split()[-1] sub_auth_boolean_str += ')+AND+' search_str = sub_auth_boolean_str + ref_search_form.cleaned_data['last_name'] + ')' - queryurl = ('http://export.arxiv.org/api/query?search_query=au:%s' + queryurl = ('https://export.arxiv.org/api/query?search_query=au:%s' % search_str + '&sortBy=submittedDate&sortOrder=descending' '&max_results=5') arxivquery = feedparser.parse(queryurl) @@ -1389,11 +1395,6 @@ def prepare_for_voting(request, rec_id): recommendation = get_object_or_404( EICRecommendation.objects.active().filter(submission__in=submissions), id=rec_id) - fellows_with_expertise = recommendation.submission.fellows.filter( - contributor__expertises__contains=[recommendation.submission.subject_area]) - - coauthorships = {} - eligibility_form = VotingEligibilityForm(request.POST or None, instance=recommendation) if eligibility_form.is_valid(): eligibility_form.save() @@ -1406,23 +1407,9 @@ def prepare_for_voting(request, rec_id): return redirect(reverse('submissions:editorial_page', args=[recommendation.submission.arxiv_identifier_w_vn_nr])) else: - # Identify possible co-authorships in last 3 years, disqualifying Fellow from voting: - if recommendation.submission.metadata is not None: - for fellow in fellows_with_expertise: - sub_auth_boolean_str = '((' + (recommendation.submission - .metadata['entries'][0]['authors'][0]['name'] - .split()[-1]) - for author in recommendation.submission.metadata['entries'][0]['authors'][1:]: - sub_auth_boolean_str += '+OR+' + author['name'].split()[-1] - sub_auth_boolean_str += ')+AND+' - search_str = sub_auth_boolean_str + fellow.contributor.user.last_name + ')' - queryurl = ('http://export.arxiv.org/api/query?search_query=au:%s' - % search_str + '&sortBy=submittedDate&sortOrder=descending' - '&max_results=5') - arxivquery = feedparser.parse(queryurl) - queryresults = arxivquery - if queryresults.entries: - coauthorships[fellow.contributor.user.last_name] = queryresults + fellows_with_expertise = recommendation.submission.fellows.filter( + contributor__expertises__contains=[recommendation.submission.subject_area]) + coauthorships = recommendation.submission.flag_coauthorships_arxiv(fellows_with_expertise) context = { 'recommendation': recommendation, diff --git a/templates/search/indexes/comments/comment_text.txt b/templates/search/indexes/comments/comment_text.txt index e187fa7fe26f1fc628c3190ea008c6eeec87dc31..896dec580c8f2d3fb36fbbbf840076ce88922a09 100644 --- a/templates/search/indexes/comments/comment_text.txt +++ b/templates/search/indexes/comments/comment_text.txt @@ -1,3 +1,3 @@ {{object.comment_text}} {{object.date_submitted}} -{{object.author}} +{% if not object.anonymous %}{{ object.author }}{% endif %}