diff --git a/SciPost_v1/settings/base.py b/SciPost_v1/settings/base.py index 760615bd5f411f88a7d34b7df20f3ac85cd48ccb..0f3ea5de4bd041519094eb14dc7baf163ca6a74f 100644 --- a/SciPost_v1/settings/base.py +++ b/SciPost_v1/settings/base.py @@ -86,6 +86,7 @@ INSTALLED_APPS = ( 'sphinxdoc', 'commentaries', 'comments', + 'finances', 'journals', 'mails', 'mailing_lists', @@ -227,9 +228,13 @@ USE_TZ = True # MEDIA MEDIA_URL = '/media/' -MEDIA_ROOT = 'local_files/media/' +MEDIA_URL_SECURE = '/files/secure/' MAX_UPLOAD_SIZE = "1310720" # Default max attachment size in Bytes +# -- These MEDIA settings are machine-dependent +MEDIA_ROOT = 'local_files/media/' +MEDIA_ROOT_SECURE = 'local_files/secure/media/' + # Static files (CSS, JavaScript, Images) STATIC_URL = '/static/' STATIC_ROOT = 'local_files/static/' diff --git a/SciPost_v1/urls.py b/SciPost_v1/urls.py index aabec88b1b1947e7d1087ee37e9d4ac3734646c2..ab3ea6f377dc701332e17b0f0a9f4152416c6126 100644 --- a/SciPost_v1/urls.py +++ b/SciPost_v1/urls.py @@ -33,6 +33,7 @@ urlpatterns = [ url(r'^commentary/', include('commentaries.urls', namespace="_commentaries")), url(r'^comments/', include('comments.urls', namespace="comments")), url(r'^funders/', include('funders.urls', namespace="funders")), + url(r'^finances/', include('finances.urls', namespace="finances")), url(r'^journals/', include('journals.urls.general', namespace="journals")), url(r'^mailing_list/', include('mailing_lists.urls', namespace="mailing_lists")), url(r'^submissions/', include('submissions.urls', namespace="submissions")), diff --git a/commentaries/migrations/0019_auto_20170925_2124.py b/commentaries/migrations/0019_auto_20170925_2124.py new file mode 100644 index 0000000000000000000000000000000000000000..869d355df63987056b768158f5b090a7e0093ef7 --- /dev/null +++ b/commentaries/migrations/0019_auto_20170925_2124.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-25 19:24 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('commentaries', '0018_auto_20170909_1649'), + ] + + operations = [ + migrations.AlterField( + model_name='commentary', + name='authors', + field=models.ManyToManyField(blank=True, related_name='commentaries', to='scipost.Contributor'), + ), + migrations.AlterField( + model_name='commentary', + name='authors_claims', + field=models.ManyToManyField(blank=True, related_name='claimed_commentaries', to='scipost.Contributor'), + ), + migrations.AlterField( + model_name='commentary', + name='authors_false_claims', + field=models.ManyToManyField(blank=True, related_name='false_claimed_commentaries', to='scipost.Contributor'), + ), + migrations.AlterField( + model_name='commentary', + name='requested_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='requested_commentaries', to='scipost.Contributor'), + ), + ] diff --git a/commentaries/models.py b/commentaries/models.py index aa5df1de3fd4e8f87c100e52c856ae7bdd18e74f..add1f6b8ca87771c4527dd64646ccb71cb405ff6 100644 --- a/commentaries/models.py +++ b/commentaries/models.py @@ -16,7 +16,8 @@ class Commentary(TimeStampedModel): A Commentary contains all the contents of a SciPost Commentary page for a given publication. """ requested_by = models.ForeignKey('scipost.Contributor', blank=True, null=True, - on_delete=models.CASCADE, related_name='requested_by') + on_delete=models.CASCADE, + related_name='requested_commentaries') vetted = models.BooleanField(default=False) vetted_by = models.ForeignKey('scipost.Contributor', blank=True, null=True, on_delete=models.CASCADE) @@ -44,11 +45,11 @@ class Commentary(TimeStampedModel): # Authors which have been mapped to contributors: authors = models.ManyToManyField('scipost.Contributor', blank=True, - related_name='authors_com') + related_name='commentaries') authors_claims = models.ManyToManyField('scipost.Contributor', blank=True, - related_name='authors_com_claims') + related_name='claimed_commentaries') authors_false_claims = models.ManyToManyField('scipost.Contributor', blank=True, - related_name='authors_com_false_claims') + related_name='false_claimed_commentaries') journal = models.CharField(max_length=300, blank=True) volume = models.CharField(max_length=50, blank=True) pages = models.CharField(max_length=50, blank=True) diff --git a/comments/migrations/0017_auto_20170729_0717.py b/comments/migrations/0017_auto_20170729_0717.py index db86be1347d9f1c75a3c61ef6d07bf5028ae94c9..8ac6202aae5df406daa83cbee1b9f94c043ded68 100644 --- a/comments/migrations/0017_auto_20170729_0717.py +++ b/comments/migrations/0017_auto_20170729_0717.py @@ -11,10 +11,10 @@ from guardian.shortcuts import assign_perm def update_all_contenttypes(**kwargs): from django.apps import apps - from django.contrib.contenttypes.management import update_contenttypes + from django.contrib.contenttypes.management import create_contenttypes for app_config in apps.get_app_configs(): - update_contenttypes(app_config, **kwargs) + create_contenttypes(app_config, **kwargs) def create_all_permissions(**kwargs): diff --git a/finances/__init__.py b/finances/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/finances/admin.py b/finances/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e --- /dev/null +++ b/finances/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/finances/apps.py b/finances/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..4a1db0117b5f1f8dca3c5ba77a8b9c55a9201f34 --- /dev/null +++ b/finances/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class FinancesConfig(AppConfig): + name = 'finances' diff --git a/finances/migrations/__init__.py b/finances/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/finances/models.py b/finances/models.py new file mode 100644 index 0000000000000000000000000000000000000000..71a836239075aa6e6e4ecb700e9c42c95c022d91 --- /dev/null +++ b/finances/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/finances/templates/finances/timesheets.html b/finances/templates/finances/timesheets.html new file mode 100644 index 0000000000000000000000000000000000000000..63d43519a766bf7535bedf40f08346554eb7c837 --- /dev/null +++ b/finances/templates/finances/timesheets.html @@ -0,0 +1,61 @@ +{% extends 'scipost/base.html' %} + +{% block breadcrumb_items %} + <span class="breadcrumb-item">Team Timesheets</span> +{% endblock %} + +{% block pagetitle %}: Team Timesheets{% endblock pagetitle %} + +{% load bootstrap %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Team Timesheets</h1> + + <form method="get" action="{% url 'finances:timesheets' %}"> + {{ form|bootstrap }} + <input type="submit" class="btn btn-primary" value="Filter"> + </form> + </div> +</div> + +<div class="row"> + <div class="col-12"> + <h2 class="mb-2 mt-4">Team Timesheets</h2> + {% for total in totals %} + <h3 class="mb-1">{{ total.user }}</h3> + <table class="table"> + <thead class="thead-default"> + <tr> + <th>Date</th> + <th>By</th> + <th>Stream</th> + <th>Event</th> + <th>Duration</th> + </tr> + </thead> + + <tbody> + {% for event in total.events %} + <tr> + <td>{{ event.noted_on }}</td> + <td>{{ event.noted_by }}</td> + <td>{{ event.stream }}</td> + <td>{{ event.get_event_display }}</td> + <td>{{ event.duration }}</td> + </tr> + {% endfor %} + <tr> + <td colspan="4"></td> + <td><strong>{{ total.duration.total }}</strong></td> + </tr> + </tbody> + </table> + {% empty %} + <h3>Timesheet is empty for this month.</h3> + {% endfor %} + </div> +</div> +{% endblock %} diff --git a/finances/tests.py b/finances/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/finances/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/finances/urls.py b/finances/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..21569e295d98c0c64a768ff55dc9c71bcbf07e7e --- /dev/null +++ b/finances/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls import url + +from . import views + +urlpatterns = [ + url(r'^$', views.timesheets, name='finance'), + url(r'^timesheets$', views.timesheets, name='timesheets'), +] diff --git a/finances/views.py b/finances/views.py new file mode 100644 index 0000000000000000000000000000000000000000..b967443f18581bfd581f6710541e6f73e46fbf4f --- /dev/null +++ b/finances/views.py @@ -0,0 +1,20 @@ +from django.contrib.auth.decorators import permission_required +from django.shortcuts import render + +from production.forms import ProductionUserMonthlyActiveFilter + + +@permission_required('scipost.can_view_timesheets', raise_exception=True) +def timesheets(request): + """ + See an overview per month of all timesheets. + """ + form = ProductionUserMonthlyActiveFilter(request.GET or None) + context = { + 'form': form, + } + + # if form.is_valid(): + context['totals'] = form.get_totals() + + return render(request, 'finances/timesheets.html', context) diff --git a/journals/migrations/0045_auto_20170925_2124.py b/journals/migrations/0045_auto_20170925_2124.py new file mode 100644 index 0000000000000000000000000000000000000000..73a946327c5389eec5dcfeb32ac89933e8a95fa3 --- /dev/null +++ b/journals/migrations/0045_auto_20170925_2124.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-25 19:24 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0044_publication_doideposit_needs_updating'), + ] + + operations = [ + migrations.AlterField( + model_name='publication', + name='authors', + field=models.ManyToManyField(blank=True, related_name='publications', to='scipost.Contributor'), + ), + migrations.AlterField( + model_name='publication', + name='authors_claims', + field=models.ManyToManyField(blank=True, related_name='claimed_publications', to='scipost.Contributor'), + ), + migrations.AlterField( + model_name='publication', + name='authors_false_claims', + field=models.ManyToManyField(blank=True, related_name='false_claimed_publications', to='scipost.Contributor'), + ), + migrations.AlterField( + model_name='publication', + name='authors_unregistered', + field=models.ManyToManyField(blank=True, related_name='publications', to='journals.UnregisteredAuthor'), + ), + migrations.AlterField( + model_name='publication', + name='first_author', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='first_author_publications', to='scipost.Contributor'), + ), + migrations.AlterField( + model_name='publication', + name='first_author_unregistered', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='first_author_publications', to='journals.UnregisteredAuthor'), + ), + ] diff --git a/journals/models.py b/journals/models.py index 4dc87d39843fc4957040606eb819aaf8259f3712..613acad1ffe2089ef1b7a3c6b1e1f50dfe7b5dd2 100644 --- a/journals/models.py +++ b/journals/models.py @@ -131,23 +131,27 @@ class Publication(models.Model): blank=True, null=True) title = models.CharField(max_length=300) author_list = models.CharField(max_length=1000, verbose_name="author list") + # Authors which have been mapped to contributors: - authors = models.ManyToManyField(Contributor, blank=True, related_name='authors_pub') + authors = models.ManyToManyField('scipost.Contributor', blank=True, + related_name='publications') authors_unregistered = models.ManyToManyField(UnregisteredAuthor, blank=True, - related_name='authors_unregistered') - first_author = models.ForeignKey(Contributor, blank=True, null=True, on_delete=models.CASCADE) + related_name='publications') + first_author = models.ForeignKey('scipost.Contributor', blank=True, null=True, + on_delete=models.CASCADE, + related_name='first_author_publications') first_author_unregistered = models.ForeignKey(UnregisteredAuthor, blank=True, null=True, on_delete=models.CASCADE, - related_name='first_author_unregistered') - authors_claims = models.ManyToManyField(Contributor, blank=True, - related_name='authors_pub_claims') - authors_false_claims = models.ManyToManyField(Contributor, blank=True, - related_name='authors_pub_false_claims') + related_name='first_author_publications') + authors_claims = models.ManyToManyField('scipost.Contributor', blank=True, + related_name='claimed_publications') + authors_false_claims = models.ManyToManyField('scipost.Contributor', blank=True, + related_name='false_claimed_publications') abstract = models.TextField() pdf_file = models.FileField(upload_to='UPLOADS/PUBLICATIONS/%Y/%m/', max_length=200) cc_license = models.CharField(max_length=32, choices=CC_LICENSES, default=CCBY4) grants = models.ManyToManyField('funders.Grant', blank=True) - funders_generic = models.ManyToManyField('funders.Funder', blank=True) # not linked to a grant + funders_generic = models.ManyToManyField('funders.Funder', blank=True) # not linked to a grant metadata = JSONField(default={}, blank=True, null=True) metadata_xml = models.TextField(blank=True, null=True) # for Crossref deposit latest_metadata_update = models.DateTimeField(blank=True, null=True) diff --git a/notifications/templates/notifications/partials/notice.html b/notifications/templates/notifications/partials/notice.html index 411a96e0e9da56cc939b11ce65bf4a44c51e01b2..5ce4c87d0a529b780c99a5cb9a5113a1cb5497c2 100644 --- a/notifications/templates/notifications/partials/notice.html +++ b/notifications/templates/notifications/partials/notice.html @@ -3,9 +3,9 @@ <div class="actions"> <a href="{% url 'notifications:mark_toggle' notice.slug %}"> {% if notice.unread %} - <i class="fa fa-circle" data-toggle="tooltip" data-placement="auto" title="Mark as unread" aria-hidden="true"></i> + <i class="fa fa-circle" data-toggle="tooltip" data-placement="auto" title="Mark as read" aria-hidden="true"></i> {% else %} - <i class="fa fa-circle-o" data-toggle="tooltip" data-placement="auto" title="Mark as read" aria-hidden="true"></i> + <i class="fa fa-circle-o" data-toggle="tooltip" data-placement="auto" title="Mark as unread" aria-hidden="true"></i> {% endif %} </a> </div> diff --git a/notifications/views.py b/notifications/views.py index 38dd0329b46e900f6b569d04e50cbb586c2fb517..e309196bb51f93c2f465baad3b931219655118d6 100644 --- a/notifications/views.py +++ b/notifications/views.py @@ -49,6 +49,10 @@ def mark_all_as_read(request): if _next: return redirect(_next) + + if request.GET.get('json'): + return JsonResponse({'unread': 0}) + return redirect('notifications:all') diff --git a/partners/models.py b/partners/models.py index 1d7f39a882f9a0ba0d2aa4c2ae123cc1ccecbe33..ff95d07990170d04286329cc4d70a0c625203715 100644 --- a/partners/models.py +++ b/partners/models.py @@ -26,6 +26,7 @@ from .constants import PROSPECTIVE_PARTNER_EVENT_EMAIL_SENT,\ from .managers import MembershipAgreementManager, ProspectivePartnerManager, PartnerManager,\ ContactRequestManager, PartnersAttachmentManager +from .storage import SecureFileStorage from scipost.constants import TITLE_CHOICES from scipost.fields import ChoiceArrayField @@ -291,7 +292,8 @@ class PartnersAttachment(models.Model): An Attachment which can (in the future) be related to a Partner, Contact, MembershipAgreement, etc. """ - attachment = models.FileField(upload_to='UPLOADS/PARTNERS/ATTACHMENTS') + attachment = models.FileField(upload_to='UPLOADS/PARTNERS/ATTACHMENTS', + storage=SecureFileStorage()) name = models.CharField(max_length=128) agreement = models.ForeignKey('partners.MembershipAgreement', related_name='attachments', blank=True) diff --git a/partners/storage.py b/partners/storage.py new file mode 100644 index 0000000000000000000000000000000000000000..1821e4f48a785229435e4963d81ac33e881aea19 --- /dev/null +++ b/partners/storage.py @@ -0,0 +1,25 @@ +from django.conf import settings +from django.core.files.storage import FileSystemStorage +from django.utils.functional import cached_property + + +class SecureFileStorage(FileSystemStorage): + """ + Inherit default FileStorage system to prevent files from being publicly accessible + from an server location that is permitted to be opened without explicit permissions. + """ + @cached_property + def location(self): + """ + This method determines the storage location for a new file. To secure the file from + 'the public', we'll store it outside the publicly accessible MEDIA_ROOT folder. + + This also means you need to explicitly handle the file reading/opening! + """ + if hasattr(settings, 'MEDIA_ROOT_SECURE'): + return self._value_or_setting(self._location, settings.MEDIA_ROOT_SECURE) + return super().location + + @cached_property + def base_url(self): + return settings.MEDIA_URL_SECURE diff --git a/partners/views.py b/partners/views.py index 04a4c5f1ef0615d1310fb3d420d3a764aad27fe6..1a94818df5390b13a87b613425656fcfd2f2b4be 100644 --- a/partners/views.py +++ b/partners/views.py @@ -1,8 +1,10 @@ +import mimetypes + from django.contrib import messages from django.contrib.auth.decorators import login_required from django.db import transaction from django.forms import modelformset_factory -from django.http import HttpResponse +from django.http import FileResponse, HttpResponse from django.shortcuts import get_object_or_404, render, reverse, redirect from django.utils import timezone @@ -95,7 +97,7 @@ def membership_request(request): return render(request, 'partners/membership_request.html', context) -@permission_required('scipost.can_promote_prospect_to_partner', return_403=True) +@permission_required('scipost.can_promote_to_production_team', return_403=True) @transaction.atomic def promote_prospartner(request, prospartner_id): prospartner = get_object_or_404(ProspectivePartner.objects.not_yet_partner(), @@ -386,7 +388,11 @@ def agreement_details(request, agreement_id): def agreement_attachments(request, agreement_id, attachment_id): attachment = get_object_or_404(PartnersAttachment.objects.my_attachments(request.user), agreement__id=agreement_id, id=attachment_id) - response = HttpResponse(attachment.attachment.read(), content_type='application/pdf') + + content_type, encoding = mimetypes.guess_type(attachment.attachment.path) + content_type = content_type or 'application/octet-stream' + response = HttpResponse(attachment.attachment.read(), content_type=content_type) + response["Content-Encoding"] = encoding response['Content-Disposition'] = ('filename=%s' % attachment.name) return response diff --git a/production/admin.py b/production/admin.py index 7969db8ec5fb3bbc2427fc3751b86fc871763e62..dc5cbd236c21019ed1fb12ece7a8a9d13a89b09e 100644 --- a/production/admin.py +++ b/production/admin.py @@ -1,10 +1,18 @@ from django.contrib import admin -from .models import ProductionStream, ProductionEvent +from guardian.admin import GuardedModelAdmin + +from .models import ProductionStream, ProductionEvent, ProductionUser def event_count(obj): - return obj.productionevent_set.count() + return obj.events.count() + + +class ProductionUserInline(admin.StackedInline): + model = ProductionUser + extra = 0 + min_num = 0 class ProductionEventInline(admin.TabularInline): @@ -13,7 +21,7 @@ class ProductionEventInline(admin.TabularInline): readonly_fields = () -class ProductionStreamAdmin(admin.ModelAdmin): +class ProductionStreamAdmin(GuardedModelAdmin): search_fields = ['submission'] list_filter = ['status'] list_display = ['submission', 'opened', 'status', event_count] @@ -22,4 +30,5 @@ class ProductionStreamAdmin(admin.ModelAdmin): ) +admin.site.register(ProductionUser) admin.site.register(ProductionStream, ProductionStreamAdmin) diff --git a/production/apps.py b/production/apps.py index 549c6bd6bb6fac43a2edebae72c2e580cb66bb11..d633dcd69d8e1462ea12d64fa04cf8492c2dafe9 100644 --- a/production/apps.py +++ b/production/apps.py @@ -8,7 +8,7 @@ class ProductionConfig(AppConfig): def ready(self): super().ready() - from .models import ProductionStream, ProductionEvent - from .signals import notify_new_stream, notify_new_event - post_save.connect(notify_new_stream, sender=ProductionStream) + from .models import ProductionEvent, ProductionStream + from .signals import notify_new_event, notify_new_stream post_save.connect(notify_new_event, sender=ProductionEvent) + post_save.connect(notify_new_stream, sender=ProductionStream) diff --git a/production/forms.py b/production/forms.py index fec37e4b4ba232b9432bfc52c173229c640c709a..4c284c839e6bd44202ee6d78d5479515211f1c2a 100644 --- a/production/forms.py +++ b/production/forms.py @@ -1,6 +1,13 @@ +import datetime + from django import forms +from django.contrib.auth import get_user_model +from django.utils.dates import MONTHS +from django.db.models import Sum + +from .models import ProductionUser, ProductionStream, ProductionEvent -from .models import ProductionEvent +today = datetime.datetime.today() class ProductionEventForm(forms.ModelForm): @@ -15,3 +22,73 @@ class ProductionEventForm(forms.ModelForm): 'comments': forms.Textarea(attrs={'rows': 4}), 'duration': forms.TextInput(attrs={'placeholder': 'HH:MM:SS'}) } + + +class AssignOfficerForm(forms.ModelForm): + class Meta: + model = ProductionStream + fields = ('officer',) + + +class AssignSupervisorForm(forms.ModelForm): + class Meta: + model = ProductionStream + fields = ('supervisor',) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['supervisor'].queryset = self.fields['supervisor'].queryset.filter( + user__groups__name='Production Supervisor') + + +class UserToOfficerForm(forms.ModelForm): + class Meta: + model = ProductionUser + fields = ( + 'user', + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['user'].queryset = self.fields['user'].queryset.filter( + production_user__isnull=True).order_by('last_name') + + +class ProductionUserMonthlyActiveFilter(forms.Form): + month = forms.ChoiceField(choices=[(k, v) for k, v in MONTHS.items()]) + year = forms.ChoiceField(choices=[(y, y) for y in reversed(range(today.year-6, today.year+1))]) + + def __init__(self, *args, **kwargs): + if not kwargs.get('data', False) and not args[0]: + args = list(args) + args[0] = { + 'month': today.month, + 'year': today.year + } + args = tuple(args) + kwargs['initial'] = { + 'month': today.month, + 'year': today.year + } + super().__init__(*args, **kwargs) + + def get_totals(self): + # Make accessible without need to explicitly check validity of form. + self.is_valid() + + users = ProductionUser.objects.filter(events__duration__isnull=False, + events__noted_on__month=self.cleaned_data['month'], + events__noted_on__year=self.cleaned_data['year'] + ).distinct() + output = [] + for user in users: + events = user.events.filter(duration__isnull=False, + noted_on__month=self.cleaned_data['month'], + noted_on__year=self.cleaned_data['year']) + output.append({ + 'events': events, + 'duration': events.aggregate(total=Sum('duration')), + 'user': user + }) + + return output diff --git a/production/managers.py b/production/managers.py index ca9b5d0ba99fb56cdce96d96963337445f27d16e..1980044f4840ed438519832433c5603dbe9fa012 100644 --- a/production/managers.py +++ b/production/managers.py @@ -3,14 +3,17 @@ from django.db import models from .constants import PRODUCTION_STREAM_COMPLETED, PRODUCTION_STREAM_ONGOING -class ProductionStreamManager(models.Manager): +class ProductionStreamQuerySet(models.QuerySet): def completed(self): return self.filter(status=PRODUCTION_STREAM_COMPLETED) def ongoing(self): return self.filter(status=PRODUCTION_STREAM_ONGOING) + def filter_for_user(self, production_user): + return self.filter(officer=production_user) + class ProductionEventManager(models.Manager): - def get_my_events(self, current_contributor): - return self.filter(noted_by=current_contributor) + def get_my_events(self, production_user): + return self.filter(noted_by=production_user) diff --git a/production/migrations/0011_productionuser.py b/production/migrations/0011_productionuser.py new file mode 100644 index 0000000000000000000000000000000000000000..ed1ec512c86ec9a248b976035bc7448adc1ca8e2 --- /dev/null +++ b/production/migrations/0011_productionuser.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-14 17:07 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('production', '0010_auto_20170707_0600'), + ] + + operations = [ + migrations.CreateModel( + name='ProductionUser', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/production/migrations/0012_productionstream_officers.py b/production/migrations/0012_productionstream_officers.py new file mode 100644 index 0000000000000000000000000000000000000000..37835f8f2b46f2c8f4b02aa52b031145ca3cc27a --- /dev/null +++ b/production/migrations/0012_productionstream_officers.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-14 18:20 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('production', '0011_productionuser'), + ] + + operations = [ + migrations.AddField( + model_name='productionstream', + name='officers', + field=models.ManyToManyField(blank=True, to='production.ProductionUser'), + ), + ] diff --git a/production/migrations/0013_auto_20170914_2220.py b/production/migrations/0013_auto_20170914_2220.py new file mode 100644 index 0000000000000000000000000000000000000000..8eaa1babf34e39c504ff3de4ad6ee7f6a0a7c3d8 --- /dev/null +++ b/production/migrations/0013_auto_20170914_2220.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-14 20:20 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('production', '0012_productionstream_officers'), + ] + + operations = [ + migrations.RenameField( + model_name='productionevent', + old_name='noted_by', + new_name='noted_by_contributor', + ), + migrations.AlterField( + model_name='productionstream', + name='officers', + field=models.ManyToManyField(blank=True, related_name='streams', to='production.ProductionUser'), + ), + migrations.AlterField( + model_name='productionuser', + name='user', + field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='production_user', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/production/migrations/0014_productionevent_noted_by.py b/production/migrations/0014_productionevent_noted_by.py new file mode 100644 index 0000000000000000000000000000000000000000..1f728d34daf03dce9adce677fb976e2d91949bc8 --- /dev/null +++ b/production/migrations/0014_productionevent_noted_by.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-14 20:20 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('production', '0013_auto_20170914_2220'), + ] + + operations = [ + migrations.AddField( + model_name='productionevent', + name='noted_by', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='production.ProductionUser'), + preserve_default=False, + ), + ] diff --git a/production/migrations/0015_auto_20170914_2237.py b/production/migrations/0015_auto_20170914_2237.py new file mode 100644 index 0000000000000000000000000000000000000000..cb9ffcc83c0d5eb059cebe7d315cbc44ba31dc36 --- /dev/null +++ b/production/migrations/0015_auto_20170914_2237.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-14 20:37 +from __future__ import unicode_literals + +from django.contrib.auth.models import Group, User +from django.db import migrations + + +def contributor_to_officer(apps, schema_editor): + # Create ProductionUser for all current Officers + ProductionUser = apps.get_model('production', 'ProductionUser') + officers = Group.objects.get(name='Production Officers') + for user in officers.user_set.all(): + ProductionUser.objects.get_or_create(user_id=user.id) + print('\n - Production Officers transfered to ProductionUser') + + # Transfer all Events + ProductionEvent = apps.get_model('production', 'ProductionEvent') + for event in ProductionEvent.objects.all(): + user = User.objects.get(contributor__id=event.noted_by_contributor.id, production_user__isnull=False) + event.noted_by.id = user.production_user.id + event.save() + print(' - ProductionEvents updated') + + return + + +def officer_to_contributor(apps, schema_editor): + # Transfer all Events + ProductionEvent = apps.get_model('production', 'ProductionEvent') + for event in ProductionEvent.objects.all(): + user = User.objects.get(production_user_id=event.noted_by.id) + event.noted_by_contributor.id = user.contributor.id + event.save() + print('\n - ProductionEvents updated') + + return + + +class Migration(migrations.Migration): + + dependencies = [ + ('production', '0014_productionevent_noted_by'), + ] + + operations = [ + migrations.RunPython(contributor_to_officer, officer_to_contributor) + ] diff --git a/production/migrations/0016_remove_productionevent_noted_by_contributor.py b/production/migrations/0016_remove_productionevent_noted_by_contributor.py new file mode 100644 index 0000000000000000000000000000000000000000..d204f883e3d7bdbd7fe5a7cb4653a5cc0fde8501 --- /dev/null +++ b/production/migrations/0016_remove_productionevent_noted_by_contributor.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-14 20:46 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('production', '0015_auto_20170914_2237'), + ] + + operations = [ + migrations.AlterField( + model_name='productionevent', + name='noted_by_contributor', + field=models.ForeignKey(default=1, null=True, on_delete=django.db.models.deletion.CASCADE, to='scipost.Contributor'), + ), + migrations.RemoveField( + model_name='productionevent', + name='noted_by_contributor', + ), + ] diff --git a/production/migrations/0017_auto_20170914_2319.py b/production/migrations/0017_auto_20170914_2319.py new file mode 100644 index 0000000000000000000000000000000000000000..95e2983d46fd0db386be12b5415cfd05520879ba --- /dev/null +++ b/production/migrations/0017_auto_20170914_2319.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-14 21:19 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('production', '0016_remove_productionevent_noted_by_contributor'), + ] + + operations = [ + migrations.AlterField( + model_name='productionevent', + name='stream', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='events', to='production.ProductionStream'), + ), + ] diff --git a/production/migrations/0018_auto_20170929_2234.py b/production/migrations/0018_auto_20170929_2234.py new file mode 100644 index 0000000000000000000000000000000000000000..335424b7a30b80facd8a09e460e363f362afbf69 --- /dev/null +++ b/production/migrations/0018_auto_20170929_2234.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-29 20:34 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('production', '0017_auto_20170914_2319'), + ] + + operations = [ + migrations.RemoveField( + model_name='productionstream', + name='officers', + ), + migrations.AddField( + model_name='productionstream', + name='officer', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='streams', to='production.ProductionUser'), + ), + migrations.AddField( + model_name='productionstream', + name='supervisor', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='supervised_streams', to='production.ProductionUser'), + ), + ] diff --git a/production/migrations/0019_auto_20170929_2310.py b/production/migrations/0019_auto_20170929_2310.py new file mode 100644 index 0000000000000000000000000000000000000000..74565042d4b6788264ef4a0a49643d2ed0105aaf --- /dev/null +++ b/production/migrations/0019_auto_20170929_2310.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-29 21:10 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('production', '0018_auto_20170929_2234'), + ] + + operations = [ + migrations.AlterModelOptions( + name='productionstream', + options={'permissions': (('can_perform_supervisory_actions', 'Can perform supervisory actions'),)}, + ), + ] diff --git a/production/migrations/0020_auto_20170930_0156.py b/production/migrations/0020_auto_20170930_0156.py new file mode 100644 index 0000000000000000000000000000000000000000..305d366b3f494fe473189539b92ec9c49c0b0bca --- /dev/null +++ b/production/migrations/0020_auto_20170930_0156.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-29 23:56 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('production', '0019_auto_20170929_2310'), + ] + + operations = [ + migrations.AlterModelOptions( + name='productionstream', + options={'permissions': (('can_work_for_stream', 'Can work for stream'), ('can_perform_supervisory_actions', 'Can perform supervisory actions'))}, + ), + migrations.AlterField( + model_name='productionevent', + name='noted_by', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='events', to='production.ProductionUser'), + ), + ] diff --git a/production/models.py b/production/models.py index 3d94ddaa295504fbb05aa1224937ee7436f2093b..17f2d73c427e11c0ebfa04d2f3d77a7fca235400 100644 --- a/production/models.py +++ b/production/models.py @@ -1,9 +1,24 @@ from django.db import models -from django.utils import timezone from django.core.urlresolvers import reverse +from django.contrib.auth.models import User +from django.utils import timezone from .constants import PRODUCTION_STREAM_STATUS, PRODUCTION_STREAM_ONGOING, PRODUCTION_EVENTS -from .managers import ProductionStreamManager, ProductionEventManager +from .managers import ProductionStreamQuerySet, ProductionEventManager + + +class ProductionUser(models.Model): + """ + Production Officers will have a ProductionUser object related to their account + to relate all production related actions to. + """ + user = models.OneToOneField(User, on_delete=models.PROTECT, unique=True, + related_name='production_user') + + # objects = ProductionUserQuerySet.as_manager() -- Not implemented yet + + def __str__(self): + return '%s, %s' % (self.user.last_name, self.user.first_name) class ProductionStream(models.Model): @@ -12,8 +27,18 @@ class ProductionStream(models.Model): closed = models.DateTimeField(default=timezone.now) status = models.CharField(max_length=32, choices=PRODUCTION_STREAM_STATUS, default=PRODUCTION_STREAM_ONGOING) + officer = models.ForeignKey('production.ProductionUser', blank=True, null=True, + related_name='streams') + supervisor = models.ForeignKey('production.ProductionUser', blank=True, null=True, + related_name='supervised_streams') - objects = ProductionStreamManager() + objects = ProductionStreamQuerySet.as_manager() + + class Meta: + permissions = ( + ('can_work_for_stream', 'Can work for stream'), + ('can_perform_supervisory_actions', 'Can perform supervisory actions'), + ) def __str__(self): return '{arxiv}, {title}'.format(arxiv=self.submission.arxiv_identifier_w_vn_nr, @@ -25,16 +50,17 @@ class ProductionStream(models.Model): return reverse('production:completed') + '#stream_' + str(self.id) def total_duration(self): - totdur = self.productionevent_set.aggregate(models.Sum('duration')) + totdur = self.events.aggregate(models.Sum('duration')) return totdur['duration__sum'] class ProductionEvent(models.Model): - stream = models.ForeignKey(ProductionStream, on_delete=models.CASCADE) + stream = models.ForeignKey(ProductionStream, on_delete=models.CASCADE, related_name='events') event = models.CharField(max_length=64, choices=PRODUCTION_EVENTS) comments = models.TextField(blank=True, null=True) noted_on = models.DateTimeField(default=timezone.now) - noted_by = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE) + noted_by = models.ForeignKey('production.ProductionUser', on_delete=models.CASCADE, + related_name='events') duration = models.DurationField(blank=True, null=True) objects = ProductionEventManager() diff --git a/production/permissions.py b/production/permissions.py new file mode 100644 index 0000000000000000000000000000000000000000..54012de7d339d7367a33dc9e62187e45402fa1d1 --- /dev/null +++ b/production/permissions.py @@ -0,0 +1,11 @@ +from django.contrib.auth.decorators import user_passes_test + + +def is_production_user(): + """Requires user to be a ProductionUser.""" + def test(u): + if u.is_authenticated(): + if hasattr(u, 'production_user') and u.production_user: + return True + return False + return user_passes_test(test) diff --git a/production/signals.py b/production/signals.py index 6a3102f9249f4a5a71f1a9603974a7590b1b4a31..2d17c3a331cd97ce5d2547cd909b5188a472ee6e 100644 --- a/production/signals.py +++ b/production/signals.py @@ -1,17 +1,27 @@ -from django.contrib.auth.models import Group, User +from django.contrib.auth.models import Group from notifications.signals import notify def notify_new_stream(sender, instance, created, **kwargs): """ - Notify the production team about a new Production Stream created. + Notify the production supervisors about a new Production Stream that is created. """ if created: - production_officers = User.objects.filter(groups__name='Production Officers') editorial_college = Group.objects.get(name='Editorial College') - for user in production_officers: - notify.send(sender=sender, recipient=user, actor=editorial_college, verb=' accepted a submission. A new productionstream has been started.', target=instance) + supervisors = Group.objects.get(name='Production Supervisor') + for recipient in supervisors.user_set.all(): + notify.send(sender=sender, recipient=recipient, actor=editorial_college, + verb=' accepted a Submission. A new Production Stream has started.', + target=instance) + + +def notify_new_stream_assignment(sender, instance, recipient, **kwargs): + """ + Notify a production officer about its new Production Stream assignment. + """ + notify.send(sender=sender, recipient=recipient, actor=sender, + verb=' assigned you to a Production Stream.', target=instance) def notify_new_event(sender, instance, created, **kwargs): @@ -19,15 +29,26 @@ def notify_new_event(sender, instance, created, **kwargs): Notify the production team about a new Production Event created. """ if created: - production_officers = User.objects.filter(groups__name='Production Officers') - for user in production_officers: - notify.send(sender=sender, recipient=user, actor=instance.noted_by.user, verb=' created a new Production Event ', target=instance) + stream = instance.stream + + if stream.officer != instance.noted_by: + notify.send(sender=sender, recipient=stream.officer.user, + actor=instance.noted_by.user, + verb=' created a new Production Event.', target=instance) + + if stream.supervisor != instance.noted_by: + notify.send(sender=sender, recipient=stream.supervisor.user, + actor=instance.noted_by.user, + verb=' created a new Production Event.', target=instance) def notify_stream_completed(sender, instance, **kwargs): """ Notify the production team about a Production Stream being completed. """ - production_officers = User.objects.filter(groups__name='Production Officers') - for user in production_officers: - notify.send(sender=sender, recipient=user, actor=sender, verb=' marked Production Stream as completed.', target=instance) + stream = instance.stream + notify.send(sender=sender, recipient=stream.officer.user, + actor=sender, verb=' marked Production Stream as completed.', target=instance) + + notify.send(sender=sender, recipient=stream.supervisor.user, + actor=sender, verb=' marked Production Stream as completed.', target=instance) diff --git a/production/templates/production/_production_stream_card.html b/production/templates/production/_production_stream_card.html deleted file mode 100644 index 5e3f387e06cfe43c5626ff9496a9ac60a505cc87..0000000000000000000000000000000000000000 --- a/production/templates/production/_production_stream_card.html +++ /dev/null @@ -1,32 +0,0 @@ -{% load bootstrap %} -{% load scipost_extras %} - -<div class="w-100" id="stream_{{stream.id}}"> - {% include 'submissions/_submission_card_content_sparse.html' with submission=stream.submission %} -</div> -<div class="card-body"> - <div class="row"> - <div class="{% if form %}col-lg-7{% else %}col-12{% endif %}"> - <h3>Events</h3> - {% include 'production/_production_events.html' with events=stream.productionevent_set.all %} - <br/> - - {% if stream.total_duration %} - <h3>Total duration for this stream: {{ stream.total_duration|duration }}</h3> - {% endif %} - {% if perms.scipost.can_publish_accepted_submission %} - <h3><a href="{% url 'production:mark_as_completed' stream_id=stream.id %}">Mark this stream as completed</a></h3> - {% endif %} - </div> - {% if form %} - <div class="col-lg-5 mt-4 mt-lg-0"> - <h3>Add an event to this production stream:</h3> - <form action="{% url 'production:add_event' stream_id=stream.id %}" method="post"> - {% csrf_token %} - {{ form|bootstrap }} - <input type="submit" class="btn btn-secondary" name="submit" value="Submit"> - </form> - </div> - {% endif %} - </div> -</div> diff --git a/production/templates/production/base.html b/production/templates/production/base.html new file mode 100644 index 0000000000000000000000000000000000000000..9dbe1e96d72f246f9e72efd84824244d9c2f11f5 --- /dev/null +++ b/production/templates/production/base.html @@ -0,0 +1,13 @@ +{% extends 'scipost/base.html' %} + +{% block breadcrumb %} + <nav class="breadcrumb py-md-2 px-0 hidden-sm-down"> + <div class="container"> + {% block breadcrumb_items %} + <a href="{% url 'production:production' %}" class="breadcrumb-item">Production page</a> + {% endblock %} + </div> + </nav> +{% endblock %} + +{% block container_class %}{{block.super}} pb-5{% endblock container_class %} diff --git a/production/templates/production/completed.html b/production/templates/production/completed.html index 2e6fb2911d22a8c576c8e12d2b91b4619e93bf10..f5a80521b88400faa419fd7edbacbfe484066f03 100644 --- a/production/templates/production/completed.html +++ b/production/templates/production/completed.html @@ -1,4 +1,4 @@ -{% extends 'scipost/_personal_page_base.html' %} +{% extends 'production/base.html' %} {% block breadcrumb_items %} {{block.super}} @@ -21,11 +21,7 @@ <ul class="list-group list-group-flush"> {% for stream in streams %} <li class="list-group-item"> - <div class="w-100" id="stream_{{stream.id}}">{% include 'submissions/_submission_card_content_sparse.html' with submission=stream.submission %}</div> - <div class="card-body"> - <h3>Events</h3> - {% include 'production/_production_events.html' with events=stream.productionevent_set.all %} - </div> + {% include 'production/partials/production_stream_card_completed.html' with stream=stream %} </li> {% empty %} <li class="list-group-item">No completed production streams found.</li> diff --git a/production/templates/production/_production_events.html b/production/templates/production/partials/production_events.html similarity index 73% rename from production/templates/production/_production_events.html rename to production/templates/production/partials/production_events.html index e7d168b13c52553b541c78e7768c03fb95d45049..4db2ab632d6bd8355d34b79b84577c94588651dc 100644 --- a/production/templates/production/_production_events.html +++ b/production/templates/production/partials/production_events.html @@ -4,10 +4,12 @@ {% for event in events %} <li id="event_{{ event.id }}"> <p class="mb-0 font-weight-bold">{{ event.get_event_display }} - {% if event.noted_by == request.user.contributor %} + {% if not non_editable %} + {% if event.noted_by == request.user.production_user %} · <a href="{% url 'production:update_event' event.id %}">Edit</a> · <a class="text-danger" href="{% url 'production:delete_event' event.id %}">Delete</a> {% endif %} + {% endif %} </p> <p class="text-muted mb-1">noted {{ event.noted_on }} by {{ event.noted_by }}</p> {% if event.duration %} @@ -21,3 +23,8 @@ <li>No events were found.</li> {% endfor %} </ul> + +{% if stream.total_duration %} + <hr class="sm"> + <p class="pl-4 ml-3">Total duration for this stream: <strong>{{ stream.total_duration|duration }}</strong></p> +{% endif %} diff --git a/production/templates/production/partials/production_stream_card.html b/production/templates/production/partials/production_stream_card.html new file mode 100644 index 0000000000000000000000000000000000000000..f1f88a5687e19c1fa867ca46cb2a9ab95de00372 --- /dev/null +++ b/production/templates/production/partials/production_stream_card.html @@ -0,0 +1,67 @@ +{% extends 'production/partials/production_stream_card_completed.html' %} + +{% load bootstrap %} +{% load guardian_tags %} + +{% get_obj_perms request.user for stream as "sub_perms" %} + +{% block actions %} + <h3>Events</h3> + {% include 'production/partials/production_events.html' with events=stream.events.all %} + + {% if perms.scipost.can_publish_accepted_submission or perms.scipost.can_assign_production_supervisor or "can_perform_supervisory_actions" in sub_perms %} + <h3>Actions</h3> + <ul> + {% if perms.scipost.can_assign_production_supervisor and assign_supervisor_form %} + <li> + <a href="javascript:;" data-toggle="toggle" data-target="#add_supervisor_{{stream.id}}">Assign Production Supervisor to this stream</a> + <div id="add_supervisor_{{stream.id}}" style="display: none;"> + <form class="my-3" action="{% url 'production:add_supervisor' stream_id=stream.id %}" method="post"> + {% csrf_token %} + {{ assign_supervisor_form|bootstrap_inline }} + <input type="submit" class="btn btn-outline-primary" name="submit" value="Add officer"> + </form> + </div> + </li> + {% endif %} + {% if "can_perform_supervisory_actions" in sub_perms and assign_officer_form %} + <li> + <a href="javascript:;" data-toggle="toggle" data-target="#add_officer_{{stream.id}}">Assign Production Officer to this stream</a> + <div id="add_officer_{{stream.id}}" style="display: none;"> + <form class="my-3" action="{% url 'production:add_officer' stream_id=stream.id %}" method="post"> + {% csrf_token %} + {{ assign_officer_form|bootstrap_inline }} + <input type="submit" class="btn btn-outline-primary" name="submit" value="Add officer"> + </form> + </div> + </li> + {% endif %} + {% if perms.scipost.can_publish_accepted_submission %} + <li><a href="{% url 'production:mark_as_completed' stream_id=stream.id %}">Mark this stream as completed</a></li> + {% endif %} + </ul> + {% endif %} +{% endblock %} + +{% block officers %} + <li>Production Supervisor: + {% if stream.supervisor %} + <strong>{{ stream.supervisor }}</strong> + {% if perms.scipost.can_assign_production_supervisor %} + · <a href="{% url 'production:remove_supervisor' stream_id=stream.id officer_id=stream.supervisor.id %}" class="text-danger">Remove from stream</a> + {% endif %} + {% else %} + <em>No Supervisor assigned yet.</em> + {% endif %} + </li> + <li>Production Officer: + {% if stream.officer %} + <strong>{{ stream.officer }}</strong> + {% if "can_perform_supervisory_actions" in sub_perms %} + · <a href="{% url 'production:remove_officer' stream_id=stream.id officer_id=stream.officer.id %}" class="text-danger">Remove from stream</a> + {% endif %} + {% else %} + <em>No Officer assigned yet.</em> + {% endif %} + </li> +{% endblock %} diff --git a/production/templates/production/partials/production_stream_card_completed.html b/production/templates/production/partials/production_stream_card_completed.html new file mode 100644 index 0000000000000000000000000000000000000000..f31b37b1a2d379c05a24c49019c45c0fc7f81d06 --- /dev/null +++ b/production/templates/production/partials/production_stream_card_completed.html @@ -0,0 +1,49 @@ +{% load bootstrap %} +{% load guardian_tags %} + +{% get_obj_perms request.user for stream as "sub_perms" %} + +<div class="w-100" id="stream_{{stream.id}}"> + {% include 'submissions/_submission_card_content_sparse.html' with submission=stream.submission %} +</div> +<div class="card-body"> + <div class="row"> + <div class="{% if prodevent_form %}col-lg-7{% else %}col-12{% endif %}"> + <h3>Officers</h3> + <ul> + {% block officers %} + <li>Production Supervisor: + {% if stream.supervisor %} + <strong>{{ stream.supervisor }}</strong> + {% else %} + <em>No Supervisor assigned.</em> + {% endif %} + </li> + <li>Production Officer: + {% if stream.officer %} + <strong>{{ stream.officer }}</strong> + {% else %} + <em>No Officer assigned.</em> + {% endif %} + </li> + {% endblock %} + </ul> + + {% block actions %} + <h3>Events</h3> + {% include 'production/partials/production_events.html' with events=stream.events.all non_editable=1 %} + {% endblock %} + + </div> + {% if prodevent_form and "can_work_for_stream" in sub_perms %} + <div class="col-lg-5 mt-4 mt-lg-0"> + <h3>Add an event to this production stream:</h3> + <form action="{% url 'production:add_event' stream_id=stream.id %}" method="post"> + {% csrf_token %} + {{ prodevent_form|bootstrap }} + <input type="submit" class="btn btn-secondary" name="submit" value="Submit"> + </form> + </div> + {% endif %} + </div> +</div> diff --git a/production/templates/production/_production_timesheet_card.html b/production/templates/production/partials/production_timesheet_card.html similarity index 96% rename from production/templates/production/_production_timesheet_card.html rename to production/templates/production/partials/production_timesheet_card.html index 7feed2713930a1cd5a94419470b4580d73e9c933..462d0b2eb08b1dd2677c645fc80472db16a01c32 100644 --- a/production/templates/production/_production_timesheet_card.html +++ b/production/templates/production/partials/production_timesheet_card.html @@ -1,5 +1,3 @@ -{% load bootstrap %} - <div class="card-body"> <table class="table mb-5"> <thead class="thead-default"> diff --git a/production/templates/production/production.html b/production/templates/production/production.html index c748477f0602c7fa9752847ad328fc7915bb4869..f9a693d6e9f132a36a469f1e2e70d013c37e4f63 100644 --- a/production/templates/production/production.html +++ b/production/templates/production/production.html @@ -1,7 +1,6 @@ -{% extends 'scipost/_personal_page_base.html' %} +{% extends 'production/base.html' %} {% block breadcrumb_items %} - {{block.super}} <span class="breadcrumb-item">Production page</span> {% endblock %} @@ -24,16 +23,16 @@ <div class="tab-nav-inner"> <ul class="nav btn-group personal-page-nav" role="tablist"> <li class="nav-item btn btn-secondary"> - <a href="#streams" class="nav-link active" data-toggle="tab">Streams</a> + <a href="#streams" class="nav-link active" data-toggle="tab">{{ perms.scipost.can_assign_production_officer|yesno:"Streams,My Streams" }}</a> </li> <li class="nav-item btn btn-secondary"> <a href="#mytimesheet" class="nav-link" data-toggle="tab">My Timesheet</a> </li> - {% if perms.scipost.can_view_timesheets %} - <li class="nav-item btn btn-secondary"> - <a href="#teamtimesheets" class="nav-link" data-toggle="tab">Team Timesheets</a> - </li> - {% endif %} + {% if perms.scipost.can_promote_user_to_production_officer %} + <li class="nav-item btn btn-secondary"> + <a href="#officers" class="nav-link" data-toggle="tab">Production Team</a> + </li> + {% endif %} </ul> </div> </div> @@ -45,7 +44,8 @@ <div class="tab-pane active" id="streams" role="tabpanel"> <div class="row"> <div class="col-12"> - <h2 class="highlight">Streams</h2> + <h2 class="highlight">{{ perms.scipost.can_assign_production_officer|yesno:"Streams,My Streams" }}</h2> + <a href="{% url 'production:completed' %}">View completed streams</a> </div> </div> @@ -61,24 +61,24 @@ </thead> <tbody id="accordion" role="tablist" aria-multiselectable="true"> - {% for stream in streams %} - <tr data-toggle="collapse" data-parent="#accordion" href="#collapse{{ stream.id }}" aria-expanded="true" aria-controls="collapse{{ stream.id }}" style="cursor: pointer;"> - <td>{{ stream.submission.author_list }}</td> - <td>{{ stream.submission.title }}</td> - <td>{{ stream.submission.acceptance_date|date:"Y-m-d" }}</td> - <td>{{ stream.productionevent_set.last.get_event_display }}</td> - <td>{{ stream.productionevent_set.last.noted_on }}</td> - </tr> - <tr id="collapse{{ stream.id }}" class="collapse" role="tabpanel" aria-labelledby="heading{{ stream.id }}" style="background-color: #fff;"> - <td colspan="5"> - {% include 'production/_production_stream_card.html' with stream=stream form=prodevent_form %} - </td> - </tr> - {% empty %} - <tr> - <td colspan="5">No production streams found.</td> - </tr> - {% endfor %} + {% for stream in streams %} + <tr data-toggle="collapse" data-parent="#accordion" href="#collapse{{ stream.id }}" aria-expanded="true" aria-controls="collapse{{ stream.id }}" style="cursor: pointer;"> + <td>{{ stream.submission.author_list }}</td> + <td>{{ stream.submission.title }}</td> + <td>{{ stream.submission.acceptance_date|date:"Y-m-d" }}</td> + <td>{{ stream.events.last.get_event_display }}</td> + <td>{{ stream.events.last.noted_on }}</td> + </tr> + <tr id="collapse{{ stream.id }}" class="collapse" role="tabpanel" aria-labelledby="heading{{ stream.id }}" style="background-color: #fff;"> + <td colspan="5"> + {% include 'production/partials/production_stream_card.html' with stream=stream prodevent_form=prodevent_form assignment_form=assignment_form %} + </td> + </tr> + {% empty %} + <tr> + <td colspan="5">No production streams found.</td> + </tr> + {% endfor %} </tbody> </table> </div> @@ -87,7 +87,10 @@ <div class="tab-pane" id="mytimesheet" role="tabpanel"> <div class="row"> <div class="col-12"> - <h2 class="highlight">My Timesheet</h2> + <h2 class="highlight">My Timesheet</h2> + {% if perms.scipost.can_view_timesheets %} + <a href="{% url 'finances:timesheets' %}">See team timesheets</a> + {% endif %} </div> </div> @@ -118,40 +121,25 @@ </table> </div> - {% if perms.scipost.can_view_timesheets %} - <div class="tab-pane" id="teamtimesheets" role="tabpanel"> - <div class="row"> - <div class="col-12"> - <h2 class="highlight">Team Timesheets</h2> + {% if perms.scipost.can_promote_user_to_production_officer %} + <div class="tab-pane" id="officers" role="tabpanel"> + <h2 class="highlight">Production Tream</h2> + <h3>Current Production Team</h3> + <ul> + {% for officer in production_officers %} + <li>{{ officer }}</li> + {% endfor %} + </ul> + + {% if new_officer_form %} + <h3>Promote user to Production Officer</h3> + <form action="{% url 'production:user_to_officer' %}" method="post"> + {% csrf_token %} + {{ new_officer_form|bootstrap }} + <input type="submit" class="btn btn-primary" value="Promote to Production Officer"> + </form> + {% endif %} </div> - </div> - - <table class="table table-hover mb-5"> - <thead class="thead-default"> - <tr> - <th>Name</th> - </tr> - </thead> - - <tbody id="accordion" role="tablist" aria-multiselectable="true"> - {% for member in production_team.all %} - <tr data-toggle="collapse" data-parent="#accordion" href="#collapse{{ member.id }}" aria-expanded="true" aria-controls="collapse{{ member.id }}" style="cursor: pointer;"> - <td>{{ member }}</td> - </tr> - <tr id="collapse{{ member.id }}" class="collapse" role="tabpanel" aria-labelledby="heading{{ member.id }}" style="background-color: #fff;"> - <td> - {% include 'production/_production_timesheet_card.html' with events=member.productionevent_set.all %} - </td> - </tr> - {% empty %} - <tr> - <td>No Team Member found.</td> - </tr> - {% endfor %} - </tbody> - </table> - - </div> {% endif %} </div> diff --git a/production/templates/production/productionevent_confirm_delete.html b/production/templates/production/productionevent_confirm_delete.html index 75feabb00b889ffabc5af687a2040082ae57002a..c6cd4c05a4da812c0739cc41bab2f72103b4f4ae 100644 --- a/production/templates/production/productionevent_confirm_delete.html +++ b/production/templates/production/productionevent_confirm_delete.html @@ -1,8 +1,7 @@ -{% extends 'scipost/_personal_page_base.html' %} +{% extends 'production/base.html' %} {% block breadcrumb_items %} {{block.super}} - <a href="{{object.get_absolute_url}}" class="breadcrumb-item">Production streams</a> <span class="breadcrumb-item">Delete production event</span> {% endblock %} diff --git a/production/templates/production/productionevent_form.html b/production/templates/production/productionevent_form.html index fc90e04c976d7d7315c0ee5f2e590cabf349d448..d520f1c75c2a200a9657e62dec024527fe0d1055 100644 --- a/production/templates/production/productionevent_form.html +++ b/production/templates/production/productionevent_form.html @@ -1,8 +1,7 @@ -{% extends 'scipost/_personal_page_base.html' %} +{% extends 'production/base.html' %} {% block breadcrumb_items %} {{block.super}} - <a href="{{object.get_absolute_url}}" class="breadcrumb-item">Production streams</a> <span class="breadcrumb-item">Edit production event</span> {% endblock %} diff --git a/production/urls.py b/production/urls.py index cc5bf527b92c39690aabca17762514101ff57daf..8631da56cd28b1f9a99f50f85072d7477ba71204 100644 --- a/production/urls.py +++ b/production/urls.py @@ -5,8 +5,17 @@ from production import views as production_views urlpatterns = [ url(r'^$', production_views.production, name='production'), url(r'^completed$', production_views.completed, name='completed'), + url(r'^officers/new$', production_views.user_to_officer, name='user_to_officer'), url(r'^streams/(?P<stream_id>[0-9]+)/events/add$', production_views.add_event, name='add_event'), + url(r'^streams/(?P<stream_id>[0-9]+)/officer/add$', + production_views.add_officer, name='add_officer'), + url(r'^streams/(?P<stream_id>[0-9]+)/officer/(?P<officer_id>[0-9]+)/remove$', + production_views.remove_officer, name='remove_officer'), + url(r'^streams/(?P<stream_id>[0-9]+)/supervisor/add$', + production_views.add_supervisor, name='add_supervisor'), + url(r'^streams/(?P<stream_id>[0-9]+)/supervisor/(?P<officer_id>[0-9]+)/remove$', + production_views.remove_supervisor, name='remove_supervisor'), url(r'^streams/(?P<stream_id>[0-9]+)/mark_completed$', production_views.mark_as_completed, name='mark_as_completed'), url(r'^events/(?P<event_id>[0-9]+)/edit', diff --git a/production/views.py b/production/views.py index 517c09c116ecdd90c20cc0dcdc5ba199f194a202..bcafcdbe0da5502211e0825a37054690ba9f205d 100644 --- a/production/views.py +++ b/production/views.py @@ -1,73 +1,185 @@ import datetime from django.contrib import messages +from django.contrib.auth.decorators import permission_required from django.contrib.auth.models import Group from django.core.urlresolvers import reverse +from django.db import transaction from django.shortcuts import get_object_or_404, render, redirect from django.utils import timezone from django.utils.decorators import method_decorator from django.views.generic.edit import UpdateView, DeleteView -from guardian.decorators import permission_required +from guardian.core import ObjectPermissionChecker +from guardian.shortcuts import assign_perm from .constants import PRODUCTION_STREAM_COMPLETED -from .models import ProductionStream, ProductionEvent -from .forms import ProductionEventForm -from .signals import notify_stream_completed - -from scipost.models import Contributor +from .models import ProductionUser, ProductionStream, ProductionEvent +from .forms import ProductionEventForm, AssignOfficerForm, UserToOfficerForm, AssignSupervisorForm +from .permissions import is_production_user +from .signals import notify_stream_completed, notify_new_stream_assignment ###################### # Production process # ###################### -@permission_required('scipost.can_view_production', return_403=True) +@is_production_user() +@permission_required('scipost.can_view_production', raise_exception=True) def production(request): """ Overview page for the production process. All papers with accepted but not yet published status are included here. """ - streams = ProductionStream.objects.ongoing().order_by('opened') + streams = ProductionStream.objects.ongoing() + if not request.user.has_perm('scipost.can_view_all_production_streams'): + # Restrict stream queryset if user is not supervisor + streams = streams.filter_for_user(request.user.production_user) + streams = streams.order_by('opened') + prodevent_form = ProductionEventForm() + assign_officer_form = AssignOfficerForm() + assign_supervisor_form = AssignSupervisorForm() ownevents = ProductionEvent.objects.filter( - noted_by=request.user.contributor, + noted_by=request.user.production_user, duration__gte=datetime.timedelta(minutes=1)).order_by('-noted_on') context = { 'streams': streams, 'prodevent_form': prodevent_form, + 'assign_officer_form': assign_officer_form, + 'assign_supervisor_form': assign_supervisor_form, 'ownevents': ownevents, } if request.user.has_perm('scipost.can_view_timesheets'): - context['production_team'] = Contributor.objects.filter( - user__groups__name='Production Officers').order_by('user__last_name') + context['production_team'] = ProductionUser.objects.all() + + if request.user.has_perm('scipost.can_promote_to_production_team'): + context['production_officers'] = ProductionUser.objects.all() + context['new_officer_form'] = UserToOfficerForm() return render(request, 'production/production.html', context) -@permission_required('scipost.can_view_production', return_403=True) +@is_production_user() +@permission_required('scipost.can_view_production', raise_exception=True) def completed(request): """ Overview page for closed production streams. """ - streams = ProductionStream.objects.completed().order_by('-opened') + streams = ProductionStream.objects.completed() + if not request.user.has_perm('scipost.can_view_all_production_streams'): + streams = streams.filter_for_user(request.user.production_user) + streams = streams.order_by('-opened') + context = {'streams': streams} return render(request, 'production/completed.html', context) -@permission_required('scipost.can_view_production', return_403=True) +@is_production_user() +@permission_required('scipost.can_promote_user_to_production_officer') +def user_to_officer(request): + form = UserToOfficerForm(request.POST or None) + if form.is_valid(): + officer = form.save() + + # Add permission group + group = Group.objects.get(name='Production Officers') + officer.user.groups.add(group) + messages.success(request, '{user} promoted to Production Officer'.format(user=officer)) + return redirect(reverse('production:production')) + + +@is_production_user() +@permission_required('scipost.can_view_production', raise_exception=True) def add_event(request, stream_id): - stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) + qs = ProductionStream.objects.ongoing() + if not request.user.has_perm('scipost.can_assign_production_officer'): + qs = qs.filter_for_user(request.user.production_user) + + stream = get_object_or_404(qs, pk=stream_id) prodevent_form = ProductionEventForm(request.POST or None) if prodevent_form.is_valid(): prodevent = prodevent_form.save(commit=False) prodevent.stream = stream - prodevent.noted_by = request.user.contributor + prodevent.noted_by = request.user.production_user prodevent.save() else: messages.warning(request, 'The form was invalidly filled.') return redirect(reverse('production:production')) +@is_production_user() +@permission_required('scipost.can_assign_production_officer', raise_exception=True) +def add_officer(request, stream_id): + stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) + checker = ObjectPermissionChecker(request.user) + if not checker.has_perm('can_perform_supervisory_actions', stream): + return redirect(reverse('production:production')) + + form = AssignOfficerForm(request.POST or None, instance=stream) + if form.is_valid(): + form.save() + officer = form.cleaned_data.get('officer') + assign_perm('can_work_for_stream', officer.user, stream) + messages.success(request, 'Officer {officer} has been assigned.'.format(officer=officer)) + notify_new_stream_assignment(request.user, stream, officer.user) + else: + for key, error in form.errors.items(): + messages.warning(request, error[0]) + return redirect(reverse('production:production')) + + +@is_production_user() +@permission_required('scipost.can_assign_production_officer', raise_exception=True) +def remove_officer(request, stream_id, officer_id): + stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) + checker = ObjectPermissionChecker(request.user) + if not checker.has_perm('can_perform_supervisory_actions', stream): + return redirect(reverse('production:production')) + + if getattr(stream.officer, 'id', 0) == int(officer_id): + officer = stream.officer + stream.officer = None + stream.save() + messages.success(request, 'Officer {officer} has been removed.'.format(officer=officer)) + + return redirect(reverse('production:production')) + + +@is_production_user() +@permission_required('scipost.can_assign_production_supervisor', raise_exception=True) +@transaction.atomic +def add_supervisor(request, stream_id): + stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) + form = AssignSupervisorForm(request.POST or None, instance=stream) + if form.is_valid(): + form.save() + supervisor = form.cleaned_data.get('supervisor') + assign_perm('can_work_for_stream', supervisor.user, stream) + messages.success(request, 'Supervisor {supervisor} has been assigned.'.format( + supervisor=supervisor)) + notify_new_stream_assignment(request.user, stream, supervisor.user) + assign_perm('can_perform_supervisory_actions', supervisor.user, stream) + else: + for key, error in form.errors.items(): + messages.warning(request, error[0]) + return redirect(reverse('production:production')) + + +@is_production_user() +@permission_required('scipost.can_assign_production_supervisor', raise_exception=True) +def remove_supervisor(request, stream_id, officer_id): + stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) + if getattr(stream.supervisor, 'id', 0) == int(officer_id): + supervisor = stream.supervisor + stream.supervisor = None + stream.save() + messages.success(request, 'Supervisor {supervisor} has been removed.'.format( + supervisor=supervisor)) + + return redirect(reverse('production:production')) + + +@method_decorator(is_production_user(), name='dispatch') @method_decorator(permission_required('scipost.can_view_production', raise_exception=True), name='dispatch') class UpdateEventView(UpdateView): @@ -77,13 +189,14 @@ class UpdateEventView(UpdateView): slug_url_kwarg = 'event_id' def get_queryset(self): - return self.model.objects.get_my_events(self.request.user.contributor) + return self.model.objects.get_my_events(self.request.user.production_user) def form_valid(self, form): messages.success(self.request, 'Event updated succesfully') return super().form_valid(form) +@method_decorator(is_production_user(), name='dispatch') @method_decorator(permission_required('scipost.can_view_production', raise_exception=True), name='dispatch') class DeleteEventView(DeleteView): @@ -92,7 +205,7 @@ class DeleteEventView(DeleteView): slug_url_kwarg = 'event_id' def get_queryset(self): - return self.model.objects.get_my_events(self.request.user.contributor) + return self.model.objects.get_my_events(self.request.user.production_user) def form_valid(self, form): messages.success(self.request, 'Event deleted succesfully') @@ -102,7 +215,8 @@ class DeleteEventView(DeleteView): return self.object.get_absolute_url() -@permission_required('scipost.can_publish_accepted_submission', return_403=True) +@is_production_user() +@permission_required('scipost.can_publish_accepted_submission', raise_exception=True) def mark_as_completed(request, stream_id): stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) stream.status = PRODUCTION_STREAM_COMPLETED diff --git a/scipost/admin.py b/scipost/admin.py index c9c58862746e64ee41675f273db517b611b9dd2f..8cfaffcf2da4ef93af08a909d043e8164df872fe 100644 --- a/scipost/admin.py +++ b/scipost/admin.py @@ -13,6 +13,7 @@ from scipost.models import Contributor, Remark,\ from journals.models import Publication from partners.admin import ContactToUserInline +from production.admin import ProductionUserInline from submissions.models import Submission @@ -38,6 +39,7 @@ class UserAdmin(UserAdmin): inlines = [ ContributorInline, ContactToUserInline, + ProductionUserInline ] search_fields = ['last_name', 'email'] diff --git a/scipost/constants.py b/scipost/constants.py index e3166d2f4bd8c9517447a913d9b732d8e8f57a7a..07c873c85863224471c0a4b8c839fd6295728176 100644 --- a/scipost/constants.py +++ b/scipost/constants.py @@ -136,7 +136,7 @@ CONTRIBUTOR_STATUS = ( # -4: disabled account (deceased) (CONTRIBUTOR_NEWLY_REGISTERED, 'newly registered'), (CONTRIBUTOR_NORMAL, 'normal user'), - (-1, 'not a professional scientist'), + (-1, 'not a professional scientist'), # Soon to be deprecated (-2, 'other account already exists'), (-3, 'barred from SciPost'), (-4, 'account disabled'), diff --git a/scipost/management/commands/add_groups_and_permissions.py b/scipost/management/commands/add_groups_and_permissions.py index 6070f1f785fb738d108bf0d9e0b3e9bf120175fe..638a67dfa9feefffa42216af275ff77ad80cd5f7 100644 --- a/scipost/management/commands/add_groups_and_permissions.py +++ b/scipost/management/commands/add_groups_and_permissions.py @@ -27,6 +27,7 @@ class Command(BaseCommand): Testers, created = Group.objects.get_or_create(name='Testers') Ambassadors, created = Group.objects.get_or_create(name='Ambassadors') JuniorAmbassadors, created = Group.objects.get_or_create(name='Junior Ambassadors') + ProductionSupervisors, created = Group.objects.get_or_create(name='Production Supervisor') ProductionOfficers, created = Group.objects.get_or_create(name='Production Officers') PartnersAdmin, created = Group.objects.get_or_create(name='Partners Administrators') @@ -52,8 +53,8 @@ class Command(BaseCommand): codename='can_read_partner_page', name='Can read Prospective Partner personal page', content_type=content_type) - can_promote_prospect_to_partner, created = Permission.objects.get_or_create( - codename='can_promote_prospect_to_partner', + can_promote_to_production_team, created = Permission.objects.get_or_create( + codename='can_promote_to_production_team', name='Can promote Prospective Partner to Partner', content_type=content_type) can_view_partners, created = Permission.objects.get_or_create( @@ -205,6 +206,22 @@ class Command(BaseCommand): content_type=content_type) # Production + can_promote_user_to_production_officer, created = Permission.objects.get_or_create( + codename='can_promote_user_to_production_officer', + name='Can promote user to production officer', + content_type=content_type) + can_assign_production_officer, created = Permission.objects.get_or_create( + codename='can_assign_production_officer', + name='Can assign production officer', + content_type=content_type) + can_view_all_production_streams, created = Permission.objects.get_or_create( + codename='can_view_all_production_streams', + name='Can view all production stream', + content_type=content_type) + can_assign_production_supervisor, created = Permission.objects.get_or_create( + codename='can_assign_production_supervisor', + name='Can assign production supervisor', + content_type=content_type) can_view_production, created = Permission.objects.get_or_create( codename='can_view_production', name='Can view production page', @@ -250,10 +267,13 @@ class Command(BaseCommand): can_assign_submissions, can_prepare_recommendations_for_voting, can_fix_College_decision, + can_promote_user_to_production_officer, can_view_production, can_attend_VGMs, can_view_timesheets, can_manage_mailchimp, + can_view_all_production_streams, + can_promote_to_production_team, ]) FinancialAdmin.permissions.set([ @@ -278,6 +298,8 @@ class Command(BaseCommand): can_publish_accepted_submission, can_attend_VGMs, can_manage_reports, + can_assign_production_supervisor, + can_view_all_production_streams, ]) EditorialCollege.permissions.set([ @@ -316,6 +338,13 @@ class Command(BaseCommand): can_draft_registration_invitations, ]) + ProductionSupervisors.permissions.set([ + can_assign_production_officer, + can_view_all_production_streams, + can_view_docs_scipost, + can_view_production, + ]) + ProductionOfficers.permissions.set([ can_view_docs_scipost, can_view_production, @@ -325,7 +354,6 @@ class Command(BaseCommand): can_read_partner_page, can_view_own_partner_details, can_manage_SPB, - can_promote_prospect_to_partner, can_email_prospartner_contact, can_view_partners, ]) diff --git a/scipost/managers.py b/scipost/managers.py index 11af2c2ed8ead11bb2932d4b4c0a9dcef7d1a36d..788aecb9a7773c31236423938c52945692f5fc1c 100644 --- a/scipost/managers.py +++ b/scipost/managers.py @@ -4,7 +4,7 @@ from django.db import models from django.db.models import Q from .constants import CONTRIBUTOR_NORMAL, INVITATION_EDITORIAL_FELLOW,\ - CONTRIBUTOR_NEWLY_REGISTERED + CONTRIBUTOR_NEWLY_REGISTERED, AUTHORSHIP_CLAIM_PENDING class FellowManager(models.Manager): @@ -25,6 +25,9 @@ class ContributorManager(models.Manager): def awaiting_validation(self): return self.filter(user__is_active=False, status=CONTRIBUTOR_NEWLY_REGISTERED) + def awaiting_vetting(self): + return self.filter(user__is_active=True, status=CONTRIBUTOR_NEWLY_REGISTERED) + def fellows(self): return self.filter(user__groups__name='Editorial College') @@ -46,3 +49,8 @@ class UnavailabilityPeriodManager(models.Manager): def today(self): today = datetime.date.today() return self.filter(start__lte=today, end__gte=today) + + +class AuthorshipClaimQuerySet(models.QuerySet): + def awaiting_vetting(self): + return self.filter(status=AUTHORSHIP_CLAIM_PENDING) diff --git a/scipost/models.py b/scipost/models.py index fe7afc37b63adc4dae58d7aeeb6d57e9d42bc4dc..88a69d14e41057de35432fbb2e73192181a9d541 100644 --- a/scipost/models.py +++ b/scipost/models.py @@ -19,7 +19,7 @@ from .constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS,\ AUTHORSHIP_CLAIM_PENDING, AUTHORSHIP_CLAIM_STATUS from .fields import ChoiceArrayField from .managers import FellowManager, ContributorManager, RegistrationInvitationManager,\ - UnavailabilityPeriodManager + UnavailabilityPeriodManager, AuthorshipClaimQuerySet def get_sentinel_user(): @@ -34,8 +34,7 @@ def get_sentinel_user(): class Contributor(models.Model): """ - All users of SciPost are Contributors. - Permissions determine the sub-types. + All *science* users of SciPost are Contributors. username, password, email, first_name and last_name are inherited from User. """ user = models.OneToOneField(User, on_delete=models.PROTECT, unique=True) @@ -265,6 +264,8 @@ class AuthorshipClaim(models.Model): status = models.SmallIntegerField(choices=AUTHORSHIP_CLAIM_STATUS, default=AUTHORSHIP_CLAIM_PENDING) + objects = AuthorshipClaimQuerySet.as_manager() + class PrecookedEmail(models.Model): """ diff --git a/scipost/permissions.py b/scipost/permissions.py new file mode 100644 index 0000000000000000000000000000000000000000..a9a936925fa6dc6560fc94c8f4d3340c2b9e6cff --- /dev/null +++ b/scipost/permissions.py @@ -0,0 +1,6 @@ + +def is_tester(user): + """ + This method checks if user is member of the Test Group. + """ + return user.groups.filter(name='Testers').exists() diff --git a/scipost/static/scipost/assets/css/_pool.scss b/scipost/static/scipost/assets/css/_pool.scss new file mode 100644 index 0000000000000000000000000000000000000000..5a59e0df5e3c27754ad7f0804bdc167be7f4c14e --- /dev/null +++ b/scipost/static/scipost/assets/css/_pool.scss @@ -0,0 +1,39 @@ +$pool-icons-width: 40px; +$pool-flex-width: calc(100% - 40px); + + +.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; + } + + .item { + flex: 0 0 $pool-flex-width; + width: $pool-flex-width; + max-width: none; + } + } + + .card.submission-detail { + position: sticky; + top: 15px; + } + + #details .loading { + position: sticky; + top: 15px; + padding: 5rem 0 3rem 0; + text-align: center; + } + + li.active { + background-color: $gray-100; + } +} diff --git a/scipost/static/scipost/assets/css/_type.scss b/scipost/static/scipost/assets/css/_type.scss index 9f21c44547325bbbf1055366f00574a18cc4a3b2..056fe6dcb5bb8c21f77aee7affb8a68a0cfec105 100644 --- a/scipost/static/scipost/assets/css/_type.scss +++ b/scipost/static/scipost/assets/css/_type.scss @@ -112,28 +112,6 @@ hr.hr12 { margin-bottom: 0; } -.circle, -.circle-clickable { - display: inline-block; - background: #fff; - border: 2px solid #333; - color: #333; - border-radius: 100%; - width: 17px; - height: 17px; - line-height: 16px; - text-align: center; - font-size: 9px; - transition: 0.1s; -} - -.circle-clickable { - cursor: pointer; - color: #bbb; - border-color: #bbb; - - &:hover { - border-color: #333; - color: #333; - } +.fa[data-toggle="tooltip"] { + font-size: 1.5em; } diff --git a/scipost/static/scipost/assets/css/style.scss b/scipost/static/scipost/assets/css/style.scss index 5c8bb75eef61d1059c45fa6a2e6d1f86d20e8b1f..be68d319863f3b5d962c823aca10301be2f28b73 100644 --- a/scipost/static/scipost/assets/css/style.scss +++ b/scipost/static/scipost/assets/css/style.scss @@ -31,6 +31,7 @@ @import "navbar"; @import "nav"; @import "page_header"; +@import "pool"; @import "popover"; @import "tables"; @import "tooltip"; diff --git a/scipost/static/scipost/assets/js/notifications.js b/scipost/static/scipost/assets/js/notifications.js index c50749a1fb74048544e60c39438e3075ba14076a..6f85c449bd19b4f917a6aee7affc17bdf1c267fc 100644 --- a/scipost/static/scipost/assets/js/notifications.js +++ b/scipost/static/scipost/assets/js/notifications.js @@ -2,7 +2,8 @@ var notify_badge_class = "live_notify_badge"; var notify_menu_class = "live_notify_list"; var notify_api_url_count = "/notifications/api/unread_count/"; var notify_api_url_list = "/notifications/api/list/"; -var notify_api_url_toggle_read = "/notifications/mark-toggle"; +var notify_api_url_toggle_read = "/notifications/mark-toggle/"; +var notify_api_url_mark_all_read = "/notifications/mark-all-as-read/"; var notify_fetch_count = "5"; var notify_refresh_period = 15000; var consecutive_misfires = 0; @@ -17,7 +18,7 @@ function initiate_popover(reinitiate) { 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>'; 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></small></div>'; + 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>'; } function get_notifications() { @@ -42,12 +43,16 @@ function initiate_popover(reinitiate) { setTimeout(function() { $('.popover [data-toggle="tooltip"]').tooltip({ animation: false, + delay: {"show": 500, "hide": 100}, fallbackPlacement: 'clockwise', placement: 'bottom' }); $('.popover .actions a').on('click', function() { mark_toggle(this) }) + $('.popover a.mark_all_read').on('click', function() { + mark_all_read(this) + }) }, 1000); }); if (reinitiate) { @@ -55,8 +60,7 @@ function initiate_popover(reinitiate) { } } - -function mark_toggle(el) { +function request_reinitiate(url) { var r = new XMLHttpRequest(); r.addEventListener('readystatechange', function(event){ if (this.readyState == 4 && this.status == 200) { @@ -64,10 +68,18 @@ function mark_toggle(el) { initiate_popover(reinitiate=true) } }) - r.open("GET", notify_api_url_toggle_read + '/' + $(el).data('slug') + '/?json=1', true); + r.open("GET", url, true); r.send(); } +function mark_all_read(el) { + request_reinitiate(notify_api_url_mark_all_read + '?json=1') +} + +function mark_toggle(el) { + request_reinitiate(notify_api_url_toggle_read + $(el).data('slug') + '/?json=1') +} + function fill_notification_badge(data) { var badges = document.getElementsByClassName(notify_badge_class); @@ -101,9 +113,9 @@ function get_notification_list() { } if(item.unread) { - var mark_as_read = '<div class="actions"><a href="#" data-slug="' + item.slug + '"><i class="fa fa-circle" data-toggle="tooltip" data-placement="auto" title="Mark as unread" aria-hidden="true"></i></a></div>'; + 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="#" data-slug="' + item.slug + '"><i class="fa fa-circle-o" data-toggle="tooltip" data-placement="auto" title="Mark as read" aria-hidden="true"></i></a></div>'; + 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>'; }).join(''); diff --git a/scipost/static/scipost/assets/js/scripts.js b/scipost/static/scipost/assets/js/scripts.js index e16263ae7d94b480e54096a08385c61e00e893ba..fcc6ddc4c8d8d56b05c1ad16940e917cccc6342c 100644 --- a/scipost/static/scipost/assets/js/scripts.js +++ b/scipost/static/scipost/assets/js/scripts.js @@ -20,6 +20,19 @@ var getUrlParameter = function getUrlParameter(sParam) { } }; +function init_page() { + // Show right tab if url contains `tab` GET request + var tab = getUrlParameter('tab') + if (tab) { + $('a[href="#' + tab + '"][data-toggle="tab"]').tab('show'); + } + + // Auto-submit hook for general form elements + $("form .auto-submit input, form.auto-submit input, form.auto-submit select").on('change', function(){ + $(this).parents('form').submit() + }); +} + $(function(){ // Remove all alerts in screen automatically after 15sec. setTimeout(function() {hide_all_alerts()}, 15000); @@ -29,20 +42,33 @@ $(function(){ $($(this).attr('data-target')).toggle(); }); - // Show right tab if url contains `tab` GET request - var tab = getUrlParameter('tab') - if (tab) { - $('a[href="#' + tab + '"][data-toggle="tab"]').tab('show'); - } - // Change `tab` GET parameter for page-reload $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { var tab_name = e.target.hash.substring(1) window.history.replaceState({}, null, '?tab=' + tab_name); }); - // Auto-submit hook for general form elements - $("form .auto-submit input").on('change', function(){ - $(this).parents('form').submit() + init_page(); + + // Simple simple Angular-like loading! + $('a[data-toggle="dynamic"]').on('click', function(event) { + event.preventDefault(); + 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>'); + + $.get(url + '?json=1').done(function(data) { + // console.log('done', data); + $(target).html(data); + $('[data-target="active-list"]') + .find('> li') + .removeClass('active') + $(self).parents('[data-target="active-list"] > li') + .addClass('active'); + init_page(); + }); }); }); diff --git a/scipost/templates/scipost/navbar.html b/scipost/templates/scipost/navbar.html index 2095054a8911f5e1770c891c825649bae7cd2bef..d946d267599f9f75c182318174de04f7b0222327 100644 --- a/scipost/templates/scipost/navbar.html +++ b/scipost/templates/scipost/navbar.html @@ -46,6 +46,11 @@ <li class="nav-item"> <a class="nav-link" href="{% url 'scipost:logout' %}">Logout</a> </li> + {% if perms.scipost.can_view_production %} + <li class="nav-item{% if '/production' in request.path %} active{% endif %}"> + <a class="nav-link" href="{% url 'production:production' %}">Production</a> + </li> + {% endif %} {% if perms.scipost.can_oversee_refereeing %} <li class="nav-item{% if '/submissions/admin' in request.path %} active{% endif %}"> <a class="nav-link" href="{% url 'submissions:admin' %}">Editorial Administration</a> diff --git a/scipost/templates/scipost/personal_page.html b/scipost/templates/scipost/personal_page.html index 0ff78b8eeff9914108b8d946cbc9e338cc1e7c15..ccdef253c64c9c545f4c960d931331a336ad1e7f 100644 --- a/scipost/templates/scipost/personal_page.html +++ b/scipost/templates/scipost/personal_page.html @@ -6,7 +6,7 @@ {% block content %} -{% if 'Registered Contributors' not in user_groups %} +{% if needs_validation %} <div class="row"> <div class="col-12"> @@ -38,38 +38,37 @@ <a href="#editorial-actions" class="nav-link" data-toggle="tab">Editorial Actions</a> </li> {% endif %} - {% if perms.scipost.can_view_production %} - <li class="nav-item btn btn-secondary"> - <a href="#production" class="nav-link" data-toggle="tab">Production</a> - </li> - {% endif %} {% if perms.scipost.can_referee %} <li class="nav-item btn btn-secondary"> <a class="nav-link" data-toggle="tab" href="#refereeing">Refereeing {% if refereeing_tab_total_count %}({{refereeing_tab_total_count}}){% endif %}</a> </li> {% endif %} - <li class="nav-item btn btn-secondary"> - <a class="nav-link" data-toggle="tab" href="#publications">Publications</a> - </li> - <li class="nav-item btn btn-secondary"> - <a class="nav-link" data-toggle="tab" href="#submissions">Submissions</a> - </li> - <li class="nav-item btn btn-secondary"> - <a class="nav-link" data-toggle="tab" href="#commentaries">Commentaries</a> - </li> - <li class="nav-item btn btn-secondary"> - <a class="nav-link" data-toggle="tab" href="#theses">Theses</a> - </li> - <li class="nav-item btn btn-secondary"> - {% with request.user.contributor.comments.regular_comments.awaiting_vetting.count as count %} - <a class="nav-link" data-toggle="tab" href="#comments">Comments{% if count %} ({{count}} unvetted){% endif %}</a> - {% endwith %} - </li> - <li class="nav-item btn btn-secondary"> - {% with request.user.contributor.comments.author_replies.awaiting_vetting.count as count %} - <a class="nav-link" data-toggle="tab" href="#author-replies">Author Replies{% if count %} ({{count}} unvetted){% endif %}</a> - {% endwith %} - </li> + {% if contributor %} + {# If user is contributor #} + <li class="nav-item btn btn-secondary"> + <a class="nav-link" data-toggle="tab" href="#publications">Publications</a> + </li> + <li class="nav-item btn btn-secondary"> + <a class="nav-link" data-toggle="tab" href="#submissions">Submissions</a> + </li> + <li class="nav-item btn btn-secondary"> + <a class="nav-link" data-toggle="tab" href="#commentaries">Commentaries</a> + </li> + <li class="nav-item btn btn-secondary"> + <a class="nav-link" data-toggle="tab" href="#theses">Theses</a> + </li> + <li class="nav-item btn btn-secondary"> + {% with contributor.comments.regular_comments.awaiting_vetting.count as count %} + <a class="nav-link" data-toggle="tab" href="#comments">Comments{% if count %} ({{count}} unvetted){% endif %}</a> + {% endwith %} + </li> + <li class="nav-item btn btn-secondary"> + {% with contributor.comments.author_replies.awaiting_vetting.count as count %} + <a class="nav-link" data-toggle="tab" href="#author-replies">Author Replies{% if count %} ({{count}} unvetted){% endif %}</a> + {% endwith %} + </li> + {# END: If user is contributor #} + {% endif %} </ul> </div> </div> @@ -93,24 +92,32 @@ <h3>Your personal details:</h3> {% include "scipost/_private_info_as_table.html" with contributor=contributor %} - <h3 class="mt-3">Your main discipline:</h3> - <ul><li>{{ contributor.get_discipline_display }}</li></ul> - - <h3 class="mt-3">Your expertises:</h3> - {% if contributor.expertises %} - {% include "scipost/_expertises_as_ul.html" with contributor=contributor %} - {% else %} - <p>You haven't listed your expertise(s).<br/> - Do so by <a href="{% url 'scipost:update_personal_data' %}">updating your personal data</a> - </p> + {% if contributor %} + {# Scientist fields #} + <h3 class="mt-3">Your main discipline:</h3> + <ul><li>{{ contributor.get_discipline_display }}</li></ul> + + <h3 class="mt-3">Your expertises:</h3> + {% if contributor.expertises %} + {% include "scipost/_expertises_as_ul.html" with contributor=contributor %} + {% else %} + <p>You haven't listed your expertise(s).<br/> + Do so by <a href="{% url 'scipost:update_personal_data' %}">updating your personal data</a> + </p> + {% endif %} + {# END: Scientist fields #} {% endif %} </div> <div class="col-md-6"> - {% if not request.user.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 %} + {% 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 #} + {% endif %} {% if 'SciPost Administrators' in user_groups %} <h3>You are a SciPost Administrator.</h3> @@ -152,49 +159,51 @@ </div> </div> - <hr> - <div class="row"> - <div class="col"> - <h2 class="highlight">Your Availability</h2> - </div> - </div> - <div class="row justify-content-center"> - <div class="col-md-4 mr-md-5"> - <p>To help with the editorial workflow, you can inform us of any periods during which you are unavailable. We will do our best to respect these.</p> - <h3 class="mb-3">Mark a period as unavailable:</h3> - <form action="{% url 'scipost:mark_unavailable_period' %}" method="post"> - {% csrf_token %} - {{ unavailability_form|bootstrap }} - <input class="btn btn-secondary" type="submit" value="Submit" /> - </form> + {% if unavailability_form %} + <hr> + <div class="row"> + <div class="col"> + <h2 class="highlight">Your Availability</h2> + </div> </div> - <div class="col-md-4 ml-md-5"> - {% if unavailabilities %} - <h3>Your unavailability periods in our records</h3> - <p class="text-muted">(YYYY-MM-DD)</p> - <table class="table"> - <tr> - <th>Start</th> - <th colspan="2">End</th> - </tr> - {% for unav in unavailabilities %} - <tr> - <td>{{ unav.start }}</td> - <td>{{ unav.end }}</td> - <td> - <form action="{% url 'scipost:delete_unavailable_period' unav.id %}" method="post"> - {% csrf_token %} - <input class="btn btn-danger" type="submit" value="Delete" /> - </form> - </td> - </tr> - {% endfor %} - </table> - {% else %} - <p>You don't have any upcoming unavailability periods on record.</p> - {% endif %} + <div class="row justify-content-center"> + <div class="col-md-4 mr-md-5"> + <p>To help with the editorial workflow, you can inform us of any periods during which you are unavailable. We will do our best to respect these.</p> + <h3 class="mb-3">Mark a period as unavailable:</h3> + <form action="{% url 'scipost:mark_unavailable_period' %}" method="post"> + {% csrf_token %} + {{ unavailability_form|bootstrap }} + <input class="btn btn-secondary" type="submit" value="Submit" /> + </form> + </div> + <div class="col-md-4 ml-md-5"> + {% if unavailabilities %} + <h3>Your unavailability periods in our records</h3> + <p class="text-muted">(YYYY-MM-DD)</p> + <table class="table"> + <tr> + <th>Start</th> + <th colspan="2">End</th> + </tr> + {% for unav in unavailabilities %} + <tr> + <td>{{ unav.start }}</td> + <td>{{ unav.end }}</td> + <td> + <form action="{% url 'scipost:delete_unavailable_period' unav.id %}" method="post"> + {% csrf_token %} + <input class="btn btn-danger" type="submit" value="Delete" /> + </form> + </td> + </tr> + {% endfor %} + </table> + {% else %} + <p>You don't have any upcoming unavailability periods on record.</p> + {% endif %} + </div> </div> - </div> + {% endif %} </div><!-- End tab --> {% if 'SciPost Administrators' in user_groups or 'Editorial Administrators' in user_groups or 'Editorial College' in user_groups or 'Advisory Board' in user_groups or 'Vetting Editors' in user_groups or 'Ambassadors' in user_groups or 'Junior Ambassadors' in user_groups %} @@ -251,6 +260,13 @@ {% endif %} </ul> {% endif %} + + {% if perms.scipost.can_view_timesheets %} + <h3>Finance</h3> + <ul> + <li><a href="{% url 'finances:timesheets' %}">Production Team Timesheets</a></li> + </ul> + {% endif %} </div> {% endif %} @@ -334,346 +350,327 @@ </div> {% if active_assignments %} - <div class="row"> - <div class="col-12"> - <h3 class="highlight">Submissions for which you are Editor-in-charge</h3> - </div> - <div class="col-12"> - <ul class="list-group list-group-flush"> - {% for assignment in active_assignments %} - <li class="list-group-item"> - {% include 'submissions/_submission_card_eic_content.html' with submission=assignment.submission %} - </li> - {% endfor %} - </ul> + <div class="row"> + <div class="col-12"> + <h3 class="highlight">Submissions for which you are Editor-in-charge</h3> + </div> + <div class="col-12"> + <ul class="list-group list-group-flush"> + {% for assignment in active_assignments %} + <li class="list-group-item"> + {% include 'submissions/_submission_card_eic_content.html' with submission=assignment.submission %} + </li> + {% endfor %} + </ul> + </div> </div> - </div> {% endif %} </div><!-- End tab --> {% endif %} - {% if perms.scipost.can_view_production %} - <!-- Tab: Production --> - <div class="tab-pane" id="production" role="tabpanel"> - <div class="row"> - <div class="col-12"> - <div class="card card-grey"> - <div class="card-body"> - <h2 class="card-title mb-0">Production Tasks</h2> - </div> - </div> - </div> - </div> - <div class="row"> - <div class="col-md-4"> - <h3>Production workflow</h3> - <ul> - <li><a href="{% url 'production:production' %}">Go to the production page</a></li> - <li><a href="{% url 'production:completed' %}">View completed streams</a></li> - </ul> - </div> - </div> - </div> - {% endif %} - - <!-- Tab: Publications --> - <div class="tab-pane" id="publications" role="tabpanel"> - <div class="row"> - <div class="col-12"> - <div class="card card-grey"> - <div class="card-body"> - <h2 class="card-title">Publications</h2> - <ul class="mb-0"> - {% if nr_publication_authorships_to_claim > 0 %} - <li><a href="{% url 'scipost:claim_authorships' %}">Potential authorships to claim (auto-detected: {{ nr_publication_authorships_to_claim}})</a></li> - {% endif %} - </ul> - </div> - </div> - </div> - </div> - - {# {% if own_publications %}#} - <div class="row" id="mypublicationslist"> - <div class="col-12"> - <h3 class="mb-3">Publications for which you are identified as an author:</h3> - </div> - <div class="col-12"> - <ul class="list-unstyled"> - {% for pub in own_publications %} - <li> - <div class="card card-grey card-publication" id="{{pub.doi_label}}"> - {% include 'journals/_publication_card_content.html' with publication=pub current_user=request.user %} - </div> - </li> - {% empty %} - <li> - <em>No Publications found</em> - </li> - {% endfor %} - </ul> - </div> - </div> - {# {% endif %}#} - </div><!-- End tab --> - - - {% if perms.scipost.can_referee %} - <!-- Tab: Refereeing --> - <div class="tab-pane" id="refereeing" role="tabpanel"> + {% if contributor %} + {# If user is contributor #} + <!-- Tab: Publications --> + <div class="tab-pane" id="publications" role="tabpanel"> <div class="row"> <div class="col-12"> <div class="card card-grey"> <div class="card-body"> - <h2 class="card-title">Refereeing Tasks</h2> + <h2 class="card-title">Publications</h2> <ul class="mb-0"> - <li><a href="{% url 'submissions:accept_or_decline_ref_invitations' %}">Accept/decline refereeing invitations</a> ({{ nr_ref_inv_to_consider }})</li> + {% if nr_publication_authorships_to_claim > 0 %} + <li><a href="{% url 'scipost:claim_authorships' %}">Potential authorships to claim (auto-detected: {{ nr_publication_authorships_to_claim}})</a></li> + {% endif %} </ul> </div> </div> </div> </div> - <div class="row"> + {# {% if own_publications %}#} + <div class="row" id="mypublicationslist"> <div class="col-12"> - <h3>Pending Refereeing Tasks:</h3> + <h3 class="mb-3">Publications for which you are identified as an author:</h3> </div> <div class="col-12"> - <ul class="list-group list-group-flush"> - {% for task in pending_ref_tasks %} - <li class="list-group-item"> - {% include 'submissions/_refereeing_invitation_card_content.html' with invitation=task %} - </li> - {% empty %} - <li class="list-group-item"><em>You do not have any pending refereeing task</em></li> - {% endfor %} + <ul class="list-unstyled"> + {% for pub in own_publications %} + <li> + <div class="card card-grey card-publication" id="{{pub.doi_label}}"> + {% include 'journals/_publication_card_content.html' with publication=pub current_user=request.user %} + </div> + </li> + {% empty %} + <li> + <em>No Publications found</em> + </li> + {% endfor %} </ul> </div> </div> + {# {% endif %}#} + </div><!-- End tab --> + - {% if contributor.reports.in_draft.exists %} + {% if perms.scipost.can_referee %} + <!-- Tab: Refereeing --> + <div class="tab-pane" id="refereeing" role="tabpanel"> <div class="row"> <div class="col-12"> - <h3>Unfinished reports:</h3> - </div> - <div class="col-12"> - <ul class="list-group list-group-flush"> - {% for report in contributor.reports.in_draft.all %} - <li class="list-group-item"> - <div class="w-100">{% include 'submissions/_submission_card_content.html' with submission=report.submission %}</div> - <div class="px-2 mb-2"><a class="px-1" href="{% url 'submissions:submit_report' report.submission.arxiv_identifier_w_vn_nr %}">Finish report</a></div> - </li> - {% endfor %} - </ul> + <div class="card card-grey"> + <div class="card-body"> + <h2 class="card-title">Refereeing Tasks</h2> + <ul class="mb-0"> + <li><a href="{% url 'submissions:accept_or_decline_ref_invitations' %}">Accept/decline refereeing invitations</a> ({{ nr_ref_inv_to_consider }})</li> + </ul> + </div> + </div> </div> </div> - {% endif %} - {% if contributor.reports.non_draft.exists %} <div class="row"> <div class="col-12"> - <h3>Finished reports:</h3> + <h3>Pending Refereeing Tasks:</h3> </div> <div class="col-12"> <ul class="list-group list-group-flush"> - {% for report in contributor.reports.non_draft.all %} + {% for task in pending_ref_tasks %} <li class="list-group-item"> - {% comment %} - Temporary: There is already a template for a "Report summary" in a parallel (unmerged) branch. Awaiting merge to use that template. - {% endcomment %} - <div class="card-body {% block cardblock_class_block %}{% endblock %}"> - <h3>Report on Submission <a href="{{report.submission.get_absolute_url}}">{{report.submission.title}}</a></h3> - <table> - <tr> - <th style='min-width: 100px;'>Received:</th><td>{{ report.date_submitted|date:'Y-n-j' }}<td> - </tr> - <tr> - <th>Status:</th><td {% if report.status == 'vetted' %}class="text-success"{% elif report.status == 'unvetted' %}class="text-danger"{% endif %}>{{report.get_status_display}}</td> - </tr> - {% if report.doi_label %} - <tr> - <th>DOI:</th><td>{{ report.doi_string }}</td></th> -{% endif %} - <tr> - <th>Anonymous:</th><td>{{report.anonymous|yesno:'Yes,No'}}</td>{% if report.anonymous %}<td>You can <a href="{% url 'journals:sign_existing_report' report_id=report.id %}">click here to sign this Report</a> (leads to confirmation page){% endif %}</td> - </tr> - </table> - </div> + {% include 'submissions/_refereeing_invitation_card_content.html' with invitation=task %} </li> + {% empty %} + <li class="list-group-item"><em>You do not have any pending refereeing task</em></li> {% endfor %} </ul> </div> </div> - {% endif %} - </div><!-- End tab --> - {% endif %} - <!-- Tab: Submissions --> - <div class="tab-pane" id="submissions" role="tabpanel"> - <div class="row"> - <div class="col-12"> - <div class="card card-grey"> - <div class="card-body"> - <h2 class="card-title">Submissions</h2> - <ul class="mb-0"> - {% if nr_submission_authorships_to_claim > 0 %} - <li><a href="{% url 'scipost:claim_authorships' %}">Potential authorships to claim (auto-detected: {{ nr_submission_authorships_to_claim}})</a></li> - {% endif %} - <li><a href="{% url 'submissions:submit_manuscript' %}">Submit an arXiv preprint to a SciPost Journal</a></li> - </ul> + {% if contributor.reports.in_draft.exists %} + <div class="row"> + <div class="col-12"> + <h3>Unfinished reports:</h3> + </div> + <div class="col-12"> + <ul class="list-group list-group-flush"> + {% for report in contributor.reports.in_draft.all %} + <li class="list-group-item"> + <div class="w-100">{% include 'submissions/_submission_card_content.html' with submission=report.submission %}</div> + <div class="px-2 mb-2"><a class="px-1" href="{% url 'submissions:submit_report' report.submission.arxiv_identifier_w_vn_nr %}">Finish report</a></div> + </li> + {% endfor %} + </ul> + </div> + </div> + {% endif %} + + {% if contributor.reports.non_draft.exists %} + <div class="row"> + <div class="col-12"> + <h3>Finished reports:</h3> + </div> + <div class="col-12"> + <ul class="list-group list-group-flush"> + {% for report in contributor.reports.non_draft.all %} + <li class="list-group-item"> + {% comment %} + Temporary: There is already a template for a "Report summary" in a parallel (unmerged) branch. Awaiting merge to use that template. + {% endcomment %} + <div class="card-body {% block cardblock_class_block %}{% endblock %}"> + <h3>Report on Submission <a href="{{report.submission.get_absolute_url}}">{{report.submission.title}}</a></h3> + <table> + <tr> + <th style='min-width: 100px;'>Received:</th><td>{{ report.date_submitted|date:'Y-n-j' }}<td> + </tr> + <tr> + <th>Status:</th><td {% if report.status == 'vetted' %}class="text-success"{% elif report.status == 'unvetted' %}class="text-danger"{% endif %}>{{report.get_status_display}}</td> + </tr> + {% if report.doi_label %} + <tr> + <th>DOI:</th><td>{{ report.doi_string }}</td></th> + {% endif %} + <tr> + <th>Anonymous:</th><td>{{report.anonymous|yesno:'Yes,No'}}</td>{% if report.anonymous %}<td>You can <a href="{% url 'journals:sign_existing_report' report_id=report.id %}">click here to sign this Report</a> (leads to confirmation page){% endif %}</td> + </tr> + </table> + </div> + </li> + {% endfor %} + </ul> + </div> + </div> + {% endif %} + </div><!-- End tab --> + {% endif %} + + <!-- Tab: Submissions --> + <div class="tab-pane" id="submissions" role="tabpanel"> + <div class="row"> + <div class="col-12"> + <div class="card card-grey"> + <div class="card-body"> + <h2 class="card-title">Submissions</h2> + <ul class="mb-0"> + {% if nr_submission_authorships_to_claim > 0 %} + <li><a href="{% url 'scipost:claim_authorships' %}">Potential authorships to claim (auto-detected: {{ nr_submission_authorships_to_claim}})</a></li> + {% endif %} + <li><a href="{% url 'submissions:submit_manuscript' %}">Submit an arXiv preprint to a SciPost Journal</a></li> + </ul> + </div> </div> </div> </div> - </div> - {# {% if own_submissions %}#} - <div class="row" id="mysubmissionslist"> - <div class="col-12"> - <h3>Submissions for which you are identified as an author:</h3> - </div> - <div class="col-12"> - <ul class="list-group list-group-flush"> - {% for sub in own_submissions %} - <li class="list-group-item"> - {% include 'submissions/_submission_card_author_content.html' with submission=sub current_user=request.user %} - </li> - {% empty %} - <li class="list-group-item"> - <em>No Submissions found</em> - </li> - {% endfor %} - </ul> + {# {% if own_submissions %}#} + <div class="row" id="mysubmissionslist"> + <div class="col-12"> + <h3>Submissions for which you are identified as an author:</h3> + </div> + <div class="col-12"> + <ul class="list-group list-group-flush"> + {% for sub in own_submissions %} + <li class="list-group-item"> + {% include 'submissions/_submission_card_author_content.html' with submission=sub current_user=request.user %} + </li> + {% empty %} + <li class="list-group-item"> + <em>No Submissions found</em> + </li> + {% endfor %} + </ul> + </div> </div> - </div> - {# {% endif %}#} - </div><!-- End tab --> + {# {% endif %}#} + </div><!-- End tab --> - <!-- Tab: Commentaries --> - <div class="tab-pane" id="commentaries" role="tabpanel"> - <div class="row"> - <div class="col-12"> - <div class="card card-grey"> - <div class="card-body"> - <h2 class="card-title">Commentaries</h2> - <ul class="mb-0"> - {% if nr_commentary_authorships_to_claim > 0 %} - <li><a href="{% url 'scipost:claim_authorships' %}">Potential authorships to claim (auto-detected: {{ nr_commentary_authorships_to_claim}})</a></li> - {% endif %} - <li><a href="{% url 'commentaries:request_commentary' %}">Request opening a SciPost Commentary Page</a></li> - </ul> + <!-- Tab: Commentaries --> + <div class="tab-pane" id="commentaries" role="tabpanel"> + <div class="row"> + <div class="col-12"> + <div class="card card-grey"> + <div class="card-body"> + <h2 class="card-title">Commentaries</h2> + <ul class="mb-0"> + {% if nr_commentary_authorships_to_claim > 0 %} + <li><a href="{% url 'scipost:claim_authorships' %}">Potential authorships to claim (auto-detected: {{ nr_commentary_authorships_to_claim}})</a></li> + {% endif %} + <li><a href="{% url 'commentaries:request_commentary' %}">Request opening a SciPost Commentary Page</a></li> + </ul> + </div> </div> </div> </div> - </div> - <div class="row" id="mycommentarieslist"> - <div class="col-12"> - <h3>Commentaries for which you are identified as an author:</h3> - </div> - <div class="col-12"> - <ul class="list-group list-group-flush"> - {% for com in own_commentaries %} - <li class="list-group-item"> - {% include 'commentaries/_commentary_card_content.html' with commentary=com %} - </li> - {% empty %} - <li class="list-group-item"><em>No Commentaries found</em></li> - {% endfor %} - </ul> + <div class="row" id="mycommentarieslist"> + <div class="col-12"> + <h3>Commentaries for which you are identified as an author:</h3> + </div> + <div class="col-12"> + <ul class="list-group list-group-flush"> + {% for com in own_commentaries %} + <li class="list-group-item"> + {% include 'commentaries/_commentary_card_content.html' with commentary=com %} + </li> + {% empty %} + <li class="list-group-item"><em>No Commentaries found</em></li> + {% endfor %} + </ul> + </div> </div> - </div> - </div><!-- End tab --> + </div><!-- End tab --> - <!-- Tab: Theses --> - <div class="tab-pane" id="theses" role="tabpanel"> - <div class="row"> - <div class="col-12"> - <div class="card card-grey"> - <div class="card-body"> - <h2 class="card-title">Theses</h2> - <ul class="mb-0"> - {% if nr_thesis_authorships_to_claim > 0 %} - <li><a href="{% url 'scipost:claim_authorships' %}">Potential authorships to claim (auto-detected: {{ nr_thesis_authorships_to_claim}})</a></li> - {% endif %} - <li><a href="{% url 'theses:request_thesislink' %}">Request a SciPost ThesisLink</a></li> - </ul> + <!-- Tab: Theses --> + <div class="tab-pane" id="theses" role="tabpanel"> + <div class="row"> + <div class="col-12"> + <div class="card card-grey"> + <div class="card-body"> + <h2 class="card-title">Theses</h2> + <ul class="mb-0"> + {% if nr_thesis_authorships_to_claim > 0 %} + <li><a href="{% url 'scipost:claim_authorships' %}">Potential authorships to claim (auto-detected: {{ nr_thesis_authorships_to_claim}})</a></li> + {% endif %} + <li><a href="{% url 'theses:request_thesislink' %}">Request a SciPost ThesisLink</a></li> + </ul> + </div> </div> </div> </div> - </div> - <div class="row" id="mytheseslist"> - <div class="col-12"> - <h3>Theses for which you are identified as an author:</h3> - </div> - <div class="col-12"> - <ul class="list-group list-group-flush"> - {% for thesis in own_thesislinks %} - <li class="list-group-item"> - {% include 'theses/_thesislink_card_content.html' with thesislink=thesis %} - </li> - {% empty %} - <li class="list-group-item"><em>No Theses found</em></li> - {% endfor %} - </ul> + <div class="row" id="mytheseslist"> + <div class="col-12"> + <h3>Theses for which you are identified as an author:</h3> + </div> + <div class="col-12"> + <ul class="list-group list-group-flush"> + {% for thesis in own_thesislinks %} + <li class="list-group-item"> + {% include 'theses/_thesislink_card_content.html' with thesislink=thesis %} + </li> + {% empty %} + <li class="list-group-item"><em>No Theses found</em></li> + {% endfor %} + </ul> + </div> </div> - </div> - </div><!-- End tab --> + </div><!-- End tab --> - <!-- Tab: Comments --> - <div class="tab-pane" id="comments" role="tabpanel"> - <div class="row"> - <div class="col-12"> - <div class="card card-grey"> - <div class="card-body"> - <h2 class="card-title mb-0">Your Comments</h2> + <!-- Tab: Comments --> + <div class="tab-pane" id="comments" role="tabpanel"> + <div class="row"> + <div class="col-12"> + <div class="card card-grey"> + <div class="card-body"> + <h2 class="card-title mb-0">Your Comments</h2> + </div> </div> </div> </div> - </div> - <div class="row" id="mycommentslist"> - <div class="col-12"> - <ul class="list-group list-group-flush"> - {% for own_comment in own_comments %} - <li class="list-group-item"> - {% include 'comments/_comment_card_extended_for_author.html' with comment=own_comment %} - </li> - {% empty %} - <li class="list-group-item"><em>You have not commented yet.</em></li> - {% endfor %} - </ul> + <div class="row" id="mycommentslist"> + <div class="col-12"> + <ul class="list-group list-group-flush"> + {% for own_comment in own_comments %} + <li class="list-group-item"> + {% include 'comments/_comment_card_extended_for_author.html' with comment=own_comment %} + </li> + {% empty %} + <li class="list-group-item"><em>You have not commented yet.</em></li> + {% endfor %} + </ul> + </div> </div> - </div> - </div><!-- End tab --> + </div><!-- End tab --> - <!-- Tab: Author Replies --> - <div class="tab-pane" id="author-replies" role="tabpanel"> - <div class="row"> - <div class="col-12"> - <div class="card card-grey"> - <div class="card-body"> - <h2 class="card-title mb-0">Your Author Replies</h2> + <!-- Tab: Author Replies --> + <div class="tab-pane" id="author-replies" role="tabpanel"> + <div class="row"> + <div class="col-12"> + <div class="card card-grey"> + <div class="card-body"> + <h2 class="card-title mb-0">Your Author Replies</h2> + </div> </div> </div> </div> - </div> - <div class="row" id="myauthorreplieslist"> - <div class="col-12"> - <ul class="list-group list-group-flush"> - {% for own_reply in own_authorreplies %} - <li class="list-group-item"> - {% include 'comments/_comment_card_extended_for_author.html' with comment=own_reply %} - </li> - {% empty %} - <li class="list-group-item"><em>You do not have Author Replies yet.</em></li> - {% endfor %} - </ul> + <div class="row" id="myauthorreplieslist"> + <div class="col-12"> + <ul class="list-group list-group-flush"> + {% for own_reply in own_authorreplies %} + <li class="list-group-item"> + {% include 'comments/_comment_card_extended_for_author.html' with comment=own_reply %} + </li> + {% empty %} + <li class="list-group-item"><em>You do not have Author Replies yet.</em></li> + {% endfor %} + </ul> + </div> </div> - </div> - </div><!-- End tab --> + </div><!-- End tab --> + + {# END: If user is contributor #} + {% endif %} </div> {% endif %} diff --git a/scipost/templatetags/scipost_extras.py b/scipost/templatetags/scipost_extras.py index 25c81e2b02c48f6bed015b8762147284b36d828e..a378cf31277cdda85e8874504559044eeb7d42a1 100644 --- a/scipost/templatetags/scipost_extras.py +++ b/scipost/templatetags/scipost_extras.py @@ -12,7 +12,9 @@ register = template.Library() @register.filter(name='sort_by') def sort_by(queryset, order): - return queryset.order_by(order) + if queryset: + return queryset.order_by(order) + return None @register.filter(name='duration') diff --git a/scipost/urls.py b/scipost/urls.py index f3af0c4a67bc30f8a0c836d7f4692467359db08a..fe4aaefa27501031192cb6ce51fddc74cbe10a87 100644 --- a/scipost/urls.py +++ b/scipost/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import include, url +from django.conf.urls import url from django.views.generic import TemplateView from . import views @@ -14,6 +14,7 @@ JOURNAL_REGEX = '(?P<doi_label>%s)' % REGEX_CHOICES urlpatterns = [ url(r'^$', views.index, name='index'), + url(r'^files/secure/(?P<path>.*)$', views.protected_serve, name='secure_file'), # General use pages url(r'^error$', TemplateView.as_view(template_name='scipost/error.html'), name='error'), diff --git a/scipost/views.py b/scipost/views.py index 5c09fe3e60b3b05544df1ba5244b9039ef2286a0..b6c67e3297e4c11354988483ab9225e43acf08be 100644 --- a/scipost/views.py +++ b/scipost/views.py @@ -1,5 +1,6 @@ from django.utils import timezone from django.shortcuts import get_object_or_404, render +from django.conf import settings from django.contrib import messages from django.contrib.auth import login, logout, update_session_auth_hash from django.contrib.auth.decorators import login_required, user_passes_test @@ -10,16 +11,19 @@ from django.core.mail import EmailMessage, EmailMultiAlternatives from django.core.paginator import Paginator from django.core.urlresolvers import reverse from django.db.models import Prefetch +from django.http import Http404 from django.shortcuts import redirect from django.template import Context, Template from django.views.decorators.http import require_POST from django.views.generic.list import ListView +from django.views.static import serve from guardian.decorators import permission_required from guardian.shortcuts import assign_perm, get_objects_for_user from haystack.generic_views import SearchView -from .constants import SCIPOST_SUBJECT_AREAS, subject_areas_raw_dict, SciPost_from_addresses_dict +from .constants import SCIPOST_SUBJECT_AREAS, subject_areas_raw_dict, SciPost_from_addresses_dict,\ + CONTRIBUTOR_NORMAL from .decorators import has_contributor from .models import Contributor, CitationNotification, UnavailabilityPeriod,\ DraftInvitation, RegistrationInvitation,\ @@ -85,6 +89,18 @@ def index(request): return render(request, 'scipost/index.html', context) +def protected_serve(request, path, show_indexes=False): + """ + Serve files that are saved outside the default MEDIA_ROOT folder for superusers only! + This will be usefull eg. in the admin pages. + """ + if not request.user.is_authenticated or not request.user.is_superuser: + # Only superusers may get to see secure files without an explicit serve method! + raise Http404 + document_root = settings.MEDIA_ROOT_SECURE + return serve(request, path, document_root, show_indexes) + + ############### # Information ############### @@ -691,137 +707,136 @@ def delete_unavailable_period(request, period_id): @login_required -@user_passes_test(has_contributor) def personal_page(request): """ The Personal Page is the main view for accessing user functions. """ - contributor = Contributor.objects.select_related('user').get(user=request.user) - user_groups = contributor.user.groups.values_list('name', flat=True) - - # Compile the unavailability periods: - now = timezone.now() - unavailabilities = UnavailabilityPeriod.objects.filter( - contributor=contributor).exclude(end__lt=now).order_by('start') - unavailability_form = UnavailabilityPeriodForm() - - # if an editor, count the number of actions required: - nr_reg_to_vet = 0 - nr_reg_awaiting_validation = 0 - nr_submissions_to_assign = 0 - nr_recommendations_to_prepare_for_voting = 0 - if contributor.is_SP_Admin(): - # count the number of pending registration requests - nr_reg_to_vet = Contributor.objects.filter(user__is_active=True, status=0).count() - nr_reg_awaiting_validation = (Contributor.objects.awaiting_validation() - # .filter(key_expires__gte=now, key_expires__lte=intwodays) - .count()) - nr_submissions_to_assign = Submission.objects.filter(status__in=['unassigned']).count() - nr_recommendations_to_prepare_for_voting = EICRecommendation.objects.filter( - submission__status__in=['voting_in_preparation']).count() - - nr_assignments_to_consider = 0 - active_assignments = None - nr_reports_to_vet = 0 - if contributor.is_MEC(): - nr_assignments_to_consider = (EditorialAssignment.objects - .filter(to=contributor, accepted=None, deprecated=False) - .count()) - active_assignments = EditorialAssignment.objects.filter( - to=contributor, accepted=True, completed=False) - nr_reports_to_vet = (Report.objects.awaiting_vetting() - .filter(submission__editor_in_charge=contributor).count()) - nr_commentary_page_requests_to_vet = 0 - nr_comments_to_vet = 0 - nr_thesislink_requests_to_vet = 0 - nr_authorship_claims_to_vet = 0 - if contributor.is_VE(): - nr_commentary_page_requests_to_vet = (Commentary.objects.awaiting_vetting() - .exclude(requested_by=contributor).count()) - nr_comments_to_vet = Comment.objects.awaiting_vetting().count() - nr_thesislink_requests_to_vet = ThesisLink.objects.filter(vetted=False).count() - nr_authorship_claims_to_vet = AuthorshipClaim.objects.filter(status='0').count() - - # Refereeing - nr_ref_inv_to_consider = RefereeInvitation.objects.filter( - referee=contributor, accepted=None, cancelled=False).count() - pending_ref_tasks = RefereeInvitation.objects.filter( - referee=contributor, accepted=True, fulfilled=False) - refereeing_tab_total_count = nr_ref_inv_to_consider + len(pending_ref_tasks) - refereeing_tab_total_count += Report.objects.in_draft().filter(author=contributor).count() - - # Verify if there exist objects authored by this contributor, - # whose authorship hasn't been claimed yet - own_publications = (Publication.objects - .filter(authors__in=[contributor]) - .order_by('-publication_date')) - own_submissions = (Submission.objects - .filter(authors__in=[contributor], is_current=True) - .order_by('-submission_date')) - own_commentaries = Commentary.objects.filter(authors=contributor).order_by('-latest_activity') - own_thesislinks = ThesisLink.objects.filter(author_as_cont__in=[contributor]) - nr_publication_authorships_to_claim = (Publication.objects.filter( - author_list__contains=contributor.user.last_name) - .exclude(authors__in=[contributor]) - .exclude(authors_claims__in=[contributor]) - .exclude(authors_false_claims__in=[contributor]) - .count()) - nr_submission_authorships_to_claim = (Submission.objects.filter( - author_list__contains=contributor.user.last_name) - .exclude(authors__in=[contributor]) - .exclude(authors_claims__in=[contributor]) - .exclude(authors_false_claims__in=[contributor]) - .count()) - nr_commentary_authorships_to_claim = (Commentary.objects.filter( - author_list__contains=contributor.user.last_name) - .exclude(authors__in=[contributor]) - .exclude(authors_claims__in=[contributor]) - .exclude(authors_false_claims__in=[contributor]) - .count()) - nr_thesis_authorships_to_claim = (ThesisLink.objects.filter( - author__contains=contributor.user.last_name) - .exclude(author_as_cont__in=[contributor]) - .exclude(author_claims__in=[contributor]) - .exclude(author_false_claims__in=[contributor]) - .count()) - own_comments = (Comment.objects.filter(author=contributor, is_author_reply=False) - .select_related('author', 'submission') - .order_by('-date_submitted')) - own_authorreplies = (Comment.objects.filter(author=contributor, is_author_reply=True) - .order_by('-date_submitted')) - - appellation = contributor.get_title_display() + ' ' + contributor.user.last_name context = { - 'contributor': contributor, - 'user_groups': user_groups, - 'appellation': appellation, - 'unavailabilities': unavailabilities, - 'unavailability_form': unavailability_form, - 'nr_reg_to_vet': nr_reg_to_vet, - 'nr_reg_awaiting_validation': nr_reg_awaiting_validation, - 'nr_commentary_page_requests_to_vet': nr_commentary_page_requests_to_vet, - 'nr_comments_to_vet': nr_comments_to_vet, - 'nr_thesislink_requests_to_vet': nr_thesislink_requests_to_vet, - 'nr_authorship_claims_to_vet': nr_authorship_claims_to_vet, - 'nr_reports_to_vet': nr_reports_to_vet, - 'nr_submissions_to_assign': nr_submissions_to_assign, - 'nr_recommendations_to_prepare_for_voting': nr_recommendations_to_prepare_for_voting, - 'nr_assignments_to_consider': nr_assignments_to_consider, - 'active_assignments': active_assignments, - 'nr_publication_authorships_to_claim': nr_publication_authorships_to_claim, - 'nr_submission_authorships_to_claim': nr_submission_authorships_to_claim, - 'nr_commentary_authorships_to_claim': nr_commentary_authorships_to_claim, - 'nr_thesis_authorships_to_claim': nr_thesis_authorships_to_claim, - 'nr_ref_inv_to_consider': nr_ref_inv_to_consider, - 'pending_ref_tasks': pending_ref_tasks, - 'refereeing_tab_total_count': refereeing_tab_total_count, - 'own_publications': own_publications, - 'own_submissions': own_submissions, - 'own_commentaries': own_commentaries, - 'own_thesislinks': own_thesislinks, - 'own_comments': own_comments, - 'own_authorreplies': own_authorreplies, + 'appellation': str(request.user), + 'needs_validation': False, } + try: + contributor = Contributor.objects.select_related('user').get(user=request.user) + context['needs_validation'] = contributor.status != CONTRIBUTOR_NORMAL + except Contributor.DoesNotExist: + contributor = None + context['user_groups'] = request.user.groups.values_list('name', flat=True) + + if contributor: + # Compile the unavailability periods: + now = timezone.now() + unavailabilities = contributor.unavailability_periods.exclude(end__lt=now).order_by('start') + unavailability_form = UnavailabilityPeriodForm() + + # if an editor, count the number of actions required: + nr_reg_to_vet = 0 + nr_reg_awaiting_validation = 0 + nr_submissions_to_assign = 0 + nr_recommendations_to_prepare_for_voting = 0 + if contributor.is_SP_Admin(): + # count the number of pending registration requests + nr_reg_to_vet = Contributor.objects.awaiting_vetting().count() + nr_reg_awaiting_validation = (Contributor.objects.awaiting_validation() + .count()) + nr_submissions_to_assign = Submission.objects.prescreening().count() + nr_recommendations_to_prepare_for_voting = EICRecommendation.objects.filter( + submission__status='voting_in_preparation').count() + + nr_assignments_to_consider = 0 + active_assignments = None + nr_reports_to_vet = 0 + if contributor.is_MEC(): + nr_assignments_to_consider = (contributor.editorial_assignments + .open().count()) + active_assignments = contributor.editorial_assignments.ongoing() + nr_reports_to_vet = (Report.objects.awaiting_vetting() + .filter(submission__editor_in_charge=contributor).count()) + + nr_commentary_page_requests_to_vet = 0 + nr_comments_to_vet = 0 + nr_thesislink_requests_to_vet = 0 + nr_authorship_claims_to_vet = 0 + if contributor.is_VE(): + nr_commentary_page_requests_to_vet = (Commentary.objects.awaiting_vetting() + .exclude(requested_by=contributor).count()) + nr_comments_to_vet = Comment.objects.awaiting_vetting().count() + nr_thesislink_requests_to_vet = ThesisLink.objects.awaiting_vetting().count() + nr_authorship_claims_to_vet = AuthorshipClaim.objects.awaiting_vetting().count() + + # Refereeing + nr_ref_inv_to_consider = contributor.referee_invitations.open().count() + pending_ref_tasks = contributor.referee_invitations.in_process() + refereeing_tab_total_count = nr_ref_inv_to_consider + len(pending_ref_tasks) + refereeing_tab_total_count += contributor.reports.in_draft().count() + + # Verify if there exist objects authored by this contributor, + # whose authorship hasn't been claimed yet + own_publications = contributor.publications.order_by('-publication_date') + own_submissions = contributor.submissions.filter(is_current=True).order_by('-submission_date') + own_commentaries = contributor.commentaries.order_by('-latest_activity') + own_thesislinks = contributor.theses.all() + nr_publication_authorships_to_claim = (Publication.objects.filter( + author_list__contains=contributor.user.last_name) + .exclude(authors=contributor) + .exclude(authors_claims=contributor) + .exclude(authors_false_claims=contributor) + .count()) + nr_submission_authorships_to_claim = (Submission.objects.filter( + author_list__contains=contributor.user.last_name) + .exclude(authors=contributor) + .exclude(authors_claims=contributor) + .exclude(authors_false_claims=contributor) + .count()) + nr_commentary_authorships_to_claim = (Commentary.objects.filter( + author_list__contains=contributor.user.last_name) + .exclude(authors=contributor) + .exclude(authors_claims=contributor) + .exclude(authors_false_claims=contributor) + .count()) + nr_thesis_authorships_to_claim = (ThesisLink.objects.filter( + author__contains=contributor.user.last_name) + .exclude(author_as_cont=contributor) + .exclude(author_claims=contributor) + .exclude(author_false_claims=contributor) + .count()) + own_comments = (contributor.comments.regular_comments() + .select_related('author', 'submission') + .order_by('-date_submitted')) + own_authorreplies = (contributor.comments.author_replies() + .order_by('-date_submitted')) + + appellation = contributor.get_title_display() + ' ' + contributor.user.last_name + + context.update({ + 'contributor': contributor, + 'appellation': appellation, + 'unavailabilities': unavailabilities, + 'unavailability_form': unavailability_form, + 'nr_reg_to_vet': nr_reg_to_vet, + 'nr_reg_awaiting_validation': nr_reg_awaiting_validation, + 'nr_commentary_page_requests_to_vet': nr_commentary_page_requests_to_vet, + 'nr_comments_to_vet': nr_comments_to_vet, + 'nr_thesislink_requests_to_vet': nr_thesislink_requests_to_vet, + 'nr_authorship_claims_to_vet': nr_authorship_claims_to_vet, + 'nr_reports_to_vet': nr_reports_to_vet, + 'nr_submissions_to_assign': nr_submissions_to_assign, + 'nr_recommendations_to_prepare_for_voting': nr_recommendations_to_prepare_for_voting, + 'nr_assignments_to_consider': nr_assignments_to_consider, + 'active_assignments': active_assignments, + 'nr_publication_authorships_to_claim': nr_publication_authorships_to_claim, + 'nr_submission_authorships_to_claim': nr_submission_authorships_to_claim, + 'nr_commentary_authorships_to_claim': nr_commentary_authorships_to_claim, + 'nr_thesis_authorships_to_claim': nr_thesis_authorships_to_claim, + 'nr_ref_inv_to_consider': nr_ref_inv_to_consider, + 'pending_ref_tasks': pending_ref_tasks, + 'refereeing_tab_total_count': refereeing_tab_total_count, + 'own_publications': own_publications, + 'own_submissions': own_submissions, + 'own_commentaries': own_commentaries, + 'own_thesislinks': own_thesislinks, + 'own_comments': own_comments, + 'own_authorreplies': own_authorreplies, + }) # Only add variables if user has right permission if request.user.has_perm('scipost.can_manage_reports'): diff --git a/submissions/__init__.py b/submissions/__init__.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1c2ca927d5cf740e7bfc6703a73f1e0f550ea623 100644 --- a/submissions/__init__.py +++ b/submissions/__init__.py @@ -0,0 +1 @@ +default_app_config = 'submissions.apps.SubmissionsConfig' diff --git a/submissions/apps.py b/submissions/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..51d7411cf0f3b4c3d4c41624e4b11733d3bded4f --- /dev/null +++ b/submissions/apps.py @@ -0,0 +1,19 @@ +from django.apps import AppConfig +from django.db.models.signals import post_save + + +class SubmissionsConfig(AppConfig): + name = 'submissions' + + def ready(self): + super().ready() + + from . import models, signals + post_save.connect(signals.notify_new_manuscript_submitted, + sender=models.Submission) + post_save.connect(signals.notify_new_editorial_recommendation, + sender=models.EICRecommendation) + post_save.connect(signals.notify_new_editorial_assignment, + sender=models.EditorialAssignment) + post_save.connect(signals.notify_new_referee_invitation, + sender=models.RefereeInvitation) diff --git a/submissions/forms.py b/submissions/forms.py index fe47475e57d4c1f80e52bd8bb1c246dddab27ba0..1fd5527db82fa9c742d3dc13888280e6852e2e85 100644 --- a/submissions/forms.py +++ b/submissions/forms.py @@ -12,7 +12,7 @@ from .constants import ASSIGNMENT_BOOL, ASSIGNMENT_REFUSAL_REASONS, STATUS_RESUB REPORT_ACTION_CHOICES, REPORT_REFUSAL_CHOICES, STATUS_REVISION_REQUESTED,\ STATUS_REJECTED, STATUS_REJECTED_VISIBLE, STATUS_RESUBMISSION_INCOMING,\ STATUS_DRAFT, STATUS_UNVETTED, REPORT_ACTION_ACCEPT, REPORT_ACTION_REFUSE,\ - STATUS_VETTED, EXPLICIT_REGEX_MANUSCRIPT_CONSTRAINTS + STATUS_VETTED, EXPLICIT_REGEX_MANUSCRIPT_CONSTRAINTS, SUBMISSION_STATUS from . import exceptions, helpers from .models import Submission, RefereeInvitation, Report, EICRecommendation, EditorialAssignment,\ iThenticateReport @@ -42,6 +42,22 @@ 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) + + def search(self, queryset, current_contributor=None): + 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']) + + if self.cleaned_data.get('editor_in_charge') and current_contributor: + queryset = queryset.filter(editor_in_charge=current_contributor) + + return queryset + + ############################### # Submission and resubmission # ############################### @@ -55,6 +71,7 @@ class SubmissionChecks: last_submission = None def __init__(self, *args, **kwargs): + self.requested_by = kwargs.pop('requested_by', None) super().__init__(*args, **kwargs) # Prefill `is_resubmission` property if data is coming from initial data if kwargs.get('initial', None): @@ -105,6 +122,11 @@ class SubmissionChecks: self.last_submission = submission if submission.status == STATUS_REVISION_REQUESTED: self.is_resubmission = True + if self.requested_by.contributor not in submission.authors.all(): + error_message = ('There exists a preprint with this arXiv identifier ' + 'but an earlier version number. Resubmission is only possible' + ' if you are a registered author of this manuscript.') + raise forms.ValidationError(error_message) elif submission.status in [STATUS_REJECTED, STATUS_REJECTED_VISIBLE]: error_message = ('This arXiv preprint has previously undergone refereeing ' 'and has been rejected. Resubmission is only possible ' @@ -222,7 +244,6 @@ class RequestSubmissionForm(SubmissionChecks, forms.ModelForm): } def __init__(self, *args, **kwargs): - self.requested_by = kwargs.pop('requested_by', None) super().__init__(*args, **kwargs) if not self.submission_is_resubmission(): diff --git a/submissions/managers.py b/submissions/managers.py index ca6bf98c7215811cffda6278abe18d7aa54fb85b..e3e15f6c51d24452110bbf3add0da166f5118642 100644 --- a/submissions/managers.py +++ b/submissions/managers.py @@ -199,6 +199,9 @@ class EditorialAssignmentQuerySet(models.QuerySet): def ongoing(self): return self.filter(completed=False).accepted() + def open(self): + return self.filter(accepted=None, deprecated=False) + class EICRecommendationManager(models.Manager): def get_for_user_in_pool(self, user): @@ -260,3 +263,9 @@ class RefereeInvitationQuerySet(models.QuerySet): def declined(self): return self.filter(accepted=False) + + def open(self): + return self.pending().filter(cancelled=False) + + def in_process(self): + return self.accepted().filter(fulfilled=False) diff --git a/submissions/migrations/0061_auto_20170727_1012.py b/submissions/migrations/0061_auto_20170727_1012.py index af6a59d02f96680652df95d98b9354de9c0fd5fd..68ee18e4cbec33f3ad1ac06f40af4c36c704bbe9 100644 --- a/submissions/migrations/0061_auto_20170727_1012.py +++ b/submissions/migrations/0061_auto_20170727_1012.py @@ -6,8 +6,6 @@ from django.db import migrations from guardian.shortcuts import assign_perm -from ..models import Report - def do_nothing(apps, schema_editor): return @@ -18,7 +16,7 @@ def update_eic_permissions(apps, schema_editor): Grant EIC of submission related to unvetted Reports permission to vet his submission's Report. """ - # Report = apps.get_model('submissions', 'Report') -- This doesn't work due to shitty imports + Report = apps.get_model('submissions', 'Report') count = 0 for rep in Report.objects.filter(status='unvetted'): eic_user = rep.submission.editor_in_charge diff --git a/submissions/migrations/0070_auto_20170925_2124.py b/submissions/migrations/0070_auto_20170925_2124.py new file mode 100644 index 0000000000000000000000000000000000000000..05381bc93ffe513e901c2074ad32db6938b26b58 --- /dev/null +++ b/submissions/migrations/0070_auto_20170925_2124.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-25 19:24 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0069_report_doideposit_needs_updating'), + ] + + operations = [ + migrations.AlterField( + model_name='submission', + name='author_comments', + field=models.TextField(blank=True, default=''), + preserve_default=False, + ), + ] diff --git a/submissions/migrations/0071_auto_20170928_2022.py b/submissions/migrations/0071_auto_20170928_2022.py new file mode 100644 index 0000000000000000000000000000000000000000..9cd9b9535cf0885adcbb6ada625ad17a5e19be0b --- /dev/null +++ b/submissions/migrations/0071_auto_20170928_2022.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-28 18:22 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0070_auto_20170925_2124'), + ] + + operations = [ + migrations.AlterField( + model_name='refereeinvitation', + name='referee', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='referee_invitations', to='scipost.Contributor'), + ), + migrations.AlterField( + model_name='submission', + name='submitted_by', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submitted_submissions', to='scipost.Contributor'), + ), + ] diff --git a/submissions/migrations/0072_auto_20170928_2022.py b/submissions/migrations/0072_auto_20170928_2022.py new file mode 100644 index 0000000000000000000000000000000000000000..e3b3b36fe8a4731e2fe187e8455c65a1c6fe799c --- /dev/null +++ b/submissions/migrations/0072_auto_20170928_2022.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-28 18:22 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0071_auto_20170928_2022'), + ] + + operations = [ + migrations.AlterField( + model_name='submission', + name='list_of_changes', + field=models.TextField(blank=True, default=''), + preserve_default=False, + ), + ] diff --git a/submissions/migrations/0073_auto_20170928_2023.py b/submissions/migrations/0073_auto_20170928_2023.py new file mode 100644 index 0000000000000000000000000000000000000000..bae204d2343331a0bc0b0cd8358dc6ed97f98064 --- /dev/null +++ b/submissions/migrations/0073_auto_20170928_2023.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-28 18:23 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0072_auto_20170928_2022'), + ] + + operations = [ + migrations.AlterField( + model_name='submission', + name='authors', + field=models.ManyToManyField(blank=True, related_name='submissions', to='scipost.Contributor'), + ), + migrations.AlterField( + model_name='submission', + name='authors_claims', + field=models.ManyToManyField(blank=True, related_name='claimed_submissions', to='scipost.Contributor'), + ), + migrations.AlterField( + model_name='submission', + name='authors_false_claims', + field=models.ManyToManyField(blank=True, related_name='false_claimed_submissions', to='scipost.Contributor'), + ), + ] diff --git a/submissions/migrations/0074_auto_20170928_2024.py b/submissions/migrations/0074_auto_20170928_2024.py new file mode 100644 index 0000000000000000000000000000000000000000..8c958fd639af3c8254b039c7a60b50a6da95e2c1 --- /dev/null +++ b/submissions/migrations/0074_auto_20170928_2024.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-28 18:24 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0073_auto_20170928_2023'), + ] + + operations = [ + migrations.AlterField( + model_name='submission', + name='referees_flagged', + field=models.TextField(blank=True, default=''), + preserve_default=False, + ), + ] diff --git a/submissions/migrations/0075_auto_20170928_2024.py b/submissions/migrations/0075_auto_20170928_2024.py new file mode 100644 index 0000000000000000000000000000000000000000..235763bdb328990da21bad47c2ae879ce9a44288 --- /dev/null +++ b/submissions/migrations/0075_auto_20170928_2024.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-28 18:24 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0074_auto_20170928_2024'), + ] + + operations = [ + migrations.AlterField( + model_name='submission', + name='referees_suggested', + field=models.TextField(blank=True, default=''), + preserve_default=False, + ), + ] diff --git a/submissions/migrations/0076_auto_20170928_2024.py b/submissions/migrations/0076_auto_20170928_2024.py new file mode 100644 index 0000000000000000000000000000000000000000..4ea15d274d0a9188c99c4668a754956d9abaf65b --- /dev/null +++ b/submissions/migrations/0076_auto_20170928_2024.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-28 18:24 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0075_auto_20170928_2024'), + ] + + operations = [ + migrations.AlterField( + model_name='submission', + name='remarks_for_editors', + field=models.TextField(blank=True, default=''), + preserve_default=False, + ), + ] diff --git a/submissions/models.py b/submissions/models.py index 65896271ea5b02c93bb43f65b0508f129c934139..401e6caef049192a906ea7e12a4d7cf2d41c4fa7 100644 --- a/submissions/models.py +++ b/submissions/models.py @@ -35,7 +35,7 @@ from journals.models import Publication ############### class Submission(models.Model): # Main submission fields - author_comments = models.TextField(blank=True, null=True) + author_comments = models.TextField(blank=True) author_list = models.CharField(max_length=1000, verbose_name="author list") discipline = models.CharField(max_length=20, choices=SCIPOST_DISCIPLINES, default='physics') domain = models.CharField(max_length=3, choices=SCIPOST_JOURNALS_DOMAINS) @@ -43,12 +43,12 @@ class Submission(models.Model): null=True, on_delete=models.CASCADE) is_current = models.BooleanField(default=True) is_resubmission = models.BooleanField(default=False) - list_of_changes = models.TextField(blank=True, null=True) + list_of_changes = models.TextField(blank=True) open_for_commenting = models.BooleanField(default=False) open_for_reporting = models.BooleanField(default=False) - referees_flagged = models.TextField(blank=True, null=True) - referees_suggested = models.TextField(blank=True, null=True) - remarks_for_editors = models.TextField(blank=True, null=True) + referees_flagged = models.TextField(blank=True) + referees_suggested = models.TextField(blank=True) + remarks_for_editors = models.TextField(blank=True) reporting_deadline = models.DateTimeField(default=timezone.now) secondary_areas = ChoiceArrayField( models.CharField(max_length=10, choices=SCIPOST_SUBJECT_AREAS), @@ -62,7 +62,8 @@ class Submission(models.Model): verbose_name='Primary subject area', default='Phys:QP') submission_type = models.CharField(max_length=10, choices=SUBMISSION_TYPE, blank=True, null=True, default=None) - submitted_by = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE) + submitted_by = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE, + related_name='submitted_submissions') # Replace this by foreignkey? submitted_to_journal = models.CharField(max_length=30, choices=SCIPOST_JOURNALS_SUBMIT, @@ -70,11 +71,11 @@ class Submission(models.Model): title = models.CharField(max_length=300) # Authors which have been mapped to contributors: - authors = models.ManyToManyField('scipost.Contributor', blank=True, related_name='authors_sub') + authors = models.ManyToManyField('scipost.Contributor', blank=True, related_name='submissions') authors_claims = models.ManyToManyField('scipost.Contributor', blank=True, - related_name='authors_sub_claims') + related_name='claimed_submissions') authors_false_claims = models.ManyToManyField('scipost.Contributor', blank=True, - related_name='authors_sub_false_claims') + related_name='false_claimed_submissions') abstract = models.TextField() # Comments can be added to a Submission @@ -275,12 +276,15 @@ class EditorialAssignment(SubmissionRelatedObjectMixin, models.Model): self.submission.title[:30] + ' by ' + self.submission.author_list[:30] + ', requested on ' + self.date_created.strftime('%Y-%m-%d')) + def get_absolute_url(self): + return reverse('submissions:assignment_request', args=(self.id,)) + class RefereeInvitation(SubmissionRelatedObjectMixin, models.Model): submission = models.ForeignKey('submissions.Submission', on_delete=models.CASCADE, related_name='referee_invitations') - referee = models.ForeignKey('scipost.Contributor', related_name='referee', blank=True, - null=True, on_delete=models.CASCADE) # Why is this blank/null=True + referee = models.ForeignKey('scipost.Contributor', related_name='referee_invitations', + blank=True, null=True, on_delete=models.CASCADE) title = models.CharField(max_length=4, choices=TITLE_CHOICES) first_name = models.CharField(max_length=30, default='') last_name = models.CharField(max_length=30, default='') @@ -353,7 +357,8 @@ class Report(SubmissionRelatedObjectMixin, models.Model): # `flagged' if author of report has been flagged by submission authors (surname check only) flagged = models.BooleanField(default=False) date_submitted = models.DateTimeField('date submitted') - author = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE) + author = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE, + related_name='reports') qualification = models.PositiveSmallIntegerField( choices=REFEREE_QUALIFICATION, verbose_name="Qualification to referee this: I am") @@ -502,9 +507,11 @@ class EICRecommendation(SubmissionRelatedObjectMixin, models.Model): # Editorial Fellows who have assessed this recommendation: eligible_to_vote = models.ManyToManyField('scipost.Contributor', blank=True, related_name='eligible_to_vote') - voted_for = models.ManyToManyField(Contributor, blank=True, related_name='voted_for') - voted_against = models.ManyToManyField(Contributor, blank=True, related_name='voted_against') - voted_abstain = models.ManyToManyField(Contributor, blank=True, related_name='voted_abstain') + voted_for = models.ManyToManyField('scipost.Contributor', blank=True, related_name='voted_for') + voted_against = models.ManyToManyField('scipost.Contributor', blank=True, + related_name='voted_against') + voted_abstain = models.ManyToManyField('scipost.Contributor', blank=True, + related_name='voted_abstain') voting_deadline = models.DateTimeField('date submitted', default=timezone.now) objects = EICRecommendationManager() @@ -513,6 +520,10 @@ class EICRecommendation(SubmissionRelatedObjectMixin, models.Model): return (self.submission.title[:20] + ' by ' + self.submission.author_list[:30] + ', ' + self.get_recommendation_display()) + def get_absolute_url(self): + # TODO: Fix this weird redirect, but it's neccesary for the notifications to have one. + return self.submission.get_absolute_url() + @property def nr_for(self): return self.voted_for.count() diff --git a/submissions/signals.py b/submissions/signals.py new file mode 100644 index 0000000000000000000000000000000000000000..664ff585d5255a68d32b3ef6c949d79ab58edda3 --- /dev/null +++ b/submissions/signals.py @@ -0,0 +1,51 @@ +from django.contrib.auth.models import User, Group + +from notifications.signals import notify + + +def notify_new_manuscript_submitted(sender, instance, created, **kwargs): + """ + Notify the Editorial Administration about a new Submission submitted. + """ + if created: + administrators = User.objects.filter(groups__name='Editorial Administrators') + for user in administrators: + notify.send(sender=sender, recipient=user, actor=instance.submitted_by, + verb=' submitted a new manuscript.', target=instance) + + +def notify_new_editorial_recommendation(sender, instance, created, **kwargs): + """ + Notify the Editorial Recommendation about a new Submission submitted. + """ + if created: + administrators = User.objects.filter(groups__name='Editorial Administrators') + editor_in_charge = instance.submission.editor_in_charge + for user in administrators: + notify.send(sender=sender, recipient=user, actor=editor_in_charge, + verb=' formulated a new Editorial Recommendation.', target=instance) + + +def notify_new_editorial_assignment(sender, instance, created, **kwargs): + """ + Notify a College Fellow about a new EIC invitation. + """ + if created: + administration = Group.objects.get(name='Editorial Administrators') + if instance.accepted: + # A new assignment is auto-accepted if user assigned himself or on resubmission. + text = ' assigned you Editor-in-charge.' + else: + text = ' invited you to become Editor-in-charge.' + notify.send(sender=sender, recipient=instance.to.user, actor=administration, + verb=text, target=instance) + + +def notify_new_referee_invitation(sender, instance, created, **kwargs): + """ + Notify a Referee about a new refereeing invitation. + """ + if created: + 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) diff --git a/submissions/templates/partials/submissions/admin/editorial_admin_summary.html b/submissions/templates/partials/submissions/admin/editorial_admin_summary.html deleted file mode 100644 index 65fdf4c9c44de594f708607afe66a71fbed717ca..0000000000000000000000000000000000000000 --- a/submissions/templates/partials/submissions/admin/editorial_admin_summary.html +++ /dev/null @@ -1,144 +0,0 @@ -{% load guardian_tags %} -{% load scipost_extras %} -{% load submissions_extras %} - - -<h5 class="pb-0">{{submission.get_subject_area_display}}</h5> -<h3 class="card-title"> - <a href="{{submission.get_absolute_url}}">{{submission.title}}</a> -</h3> - -<p class="card-text mb-3">by {{submission.author_list}}</p> -<h3>Info</h3> -<table class="text-muted w-100 mb-1"> - <tr> - <td style="min-width: 40%;">Version</td> - <td>{{submission.arxiv_vn_nr}} ({% if submission.is_current %}current version{% else %}deprecated version {{submission.arxiv_vn_nr}}{% endif %})</td> - </tr> - <tr> - <td>Submitted</td> - <td>{{submission.submission_date}} to {{submission.get_submitted_to_journal_display}}</td> - </tr> - - {% if submission.acceptance_date %} - <tr> - <td>Accepted</td> - <td>{{submission.acceptance_date}}</td> - </tr> - {% endif %} - - <tr> - <td>Latest activity</td> - <td>{{submission.latest_activity}}</td> - </tr> - <tr> - <td>Editor-in-charge</td> - <td> - {% if submission.editor_in_charge %} - {{ submission.editor_in_charge }} - {% 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 %} - - - {% endif %} - </td> - </tr> - <tr> - <td>Status</td> - <td>{{ submission.get_status_display }}</td> - </tr> - <tr> - <td>Refereeing cycle</td> - <td>{{ submission.get_refereeing_cycle_display }}</td> - </tr> - - {% include 'partials/submissions/refereeing_status_as_tr.html' with submission=submission %} - - <tr> - <td>Comments</td> - <td> - {{submission.comments.vetted.count}} - <span class="circle-clickable" data-toggle="tooltip" data-placement='bottom' data-html="true" title="{{submission.comments.regular_comments.vetted.count}} comments<br>{{submission.comments.author_replies.vetted.count}} author replies<hr>{{submission.comments.awaiting_vetting.count}} awaiting vetting">?</span> - </td> - </tr> - - <tr> - <td>Reporting deadline</td> - <td> - {% if submission.reporting_deadline > now %} - in {{submission.reporting_deadline|timeuntil}} - {% else %} - {{submission.reporting_deadline|timesince}} ago - {% endif %} - </td> - </tr> - - <tr> - <td>Plagiarism score</td> - <td> - {% if submission.plagiarism_report %} - {{ submission.plagiarism_report.score }}% - {% else %} - <a href="{% url 'submissions:plagiarism' submission.arxiv_identifier_w_vn_nr %}">Run plagiarism check</a> - {% endif %} - </td> - </tr> - -</table> -<a href="{% url 'submissions:editorial_page' submission.arxiv_identifier_w_vn_nr %}" class="d-inline-block mb-3">Go to Editorial Page</a> - -<h3>Actions</h3> - -<ul class="pl-4 mb-3"> - {# EIC Assignments #} - {% if perms.scipost.can_assign_submissions %} - {% if not submission.editor_in_charge %} - <li>EIC Assignment requests:</li> - <ul> - {% for assignment in submission.editorial_assignments.all %} - {% include 'submissions/_assignment_info.html' with assignment=assignment %} - {% empty %} - <li>None found. <a href="{% url 'submissions:assign_submission' submission.arxiv_identifier_w_vn_nr %}">Send a first assignment request</a></li> - {% endfor %} - </ul> - <li><a href="{% url 'submissions:assign_submission' submission.arxiv_identifier_w_vn_nr %}">Send a new assignment request</a></li> - <li><a href="{% url 'submissions:assignment_failed' submission.arxiv_identifier_w_vn_nr %}">Close pre-screening: failure to find EIC</a></li> - {% endif %} - {% endif %} - - {# Plagiarism #} - <li><a href="{% url 'submissions:plagiarism' submission.arxiv_identifier_w_vn_nr %}">Manage plagiarism report</a></li> - - {# Compile pdfs #} - {% if submission.reports.accepted.exists %} - <li><a href="{% url 'submissions:reports_accepted_list' %}?submission={{submission.arxiv_identifier_w_vn_nr}}">Compile accepted reports</a></li> - {% endif %} - - {# Communication #} - {% if submission.editor_in_charge %} - <li><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></li> - {% endif %} - - {# EIC Recommendations #} - {% if submission.eicrecommendations.exists %} - <li>See Editorial Recommendations:</li> - <ul> - {% for rec in submission.eicrecommendations.all %} - <li><a href="{% url 'submissions:eic_recommendation_detail' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr rec_id=rec.id %}">{{rec.get_recommendation_display}}</a></li> - {% endfor %} - </ul> - {% endif %} - - {# Accepted submission actions #} - {% if submission.status == 'accepted' %} - <li><a href="{% url 'submissions:treated_submission_pdf_compile' submission.arxiv_identifier_w_vn_nr %}">Update the Refereeing Package pdf</a></li> - <li>After proofs have been accepted, you can <a href="{% url 'journals:initiate_publication' %}">initiate the publication process</a> (leads to the validation page)</li> - {% endif %} -</ul> - - -<h3>Events</h3> -<a href="javascript:;" data-toggle="toggle" data-target="#eventslist">Show/hide events</a> -<div id="eventslist"> - {% include 'submissions/submission_event_list.html' with events=submission.events.for_eic %} -</div> diff --git a/submissions/templates/partials/submissions/admin/recommendation_tooltip.html b/submissions/templates/partials/submissions/admin/recommendation_tooltip.html new file mode 100644 index 0000000000000000000000000000000000000000..79e47405ebb8b2c351e301b36784232fccf40907 --- /dev/null +++ b/submissions/templates/partials/submissions/admin/recommendation_tooltip.html @@ -0,0 +1,9 @@ +<i class="fa fa-info-circle {{ classes }}" data-toggle="tooltip" data-html="true" title=" + Eligible to vote ({{ recommendation.eligible_to_vote.count }}) + <hr> + Agreed ({{ recommendation.voted_for.count }}) + <br> + Disagreed ({{ recommendation.voted_against.count }}) + <br> + Abstained ({{ recommendation.voted_abstain.count }}) +"></i> diff --git a/submissions/templates/partials/submissions/admin/submission_details.html b/submissions/templates/partials/submissions/admin/submission_details.html new file mode 100644 index 0000000000000000000000000000000000000000..39969bdc536c7e7dc0974c1fdb5ceaf89b2bd093 --- /dev/null +++ b/submissions/templates/partials/submissions/admin/submission_details.html @@ -0,0 +1,147 @@ +{% load guardian_tags %} +{% load scipost_extras %} +{% load submissions_extras %} + + +<div class="card border-secondary mt-2 submission-detail"> + <div class="card-body"> + <h5 class="pb-0">{{submission.get_subject_area_display}}</h5> + <h3 class="card-title"> + <a href="{{submission.get_absolute_url}}">{{submission.title}}</a> + </h3> + + <p class="card-text mb-3">by {{submission.author_list}}</p> + <h3>Info</h3> + <table class="text-muted w-100 mb-1"> + <tr> + <td style="min-width: 40%;">Version</td> + <td>{{submission.arxiv_vn_nr}} ({% if submission.is_current %}current version{% else %}deprecated version {{submission.arxiv_vn_nr}}{% endif %})</td> + </tr> + <tr> + <td>Submitted</td> + <td>{{submission.submission_date}} to {{submission.get_submitted_to_journal_display}}</td> + </tr> + + {% if submission.acceptance_date %} + <tr> + <td>Accepted</td> + <td>{{submission.acceptance_date}}</td> + </tr> + {% endif %} + + <tr> + <td>Latest activity</td> + <td>{{submission.latest_activity}}</td> + </tr> + <tr> + <td>Editor-in-charge</td> + <td> + {% if submission.editor_in_charge %} + {{ submission.editor_in_charge }} + {% 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 %} + - + {% endif %} + </td> + </tr> + <tr> + <td>Status</td> + <td>{{ submission.get_status_display }}</td> + </tr> + <tr> + <td>Refereeing cycle</td> + <td>{{ submission.get_refereeing_cycle_display }}</td> + </tr> + + {% include 'partials/submissions/refereeing_status_as_tr.html' with submission=submission %} + + <tr> + <td>Comments</td> + <td> + {{submission.comments.vetted.count}} + <span class="circle-clickable" data-toggle="tooltip" data-placement='bottom' data-html="true" title="{{submission.comments.regular_comments.vetted.count}} comments<br>{{submission.comments.author_replies.vetted.count}} author replies<hr>{{submission.comments.awaiting_vetting.count}} awaiting vetting">?</span> + </td> + </tr> + + <tr> + <td>Reporting deadline</td> + <td> + {% if submission.reporting_deadline > now %} + in {{submission.reporting_deadline|timeuntil}} + {% else %} + {{submission.reporting_deadline|timesince}} ago + {% endif %} + </td> + </tr> + + <tr> + <td>Plagiarism score</td> + <td> + {% if submission.plagiarism_report %} + {{ submission.plagiarism_report.score }}% + {% else %} + <a href="{% url 'submissions:plagiarism' submission.arxiv_identifier_w_vn_nr %}">Run plagiarism check</a> + {% endif %} + </td> + </tr> + + </table> + <a href="{% url 'submissions:editorial_page' submission.arxiv_identifier_w_vn_nr %}" class="d-inline-block mb-3">Go to Editorial Page</a> + + <h3>Actions</h3> + + <ul class="pl-4 mb-3"> + {# EIC Assignments #} + {% if perms.scipost.can_assign_submissions %} + {% if not submission.editor_in_charge %} + <li>EIC Assignment requests:</li> + <ul> + {% for assignment in submission.editorial_assignments.all %} + {% include 'submissions/_assignment_info.html' with assignment=assignment %} + {% empty %} + <li>None found. <a href="{% url 'submissions:assign_submission' submission.arxiv_identifier_w_vn_nr %}">Send a first assignment request</a></li> + {% endfor %} + </ul> + <li><a href="{% url 'submissions:assign_submission' submission.arxiv_identifier_w_vn_nr %}">Send a new assignment request</a></li> + <li><a href="{% url 'submissions:assignment_failed' submission.arxiv_identifier_w_vn_nr %}">Close pre-screening: failure to find EIC</a></li> + {% endif %} + {% endif %} + + {# Plagiarism #} + <li><a href="{% url 'submissions:plagiarism' submission.arxiv_identifier_w_vn_nr %}">Manage plagiarism report</a></li> + + {# Compile pdfs #} + {% if submission.reports.accepted.exists %} + <li><a href="{% url 'submissions:reports_accepted_list' %}?submission={{submission.arxiv_identifier_w_vn_nr}}">Compile accepted reports</a></li> + {% endif %} + + {# Communication #} + {% if submission.editor_in_charge %} + <li><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></li> + {% endif %} + + {# EIC Recommendations #} + {% if submission.eicrecommendations.exists %} + <li>See Editorial Recommendations:</li> + <ul> + {% for rec in submission.eicrecommendations.all %} + <li><a href="{% url 'submissions:eic_recommendation_detail' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr rec_id=rec.id %}">{{rec.get_recommendation_display}}</a></li> + {% endfor %} + </ul> + {% endif %} + + {# Accepted submission actions #} + {% if submission.status == 'accepted' %} + <li><a href="{% url 'submissions:treated_submission_pdf_compile' submission.arxiv_identifier_w_vn_nr %}">Update the Refereeing Package pdf</a></li> + <li>After proofs have been accepted, you can <a href="{% url 'journals:initiate_publication' %}">initiate the publication process</a> (leads to the validation page)</li> + {% endif %} + </ul> + + + <h3>Events</h3> + <div id="eventslist"> + {% include 'submissions/submission_event_list.html' with events=submission.events.for_eic %} + </div> + </div> +</div> diff --git a/submissions/templates/partials/submissions/admin/submission_li.html b/submissions/templates/partials/submissions/admin/submission_li.html new file mode 100644 index 0000000000000000000000000000000000000000..0581df05cb3f822992a09b38bb1357069bc57e87 --- /dev/null +++ b/submissions/templates/partials/submissions/admin/submission_li.html @@ -0,0 +1,24 @@ +<div class="row pool-item mb-0"> + <div class="icons{% if is_current %} text-info{% endif %}"> + {% include 'partials/submissions/admin/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="This Submission does not have a Editor-in-charge"></i> + {% endif %} + </div> + <div class="item col-auto"> + <p class="mb-1"> + <a href="{% url 'submissions:admin' submission.arxiv_identifier_w_vn_nr %}">{{ submission.title }}</a><br> + <em>by {{ submission.author_list }}</em> + </p> + + <p class="card-text mb-2"> + <a href="{% url 'submissions:admin' submission.arxiv_identifier_w_vn_nr %}" data-toggle="dynamic" data-target="#details" data-toggle="dynamic" data-target="">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> + {% endif %} + </p> + + <p class="label label-{% if submission.status == 'unassigned' %}outline-danger{% else %}secondary{% endif %} label-sm">{{ submission.get_status_display }}</p> + </div> +</div> diff --git a/submissions/templates/partials/submissions/admin/submission_tooltip.html b/submissions/templates/partials/submissions/admin/submission_tooltip.html index c2968b9a79145bbd96ea69ada144e497ee8f85a3..bdd541b910663d2361fb20b7bf4c1e85aa1ad565 100644 --- a/submissions/templates/partials/submissions/admin/submission_tooltip.html +++ b/submissions/templates/partials/submissions/admin/submission_tooltip.html @@ -1,4 +1,4 @@ -<span class="circle-clickable no-break" data-toggle="tooltip" data-html="true" +<i class="fa fa-info-circle" data-toggle="tooltip" data-html="true" title=" {{submission.arxiv_identifier_w_vn_nr}}<hr>Status: {{submission.get_status_display}}<br>Latest activity: {{submission.latest_activity}} -">?</span> +"></i> diff --git a/submissions/templates/partials/submissions/pool/required_actions_tooltip.html b/submissions/templates/partials/submissions/pool/required_actions_tooltip.html new file mode 100644 index 0000000000000000000000000000000000000000..1e9ce2a7234240d0a5ef96e121cb45c37f694b06 --- /dev/null +++ b/submissions/templates/partials/submissions/pool/required_actions_tooltip.html @@ -0,0 +1,10 @@ +{% if submission.cycle.has_required_actions and submission.cycle.get_required_actions %} + <i class="fa fa-exclamation-circle {{ classes }}" data-toggle="tooltip" data-html="true" title=" + Required Actions: + <ul class='mb-0 pl-3 text-left'> + {% for action in submission.cycle.get_required_actions %} + <li>{{action.1}}</li> + {% endfor %} + </ul> + "></i> +{% endif %} diff --git a/submissions/templates/partials/submissions/pool/submission_details.html b/submissions/templates/partials/submissions/pool/submission_details.html new file mode 100644 index 0000000000000000000000000000000000000000..4b45732367b38ee70410bf1231deabbc75c5fc9e --- /dev/null +++ b/submissions/templates/partials/submissions/pool/submission_details.html @@ -0,0 +1,60 @@ +{% load guardian_tags %} +{% load scipost_extras %} +{% load submissions_extras %} + + +<div class="card submission-detail"> + {% include 'submissions/_submission_card_fellow_content.html' with submission=submission %} + + <div class="card-body"> + <h3>Remarks on this submission:</h3> + {% if remark_form %} + {% include 'submissions/_remark_add_form.html' with submission=submission form=remark_form auto_show=1 %} + {% endif %} + <p class="mb-1">Current remarks:</p> + <ul> + {% for rem in submission.remarks.all %} + {% include 'scipost/_remark_li.html' with remark=rem %} + {% empty %} + <li>No Remarks found.</li> + {% endfor %} + </ul> + + + + {% get_obj_perms request.user for submission as "sub_perms" %} + {% if "can_take_editorial_actions" in sub_perms or is_ECAdmin %} + {% 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 perms.scipost.can_assign_submissions %} + {% if submission.editorial_assignments.exists %} + <h4>EIC Assignment requests:</h4> + <ul> + {% for assignment in submission.editorial_assignments.all %} + {% include 'submissions/_assignment_info.html' with assignment=assignment %} + {% endfor %} + </ul> + {% endif %} + {% if submission.editor_in_charge == None %} + <h4>Actions:</h4> + <ul> + <li><a href="{% url 'submissions:assign_submission' submission.arxiv_identifier_w_vn_nr %}">Send a new assignment request</a></li> + <li><a href="{% url 'submissions:assignment_failed' submission.arxiv_identifier_w_vn_nr %}">Close pre-screening: failure to find EIC</a></li> + </ul> + {% endif %} + {% endif %} + + {% if is_ECAdmin %} + <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> + {% if submission.status == 'accepted' %} + <h4>After proofs have been accepted, you can <a href="{% url 'journals:initiate_publication' %}">initiate the publication process</a> (leads to the validation page)</h4> + {% endif %} + {% endif %} + </div> +</div> diff --git a/submissions/templates/partials/submissions/pool/submission_li.html b/submissions/templates/partials/submissions/pool/submission_li.html new file mode 100644 index 0000000000000000000000000000000000000000..73bc7fa516e69125906942aef32ac11b5fbac287 --- /dev/null +++ b/submissions/templates/partials/submissions/pool/submission_li.html @@ -0,0 +1,37 @@ +<div class="row pool-item mb-0"> + <div class="icons{% if is_current %} text-info{% endif %}"> + {% include 'partials/submissions/admin/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> + + <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> + {% 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> + {% 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 %} + + <p class="label label-{% if submission.status == 'unassigned' %}outline-danger{% else %}secondary{% endif %} label-sm">{{ submission.get_status_display }}</p> + + </div> +</div> diff --git a/submissions/templates/submissions/_remark_add_form.html b/submissions/templates/submissions/_remark_add_form.html index 1541145e29fca3ec142bae5ec2d6fde733a54bea..f2872b2a19755ab5d12e5f230171d049ad97a865 100644 --- a/submissions/templates/submissions/_remark_add_form.html +++ b/submissions/templates/submissions/_remark_add_form.html @@ -16,11 +16,19 @@ $(document).ready(function(){ }); </script> -<button class="btn btn-secondary mb-2" data-toggle="toggle" data-target="#remarkForm{{ submission.id }}" id="remarkButton{{ submission.id }}">Add a remark on this Submission</button> -<div class="submitRemarkForm pb-2" id="remarkForm{{ submission.id }}" style="display:none;"> - <form action="{% url 'submissions:add_remark' submission.arxiv_identifier_w_vn_nr %}" method="post"> +{% if auto_show %} + <form action="{% url 'submissions:add_remark' submission.arxiv_identifier_w_vn_nr %}" method="post" class="pb-2"> {% csrf_token %} {{ form|bootstrap:'0,12' }} <input class="btn btn-secondary" type="submit" value="Submit" /> </form> -</div> +{% else %} + <button class="btn btn-secondary mb-2" data-toggle="toggle" data-target="#remarkForm{{ submission.id }}" id="remarkButton{{ submission.id }}">Add a remark on this Submission</button> + <div class="submitRemarkForm pb-2" id="remarkForm{{ submission.id }}" style="display:none;"> + <form action="{% url 'submissions:add_remark' submission.arxiv_identifier_w_vn_nr %}" method="post"> + {% csrf_token %} + {{ form|bootstrap:'0,12' }} + <input class="btn btn-secondary" type="submit" value="Submit" /> + </form> + </div> +{% endif %} diff --git a/submissions/templates/submissions/_submission_assignment_request.html b/submissions/templates/submissions/_submission_assignment_request.html index cceb540850b6a489ae5161372e3c018177ad76dd..4f274474ef169f87858842bf39fdadcec8618868 100644 --- a/submissions/templates/submissions/_submission_assignment_request.html +++ b/submissions/templates/submissions/_submission_assignment_request.html @@ -8,7 +8,7 @@ <h1>Accept or Decline this Assignment</h1> <h3 class="mb-2">By accepting, you will be required to start a refereeing round on the next screen.</h3> - <form action="{% url 'submissions:accept_or_decline_assignment_ack' assignment_id=assignment.id %}" method="post"> + <form action="{% url 'submissions:assignment_request' assignment_id=assignment.id %}" method="post"> {% csrf_token %} <div class="form-group row"> <div class="col-12"> diff --git a/submissions/templates/submissions/accept_or_decline_assignment_ack.html b/submissions/templates/submissions/accept_or_decline_assignment_ack.html deleted file mode 100644 index 67a7551cca06e5c12e852c533d727220874f89e1..0000000000000000000000000000000000000000 --- a/submissions/templates/submissions/accept_or_decline_assignment_ack.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends 'scipost/base.html' %} - -{% block pagetitle %}: accept or decline assignment (ack){% endblock pagetitle %} - -{% block content %} - - - {% if errormessage %} - <p>{{ errormessage }}</p> - <p>Return to the <a href="{% url 'submissions:pool' %}">Submissions Pool</a>.</p> - - {% elif assignment.accepted %} - <h1>Thank you for becoming Editor-in-charge of this submission.</h1> - <p>Please go to the <a href="{% url 'submissions:editorial_page' arxiv_identifier_w_vn_nr=assignment.submission.arxiv_identifier_w_vn_nr %}">Submission's editorial page</a> and select referees now.</p> - {% else %} - <h1>Thank you for considering.</h1> - <p>Return to the <a href="{% url 'submissions:pool' %}">Submissions Pool</a>.</p> - {% endif %} - -{% endblock content %} diff --git a/submissions/templates/submissions/admin/base.html b/submissions/templates/submissions/admin/base.html new file mode 100644 index 0000000000000000000000000000000000000000..e238b581c8e1952ae403c814db783bf57f98f4ea --- /dev/null +++ b/submissions/templates/submissions/admin/base.html @@ -0,0 +1,13 @@ +{% extends 'scipost/base.html' %} + +{% block breadcrumb %} + <nav class="breadcrumb py-md-2 px-0 hidden-sm-down"> + <div class="container"> + {% block breadcrumb_items %} + <a href="{% url 'submissions:admin' %}" class="breadcrumb-item">Editorial Administration</a> + {% endblock %} + </div> + </nav> +{% endblock %} + +{% block container_class %}{{block.super}} pb-5{% endblock container_class %} diff --git a/submissions/templates/submissions/admin/editorial_admin.html b/submissions/templates/submissions/admin/editorial_admin.html index f4daca1875640058f3f10369785d8e4747bdc7ab..8c23d6beb54f3791e2f0fda8b08b3590c74bd77d 100644 --- a/submissions/templates/submissions/admin/editorial_admin.html +++ b/submissions/templates/submissions/admin/editorial_admin.html @@ -1,12 +1,15 @@ -{% extends 'scipost/_personal_page_base.html' %} +{% extends 'submissions/admin/base.html' %} + +{% load scipost_extras %} {% block pagetitle %}: Editorial Administration{% endblock pagetitle %} {% block breadcrumb_items %} - {{block.super}} - <span class="breadcrumb-item">Editorial Administration</span> + {{ block.super }} + <span class="breadcrumb-item">{% if submission %}{{ submission.arxiv_identifier_w_vn_nr }}{% else %}All events in the last 24 hours{% endif %}</span> {% endblock %} +{% block body_class %}{{ block.super }} editorial-admin{% endblock %} {% block content %} <div class="row"> @@ -20,26 +23,38 @@ <a href="{% url 'submissions:pool' %}">Go to the pool</a> </p> + {% if recommendations_to_prepare_for_voting %} + <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 %} + <li>On Editorial Recommendation: {{ recommendation }}<br> + <a href="{% url 'submissions:prepare_for_voting' rec_id=recommendation.id %}">Prepare for voting</a> + </li> + {% endfor %} + </ul> + {% endif %} + + {% if recommendations_undergoing_voting %} + <h3>Recommendations undergoing voting <i class="fa fa-exclamation-circle text-warning"></i></h3> + <ul class="fa-ul"> + {% for recommendation in recommendations_undergoing_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> + </li> + {% endfor %} + </ul> + {% endif %} + + {% if recommendations_to_prepare_for_voting or recommendations_undergoing_voting %} + <hr> + {% endif %} + <h3>Submissions currently in pre-screening</h3> - <ul class="list-unstyled"> + <ul class="list-unstyled" data-target="active-list"> {% for sub in submission_list.prescreening %} - <li> - {% include 'partials/submissions/admin/submission_tooltip.html' with submission=sub %} - {% if sub == submission %} - <strong> - <a href="?submission={{sub.arxiv_identifier_w_vn_nr}}">{{sub.title}}</a> - <div class="pl-md-4"> - <em>by {{sub.author_list}}</em><br> - latest activity: {{sub.latest_activity|timesince}} ago - </div> - </strong> - {% else %} - <a href="?submission={{sub.arxiv_identifier_w_vn_nr}}">{{sub.title}}</a> - <div class="pl-md-4"> - <em>by {{sub.author_list}}</em><br> - latest activity: {{sub.latest_activity|timesince}} ago - </div> - {% endif %} + <li class="p-2{% if sub == submission %} active{% endif %}"> + {% include 'partials/submissions/admin/submission_li.html' with submission=sub %} </li> {% empty %} <li>No Submissions are currently in pre-screening</li> @@ -47,25 +62,10 @@ </ul> <h3>Submissions currently in refereeing round</h3> - <ul class="list-unstyled"> + <ul class="list-unstyled" data-target="active-list"> {% for sub in submission_list.actively_refereeing %} - <li> - {% include 'partials/submissions/admin/submission_tooltip.html' with submission=sub %} - {% if sub == submission %} - <strong> - <a href="?submission={{sub.arxiv_identifier_w_vn_nr}}">{{sub.title}}</a> - <div class="pl-md-4"> - <em>by {{sub.author_list}}</em><br> - latest activity: {{sub.latest_activity|timesince}} ago - </div> - </strong> - {% else %} - <a href="?submission={{sub.arxiv_identifier_w_vn_nr}}">{{sub.title}}</a> - <div class="pl-md-4"> - <em>by {{sub.author_list}}</em><br> - latest activity: {{sub.latest_activity|timesince}} ago - </div> - {% endif %} + <li class="p-2{% if sub == submission %} active{% endif %}"> + {% include 'partials/submissions/admin/submission_li.html' with submission=sub %} </li> {% empty %} <li>No Submissions are currently in refereeing round</li> @@ -73,25 +73,10 @@ </ul> <h3>Submissions accepted</h3> - <ul class="list-unstyled"> + <ul class="list-unstyled" data-target="active-list"> {% for sub in submission_list.accepted %} - <li> - {% include 'partials/submissions/admin/submission_tooltip.html' with submission=sub %} - {% if sub == submission %} - <strong> - <a href="?submission={{sub.arxiv_identifier_w_vn_nr}}">{{sub.title}}</a> - <div class="pl-md-4"> - <em>by {{sub.author_list}}</em><br> - latest activity: {{sub.latest_activity|timesince}} ago - </div> - </strong> - {% else %} - <a href="?submission={{sub.arxiv_identifier_w_vn_nr}}">{{sub.title}}</a> - <div class="pl-md-4"> - <em>by {{sub.author_list}}</em><br> - latest activity: {{sub.latest_activity|timesince}} ago - </div> - {% endif %} + <li class="p-2{% if sub == submission %} active{% endif %}"> + {% include 'partials/submissions/admin/submission_li.html' with submission=sub %} </li> {% empty %} <li>All accepted Submissions are published</li> @@ -99,13 +84,9 @@ </ul> </div> - <div class="col-md-5"> + <div class="col-md-5" id="details"> {% if submission %} - <div class="card border-secondary mt-2"> - <div class="card-body"> - {% include 'partials/submissions/admin/editorial_admin_summary.html' with submission=submission %} - </div> - </div> + {% include 'partials/submissions/admin/submission_details.html' with submission=submission %} {% else %} <h3><em>Click on a submission to see its summary and actions</em></h3> <h2>All events in the last 24 hours</h2> diff --git a/submissions/templates/submissions/admin/eic_recommendation_detail.html b/submissions/templates/submissions/admin/eic_recommendation_detail.html index e453c3dc64de64d4d7a1ae07d45b33b4dc8532eb..f23a39ca8801f4f05d342ea65aab60d0368eaa4e 100644 --- a/submissions/templates/submissions/admin/eic_recommendation_detail.html +++ b/submissions/templates/submissions/admin/eic_recommendation_detail.html @@ -1,4 +1,4 @@ -{% extends 'submissions/_pool_base.html' %} +{% extends 'submissions/admin/base.html' %} {% block pagetitle %}: editorial recommendation for submission{% endblock pagetitle %} @@ -7,7 +7,6 @@ {% block breadcrumb_items %} {{block.super}} - <a href="{% url 'submissions:admin' %}?submission={{submission.arxiv_identifier_w_vn_nr}}" class="breadcrumb-item">Editorial Administration</a> <span class="breadcrumb-item">Editorial Recommendation</span> {% endblock %} diff --git a/submissions/templates/submissions/admin/plagiarism_report.html b/submissions/templates/submissions/admin/plagiarism_report.html index 2b13c4fb62ec96eb757ffee4a64fbf7416a45963..8aa8d6f2d491233a829d57123779d107ae7038d4 100644 --- a/submissions/templates/submissions/admin/plagiarism_report.html +++ b/submissions/templates/submissions/admin/plagiarism_report.html @@ -1,4 +1,4 @@ -{% extends 'scipost/_personal_page_base.html' %} +{% extends 'submissions/admin/base.html' %} {% load bootstrap %} @@ -6,7 +6,6 @@ {% block breadcrumb_items %} {{block.super}} - <a href="{% url 'submissions:admin' %}?submission={{submission.arxiv_identifier_w_vn_nr}}" class="breadcrumb-item">Editorial Administration</a> <span class="breadcrumb-item">Plagiarism Report ({{ submission.arxiv_identifier_w_vn_nr }})</span> {% endblock %} diff --git a/submissions/templates/submissions/admin/recommendation.html b/submissions/templates/submissions/admin/recommendation.html new file mode 100644 index 0000000000000000000000000000000000000000..a4eed96cbcd805ad7f90cddf7655bfa75873fdae --- /dev/null +++ b/submissions/templates/submissions/admin/recommendation.html @@ -0,0 +1,84 @@ +{% extends 'submissions/pool/base.html' %} + +{% load bootstrap %} +{% load scipost_extras %} + +{% block breadcrumb_items %} + <a href="{% url 'submissions:admin' %}" class="breadcrumb-item">Editorial Administration</a> + <span class="breadcrumb-item">Editorial Recommendation</span> +{% endblock %} + +{% block pagetitle %}: Editorial Recommendation{% endblock pagetitle %} + +{% block content %} + <h1 class="highlight">Editorial Recommendation</h1> + + <div class="card card-outline-secondary"> + {% include 'submissions/_submission_card_fellow_content_sparse.html' with submission=object.submission %} + </div> + + <div class="card card-outline-secondary"> + {% include 'submissions/_recommendation_fellow_content.html' with recommendation=object %} + <div class="card-body"> + {% if object.remarks.exists %} + <h3 class="card-title">Remarks by Fellows:</h3> + <ul> + {% for remark in object.remarks.all|sort_by:'date' %} + {% include 'partials/submissions/remark_as_li.html' with remark=remark %} + {% endfor %} + </ul> + {% endif %} + + <h3 class="card-title">Fellows eligible to vote:</h3> + <ul> + <li> + {% for eligible in object.eligible_to_vote.all|sort_by:'user__last_name' %} + {{ eligible.user.last_name }}, + {% endfor %} + </li> + </ul> + + <h3 class="card-title">Voting results up to now:</h3> + <ul> + <li> + Agreed: ({{ object.voted_for.all.count }}) + {% for agreed in object.voted_for.all|sort_by:'user__last_name' %} + {{ agreed.user.last_name }}, + {% endfor %} + </li> + <li> + Disagreed: ({{ object.voted_against.all.count }}) + {% for disagreed in object.voted_against.all|sort_by:'user__last_name' %} + {{ disagreed.user.last_name }}, + {% endfor %} + </li> + <li> + Abstained: ({{ object.voted_abstain.all.count }}) + {% for abstained in object.voted_abstain.all|sort_by:'user__last_name' %} + {{ abstained.user.last_name }}, + {% endfor %} + </li> + </ul> + + {% if object.remarks.exists %} + <h3 class="card-title">Remarks:</h3> + <ul> + {% for rem in object.remarks.all %} + <li>{{ rem }}</li> + {% empty %} + <li><em>No remarks</em></li> + {% endfor %} + </ul> + {% endif %} + </div> + <div class="card-footer"> + <h3 class="card-title">Administrative actions on recommendations undergoing voting:</h3> + <ul> + <li>To send an email reminder to each Fellow with at least one voting duty: <a href="{% url 'submissions:remind_Fellows_to_vote' %}">click here</a></li> + <li>To fix the College decision and follow the Editorial Recommendation as is: <a href="{% url 'submissions:fix_College_decision' rec_id=object.id %}">click here</a></li> + <li>To request a modification of the Recommendation to request for revision: click here</li> + </ul> + </div> + </div> + +{% endblock %} diff --git a/submissions/templates/submissions/pool/assignment_request.html b/submissions/templates/submissions/pool/assignment_request.html new file mode 100644 index 0000000000000000000000000000000000000000..2f0b46cde76c735220de426c609285ce8f891ef8 --- /dev/null +++ b/submissions/templates/submissions/pool/assignment_request.html @@ -0,0 +1,20 @@ +{% extends 'submissions/pool/base.html' %} + +{% load bootstrap %} +{% load guardian_tags %} +{% load scipost_extras %} +{% load submissions_extras %} + +{% block breadcrumb_items %} + {{ block.super }} + <span class="breadcrumb-item">Assignment Request</span> +{% endblock %} + +{% block pagetitle %}: Assignment Request{% endblock pagetitle %} + +{% block content %} + <h1 class="highlight">Assignment request</h1> + <h3>Can you act as Editor-in-charge? (see below to accept/decline)</h3> + + {% include 'submissions/_submission_assignment_request.html' with assignment=assignment consider_assignment_form=form %} +{% endblock %} diff --git a/submissions/templates/submissions/pool/base.html b/submissions/templates/submissions/pool/base.html new file mode 100644 index 0000000000000000000000000000000000000000..ca638eb5ab558e81c5911ad27e2788d812086d5a --- /dev/null +++ b/submissions/templates/submissions/pool/base.html @@ -0,0 +1,13 @@ +{% extends 'scipost/base.html' %} + +{% block body_class %}{{ block.super }} pool{% endblock %} + +{% block breadcrumb %} + <nav class="breadcrumb py-md-2 px-0"> + <div class="container"> + {% block breadcrumb_items %} + <a href="{% url 'submissions:pool' %}" class="breadcrumb-item">Pool</a> + {% endblock %} + </div> + </nav> +{% endblock %} diff --git a/submissions/templates/submissions/pool/pool.html b/submissions/templates/submissions/pool/pool.html new file mode 100644 index 0000000000000000000000000000000000000000..f304a5068691ef1752024a24e507eb474d131f58 --- /dev/null +++ b/submissions/templates/submissions/pool/pool.html @@ -0,0 +1,90 @@ +{% extends 'submissions/pool/base.html' %} + +{% load bootstrap %} +{% load guardian_tags %} +{% load scipost_extras %} +{% load submissions_extras %} + +{% block breadcrumb_items %} + <a href="{% url 'scipost:personal_page' %}" class="breadcrumb-item">Personal Page</a> + {% if submission %} + <a href="{% url 'submissions:pool' %}" class="breadcrumb-item">Pool</a> + <span class="breadcrumb-item">{{ submission.arxiv_identifier_w_vn_nr }}</span> + {% else %} + <span class="breadcrumb-item">Pool</span> + {% endif %} +{% endblock %} + +{% block pagetitle %}: Submissions Pool{% endblock pagetitle %} + +{% block content %} + {% with is_ECAdmin=request.user|is_in_group:'Editorial Administrators' %} + <a href="{% url 'submissions:pool' %}?test=1">See old pool layout</a> + <div class="row"> + <div class="col-md-7"> + <h1>SciPost Submissions Pool</h1> + {% if is_ECAdmin %} + <a href="{% url 'submissions:admin' %}">Go to the Editorial Administration</a> + {% endif %} + + {% if assignments_to_consider %} + <h3>Your open Assignment Requests <i class="fa fa-exclamation-circle text-warning"></i></h3> + <ul> + {% for assignment in assignments_to_consider %} + <li>On submission: {{ assignment.submission }}<br> + <a href="{% url 'submissions:assignment_request' assignment.id %}">Accept or decline here</a> + </li> + {% endfor %} + </ul> + {% endif %} + + {% if recs_to_vote_on %} + <h3>Recommendations to vote on <i class="fa fa-exclamation-circle text-warning"></i></h3> + <ul> + {% for recommendation in recs_to_vote_on %} + <li>On Editorial Recommendation of: {{ recommendation.submission }}<br> + <a href="{% url 'submissions:vote_on_rec' rec_id=recommendation.id %}">See the Editorial Recommendation</a> + </li> + {% endfor %} + </ul> + {% endif %} + + {% if assignments_to_consider or recs_to_vote_on %} + <hr> + {% endif %} + + {% if search_form %} + <h3>Filter by status</h3> + <form method="get" class="auto-submit mb-3"> + {{ search_form|bootstrap:'12,12' }} + </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 %} + {% else %} + <h3><em>Click on a submission to see its summary and actions</em></h3> + {% endif %} + </div> + </div> + {% endwith %} +{% endblock %} diff --git a/submissions/templates/submissions/pool/recommendation.html b/submissions/templates/submissions/pool/recommendation.html new file mode 100644 index 0000000000000000000000000000000000000000..d2c050bbb0c1b3090e5f8c6d83a35bb2c81c6b24 --- /dev/null +++ b/submissions/templates/submissions/pool/recommendation.html @@ -0,0 +1,29 @@ +{% extends 'submissions/pool/base.html' %} + +{% load bootstrap %} + +{% block breadcrumb_items %} + {{ block.super }} + <span class="breadcrumb-item">Editorial Recommendation</span> +{% endblock %} + +{% block pagetitle %}: Editorial Recommendation{% endblock pagetitle %} + +{% block content %} + <h1>Editorial Recommendation to vote on</h1> + + {% include 'submissions/_submission_card_fellow_content.html' with submission=recommendation.submission %} + + {# <div class="card card-outline-secondary">#} + {% include 'submissions/_recommendation_fellow_content.html' with recommendation=recommendation %} + <div class="card-footer"> + <h3>Your position on this recommendation</h3> + <form action="{% url 'submissions:vote_on_rec' rec_id=recommendation.id %}" method="post"> + {% csrf_token %} + {{ form|bootstrap:'0,12' }} + <input type="submit" name="submit" value="Cast your vote" class="btn btn-primary submitButton" id="submit-id-submit"> + </form> + </div> + {# </div>#} + +{% endblock %} diff --git a/submissions/urls.py b/submissions/urls.py index c1f46c9ab3839e1758c764de519a0ae91a24eecf..6cd83e4ec6d855ebde05931aa76a0cafb7b863b5 100644 --- a/submissions/urls.py +++ b/submissions/urls.py @@ -26,6 +26,8 @@ urlpatterns = [ # Editorial Administration url(r'^admin$', views.EditorialSummaryView.as_view(), name='admin'), + url(r'^admin/{regex}$'.format(regex=SUBMISSIONS_COMPLETE_REGEX), + views.EditorialSummaryView.as_view(), name='admin'), url(r'^admin/treated$', views.treated_submissions_list, name='treated_submissions_list'), url(r'^admin/{regex}/reports/compile$'.format(regex=SUBMISSIONS_COMPLETE_REGEX), views.treated_submission_pdf_compile, name='treated_submission_pdf_compile'), @@ -40,13 +42,16 @@ urlpatterns = [ views.report_pdf_compile, name='report_pdf_compile'), url(r'^admin/reports/(?P<report_id>[0-9]+)/compile$', views.report_pdf_compile, name='report_pdf_compile'), + url(r'^admin/{regex}/recommendation$'.format(regex=SUBMISSIONS_COMPLETE_REGEX), + views.AdminRecommendationView.as_view(), name='admin_recommendation'), url(r'^submit_manuscript$', views.RequestSubmission.as_view(), name='submit_manuscript'), url(r'^submit_manuscript/prefill$', views.prefill_using_arxiv_identifier, name='prefill_using_identifier'), - url(r'^pool$', views.pool, name='pool'), + url(r'^pool/$', views.pool, name='pool'), + url(r'^pool/{regex}/$'.format(regex=SUBMISSIONS_COMPLETE_REGEX), views.pool, name='pool'), url(r'^submissions_by_status/(?P<status>[a-zA-Z_]+)$', - views.submissions_by_status, name='submissions_by_status'), + views.submissions_by_status, name='submissions_by_status'), # DEPRECATED url(r'^add_remark/{regex}$'.format(regex=SUBMISSIONS_COMPLETE_REGEX), views.add_remark, name='add_remark'), @@ -55,8 +60,8 @@ urlpatterns = [ views.assign_submission, name='assign_submission'), url(r'^assign_submission_ack/{regex}$'.format(regex=SUBMISSIONS_COMPLETE_REGEX), views.assign_submission_ack, name='assign_submission_ack'), - url(r'^accept_or_decline_assignment_ack/(?P<assignment_id>[0-9]+)$', - views.accept_or_decline_assignment_ack, name='accept_or_decline_assignment_ack'), + url(r'^pool/assignment_request/(?P<assignment_id>[0-9]+)$', + views.assignment_request, name='assignment_request'), url(r'^volunteer_as_EIC/{regex}$'.format(regex=SUBMISSIONS_COMPLETE_REGEX), views.volunteer_as_EIC, name='volunteer_as_EIC'), url(r'^assignment_failed/{regex}$'.format(regex=SUBMISSIONS_COMPLETE_REGEX), diff --git a/submissions/views.py b/submissions/views.py index fa5058d73c28450e2ecb096ecf2a806cbb636b03..7007d5d231c2a9af3164314afbd0f38d241bee4b 100644 --- a/submissions/views.py +++ b/submissions/views.py @@ -31,13 +31,14 @@ from .forms import SubmissionIdentifierForm, RequestSubmissionForm, SubmissionSe ConsiderRefereeInvitationForm, EditorialCommunicationForm,\ EICRecommendationForm, ReportForm, VetReportForm, VotingEligibilityForm,\ SubmissionCycleChoiceForm, ReportPDFForm, SubmissionReportsForm,\ - iThenticateReportForm + iThenticateReportForm, SubmissionPoolFilterForm from .utils import SubmissionUtils from mails.views import MailEditingSubView from scipost.forms import ModifyPersonalMessageForm, RemarkForm from scipost.models import Contributor, Remark, RegistrationInvitation from scipost.utils import Utils +from scipost.permissions import is_tester from comments.forms import CommentForm from production.models import ProductionStream @@ -95,10 +96,11 @@ class RequestSubmission(CreateView): @login_required @permission_required('scipost.can_submit_manuscript', raise_exception=True) def prefill_using_arxiv_identifier(request): - query_form = SubmissionIdentifierForm(request.POST or None, initial=request.GET or None) + query_form = SubmissionIdentifierForm(request.POST or None, initial=request.GET or None, + requested_by=request.user) if query_form.is_valid(): prefill_data = query_form.request_arxiv_preprint_form_prefill_data() - form = RequestSubmissionForm(initial=prefill_data) + form = RequestSubmissionForm(initial=prefill_data, requested_by=request.user) # Submit message to user if query_form.submission_is_resubmission(): @@ -324,7 +326,7 @@ def editorial_workflow(request): @login_required @permission_required('scipost.can_view_pool', raise_exception=True) -def pool(request): +def pool(request, arxiv_identifier_w_vn_nr=None): """ The Submissions pool contains all submissions which are undergoing the editorial process, from submission @@ -335,14 +337,13 @@ def pool(request): .prefetch_related('referee_invitations', 'remarks', 'comments')) recommendations_undergoing_voting = (EICRecommendation.objects .get_for_user_in_pool(request.user) - .filter(submission__status__in=['put_to_EC_voting'])) + .filter(submission__status='put_to_EC_voting')) recommendations_to_prepare_for_voting = (EICRecommendation.objects .get_for_user_in_pool(request.user) .filter( - submission__status__in=['voting_in_preparation'])) + submission__status='voting_in_preparation')) contributor = Contributor.objects.get(user=request.user) - assignments_to_consider = EditorialAssignment.objects.filter( - to=contributor, accepted=None, deprecated=False) + assignments_to_consider = EditorialAssignment.objects.open().filter(to=contributor) consider_assignment_form = ConsiderAssignmentForm() recs_to_vote_on = (EICRecommendation.objects.get_for_user_in_pool(request.user) .filter(eligible_to_vote=contributor) @@ -353,21 +354,53 @@ def pool(request): .exclude(submission__status__in=SUBMISSION_STATUS_VOTING_DEPRECATED)) rec_vote_form = RecommendationVoteForm() remark_form = RemarkForm() - context = {'submissions_in_pool': submissions_in_pool, - 'submission_status': SUBMISSION_STATUS, - 'recommendations_undergoing_voting': recommendations_undergoing_voting, - 'recommendations_to_prepare_for_voting': recommendations_to_prepare_for_voting, - 'assignments_to_consider': assignments_to_consider, - 'consider_assignment_form': consider_assignment_form, - 'recs_to_vote_on': recs_to_vote_on, - 'rec_vote_form': rec_vote_form, - 'remark_form': remark_form, } - return render(request, 'submissions/pool.html', context) + context = { + 'submissions_in_pool': submissions_in_pool, + 'submission_status': SUBMISSION_STATUS, + 'recommendations_undergoing_voting': recommendations_undergoing_voting, + 'recommendations_to_prepare_for_voting': recommendations_to_prepare_for_voting, + 'assignments_to_consider': assignments_to_consider, + 'consider_assignment_form': consider_assignment_form, + 'recs_to_vote_on': recs_to_vote_on, + 'rec_vote_form': rec_vote_form, + 'remark_form': remark_form, + '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 + if arxiv_identifier_w_vn_nr: + try: + context['submission'] = context['submissions_in_pool'].get( + arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) + except Submission.DoesNotExist: + pass + + # Temporary test logic: only testers see the new Pool + if context['submission'] and request.GET.get('json'): + template = 'partials/submissions/pool/submission_details.html' + elif is_tester(request.user) and not request.GET.get('test'): + template = 'submissions/pool/pool.html' + else: + template = 'submissions/pool.html' + return render(request, template, context) @login_required @permission_required('scipost.can_view_pool', raise_exception=True) def submissions_by_status(request, status): + # --- + # DEPRECATED AS PER NEW POOL + # --- status_dict = dict(SUBMISSION_STATUS) if status not in status_dict.keys(): errormessage = 'Unknown status.' @@ -403,7 +436,7 @@ def add_remark(request, arxiv_identifier_w_vn_nr): messages.success(request, 'Your remark has succesfully been posted') else: messages.warning(request, 'The form was invalidly filled.') - return redirect(reverse('submissions:pool')) + return redirect(reverse('submissions:pool', args=(arxiv_identifier_w_vn_nr,))) @login_required @@ -448,56 +481,76 @@ def assign_submission_ack(request, arxiv_identifier_w_vn_nr): @login_required @permission_required('scipost.can_take_charge_of_submissions', raise_exception=True) @transaction.atomic -def accept_or_decline_assignment_ack(request, assignment_id): - contributor = Contributor.objects.get(user=request.user) - assignment = get_object_or_404(EditorialAssignment, pk=assignment_id) +def assignment_request(request, assignment_id): + """ + Process EditorialAssignment acceptance/denial form or show if not submitted. + """ + assignment = get_object_or_404(EditorialAssignment.objects.open(), + to=request.user.contributor, pk=assignment_id) + errormessage = None if assignment.submission.status == 'assignment_failed': errormessage = 'This Submission has failed pre-screening and has been rejected.' - context = {'errormessage': errormessage} - return render(request, 'submissions/accept_or_decline_assignment_ack.html', context) - if assignment.submission.editor_in_charge: + + elif assignment.submission.editor_in_charge: errormessage = (assignment.submission.editor_in_charge.get_title_display() + ' ' + assignment.submission.editor_in_charge.user.last_name + ' has already agreed to be Editor-in-charge of this Submission.') - context = {'errormessage': errormessage} - return render(request, 'submissions/accept_or_decline_assignment_ack.html', context) - if request.method == 'POST': - form = ConsiderAssignmentForm(request.POST) - if form.is_valid(): - assignment.date_answered = timezone.now() - if form.cleaned_data['accept'] == 'True': - assignment.accepted = True - assignment.to = contributor - assignment.submission.status = 'EICassigned' - assignment.submission.editor_in_charge = contributor - assignment.submission.open_for_reporting = True - deadline = timezone.now() + datetime.timedelta(days=28) # for papers - if assignment.submission.submitted_to_journal == 'SciPost Physics Lecture Notes': - deadline += datetime.timedelta(days=28) - assignment.submission.reporting_deadline = deadline - assignment.submission.open_for_commenting = True - assignment.submission.latest_activity = timezone.now() - - SubmissionUtils.load({'assignment': assignment}) - SubmissionUtils.deprecate_other_assignments() - assign_perm('can_take_editorial_actions', contributor.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() - - # Add SubmissionEvents - assignment.submission.add_general_event('The Editor-in-charge has been assigned.') - else: - assignment.accepted = False - assignment.refusal_reason = form.cleaned_data['refusal_reason'] - assignment.submission.status = 'unassigned' - assignment.save() - assignment.submission.save() - - context = {'assignment': assignment} - return render(request, 'submissions/accept_or_decline_assignment_ack.html', context) + + if errormessage: + # Assignments can get stuck here, + # if errormessage is given the contributor can't close the assignment!! + messages.warning(request, errormessage) + return redirect(reverse('submissions:pool')) + + form = ConsiderAssignmentForm(request.POST or None) + if form.is_valid(): + assignment.date_answered = timezone.now() + if form.cleaned_data['accept'] == 'True': + assignment.accepted = True + assignment.to = request.user.contributor + assignment.submission.status = 'EICassigned' + assignment.submission.editor_in_charge = request.user.contributor + assignment.submission.open_for_reporting = True + deadline = timezone.now() + datetime.timedelta(days=28) # for papers + if assignment.submission.submitted_to_journal == 'SciPost Physics Lecture Notes': + deadline += datetime.timedelta(days=28) + assignment.submission.reporting_deadline = deadline + assignment.submission.open_for_commenting = True + assignment.submission.latest_activity = timezone.now() + + 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() + + # Add SubmissionEvents + assignment.submission.add_general_event('The Editor-in-charge has been assigned.') + msg = 'Thank you for becoming Editor-in-charge of this submission.' + url = reverse('submissions:editorial_page', + args=(assignment.submission.arxiv_identifier_w_vn_nr,)) + else: + assignment.accepted = False + assignment.refusal_reason = form.cleaned_data['refusal_reason'] + assignment.submission.status = 'unassigned' + msg = 'Thank you for considering' + url = reverse('submissions:pool') + # Save assignment and submission + assignment.save() + assignment.submission.save() + + # Form submitted, redirect user + messages.success(request, msg) + return redirect(url) + + context = { + 'assignment': assignment, + 'form': form + } + return render(request, 'submissions/pool/assignment_request.html', context) @login_required @@ -506,7 +559,7 @@ def accept_or_decline_assignment_ack(request, assignment_id): 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 accept_or_decline_assignment_ack method. + This is an adapted version of the assignment_request method. """ submission = get_object_or_404(Submission.objects.get_pool(request.user), arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) @@ -1071,6 +1124,16 @@ def eic_recommendation(request, arxiv_identifier_w_vn_nr): return redirect(reverse('submissions:editorial_page', args=[submission.arxiv_identifier_w_vn_nr])) + # Find EditorialAssignment for user + try: + assignment = submission.editorial_assignments.get(submission=submission, + to=request.user.contributor) + except EditorialAssignment.DoesNotExist: + messages.warning(request, ('You cannot formulate an Editorial Recommendation,' + ' because you are not assigned as editor-in-charge.')) + return redirect(reverse('submissions:editorial_page', + args=[submission.arxiv_identifier_w_vn_nr])) + form = EICRecommendationForm(request.POST or None) if form.is_valid(): # Create new EICRecommendation @@ -1107,8 +1170,6 @@ def eic_recommendation(request, arxiv_identifier_w_vn_nr): submission.save() # The EIC has fulfilled this editorial assignment. - assignment = get_object_or_404(EditorialAssignment, - submission=submission, to=request.user.contributor) assignment.completed = True assignment.save() messages.success(request, 'Your Editorial Recommendation has been succesfully submitted') @@ -1366,9 +1427,14 @@ def vote_on_rec(request, rec_id): remark=form.cleaned_data['remark']) remark.save() recommendation.save() + messages.success(request, 'Thank you for your vote.') return redirect(reverse('submissions:pool')) - return redirect(reverse('submissions:pool')) + context = { + 'recommendation': recommendation, + 'form': form + } + return render(request, 'submissions/pool/recommendation.html', context) @permission_required('scipost.can_prepare_recommendations_for_voting', raise_exception=True) @@ -1423,6 +1489,9 @@ def fix_College_decision(request, rec_id): # Create a ProductionStream object prodstream = ProductionStream(submission=submission) prodstream.save() + ed_admins = Group.objects.get(name='Editorial Administrators') + assign_perm('can_perform_supervisory_actions', ed_admins, prodstream) + assign_perm('can_work_for_stream', ed_admins, prodstream) # Add SubmissionEvent for authors # Do not write a new event for minor/major modification: already done at moment of @@ -1478,16 +1547,30 @@ class EditorialSummaryView(SubmissionAdminViewMixin, ListView): context = super().get_context_data(*args, **kwargs) # Pick submission from `submission_list` to include proper filters such as author filters. - try: - arxiv_id = self.request.GET.get('submission') - assert arxiv_id - context['submission'] = (context['submission_list'] - .get(arxiv_identifier_w_vn_nr=arxiv_id)) - except (AssertionError, Submission.DoesNotExist): - context['submission'] = None + if self.kwargs.get('arxiv_identifier_w_vn_nr'): + try: + context['submission'] = context['submission_list'].get( + arxiv_identifier_w_vn_nr=self.kwargs['arxiv_identifier_w_vn_nr']) + except Submission.DoesNotExist: + context['submission'] = None + + if not context.get('submission'): context['latest_events'] = SubmissionEvent.objects.for_eic().last_hours() + + context['recommendations_undergoing_voting'] = ( + EICRecommendation.objects.get_for_user_in_pool(self.request.user) + .filter(submission__status='put_to_EC_voting')) + context['recommendations_to_prepare_for_voting'] = ( + EICRecommendation.objects.get_for_user_in_pool(self.request.user) + .filter(submission__status='voting_in_preparation')) return context + def get_template_names(self): + if self.request.GET.get('json'): + return ['partials/submissions/admin/submission_details.html'] + else: + return ['submissions/admin/editorial_admin.html'] + class PlagiarismView(SubmissionAdminViewMixin, UpdateView): permission_required = 'scipost.can_do_plagiarism_checks' @@ -1513,3 +1596,14 @@ class PlagiarismReportPDFView(SubmissionAdminViewMixin, SingleObjectMixin, Redir if not url: raise Http404 return url + + +class AdminRecommendationView(SubmissionAdminViewMixin, DetailView): + permission_required = 'scipost.can_fix_College_decision' + template_name = 'submissions/admin/recommendation.html' + editorial_page = True + + def get_object(self): + """ Get the EICRecommendation as a submission-related instance. """ + submission = super().get_object() + return submission.eicrecommendations.first() diff --git a/theses/managers.py b/theses/managers.py index 4d2b06986cce30276d4753244d0fb149061f08b6..532868fe98d8a5da4ae06189320b3da18db7736e 100644 --- a/theses/managers.py +++ b/theses/managers.py @@ -16,5 +16,8 @@ class ThesisLinkManager(models.Manager): def vetted(self): return self.filter(vetted=True) + def awaiting_vetting(self): + return self.filter(vetted=False) + def open_for_commenting(self): return self.filter(open_for_commenting=True) diff --git a/theses/migrations/0009_auto_20170925_2124.py b/theses/migrations/0009_auto_20170925_2124.py new file mode 100644 index 0000000000000000000000000000000000000000..e921672ad57d7c04e01babef5401e0d8ed63f403 --- /dev/null +++ b/theses/migrations/0009_auto_20170925_2124.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-25 19:24 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('theses', '0008_auto_20170909_1649'), + ] + + operations = [ + migrations.AlterField( + model_name='thesislink', + name='author_as_cont', + field=models.ManyToManyField(blank=True, related_name='theses', to='scipost.Contributor'), + ), + migrations.AlterField( + model_name='thesislink', + name='author_claims', + field=models.ManyToManyField(blank=True, related_name='claimed_theses', to='scipost.Contributor'), + ), + migrations.AlterField( + model_name='thesislink', + name='author_false_claims', + field=models.ManyToManyField(blank=True, related_name='false_claimed_theses', to='scipost.Contributor'), + ), + migrations.AlterField( + model_name='thesislink', + name='requested_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='requested_theses', to='scipost.Contributor'), + ), + migrations.AlterField( + model_name='thesislink', + name='supervisor_as_cont', + field=models.ManyToManyField(blank=True, related_name='supervised_theses', to='scipost.Contributor', verbose_name='supervisor(s)'), + ), + ] diff --git a/theses/models.py b/theses/models.py index ad143b5a72d9c5cfa1b4fff33fe4cdbfa0d0afca..a6c05936e7ac238d5cc3976e9aa2c3e10092ab69 100644 --- a/theses/models.py +++ b/theses/models.py @@ -14,7 +14,7 @@ class ThesisLink(models.Model): """ An URL pointing to a thesis """ requested_by = models.ForeignKey( 'scipost.Contributor', blank=True, null=True, - related_name='thesislink_requested_by', + related_name='requested_theses', on_delete=models.CASCADE) vetted = models.BooleanField(default=False) vetted_by = models.ForeignKey( @@ -37,18 +37,18 @@ class ThesisLink(models.Model): author = models.CharField(max_length=1000) author_as_cont = models.ManyToManyField( 'scipost.Contributor', blank=True, - related_name='author_cont') + related_name='theses') author_claims = models.ManyToManyField( 'scipost.Contributor', blank=True, - related_name='authors_thesis_claims') + related_name='claimed_theses') author_false_claims = models.ManyToManyField( 'scipost.Contributor', blank=True, - related_name='authors_thesis_false_claims') + related_name='false_claimed_theses') supervisor = models.CharField(max_length=1000) supervisor_as_cont = models.ManyToManyField( 'scipost.Contributor', blank=True, verbose_name='supervisor(s)', - related_name='supervisor_cont') + related_name='supervised_theses') institution = models.CharField( max_length=300, verbose_name='degree granting institution')