diff --git a/SciPost_v1/settings/base.py b/SciPost_v1/settings/base.py index d5d7a8f606ab5cd27ab52f6036260623c1fe7cac..44532bc63c162b25e9049c152ba72e079bfeee4a 100644 --- a/SciPost_v1/settings/base.py +++ b/SciPost_v1/settings/base.py @@ -84,6 +84,7 @@ INSTALLED_APPS = ( 'haystack', 'rest_framework', 'sphinxdoc', + 'colleges', 'commentaries', 'comments', 'finances', @@ -96,6 +97,7 @@ INSTALLED_APPS = ( 'submissions', 'theses', 'virtualmeetings', + 'proceedings', 'production', 'partners', 'funders', diff --git a/SciPost_v1/urls.py b/SciPost_v1/urls.py index cd3462f1bd2cd18e7e145be4bda23e1cc41d080b..5e06ce57cc97bd5caf2b866a23a16bbb4ccaf333 100644 --- a/SciPost_v1/urls.py +++ b/SciPost_v1/urls.py @@ -29,6 +29,7 @@ urlpatterns = [ url(r'^10.21468/%s/' % JOURNAL_REGEX, include('journals.urls.journal', namespace="journal")), url(r'^%s/' % JOURNAL_REGEX, include('journals.urls.journal', namespace="_journal")), url(r'^', include('scipost.urls', namespace="scipost")), + url(r'^colleges/', include('colleges.urls', namespace="colleges")), url(r'^commentaries/', include('commentaries.urls', namespace="commentaries")), url(r'^commentary/', include('commentaries.urls', namespace="_commentaries")), url(r'^comments/', include('comments.urls', namespace="comments")), @@ -44,6 +45,7 @@ urlpatterns = [ url(r'^news/', include('news.urls', namespace="news")), url(r'^notifications/', include('notifications.urls', namespace="notifications")), url(r'^petitions/', include('petitions.urls', namespace="petitions")), + url(r'^proceedings/', include('proceedings.urls', namespace="proceedings")), url(r'^production/', include('production.urls', namespace="production")), url(r'^partners/', include('partners.urls', namespace="partners")), url(r'^stats/', include('stats.urls', namespace="stats")), diff --git a/colleges/__init__.py b/colleges/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/colleges/admin.py b/colleges/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..d81df17fd46f1abefbc1a7fc464a409d6550f6c2 --- /dev/null +++ b/colleges/admin.py @@ -0,0 +1,18 @@ +from django.contrib import admin + +from .models import Fellowship + + +def fellowhip_is_active(fellowship): + return fellowship.is_active() + + +class FellowshipAdmin(admin.ModelAdmin): + search_fields = ['contributor__last_name', 'contributor__first_name'] + list_display = ('__str__', 'guest', fellowhip_is_active, ) + list_filter = ('guest',) + fellowhip_is_active.boolean = True + date_hierarchy = 'created' + + +admin.site.register(Fellowship, FellowshipAdmin) diff --git a/colleges/apps.py b/colleges/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..e1d74cff7d6248bb7702cad64f2ba1c406061042 --- /dev/null +++ b/colleges/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CollegesConfig(AppConfig): + name = 'colleges' diff --git a/colleges/forms.py b/colleges/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..3de926d4c0b099495f19b5feb73e430ab320956c --- /dev/null +++ b/colleges/forms.py @@ -0,0 +1,169 @@ +import datetime + +from django import forms + +from proceedings.models import Proceedings +from submissions.models import Submission +from scipost.models import Contributor + +from .models import Fellowship + + +class AddFellowshipForm(forms.ModelForm): + class Meta: + model = Fellowship + fields = ( + 'guest', + 'contributor', + 'start_date', + 'until_date', + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['contributor'].queryset = Contributor.objects.fellows() + self.fields['contributor'].label = "Fellow" + + def clean(self): + start = self.cleaned_data.get('start_date') + until = self.cleaned_data.get('until_date') + if start and until: + if until <= start: + self.add_error('until_date', 'The given dates are not in chronological order.') + + +class FellowshipForm(forms.ModelForm): + class Meta: + model = Fellowship + fields = ( + 'guest', + 'start_date', + 'until_date', + ) + + def clean(self): + start = self.cleaned_data.get('start_date') + until = self.cleaned_data.get('until_date') + if start and until: + if until <= start: + self.add_error('until_date', 'The given dates are not in chronological order.') + + +class FellowshipTerminateForm(forms.ModelForm): + class Meta: + model = Fellowship + fields = [] + + def save(self): + today = datetime.date.today() + fellowship = self.instance + if fellowship.until_date > today: + fellowship.until_date = today + return fellowship.save() + + +class FellowshipRemoveSubmissionForm(forms.ModelForm): + """ + Use this form in admin-accessible views only! It could possibly reveal the + identity of the Editor-in-charge! + """ + class Meta: + model = Fellowship + fields = [] + + def __init__(self, *args, **kwargs): + self.submission = kwargs.pop('submission') + super().__init__(*args, **kwargs) + + def clean(self): + if self.submission.editor_in_charge == self.instance.contributor: + self.add_error(None, ('Submission cannot be removed as the Fellow is' + ' Editor-in-charge of this Submission.')) + + def save(self): + fellowship = self.instance + fellowship.pool.remove(self.submission) + return fellowship + + +class FellowshipAddSubmissionForm(forms.ModelForm): + submission = forms.ModelChoiceField(queryset=None, to_field_name='arxiv_identifier_w_vn_nr', + empty_label="Please choose the Submission to add to the pool") + + class Meta: + model = Fellowship + fields = [] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + pool = self.instance.pool.values_list('id', flat=True) + self.fields['submission'].queryset = Submission.objects.exclude(id__in=pool) + + def save(self): + submission = self.cleaned_data['submission'] + fellowship = self.instance + fellowship.pool.add(submission) + return fellowship + + +class SubmissionAddFellowshipForm(forms.ModelForm): + fellowship = forms.ModelChoiceField(queryset=None, to_field_name='id', + empty_label="Please choose the Fellow to add to the Pool") + + class Meta: + model = Submission + fields = [] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + pool = self.instance.fellows.values_list('id', flat=True) + self.fields['fellowship'].queryset = Fellowship.objects.active().exclude(id__in=pool) + + def save(self): + fellowship = self.cleaned_data['fellowship'] + submission = self.instance + submission.fellows.add(fellowship) + return submission + + +class FellowshipRemoveProceedingsForm(forms.ModelForm): + """ + Use this form in admin-accessible views only! It could possibly reveal the + identity of the Editor-in-charge! + """ + class Meta: + model = Fellowship + fields = [] + + def __init__(self, *args, **kwargs): + self.proceedings = kwargs.pop('proceedings') + super().__init__(*args, **kwargs) + + def clean(self): + if self.proceedings.lead_fellow == self.instance: + self.add_error(None, 'Fellowship cannot be removed as it is assigned as lead fellow.') + + def save(self): + fellowship = self.instance + self.proceedings.fellowships.remove(fellowship) + return fellowship + + +class FellowshipAddProceedingsForm(forms.ModelForm): + proceedings = forms.ModelChoiceField(queryset=None, to_field_name='id', + empty_label="Please choose the Proceedings to add to the Pool") + + class Meta: + model = Fellowship + fields = [] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + proceedings = self.instance.proceedings.values_list('id', flat=True) + self.fields['proceedings'].queryset = Proceedings.objects.exclude(id__in=proceedings) + + def save(self): + proceedings = self.cleaned_data['proceedings'] + fellowship = self.instance + proceedings.fellowships.add(fellowship) + return fellowship diff --git a/colleges/managers.py b/colleges/managers.py new file mode 100644 index 0000000000000000000000000000000000000000..5bea48f0ce53f87bbeea382ffda04417841f5969 --- /dev/null +++ b/colleges/managers.py @@ -0,0 +1,42 @@ +import datetime + +from django.db import models +from django.db.models import Q + + +class FellowQuerySet(models.QuerySet): + def guests(self): + return self.filter(guest=True) + + def regular(self): + return self.filter(guest=False) + + def active(self): + today = datetime.date.today() + return self.filter( + Q(start_date__lte=today, until_date__isnull=True) | + Q(start_date__isnull=True, until_date__gte=today) | + Q(start_date__lte=today, until_date__gte=today) | + Q(start_date__isnull=True, until_date__isnull=True) + ).order_by('contributor__user__last_name') + + def return_active_for_submission(self, submission): + """ + This method returns a *list* of Fellowships that passed the 'author-check' for + a specific submission. + """ + try: + qs = self.exclude(contributor__in=submission.authors.all()).active() + false_claims = submission.authors_false_claims.all() + author_list = submission.author_list.lower() + fellowships = [] + for fellowship in qs: + contributor = fellowship.contributor + user = contributor.user + if user.last_name.lower() in author_list and contributor not in false_claims: + continue + + fellowships.append(fellowship) + return fellowships + except AttributeError: + return [] diff --git a/colleges/migrations/0001_initial.py b/colleges/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..d7f01c265d829fef68276f3a03f7f19d71f5accf --- /dev/null +++ b/colleges/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-20 07:00 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import scipost.db.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('scipost', '0065_authorshipclaim_publication'), + ] + + operations = [ + migrations.CreateModel( + name='Fellowship', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('latest_activity', scipost.db.fields.AutoDateTimeField(blank=True, default=django.utils.timezone.now, editable=False)), + ('start_date', models.DateField(blank=True, null=True)), + ('until_date', models.DateField(blank=True, null=True)), + ('guest', models.BooleanField(default=False, verbose_name='Guest Fellowship')), + ('contributor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fellowships', to='scipost.Contributor')), + ], + ), + migrations.AlterUniqueTogether( + name='fellowship', + unique_together=set([('contributor', 'start_date', 'until_date')]), + ), + ] diff --git a/colleges/migrations/0002_auto_20171020_0931.py b/colleges/migrations/0002_auto_20171020_0931.py new file mode 100644 index 0000000000000000000000000000000000000000..c5ba871e08ad7325de9776d50878f8233e10b308 --- /dev/null +++ b/colleges/migrations/0002_auto_20171020_0931.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-20 07:31 +from __future__ import unicode_literals + +from django.db import migrations + + +def fill_editorial_college(apps, schema_editor): + Fellowship = apps.get_model('colleges', 'Fellowship') + Contributor = apps.get_model('scipost', 'Contributor') + for contributor in Contributor.objects.filter(user__groups__name='Editorial College'): + Fellowship.objects.get_or_create(contributor=contributor) + + +def return_empty(*args, **kwargs): + return + + +class Migration(migrations.Migration): + + dependencies = [ + ('colleges', '0001_initial'), + ] + + operations = [ + migrations.RunPython(fill_editorial_college, return_empty), + ] diff --git a/colleges/migrations/__init__.py b/colleges/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/colleges/models.py b/colleges/models.py new file mode 100644 index 0000000000000000000000000000000000000000..8455d618ae862896b6646658d2bf3817cd6df31e --- /dev/null +++ b/colleges/models.py @@ -0,0 +1,54 @@ +import datetime + +from django.db import models +from django.urls import reverse + +from scipost.behaviors import TimeStampedModel + +from .managers import FellowQuerySet + + +class Fellowship(TimeStampedModel): + """ + Editorial College Fellowship connecting Editorial College and Contributors, + possibly with a limiting start/until date. + + 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) + until_date = models.DateField(null=True, blank=True) + + guest = models.BooleanField('Guest Fellowship', default=False) + + objects = FellowQuerySet.as_manager() + + class Meta: + unique_together = ('contributor', 'start_date', 'until_date') + + def __str__(self): + _str = self.contributor.__str__() + if self.guest: + _str += ' (guest fellowship)' + return _str + + def get_absolute_url(self): + 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 self.contributor.fellowships.all() + + def is_active(self): + today = datetime.date.today() + if not self.start_date: + if not self.until_date: + return True + return today <= self.until_date + elif not self.until_date: + return today >= self.start_date + return today >= self.start_date and today <= self.until_date diff --git a/colleges/templates/colleges/fellowship_add.html b/colleges/templates/colleges/fellowship_add.html new file mode 100644 index 0000000000000000000000000000000000000000..76f1b2ec8d1c58fcec7019f638503d3bab7fa464 --- /dev/null +++ b/colleges/templates/colleges/fellowship_add.html @@ -0,0 +1,24 @@ +{% extends 'submissions/admin/base.html' %} + +{% load bootstrap %} + +{% block breadcrumb_items %} + {{ block.super }} + <a href="{% url 'colleges:fellowships' %}" class="breadcrumb-item">Active Fellowships</a> + <span class="breadcrumb-item">Add Fellowship</span> +{% endblock %} + +{% block pagetitle %}: Add Fellowship{% endblock pagetitle %} + +{% block content %} + <h1>Add Fellowship</h1> + <br> + + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input class="btn btn-primary" type="submit" value="Add Fellowship"> + </form> + + +{% endblock %} diff --git a/colleges/templates/colleges/fellowship_details.html b/colleges/templates/colleges/fellowship_details.html new file mode 100644 index 0000000000000000000000000000000000000000..8abb91812d0ec821deacb872a26e7a54f5ea37a5 --- /dev/null +++ b/colleges/templates/colleges/fellowship_details.html @@ -0,0 +1,156 @@ +{% extends 'submissions/admin/base.html' %} + +{% block breadcrumb_items %} + {{ block.super }} + <a href="{% url 'colleges:fellowships' %}" class="breadcrumb-item">Active Fellowships</a> + <span class="breadcrumb-item">Fellowship details</span> +{% endblock %} + +{% block pagetitle %}: Fellowship details{% endblock pagetitle %} + +{% block content %} + <h1>Fellowship details</h1> + <h2 class="text-primary">{{ fellowship }}</h2> + <br> + + <div class="row"> + <div class="col-md-6"> + <h3>Details</h3> + + <table class="table"> + <tbody> + <tr> + <th>Fellowship ID</th> + <td>{{ fellowship.id }}</td> + </tr> + <tr> + <th>Fellow</th> + <td>{{ fellowship }}</td> + </tr> + <tr> + <th>Discipline</th> + <td>{{ fellowship.contributor.get_discipline_display }}</td> + </tr> + <tr> + <th>Start date</th> + <td>{{ fellowship.start_date|default:'<i>No start date</i>' }}</td> + </tr> + <tr> + <th>End date</th> + <td>{{ fellowship.until_date|default:'<i>No end date</i>' }}</td> + </tr> + <tr> + <th>Pool size</th> + <td>{{ fellowship.pool.count }}</td> + </tr> + <tr> + <th>Type</th> + <td>{{ fellowship.guest|yesno:"Guest fellowship,Regular fellowship"|safe }}</td> + </tr> + </tbody> + </table> + + <form method="post" action="{% url 'colleges:fellowship_terminate' fellowship.id %}" class="d-inline"> + {% csrf_token %} + <button type="submit" class="btn btn-danger">Terminate Fellowship</button> + </form> + <a href="{% url 'colleges:fellowship_edit' fellowship.id %}" class="btn btn-info ml-2">Edit Fellowship</a> + </div> + <div class="col-md-6"> + <h3>All fellowships of this fellow</h3> + + <table class="table"> + <thead> + <tr> + <th>Fellowship ID</th> + <th>Type</th> + <th colspan="2">Date range</th> + </tr> + </thead> + <tbody> + {% for fellowship in fellowship.sibling_fellowships %} + <tr> + <td>{{ fellowship.id }}</td> + <td>{{ fellowship.guest|yesno:"Guest fellowship,Regular fellowship"|safe }}</td> + <td> + {% if fellowship.start_date %} + from {{ fellowship.start_date }} + {% endif %} + {% if fellowship.until_date %} + until {{ fellowship.until_date }} + {% endif %} + {% if not fellowship.start_date and not fellowship.until_date %} + <i>Unlimited</i> + {% endif %} + </td> + <td><a href="{{ fellowship.get_absolute_url }}">See details</a></td> + </tr> + {% endfor %} + </tbody> + </table> + <a href="{% url 'colleges:fellowship_add' %}?contributor={{ fellowship.contributor.id }}">Add new Fellowship for {{ fellowship.contributor }}</a> + </div> + </div> + + {% if fellowship.guest %} + <h3>Proceedings this Guest Fellowship is assigned to</h3> + <table class="table table-hover"> + <thead> + <tr> + <th>Event</th> + <th>Issue</th> + <th>Submissions Open</th> + <th>Submissions Deadline</th> + <th>Submissions Close</th> + <th></th> + </tr> + </thead> + <tbody> + {% for proceedings in fellowship.proceedings.all %} + <tr> + <td>{{ proceedings.event_name }}</td> + <td><a href="{{ proceedings.get_absolute_url }}">{{ proceedings.issue }}</a></td> + <td>{{ proceedings.submissions_open }}</td> + <td>{{ proceedings.submissions_deadline }}</td> + <td>{{ proceedings.submissions_close }}</td> + <td><a class="text-danger" href="{% url 'colleges:fellowship_remove_proceedings' fellowship.id proceedings.id %}">Remove Proceedings</a></td> + </tr> + {% endfor %} + <tr> + <td colspan="6" class="py-3 text-center"><a href="{% url 'colleges:fellowship_add_proceedings' fellowship.id %}">Add Proceedings to Guest Fellowship</a></td> + </tr> + </tbody> + </table> + {% endif %} + + <h3>Pool for this Fellowship</h3> + <table class="table table-hover"> + <thead> + <tr> + <th>Submission</th> + <th colspan="2">Status</th> + </tr> + </thead> + <tbody> + {% for submission in fellowship.pool.all %} + <tr> + <td> + <a href="{{ submission.get_absolute_url }}">{{ submission.arxiv_identifier_w_vn_nr }}, {{ submission.title|truncatechars:50 }}</a> + </td> + <td>{{ submission.get_status_display }}</td> + <td> + {% if submission.editor_in_charge == fellowship.contributor %} + <strong>Fellow is Editor-in-charge</strong> + {% else %} + <a class="text-danger" href="{% url 'colleges:fellowship_remove_submission' fellowship.id submission.arxiv_identifier_w_vn_nr %}">Remove from this Fellowship's pool</a> + {% endif %} + </td> + </tr> + {% endfor %} + <tr> + <td colspan="3" class="py-3 text-center"><a href="{% url 'colleges:fellowship_add_submission' fellowship.id %}">Add Submission to this Fellowship's pool</a></td> + </tr> + </tbody> + </table> + +{% endblock %} diff --git a/colleges/templates/colleges/fellowship_edit.html b/colleges/templates/colleges/fellowship_edit.html new file mode 100644 index 0000000000000000000000000000000000000000..809e85b64e27ac0871fff813e2416a90a2bb506d --- /dev/null +++ b/colleges/templates/colleges/fellowship_edit.html @@ -0,0 +1,26 @@ +{% extends 'submissions/admin/base.html' %} + +{% load bootstrap %} + +{% block breadcrumb_items %} + {{ block.super }} + <a href="{% url 'colleges:fellowships' %}" class="breadcrumb-item">Active Fellowships</a> + <a href="{{ fellowship.get_absolute_url }}" class="breadcrumb-item">Fellowship details</a> + <span class="breadcrumb-item">Edit Fellowship</span> +{% endblock %} + +{% block pagetitle %}: Edit Fellowship{% endblock pagetitle %} + +{% block content %} + <h1>Edit Fellowship</h1> + <h2 class="text-primary">{{ fellowship }}</h2> + <br> + + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input class="btn btn-primary" type="submit" value="Save"> + </form> + + +{% endblock %} diff --git a/colleges/templates/colleges/fellowship_proceedings_add.html b/colleges/templates/colleges/fellowship_proceedings_add.html new file mode 100644 index 0000000000000000000000000000000000000000..391f4bf71fe36acc8ef60cc6f3bde01ac1071001 --- /dev/null +++ b/colleges/templates/colleges/fellowship_proceedings_add.html @@ -0,0 +1,26 @@ +{% extends 'submissions/admin/base.html' %} + +{% load bootstrap %} + +{% block breadcrumb_items %} + {{ block.super }} + <a href="{% url 'colleges:fellowships' %}" class="breadcrumb-item">Active Fellowships</a> + <a href="{{ fellowship.get_absolute_url }}" class="breadcrumb-item">Fellowship details</a> + <span class="breadcrumb-item">Add Proceedings</span> +{% endblock %} + +{% block pagetitle %}: Add Proceedings to Fellowship{% endblock pagetitle %} + +{% block content %} + <h1>Add Proceedings to Fellowship</h1> + <h2 class="text-primary">Fellowship {{ fellowship }}</h2> + <br> + + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input class="btn btn-primary px-3" type="submit" value="Add Submission"> + </form> + + +{% endblock %} diff --git a/colleges/templates/colleges/fellowship_proceedings_remove.html b/colleges/templates/colleges/fellowship_proceedings_remove.html new file mode 100644 index 0000000000000000000000000000000000000000..84f51f9596766dd7f2cd5fe4a055c3e110acc132 --- /dev/null +++ b/colleges/templates/colleges/fellowship_proceedings_remove.html @@ -0,0 +1,29 @@ +{% extends 'submissions/admin/base.html' %} + +{% load bootstrap %} + +{% block breadcrumb_items %} + {{ block.super }} + <a href="{% url 'colleges:fellowships' %}" class="breadcrumb-item">Active Fellowships</a> + <a href="{{ fellowship.get_absolute_url }}" class="breadcrumb-item">Fellowship details</a> + <span class="breadcrumb-item">Remove Proceedings from Pool</span> +{% endblock %} + +{% block pagetitle %}: Remove Proceedings from Fellowship{% endblock pagetitle %} + +{% block content %} + <h1>Remove Proceedings from Pool</h1> + <h2 class="text-primary">Fellowship {{ fellowship }}</h2> + + <h3>Proceedings details</h3> + {% include 'partials/proceedings/summary.html' with proceedings=proceedings %} + <br> + + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input class="btn btn-danger px-3" type="submit" value="Remove from Pool"> + </form> + + +{% endblock %} diff --git a/colleges/templates/colleges/fellowship_submission_add.html b/colleges/templates/colleges/fellowship_submission_add.html new file mode 100644 index 0000000000000000000000000000000000000000..36c617c158073e29e21012c88938137f9f2c7e55 --- /dev/null +++ b/colleges/templates/colleges/fellowship_submission_add.html @@ -0,0 +1,26 @@ +{% extends 'submissions/admin/base.html' %} + +{% load bootstrap %} + +{% block breadcrumb_items %} + {{ block.super }} + <a href="{% url 'colleges:fellowships' %}" class="breadcrumb-item">Active Fellowships</a> + <a href="{{ fellowship.get_absolute_url }}" class="breadcrumb-item">Fellowship details</a> + <span class="breadcrumb-item">Add Submission to Pool</span> +{% endblock %} + +{% block pagetitle %}: Add Submission to Fellowship{% endblock pagetitle %} + +{% block content %} + <h1>Add Submission to Pool</h1> + <h2 class="text-primary">Fellowship {{ fellowship }}</h2> + <br> + + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input class="btn btn-primary px-3" type="submit" value="Add Submission"> + </form> + + +{% endblock %} diff --git a/colleges/templates/colleges/fellowship_submission_remove.html b/colleges/templates/colleges/fellowship_submission_remove.html new file mode 100644 index 0000000000000000000000000000000000000000..c5defe25aec8a8c6f7f2828fc603dbcecfedfa9b --- /dev/null +++ b/colleges/templates/colleges/fellowship_submission_remove.html @@ -0,0 +1,29 @@ +{% extends 'submissions/admin/base.html' %} + +{% load bootstrap %} + +{% block breadcrumb_items %} + {{ block.super }} + <a href="{% url 'colleges:fellowships' %}" class="breadcrumb-item">Active Fellowships</a> + <a href="{{ fellowship.get_absolute_url }}" class="breadcrumb-item">Fellowship details</a> + <span class="breadcrumb-item">Remove Submission from Pool</span> +{% endblock %} + +{% block pagetitle %}: Remove Submission from Fellowship{% endblock pagetitle %} + +{% block content %} + <h1>Remove Submission from Pool</h1> + <h2 class="text-primary">Fellowship {{ fellowship }}</h2> + + <h3>Submission details</h3> + {% include 'submissions/_submission_summary_short.html' with submission=submission %} + <br> + + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input class="btn btn-danger px-3" type="submit" value="Remove from Pool"> + </form> + + +{% endblock %} diff --git a/colleges/templates/colleges/fellowships.html b/colleges/templates/colleges/fellowships.html new file mode 100644 index 0000000000000000000000000000000000000000..ce1ef2db9cd0fd3269db819a502fa3869ef3d1d2 --- /dev/null +++ b/colleges/templates/colleges/fellowships.html @@ -0,0 +1,74 @@ +{% extends 'submissions/admin/base.html' %} + +{% block breadcrumb_items %} + {{ block.super }} + <span class="breadcrumb-item">Active Fellowships</span> +{% endblock %} + +{% block pagetitle %}: Active Fellowships{% endblock pagetitle %} + +{% block content %} + <h1>Active Regular Fellowships</h1> + <a href="{% url 'colleges:fellowship_add' %}">Add new Fellowship</a> + + <table class="table mt-3"> + <thead> + <tr> + <th>Fellow</th> + <th>Discipline</th> + <th>Start date</th> + <th>End date</th> + <th></th> + </tr> + </thead> + <tbody> + {% for fellow in fellowships.regular %} + <tr> + <td>{{ fellow.contributor }}</td> + <td>{{ fellow.contributor.get_discipline_display }}</td> + <td>{{ fellow.start_date|default:'<i>No start date</i>' }}</td> + <td>{{ fellow.until_date|default:'<i>No end date</i>' }}</td> + <td> + <a href="{{ fellow.get_absolute_url }}">View Fellowship details</a> + </td> + </tr> + {% empty %} + <tr> + <td class="text-danger py-2" colspan="4">There are no active fellowships!</td> + </tr> + {% endfor %} + </tbody> + </table> + + <h1>Active Guest Fellowships</h1> + <a href="{% url 'colleges:fellowship_add' %}?guest=1">Add new Guest Fellowship</a> + + <table class="table mt-3"> + <thead> + <tr> + <th>Fellow</th> + <th>Discipline</th> + <th>Start date</th> + <th>End date</th> + <th></th> + </tr> + </thead> + <tbody> + {% for fellow in fellowships.guests %} + <tr> + <td>{{ fellow.contributor }}</td> + <td>{{ fellow.contributor.get_discipline_display }}</td> + <td>{{ fellow.start_date|default:'<i>No start date</i>' }}</td> + <td>{{ fellow.until_date|default:'<i>No end date</i>' }}</td> + <td> + <a href="{{ fellow.get_absolute_url }}">View Fellowship details</a> + </td> + </tr> + {% empty %} + <tr> + <td class="py-2" colspan="4">There are no active guests fellowships</td> + </tr> + {% endfor %} + </tbody> + </table> +{% endblock %} diff --git a/colleges/templates/colleges/submission_add.html b/colleges/templates/colleges/submission_add.html new file mode 100644 index 0000000000000000000000000000000000000000..e071c00b49bf87ae604544bc030a88a39680d426 --- /dev/null +++ b/colleges/templates/colleges/submission_add.html @@ -0,0 +1,30 @@ +{% extends 'submissions/admin/base.html' %} + +{% load bootstrap %} + +{% block breadcrumb_items %} + {{ block.super }} + <a href="{% url 'colleges:submission' submission.arxiv_identifier_w_vn_nr %}" class="breadcrumb-item">Submission Pool Composition</a> + <span class="breadcrumb-item">Add Fellowship</span> +{% endblock %} + +{% block pagetitle %}: Add Fellowship{% endblock pagetitle %} + +{% block content %} + <h1>Add Fellowship to Submission's Pool</h1> + <h2 class="text-primary">{{submission.title}}</h2> + <h3 class="mb-3">by {{submission.author_list}}</h3> + {% include 'submissions/_submission_summary.html' with submission=submission hide_title=1 %} + <br> + + <h3>Choose one of the following (active) Fellowships to add to the Pool:</h3> + {% include 'partials/colleges/conflicts_of_interests.html' with submission=submission %} + <br> + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input class="btn btn-primary" type="submit" value="Add Fellowship"> + </form> + + +{% endblock %} diff --git a/colleges/templates/colleges/submission_pool.html b/colleges/templates/colleges/submission_pool.html new file mode 100644 index 0000000000000000000000000000000000000000..1b487dc58ba434d0d1b45f38888154d73703447d --- /dev/null +++ b/colleges/templates/colleges/submission_pool.html @@ -0,0 +1,48 @@ +{% extends 'submissions/admin/base.html' %} + +{% load bootstrap %} + +{% block breadcrumb_items %} + {{ block.super }} + <span class="breadcrumb-item">Submission Pool Composition</span> +{% endblock %} + +{% block pagetitle %}: Submission Pool Composition{% endblock pagetitle %} + +{% block content %} + <h1>Submission Pool Composition</h1> + <h2 class="text-primary">{{submission.title}}</h2> + <h3 class="mb-3">by {{submission.author_list}}</h3> + {% include 'submissions/_submission_summary.html' with submission=submission hide_title=1 %} + <br> + + <h3>Pool Composition</h3> + {% include 'partials/colleges/conflicts_of_interests.html' with submission=submission %} + <table class="table table-hover"> + <thead> + <tr> + <th>Fellowship ID</th> + <th>Fellow</th> + <th>Type</th> + <th colspan="2">Date range</th> + </tr> + </thead> + <tbody> + {% for fellowship in submission.fellows.all %} + <tr> + <td>{{ fellowship.id }}</td> + <td>{{ fellowship.contributor }}</td> + <td>{{ fellowship.guest|yesno:"Guest fellowship,Regular fellowship"|safe }}</td> + <td> + <a class="text-danger" href="{% url 'colleges:fellowship_remove_submission' fellowship.id submission.arxiv_identifier_w_vn_nr %}">Remove this Fellowship from Submission's pool</a> + </td> + </tr> + {% endfor %} + <tr> + <td colspan="4" class="py-3 text-center"><a href="{% url 'colleges:submission_add_fellowship' submission.arxiv_identifier_w_vn_nr %}">Add Fellowship to this Submission's pool</a></td> + </tr> + </tbody> + </table> + + +{% endblock %} diff --git a/colleges/templates/partials/colleges/conflicts_of_interests.html b/colleges/templates/partials/colleges/conflicts_of_interests.html new file mode 100644 index 0000000000000000000000000000000000000000..6ad78a9774a6bd11809199f0f337b4a2c272645b --- /dev/null +++ b/colleges/templates/partials/colleges/conflicts_of_interests.html @@ -0,0 +1,5 @@ +{% comment %} + + Pretty damn complicated + +{% endcomment %} diff --git a/colleges/tests.py b/colleges/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/colleges/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/colleges/urls.py b/colleges/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..e5ddb94593e636403f50668cb851337abfa3e211 --- /dev/null +++ b/colleges/urls.py @@ -0,0 +1,31 @@ +from django.conf.urls import url + +from submissions.constants import SUBMISSIONS_COMPLETE_REGEX + +from . import views + +urlpatterns = [ + # Fellowships + url(r'^fellowships/$', views.fellowships, name='fellowships'), + url(r'^fellowships/add$', views.fellowship_add, name='fellowship_add'), + url(r'^fellowships/(?P<id>[0-9]+)/$', views.fellowship_detail, name='fellowship'), + url(r'^fellowships/(?P<id>[0-9]+)/edit$', views.fellowship_edit, name='fellowship_edit'), + url(r'^fellowships/(?P<id>[0-9]+)/terminate$', views.fellowship_terminate, + name='fellowship_terminate'), + url(r'^fellowships/submissions/{regex}/$'.format( + regex=SUBMISSIONS_COMPLETE_REGEX), views.submission_pool, + name='submission'), + url(r'^fellowships/submissions/{regex}/add$'.format( + regex=SUBMISSIONS_COMPLETE_REGEX), views.submission_add_fellowship, + name='submission_add_fellowship'), + url(r'^fellowships/(?P<id>[0-9]+)/submissions/{regex}/remove$'.format( + regex=SUBMISSIONS_COMPLETE_REGEX), views.fellowship_remove_submission, + name='fellowship_remove_submission'), + url(r'^fellowships/(?P<id>[0-9]+)/submissions/add$', + views.fellowship_add_submission, name='fellowship_add_submission'), + + url(r'^fellowships/(?P<id>[0-9]+)/proceedings/add$', + views.fellowship_add_proceedings, name='fellowship_add_proceedings'), + url(r'^fellowships/(?P<id>[0-9]+)/proceedings/(?P<proceedings_id>[0-9]+)/remove$', + views.fellowship_remove_proceedings, name='fellowship_remove_proceedings'), +] diff --git a/colleges/views.py b/colleges/views.py new file mode 100644 index 0000000000000000000000000000000000000000..81bc5f917f3c16d522aedcd778f2fedfd4bee4d2 --- /dev/null +++ b/colleges/views.py @@ -0,0 +1,229 @@ +from django.contrib import messages +from django.contrib.auth.decorators import login_required, permission_required +from django.shortcuts import get_object_or_404, render, redirect +from django.core.urlresolvers import reverse + +from proceedings.models import Proceedings +from submissions.models import Submission + +from .forms import FellowshipForm, FellowshipTerminateForm, FellowshipRemoveSubmissionForm,\ + FellowshipAddSubmissionForm, AddFellowshipForm, SubmissionAddFellowshipForm,\ + FellowshipRemoveProceedingsForm, FellowshipAddProceedingsForm +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. + """ + fellowships = Fellowship.objects.active() + + context = { + 'fellowships': fellowships + } + return render(request, 'colleges/fellowships.html', context) + + +@login_required +@permission_required('scipost.can_manage_college_composition', raise_exception=True) +def fellowship_detail(request, id): + """ + View details of a specific fellowship + """ + fellowship = get_object_or_404(Fellowship, id=id) + + context = { + 'fellowship': fellowship + } + return render(request, 'colleges/fellowship_details.html', context) + + +@login_required +@permission_required('scipost.can_manage_college_composition', raise_exception=True) +def fellowship_add(request): + """ + Create a new Fellowship. + """ + form = AddFellowshipForm(request.POST or None, initial=request.GET or None) + + if form.is_valid(): + fellowship = form.save() + messages.success(request, 'Fellowship added.') + return redirect(fellowship.get_absolute_url()) + + context = { + 'form': form + } + return render(request, 'colleges/fellowship_add.html', context) + + +@login_required +@permission_required('scipost.can_manage_college_composition', raise_exception=True) +def fellowship_edit(request, id): + """ + Edit basic information about fellowship. + """ + fellowship = get_object_or_404(Fellowship, id=id) + form = FellowshipForm(request.POST or None, instance=fellowship) + + if form.is_valid(): + form.save() + messages.success(request, 'Fellowship updated.') + return redirect(fellowship.get_absolute_url()) + + context = { + 'fellowship': fellowship, + 'form': form + } + return render(request, 'colleges/fellowship_edit.html', context) + + +@login_required +@permission_required('scipost.can_manage_college_composition', raise_exception=True) +def fellowship_terminate(request, id): + """ + Terminate Fellowship by setting the until_date to today's date. + """ + fellowship = get_object_or_404(Fellowship, id=id) + form = FellowshipTerminateForm(request.POST or None, instance=fellowship) + + if form.is_valid(): + form.save() + messages.success(request, 'Fellowship terminated.') + else: + messages.warning(request, 'Fellowship has not been terminated, please try again.') + return redirect(fellowship.get_absolute_url()) + + +@login_required +@permission_required('scipost.can_manage_college_composition', raise_exception=True) +def submission_pool(request, arxiv_identifier_w_vn_nr): + """ + List all Fellowships related to Submission. + """ + submission = get_object_or_404(Submission, arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) + + context = { + 'submission': submission + } + return render(request, 'colleges/submission_pool.html', context) + + +@login_required +@permission_required('scipost.can_manage_college_composition', raise_exception=True) +def submission_add_fellowship(request, arxiv_identifier_w_vn_nr): + """ + Add Fellowship to the pool of a Submission. + """ + submission = get_object_or_404(Submission, arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) + form = SubmissionAddFellowshipForm(request.POST or None, instance=submission) + + if form.is_valid(): + form.save() + messages.success(request, 'Fellowship {fellowship} ({id}) added to Submission.'.format( + fellowship=form.cleaned_data['fellowship'].contributor, + id=form.cleaned_data['fellowship'].id)) + return redirect(reverse('colleges:submission', + args=(submission.arxiv_identifier_w_vn_nr,))) + + context = { + 'submission': submission, + 'form': form, + } + return render(request, 'colleges/submission_add.html', context) + + +@login_required +@permission_required('scipost.can_manage_college_composition', raise_exception=True) +def fellowship_remove_submission(request, id, arxiv_identifier_w_vn_nr): + """ + Remove Submission from the pool of a Fellowship. + """ + fellowship = get_object_or_404(Fellowship, id=id) + submission = get_object_or_404(fellowship.pool.all(), + arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) + form = FellowshipRemoveSubmissionForm(request.POST or None, + submission=submission, instance=fellowship) + + if form.is_valid() and request.POST: + form.save() + messages.success(request, 'Submission {sub} removed from Fellowship.'.format( + sub=arxiv_identifier_w_vn_nr)) + return redirect(fellowship.get_absolute_url()) + + context = { + 'fellowship': fellowship, + 'form': form, + 'submission': submission + } + return render(request, 'colleges/fellowship_submission_remove.html', context) + + +@login_required +@permission_required('scipost.can_manage_college_composition', raise_exception=True) +def fellowship_add_submission(request, id): + """ + Add Submission to the pool of a Fellowship. + """ + fellowship = get_object_or_404(Fellowship, id=id) + form = FellowshipAddSubmissionForm(request.POST or None, instance=fellowship) + + if form.is_valid(): + form.save() + messages.success(request, 'Submission {submission} added to Fellowship.'.format( + submission=form.cleaned_data['submission'].arxiv_identifier_w_vn_nr)) + return redirect(fellowship.get_absolute_url()) + + context = { + 'fellowship': fellowship, + 'form': form, + } + return render(request, 'colleges/fellowship_submission_add.html', context) + + +@login_required +@permission_required('scipost.can_manage_college_composition', raise_exception=True) +def fellowship_remove_proceedings(request, id, proceedings_id): + """ + Remove Proceedings from the pool of a Fellowship. + """ + fellowship = get_object_or_404(Fellowship, id=id) + proceedings = get_object_or_404(fellowship.proceedings.all(), id=proceedings_id) + form = FellowshipRemoveProceedingsForm(request.POST or None, + proceedings=proceedings, instance=fellowship) + + if form.is_valid() and request.POST: + form.save() + messages.success(request, 'Proceedings %s removed from Fellowship.' % str(proceedings)) + return redirect(fellowship.get_absolute_url()) + + context = { + 'fellowship': fellowship, + 'form': form, + 'proceedings': proceedings + } + return render(request, 'colleges/fellowship_proceedings_remove.html', context) + + +@login_required +@permission_required('scipost.can_manage_college_composition', raise_exception=True) +def fellowship_add_proceedings(request, id): + """ + Add Proceedings to the pool of a Fellowship. + """ + fellowship = get_object_or_404(Fellowship, id=id) + form = FellowshipAddProceedingsForm(request.POST or None, instance=fellowship) + + if form.is_valid(): + form.save() + proceedings = form.cleaned_data.get('proceedings', '') + messages.success(request, 'Proceedings %s added to Fellowship.' % str(proceedings)) + return redirect(fellowship.get_absolute_url()) + + context = { + 'fellowship': fellowship, + 'form': form, + } + return render(request, 'colleges/fellowship_proceedings_add.html', context) diff --git a/cronjob_production.sh b/cronjob_production.sh new file mode 100644 index 0000000000000000000000000000000000000000..844269c99fce142ebacc0e7caf176b3c84888f5a --- /dev/null +++ b/cronjob_production.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +cd /home/jscaux/webapps/scipost/SciPost_v1/ +export DJANGO_SETTINGS_MODULE='SciPost_v1.settings.production' + +python3.5 manage.py remind_fellows_to_submit_report diff --git a/cronjob_staging.sh b/cronjob_staging.sh new file mode 100644 index 0000000000000000000000000000000000000000..98bfc21a2764d8c2803e8b0738b20b880b5f18af --- /dev/null +++ b/cronjob_staging.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +cd /home/jdewit/webapps/scipost/SciPost_v1/ +export DJANGO_SETTINGS_MODULE='SciPost_v1.settings.staging_release' + +/usr/local/bin/python3.5 manage.py remind_fellows_to_submit_report diff --git a/journals/constants.py b/journals/constants.py index 7673cf16062813b0d1ceba90839c40b082fce962..3c1d7b5b173d7d7468dc6758ab0621abbb99ea34 100644 --- a/journals/constants.py +++ b/journals/constants.py @@ -7,7 +7,8 @@ SCIPOST_JOURNAL_PHYSICS_PROC = 'SciPostPhysProc' # Journal open for submission SCIPOST_JOURNALS_SUBMIT = ( (SCIPOST_JOURNAL_PHYSICS, 'SciPost Physics'), - (SCIPOST_JOURNAL_PHYSICS_LECTURE_NOTES, 'SciPost Physics Lecture Notes') + (SCIPOST_JOURNAL_PHYSICS_LECTURE_NOTES, 'SciPost Physics Lecture Notes'), + (SCIPOST_JOURNAL_PHYSICS_PROC, 'SciPost Proceedings') ) # Journal closed for submission diff --git a/journals/forms.py b/journals/forms.py index 688ade7e6c2f450da0473aeadc48cabb756c1dff..3883c6af3ad372e3489770a8ea5b00f3cfe6e822 100644 --- a/journals/forms.py +++ b/journals/forms.py @@ -11,7 +11,7 @@ from submissions.models import Submission class InitiatePublicationForm(forms.Form): accepted_submission = forms.ModelChoiceField(queryset=Submission.objects.accepted()) to_be_issued_in = forms.ModelChoiceField( - queryset=Issue.objects.filter(until_date__gt=timezone.now())) + queryset=Issue.objects.filter(until_date__gte=timezone.now())) def __init__(self, *args, **kwargs): super(InitiatePublicationForm, self).__init__(*args, **kwargs) diff --git a/journals/migrations/0046_auto_20171019_1942.py b/journals/migrations/0046_auto_20171019_1942.py new file mode 100644 index 0000000000000000000000000000000000000000..1c199f6062ca8483423a788fdcfee6aa3fbc523d --- /dev/null +++ b/journals/migrations/0046_auto_20171019_1942.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-19 17:42 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0045_auto_20170925_2124'), + ] + + operations = [ + migrations.AlterField( + model_name='journal', + name='name', + field=models.CharField(choices=[('SciPostPhys', 'SciPost Physics'), ('SciPostPhysLectNotes', 'SciPost Physics Lecture Notes'), ('SciPostPhysProc', 'SciPost Proceedings'), ('SciPostPhysSel', 'SciPost Physics Select'), ('SciPostPhysProc', 'SciPost Physics Proceedings')], max_length=100, unique=True), + ), + ] diff --git a/journals/templates/journals/_publication_details.html b/journals/templates/journals/_publication_details.html index a8844ec1995f4804de33d1e8d72f772635353a11..014da1f23f015d0a82d9b59bf0be546a35470a09 100644 --- a/journals/templates/journals/_publication_details.html +++ b/journals/templates/journals/_publication_details.html @@ -4,14 +4,14 @@ <h2 class="pb-1 text-blue">{{publication.title}}</h2> <p class="mb-1">{{ publication.author_list }}</p> - <p class="text-muted"> + <p class="text-muted mb-0"> {{ publication.citation }} · published {{ publication.publication_date|date:'j F Y' }} {% if publication.cc_license != 'CC BY 4.0' %} · licensed under {{publication.get_cc_license_display}} {% endif %} </p> - <ul class="publicationClickables"> + <ul class="publicationClickables mt-3"> <li>doi: {{publication.doi_string}}</li> <li class="publicationPDF"> <a href="{{publication.get_absolute_url}}/pdf" target="_blank">pdf</a> @@ -29,6 +29,11 @@ </div> <div class="row"> <div class="col-12"> + {% if publication.in_issue.proceedings %} + <h3>Proceedings event</h3> + <p><a href="{{ publication.in_issue.get_absolute_url }}">{{ publication.in_issue.proceedings.event_name }}</a></p> + {% endif %} + <h3>Abstract</h3> <p class="abstract">{{ publication.abstract }}</p> </div> diff --git a/journals/templates/journals/journal_issue_detail.html b/journals/templates/journals/journal_issue_detail.html index c15afdb79d285325108d0deea87cec3ed6452ca5..6b646613d4d4de29b67a170ad2c1d32a6e20dbf2 100644 --- a/journals/templates/journals/journal_issue_detail.html +++ b/journals/templates/journals/journal_issue_detail.html @@ -27,6 +27,10 @@ {{block.super}} {% endwith %} + {% if issue.proceedings %} + {% include 'partials/proceedings/description.html' with proceedings=issue.proceedings %} + {% endif %} + <div class="row"> <div class="col-12"> <ul class="list-unstyled"> diff --git a/journals/views.py b/journals/views.py index 160cee4591d95f60e4eb99ae58df0604dab09bfa..792062e6b868e99ac9f1f681f05f768a39ded83f 100644 --- a/journals/views.py +++ b/journals/views.py @@ -245,7 +245,6 @@ def validate_publication(request): publication = validate_publication_form.save() # Fill in remaining data - #publication.pdf_file = request.FILES['pdf_file'] submission = publication.accepted_submission publication.authors.add(*submission.authors.all()) if publication.first_author: diff --git a/notifications/admin.py b/notifications/admin.py index 2cf8c7c7b329a7a98b23bb5332d71059d5023d56..247e0620f5986d20ab061e97750e1053eb4ca53a 100644 --- a/notifications/admin.py +++ b/notifications/admin.py @@ -5,8 +5,8 @@ from .models import Notification class NotificationAdmin(admin.ModelAdmin): raw_id_fields = ('recipient', ) list_display = ('recipient', 'actor', - 'level', 'target', 'unread', 'public') - list_filter = ('level', 'unread', 'public', 'created', ) + 'level', 'target', 'unread',) + list_filter = ('level', 'unread', 'created',) admin.site.register(Notification, NotificationAdmin) diff --git a/notifications/constants.py b/notifications/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..d160034357dfbda4f17bda8cb90a1eab3f2b34ae --- /dev/null +++ b/notifications/constants.py @@ -0,0 +1,7 @@ +NOTIFICATION_REFEREE_DEADLINE = 'referee_task_deadline' +NOTIFICATION_REFEREE_OVERDUE = 'referee_task_overdue' + +NOTIFICATION_TYPES = ( + (NOTIFICATION_REFEREE_DEADLINE, 'Refereeing Task is approaching its deadline'), + (NOTIFICATION_REFEREE_OVERDUE, 'Refereeing Task is overdue') +) diff --git a/notifications/migrations/0002_auto_20171021_1821.py b/notifications/migrations/0002_auto_20171021_1821.py new file mode 100644 index 0000000000000000000000000000000000000000..30945c04698db4acdb74c2157fc7a2c76e372d3a --- /dev/null +++ b/notifications/migrations/0002_auto_20171021_1821.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-21 16:21 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('notifications', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='notification', + name='emailed', + ), + migrations.RemoveField( + model_name='notification', + name='public', + ), + migrations.AddField( + model_name='notification', + name='internal_type', + field=models.CharField(blank=True, choices=[('referee_task_deadline', 'Refereeing Task is approaching its deadline'), ('referee_task_overdue', 'Refereeing Task is overdue')], max_length=255), + ), + ] diff --git a/notifications/models.py b/notifications/models.py index ecec80cfea09c80841445b49b984d9b8cde60c25..d7d9fef894b28bc7af060517cb9d136c502f0940 100644 --- a/notifications/models.py +++ b/notifications/models.py @@ -5,6 +5,7 @@ from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey from django.utils import timezone +from .constants import NOTIFICATION_TYPES from .managers import NotificationQuerySet @@ -52,8 +53,9 @@ class Notification(models.Model): created = models.DateTimeField(default=timezone.now) - public = models.BooleanField(default=True) - emailed = models.BooleanField(default=False) + # This field is for internal use only. It is used to prevent duplicate sending + # of notifications. + internal_type = models.CharField(max_length=255, blank=True, choices=NOTIFICATION_TYPES) objects = NotificationQuerySet.as_manager() diff --git a/notifications/signals.py b/notifications/signals.py index af8be2f2807ae3a16573f49e4319d49b5a615dac..94355790559049e14241f89fb8c4cddd8d65f779 100644 --- a/notifications/signals.py +++ b/notifications/signals.py @@ -4,7 +4,7 @@ from .models import Notification notify = Signal(providing_args=[ - 'recipient', 'actor', 'verb', 'action_object', 'target', 'description', 'level' + 'recipient', 'actor', 'verb', 'action_object', 'target', 'description', 'level', 'type' ]) @@ -23,7 +23,8 @@ def notify_receiver(sender, **kwargs): action_object=kwargs.get('action_object'), target=kwargs.get('target'), description=kwargs.get('description'), - level=kwargs.get('level', 'info') + level=kwargs.get('level', 'info'), + internal_type=kwargs.get('type', '') ) notification.save() print("Request finished!") diff --git a/notifications/templates/notifications/partials/notice.html b/notifications/templates/notifications/partials/notice.html index 5ce4c87d0a529b780c99a5cb9a5113a1cb5497c2..d9ee3bf07a29e07d1dd189665d865c6157ad5dc2 100644 --- a/notifications/templates/notifications/partials/notice.html +++ b/notifications/templates/notifications/partials/notice.html @@ -14,7 +14,7 @@ {% if notice.actor.first_name and notice.actor.last_name %} {{ notice.actor.first_name}} {{ notice.actor.last_name }} {% else %} - {{ notice.actor }} + {# {{ notice.actor }}#} {% endif %} </strong> {{ notice.verb }} diff --git a/notifications/urls.py b/notifications/urls.py index 23bb20e9e1e4aedb2bc6ec2191bf9dbf2f4c5101..7fd8627126d17a50579d3d39bdd523c1841ab137 100644 --- a/notifications/urls.py +++ b/notifications/urls.py @@ -4,7 +4,6 @@ from . import views urlpatterns = [ - url(r'^$', views.AllNotificationsList.as_view(), name='all'), url(r'^redirect/(?P<slug>\d+)$', views.forward, name='forward'), url(r'^mark-all-as-read/$', views.mark_all_as_read, name='mark_all_as_read'), url(r'^mark-toggle/(?P<slug>\d+)/$', views.mark_toggle, name='mark_toggle'), diff --git a/notifications/views.py b/notifications/views.py index e309196bb51f93c2f465baad3b931219655118d6..bcdf7e3b03fc93946b0d254b3136d9c9b342fc5a 100644 --- a/notifications/views.py +++ b/notifications/views.py @@ -3,8 +3,6 @@ from django.contrib.auth.models import User from django.forms import model_to_dict from django.http import JsonResponse from django.shortcuts import get_object_or_404, redirect -from django.utils.decorators import method_decorator -from django.views.generic import ListView from .models import Notification from .utils import id2slug, slug2id @@ -14,20 +12,6 @@ def is_test_user(user): return user.groups.filter(name='Testers').exists() -@method_decorator(login_required, name='dispatch') -@method_decorator(user_passes_test(is_test_user), name='dispatch') -class NotificationViewList(ListView): - context_object_name = 'notifications' - - -class AllNotificationsList(NotificationViewList): - """ - Index page for authenticated user - """ - def get_queryset(self): - return self.request.user.notifications.all() - - @login_required @user_passes_test(is_test_user) def forward(request, slug): @@ -125,7 +109,10 @@ def live_notification_list(request): else: struct['actor'] = str(n.actor) if n.target: - struct['target'] = str(n.target) + if hasattr(n.target, 'notification_name'): + struct['target'] = n.target.notification_name + else: + struct['target'] = str(n.target) struct['forward_link'] = n.get_absolute_url() if n.action_object: struct['action_object'] = str(n.action_object) diff --git a/proceedings/__init__.py b/proceedings/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/proceedings/admin.py b/proceedings/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..67d87120b6087ac7fe9e8825899d9311b5c0e8ed --- /dev/null +++ b/proceedings/admin.py @@ -0,0 +1,11 @@ +from django.contrib import admin + +from .models import Proceedings + + +class ProceedingsAdmin(admin.ModelAdmin): + list_display = ('__str__', 'issue',) + list_filter = ('issue',) + + +admin.site.register(Proceedings, ProceedingsAdmin) diff --git a/proceedings/apps.py b/proceedings/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..dd2aa26255e92b46cce1f14e6677000b9f6023e7 --- /dev/null +++ b/proceedings/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ProceedingsConfig(AppConfig): + name = 'proceedings' diff --git a/proceedings/forms.py b/proceedings/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..bd8be93f0fc8c85e6fe364db1c978d2ea4da9c4b --- /dev/null +++ b/proceedings/forms.py @@ -0,0 +1,18 @@ +from django import forms + +from .models import Proceedings + + +class ProceedingsForm(forms.ModelForm): + class Meta: + model = Proceedings + fields = ( + 'issue', + 'event_name', + 'event_description', + 'event_start_date', + 'event_end_date', + 'submissions_open', + 'submissions_deadline', + 'submissions_close', + ) diff --git a/proceedings/managers.py b/proceedings/managers.py new file mode 100644 index 0000000000000000000000000000000000000000..d16a9b116b482a057840e22b2a2b0397edfee902 --- /dev/null +++ b/proceedings/managers.py @@ -0,0 +1,9 @@ +import datetime + +from django.db import models + + +class ProceedingsQuerySet(models.QuerySet): + def open_for_submission(self): + today = datetime.date.today() + return self.filter(submissions_open__lte=today, submissions_close__gte=today) diff --git a/proceedings/migrations/0001_initial.py b/proceedings/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..32cf6e68fd089eec86c74ede3ae26c4761caaa98 --- /dev/null +++ b/proceedings/migrations/0001_initial.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-20 07:00 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import scipost.db.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('colleges', '0001_initial'), + ('journals', '0046_auto_20171019_1942'), + ] + + operations = [ + migrations.CreateModel( + name='Proceedings', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('latest_activity', scipost.db.fields.AutoDateTimeField(blank=True, default=django.utils.timezone.now, editable=False)), + ('issue_name', models.CharField(max_length=256)), + ('event_name', models.CharField(blank=True, max_length=256)), + ('event_description', models.TextField(blank=True)), + ('event_start_date', models.DateField(blank=True, null=True)), + ('event_end_date', models.DateField(blank=True, null=True)), + ('submissions_open', models.DateField()), + ('submissions_deadline', models.DateField()), + ('submissions_close', models.DateField()), + ('fellowships', models.ManyToManyField(blank=True, related_name='proceedings', to='colleges.Fellowship')), + ('issue', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='proceedings', to='journals.Issue')), + ('lead_fellow', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='colleges.Fellowship')), + ], + options={ + 'verbose_name': 'Proceedings', + 'verbose_name_plural': 'Proceedings', + 'default_related_name': 'proceedings', + }, + ), + ] diff --git a/proceedings/migrations/0002_remove_proceedings_issue_name.py b/proceedings/migrations/0002_remove_proceedings_issue_name.py new file mode 100644 index 0000000000000000000000000000000000000000..0045b112d3a41eec21b759ac43ec1ae0f049070f --- /dev/null +++ b/proceedings/migrations/0002_remove_proceedings_issue_name.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-21 12:56 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('proceedings', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='proceedings', + name='issue_name', + ), + ] diff --git a/proceedings/migrations/__init__.py b/proceedings/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/proceedings/models.py b/proceedings/models.py new file mode 100644 index 0000000000000000000000000000000000000000..83408cac946556362879ca29e7787118a50c2e6b --- /dev/null +++ b/proceedings/models.py @@ -0,0 +1,52 @@ +import datetime + +from django.core.urlresolvers import reverse +from django.db import models + +from scipost.behaviors import TimeStampedModel + +from .managers import ProceedingsQuerySet + + +class Proceedings(TimeStampedModel): + """ + A Proceeding is a special kind of Journal Issue. + """ + # Link to the actual Journal platform + issue = models.OneToOneField('journals.Issue', related_name='proceedings', + limit_choices_to={ + 'in_volume__in_journal__name': 'SciPostPhysProc'}) + + # Event the Proceedings is for + event_name = models.CharField(max_length=256, blank=True) + event_description = models.TextField(blank=True) + event_start_date = models.DateField(null=True, blank=True) + event_end_date = models.DateField(null=True, blank=True) + + # Fellows + lead_fellow = models.ForeignKey('colleges.Fellowship', null=True, blank=True, related_name='+') + fellowships = models.ManyToManyField('colleges.Fellowship', blank=True, + limit_choices_to={'guest': True}) + + # Submission data + submissions_open = models.DateField() + submissions_deadline = models.DateField() + submissions_close = models.DateField() + + objects = ProceedingsQuerySet.as_manager() + + class Meta: + verbose_name = 'Proceedings' + verbose_name_plural = 'Proceedings' + default_related_name = 'proceedings' + + def __str__(self): + return self.event_name + + def get_absolute_url(self): + return reverse('proceedings:proceedings_details', args=(self.id,)) + + @property + def open_for_submission(self): + today = datetime.date.today() + return self.submissions_open <= today and self.submissions_close >= today diff --git a/proceedings/templates/partials/proceedings/description.html b/proceedings/templates/partials/proceedings/description.html new file mode 100644 index 0000000000000000000000000000000000000000..038474ca7039369f707797110c2384a04a68e803 --- /dev/null +++ b/proceedings/templates/partials/proceedings/description.html @@ -0,0 +1,3 @@ +<h3>Event: {{ proceedings.event_name }}</h3> +<h4 class="pt-0 text-muted">From {{ proceedings.event_start_date }} until {{ proceedings.event_end_date }}</h4> +<p class="mt-1">{{ proceedings.event_description|linebreaksbr }}</p> diff --git a/proceedings/templates/partials/proceedings/summary.html b/proceedings/templates/partials/proceedings/summary.html new file mode 100644 index 0000000000000000000000000000000000000000..b7b2e7e513de253e68314826a955d30752d14e04 --- /dev/null +++ b/proceedings/templates/partials/proceedings/summary.html @@ -0,0 +1,42 @@ +<table class="proceedings summary"> + <tr> + <th>Event name</th> + <td>{{ proceedings.event_name }}</td> + </tr> + <tr> + <th>Description</th> + <td>{{ proceedings.event_description|default:'-'|linebreaksbr }}</td> + </tr> + {% if proceedings.event_start_date %} + <tr> + <th>Event start date</th> + <td>{{ proceedings.event_start_date }}</td> + </tr> + {% endif %} + {% if proceedings.event_end_date %} + <tr> + <th>Event end date</th> + <td>{{ proceedings.event_end_date }}</td> + </tr> + {% endif %} + <tr> + <th>Guest Fellowships</th> + <td>{{ proceedings.fellowships.count }}</td> + </tr> + <tr> + <th>Lead Fellowship</th> + <td>{{ proceedings.lead_fellow }}</td> + </tr> + <tr> + <th>Submission Open</th> + <td>{{ proceedings.submissions_open }}</td> + </tr> + <tr> + <th>Submission Deadline</th> + <td>{{ proceedings.submissions_deadline }}</td> + </tr> + <tr> + <th>Submission Close</th> + <td>{{ proceedings.submissions_close }}</td> + </tr> +</table> diff --git a/proceedings/templates/proceedings/proceedings.html b/proceedings/templates/proceedings/proceedings.html new file mode 100644 index 0000000000000000000000000000000000000000..cbfac16b9d5a503543dc4ccada6e305a90a54922 --- /dev/null +++ b/proceedings/templates/proceedings/proceedings.html @@ -0,0 +1,42 @@ +{% extends 'submissions/admin/base.html' %} + +{% block breadcrumb_items %} + {{ block.super }} + <span class="breadcrumb-item">Proceedings</span> +{% endblock %} + +{% block pagetitle %}: Manage Proceedings{% endblock pagetitle %} + +{% block content %} + <h1>Manage Proceedings</h1> + <a href="{% url 'proceedings:proceedings_add' %}">Add new Proceedings</a> + + <table class="table mt-3"> + <thead> + <tr> + <th>Event name</th> + <th>Submission Deadline</th> + <th>Guest Fellowships</th> + <th>Submissions</th> + <th></th> + </tr> + </thead> + <tbody> + {% for proc in proceedings %} + <tr> + <td>{{ proc.event_name }}</td> + <td>{{ proc.submissions_deadline }}</td> + <td>{{ proc.fellowships.count }}</td> + <td>{{ proc.submissions.count }}</td> + <td> + <a href="{% url 'proceedings:proceedings_details' proc.id %}">View Proceedings details</a> + </td> + </tr> + {% empty %} + <tr> + <td class="text-danger py-2" colspan="5">There are no Proceedings!</td> + </tr> + {% endfor %} + </tbody> + </table> +{% endblock %} diff --git a/proceedings/templates/proceedings/proceedings_add.html b/proceedings/templates/proceedings/proceedings_add.html new file mode 100644 index 0000000000000000000000000000000000000000..5a5d195de8a740040ff343b7b39961408ed8cc52 --- /dev/null +++ b/proceedings/templates/proceedings/proceedings_add.html @@ -0,0 +1,22 @@ +{% extends 'submissions/admin/base.html' %} + +{% load bootstrap %} + +{% block breadcrumb_items %} + {{ block.super }} + <a href="{% url 'proceedings:proceedings' %}" class="breadcrumb-item">Proceedings</a> + <span class="breadcrumb-item">Add new</span> +{% endblock %} + +{% block pagetitle %}: Add new Proceedings{% endblock pagetitle %} + +{% block content %} + <h1>Add new Proceedings</h1> + + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <button type="submit" class="btn btn-primary">Add Proceedings</button> + </form> + +{% endblock %} diff --git a/proceedings/templates/proceedings/proceedings_details.html b/proceedings/templates/proceedings/proceedings_details.html new file mode 100644 index 0000000000000000000000000000000000000000..f2efc0fbcb74d3d698d538608622de8a90fb11c0 --- /dev/null +++ b/proceedings/templates/proceedings/proceedings_details.html @@ -0,0 +1,84 @@ +{% extends 'submissions/admin/base.html' %} + +{% block breadcrumb_items %} + {{ block.super }} + <a href="{% url 'proceedings:proceedings' %}" class="breadcrumb-item">Proceedings</a> + <span class="breadcrumb-item">Proceedings details</span> +{% endblock %} + +{% block pagetitle %}: Proceedings details{% endblock pagetitle %} + +{% block content %} + <h1>Proceedings details</h1> + <h2 class="text-primary">{{ proceedings }}</h2> + <br> + + {% include 'partials/proceedings/summary.html' with proceedings=proceedings %} + +{% comment %} + <form method="post" action="{% url 'colleges:fellowship_terminate' fellowship.id %}" class="d-inline"> + {% csrf_token %} + <button type="submit" class="btn btn-danger">Terminate Fellowship</button> + </form> + <a href="{% url 'colleges:fellowship_edit' fellowship.id %}" class="btn btn-info ml-2">Edit Fellowship</a> + {% endcomment %} + + + <h3 class="mt-3">All Guest Fellowships of this Proceedings</h3> + + <table class="table"> + <thead> + <tr> + <th>Fellowship ID</th> + <th>Fellow</th> + <th>Type</th> + <th colspan="2">Date range</th> + </tr> + </thead> + <tbody> + {% for fellowship in proceedings.fellowships.all %} + <tr> + <td>{{ fellowship.id }}</td> + <td>{{ fellowship.contributor }}</td> + <td>{{ fellowship.guest|yesno:"Guest fellowship,Regular fellowship"|safe }}</td> + <td> + {% if fellowship.start_date %} + from {{ fellowship.start_date }} + {% endif %} + {% if fellowship.until_date %} + until {{ fellowship.until_date }} + {% endif %} + {% if not fellowship.start_date and not fellowship.until_date %} + <i>Unlimited</i> + {% endif %} + </td> + <td><a href="{{ fellowship.get_absolute_url }}">See details</a></td> + </tr> + {% endfor %} + </tbody> + </table> + + <h3>All Submissions for this Proceedings Issue</h3> + <table class="table table-hover"> + <thead> + <tr> + <th>Submission</th> + <th>Author List</th> + <th colspan="2">Status</th> + </tr> + </thead> + <tbody> + {% for submission in proceedings.submissions.all %} + <tr> + <td> + <a href="{{ submission.get_absolute_url }}">{{ submission.arxiv_identifier_w_vn_nr }}, {{ submission.title|truncatechars:50 }}</a> + </td> + <td>{{ submission.author_list }}</td> + <td>{{ submission.get_status_display }}</td> + <td><a href="{% url 'submissions:editorial_page' submission.arxiv_identifier_w_vn_nr %}">Editorial Page</a></td> + </tr> + {% endfor %} + </tbody> + </table> + +{% endblock %} diff --git a/proceedings/templates/proceedings/proceedings_edit.html b/proceedings/templates/proceedings/proceedings_edit.html new file mode 100644 index 0000000000000000000000000000000000000000..6b837910a45764d72eb469526df95ceb5560f006 --- /dev/null +++ b/proceedings/templates/proceedings/proceedings_edit.html @@ -0,0 +1,22 @@ +{% extends 'submissions/admin/base.html' %} + +{% load bootstrap %} + +{% block breadcrumb_items %} + {{ block.super }} + <a href="{% url 'proceedings:proceedings' %}" class="breadcrumb-item">Proceedings</a> + <span class="breadcrumb-item">Edit new</span> +{% endblock %} + +{% block pagetitle %}: Edit Proceedings{% endblock pagetitle %} + +{% block content %} + <h1>Edit Proceedings</h1> + + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <button type="submit" class="btn btn-primary">Save Proceedings</button> + </form> + +{% endblock %} diff --git a/proceedings/tests.py b/proceedings/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/proceedings/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/proceedings/urls.py b/proceedings/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..1f51ce041cb7d6b89feb0fdad2d7eff10376864e --- /dev/null +++ b/proceedings/urls.py @@ -0,0 +1,11 @@ +from django.conf.urls import url + +from . import views + +urlpatterns = [ + # Proceedings + url(r'^$', views.proceedings, name='proceedings'), + url(r'^add/$', views.ProceedingsAddView.as_view(), name='proceedings_add'), + url(r'^(?P<id>[0-9]+)/$', views.proceedings_details, name='proceedings_details'), + url(r'^(?P<id>[0-9]+)/edit$', views.ProceedingsUpdateView.as_view(), name='proceedings_edit'), +] diff --git a/proceedings/views.py b/proceedings/views.py new file mode 100644 index 0000000000000000000000000000000000000000..d2db1c612dc05c19bc59dfa538a63cfe36854bb5 --- /dev/null +++ b/proceedings/views.py @@ -0,0 +1,46 @@ +from django.contrib.auth.decorators import login_required, permission_required +from django.shortcuts import get_object_or_404, render +from django.views.generic.edit import CreateView, UpdateView + +from .forms import ProceedingsForm +from .models import Proceedings + + +@login_required +@permission_required('scipost.can_manage_college_composition', raise_exception=True) +def proceedings(request): + """ + List all Proceedings + """ + context = { + 'proceedings': Proceedings.objects.all() + } + return render(request, 'proceedings/proceedings.html', context) + + +@login_required +@permission_required('scipost.can_manage_college_composition', raise_exception=True) +def proceedings_details(request, id): + """ + Show Proceedings details + """ + proceedings = get_object_or_404(Proceedings, id=id) + context = { + 'proceedings': proceedings + } + return render(request, 'proceedings/proceedings_details.html', context) + + +class ProceedingsAddView(CreateView): + models = Proceedings + form_class = ProceedingsForm + template_name = 'proceedings/proceedings_add.html' + + +class ProceedingsUpdateView(UpdateView): + models = Proceedings + form_class = ProceedingsForm + template_name = 'proceedings/proceedings_edit.html' + + def get_object(self): + return get_object_or_404(Proceedings, id=self.kwargs['id']) diff --git a/production/models.py b/production/models.py index 549027075dea4866f62f1cb3cc2d5620cfd60868..65ace229bff3e4de8987e7199005eeb600c4b2f6 100644 --- a/production/models.py +++ b/production/models.py @@ -70,6 +70,10 @@ class ProductionStream(models.Model): def completed(self): return self.status == PRODUCTION_STREAM_COMPLETED + @property + def notification_name(self): + return self.submission.arxiv_identifier_w_vn_nr + class ProductionEvent(models.Model): stream = models.ForeignKey(ProductionStream, on_delete=models.CASCADE, related_name='events') @@ -97,6 +101,10 @@ class ProductionEvent(models.Model): def editable(self): return self.event in [EVENT_MESSAGE, EVENT_HOUR_REGISTRATION] and not self.stream.completed + @property + def notification_name(self): + return self.stream.notification_name + def proofs_upload_location(instance, filename): submission = instance.stream.submission diff --git a/scipost/management/commands/add_groups_and_permissions.py b/scipost/management/commands/add_groups_and_permissions.py index 6e3067a54ec7c406d004e45ebeedbbd14879203a..188cfc787e2c90035a8c035f2b58116452227c70 100644 --- a/scipost/management/commands/add_groups_and_permissions.py +++ b/scipost/management/commands/add_groups_and_permissions.py @@ -38,7 +38,6 @@ class Command(BaseCommand): content_type = ContentType.objects.get_for_model(Contributor) content_type_contact = ContentType.objects.get_for_model(Contact) content_type_draft_invitation = ContentType.objects.get_for_model(DraftInvitation) - content_type_report = ContentType.objects.get_for_model(Report) # Supporting Partners can_manage_SPB, created = Permission.objects.get_or_create( @@ -110,6 +109,10 @@ class Command(BaseCommand): content_type=content_type) # Editorial College + can_manage_college_composition, created = Permission.objects.get_or_create( + codename='can_manage_college_composition', + name='Can manage Editorial College compositions', + content_type=content_type) can_attend_VGMs, created = Permission.objects.get_or_create( codename='can_attend_VGMs', name='Can attend Virtual General Meetings', @@ -153,7 +156,7 @@ class Command(BaseCommand): can_vet_submitted_reports, created = Permission.objects.get_or_create( codename='can_vet_submitted_reports', name='Can vet submitted Reports', - content_type=content_type_report) + content_type=content_type) # Submissions can_submit_manuscript, created = Permission.objects.get_or_create( @@ -304,6 +307,7 @@ class Command(BaseCommand): can_do_plagiarism_checks, can_oversee_refereeing, can_prepare_recommendations_for_voting, + can_manage_college_composition, can_fix_College_decision, can_view_production, can_view_timesheets, diff --git a/scipost/static/scipost/assets/css/_alert.scss b/scipost/static/scipost/assets/css/_alert.scss index 6b7a7e21044e48ef372aaaaf4b47731ddcb3f9f8..0e046a8f52e1852e5ed4bfe7ff18ea25108936bf 100644 --- a/scipost/static/scipost/assets/css/_alert.scss +++ b/scipost/static/scipost/assets/css/_alert.scss @@ -14,6 +14,10 @@ &:last-child { margin-bottom: 1rem; } + + p:last-child { + margin-bottom: 0; + } } .alert-dismissible { diff --git a/scipost/static/scipost/assets/css/_list_group.scss b/scipost/static/scipost/assets/css/_list_group.scss index 7387c9c85f91dd41b8e5ec56003f3fc6bcb80099..4755ec289813f41a3be705df5c68db980d203c26 100644 --- a/scipost/static/scipost/assets/css/_list_group.scss +++ b/scipost/static/scipost/assets/css/_list_group.scss @@ -23,9 +23,3 @@ ul.events-list { } } } - -ul[data-target="active-list"] { - li.active { - background-color: $gray-100; - } -} diff --git a/scipost/static/scipost/assets/css/_navbar.scss b/scipost/static/scipost/assets/css/_navbar.scss index 452b2716637529ae319a7b1506839cd1a91581ce..fc6038b4549af2357dc9d264e4a24afd6496289c 100644 --- a/scipost/static/scipost/assets/css/_navbar.scss +++ b/scipost/static/scipost/assets/css/_navbar.scss @@ -95,10 +95,43 @@ .badge { vertical-align: top; - margin-left: -5px; - margin-top: -2px; - height: 16px; + margin-left: -15px; + margin-top: -6px; + height: 15px; min-width: 16px; - line-height: 12px; + line-height: 10px; + display: none; + padding: 0.25em; + border-radius: 99px; + } +} + +.notifications_container { + color: $scipost-lightestblue; + + .user { + color: $scipost-darkblue; + } + + .fa { + font-size: 150%; + vertical-align: bottom; + margin: 0 0.25rem; + } + + &.positive_count { + color: $scipost-orange; + + .user { + color: $scipost-darkblue; + } + + .badge { + display: inline-block; + } + } + + &::after { + content: none; } } diff --git a/scipost/static/scipost/assets/css/_pool.scss b/scipost/static/scipost/assets/css/_pool.scss index 1b2ac64b648a293df03462ee9e84fbfc45e227bb..8b6dbdff6d5d38938167dbc50a97e8f29ab90914 100644 --- a/scipost/static/scipost/assets/css/_pool.scss +++ b/scipost/static/scipost/assets/css/_pool.scss @@ -1,39 +1,37 @@ -$pool-icons-width: 40px; -$pool-flex-width: calc(100% - 40px); +.pool-list { + > .submission { + border: 1px solid #ddd; + margin-bottom: 2px; + padding-left: 3rem; + padding-right: 3rem; + position: relative; - -.editorial-admin, -.pool { - .pool-item { .icons { - padding-left: 10px; - padding-right: 10px; - position: relative; - min-height: 1px; - flex: 0 0 $pool-icons-width; - max-width: $pool-icons-width; + max-width: 3rem; + width: 3rem; + position: absolute; + left: 0; + top: 0; + padding: 0.75rem 0.5rem; + text-align: center; } - .item { - flex: 0 0 $pool-flex-width; - width: $pool-flex-width; - max-width: none; + &.active { + border-color: $scipost-darkblue; } } - .card.submission-detail { - position: sticky; - position: -webkit-sticky; - position: -moz-sticky; - position: -ms-sticky; - position: -o-sticky; - top: 15px; - } - - #details .loading { + .loading { position: sticky; top: 15px; padding: 5rem 0 3rem 0; text-align: center; } + + .loading-container { + .submission_title, + .author_list { + display: none; + } + } } diff --git a/scipost/static/scipost/assets/css/_popover.scss b/scipost/static/scipost/assets/css/_popover.scss index 6cc116b60479e5d4661291068b077b55cec1d9c6..5ef240e8b434fe68d4c9fd09f3c5052a61ad69f6 100644 --- a/scipost/static/scipost/assets/css/_popover.scss +++ b/scipost/static/scipost/assets/css/_popover.scss @@ -3,15 +3,53 @@ box-shadow: #ccc 0px 1px 2px 1px; } +.navbar-counter .nav-link:hover { + background-color: $white; +} + +.popover.bs-popover-bottom, +.popover.bs-popover-auto[x-placement^="bottom"] { + .arrow::before { + border-bottom-color: rgb(221, 221, 221); + } + + .arrow::after { + border-bottom-color: #f7f7f7; + } +} + .notifications { + border: 0; + border-radius: 0; + + .popover-header { + font-size: 100%; + padding: 0.3rem 1rem; + font-weight: 600; + text-transform: uppercase; + + a { + color: $scipost-darkblue; + } + } + .popover-body { padding: 0; } &.popover .list-group-item { - padding: 9px 14px; + padding: 0.4rem 1rem; border-radius: 0; border-top: 1px solid #fff; + border-left: 0; + border-right: 0; + flex-direction: row; + justify-content: space-between; + display: flex; + + &:last-child { + border-bottom: 0; + } } .actions { @@ -19,8 +57,12 @@ opacity: 0.0; transition: opacity 0.1s; width: 20px; - float: right; height: 100%; + padding-left: 0.25rem; + + .fa[data-toggle="tooltip"] { + font-size: 1em; + } a:hover { .fa-circle-o:before { diff --git a/scipost/static/scipost/assets/js/notifications.js b/scipost/static/scipost/assets/js/notifications.js index 5900e9eddb03d2e24297916bd10a3a260402d966..49fbe7f24e3f96a1d8a2c82cd9191f42a3f1cfd6 100644 --- a/scipost/static/scipost/assets/js/notifications.js +++ b/scipost/static/scipost/assets/js/notifications.js @@ -1,3 +1,4 @@ +var notify_container_class = "notifications_container"; var notify_badge_class = "live_notify_badge"; var notify_menu_class = "live_notify_list"; var notify_api_url_count = "/notifications/api/unread_count/"; @@ -15,10 +16,10 @@ function initiate_popover(reinitiate) { reinitiate = false; } - var notification_template = '<div class="popover notifications" role="tooltip"><div class="arrow"></div><h3 class="popover-header h2"></h3><div class="popover-body"></div></div>'; + var notification_template = '<div class="popover notifications" role="tooltip"><div class="arrow"></div><p class="popover-header"></p><div class="popover-body"></div></div>'; function get_notifications_title() { - return 'Latest notifications <div class="badge badge-warning badge-pill live_notify_badge"></div><div class="mt-1"><small><a href="/notifications">See all my notifications</a> · <a href="javascript:;" class="mark_all_read">Mark all as read</a></small></div>'; + return 'My inbox'; } function get_notifications() { @@ -83,9 +84,15 @@ function mark_toggle(el) { function fill_notification_badge(data) { var badges = document.getElementsByClassName(notify_badge_class); + var container = $('.' + notify_container_class); if (badges) { for(var i = 0; i < badges.length; i++){ badges[i].innerHTML = data.unread_count; + if (data.unread_count > 0) { + container.addClass('positive_count'); + } else { + container.removeClass('positive_count'); + } } } } @@ -94,7 +101,7 @@ function get_notification_list() { fetch_api_data(notify_api_url_list, true, function(data) { var messages = data.list.map(function (item) { - var message = ''; + var message = "<div>"; if(typeof item.actor !== 'undefined'){ message += '<strong>' + item.actor + '</strong>'; } @@ -109,15 +116,16 @@ function get_notification_list() { } } if(typeof item.timesince !== 'undefined'){ - message += " <div class='text-muted'>" + item.timesince + " ago</div>"; + message += "<br><small class='text-muted'>" + item.timesince + " ago</small>"; } + message += "</div>"; if(item.unread) { var mark_as_read = '<div class="actions"><a href="javascript:;" data-slug="' + item.slug + '"><i class="fa fa-circle" data-toggle="tooltip" data-placement="auto" title="Mark as read" aria-hidden="true"></i></a></div>'; } else { var mark_as_read = '<div class="actions"><a href="javascript:;" data-slug="' + item.slug + '"><i class="fa fa-circle-o" data-toggle="tooltip" data-placement="auto" title="Mark as unread" aria-hidden="true"></i></a></div>'; } - return '<li class="list-group-item ' + (item.unread ? ' active' : '') + '">' + mark_as_read + message + '</li>'; + return '<li class="list-group-item ' + (item.unread ? ' active' : '') + '">' + message + mark_as_read + '</li>'; }).join(''); if (messages == '') { diff --git a/scipost/static/scipost/assets/js/scripts.js b/scipost/static/scipost/assets/js/scripts.js index 35883ec42ba87948933db9c2d3973914f7b41b2a..d3478c7210700de698ea232ae6c0ef3f30c63a50 100644 --- a/scipost/static/scipost/assets/js/scripts.js +++ b/scipost/static/scipost/assets/js/scripts.js @@ -28,7 +28,6 @@ var getUrlParameter = function getUrlParameter(sParam) { }; function init_page() { - console.log('init!') // Show right tab if url contains `tab` GET request var tab = getUrlParameter('tab') if (tab) { @@ -45,6 +44,14 @@ function init_page() { $($(this).attr('data-target')).toggle(); }); + // Make links that could possibly hide html blocks + $('[data-toggle="hide"]').on('click', function() { + $($(this).attr('data-target')) + .hide() + .parents('.active') + .removeClass('active'); + }); + activate_tooltip(); } @@ -66,12 +73,12 @@ $(function(){ var self = this, url = $(this).attr('href'), target = $(this).attr('data-target'); - // console.log('click', url, target); - $(target).html('<div class="loading"><i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw"></i></div>'); + $(target) + .show() + .html('<div class="loading"><i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw"></i></div>'); $.get(url + '?json=1').done(function(data) { - // console.log('done', data); $(target).html(data).promise().done(function() { init_page(); }); diff --git a/scipost/templates/scipost/navbar.html b/scipost/templates/scipost/navbar.html index ef66e3d109922ea8d862d26fffecbbd32430eb0c..93ccffea2081b2623fabd849169e3cf6546e36a8 100644 --- a/scipost/templates/scipost/navbar.html +++ b/scipost/templates/scipost/navbar.html @@ -29,9 +29,9 @@ {% if request.user|is_in_group:'Testers' %} <li class="nav-item highlighted dropdown navbar-counter"> <div class="nav-link"> - <span class="user">{% if user.last_name %}{% if user.contributor %}{{ user.contributor.get_title_display }} {% endif %}{{ user.first_name }} {{ user.last_name }}{% else %}{{ user.username }}{% endif %}</span> - <a href="javascript:;" class="d-inline-block ml-1 dropdown-toggle" id="notifications_badge"> - <i class="fa fa-bell-o" aria-hidden="true"></i> + <a href="javascript:;" class="d-inline-block ml-1 dropdown-toggle notifications_container" id="notifications_badge"> + <span class="user">{% if user.last_name %}{% if user.contributor %}{{ user.contributor.get_title_display }} {% endif %}{{ user.first_name }} {{ user.last_name }}{% else %}{{ user.username }}{% endif %}</span> + <i class="fa fa-inbox" aria-hidden="true"></i> {% live_notify_badge classes="badge badge-pill badge-primary" %} </a> {% live_notify_list classes="update_notifications d-none" %} diff --git a/scipost/templates/scipost/personal_page.html b/scipost/templates/scipost/personal_page.html index df6434f83530092e5f3ad28a0e65b77f61e91af5..b5b567f3afe94eb0a76a3955190d5be7725e460f 100644 --- a/scipost/templates/scipost/personal_page.html +++ b/scipost/templates/scipost/personal_page.html @@ -110,13 +110,13 @@ </div> <div class="col-md-6"> {% if contributor %} - {# Scientist fields #} - {% if not contributor.is_currently_available %} - <h3 class="text-warning">You are currently unavailable</h3> - <p>Check your availability underneath if this should not be the case.</p> - <hr> - {% endif %} - {# END: Scientist fields #} + {# Scientist fields #} + {% if not contributor.is_currently_available %} + <h3 class="text-warning">You are currently unavailable</h3> + <p>Check your availability underneath if this should not be the case.</p> + <hr> + {% endif %} + {# END: Scientist fields #} {% endif %} {% if not request.user.contributor.petition_signatories.exists %} @@ -124,8 +124,8 @@ <h3 class="text-danger">Scientists, please help us out!</h3> <p class="mb-1">If it is not listed on our Partners page, please encourage your institution (through a librarian, director, ...) to join by <a class="h3 text-blue" href="{% url 'petitions:petition' slug='join-SPB' %}">signing our petition</a>.</p> </div> + <hr> {% endif %} - <hr> {% if 'SciPost Administrators' in user_groups %} <h3>You are a SciPost Administrator.</h3> @@ -158,6 +158,41 @@ <h3>You are a SciPost Production Officer.</h3> {% endif %} + {% if contributor.fellowships.exists %} + <h3>Your Fellowships:</h3> + <ul class="mb-2"> + {% for fellowship in contributor.fellowships.all %} + <li class="pt-1"> + {{ fellowship.contributor.get_discipline_display }} + + {% if fellowship.guest %} + (Guest Fellowship) + {% else %} + (Regular Fellowship) + {% endif %} + + {% if not fellowship.is_active %} + <span class="label label-outline-warning label-sm">Inactive</span> + {% endif %} + + {% if fellowship.start_date or fellowship.until_date %} + <div class="text-muted"> + {% if fellowship.start_date %} + from {{ fellowship.start_date }} + {% endif %} + + {% if fellowship.until_date %} + until {{ fellowship.until_date }} + {% endif %} + </div> + {% endif %} + </li> + + {% endfor %} + </ul> + <a href="{% url 'submissions:pool' %}" class="h3 text-primary ml-4 px-3 d-block-inline">Go to the Submissions Pool</a> + {% endif %} + <h3 class="mt-3">Update your personal data or password</h3> <ul> @@ -310,6 +345,8 @@ <li><a href="{% url 'journals:manage_metadata' %}">Manage Publication metadata</a></li> <li><a href="{% url 'journals:manage_report_metadata' %}">Manage Report metadata</a></li> <li><a href="{% url 'journals:manage_comment_metadata' %}">Manage Comment metadata</a></li> + <li><a href="{% url 'colleges:fellowships' %}">Manage Fellowships</a></li> + <li><a href="{% url 'proceedings:proceedings' %}">Manage Proceedings Issues</a></li> </ul> {% endif %} diff --git a/scipost/templatetags/user_groups.py b/scipost/templatetags/user_groups.py index 408837b3610ed36499ad4a021c0c8124ccecf6c2..8cd171deae8df806f99faa01f15ed397457ab72e 100644 --- a/scipost/templatetags/user_groups.py +++ b/scipost/templatetags/user_groups.py @@ -10,3 +10,18 @@ def is_edcol_admin(user): This assignment is limited to a certain context block! """ return user.groups.filter(name='Editorial Administrators').exists() or user.is_superuser + + +@register.simple_tag +def is_editor_in_charge(user, submission): + """ + Assign template variable (boolean) to check if user is Editorial Administator. + This assignment is limited to a certain context block! + """ + if user.is_superuser: + return True + + if not hasattr(user, 'contributor'): + return False + + return submission.editor_in_charge == user.contributor diff --git a/submissions/admin.py b/submissions/admin.py index 252b1d19bf854f93693cce13743ce505c905c48e..8a2f0f0e8b2c17c38316d4457694c62ba59aa519 100644 --- a/submissions/admin.py +++ b/submissions/admin.py @@ -86,7 +86,8 @@ class SubmissionAdmin(GuardedModelAdmin): 'discipline', 'domain', 'subject_area', - 'secondary_areas'), + 'secondary_areas', + 'proceedings'), }), ('Authors', { 'classes': ('collapse',), @@ -110,8 +111,10 @@ class SubmissionAdmin(GuardedModelAdmin): 'referees_suggested', 'remarks_for_editors', 'submitted_to_journal', + 'proceedings', 'pdf_refereeing_pack', - 'plagiarism_report'), + 'plagiarism_report', + 'fellows'), }), ('Meta', { 'classes': ('collapse',), diff --git a/submissions/forms.py b/submissions/forms.py index ef88d0341896ece923b8c7d75697eaea8b29d8a9..e3f25ca00d3876b6638dae59aeefa276b044b7fc 100644 --- a/submissions/forms.py +++ b/submissions/forms.py @@ -2,12 +2,9 @@ import re from django import forms from django.conf import settings -from django.contrib.auth.models import Group from django.db import transaction from django.utils import timezone -from guardian.shortcuts import assign_perm - from .constants import ASSIGNMENT_BOOL, ASSIGNMENT_REFUSAL_REASONS, STATUS_RESUBMITTED,\ REPORT_ACTION_CHOICES, REPORT_REFUSAL_CHOICES, STATUS_REVISION_REQUESTED,\ STATUS_REJECTED, STATUS_REJECTED_VISIBLE, STATUS_RESUBMISSION_INCOMING,\ @@ -17,6 +14,8 @@ from . import exceptions, helpers from .models import Submission, RefereeInvitation, Report, EICRecommendation, EditorialAssignment,\ iThenticateReport +from colleges.models import Fellowship +from journals.constants import SCIPOST_JOURNAL_PHYSICS_PROC from scipost.constants import SCIPOST_SUBJECT_AREAS from scipost.services import ArxivCaller from scipost.models import Contributor @@ -43,19 +42,30 @@ class SubmissionSearchForm(forms.Form): class SubmissionPoolFilterForm(forms.Form): - status = forms.ChoiceField(choices=((None, 'All statuses'),) + SUBMISSION_STATUS, - required=False) - editor_in_charge = forms.BooleanField(label='Show only Submissions for which I am editor in charge.', required=False) + status = forms.ChoiceField( + choices=((None, 'All submissions currently under evaluation'),) + SUBMISSION_STATUS, + required=False) + editor_in_charge = forms.BooleanField( + label='Show only Submissions for which I am editor in charge.', required=False) - def search(self, queryset, current_contributor=None): + def search(self, queryset, current_user): if self.cleaned_data.get('status'): # Do extra check on non-required field to never show errors on template - queryset = queryset.filter(status=self.cleaned_data['status']) + queryset = queryset.pool_full(current_user).filter(status=self.cleaned_data['status']) + else: + # If no specific status if requested, just return the Pool by default + queryset = queryset.pool(current_user) + + if self.cleaned_data.get('editor_in_charge') and hasattr(current_user, 'contributor'): + queryset = queryset.filter(editor_in_charge=current_user.contributor) - if self.cleaned_data.get('editor_in_charge') and current_contributor: - queryset = queryset.filter(editor_in_charge=current_contributor) + return queryset.order_by('-submission_date') - return queryset + def status_verbose(self): + try: + return dict(SUBMISSION_STATUS)[self.cleaned_data['status']] + except KeyError: + return '' ############################### @@ -222,6 +232,7 @@ class RequestSubmissionForm(SubmissionChecks, forms.ModelForm): 'is_resubmission', 'discipline', 'submitted_to_journal', + 'proceedings', 'submission_type', 'domain', 'subject_area', @@ -256,6 +267,19 @@ class RequestSubmissionForm(SubmissionChecks, forms.ModelForm): self.fields['list_of_changes'].widget.attrs.update({ 'placeholder': 'Give a point-by-point list of changes (will be viewable online)'}) + # Proceedings submission + qs = self.fields['proceedings'].queryset.open_for_submission() + self.fields['proceedings'].queryset = qs + self.fields['proceedings'].empty_label = None + if not qs.exists(): + # Open the proceedings Journal for submission + def filter_proceedings(item): + return item[0] != SCIPOST_JOURNAL_PHYSICS_PROC + + self.fields['submitted_to_journal'].choices = filter( + filter_proceedings, self.fields['submitted_to_journal'].choices) + del self.fields['proceedings'] + # Update placeholder for the other fields self.fields['arxiv_link'].widget.attrs.update({ 'placeholder': 'ex.: arxiv.org/abs/1234.56789v1'}) @@ -278,6 +302,10 @@ class RequestSubmissionForm(SubmissionChecks, forms.ModelForm): self.do_pre_checks(cleaned_data['arxiv_identifier_w_vn_nr']) self.arxiv_meets_regex(cleaned_data['arxiv_identifier_w_vn_nr'], cleaned_data['submitted_to_journal']) + + if self.cleaned_data['submitted_to_journal'] != SCIPOST_JOURNAL_PHYSICS_PROC: + del self.cleaned_data['proceedings'] + return cleaned_data def clean_author_list(self): @@ -327,11 +355,6 @@ class RequestSubmissionForm(SubmissionChecks, forms.ModelForm): @transaction.atomic def reassign_eic_and_admins(self, submission): - # Assign permissions - assign_perm('can_take_editorial_actions', submission.editor_in_charge.user, submission) - ed_admins = Group.objects.get(name='Editorial Administrators') - assign_perm('can_take_editorial_actions', ed_admins, submission) - # Assign editor assignment = EditorialAssignment( submission=submission, @@ -342,6 +365,18 @@ class RequestSubmissionForm(SubmissionChecks, forms.ModelForm): submission.save() return submission + def set_pool(self, submission): + qs = Fellowship.objects.active() + fellows = qs.regular().filter( + contributor__discipline=submission.discipline).return_active_for_submission(submission) + submission.fellows.set(fellows) + + if submission.proceedings: + # Add Guest Fellowships if the Submission is a Proceedings manuscript + guest_fellows = qs.guests().filter( + proceedings=submission.proceedings).return_active_for_submission(submission) + submission.fellows.add(*guest_fellows) + @transaction.atomic def save(self): """ @@ -368,6 +403,7 @@ class RequestSubmissionForm(SubmissionChecks, forms.ModelForm): submission = self.copy_and_save_data_from_resubmission(submission) submission = self.reassign_eic_and_admins(submission) submission.authors.add(self.requested_by.contributor) + self.set_pool(submission) return submission @@ -434,21 +470,37 @@ class SetRefereeingDeadlineForm(forms.Form): return self.cleaned_data.get('deadline') -class VotingEligibilityForm(forms.Form): +class VotingEligibilityForm(forms.ModelForm): + eligible_fellows = forms.ModelMultipleChoiceField( + queryset=Contributor.objects.none(), + widget=forms.CheckboxSelectMultiple({'checked': 'checked'}), + required=True, label='Eligible for voting') + + class Meta: + model = EICRecommendation + fields = () def __init__(self, *args, **kwargs): - discipline = kwargs.pop('discipline') - subject_area = kwargs.pop('subject_area') - super(VotingEligibilityForm, self).__init__(*args, **kwargs) - self.fields['eligible_Fellows'] = forms.ModelMultipleChoiceField( - queryset=Contributor.objects.filter( - user__groups__name__in=['Editorial College'], - user__contributor__discipline=discipline, - user__contributor__expertises__contains=[subject_area] - ).order_by('user__last_name'), - widget=forms.CheckboxSelectMultiple({'checked': 'checked'}), - required=True, label='Eligible for voting', - ) + super().__init__(*args, **kwargs) + # Do we need this discipline filter still with the new Pool construction??? + # -- JdW; Oct 20th, 2017 + self.fields['eligible_fellows'].queryset = Contributor.objects.filter( + fellowships__pool=self.instance.submission, + discipline=self.instance.submission.discipline, + expertises__contains=[self.instance.submission.subject_area] + ).order_by('user__last_name') + + def save(self, commit=True): + recommendation = self.instance + recommendation.eligible_to_vote = self.cleaned_data['eligible_fellows'] + submission = self.instance.submission + submission.status = 'put_to_EC_voting' + + if commit: + recommendation.save() + submission.save() + recommendation.voted_for.add(recommendation.submission.editor_in_charge) + return recommendation ############ @@ -616,12 +668,15 @@ class EditorialCommunicationForm(forms.Form): class EICRecommendationForm(forms.ModelForm): class Meta: model = EICRecommendation - fields = ['recommendation', - 'remarks_for_authors', 'requested_changes', - 'remarks_for_editorial_college'] + fields = [ + 'recommendation', + 'remarks_for_authors', + 'requested_changes', + 'remarks_for_editorial_college' + ] def __init__(self, *args, **kwargs): - super(EICRecommendationForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.fields['remarks_for_authors'].widget.attrs.update( {'placeholder': 'Your general remarks for the authors', 'rows': 10, 'cols': 100}) diff --git a/submissions/management/__init__.py b/submissions/management/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/submissions/management/commands/__init__.py b/submissions/management/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/submissions/management/commands/remind_fellows_to_submit_report.py b/submissions/management/commands/remind_fellows_to_submit_report.py new file mode 100644 index 0000000000000000000000000000000000000000..d7a7a6988cad29edf8318667627eecaaa38d59fd --- /dev/null +++ b/submissions/management/commands/remind_fellows_to_submit_report.py @@ -0,0 +1,13 @@ +from django.core.management.base import BaseCommand +# from django.contrib.auth.models import User + +from ...models import RefereeInvitation +from ...signals import notify_invitation_approaching_deadline, notify_invitation_overdue + + +class Command(BaseCommand): + def handle(self, *args, **options): + for invitation in RefereeInvitation.objects.approaching_deadline(): + notify_invitation_approaching_deadline(RefereeInvitation, invitation, False) + for invitation in RefereeInvitation.objects.overdue(): + notify_invitation_overdue(RefereeInvitation, invitation, False) diff --git a/submissions/managers.py b/submissions/managers.py index e3e15f6c51d24452110bbf3add0da166f5118642..c992faacfdd72f851cbf3377ecd56a348f49af06 100644 --- a/submissions/managers.py +++ b/submissions/managers.py @@ -12,7 +12,9 @@ from .constants import SUBMISSION_STATUS_OUT_OF_POOL, SUBMISSION_STATUS_PUBLICLY STATUS_REJECTED, STATUS_REJECTED_VISIBLE,\ STATUS_ACCEPTED, STATUS_RESUBMITTED, STATUS_RESUBMITTED_REJECTED_VISIBLE,\ EVENT_FOR_EIC, EVENT_GENERAL, EVENT_FOR_AUTHOR,\ - STATUS_UNASSIGNED, STATUS_ASSIGNMENT_FAILED, STATUS_WITHDRAWN + STATUS_UNASSIGNED, STATUS_ASSIGNMENT_FAILED, STATUS_WITHDRAWN,\ + STATUS_PUT_TO_EC_VOTING, STATUS_VOTING_IN_PREPARATION,\ + SUBMISSION_STATUS_VOTING_DEPRECATED, STATUS_REVISION_REQUESTED class SubmissionQuerySet(models.QuerySet): @@ -25,7 +27,7 @@ class SubmissionQuerySet(models.QuerySet): """ # This method used a double query, which is a consequence of the complex distinct() # filter combined with the PostGresQL engine. Without the double query, ordering - # on a specific field after filtering would be impossible. + # on a specific field after filtering seems impossible. ids = (queryset .order_by('arxiv_identifier_wo_vn_nr', '-arxiv_vn_nr') .distinct('arxiv_identifier_wo_vn_nr') @@ -44,26 +46,69 @@ class SubmissionQuerySet(models.QuerySet): except AttributeError: return self.none() - def get_pool(self, user): + def _pool(self, user): """ - Return subset of active and newest 'alive' submissions. + This filter creates 'the complete pool' for an user. This new-style pool does + explicitly not have the author filter anymore, but registered pools for every Submission. + + !!! IMPORTANT SECURITY NOTICE !!! + All permissions regarding Editorial College actions are implicitly taken care + of in this filter method! ALWAYS use this filter method in your Editorial College + related view/action. """ - return (self.user_filter(user) - .exclude(is_current=False) - .exclude(status__in=SUBMISSION_STATUS_OUT_OF_POOL) - .order_by('-submission_date')) + if not hasattr(user, 'contributor'): + return self.none() - def filter_editorial_page(self, user): + if user.has_perm('scipost.can_oversee_refereeing'): + # Editorial Administators do have permission to see all submissions + # without being one of the College Fellows. Therefore, use the 'old' author + # filter to still filter out their conflicts of interests. + return self.user_filter(user) + else: + qs = user.contributor.fellowships.active() + return self.filter(fellows__in=qs) + + def pool(self, user): + """ + Return the pool for a certain user: filtered to "in active referee phase". """ - Return Submissions currently 'alive' (being refereed, not published). + qs = self._pool(user) + qs = qs.exclude(is_current=False).exclude(status__in=SUBMISSION_STATUS_OUT_OF_POOL) + return qs - It is meant to allow opening and editing certain submissions that are officially - out of the submission cycle i.e. due to resubmission, but should still have the - possibility to be opened by the EIC. + def pool_editable(self, user): + """ + Return the editable pool for a certain user. + + This is similar to the regular pool, however it also contains submissions that are + hidden in the regular pool, but should still be able to be opened by for example + the Editor-in-charge. + """ + qs = self._pool(user) + qs = qs.exclude(status__in=SUBMISSION_HTTP404_ON_EDITORIAL_PAGE) + return qs + + def pool_full(self, user): + """ + Return the *FULL* pool for a certain user. + This makes sure the user can see all history of Submissions related to its Fellowship(s). + + Do not use this filter by default however, as this also contains Submissions + that are for example either rejected or accepted already and thus "inactive." + """ + qs = self._pool(user) + return qs + + def filter_for_eic(self, user): + """ + Return the set of Submissions the user is Editor-in-charge for or return the pool if + User is Editorial Administrator. """ - return (self.user_filter(user) - .exclude(status__in=SUBMISSION_HTTP404_ON_EDITORIAL_PAGE) - .order_by('-submission_date')) + qs = self._pool(user) + + if not user.has_perm('scipost.can_oversee_refereeing') and hasattr(user, 'contributor'): + qs = qs.filter(editor_in_charge=user.contributor) + return qs def prescreening(self): """ @@ -77,7 +122,8 @@ class SubmissionQuerySet(models.QuerySet): """ return (self.exclude(is_current=False) .exclude(status__in=SUBMISSION_STATUS_OUT_OF_POOL) - .exclude(status__in=[STATUS_UNASSIGNED, STATUS_ACCEPTED])) + .exclude(status__in=[STATUS_UNASSIGNED, STATUS_ACCEPTED, + STATUS_REVISION_REQUESTED])) def public(self): """ @@ -121,19 +167,18 @@ class SubmissionQuerySet(models.QuerySet): identifiers.append(sub.arxiv_identifier_wo_vn_nr) return self.filter(arxiv_identifier_wo_vn_nr__in=identifiers) - def accepted(self): return self.filter(status=STATUS_ACCEPTED) + def revision_requested(self): + return self.filter(status=STATUS_REVISION_REQUESTED) def published(self): return self.filter(status=STATUS_PUBLISHED) - def assignment_failed(self): return self.filter(status=STATUS_ASSIGNMENT_FAILED) - def rejected(self): return self._newest_version_only(self.filter(status__in=[STATUS_REJECTED, STATUS_REJECTED_VISIBLE])) @@ -141,7 +186,6 @@ class SubmissionQuerySet(models.QuerySet): def withdrawn(self): return self._newest_version_only(self.filter(status=STATUS_WITHDRAWN)) - def open_for_reporting(self): """ Return Submissions that have appriopriate status for reporting. @@ -203,7 +247,7 @@ class EditorialAssignmentQuerySet(models.QuerySet): return self.filter(accepted=None, deprecated=False) -class EICRecommendationManager(models.Manager): +class EICRecommendationQuerySet(models.QuerySet): def get_for_user_in_pool(self, user): """ -- DEPRECATED -- @@ -221,6 +265,8 @@ class EICRecommendationManager(models.Manager): def filter_for_user(self, user, **kwargs): """ + -- DEPRECATED -- + Return list of EICRecommendation's which are owned/assigned author through the related submission. """ @@ -229,6 +275,23 @@ class EICRecommendationManager(models.Manager): except AttributeError: return self.none() + def user_may_vote_on(self, user): + if not hasattr(user, 'contributor'): + return self.none() + + return (self.filter(eligible_to_vote=user.contributor) + .exclude(recommendation__in=[-1, -2]) + .exclude(voted_for=user.contributor) + .exclude(voted_against=user.contributor) + .exclude(voted_abstain=user.contributor) + .exclude(submission__status__in=SUBMISSION_STATUS_VOTING_DEPRECATED)) + + def put_to_voting(self): + return self.filter(submission__status=STATUS_PUT_TO_EC_VOTING) + + def voting_in_preparation(self): + return self.filter(submission__status=STATUS_VOTING_IN_PREPARATION) + class ReportQuerySet(models.QuerySet): def accepted(self): @@ -269,3 +332,17 @@ class RefereeInvitationQuerySet(models.QuerySet): def in_process(self): return self.accepted().filter(fulfilled=False) + + def approaching_deadline(self): + qs = self.in_process() + psuedo_deadline = datetime.datetime.now() + datetime.timedelta(days=2) + deadline = datetime.datetime.now() + qs = qs.filter(submission__reporting_deadline__lte=psuedo_deadline, + submission__reporting_deadline__gte=deadline) + return qs + + def overdue(self): + qs = self.in_process() + deadline = datetime.datetime.now() + qs = qs.filter(submission__reporting_deadline__lte=deadline) + return qs diff --git a/submissions/migrations/0077_auto_20171020_0900.py b/submissions/migrations/0077_auto_20171020_0900.py new file mode 100644 index 0000000000000000000000000000000000000000..26df6de6233dff92c7a78e96c63a55ecf981c55c --- /dev/null +++ b/submissions/migrations/0077_auto_20171020_0900.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-20 07:00 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('colleges', '0001_initial'), + ('proceedings', '0001_initial'), + ('submissions', '0076_auto_20170928_2024'), + ] + + operations = [ + migrations.AddField( + model_name='submission', + name='fellows', + field=models.ManyToManyField(blank=True, related_name='pool', to='colleges.Fellowship'), + ), + migrations.AddField( + model_name='submission', + name='proceedings', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='proceedings.Proceedings'), + ), + migrations.AlterField( + model_name='submission', + name='submitted_to_journal', + field=models.CharField(choices=[('SciPostPhys', 'SciPost Physics'), ('SciPostPhysLectNotes', 'SciPost Physics Lecture Notes'), ('SciPostPhysProc', 'SciPost Proceedings')], max_length=30, verbose_name='Journal to be submitted to'), + ), + ] diff --git a/submissions/migrations/0078_auto_20171020_1054.py b/submissions/migrations/0078_auto_20171020_1054.py new file mode 100644 index 0000000000000000000000000000000000000000..703f779660ac65d2c5a636484175960f8ac08134 --- /dev/null +++ b/submissions/migrations/0078_auto_20171020_1054.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-20 08:54 +from __future__ import unicode_literals + +from django.db import migrations +from django.db.models import Q + + +def fill_pools(apps, schema_editor): + Fellowship = apps.get_model('colleges', 'Fellowship') + Submission = apps.get_model('submissions', 'Submission') + for fellowship in Fellowship.objects.all(): + # Fellowship.objects.get_or_create(contributor=contributor) + contributor = fellowship.contributor + submissions = Submission.objects.exclude(authors=contributor).exclude( + Q(author_list__icontains=contributor.user.last_name), + ~Q(authors_false_claims=contributor)) + fellowship.pool.add(*submissions) + + +def return_empty(*args, **kwargs): + return + + +class Migration(migrations.Migration): + + dependencies = [ + ('colleges', '0002_auto_20171020_0931'), + ('submissions', '0077_auto_20171020_0900'), + ] + + operations = [ + migrations.RunPython(fill_pools, return_empty), + ] diff --git a/submissions/migrations/0079_auto_20171021_1456.py b/submissions/migrations/0079_auto_20171021_1456.py new file mode 100644 index 0000000000000000000000000000000000000000..a9f4d043d2750ff0b5afcbb95cedf99c14e98f73 --- /dev/null +++ b/submissions/migrations/0079_auto_20171021_1456.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-21 12:56 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0078_auto_20171020_1054'), + ] + + operations = [ + migrations.AlterModelOptions( + name='report', + options={'ordering': ['-date_submitted']}, + ), + migrations.AlterModelOptions( + name='submission', + options={}, + ), + ] diff --git a/submissions/mixins.py b/submissions/mixins.py index 4f84bb7065c9863fe725e231cb6cb02eb08a1a48..467ceab478632e655f21000bbd08b7499f0f099d 100644 --- a/submissions/mixins.py +++ b/submissions/mixins.py @@ -69,8 +69,8 @@ class SubmissionAdminViewMixin(FriendlyPermissionMixin, SubmissionFormViewMixin) """ qs = super().get_queryset() if self.pool: - return qs.get_pool(self.request.user) - return qs.filter_editorial_page(self.request.user) + return qs.pool(self.request.user) + return qs.filter_for_eic(self.request.user) def get_object(self): """ diff --git a/submissions/models.py b/submissions/models.py index e5fb47eb4424289c7bd3dd6f6d1f1d629694db8a..d03e96c2888af6a9592e40ed7c6bc30106b5b2d2 100644 --- a/submissions/models.py +++ b/submissions/models.py @@ -15,7 +15,7 @@ from .constants import ASSIGNMENT_REFUSAL_REASONS, ASSIGNMENT_NULLBOOL,\ REPORT_STATUSES, STATUS_UNVETTED, SUBMISSION_EIC_RECOMMENDATION_REQUIRED,\ SUBMISSION_CYCLES, CYCLE_DEFAULT, CYCLE_SHORT, CYCLE_DIRECT_REC,\ EVENT_GENERAL, EVENT_TYPES, EVENT_FOR_AUTHOR, EVENT_FOR_EIC -from .managers import SubmissionQuerySet, EditorialAssignmentQuerySet, EICRecommendationManager,\ +from .managers import SubmissionQuerySet, EditorialAssignmentQuerySet, EICRecommendationQuerySet,\ ReportQuerySet, SubmissionEventQuerySet, RefereeInvitationQuerySet from .utils import ShortSubmissionCycle, DirectRecommendationSubmissionCycle,\ GeneralSubmissionCycle @@ -24,7 +24,6 @@ from comments.models import Comment from scipost.behaviors import TimeStampedModel from scipost.constants import TITLE_CHOICES from scipost.fields import ChoiceArrayField -from scipost.models import Contributor from scipost.constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS from journals.constants import SCIPOST_JOURNALS_SUBMIT, SCIPOST_JOURNALS_DOMAINS from journals.models import Publication @@ -58,6 +57,8 @@ class Submission(models.Model): status = models.CharField(max_length=30, choices=SUBMISSION_STATUS, default=STATUS_UNASSIGNED) refereeing_cycle = models.CharField(max_length=30, choices=SUBMISSION_CYCLES, default=CYCLE_DEFAULT) + fellows = models.ManyToManyField('colleges.Fellowship', blank=True, + related_name='pool') subject_area = models.CharField(max_length=10, choices=SCIPOST_SUBJECT_AREAS, verbose_name='Primary subject area', default='Phys:QP') submission_type = models.CharField(max_length=10, choices=SUBMISSION_TYPE, @@ -68,6 +69,8 @@ class Submission(models.Model): # Replace this by foreignkey? submitted_to_journal = models.CharField(max_length=30, choices=SCIPOST_JOURNALS_SUBMIT, verbose_name="Journal to be submitted to") + proceedings = models.ForeignKey('proceedings.Proceedings', null=True, blank=True, + related_name='submissions') title = models.CharField(max_length=300) # Authors which have been mapped to contributors: @@ -104,11 +107,6 @@ class Submission(models.Model): objects = SubmissionQuerySet.as_manager() - class Meta: - permissions = ( - ('can_take_editorial_actions', 'Can take editorial actions'), - ) - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._update_cycle() @@ -167,21 +165,37 @@ class Submission(models.Model): def get_absolute_url(self): return reverse('submissions:submission', args=[self.arxiv_identifier_w_vn_nr]) + @property + def notification_name(self): + return self.arxiv_identifier_w_vn_nr + + @property def eic_recommendation_required(self): - return self.status not in SUBMISSION_EIC_RECOMMENDATION_REQUIRED + return self.status in SUBMISSION_EIC_RECOMMENDATION_REQUIRED @property def reporting_deadline_has_passed(self): return timezone.now() > self.reporting_deadline + @property + def original_submission_date(self): + return Submission.objects.filter( + arxiv_identifier_wo_vn_nr=self.arxiv_identifier_wo_vn_nr).first().submission_date + @cached_property def other_versions(self): + """ + Return all other versions of the Submission that are publicly accessible. + """ return Submission.objects.public().filter( arxiv_identifier_wo_vn_nr=self.arxiv_identifier_wo_vn_nr ).exclude(pk=self.id).order_by('-arxiv_vn_nr') @cached_property def other_versions_pool(self): + """ + Return all other versions of the Submission. + """ return Submission.objects.filter( arxiv_identifier_wo_vn_nr=self.arxiv_identifier_wo_vn_nr ).exclude(pk=self.id).order_by('-arxiv_vn_nr') @@ -285,6 +299,10 @@ class EditorialAssignment(SubmissionRelatedObjectMixin, models.Model): def get_absolute_url(self): return reverse('submissions:assignment_request', args=(self.id,)) + @property + def notification_name(self): + return self.submission.arxiv_identifier_w_vn_nr + class RefereeInvitation(SubmissionRelatedObjectMixin, models.Model): submission = models.ForeignKey('submissions.Submission', on_delete=models.CASCADE, @@ -316,12 +334,19 @@ class RefereeInvitation(SubmissionRelatedObjectMixin, models.Model): self.submission.title[:30] + ' by ' + self.submission.author_list[:30] + ', invited on ' + self.date_invited.strftime('%Y-%m-%d')) + def get_absolute_url(self): + return reverse('submissions:accept_or_decline_ref_invitations', args=(self.id,)) + @property def referee_str(self): if self.referee: return str(self.referee) return self.last_name + ', ' + self.first_name + @property + def notification_name(self): + return self.submission.arxiv_identifier_w_vn_nr + def reset_content(self): self.nr_reminders = 0 self.date_last_reminded = None @@ -409,9 +434,6 @@ class Report(SubmissionRelatedObjectMixin, models.Model): unique_together = ('submission', 'report_nr') default_related_name = 'reports' ordering = ['-date_submitted'] - permissions = ( - ('can_vet_submitted_reports', 'Can vet submitted Reports'), - ) def __str__(self): return (self.author.user.first_name + ' ' + self.author.user.last_name + ' on ' + @@ -430,6 +452,10 @@ class Report(SubmissionRelatedObjectMixin, models.Model): def get_absolute_url(self): return self.submission.get_absolute_url() + '#report_' + str(self.report_nr) + @property + def notification_name(self): + return self.submission.arxiv_identifier_w_vn_nr + @property def doi_string(self): if self.doi_label: @@ -520,7 +546,7 @@ class EICRecommendation(SubmissionRelatedObjectMixin, models.Model): related_name='voted_abstain') voting_deadline = models.DateTimeField('date submitted', default=timezone.now) - objects = EICRecommendationManager() + objects = EICRecommendationQuerySet.as_manager() def __str__(self): return (self.submission.title[:20] + ' by ' + self.submission.author_list[:30] + @@ -530,6 +556,10 @@ class EICRecommendation(SubmissionRelatedObjectMixin, models.Model): # TODO: Fix this weird redirect, but it's neccesary for the notifications to have one. return self.submission.get_absolute_url() + @property + def notification_name(self): + return self.submission.arxiv_identifier_w_vn_nr + @property def nr_for(self): return self.voted_for.count() diff --git a/submissions/signals.py b/submissions/signals.py index d14134ba0d4753b025f251d8f2a8ca04cf406460..be833cb833d351eab88e1d0d384e638ef4992762 100644 --- a/submissions/signals.py +++ b/submissions/signals.py @@ -1,5 +1,7 @@ from django.contrib.auth.models import User, Group +from notifications.constants import NOTIFICATION_REFEREE_DEADLINE, NOTIFICATION_REFEREE_OVERDUE +from notifications.models import Notification from notifications.signals import notify @@ -49,3 +51,37 @@ def notify_new_referee_invitation(sender, instance, created, **kwargs): notify.send(sender=sender, recipient=instance.referee.user, actor=instance.submission.editor_in_charge, verb=' would like to invite you to referee a Submission.', target=instance) + + +def notify_invitation_approaching_deadline(sender, instance, created, **kwargs): + """ + Notify Referee its unfinished duty is approaching the deadline. + """ + if instance.referee: + notifications = Notification.objects.filter( + recipient=instance.referee.user, internal_type=NOTIFICATION_REFEREE_DEADLINE).unread() + if not notifications.exists(): + # User doesn't already have a notification to remind him. + administration = Group.objects.get(name='Editorial Administrators') + notify.send(sender=sender, recipient=instance.referee.user, + actor=administration, + verb=(' would like to remind you that your Refereeing Task is ' + 'approaching its deadline, please submit your Report'), + target=instance.submission, type=NOTIFICATION_REFEREE_DEADLINE) + + +def notify_invitation_overdue(sender, instance, created, **kwargs): + """ + Notify Referee its unfinished duty is overdue. + """ + if instance.referee: + notifications = Notification.objects.filter( + recipient=instance.referee.user, internal_type=NOTIFICATION_REFEREE_OVERDUE).unread() + if not notifications.exists(): + # User doesn't already have a notification to remind him. + administration = Group.objects.get(name='Editorial Administrators') + notify.send(sender=sender, recipient=instance.referee.user, + actor=administration, + verb=(' would like to remind you that your Refereeing Task is overdue, ' + 'please submit your Report'), + target=instance.submission, type=NOTIFICATION_REFEREE_OVERDUE) diff --git a/submissions/templates/partials/submissions/pool/submission_details.html b/submissions/templates/partials/submissions/pool/submission_details.html index 032739c452da25847ed4887f2cc669d4f9af6e3c..28140d19272a34b017e5be0a57e12908c10af62e 100644 --- a/submissions/templates/partials/submissions/pool/submission_details.html +++ b/submissions/templates/partials/submissions/pool/submission_details.html @@ -1,15 +1,20 @@ -{% load guardian_tags %} {% load scipost_extras %} {% load submissions_extras %} {% load user_groups %} -{% get_obj_perms request.user for submission as "sub_perms" %} -{% is_edcol_admin request.user as is_ECAdmin %} +{% is_editor_in_charge request.user submission as is_editor_in_charge %} +{% is_edcol_admin request.user as is_editorial_admin %} -<div class="card submission-detail"> - {% include 'submissions/_submission_card_fellow_content.html' with submission=submission %} - <div class="card-body"> +<hr> +<div class="bg-white submission-detail mt-1"> + <div class="text-center mb-1"> + <a href="javascript:;" class="d-inline-block px-3" data-toggle="hide" data-target="#container_{{ submission.id }}">Hide details <i class="fa fa-angle-up text-blue" aria-hidden="true"></i></a> + </div> + + {% include 'submissions/_submission_card_fellow_content.html' with submission=submission hide_title=1 cardblock_class='px-0 pt-0' %} + + <div> <h3>Remarks on this submission:</h3> {% if remark_form %} {% include 'submissions/_remark_add_form.html' with submission=submission form=remark_form auto_show=1 %} @@ -23,14 +28,15 @@ {% endfor %} </ul> - {% if "can_take_editorial_actions" in sub_perms or is_ECAdmin %} + {% if is_editor_in_charge or is_editorial_admin %} + <br> {% include 'submissions/_required_actions_block.html' with submission=submission %} <h4> <a href="{% url 'submissions:editorial_page' submission.arxiv_identifier_w_vn_nr %}">Go to this Submission's Editorial Page</a> </h4> {% endif %} - {% if is_ECAdmin %} + {% if is_editorial_admin %} <h3>Editorial Administration</h3> <ul class="pl-4 mb-3"> {# EIC Assignments #} @@ -86,4 +92,8 @@ </div> {% endif %} </div> + + <div class="text-center mb-1"> + <a href="javascript:;" class="d-inline-block px-3" data-toggle="hide" data-target="#container_{{ submission.id }}">Hide details <i class="fa fa-angle-up text-blue" aria-hidden="true"></i></a> + </div> </div> diff --git a/submissions/templates/partials/submissions/pool/submission_li.html b/submissions/templates/partials/submissions/pool/submission_li.html index f56f5df73002123e71f31d908920f7cc9d0bfa47..fdd9854706c11655521eb30a85470216a5f4aeef 100644 --- a/submissions/templates/partials/submissions/pool/submission_li.html +++ b/submissions/templates/partials/submissions/pool/submission_li.html @@ -1,37 +1,52 @@ -<div class="row pool-item mb-0"> - <div class="icons{% if is_current %} text-info{% endif %}"> - {% include 'partials/submissions/pool/submission_tooltip.html' with submission=submission %} +<div class="icons"> + {% include 'partials/submissions/pool/submission_tooltip.html' with submission=submission %} - {% if submission.status == 'unassigned' %} - <i class="fa fa-exclamation mt-1 px-1 text-danger" data-toggle="tooltip" data-html="true" title="You can volunteer to become Editor-in-charge"></i> - {% endif %} - </div> - <div class="item col-auto"> - <p class="mb-1"> - <a href="{% url 'submissions:pool' submission.arxiv_identifier_w_vn_nr %}">{{ submission.title }}</a><br> - <em>by {{ submission.author_list }}</em> - </p> + {% if submission.status == 'unassigned' %} + <i class="fa fa-exclamation mt-1 px-1 text-danger" data-toggle="tooltip" data-html="true" title="You can volunteer to become Editor-in-charge"></i> + {% endif %} +</div> +<div class="pool-item"> + <p class="mb-1"> + <a href="{% url 'submissions:pool' submission.arxiv_identifier_w_vn_nr %}">{{ submission.title }}</a><br> + <em>by {{ submission.author_list }}</em> + </p> - <p class="card-text mb-3"> - <a href="{% url 'submissions:pool' submission.arxiv_identifier_w_vn_nr %}" data-toggle="dynamic" data-target="#details">See details</a> + <div class="row mb-0"> + <div class="col-3"> + <small class="text-muted">Editor-in-charge</small> + <br> + {% if submission.status == 'unassigned' %} + <span class="card-text text-danger">You can volunteer to become Editor-in-charge by <a href="{% url 'submissions:volunteer_as_EIC' submission.arxiv_identifier_w_vn_nr %}">clicking here</a>.</span> + {% elif submission.editor_in_charge == request.user.contributor %} + <strong>You are Editor-in-charge</strong> + {% else %} + {{ submission.editor_in_charge }} + {% endif %} + </div> + <div class="col-2"> + <small class="text-muted">Actions</small> + <br> + <a href="{% url 'submissions:pool' submission.arxiv_identifier_w_vn_nr %}" data-toggle="dynamic" data-target="#container_{{ submission.id }}">See details</a> {% if submission.editor_in_charge == request.user.contributor %} - · <a href="{% url 'submissions:editorial_page' submission.arxiv_identifier_w_vn_nr %}">Go directly to editorial page</a> + · <a href="{% url 'submissions:editorial_page' submission.arxiv_identifier_w_vn_nr %}">Editorial page</a> {% endif %} - </p> - - {% if submission.cycle.has_required_actions and submission.cycle.get_required_actions %} - <p class="card-text bg-danger text-white p-1 px-2">This Submission contains required actions, <a href="{% url 'submissions:pool' submission.arxiv_identifier_w_vn_nr %}" class="text-white" data-toggle="dynamic" data-target="#details">click to see details.</a> {% include 'partials/submissions/pool/required_actions_tooltip.html' with submission=submission classes='text-white' %}</p> - {% endif %} - - {% if submission.status == 'unassigned' %} - <p class="card-text text-danger">You can volunteer to become Editor-in-charge by <a href="{% url 'submissions:volunteer_as_EIC' submission.arxiv_identifier_w_vn_nr %}">clicking here</a>.</p> - {% elif submission.editor_in_charge == request.user.contributor %} - <p class="card-text"><strong>You are Editor-in-charge</strong></p> - {% else %} - <p class="card-text">Editor-in-charge: <em>{{ submission.editor_in_charge }}</em></p> - {% endif %} + </div> + <div class="col-2"> + <small class="text-muted">Original Submission date</small> + <br> + {{ submission.original_submission_date }} + </div> + <div class="col-5"> + <small class="text-muted">Status</small> + <br> + <span class="label label-sm label-secondary">{{ submission.get_status_display }}</span> + </div> + </div> - <p class="label label-{% if submission.status == 'unassigned' %}outline-danger{% else %}secondary{% endif %} label-sm">{{ submission.get_status_display }}</p> + {% if submission.cycle.has_required_actions and submission.cycle.get_required_actions %} + <div class="card-text bg-danger text-white mt-1 py-1 px-2"> + This Submission contains required actions, <a href="{% url 'submissions:pool' submission.arxiv_identifier_w_vn_nr %}" class="text-white" data-toggle="dynamic" data-target="#container_{{ submission.id }}">click to see details.</a> {% include 'partials/submissions/pool/required_actions_tooltip.html' with submission=submission classes='text-white' %} + </div> + {% endif %} - </div> </div> diff --git a/submissions/templates/partials/submissions/pool/submissions_list.html b/submissions/templates/partials/submissions/pool/submissions_list.html new file mode 100644 index 0000000000000000000000000000000000000000..9ea81ede0c4b34296283b7ed25bfb315faf178dc --- /dev/null +++ b/submissions/templates/partials/submissions/pool/submissions_list.html @@ -0,0 +1,22 @@ +<ul class="list-unstyled pool-list mt-2" data-target="active-list"> + <!-- Submissions list --> + {% for sub in submissions %} + <li class="submission py-2{% if sub == submission %} active{% endif %}" id="submission_{{ submission.id }}"> + {% if sub == submission %} + {% include 'partials/submissions/pool/submission_li.html' with submission=sub is_current=1 %} + {% else %} + {% include 'partials/submissions/pool/submission_li.html' with submission=sub is_current=0 %} + {% endif %} + + <div class="loading-container" id="container_{{ sub.id }}"> + {% if submission and submission == sub %} + {% include 'partials/submissions/pool/submission_details.html' with submission=submission remark_form=remark_form is_ECAdmin=is_ECAdmin user=request.user %} + {% endif %} + </div> + </li> + {% empty %} + <li> + <h3 class="text-center"><i class="fa fa-question fa-2x"></i><br>No Submissions found.</h3> + </li> + {% endfor %} +</ul> diff --git a/submissions/templates/submissions/_submission_card_base.html b/submissions/templates/submissions/_submission_card_base.html index fa4d0d8c90afdca722169f35e2d1865095471efc..a9eb8cdba588f7f64360ad172ae0b56e45b01499 100644 --- a/submissions/templates/submissions/_submission_card_base.html +++ b/submissions/templates/submissions/_submission_card_base.html @@ -1,8 +1,11 @@ -<div class="card-body {% block cardblock_class_block %}{% endblock %}"> - <h5 class="pb-0">{{submission.get_subject_area_display}}</h5> - <h3 class="card-title {% block title_class_block %}{% endblock %}"> - <a href="{{submission.get_absolute_url}}">{{submission.title}}</a> - </h3> +<div class="card-body {% block cardblock_class_block %}{% endblock %} {{ cardblock_class }}"> + + <div class="submission_title"> + <h5 class="pb-0">{{submission.get_subject_area_display}}</h5> + <h3 class="card-title {% block title_class_block %}{% endblock %}"> + <a href="{{submission.get_absolute_url}}">{{submission.title}}</a> + </h3> + </div> {% block card_block_footer %}{% endblock %} </div> diff --git a/submissions/templates/submissions/_submission_card_fellow_content.html b/submissions/templates/submissions/_submission_card_fellow_content.html index 49c7e881e218d6a7ee18075fcb771ff665ebb658..f35e20fb89d77679308004e7e2845846dc3481df 100644 --- a/submissions/templates/submissions/_submission_card_fellow_content.html +++ b/submissions/templates/submissions/_submission_card_fellow_content.html @@ -1,7 +1,8 @@ {% extends 'submissions/_submission_card_base.html' %} {% block card_block_footer %} - <p class="card-text mb-3">by {{submission.author_list}}</p> + <p class="card-text mb-3 author_list">by {{submission.author_list}}</p> + <table class="text-muted w-100 mb-1"> <tr> <td style="min-width: 40%;">Version</td> @@ -31,7 +32,7 @@ {% elif perms.scipost.can_assign_submissions %} <a href="{% url 'submissions:assign_submission' submission.arxiv_identifier_w_vn_nr %}">Send a new assignment request</a> {% else %} - - + <strong class="text-danger">You can volunteer to become Editor-in-charge by <a href="{% url 'submissions:volunteer_as_EIC' submission.arxiv_identifier_w_vn_nr %}">clicking here</a>.</strong> {% endif %} </td> </tr> @@ -78,5 +79,12 @@ </tr> {% endif %} + {% if perms.scipost.can_manage_college_composition %} + <tr> + <td>Pool size</td> + <td><a href="{% url 'colleges:submission' submission.arxiv_identifier_w_vn_nr %}">{{ submission.fellows.count }} Fellowships</a></td> + </tr> + {% endif %} + </table> {% endblock %} diff --git a/submissions/templates/submissions/_submission_card_in_pool.html b/submissions/templates/submissions/_submission_card_in_pool.html index ad484a07864a5b1a45ee339273c28547a87e8cb3..d75435ceac00ea15807a6e43641fbe1e402986e8 100644 --- a/submissions/templates/submissions/_submission_card_in_pool.html +++ b/submissions/templates/submissions/_submission_card_in_pool.html @@ -1,9 +1,12 @@ {% load guardian_tags %} {% load scipost_extras %} {% load submissions_extras %} +{% load user_groups %} +{% is_editor_in_charge request.user submission as is_editor_in_charge %} +{% is_edcol_admin request.user as is_editorial_admin %} -{% include 'submissions/_submission_card_fellow_content.html' with submission=submission %} +{% include 'submissions/_submission_card_fellow_content.html' with submission=submission hide_title=1 %} <div class="card-body"> {% if submission.remarks.all %} @@ -19,8 +22,7 @@ {% include 'submissions/_remark_add_form.html' with submission=submission form=remark_form %} {% endif %} - {% get_obj_perms request.user for submission as "sub_perms" %} - {% if "can_take_editorial_actions" in sub_perms or is_ECAdmin %} + {% if is_editor_in_charge or is_editorial_admin %} {% include 'submissions/_required_actions_block.html' with submission=submission %} <h4> <a href="{% url 'submissions:editorial_page' submission.arxiv_identifier_w_vn_nr %}">Go to this Submission's Editorial Page</a> @@ -45,7 +47,7 @@ {% endif %} {% endif %} - {% if is_ECAdmin %} + {% if is_editorial_admin %} <h4> <a href="{% url 'submissions:communication' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr comtype='StoE' %}">Send a communication to the Editor-in-charge</a> </h4> diff --git a/submissions/templates/submissions/accept_or_decline_ref_invitation_ack.html b/submissions/templates/submissions/accept_or_decline_ref_invitation_ack.html deleted file mode 100644 index 3e47b51a043e1f5786e576362a63958d6c054733..0000000000000000000000000000000000000000 --- a/submissions/templates/submissions/accept_or_decline_ref_invitation_ack.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends 'scipost/base.html' %} - -{% block pagetitle %}: accept or decline refereeing invitation (ack){% endblock pagetitle %} - -{% block content %} - -{% if invitation.accepted == True %} - <h2>Thank you for agreeing to referee this Submission.</h2> - <p>When you are ready, please go to the <a href="{% url 'submissions:submission' arxiv_identifier_w_vn_nr=invitation.submission.arxiv_identifier_w_vn_nr %}">Submission's page</a> to submit your Report.</p> -{% else %} - <h1>You have declined to contribute a Report.</h1> - <p>Nonetheless, we thank you very much for considering this refereeing invitation.</p> -{% endif %} - -{% endblock content %} diff --git a/submissions/templates/submissions/accept_or_decline_ref_invitations.html b/submissions/templates/submissions/accept_or_decline_ref_invitations.html index 932ecaa432168d1e5e9369b07dd5d572c23e56fe..b20a59fba2ad3223205d1952dff2fc7a0e94df35 100644 --- a/submissions/templates/submissions/accept_or_decline_ref_invitations.html +++ b/submissions/templates/submissions/accept_or_decline_ref_invitations.html @@ -1,14 +1,19 @@ -{% extends 'scipost/base.html' %} +{% extends 'scipost/_personal_page_base.html' %} {% block pagetitle %}: accept or decline refereeing invitations{% endblock pagetitle %} {% load bootstrap %} + +{% block breadcrumb_items %} + {{ block.super }} + <span class="breadcrumb-item">Accept or decline refereeing invitations</span> +{% endblock %} + {% block content %} <script> $(document).ready(function(){ - $('[name="accept"]').on('change', function() { if($('[name="accept"]:checked').val() == 'False') { $('#id_refusal_reason').parents('.form-group').show(); @@ -21,7 +26,7 @@ $(document).ready(function(){ </script> -{% if not invitation_to_consider %} +{% if not invitation %} <div class="row"> <div class="col-12"> <h1>There are no Refereeing Invitations for you to consider.</h1> @@ -31,18 +36,18 @@ $(document).ready(function(){ <div class="row"> <div class="col-12"> <h1 class="highlight">SciPost Submission which you are asked to Referee (see below to accept/decline):</h1> - {% include 'submissions/_submission_summary.html' with submission=invitation_to_consider.submission %} + {% include 'submissions/_submission_summary.html' with submission=invitation.submission %} </div> </div> <div class="row"> <div class="col-12"> <h2 class="highlight">Accept or Decline this Refereeing Invitation</h2> <h3>Please let us know if you can provide us with a Report for this Submission:</h3> - <p class="text-muted">We will expect your report by <b>{{invitation_to_consider.submission.reporting_deadline}}</b></p> - <form action="{% url 'submissions:accept_or_decline_ref_invitation_ack' invitation_id=invitation_to_consider.id %}" method="post"> + <p>We will expect your report by <b>{{invitation.submission.reporting_deadline}}</b></p> + <form action="{% url 'submissions:accept_or_decline_ref_invitations' invitation_id=invitation.id %}" method="post"> {% csrf_token %} - {{ form|bootstrap }} - <input type="submit" class="btn btn-secondary" value="Submit" /> + {{ form|bootstrap:'4,8' }} + <input type="submit" class="btn btn-primary" value="Submit" /> </form> </div> </div> diff --git a/submissions/templates/submissions/communication.html b/submissions/templates/submissions/communication.html index 98a2ce31673380e5f57dfbca885036e2146b6dd6..fa7fcf1a33412b4defc1a730ad2135b989f4ab47 100644 --- a/submissions/templates/submissions/communication.html +++ b/submissions/templates/submissions/communication.html @@ -14,48 +14,40 @@ {% block content %} -{% if errormessage %} - <div class="row"> - <div class="col-12"> - <p>{{ errormessage }}</p> - </div> - </div> -{% else %} - <div class="row"> - <div class="col-12"> - <h1 class="highlight">Send a Communication</h1> - {% if comtype == 'EtoA' %} - <p>to the submitting Author of Submission</p> - {% elif comtype == 'AtoE' or comtype == 'RtoE' or comtype == 'StoE' %} - <h3>to the Editor-in-charge of Submission</h3> - {% elif comtype == 'EtoR' %} - <p>to Referee of Submission</p> - {% elif comtype == 'EtoS' %} - <p>to SciPost Editorial Administrators</p> - {% endif %} - <div class="card"> - {% include 'submissions/_submission_card_content.html' with submission=submission %} - </div> +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Send a Communication</h1> + {% if comtype == 'EtoA' %} + <p>to the submitting Author of Submission</p> + {% elif comtype == 'AtoE' or comtype == 'RtoE' or comtype == 'StoE' %} + <h3>to the Editor-in-charge of Submission</h3> + {% elif comtype == 'EtoR' %} + <p>to Referee of Submission</p> + {% elif comtype == 'EtoS' %} + <p>to SciPost Editorial Administrators</p> + {% endif %} + + <div class="card"> + {% include 'submissions/_submission_card_content.html' with submission=submission %} </div> </div> - - <div class="row"> - <div class="col-12"> - {% if referee_id %} - <form action="{% url 'submissions:communication' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr comtype=comtype referee_id=referee_id %}" method="post"> - {% csrf_token %} - {{ form|bootstrap:'0,12' }} - <input class="btn btn-secondary" type="submit" value="Send communication"/> - </form> - {% else %} - <form action="{% url 'submissions:communication' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr comtype=comtype %}" method="post"> - {% csrf_token %} - {{ form|bootstrap:'0,12' }} - <input class="btn btn-secondary" type="submit" value="Send communication"/> - </form> - {% endif %} - +</div> + +<div class="row"> + <div class="col-12"> + {% if referee_id %} + <form action="{% url 'submissions:communication' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr comtype=comtype referee_id=referee_id %}" method="post"> + {% csrf_token %} + {{ form|bootstrap:'0,12' }} + <input class="btn btn-secondary" type="submit" value="Send communication"/> + </form> + {% else %} + <form action="{% url 'submissions:communication' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr comtype=comtype %}" method="post"> + {% csrf_token %} + {{ form|bootstrap:'0,12' }} + <input class="btn btn-secondary" type="submit" value="Send communication"/> + </form> {% endif %} </div> </div> diff --git a/submissions/templates/submissions/editorial_page.html b/submissions/templates/submissions/editorial_page.html index 6d29d4075433e05a154953719b270919be79b224..770ec03c969d11a3c0e9f4999962ba5da8203300 100644 --- a/submissions/templates/submissions/editorial_page.html +++ b/submissions/templates/submissions/editorial_page.html @@ -4,6 +4,7 @@ {% load scipost_extras %} {% load bootstrap %} +{% load user_groups %} {% block breadcrumb_items %} {{block.super}} @@ -13,6 +14,8 @@ {% block content %} +{% is_edcol_admin request.user as is_editorial_admin %} + <h2>Editorial Page for Submission</h2> <h1 class="text-primary">{{submission.title}}</h1> <h3>by {{submission.author_list}}</h3> @@ -211,11 +214,16 @@ <li><a href="{% url 'submissions:close_refereeing_round' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}">Close the refereeing round</a> (deactivates submission of new Reports and Comments)</li> {% endif %} {% endif %} - <li> - <a href="{% url 'submissions:eic_recommendation' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}">Formulate an Editorial Recommendation</a> - <p>If you recommend revisions, this will be communicated directly to the Authors, who will be asked to resubmit. - <br/>If you recommend acceptance or rejection, this will be put to the Editorial College for ratification.</p> - </li> + {% if submission.eic_recommendation_required %} + <li> + <a href="{% url 'submissions:eic_recommendation' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}">Formulate an Editorial Recommendation</a> + <p> + If you recommend revisions, this will be communicated directly to the Authors, who will be asked to resubmit. + <br> + If you recommend acceptance or rejection, this will be put to the Editorial College for ratification. + </p> + </li> + {% endif %} </ul> </div> </div> @@ -223,8 +231,13 @@ <h2 class="mt-3">Communications</h2> <ul> - <li><a href="{% url 'submissions:communication' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr comtype='EtoA' %}">Draft and send a communication with the submitting Author</a></li> - <li><a href="{% url 'submissions:communication' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr comtype='EtoS' %}">Draft and send a communication with SciPost Editorial Administration</a></li> + {% if submission.editor_in_charge == request.user.contributor %} + <li><a href="{% url 'submissions:communication' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr comtype='EtoA' %}">Draft and send a communication with the submitting Author</a></li> + <li><a href="{% url 'submissions:communication' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr comtype='EtoS' %}">Draft and send a communication with SciPost Editorial Administration</a></li> + {% endif %} + {% if is_editorial_admin %} + <li><a href="{% url 'submissions:communication' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr comtype='StoE' %}">Draft and send a communication as Editorial Administrator to the Editor-in-charge</a></li> + {% endif %} </ul> <div class="row"> diff --git a/submissions/templates/submissions/eic_recommendation.html b/submissions/templates/submissions/eic_recommendation.html index 08a04e9e5ed04323ade1b43941778e8bfc24e8d1..180b5c17bfc3d82c593b3c6e58764177af43864a 100644 --- a/submissions/templates/submissions/eic_recommendation.html +++ b/submissions/templates/submissions/eic_recommendation.html @@ -55,6 +55,24 @@ </div> </div> +{% if submission.editor_in_charge != request.user.contributor %} + <div class="row"> + <div class="col-12"> + <div class="card border-danger"> + <div class="card-body d-flex flex-row"> + <div class="p-2"> + <i class="fa fa-2x fa-exclamation-triangle text-warning" aria-hidden="true"></i> + </div> + <div class="px-2"> + You are not assigned as Editor in charge. However, you can formulate an Editorial Recommendation because you are Editorial Administrator. <strong>This Editorial Recommendation will still be signed by the Editor-in-charge.</strong> + </div> + + </div> + </div> + </div> + </div> +{% endif %} + <div class="row"> <div class="col-12"> <form action="{% url 'submissions:eic_recommendation' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}" method="post"> diff --git a/submissions/templates/submissions/new_submission.html b/submissions/templates/submissions/new_submission.html index 8e278f366204b9c560033fe9b80da9243c68f2d7..0a12296251de12a1a80c8b11b4d396942437f47a 100644 --- a/submissions/templates/submissions/new_submission.html +++ b/submissions/templates/submissions/new_submission.html @@ -10,12 +10,15 @@ $(document).ready(function(){ $('select#id_submitted_to_journal').on('change', function (){ var selection = $(this).val(); + $("#id_proceedings, #id_submission_type").parents('.form-group').hide() + switch(selection){ case "SciPostPhys": $("#id_submission_type").parents('.form-group').show() break; - default: - $("#id_submission_type").parents('.form-group').hide() + case "SciPostPhysProc": + $("#id_proceedings").parents('.form-group').show() + break; } }).trigger('change'); }); diff --git a/submissions/templates/submissions/pool.html b/submissions/templates/submissions/pool.html index 632bb4fe8b832e60ace7193cee4052cfb862c128..af4c97ff48b010362327f1a1b43852224d1f6704 100644 --- a/submissions/templates/submissions/pool.html +++ b/submissions/templates/submissions/pool.html @@ -21,7 +21,7 @@ <div class="col-lg-8"> {% if is_ECAdmin %} - {% if recommendations_undergoing_voting %} + {% if recommendations.put_to_voting.exists %} <div class="row"> <div class="col-12"> <h3 class="highlight mt-0">Administrative actions on recommendations undergoing voting:</h3> @@ -38,7 +38,7 @@ </div> <div class="row"> - {% for rec in recommendations_undergoing_voting %} + {% for rec in recommendations.put_to_voting %} {% if not forloop.first %} <hr> {% endif %} @@ -116,14 +116,14 @@ <hr> {% endif %} - {% if recommendations_to_prepare_for_voting %} + {% if recommendations.voting_in_preparation.exists %} <div class="row"> <div class="col-12"> <h1 class="highlight mt-0">Recommendations to prepare for voting</h1> </div> </div> - {% for rec in recommendations_to_prepare_for_voting %} + {% for rec in recommendations.voting_in_preparation %} {% if not forloop.first %} <hr> {% endif %} @@ -229,7 +229,7 @@ <div class="row"> <div class="col-12"> <!-- Submissions list --> - {% for sub in submissions_in_pool %} + {% for sub in submissions %} <div class="card card-outline-secondary mt-1" id="pool_submission_{{sub.id}}"> {% include 'submissions/_submission_card_in_pool.html' with submission=sub remark_form=remark_form is_ECAdmin=is_ECAdmin user=request.user %} </div> @@ -258,14 +258,14 @@ </div><!-- end status --> {% if is_ECAdmin %} - {% if recommendations_undergoing_voting %} + {% if recommendations.put_to_voting.exists %} <!-- Preparing --> <a href="#rec_filter_voting" data-toggle="collapse" class="collapsed"> <h3 class="card-title text-gray-dark">Recommendations undergoing voting ({{recommendations_undergoing_voting|length}})</h3> </a> <div id="rec_filter_voting" class="collapse"> <ul class="list-group list-group-flush"> - {% for recommendation in recommendations_undergoing_voting %} + {% for recommendation in recommendations.put_to_voting %} <li class="list-group-item"> <div class="card-body"> <a href="#undergoing_rec_{{recommendation.id}}">{{recommendation.submission.title}}</a> @@ -278,14 +278,14 @@ </div><!-- end preparing --> {% endif %} - {% if recommendations_to_prepare_for_voting %} + {% if recommendations.voting_in_preparation.exists %} <!-- Preparing --> <a href="#rec_filter_prepare" data-toggle="collapse" class="collapsed"> <h3 class="card-title text-gray-dark">Recommendations to prepare ({{recommendations_to_prepare_for_voting|length}})</h3> </a> <div id="rec_filter_prepare" class="collapse"> <ul class="list-group list-group-flush"> - {% for recommendation in recommendations_to_prepare_for_voting %} + {% for recommendation in recommendations.voting_in_preparation %} <li class="list-group-item"> <div class="card-body"> <a href="#prepare_rec_{{recommendation.id}}">{{recommendation.submission.title}}</a> @@ -301,11 +301,11 @@ <!-- Pool --> <a href="#pool_filter_submissions" data-toggle="collapse"> - <h3 class="card-title text-gray-dark">Submissions in pool ({{submissions_in_pool|length}})</h3> + <h3 class="card-title text-gray-dark">Submissions in pool ({{submissions|length}})</h3> </a> <div id="pool_filter_submissions" class="collapse show"> <ul class="list-group list-group-flush"> - {% for submission in submissions_in_pool %} + {% for submission in submissions %} <li class="list-group-item"> <div class="card-body" style="overflow: auto;"> <a href="#pool_submission_{{submission.id}}">{{submission.title}}</a> diff --git a/submissions/templates/submissions/pool/pool.html b/submissions/templates/submissions/pool/pool.html index 97758bf7d200b5414411822a62b3c25fa99375bd..d7e0c9b4a58ea51079dbeb1bb755054099a58240 100644 --- a/submissions/templates/submissions/pool/pool.html +++ b/submissions/templates/submissions/pool/pool.html @@ -23,18 +23,18 @@ <a href="{% url 'submissions:pool' %}?test=1">See old pool layout</a> <div class="row"> - <div class="col-md-7"> + <div class="col-12"> <h1>SciPost Submissions Pool</h1> {% if is_ECAdmin %} - {% if recommendations_to_prepare_for_voting or recommendations_undergoing_voting %} + {% if recommendations.voting_in_preparation.exists or recommendations.put_to_voting.exists %} <div class="quote-border"> <h2 class="text-primary">Administrative Tasks</h2> - {% if recommendations_to_prepare_for_voting %} + {% if recommendations.voting_in_preparation.exists %} <h3>Recommendations to prepare for voting <i class="fa fa-exclamation-circle text-warning"></i></h3> <ul> - {% for recommendation in recommendations_to_prepare_for_voting %} + {% for recommendation in recommendations.voting_in_preparation %} <li>On Editorial Recommendation: {{ recommendation }}<br> <a href="{% url 'submissions:prepare_for_voting' rec_id=recommendation.id %}">Prepare for voting</a> </li> @@ -42,10 +42,10 @@ </ul> {% endif %} - {% if recommendations_undergoing_voting %} + {% if recommendations.put_to_voting.exists %} <h3>Recommendations undergoing voting <i class="fa fa-exclamation-circle text-warning"></i></h3> <ul class="fa-ul"> - {% for recommendation in recommendations_undergoing_voting %} + {% for recommendation in recommendations.put_to_voting %} <li>{% include 'partials/submissions/admin/recommendation_tooltip.html' with classes='fa-li' recommendation=recommendation %} On Editorial Recommendation: {{ recommendation }}<br> <a href="{% url 'submissions:admin_recommendation' recommendation.submission.arxiv_identifier_w_vn_nr %}">See Editorial Recommendation</a> @@ -89,37 +89,35 @@ </form> {% endif %} - <ul class="list-unstyled" data-target="active-list"> - <!-- Submissions list --> - {% for sub in submissions_in_pool %} - <li class="p-2{% if sub == submission %} active{% endif %}"> - {% if sub == submission %} - {% include 'partials/submissions/pool/submission_li.html' with submission=sub is_current=1 %} - {% else %} - {% include 'partials/submissions/pool/submission_li.html' with submission=sub is_current=0 %} - {% endif %} - </li> - {% empty %} - <li> - <h3 class="text-center"><i class="fa fa-question fa-2x"></i><br>No Submissions found.</h3> - </li> - {% endfor %} - </ul> - </div><!-- End page content --> - - <div class="col-md-5" id="details"> - {% if submission %} - {% include 'partials/submissions/pool/submission_details.html' with submission=submission remark_form=remark_form is_ECAdmin=is_ECAdmin user=request.user %} + {% if search_form.status.value %} + <h3>All Submissions with status: <span class="text-primary">{{ search_form.status_verbose }}</span></h3> + {% include 'partials/submissions/pool/submissions_list.html' with submissions=submissions %} {% else %} - <h3><em>Click on a submission to see its summary and actions</em></h3> + <h3>Submissions currently in pre-screening</h3> + {% include 'partials/submissions/pool/submissions_list.html' with submissions=submissions.prescreening %} - {% if is_ECAdmin %} - <h2>All events in the last 24 hours</h2> - <div id="eventslist"> - {% include 'submissions/submission_event_list_general.html' with events=latest_events %} - </div> - {% endif %} + <h3>Submissions currently in active refereeing phase</h3> + {% include 'partials/submissions/pool/submissions_list.html' with submissions=submissions.actively_refereeing %} + + <h3>Submissions awaiting resubmission</h3> + {% include 'partials/submissions/pool/submissions_list.html' with submissions=submissions.revision_requested %} + + <h3>Submissions accepted</h3> + {% include 'partials/submissions/pool/submissions_list.html' with submissions=submissions.accepted %} {% endif %} - </div> + + </div><!-- End page content --> </div> {% endblock %} + +{% comment %} + <h3><em>Click on a submission to see its summary and actions</em></h3> + + {% if is_ECAdmin %} + <h2>All events in the last 24 hours</h2> + <div id="eventslist"> + {% include 'submissions/submission_event_list_general.html' with events=latest_events %} + </div> + {% endif %} + + {% endcomment %} diff --git a/submissions/templates/submissions/prepare_for_voting.html b/submissions/templates/submissions/prepare_for_voting.html index 0b26ca06cda441e3dab3c9a70ed28bf06b4c9940..ce9a61aef742d4234b1f147f4e0e2f79e5d97c39 100644 --- a/submissions/templates/submissions/prepare_for_voting.html +++ b/submissions/templates/submissions/prepare_for_voting.html @@ -57,8 +57,8 @@ <div class="col-md-6"> <p>Fellows with expertise matching the Submission's subject area:</p> <ul> - {% for Fellow in Fellows_with_expertise %} - <li>{{ Fellow.user.last_name }}</li> + {% for fellow in fellows_with_expertise %} + <li>{{ fellow.contributor.user.last_name }}</li> {% endfor %} </ul> </div> diff --git a/submissions/templatetags/submissions_extras.py b/submissions/templatetags/submissions_extras.py index de71caafbddb5aae5dc1edbe542a717123796841..c9b1abc8ec9d50d005a60829a77a0e271edcaafe 100644 --- a/submissions/templatetags/submissions_extras.py +++ b/submissions/templatetags/submissions_extras.py @@ -1,8 +1,5 @@ -import datetime - from django import template -from submissions.constants import SUBMISSION_STATUS_OUT_OF_POOL from submissions.models import Submission register = template.Library() diff --git a/submissions/urls.py b/submissions/urls.py index 2b9232e49bee589d62af54a5cb7668bc139f6553..63ba083da77f6d3e0da813b3389376a2f11dc871 100644 --- a/submissions/urls.py +++ b/submissions/urls.py @@ -76,8 +76,8 @@ urlpatterns = [ views.send_refereeing_invitation, name='send_refereeing_invitation'), url(r'^accept_or_decline_ref_invitations/$', views.accept_or_decline_ref_invitations, name='accept_or_decline_ref_invitations'), - url(r'^accept_or_decline_ref_invitation/(?P<invitation_id>[0-9]+)$', - views.accept_or_decline_ref_invitation_ack, name='accept_or_decline_ref_invitation_ack'), + url(r'^accept_or_decline_ref_invitations/(?P<invitation_id>[0-9]+)$', + views.accept_or_decline_ref_invitations, name='accept_or_decline_ref_invitations'), url(r'^decline_ref_invitation/(?P<invitation_key>.+)$', views.decline_ref_invitation, name='decline_ref_invitation'), url(r'^ref_invitation_reminder/{regex}/(?P<invitation_id>[0-9]+)$'.format(regex=SUBMISSIONS_COMPLETE_REGEX), diff --git a/submissions/utils.py b/submissions/utils.py index b741e421aef5c2d531c7cd9cf5536764694deba4..90fad6bad125cb646f92ac9ceea5b9094835cbef 100644 --- a/submissions/utils.py +++ b/submissions/utils.py @@ -1137,19 +1137,19 @@ class SubmissionUtils(BaseMailUtil): recipient_greeting = ('Dear ' + cls.communication.submission.submitted_by.get_title_display() + ' ' + cls.communication.submission.submitted_by.user.last_name) - bcc_emails.append(cls.communication.submission.editor_in_charge) + bcc_emails.append(cls.communication.submission.editor_in_charge.user.email) bcc_emails.append('submissions@scipost.org') elif cls.communication.comtype in ['EtoR']: recipient_email.append(cls.communication.referee.user.email) recipient_greeting = ('Dear ' + cls.communication.referee.get_title_display() + ' ' + cls.communication.referee.user.last_name) - bcc_emails.append(cls.communication.submission.editor_in_charge) + bcc_emails.append(cls.communication.submission.editor_in_charge.user.email) bcc_emails.append('submissions@scipost.org') elif cls.communication.comtype in ['EtoS']: recipient_email.append('submissions@scipost.org') recipient_greeting = 'Dear Editorial Administrators' - bcc_emails.append(cls.communication.submission.editor_in_charge) + bcc_emails.append(cls.communication.submission.editor_in_charge.user.email) further_action_page = 'https://scipost.org/submissions/pool' email_text = (recipient_greeting + diff --git a/submissions/views.py b/submissions/views.py index b0fbd035edfcb72d960c8a2889e03ea2e9d41e1f..5f905b5a4eb3e981f8ce208348adf8cd78522a32 100644 --- a/submissions/views.py +++ b/submissions/views.py @@ -7,7 +7,7 @@ from django.contrib.auth.models import Group from django.core.urlresolvers import reverse, reverse_lazy from django.db import transaction, IntegrityError from django.http import Http404, HttpResponse, HttpResponseRedirect -from django.shortcuts import get_object_or_404, render, redirect +from django.shortcuts import get_object_or_404, get_list_or_404, render, redirect from django.template import Template, Context from django.utils import timezone from django.utils.decorators import method_decorator @@ -16,10 +16,9 @@ from django.views.generic.detail import DetailView, SingleObjectMixin from django.views.generic.edit import CreateView, UpdateView from django.views.generic.list import ListView -from guardian.decorators import permission_required_or_403 -from guardian.shortcuts import assign_perm, get_objects_for_user +from guardian.shortcuts import assign_perm -from .constants import SUBMISSION_STATUS_VOTING_DEPRECATED, STATUS_VETTED, STATUS_EIC_ASSIGNED,\ +from .constants import STATUS_VETTED, STATUS_EIC_ASSIGNED,\ SUBMISSION_STATUS_PUBLICLY_INVISIBLE, SUBMISSION_STATUS, ED_COMM_CHOICES,\ STATUS_DRAFT, CYCLE_DIRECT_REC from .models import Submission, EICRecommendation, EditorialAssignment,\ @@ -339,61 +338,54 @@ def pool(request, arxiv_identifier_w_vn_nr=None): to publication acceptance or rejection. All members of the Editorial College have access. """ - submissions_in_pool = (Submission.objects.get_pool(request.user) - .prefetch_related('referee_invitations', 'remarks', 'comments')) - recommendations_undergoing_voting = (EICRecommendation.objects - .get_for_user_in_pool(request.user) - .filter(submission__status='put_to_EC_voting')) - recommendations_to_prepare_for_voting = (EICRecommendation.objects - .get_for_user_in_pool(request.user) - .filter( - submission__status='voting_in_preparation')) - contributor = Contributor.objects.get(user=request.user) - assignments_to_consider = EditorialAssignment.objects.open().filter(to=contributor) + # Objects + + # The following is in test phase. Update if test is done + # -- + # Search + search_form = SubmissionPoolFilterForm(request.GET or None) + if search_form.is_valid(): + submissions = search_form.search(Submission.objects.all(), request.user) + else: + # Mainly as fallback for the old-pool while in test phase. + submissions = Submission.objects.pool(request.user) + + # submissions = Submission.objects.pool(request.user) + recommendations = EICRecommendation.objects.filter(submission__in=submissions) + recs_to_vote_on = recommendations.user_may_vote_on(request.user) + assignments_to_consider = EditorialAssignment.objects.open().filter(to=request.user.contributor) + + # Forms consider_assignment_form = ConsiderAssignmentForm() - recs_to_vote_on = (EICRecommendation.objects.get_for_user_in_pool(request.user) - .filter(eligible_to_vote=contributor) - .exclude(recommendation__in=[-1, -2]) - .exclude(voted_for=contributor) - .exclude(voted_against=contributor) - .exclude(voted_abstain=contributor) - .exclude(submission__status__in=SUBMISSION_STATUS_VOTING_DEPRECATED)) rec_vote_form = RecommendationVoteForm() remark_form = RemarkForm() context = { - 'submissions_in_pool': submissions_in_pool, + 'submissions': submissions.order_by('status', '-submission_date'), + 'search_form': search_form, 'submission_status': SUBMISSION_STATUS, - 'recommendations_undergoing_voting': recommendations_undergoing_voting, - 'recommendations_to_prepare_for_voting': recommendations_to_prepare_for_voting, + 'recommendations': recommendations, 'assignments_to_consider': assignments_to_consider, 'consider_assignment_form': consider_assignment_form, 'recs_to_vote_on': recs_to_vote_on, 'rec_vote_form': rec_vote_form, 'remark_form': remark_form, - 'submission': None } # The following is in test phase. Update if test is done # -- - # Search - search_form = SubmissionPoolFilterForm(request.GET or None) - if search_form.is_valid(): - context['submissions_in_pool'] = search_form.search(context['submissions_in_pool'], - request.user.contributor) - context['search_form'] = search_form - # Show specific submission in the pool + context['submission'] = None if arxiv_identifier_w_vn_nr: try: - context['submission'] = context['submissions_in_pool'].get( + context['submission'] = Submission.objects.pool_full(request.user).get( arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) except Submission.DoesNotExist: pass # EdColAdmin related variables - if request.user.contributor.is_EdCol_Admin(): + if request.user.has_perm('scipost.can_oversee_refereeing'): context['latest_events'] = SubmissionEvent.objects.for_eic().last_hours() # Temporary test logic: only testers see the new Pool @@ -416,7 +408,7 @@ def submissions_by_status(request, status): if status not in status_dict.keys(): errormessage = 'Unknown status.' return render(request, 'scipost/error.html', {'errormessage': errormessage}) - submissions_of_status = (Submission.objects.get_pool(request.user) + submissions_of_status = (Submission.objects.pool_full(request.user) .filter(status=status).order_by('-submission_date')) context = { @@ -428,13 +420,13 @@ def submissions_by_status(request, status): @login_required -@permission_required('scipost.can_view_pool', raise_exception=True) +# @permission_required('scipost.can_view_pool', raise_exception=True) def add_remark(request, arxiv_identifier_w_vn_nr): """ With this method, an Editorial Fellow or Board Member is adding a remark on a Submission. """ - submission = get_object_or_404(Submission.objects.get_pool(request.user), + submission = get_object_or_404(Submission.objects.pool_editable(request.user), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) remark_form = RemarkForm(request.POST or None) @@ -453,7 +445,11 @@ def add_remark(request, arxiv_identifier_w_vn_nr): @login_required @permission_required('scipost.can_assign_submissions', raise_exception=True) def assign_submission(request, arxiv_identifier_w_vn_nr): - submission_to_assign = get_object_or_404(Submission.objects.get_pool(request.user), + """ + Assign Editor-in-charge to Submission. + Action done by SciPost Administration or Editorial College Administration. + """ + submission_to_assign = get_object_or_404(Submission.objects.pool_editable(request.user), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) form = AssignSubmissionForm(discipline=submission_to_assign.discipline) context = {'submission_to_assign': submission_to_assign, @@ -464,7 +460,11 @@ def assign_submission(request, arxiv_identifier_w_vn_nr): @login_required @permission_required('scipost.can_assign_submissions', raise_exception=True) def assign_submission_ack(request, arxiv_identifier_w_vn_nr): - submission = get_object_or_404(Submission.objects.get_pool(request.user), + """ + Assign Editor-in-charge to Submission. + Action done by SciPost Administration or Editorial College Administration. + """ + submission = get_object_or_404(Submission.objects.pool_editable(request.user), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) if request.method == 'POST': form = AssignSubmissionForm(request.POST, discipline=submission.discipline) @@ -532,9 +532,6 @@ def assignment_request(request, assignment_id): SubmissionUtils.load({'assignment': assignment}) SubmissionUtils.deprecate_other_assignments() - assign_perm('can_take_editorial_actions', request.user, assignment.submission) - ed_admins = Group.objects.get(name='Editorial Administrators') - assign_perm('can_take_editorial_actions', ed_admins, assignment.submission) SubmissionUtils.send_EIC_appointment_email() SubmissionUtils.send_author_prescreening_passed_email() @@ -572,7 +569,7 @@ def volunteer_as_EIC(request, arxiv_identifier_w_vn_nr): Called when a Fellow volunteers while perusing the submissions pool. This is an adapted version of the assignment_request method. """ - submission = get_object_or_404(Submission.objects.get_pool(request.user), + submission = get_object_or_404(Submission.objects.pool(request.user), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) errormessage = None if submission.status == 'assignment_failed': @@ -607,9 +604,6 @@ def volunteer_as_EIC(request, arxiv_identifier_w_vn_nr): SubmissionUtils.load({'assignment': assignment}) SubmissionUtils.deprecate_other_assignments() - assign_perm('can_take_editorial_actions', contributor.user, submission) - ed_admins = Group.objects.get(name='Editorial Administrators') - assign_perm('can_take_editorial_actions', ed_admins, submission) SubmissionUtils.send_EIC_appointment_email() SubmissionUtils.send_author_prescreening_passed_email() @@ -630,7 +624,7 @@ def assignment_failed(request, arxiv_identifier_w_vn_nr): The submission is rejected. This method is called from pool.html by an Editorial Administrator. """ - submission = get_object_or_404(Submission.objects.get_pool(request.user), + submission = get_object_or_404(Submission.objects.pool(request.user), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) if request.method == 'POST': form = ModifyPersonalMessageForm(request.POST) @@ -680,10 +674,13 @@ def assignments(request): @login_required -@permission_required_or_403('can_take_editorial_actions', - (Submission, 'arxiv_identifier_w_vn_nr', 'arxiv_identifier_w_vn_nr')) def editorial_page(request, arxiv_identifier_w_vn_nr): - submission = get_object_or_404(Submission.objects.filter_editorial_page(request.user), + """ + The central page for the EIC to manage all its Editorial duties. + + Accessible for: Editor-in-charge and Editorial Administration + """ + submission = get_object_or_404(Submission.objects.filter_for_eic(request.user), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) context = { @@ -695,11 +692,16 @@ def editorial_page(request, arxiv_identifier_w_vn_nr): @login_required -@permission_required_or_403('can_take_editorial_actions', - (Submission, 'arxiv_identifier_w_vn_nr', 'arxiv_identifier_w_vn_nr')) def cycle_form_submit(request, arxiv_identifier_w_vn_nr): - submission = get_object_or_404(Submission.objects.get_pool(request.user), + """ + If Submission is `resubmission_incoming` the EIC should first choose what refereeing + cycle to choose. + + Accessible for: Editor-in-charge and Editorial Administration + """ + submission = get_object_or_404(Submission.objects.filter_for_eic(request.user), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) + form = SubmissionCycleChoiceForm(request.POST or None, instance=submission) if form.is_valid(): submission = form.save() @@ -717,10 +719,13 @@ def cycle_form_submit(request, arxiv_identifier_w_vn_nr): @login_required -@permission_required_or_403('can_take_editorial_actions', - (Submission, 'arxiv_identifier_w_vn_nr', 'arxiv_identifier_w_vn_nr')) def select_referee(request, arxiv_identifier_w_vn_nr): - submission = get_object_or_404(Submission.objects.filter_editorial_page(request.user), + """ + Select/Invite referees by first listing them here. + + Accessible for: Editor-in-charge and Editorial Administration + """ + submission = get_object_or_404(Submission.objects.filter_for_eic(request.user), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) context = {} queryresults = '' @@ -758,8 +763,6 @@ def select_referee(request, arxiv_identifier_w_vn_nr): @login_required -@permission_required_or_403('can_take_editorial_actions', - (Submission, 'arxiv_identifier_w_vn_nr', 'arxiv_identifier_w_vn_nr')) @transaction.atomic def recruit_referee(request, arxiv_identifier_w_vn_nr): """ @@ -769,9 +772,12 @@ def recruit_referee(request, arxiv_identifier_w_vn_nr): This function emails a registration invitation to this person. The pending refereeing invitation is then recognized upon registration, using the invitation token. + + Accessible for: Editor-in-charge and Editorial Administration """ - submission = get_object_or_404(Submission.objects.filter_editorial_page(request.user), + submission = get_object_or_404(Submission.objects.filter_for_eic(request.user), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) + if request.method == 'POST': ref_recruit_form = RefereeRecruitmentForm(request.POST) if ref_recruit_form.is_valid(): @@ -820,8 +826,6 @@ def recruit_referee(request, arxiv_identifier_w_vn_nr): @login_required -@permission_required_or_403('can_take_editorial_actions', - (Submission, 'arxiv_identifier_w_vn_nr', 'arxiv_identifier_w_vn_nr')) @transaction.atomic def send_refereeing_invitation(request, arxiv_identifier_w_vn_nr, contributor_id): """ @@ -829,10 +833,13 @@ def send_refereeing_invitation(request, arxiv_identifier_w_vn_nr, contributor_id in the case where the referee is an identified Contributor. For a referee who isn't a Contributor yet, the method recruit_referee above is called instead. + + Accessible for: Editor-in-charge and Editorial Administration """ - submission = get_object_or_404(Submission.objects.get_pool(request.user), + submission = get_object_or_404(Submission.objects.filter_for_eic(request.user), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) contributor = get_object_or_404(Contributor, pk=contributor_id) + if not contributor.is_currently_available: errormessage = ('This Contributor is marked as currently unavailable. ' 'Please go back and select another referee.') @@ -865,15 +872,17 @@ def send_refereeing_invitation(request, arxiv_identifier_w_vn_nr, contributor_id @login_required -@permission_required_or_403('can_take_editorial_actions', - (Submission, 'arxiv_identifier_w_vn_nr', 'arxiv_identifier_w_vn_nr')) def ref_invitation_reminder(request, arxiv_identifier_w_vn_nr, invitation_id): """ This method is used by the Editor-in-charge from the editorial_page when a referee has been invited but hasn't answered yet. It can be used for registered as well as unregistered referees. + + Accessible for: Editor-in-charge and Editorial Administration """ - invitation = get_object_or_404(RefereeInvitation, pk=invitation_id) + submission = get_object_or_404(Submission.objects.filter_for_eic(request.user), + arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) + invitation = get_object_or_404(submission.referee_invitations.all(), pk=invitation_id) invitation.nr_reminders += 1 invitation.date_last_reminded = timezone.now() invitation.save() @@ -882,42 +891,50 @@ def ref_invitation_reminder(request, arxiv_identifier_w_vn_nr, invitation_id): SubmissionUtils.send_ref_reminder_email() else: SubmissionUtils.send_unreg_ref_reminder_email() + messages.success(request, 'Reminder sent succesfully.') return redirect(reverse('submissions:editorial_page', kwargs={'arxiv_identifier_w_vn_nr': arxiv_identifier_w_vn_nr})) @login_required @permission_required('scipost.can_referee', raise_exception=True) -def accept_or_decline_ref_invitations(request): +def accept_or_decline_ref_invitations(request, invitation_id=None): """ RefereeInvitations need to be either accepted or declined by the invited user using this view. The decision will be taken one invitation at a time. """ - invitation = RefereeInvitation.objects.filter(referee__user=request.user, - accepted=None, - cancelled=False).first() + invitation = RefereeInvitation.objects.filter( + referee__user=request.user, accepted=None, cancelled=False) + if invitation_id: + try: + invitation = invitation.get(id=invitation_id) + except RefereeInvitation.DoesNotExist: + invitation = invitation.first() + else: + invitation = invitation.first() + if not invitation: - messages.success(request, 'There are no Refereeing Invitations for you to consider.') + messages.success(request, 'There are no more Refereeing Invitations for you to consider.') return redirect(reverse('scipost:personal_page')) - form = ConsiderRefereeInvitationForm() - context = {'invitation_to_consider': invitation, 'form': form} - return render(request, 'submissions/accept_or_decline_ref_invitations.html', context) - -@login_required -@permission_required('scipost.can_referee', raise_exception=True) -def accept_or_decline_ref_invitation_ack(request, invitation_id): - invitation = get_object_or_404(RefereeInvitation, pk=invitation_id) form = ConsiderRefereeInvitationForm(request.POST or None) if form.is_valid(): invitation.date_responded = timezone.now() if form.cleaned_data['accept'] == 'True': invitation.accepted = True decision_string = 'accepted' + messages.success(request, ('<h3>Thank you for agreeing to referee this Submission</h3>' + '<p>When you are ready, please go to the ' + '<a href="{url}">Submission\'s page</a> to' + ' submit your Report.</p>'.format( + url=invitation.submission.get_absolute_url()))) else: invitation.accepted = False decision_string = 'declined' invitation.refusal_reason = form.cleaned_data['refusal_reason'] + messages.success(request, ('<h1>You have declined to contribute a Report</h1>' + 'Nonetheless, we thank you very much for considering' + ' this refereeing invitation.</p>')) invitation.save() SubmissionUtils.load({'invitation': invitation}, request) SubmissionUtils.email_referee_response_to_EIC() @@ -929,9 +946,13 @@ def accept_or_decline_ref_invitation_ack(request, invitation_id): invitation.submission.add_event_for_eic('Referee %s has %s the refereeing invitation.' % (invitation.referee.user.last_name, decision_string)) - - context = {'invitation': invitation} - return render(request, 'submissions/accept_or_decline_ref_invitation_ack.html', context) + return redirect('submissions:accept_or_decline_ref_invitations') + form = ConsiderRefereeInvitationForm() + context = { + 'invitation': invitation, + 'form': form + } + return render(request, 'submissions/accept_or_decline_ref_invitations.html', context) def decline_ref_invitation(request, invitation_key): @@ -965,15 +986,20 @@ def decline_ref_invitation(request, invitation_key): @login_required -@permission_required_or_403('can_take_editorial_actions', - (Submission, 'arxiv_identifier_w_vn_nr', 'arxiv_identifier_w_vn_nr')) def cancel_ref_invitation(request, arxiv_identifier_w_vn_nr, invitation_id): """ This method is used by the Editor-in-charge from the editorial_page to remove a referee for the list of invited ones. It can be used for registered as well as unregistered referees. + + Accessible for: Editor-in-charge and Editorial Administration """ - invitation = get_object_or_404(RefereeInvitation, pk=invitation_id) + try: + submissions = Submission.objects.filter_for_eic(request.user) + invitation = submissions.referee_invitations.get(pk=invitation_id) + except RefereeInvitation.DoesNotExist: + raise Http404 + invitation.cancelled = True invitation.save() SubmissionUtils.load({'invitation': invitation}) @@ -989,11 +1015,15 @@ def cancel_ref_invitation(request, arxiv_identifier_w_vn_nr, invitation_id): @login_required -@permission_required_or_403('can_take_editorial_actions', - (Submission, 'arxiv_identifier_w_vn_nr', 'arxiv_identifier_w_vn_nr')) def extend_refereeing_deadline(request, arxiv_identifier_w_vn_nr, days): - submission = get_object_or_404(Submission.objects.get_pool(request.user), + """ + Extend Refereeing deadline for Submission and open reporting and commenting. + + Accessible for: Editor-in-charge and Editorial Administration + """ + submission = get_object_or_404(Submission.objects.filter_for_eic(request.user), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) + submission.reporting_deadline += datetime.timedelta(days=int(days)) submission.open_for_reporting = True submission.open_for_commenting = True @@ -1007,10 +1037,14 @@ def extend_refereeing_deadline(request, arxiv_identifier_w_vn_nr, days): @login_required -@permission_required_or_403('can_take_editorial_actions', - (Submission, 'arxiv_identifier_w_vn_nr', 'arxiv_identifier_w_vn_nr')) def set_refereeing_deadline(request, arxiv_identifier_w_vn_nr): - submission = get_object_or_404(Submission.objects.get_pool(request.user), + """ + Set Refereeing deadline for Submission and open reporting and commenting if + the new date is in the future. + + Accessible for: Editor-in-charge and Editorial Administration + """ + submission = get_object_or_404(Submission.objects.filter_for_eic(request.user), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) form = SetRefereeingDeadlineForm(request.POST or None) @@ -1023,12 +1057,7 @@ def set_refereeing_deadline(request, arxiv_identifier_w_vn_nr): submission.latest_activity = timezone.now() submission.save() submission.add_general_event('A new refereeing deadline is set.') - context = {'ack_header': 'New reporting deadline set.', - 'followup_message': 'Return to the ', - 'followup_link': reverse('submissions:editorial_page', - kwargs={'arxiv_identifier_w_vn_nr': submission.arxiv_identifier_w_vn_nr}), - 'followup_link_label': 'Submission\'s Editorial Page'} - return render(request, 'scipost/acknowledgement.html', context) + messages.success(request, 'New reporting deadline set.') else: messages.error(request, 'The deadline has not been set. Please try again.') @@ -1037,8 +1066,6 @@ def set_refereeing_deadline(request, arxiv_identifier_w_vn_nr): @login_required -@permission_required_or_403('can_take_editorial_actions', - (Submission, 'arxiv_identifier_w_vn_nr', 'arxiv_identifier_w_vn_nr')) def close_refereeing_round(request, arxiv_identifier_w_vn_nr): """ Called by the Editor-in-charge when a satisfactory number of @@ -1046,9 +1073,12 @@ def close_refereeing_round(request, arxiv_identifier_w_vn_nr): Automatically emails the authors to ask them if they want to round off any replies to reports or comments before the editorial recommendation is formulated. + + Accessible for: Editor-in-charge and Editorial Administration """ - submission = get_object_or_404(Submission.objects.get_pool(request.user), + submission = get_object_or_404(Submission.objects.filter_for_eic(request.user), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) + submission.open_for_reporting = False submission.open_for_commenting = False if submission.status == 'EICassigned': # only close if currently undergoing refereeing @@ -1064,7 +1094,9 @@ def close_refereeing_round(request, arxiv_identifier_w_vn_nr): @permission_required('scipost.can_oversee_refereeing', raise_exception=True) def refereeing_overview(request): - submissions_under_refereeing = (Submission.objects.filter(status=STATUS_EIC_ASSIGNED) + submissions_under_refereeing = (Submission.objects + .pool_editable(request.user) + .filter(status=STATUS_EIC_ASSIGNED) .order_by('submission_date')) context = {'submissions_under_refereeing': submissions_under_refereeing} return render(request, 'submissions/refereeing_overview.html', context) @@ -1076,13 +1108,14 @@ def communication(request, arxiv_identifier_w_vn_nr, comtype, referee_id=None): Communication between editor-in-charge, author or referee occurring during the submission refereeing. """ - submission = get_object_or_404(Submission, arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) + submission = get_object_or_404(Submission.objects.pool_full(request.user), + arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) errormessage = None if comtype not in dict(ED_COMM_CHOICES).keys(): errormessage = 'Unknown type of cummunication.' # TODO: Verify that this is requested by an authorized contributor (eic, ref, author) elif (comtype in ['EtoA', 'EtoR', 'EtoS'] and - not request.user.has_perm('can_take_editorial_actions', submission)): + submission.editor_in_charge != request.user.contributor): errormessage = 'Only the Editor-in-charge can perform this action.' elif (comtype in ['AtoE'] and not (request.user.contributor == submission.submitted_by)): @@ -1095,8 +1128,8 @@ def communication(request, arxiv_identifier_w_vn_nr, comtype, referee_id=None): not request.user.groups.filter(name='Editorial Administrators').exists()): errormessage = 'Only Editorial Administrators can perform this action.' if errormessage is not None: - context = {'errormessage': errormessage, 'comtype': comtype} - return render(request, 'submissions/communication.html', context) + messages.warning(request, errormessage) + return redirect(reverse('submissions:pool')) form = EditorialCommunicationForm(request.POST or None) if form.is_valid(): @@ -1122,13 +1155,17 @@ def communication(request, arxiv_identifier_w_vn_nr, comtype, referee_id=None): @login_required -@permission_required_or_403('can_take_editorial_actions', - (Submission, 'arxiv_identifier_w_vn_nr', 'arxiv_identifier_w_vn_nr')) @transaction.atomic def eic_recommendation(request, arxiv_identifier_w_vn_nr): - submission = get_object_or_404(Submission.objects.filter_editorial_page(request.user), + """ + Write EIC Recommendation. + + Accessible for: Editor-in-charge and Editorial Administration + """ + submission = get_object_or_404(Submission.objects.filter_for_eic(request.user), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) - if submission.eic_recommendation_required(): + + if not submission.eic_recommendation_required: messages.warning(request, ('<h3>An Editorial Recommendation is not required</h3>' 'This submission\'s current status is: <em>%s</em>' % submission.get_status_display())) @@ -1137,11 +1174,13 @@ def eic_recommendation(request, arxiv_identifier_w_vn_nr): # Find EditorialAssignment for user try: - assignment = submission.editorial_assignments.get(submission=submission, - to=request.user.contributor) + assignment = submission.editorial_assignments.accepted().get( + to=submission.editor_in_charge) except EditorialAssignment.DoesNotExist: messages.warning(request, ('You cannot formulate an Editorial Recommendation,' - ' because you are not assigned as editor-in-charge.')) + ' because the Editorial Assignment has not been set properly.' + ' Please ' + '<a href="mailto:admin@scipost.org">report the problem</a>.')) return redirect(reverse('submissions:editorial_page', args=[submission.arxiv_identifier_w_vn_nr])) @@ -1264,10 +1303,6 @@ def submit_report(request, arxiv_identifier_w_vn_nr): SubmissionUtils.email_EIC_report_delivered() SubmissionUtils.email_referee_report_delivered() - # Assign explicit permission to EIC to vet this report - assign_perm('submissions.can_vet_submitted_reports', submission.editor_in_charge.user, - newreport) - # Add SubmissionEvents for the EIC only, as it can also be rejected still submission.add_event_for_eic('%s has submitted a new Report.' % request.user.last_name) @@ -1280,12 +1315,13 @@ def submit_report(request, arxiv_identifier_w_vn_nr): @login_required -@permission_required('submissions.can_vet_submitted_reports', raise_exception=True) def vet_submitted_reports_list(request): """ Reports with status `unvetted` will be shown (oldest first). """ - reports_to_vet = Report.objects.awaiting_vetting().order_by('date_submitted') + submissions = get_list_or_404(Submission.objects.filter_for_eic(request.user)) + reports_to_vet = Report.objects.filter( + submission__in=submissions).awaiting_vetting().order_by('date_submitted') context = {'reports_to_vet': reports_to_vet} return render(request, 'submissions/vet_submitted_reports_list.html', context) @@ -1300,11 +1336,9 @@ def vet_submitted_report(request, report_id): After vetting an email is sent to the report author, bcc EIC. If report has not been refused, the submission author is also mailed. """ - # Method `get_objects_for_user` gets all Reports that are assigned to the user - # or *all* Reports if user is SciPost Admin or Vetting Editor. - report = get_object_or_404((get_objects_for_user(request.user, - 'submissions.can_vet_submitted_reports') - .awaiting_vetting()), id=report_id) + submissions = Submission.objects.filter_for_eic(request.user) + report = get_object_or_404(Report.objects.filter( + submission__in=submissions).awaiting_vetting(), id=report_id) form = VetReportForm(request.POST or None, initial={'report': report}) if form.is_valid(): @@ -1339,72 +1373,65 @@ def vet_submitted_report(request, report_id): return render(request, 'submissions/vet_submitted_report.html', context) +@login_required @permission_required('scipost.can_prepare_recommendations_for_voting', raise_exception=True) @transaction.atomic def prepare_for_voting(request, rec_id): - recommendation = get_object_or_404((EICRecommendation.objects - .get_for_user_in_pool(request.user)), id=rec_id) - Fellows_with_expertise = Contributor.objects.filter( - user__groups__name__in=['Editorial College'], - expertises__contains=[recommendation.submission.subject_area]) + submissions = Submission.objects.pool_editable(request.user) + recommendation = get_object_or_404( + EICRecommendation.objects.filter(submission__in=submissions), id=rec_id) + + fellows_with_expertise = recommendation.submission.fellows.filter( + contributor__expertises__contains=[recommendation.submission.subject_area]) + coauthorships = {} - if request.method == 'POST': - eligibility_form = VotingEligibilityForm( - request.POST, - discipline=recommendation.submission.discipline, - subject_area=recommendation.submission.subject_area - ) - if eligibility_form.is_valid(): - recommendation.eligible_to_vote = eligibility_form.cleaned_data['eligible_Fellows'] - recommendation.voted_for.add(recommendation.submission.editor_in_charge) - recommendation.save() - recommendation.submission.status = 'put_to_EC_voting' - recommendation.submission.save() - messages.success(request, 'We have registered your selection.') - # Add SubmissionEvents - recommendation.submission.add_event_for_eic('The Editorial Recommendation has been ' - 'put forward to the College for voting.') + eligibility_form = VotingEligibilityForm(request.POST or None, instance=recommendation) + if eligibility_form.is_valid(): + eligibility_form.save() + messages.success(request, 'We have registered your selection.') - return redirect(reverse('submissions:editorial_page', - args=[recommendation.submission.arxiv_identifier_w_vn_nr])) + # Add SubmissionEvents + recommendation.submission.add_event_for_eic('The Editorial Recommendation has been ' + 'put forward to the College for voting.') + + 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: + 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.user.last_name + ')' + 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.user.last_name] = queryresults - - eligibility_form = VotingEligibilityForm( - discipline=recommendation.submission.discipline, - subject_area=recommendation.submission.subject_area) + coauthorships[fellow.contributor.user.last_name] = queryresults context = { 'recommendation': recommendation, - 'Fellows_with_expertise': Fellows_with_expertise, + 'fellows_with_expertise': fellows_with_expertise, 'coauthorships': coauthorships, 'eligibility_form': eligibility_form, } return render(request, 'submissions/prepare_for_voting.html', context) -@permission_required('scipost.can_take_charge_of_submissions', raise_exception=True) +@login_required @transaction.atomic def vote_on_rec(request, rec_id): - recommendation = get_object_or_404((EICRecommendation.objects - .get_for_user_in_pool(request.user)), id=rec_id) + submissions = Submission.objects.pool_editable(request.user) + recommendation = get_object_or_404( + EICRecommendation.objects.filter(submission__in=submissions), id=rec_id) + form = RecommendationVoteForm(request.POST or None) if form.is_valid(): if form.cleaned_data['vote'] == 'agree': @@ -1453,13 +1480,15 @@ def remind_Fellows_to_vote(request): """ This method sends an email to all Fellow with pending voting duties. It must be called by and Editorial Administrator. + + TODO: This reminder function doesn't filter per submission?! """ - recommendations_undergoing_voting = (EICRecommendation.objects - .get_for_user_in_pool(request.user) - .filter(submission__status__in=['put_to_EC_voting'])) + submissions = Submission.objects.pool_editable(request.user) + recommendations = EICRecommendation.objects.filter(submission__in=submissions).put_to_voting() + Fellow_emails = [] Fellow_names = [] - for rec in recommendations_undergoing_voting: + for rec in recommendations: for Fellow in rec.eligible_to_vote.all(): if (Fellow not in rec.voted_for.all() and Fellow not in rec.voted_against.all() @@ -1487,10 +1516,15 @@ def fix_College_decision(request, rec_id): Terminates the voting on a Recommendation. Called by an Editorial Administrator. + # TODO - 2 bugs: + TO FIX: If multiple recommendations are submitted; decisions may be overruled unexpectedly. + TO FIX: A college decision can be fixed multiple times, there is no already-fixed mechanism!!! """ - recommendation = get_object_or_404((EICRecommendation.objects - .get_for_user_in_pool(request.user)), pk=rec_id) + submissions = Submission.objects.pool_full(request.user) + recommendation = get_object_or_404(EICRecommendation.objects.filter( + submission__in=submissions), id=rec_id) + submission = recommendation.submission if recommendation.recommendation in [1, 2, 3]: # Publish as Tier I, II or III