From f34e8c7f6689b768b1e297c5611110d8c44190cb Mon Sep 17 00:00:00 2001
From: Jorran de Wit <jorrandewit@outlook.com>
Date: Mon, 9 Oct 2017 20:24:31 +0200
Subject: [PATCH] Add Invitations Officer to Stream

---
 .../migrations/0037_merge_20171009_2000.py    |  16 +++
 production/forms.py                           |   6 ++
 production/managers.py                        |   7 +-
 ...29_productionstream_invitations_officer.py |  21 ++++
 production/models.py                          |   2 +
 .../partials/production_stream_card.html      | 100 ++++++++++++------
 .../production_stream_card_completed.html     |   9 +-
 production/urls.py                            |   4 +
 production/views.py                           |  65 +++++++++++-
 9 files changed, 191 insertions(+), 39 deletions(-)
 create mode 100644 partners/migrations/0037_merge_20171009_2000.py
 create mode 100644 production/migrations/0029_productionstream_invitations_officer.py

diff --git a/partners/migrations/0037_merge_20171009_2000.py b/partners/migrations/0037_merge_20171009_2000.py
new file mode 100644
index 000000000..926f446e1
--- /dev/null
+++ b/partners/migrations/0037_merge_20171009_2000.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2017-10-09 18:00
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('partners', '0034_merge_20171004_1946'),
+        ('partners', '0036_auto_20171004_2014'),
+    ]
+
+    operations = [
+    ]
diff --git a/production/forms.py b/production/forms.py
index 9fc2edc32..1ec87f0f5 100644
--- a/production/forms.py
+++ b/production/forms.py
@@ -36,6 +36,12 @@ class AssignOfficerForm(forms.ModelForm):
         return stream
 
 
+class AssignInvitationsOfficerForm(forms.ModelForm):
+    class Meta:
+        model = ProductionStream
+        fields = ('invitations_officer',)
+
+
 class AssignSupervisorForm(forms.ModelForm):
     class Meta:
         model = ProductionStream
diff --git a/production/managers.py b/production/managers.py
index 707df554a..410c02b9c 100644
--- a/production/managers.py
+++ b/production/managers.py
@@ -11,7 +11,12 @@ class ProductionStreamQuerySet(models.QuerySet):
         return self.exclude(status=PRODUCTION_STREAM_COMPLETED)
 
     def filter_for_user(self, production_user):
-        return self.filter(officer=production_user)
+        """
+        Return ProductionStreams that are only assigned to me as a Production Officer
+        or a Inivtations Officer.
+        """
+        return self.filter(models.Q(officer=production_user)
+                           | models.Q(invitations_officer=production_user))
 
 
 class ProductionEventManager(models.Manager):
diff --git a/production/migrations/0029_productionstream_invitations_officer.py b/production/migrations/0029_productionstream_invitations_officer.py
new file mode 100644
index 000000000..8159df2f1
--- /dev/null
+++ b/production/migrations/0029_productionstream_invitations_officer.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2017-10-09 18:00
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('production', '0028_auto_20171007_1311'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='productionstream',
+            name='invitations_officer',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='invitations_officer_streams', to='production.ProductionUser'),
+        ),
+    ]
diff --git a/production/models.py b/production/models.py
index fa6da78db..3608fd490 100644
--- a/production/models.py
+++ b/production/models.py
@@ -42,6 +42,8 @@ class ProductionStream(models.Model):
                                 related_name='streams')
     supervisor = models.ForeignKey('production.ProductionUser', blank=True, null=True,
                                    related_name='supervised_streams')
+    invitations_officer = models.ForeignKey('production.ProductionUser', blank=True, null=True,
+                                            related_name='invitations_officer_streams')
 
     work_logs = GenericRelation(WorkLog, related_query_name='streams')
 
diff --git a/production/templates/production/partials/production_stream_card.html b/production/templates/production/partials/production_stream_card.html
index 0a3537e74..7a5470487 100644
--- a/production/templates/production/partials/production_stream_card.html
+++ b/production/templates/production/partials/production_stream_card.html
@@ -33,45 +33,65 @@
 
   {% include 'partials/finances/logs.html' with logs=stream.work_logs.all %}
 
-    {% if "can_perform_supervisory_actions" in sub_perms %}
+    {% if "can_perform_supervisory_actions" in sub_perms or "can_work_for_stream" 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 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>
+              {% if "can_perform_supervisory_actions" in sub_perms %}
+                  {% 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 %}
+                      {% if 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 assign_officer_form %}
+                        <li>
+                            <a href="javascript:;" data-toggle="toggle" data-target="#add_invs_officer_{{stream.id}}">Assign Invitations Officer to this stream</a>
+                            <div id="add_invs_officer_{{stream.id}}" style="display: none;">
+                                <form class="my-3" action="{% url 'production:add_invitations_officer' stream_id=stream.id %}" method="post">
+                                  	{% csrf_token %}
+                                  	{{ assign_invitiations_officer_form|bootstrap_inline }}
+                                  	<input type="submit" class="btn btn-outline-primary" name="submit" value="Add officer">
+                                </form>
+                            </div>
+                        </li>
+                    {% endif %}
+                  {% endif %}
               {% endif %}
-              {% if perms.scipost.can_upload_proofs and stream.status != 'accepted' and stream.status != 'completed' and stream.status != 'cited' and upload_proofs_form %}
-                  <li>
-                      <a href="javascript:;" data-toggle="toggle" data-target="#upload_proofs">Upload Proofs</a>
-                      <div id="upload_proofs" style="display: none;">
-                          <form class="my-3" action="{% url 'production:upload_proofs' stream_id=stream.id %}" method="post" enctype="multipart/form-data">
-                            	{% csrf_token %}
-                            	{{ upload_proofs_form|bootstrap_inline }}
-                            	<input type="submit" class="btn btn-outline-primary" name="submit" value="Upload">
-                          </form>
-                      </div>
-                  </li>
+
+              {% if "can_work_for_stream" in sub_perms %}
+                  {% if perms.scipost.can_upload_proofs and upload_proofs_form %}
+                      <li>
+                          <a href="javascript:;" data-toggle="toggle" data-target="#upload_proofs">Upload Proofs</a>
+                          <div id="upload_proofs" style="display: none;">
+                              <form class="my-3" action="{% url 'production:upload_proofs' stream_id=stream.id %}" method="post" enctype="multipart/form-data">
+                                	{% csrf_token %}
+                                	{{ upload_proofs_form|bootstrap_inline }}
+                                	<input type="submit" class="btn btn-outline-primary" name="submit" value="Upload">
+                              </form>
+                          </div>
+                      </li>
+                  {% endif %}
               {% 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>
@@ -103,4 +123,14 @@
               <em>No Officer assigned yet.</em>
           {% endif %}
     </li>
+    <li>Invitations Officer:
+          {% if stream.invitations_officer %}
+              <strong>{{ stream.invitations_officer }}</strong>
+              {% if "can_work_for_stream" in sub_perms and perms.scipost.can_assign_production_officer %}
+                  &middot; <a href="{% url 'production:remove_invitations_officer' stream_id=stream.id officer_id=stream.invitations_officer.id %}" class="text-danger">Remove from stream</a>
+              {% endif %}
+          {% else %}
+              <em>No Invitations 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
index 13f535c15..eaac3f05c 100644
--- a/production/templates/production/partials/production_stream_card_completed.html
+++ b/production/templates/production/partials/production_stream_card_completed.html
@@ -9,7 +9,7 @@
 <div class="card-body">
       <h3>Stream details</h3>
       <ul>
-          <li>Status: <em>{{ stream.get_status_display }}</em></li>
+          <li>Status: <span class="label label-secondary label-sm">{{ stream.get_status_display }}</span></li>
           {% block officers %}
               <li>Production Supervisor:
                     {% if stream.supervisor %}
@@ -25,6 +25,13 @@
                         <em>No Officer assigned.</em>
                     {% endif %}
               </li>
+              <li>Invitations Officer:
+                    {% if stream.invitations_officer %}
+                        <strong>{{ stream.invitations_officer }}</strong>
+                    {% else %}
+                        <em>No Invitations Officer assigned.</em>
+                    {% endif %}
+              </li>
           {% endblock %}
       </ul>
 
diff --git a/production/urls.py b/production/urls.py
index 3a17b372c..66349aec7 100644
--- a/production/urls.py
+++ b/production/urls.py
@@ -27,6 +27,10 @@ urlpatterns = [
         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]+)/invitations_officer/add$',
+        production_views.add_invitations_officer, name='add_invitations_officer'),
+    url(r'^streams/(?P<stream_id>[0-9]+)/invitations_officer/(?P<officer_id>[0-9]+)/remove$',
+        production_views.remove_invitations_officer, name='remove_invitations_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$',
diff --git a/production/views.py b/production/views.py
index 20110d94d..b4f5d56a3 100644
--- a/production/views.py
+++ b/production/views.py
@@ -20,7 +20,8 @@ from finances.forms import WorkLogForm
 from . import constants
 from .models import ProductionUser, ProductionStream, ProductionEvent, Proof
 from .forms import ProductionEventForm, AssignOfficerForm, UserToOfficerForm,\
-                   AssignSupervisorForm, StreamStatusForm, ProofUploadForm, ProofDecisionForm
+                   AssignSupervisorForm, StreamStatusForm, ProofUploadForm, ProofDecisionForm,\
+                   AssignInvitationsOfficerForm
 from .permissions import is_production_user
 from .signals import notify_stream_status_change,  notify_new_stream_assignment
 from .utils import proof_slug_to_id
@@ -54,10 +55,14 @@ def production(request, stream_id=None):
 
     if stream_id:
         try:
+            # "Pre-load" ProductionStream
             context['stream'] = streams.get(id=stream_id)
             context['assign_officer_form'] = AssignOfficerForm()
+            context['assign_invitiations_officer_form'] = AssignInvitationsOfficerForm()
             context['assign_supervisor_form'] = AssignSupervisorForm()
             context['prodevent_form'] = ProductionEventForm()
+            context['work_log_form'] = WorkLogForm()
+            context['upload_proofs_form'] = ProofUploadForm()
         except ProductionStream.DoesNotExist:
             pass
 
@@ -98,6 +103,7 @@ def stream(request, stream_id):
     stream = get_object_or_404(streams, id=stream_id)
     prodevent_form = ProductionEventForm()
     assign_officer_form = AssignOfficerForm()
+    assign_invitiations_officer_form = AssignInvitationsOfficerForm()
     assign_supervisor_form = AssignSupervisorForm()
     upload_proofs_form = ProofUploadForm()
     work_log_form = WorkLogForm()
@@ -108,6 +114,7 @@ def stream(request, stream_id):
         'prodevent_form': prodevent_form,
         'assign_officer_form': assign_officer_form,
         'assign_supervisor_form': assign_supervisor_form,
+        'assign_invitiations_officer_form': assign_invitiations_officer_form,
         'status_form': status_form,
         'upload_proofs_form': upload_proofs_form,
         'work_log_form': work_log_form,
@@ -202,6 +209,36 @@ def add_officer(request, stream_id):
     return redirect(reverse('production:production', args=(stream.id,)))
 
 
+@is_production_user()
+@permission_required('scipost.can_assign_production_officer', raise_exception=True)
+@transaction.atomic
+def add_invitations_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', args=(stream.id,)))
+
+    form = AssignInvitationsOfficerForm(request.POST or None, instance=stream)
+    if form.is_valid():
+        form.save()
+        officer = form.cleaned_data.get('invitations_officer')
+        assign_perm('can_work_for_stream', officer.user, stream)
+        messages.success(request, 'Invitations Officer {officer} has been assigned.'.format(
+            officer=officer))
+        notify_new_stream_assignment(request.user, stream, officer.user)
+        event = ProductionEvent(
+            stream=stream,
+            event='assignment',
+            comments=' tasked Invitations Officer with invitations:',
+            noted_to=officer,
+            noted_by=request.user.production_user)
+        event.save()
+    else:
+        for key, error in form.errors.items():
+            messages.warning(request, error[0])
+    return redirect(reverse('production:production', args=(stream.id,)))
+
+
 @is_production_user()
 @permission_required('scipost.can_assign_production_officer', raise_exception=True)
 @transaction.atomic
@@ -215,12 +252,36 @@ def remove_officer(request, stream_id, officer_id):
         officer = stream.officer
         stream.officer = None
         stream.save()
-        remove_perm('can_work_for_stream', officer.user, stream)
+        if officer not in [stream.invitations_officer, stream.supervisor]:
+            # Remove Officer from stream if not assigned anymore
+            remove_perm('can_work_for_stream', officer.user, stream)
         messages.success(request, 'Officer {officer} has been removed.'.format(officer=officer))
 
     return redirect(reverse('production:production', args=(stream.id,)))
 
 
+@is_production_user()
+@permission_required('scipost.can_assign_production_officer', raise_exception=True)
+@transaction.atomic
+def remove_invitations_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', args=(stream.id,)))
+
+    if getattr(stream.invitations_officer, 'id', 0) == int(officer_id):
+        officer = stream.invitations_officer
+        stream.invitations_officer = None
+        stream.save()
+        if officer not in [stream.officer, stream.supervisor]:
+            # Remove Officer from stream if not assigned anymore
+            remove_perm('can_work_for_stream', officer.user, stream)
+        messages.success(request, 'Invitations Officer {officer} has been removed.'.format(
+            officer=officer))
+
+    return redirect(reverse('production:production', args=(stream.id,)))
+
+
 @is_production_user()
 @permission_required('scipost.can_assign_production_supervisor', raise_exception=True)
 @transaction.atomic
-- 
GitLab