From 54b34eb640c62f71cc3a6801cd90a7e83c973ce8 Mon Sep 17 00:00:00 2001
From: Jorran de Wit <jorrandewit@outlook.com>
Date: Sun, 1 Oct 2017 01:09:49 +0200
Subject: [PATCH] Implement Proofs system

---
 .../migrations/0033_auto_20170930_2230.py     |  21 ++
 partners/models.py                            |   2 +-
 production/admin.py                           |   3 +-
 production/apps.py                            |   5 +-
 production/constants.py                       |  31 +--
 production/forms.py                           |   8 +-
 .../migrations/0024_auto_20170930_2230.py     |  55 +++++
 .../migrations/0025_auto_20170930_2244.py     |  26 +++
 .../0026_proof_accessible_for_authors.py      |  20 ++
 production/models.py                          |  46 ++++-
 production/signals.py                         |   9 +-
 .../partials/production_stream_card.html      |  73 ++++---
 .../production_stream_card_completed.html     |  16 ++
 .../partials/stream_status_changes.html       |   2 +-
 production/templates/production/proofs.html   |  50 +++++
 .../templates/production/upload_proofs.html   |  29 +++
 production/urls.py                            |  12 ++
 production/utils.py                           |   6 +
 production/views.py                           | 192 +++++++++++++++++-
 .../commands/add_groups_and_permissions.py    |  13 ++
 {partners => scipost}/storage.py              |   0
 21 files changed, 559 insertions(+), 60 deletions(-)
 create mode 100644 partners/migrations/0033_auto_20170930_2230.py
 create mode 100644 production/migrations/0024_auto_20170930_2230.py
 create mode 100644 production/migrations/0025_auto_20170930_2244.py
 create mode 100644 production/migrations/0026_proof_accessible_for_authors.py
 create mode 100644 production/templates/production/proofs.html
 create mode 100644 production/templates/production/upload_proofs.html
 create mode 100644 production/utils.py
 rename {partners => scipost}/storage.py (100%)

diff --git a/partners/migrations/0033_auto_20170930_2230.py b/partners/migrations/0033_auto_20170930_2230.py
new file mode 100644
index 000000000..c5a91d320
--- /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 ff95d0799..81ae52d85 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 dc5cbd236..8d7fef8b4 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 d633dcd69..e08481e72 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 216d60b5c..56884a9f8 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 12202657c..ec9c43597 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 000000000..eb444dd49
--- /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 000000000..d8b1ef062
--- /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 000000000..4854c5c20
--- /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 367f99bb5..868c4a416 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 b0ad606bc..d89834391 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 530e669e1..b495d127a 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 %}
                   &middot; <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 980a42168..2745f2d65 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 0d8dabf6f..6ade74af2 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 000000000..dbd22466a
--- /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>
+                        &middot;
+                        <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 000000000..09970ec96
--- /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 51af1e5d8..2b244223a 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 000000000..f173d7a42
--- /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 f33d9eeb0..30a20ee78 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 d3bbd031c..6e3067a54 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
-- 
GitLab