diff --git a/partners/migrations/0033_auto_20170930_2230.py b/partners/migrations/0033_auto_20170930_2230.py new file mode 100644 index 0000000000000000000000000000000000000000..c5a91d3203db66d8420cb03e9e6d9f5eb5cd39d3 --- /dev/null +++ b/partners/migrations/0033_auto_20170930_2230.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-30 20:30 +from __future__ import unicode_literals + +from django.db import migrations, models +import scipost.storage + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0032_auto_20170829_0727'), + ] + + operations = [ + migrations.AlterField( + model_name='partnersattachment', + name='attachment', + field=models.FileField(storage=scipost.storage.SecureFileStorage(), upload_to='UPLOADS/PARTNERS/ATTACHMENTS'), + ), + ] diff --git a/partners/models.py b/partners/models.py index ff95d07990170d04286329cc4d70a0c625203715..81ae52d853425aa9bf38d41ed89d8fd12f9b311a 100644 --- a/partners/models.py +++ b/partners/models.py @@ -26,11 +26,11 @@ 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 from scipost.models import get_sentinel_user, Contributor +from scipost.storage import SecureFileStorage ######################## diff --git a/production/admin.py b/production/admin.py index dc5cbd236c21019ed1fb12ece7a8a9d13a89b09e..8d7fef8b4cc21ea13c156ee4a763b3f7f9dfa507 100644 --- a/production/admin.py +++ b/production/admin.py @@ -2,7 +2,7 @@ from django.contrib import admin from guardian.admin import GuardedModelAdmin -from .models import ProductionStream, ProductionEvent, ProductionUser +from .models import ProductionStream, ProductionEvent, ProductionUser, Proof def event_count(obj): @@ -30,5 +30,6 @@ class ProductionStreamAdmin(GuardedModelAdmin): ) +admin.site.register(Proof) admin.site.register(ProductionUser) admin.site.register(ProductionStream, ProductionStreamAdmin) diff --git a/production/apps.py b/production/apps.py index d633dcd69d8e1462ea12d64fa04cf8492c2dafe9..e08481e723d02cee2c63470bf7991bb214c79a10 100644 --- a/production/apps.py +++ b/production/apps.py @@ -8,7 +8,8 @@ class ProductionConfig(AppConfig): def ready(self): super().ready() - from .models import ProductionEvent, ProductionStream - from .signals import notify_new_event, notify_new_stream + from .models import ProductionEvent, ProductionStream, Proof + from .signals import notify_new_event, notify_new_stream, notify_proof_upload post_save.connect(notify_new_event, sender=ProductionEvent) post_save.connect(notify_new_stream, sender=ProductionStream) + post_save.connect(notify_proof_upload, sender=Proof) diff --git a/production/constants.py b/production/constants.py index 216d60b5c1c2d6e4ddaa4b2ddc441d929979a266..56884a9f89680a1a3e62ca359da8ad50539fe449 100644 --- a/production/constants.py +++ b/production/constants.py @@ -30,18 +30,21 @@ PRODUCTION_EVENTS = ( ('status', 'Status change'), (EVENT_MESSAGE, 'Message'), (EVENT_HOUR_REGISTRATION, 'Hour registration'), - # ('assigned_to_supervisor', 'Assigned by EdAdmin to Supervisor'), - # ('message_edadmin_to_supervisor', 'Message from EdAdmin to Supervisor'), - # ('message_supervisor_to_edadmin', 'Message from Supervisor to EdAdmin'), - # ('officer_tasked_with_proof_production', 'Supervisor tasked officer with proofs production'), - # ('message_supervisor_to_officer', 'Message from Supervisor to Officer'), - # ('message_officer_to_supervisor', 'Message from Officer to Supervisor'), - # ('proofs_produced', 'Proofs have been produced'), - # ('proofs_checked_by_supervisor', 'Proofs have been checked by Supervisor'), - # ('proofs_sent_to_authors', 'Proofs sent to Authors'), - # ('proofs_returned_by_authors', 'Proofs returned by Authors'), - # ('corrections_implemented', 'Corrections implemented'), - # ('authors_have_accepted_proofs', 'Authors have accepted proofs'), - # ('paper_published', 'Paper has been published'), - # ('cited_notified', 'Cited people have been notified/invited to SciPost'), +) + +PROOF_UPLOADED = 'uploaded' +PROOF_SENT = 'sent' +PROOF_ACCEPTED_SUP = 'accepted_sup' +PROOF_DECLINED_SUP = 'declined_sup' +PROOF_ACCEPTED = 'accepted' +PROOF_DECLINED = 'declined' +PROOF_RENEWED = 'renewed' +PROOF_STATUSES = ( + (PROOF_UPLOADED, 'Proofs uploaded'), + (PROOF_SENT, 'Proofs sent to authors'), + (PROOF_ACCEPTED_SUP, 'Proofs accepted by supervisor'), + (PROOF_DECLINED_SUP, 'Proofs declined by supervisor'), + (PROOF_ACCEPTED, 'Proofs accepted by authors'), + (PROOF_DECLINED, 'Proofs declined by authors'), + (PROOF_RENEWED, 'Proofs renewed'), ) diff --git a/production/forms.py b/production/forms.py index 12202657c8d06ce549dc42c7767801ca3404a445..ec9c435974e458bc860fa345f45638fd20417924 100644 --- a/production/forms.py +++ b/production/forms.py @@ -5,7 +5,7 @@ from django.utils.dates import MONTHS from django.db.models import Sum from . import constants -from .models import ProductionUser, ProductionStream, ProductionEvent +from .models import ProductionUser, ProductionStream, ProductionEvent, Proof from .signals import notify_stream_status_change today = datetime.datetime.today() @@ -153,3 +153,9 @@ class ProductionUserMonthlyActiveFilter(forms.Form): }) return output + + +class ProofUploadForm(forms.ModelForm): + class Meta: + model = Proof + fields = ('attachment',) diff --git a/production/migrations/0024_auto_20170930_2230.py b/production/migrations/0024_auto_20170930_2230.py new file mode 100644 index 0000000000000000000000000000000000000000..eb444dd499d84b11bc866bd2004638c83a07ae43 --- /dev/null +++ b/production/migrations/0024_auto_20170930_2230.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-30 20:30 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import production.models +import scipost.storage + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('production', '0023_auto_20170930_1759'), + ] + + operations = [ + migrations.CreateModel( + name='Proof', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('attachment', models.FileField(storage=scipost.storage.SecureFileStorage(), upload_to=production.models.proofs_upload_location)), + ('version', models.PositiveSmallIntegerField(default=0)), + ('created', models.DateTimeField(auto_now_add=True)), + ('status', models.CharField(choices=[('uploaded', 'Proof uploaded'), ('accepted', 'Proof accepted'), ('declined', 'Proof declined'), ('renewed', 'Proof renewed')], default='uploaded', max_length=16)), + ], + ), + migrations.AlterField( + model_name='productionevent', + name='event', + field=models.CharField(choices=[('assignment', 'Assignment'), ('status', 'Status change'), ('message', 'Message'), ('registration', 'Hour registration')], default='message', max_length=64), + ), + migrations.AlterField( + model_name='productionstream', + name='status', + field=models.CharField(choices=[('initiated', 'New Stream started'), ('tasked', 'Supervisor tasked officer with proofs production'), ('produced', 'Proofs have been produced'), ('checked', 'Proofs have been checked by Supervisor'), ('sent', 'Proofs sent to Authors'), ('returned', 'Proofs returned by Authors'), ('corrected', 'Corrections implemented'), ('accepted', 'Authors have accepted proofs'), ('published', 'Paper has been published'), ('cited', 'Cited people have been notified/invited to SciPost'), ('completed', 'Completed')], default='initiated', max_length=32), + ), + migrations.AlterField( + model_name='productionstream', + name='submission', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='production_stream', to='submissions.Submission'), + ), + migrations.AddField( + model_name='proof', + name='stream', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='proofs', to='production.ProductionStream'), + ), + migrations.AddField( + model_name='proof', + name='upload_by', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/production/migrations/0025_auto_20170930_2244.py b/production/migrations/0025_auto_20170930_2244.py new file mode 100644 index 0000000000000000000000000000000000000000..d8b1ef0629676a83aff2a9b9709e0ac47446b9e6 --- /dev/null +++ b/production/migrations/0025_auto_20170930_2244.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-30 20:44 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('production', '0024_auto_20170930_2230'), + ] + + operations = [ + migrations.RemoveField( + model_name='proof', + name='upload_by', + ), + migrations.AddField( + model_name='proof', + name='uploaded_by', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='production.ProductionUser'), + preserve_default=False, + ), + ] diff --git a/production/migrations/0026_proof_accessible_for_authors.py b/production/migrations/0026_proof_accessible_for_authors.py new file mode 100644 index 0000000000000000000000000000000000000000..4854c5c206ad4cc4205abb2fa3e7f1ebf6fba6e3 --- /dev/null +++ b/production/migrations/0026_proof_accessible_for_authors.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-30 20:49 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('production', '0025_auto_20170930_2244'), + ] + + operations = [ + migrations.AddField( + model_name='proof', + name='accessible_for_authors', + field=models.BooleanField(default=False), + ), + ] diff --git a/production/models.py b/production/models.py index 367f99bb51d25ff26536f95852dc2d5803917c8a..868c4a4166d42b2720e2001fa6fb6e0edc3b7fdc 100644 --- a/production/models.py +++ b/production/models.py @@ -1,12 +1,17 @@ from django.db import models +from django.conf import settings from django.core.urlresolvers import reverse from django.contrib.auth.models import User from django.utils import timezone from django.utils.functional import cached_property from .constants import PRODUCTION_STREAM_STATUS, PRODUCTION_STREAM_INITIATED, PRODUCTION_EVENTS,\ - EVENT_MESSAGE, EVENT_HOUR_REGISTRATION, PRODUCTION_STREAM_COMPLETED + EVENT_MESSAGE, EVENT_HOUR_REGISTRATION, PRODUCTION_STREAM_COMPLETED,\ + PROOF_STATUSES, PROOF_UPLOADED from .managers import ProductionStreamQuerySet, ProductionEventManager +from .utils import proof_id_to_slug + +from scipost.storage import SecureFileStorage class ProductionUser(models.Model): @@ -86,3 +91,42 @@ class ProductionEvent(models.Model): @cached_property def editable(self): return self.event in [EVENT_MESSAGE, EVENT_HOUR_REGISTRATION] and not self.stream.completed + + +def proofs_upload_location(instance, filename): + submission = instance.stream.submission + return 'UPLOADS/PROOFS/{year}/{arxiv}/{filename}'.format( + year=submission.submission_date.year, + arxiv=submission.arxiv_identifier_wo_vn_nr, + filename=filename) + + +class Proof(models.Model): + """ + A Proof directly related to a ProductionStream and Submission in SciPost. + It's meant to help the Production team + """ + attachment = models.FileField(upload_to=proofs_upload_location, storage=SecureFileStorage()) + version = models.PositiveSmallIntegerField(default=0) + stream = models.ForeignKey('production.ProductionStream', related_name='proofs') + uploaded_by = models.ForeignKey('production.ProductionUser', related_name='+') + created = models.DateTimeField(auto_now_add=True) + status = models.CharField(max_length=16, choices=PROOF_STATUSES, default=PROOF_UPLOADED) + accessible_for_authors = models.BooleanField(default=False) + + class Meta: + ordering = ['version'] + + def get_absolute_url(self): + return reverse('production:proof', + kwargs={'stream_id': self.stream.id, 'version': self.version}) + + def save(self, *args, **kwargs): + # Control Report count per Submission. + if not self.version: + self.version = self.stream.proofs.count() + 1 + return super().save(*args, **kwargs) + + @property + def slug(self): + return proof_id_to_slug(self.id) diff --git a/production/signals.py b/production/signals.py index b0ad606bcd8e870e5596b928b579591d41eaf6c8..d89834391fe659fbc15119ddfcd9934d14efd13e 100644 --- a/production/signals.py +++ b/production/signals.py @@ -57,7 +57,7 @@ def notify_stream_status_change(sender, instance, created, **kwargs): for user in administators.user_set.all(): notify.send(sender=sender, recipient=user, actor=sender, - verb=' has marked proofs as being accepted.', target=instance) + verb=' has marked Proofs accepted.', target=instance) elif instance.status == constants.PROOFS_PUBLISHED: if instance.supervisor: notify.send(sender=sender, recipient=instance.supervisor.user, @@ -84,3 +84,10 @@ def notify_stream_status_change(sender, instance, created, **kwargs): notify.send(sender=sender, recipient=instance.supervisor.user, actor=sender, verb=' changed the Production Stream status.', target=instance) + + +def notify_proof_upload(sender, instance, created, **kwargs): + if created and instance.stream.supervisor: + notify.send(sender=sender, recipient=instance.stream.supervisor.user, + actor=instance.uploaded_by.user, verb=' uploaded new Proofs to Production Stream.', + target=instance) diff --git a/production/templates/production/partials/production_stream_card.html b/production/templates/production/partials/production_stream_card.html index 530e669e1ba491b9e468266cc59aa07ee3494d91..b495d127a56e28307a09af12a27ee61af1c94972 100644 --- a/production/templates/production/partials/production_stream_card.html +++ b/production/templates/production/partials/production_stream_card.html @@ -17,37 +17,46 @@ </form> {% endif %} - {% 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 supervisor"> - </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> + + {% if "can_work_for_stream" in sub_perms %} + {% if perms.scipost.can_publish_accepted_submission or perms.scipost.can_assign_production_supervisor or perms.scipost.can_assign_production_officer or perms.scipost.can_upload_proofs %} + <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 supervisor"> + </form> + </div> + </li> + {% endif %} + {% if perms.scipost.can_assign_production_officer 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_upload_proofs and stream.status != 'accepted' and stream.status != 'completed' and stream.status != 'cited' %} + <li><a href="{% url 'production:upload_proofs' stream_id=stream.id %}">Upload Proofs</a></li> + {% endif %} + {% if perms.scipost.can_publish_accepted_submission %} + {% if not stream.submission.publication %} + <li><a href="{% url 'journals:initiate_publication' %}">Initiate the publication process</a></li> + {% endif %} + <li><a href="{% url 'production:mark_as_completed' stream_id=stream.id %}">Mark this stream as completed</a></li> + {% endif %} + </ul> + {% endif %} {% endif %} {% endblock %} @@ -65,7 +74,7 @@ <li>Production Officer: {% if stream.officer %} <strong>{{ stream.officer }}</strong> - {% if "can_perform_supervisory_actions" in sub_perms %} + {% if "can_work_for_stream" in sub_perms and perms.scipost.can_assign_production_officer %} · <a href="{% url 'production:remove_officer' stream_id=stream.id officer_id=stream.officer.id %}" class="text-danger">Remove from stream</a> {% endif %} {% else %} diff --git a/production/templates/production/partials/production_stream_card_completed.html b/production/templates/production/partials/production_stream_card_completed.html index 980a421687e91dc62177947387154fdb49ea5b13..2745f2d6505b8c73f6def8e51c5cf18ebfd7d30a 100644 --- a/production/templates/production/partials/production_stream_card_completed.html +++ b/production/templates/production/partials/production_stream_card_completed.html @@ -32,4 +32,20 @@ <h3>Events</h3> {% include 'production/partials/production_events.html' with events=stream.events.all non_editable=1 %} {% endblock %} + + {% if "can_work_for_stream" in sub_perms %} + <h3>Proofs</h3> + <ul> + {% for proof in stream.proofs.all %} + <li class="py-1"> + <a href="{% url 'production:proof' stream_id=stream.id version=proof.version %}">Version {{ proof.version }}</a><br> + Uploaded by {{ proof.uploaded_by.user.first_name }} {{ proof.uploaded_by.user.last_name }}<br> + Accessible for authors: <strong>{{ proof.accessible_for_authors|yesno:'Yes,No' }}</strong><br> + <span class="label label-secondary label-sm">{{ proof.get_status_display }}</span> + </li> + {% empty %} + <li>No Proofs found.</li> + {% endfor %} + </ul> + {% endif %} </div> diff --git a/production/templates/production/partials/stream_status_changes.html b/production/templates/production/partials/stream_status_changes.html index 0d8dabf6fe66bbe9323fbe81c939f71695f6bcb4..6ade74af290681a717806091502ddbde14db562f 100644 --- a/production/templates/production/partials/stream_status_changes.html +++ b/production/templates/production/partials/stream_status_changes.html @@ -7,7 +7,7 @@ {{ form|bootstrap_inline }} <div class="form-group row"> <div class="col-form-label col ml-2"> - <button type="submit" class="btn btn-primary">Submit</button> + <button type="submit" class="btn btn-primary">Change</button> </div> </div> </form> diff --git a/production/templates/production/proofs.html b/production/templates/production/proofs.html new file mode 100644 index 0000000000000000000000000000000000000000..dbd22466ab3c92b77d19bd47009fba54f9511780 --- /dev/null +++ b/production/templates/production/proofs.html @@ -0,0 +1,50 @@ +{% extends 'production/base.html' %} + +{% block breadcrumb_items %} + {{block.super}} + <a href="{{ stream.get_absolute_url }}" class="breadcrumb-item">Production Stream</a> + <span class="breadcrumb-item">Proofs (version {{ proof.version }})</span> +{% endblock %} + +{% load bootstrap %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Proofs (version {{ proof.version }})</h1> + {% include 'submissions/_submission_card_content_sparse.html' with submission=stream.submission %} + </div> +</div> +<div class="row"> + <div class="col-12"> + <h3>Info</h3> + <ul> + <li>Version: {{ proof.version }}</li> + <li>Status: <span class="label label-secondary label-sm">{{ proof.get_status_display }}</span></li> + <li>Uploaded by: {{ proof.uploaded_by }}</li> + <li>Accessible for Authors: {{ proof.accessible_for_authors|yesno:'Yes,No' }}</li> + </ul> + + <h3>Actions</h3> + <ul> + <li><a href="{% url 'production:proof_pdf' proof.slug %}" target="_blank">Download file</a></li> + {% if perms.scipost.can_run_proofs_by_authors %} + {% if proof.status == 'uploaded' %} + <li> + <a href="{% url 'production:decision' proof.stream.id proof.version 'accept' %}">Accept proofs</a> + · + <a href="{% url 'production:decision' proof.stream.id proof.version 'decline' %}" class="text-danger">Decline proofs</a> + </li> + {% elif proof.status == 'accepted_sup' %} + <li><a href="{% url 'production:send_proofs' proof.stream.id proof.version %}">Send proofs to authors</a></li> + {% endif %} + {% if proof.status != 'uploaded' %} + <li><a href="{% url 'production:toggle_accessibility' proof.stream.id proof.version %}">{{ proof.accessible_for_authors|yesno:'Make accessible,Hide' }} for authors</a></li> + {% endif %} + {% endif %} + </ul> + </div> +</div> + +{% endblock content %} diff --git a/production/templates/production/upload_proofs.html b/production/templates/production/upload_proofs.html new file mode 100644 index 0000000000000000000000000000000000000000..09970ec96a500852c38c7b745db653ec10f0c9d9 --- /dev/null +++ b/production/templates/production/upload_proofs.html @@ -0,0 +1,29 @@ +{% extends 'production/base.html' %} + +{% block breadcrumb_items %} + {{block.super}} + <span class="breadcrumb-item">Upload Proofs</span> +{% endblock %} + +{% load bootstrap %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Upload Proofs</h1> + {% include 'submissions/_submission_card_content_sparse.html' with submission=stream.submission %} + </div> +</div> +<div class="row"> + <div class="col-12"> + <form method="post" enctype="multipart/form-data"> + {% csrf_token %} + {{ form|bootstrap }} + <input type="submit" class="btn btn-secondary" name="submit" value="Upload"> + </form> + </ul> + </div> +</div> + +{% endblock content %} diff --git a/production/urls.py b/production/urls.py index 51af1e5d878274bd2934d8781cf3913a1dd135a4..2b244223a76d8f17a7c24ccffda6b7d76ef3aca2 100644 --- a/production/urls.py +++ b/production/urls.py @@ -10,6 +10,16 @@ urlpatterns = [ production_views.stream, name='stream'), url(r'^streams/(?P<stream_id>[0-9]+)/status$', production_views.update_status, name='update_status'), + url(r'^streams/(?P<stream_id>[0-9]+)/proofs/upload$', + production_views.upload_proofs, name='upload_proofs'), + url(r'^streams/(?P<stream_id>[0-9]+)/proofs/(?P<version>[0-9]+)$', + production_views.proof, name='proof'), + url(r'^streams/(?P<stream_id>[0-9]+)/proofs/(?P<version>[0-9]+)/decision/(?P<decision>accept|decline)$', + production_views.decision, name='decision'), + url(r'^streams/(?P<stream_id>[0-9]+)/proofs/(?P<version>[0-9]+)/send_to_authors$', + production_views.send_proofs, name='send_proofs'), + url(r'^streams/(?P<stream_id>[0-9]+)/proofs/(?P<version>[0-9]+)/toggle_access$', + production_views.toggle_accessibility, name='toggle_accessibility'), 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$', @@ -26,4 +36,6 @@ urlpatterns = [ production_views.UpdateEventView.as_view(), name='update_event'), url(r'^events/(?P<event_id>[0-9]+)/delete', production_views.DeleteEventView.as_view(), name='delete_event'), + url(r'^proofs/(?P<slug>[0-9]+)$', + production_views.proof_pdf, name='proof_pdf'), ] diff --git a/production/utils.py b/production/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f173d7a42a5bd7c0e88145715bfc29a9d3d3fb41 --- /dev/null +++ b/production/utils.py @@ -0,0 +1,6 @@ +def proof_id_to_slug(id): + return int(id) + 8932 + + +def proof_slug_to_id(slug): + return int(slug) - 8932 diff --git a/production/views.py b/production/views.py index f33d9eeb0aa9b43487cb9ca16974ca2778304212..30a20ee78a1e9f6a8e1d6716911a038c063e3671 100644 --- a/production/views.py +++ b/production/views.py @@ -1,10 +1,12 @@ import datetime +import mimetypes from django.contrib import messages -from django.contrib.auth.decorators import permission_required +from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.models import Group from django.core.urlresolvers import reverse from django.db import transaction +from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404, render, redirect from django.utils import timezone from django.utils.decorators import method_decorator @@ -14,11 +16,12 @@ from guardian.core import ObjectPermissionChecker from guardian.shortcuts import assign_perm, remove_perm from . import constants -from .models import ProductionUser, ProductionStream, ProductionEvent +from .models import ProductionUser, ProductionStream, ProductionEvent, Proof from .forms import ProductionEventForm, AssignOfficerForm, UserToOfficerForm,\ - AssignSupervisorForm, StreamStatusForm + AssignSupervisorForm, StreamStatusForm, ProofUploadForm from .permissions import is_production_user from .signals import notify_stream_status_change, notify_new_stream_assignment +from .utils import proof_slug_to_id ###################### @@ -296,13 +299,190 @@ def mark_as_completed(request, stream_id): ) prodevent.save() notify_stream_status_change(request.user, stream) + messages.success(request, 'Stream marked as completed.') return redirect(reverse('production:production')) -def upload_proofs(request): +@is_production_user() +@permission_required('scipost.can_upload_proofs', raise_exception=True) +def upload_proofs(request, stream_id): + """ + Called by a member of the Production Team. + Upload the production version .pdf of a submission. + """ + stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) + checker = ObjectPermissionChecker(request.user) + if not checker.has_perm('can_work_for_stream', stream): + return redirect(reverse('production:production')) + + form = ProofUploadForm(request.POST or None, request.FILES or None) + if form.is_valid(): + proof = form.save(commit=False) + proof.stream = stream + proof.uploaded_by = request.user.production_user + proof.save() + Proof.objects.filter(stream=stream).exclude(version=proof.version).update( + status=constants.PROOF_RENEWED) + messages.success(request, 'Proof uploaded.') + + # Update Stream status + if stream.status == constants.PROOFS_TASKED: + stream.status = constants.PROOFS_PRODUCED + stream.save() + elif stream.status == constants.PROOFS_RETURNED: + stream.status = constants.PROOFS_CORRECTED + stream.save() + + prodevent = ProductionEvent( + stream=stream, + event='status', + comments='New Proofs uploaded, version {v}'.format(v=proof.version), + noted_by=request.user.production_user + ) + prodevent.save() + return redirect(stream.get_absolute_url()) + + context = { + 'stream': stream, + 'form': form + } + return render(request, 'production/upload_proofs.html', context) + + +@is_production_user() +@permission_required('scipost.can_view_production', raise_exception=True) +def proof(request, stream_id, version): """ - TODO Called by a member of the Production Team. Upload the production version .pdf of a submission. """ - return render(request, 'production/upload_proofs.html') + stream = get_object_or_404(ProductionStream.objects.all(), pk=stream_id) + checker = ObjectPermissionChecker(request.user) + if not checker.has_perm('can_work_for_stream', stream): + return redirect(reverse('production:production')) + + try: + proof = stream.proofs.get(version=version) + except Proof.DoesNotExist: + raise Http404 + + context = { + 'stream': stream, + 'proof': proof + } + return render(request, 'production/proofs.html', context) + + +def proof_pdf(request, slug): + """ Open Proof pdf. """ + if not request.user.is_authenticated: + # Don't use the decorator but this strategy, + # because now it will return 404 instead of a redirect to the login page. + raise Http404 + + proof = Proof.objects.get(id=proof_slug_to_id(slug)) + stream = proof.stream + + # Check if user has access! + checker = ObjectPermissionChecker(request.user) + access = checker.has_perm('can_work_for_stream', stream) and request.user.has_perm('scipost.can_view_production') + if not access: + access = request.user in proof.stream.submission.authors.all() + if not access: + raise Http404 + + # Passed the test! The user may see the file! + content_type, encoding = mimetypes.guess_type(proof.attachment.path) + content_type = content_type or 'application/octet-stream' + response = HttpResponse(proof.attachment.read(), content_type=content_type) + response["Content-Encoding"] = encoding + return response + + +@is_production_user() +@permission_required('scipost.can_run_proofs_by_authors', raise_exception=True) +def toggle_accessibility(request, stream_id, version): + """ + Open/close accessibility of proofs to the authors. + """ + stream = get_object_or_404(ProductionStream.objects.all(), pk=stream_id) + checker = ObjectPermissionChecker(request.user) + if not checker.has_perm('can_work_for_stream', stream): + return redirect(reverse('production:production')) + + try: + proof = stream.proofs.exclude(status=constants.PROOF_UPLOADED).get(version=version) + except Proof.DoesNotExist: + raise Http404 + + proof.accessible_for_authors = not proof.accessible_for_authors + proof.save() + messages.success(request, 'Proofs accessibility updated.') + return redirect(stream.get_absolute_url()) + + +@is_production_user() +@permission_required('scipost.can_run_proofs_by_authors', raise_exception=True) +def decision(request, stream_id, version, decision): + """ + Send/open proofs to the authors. + """ + stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) + checker = ObjectPermissionChecker(request.user) + if not checker.has_perm('can_work_for_stream', stream): + return redirect(reverse('production:production')) + + try: + proof = stream.proofs.get(version=version, status=constants.PROOF_UPLOADED) + except Proof.DoesNotExist: + raise Http404 + + if decision == 'accept': + proof.status = constants.PROOF_ACCEPTED_SUP + stream.status = constants.PROOFS_CHECKED + decision = 'accepted' + else: + proof.status = constants.PROOF_DECLINED_SUP + stream.status = constants.PROOFS_TASKED + decision = 'declined' + stream.save() + proof.save() + + prodevent = ProductionEvent( + stream=stream, + event='status', + comments='Proofs version {version} are {decision}.'.format(version=proof.version, + decision=decision), + noted_by=request.user.production_user + ) + prodevent.save() + messages.success(request, 'Proofs have been {decision}.'.format(decision=decision)) + return redirect(stream.get_absolute_url()) + + +@is_production_user() +@permission_required('scipost.can_run_proofs_by_authors', raise_exception=True) +def send_proofs(request, stream_id, version): + """ + Send/open proofs to the authors. + """ + stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) + checker = ObjectPermissionChecker(request.user) + if not checker.has_perm('can_work_for_stream', stream): + return redirect(reverse('production:production')) + + try: + proof = stream.proofs.get(version=version, status=constants.PROOF_UPLOADED) + except Proof.DoesNotExist: + raise Http404 + + proof.status = constants.PROOF_SENT + proof.accessible_for_authors = True + proof.save() + + if stream.status not in [constants.PROOFS_PUBLISHED, constants.PROOFS_CITED]: + stream.status = constants.PROOFS_SENT + stream.save() + + messages.success(request, 'Proofs have been sent.') + return redirect(stream.get_absolute_url()) diff --git a/scipost/management/commands/add_groups_and_permissions.py b/scipost/management/commands/add_groups_and_permissions.py index d3bbd031c220b7d39b2ed04a2814cb04615690a2..6e3067a54ec7c406d004e45ebeedbbd14879203a 100644 --- a/scipost/management/commands/add_groups_and_permissions.py +++ b/scipost/management/commands/add_groups_and_permissions.py @@ -226,10 +226,18 @@ class Command(BaseCommand): codename='can_view_production', name='Can view production page', content_type=content_type) + can_upload_proofs, created = Permission.objects.get_or_create( + codename='can_upload_proofs', + name='Can upload proofs', + content_type=content_type) can_take_decisions_related_to_proofs, created = Permission.objects.get_or_create( codename='can_take_decisions_related_to_proofs', name='Can take decisions related to proofs', content_type=content_type) + can_run_proofs_by_authors, created = Permission.objects.get_or_create( + codename='can_run_proofs_by_authors', + name='Can run proof by authors', + content_type=content_type) can_publish_accepted_submission, created = Permission.objects.get_or_create( codename='can_publish_accepted_submission', name='Can publish accepted submission', @@ -305,6 +313,8 @@ class Command(BaseCommand): can_assign_production_supervisor, can_view_all_production_streams, can_take_decisions_related_to_proofs, + can_upload_proofs, + can_run_proofs_by_authors, ]) EditorialCollege.permissions.set([ @@ -347,13 +357,16 @@ class Command(BaseCommand): can_assign_production_officer, can_take_decisions_related_to_proofs, can_view_all_production_streams, + can_run_proofs_by_authors, can_view_docs_scipost, can_view_production, + can_upload_proofs, ]) ProductionOfficers.permissions.set([ can_view_docs_scipost, can_view_production, + can_upload_proofs, ]) PartnersAdmin.permissions.set([ diff --git a/partners/storage.py b/scipost/storage.py similarity index 100% rename from partners/storage.py rename to scipost/storage.py