From 40a2a3f3225795932c55f4da6594bfca4f4023c6 Mon Sep 17 00:00:00 2001
From: Jorran de Wit <jorrandewit@outlook.com>
Date: Sat, 24 Jun 2017 14:21:55 +0200
Subject: [PATCH] Optimize Partners; Add event handling

---
 partners/constants.py                         |  4 +-
 partners/forms.py                             | 30 +++++++--
 .../migrations/0017_auto_20170624_1358.py     | 27 ++++++++
 partners/models.py                            | 20 ++++--
 .../templates/partners/_partner_card.html     |  3 +-
 .../templates/partners/partners_detail.html   | 64 +++++++++++++++++++
 partners/urls.py                              |  1 +
 partners/views.py                             | 28 ++++++--
 .../commands/add_groups_and_permissions.py    |  6 ++
 9 files changed, 167 insertions(+), 16 deletions(-)
 create mode 100644 partners/migrations/0017_auto_20170624_1358.py
 create mode 100644 partners/templates/partners/partners_detail.html

diff --git a/partners/constants.py b/partners/constants.py
index 9c40cf5aa..a50de3623 100644
--- a/partners/constants.py
+++ b/partners/constants.py
@@ -64,10 +64,10 @@ CONSORTIUM_STATUS = (
     ('Inactive', 'Inactive'),
 )
 
-
+PARTNER_STATUS_UPDATE = 'status_update'
 PARTNER_EVENTS = (
     ('initial', 'Contacted (initial)'),
-    ('status_update', 'Status updated'),
+    (PARTNER_STATUS_UPDATE, 'Status updated'),
     ('comment', 'Comment added'),
 )
 
diff --git a/partners/forms.py b/partners/forms.py
index b87dcf430..c6d76ba07 100644
--- a/partners/forms.py
+++ b/partners/forms.py
@@ -9,9 +9,10 @@ from django_countries import countries
 from django_countries.widgets import CountrySelectWidget
 from django_countries.fields import LazyTypedChoiceField
 
-from .constants import PARTNER_KINDS, PROSPECTIVE_PARTNER_PROCESSED, CONTACT_TYPES
+from .constants import PARTNER_KINDS, PROSPECTIVE_PARTNER_PROCESSED, CONTACT_TYPES,\
+                       PARTNER_STATUS_UPDATE
 from .models import Partner, ProspectivePartner, ProspectiveContact, ProspectivePartnerEvent,\
-                    Institution, Contact
+                    Institution, Contact, PartnerEvent
 
 from scipost.models import TITLE_CHOICES
 
@@ -48,14 +49,27 @@ class ActivationForm(forms.ModelForm):
     def activate_user(self):
         if self.errors:
             return forms.ValidationError
+
+        # Activate account
         self.instance.is_active = True
         self.instance.set_password(self.cleaned_data['password_new'])
         self.instance.save()
+
+        # Add permission groups to user
         group = Group.objects.get(name='Partners Accounts')
         self.instance.groups.add(group)
         return self.instance
 
 
+class PartnerEventForm(forms.ModelForm):
+    class Meta:
+        model = PartnerEvent
+        fields = (
+            'event',
+            'comments',
+        )
+
+
 class InstitutionForm(forms.ModelForm):
     class Meta:
         model = Institution
@@ -193,7 +207,7 @@ class ContactFormset(forms.BaseModelFormSet):
 
 class PromoteToPartnerForm(forms.ModelForm):
     address = forms.CharField(widget=forms.Textarea(), required=False)
-    acronym = forms.CharField()
+    acronym = forms.CharField(max_length=16)
 
     class Meta:
         model = ProspectivePartner
@@ -203,7 +217,7 @@ class PromoteToPartnerForm(forms.ModelForm):
             'country',
         )
 
-    def promote_to_partner(self):
+    def promote_to_partner(self, current_user):
         # Create new instances
         institution = Institution(
             kind=self.cleaned_data['kind'],
@@ -218,6 +232,14 @@ class PromoteToPartnerForm(forms.ModelForm):
             main_contact=None
         )
         partner.save()
+        event = PartnerEvent(
+            partner=partner,
+            event=PARTNER_STATUS_UPDATE,
+            comments='ProspectivePartner has been upgraded to Partner by %s %s'
+                     % (current_user.first_name, current_user.last_name),
+            noted_by=current_user
+        )
+        event.save()
 
         # Close Prospect
         self.instance.status = PROSPECTIVE_PARTNER_PROCESSED
diff --git a/partners/migrations/0017_auto_20170624_1358.py b/partners/migrations/0017_auto_20170624_1358.py
new file mode 100644
index 000000000..bd94e1524
--- /dev/null
+++ b/partners/migrations/0017_auto_20170624_1358.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-06-24 11:58
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('partners', '0016_auto_20170624_0905'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='partnerevent',
+            name='noted_by',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.AlterField(
+            model_name='partnerevent',
+            name='partner',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='events', to='partners.Partner'),
+        ),
+    ]
diff --git a/partners/models.py b/partners/models.py
index 1b6b27708..5aeb7d68f 100644
--- a/partners/models.py
+++ b/partners/models.py
@@ -6,6 +6,7 @@ import string
 from django.contrib.auth.models import User
 from django.db import models
 from django.utils import timezone
+from django.urls import reverse
 
 from django_countries.fields import CountryField
 
@@ -27,7 +28,7 @@ from .managers import MembershipAgreementManager, ProspectivePartnerManager
 
 from scipost.constants import TITLE_CHOICES
 from scipost.fields import ChoiceArrayField
-from scipost.models import get_sentinel_user
+from scipost.models import get_sentinel_user, Contributor
 
 
 ########################
@@ -157,7 +158,14 @@ class Contact(models.Model):
         self.partners.remove(partner)
         if self.partners.exists():
             return self
-        return super().delete(*args, **kwargs)
+        try:
+            # User also has a Contributor-side, do not remove complete User
+            self.user.contributor
+            return super().delete(*args, **kwargs)
+        except Contributor.DoesNotExist:
+            # Remove User; casade-remove this Contact
+            self.user.delete()
+            return self
 
     @property
     def kind_display(self):
@@ -186,13 +194,17 @@ class Partner(models.Model):
             return self.institution.acronym + ' (' + self.get_status_display() + ')'
         return self.get_status_display()
 
+    def get_absolute_url(self):
+        return reverse('partners:partner_view', args=(self.id,))
+
 
 class PartnerEvent(models.Model):
-    partner = models.ForeignKey('partners.Partner', on_delete=models.CASCADE)
+    partner = models.ForeignKey('partners.Partner', on_delete=models.CASCADE,
+                                related_name='events')
     event = models.CharField(max_length=64, choices=PARTNER_EVENTS)
     comments = models.TextField(blank=True)
     noted_on = models.DateTimeField(auto_now_add=True)
-    noted_by = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE)
+    noted_by = models.ForeignKey(User, on_delete=models.CASCADE)
 
     def __str__(self):
         return '%s: %s' % (str(self.partner), self.get_event_display())
diff --git a/partners/templates/partners/_partner_card.html b/partners/templates/partners/_partner_card.html
index e9f858ee1..246348b71 100644
--- a/partners/templates/partners/_partner_card.html
+++ b/partners/templates/partners/_partner_card.html
@@ -7,7 +7,7 @@
     </div>
     <div class="col-md-4">
       <address>
-        <h3>{{ partner.institution.name }}</h3>
+        <h3><a href="{{partner.get_absolute_url}}">{{ partner.institution.name }}</a></h3>
         <strong>{{ partner.institution.acronym }} ({{ partner.institution.get_kind_display }})</strong><br>
         {{ partner.institution.address|linebreaks }}
         {{ partner.institution.get_country_display }}<br>
@@ -33,6 +33,7 @@
             <li><a href="{% url 'partners:partner_edit' partner.id %}">Edit Partner</a></li>
             <li><a href="{% url 'partners:institution_edit' partner.institution.id %}">Edit Institution</a></li>
             <li><a href="{% url 'partners:partner_add_contact' partner.id %}">Add Contact</a></li>
+            <li><a href="{{partner.get_absolute_url}}">View events ({{partner.events.count}})</a></li>
         </ul>
 
     </div>
diff --git a/partners/templates/partners/partners_detail.html b/partners/templates/partners/partners_detail.html
new file mode 100644
index 000000000..b8d083511
--- /dev/null
+++ b/partners/templates/partners/partners_detail.html
@@ -0,0 +1,64 @@
+{% extends 'partners/_partners_page_base.html' %}
+
+{% block breadcrumb_items %}
+    {{block.super}}
+    <span class="breadcrumb-item">Partner details</span>
+{% endblock %}
+
+{% block pagetitle %}{{block.super}} Partner details{% endblock pagetitle %}
+
+{% load bootstrap %}
+
+{% block content %}
+
+<div class="row">
+    <div class="col-12">
+        <h1 class="highlight">Partner {{partner}}</h1>
+    </div>
+</div>
+
+<div class="row">
+    <div class="col-md-6">
+        <a href="{% url 'partners:partner_edit' partner.id %}">Edit partner</a>
+        <address>
+            <h3>{{ partner.institution.name }}</h3>
+            <strong>{{ partner.institution.acronym }} ({{ partner.institution.get_kind_display }})</strong><br>
+            {{ partner.institution.address|linebreaks }}
+            {{ partner.institution.get_country_display }}<br>
+            Main contact: {{ partner.main_contact|default_if_none:'<em>Unknown</em>' }}
+        </address>
+
+        <hr>
+        <h3>Contacts</h3>
+        <ul>
+            {% for contact in partner.contact_set.all %}
+                <li>
+                    <h4>{{ contact.get_title_display }} {{ contact.user.first_name }} {{ contact.user.last_name }} {% if not contact.user.is_active %}<span class="label label-sm label-warning">Inactive</span>{% endif %}</h4>
+                    <div>({{ contact.kind_display }})</div>
+                    <div class="mb-2"><a href="mailto:{{ contact.user.email }}">{{ contact.user.email }}</a></div>
+                </li>
+            {% endfor %}
+        </ul>
+    </div>
+    <div class="col-md-6">
+        <h3>Partner Events</h3>
+        <ul>
+            {% for event in partner.events.all %}
+                {% include 'partners/_prospartner_event_li.html' with event=event %}
+            {% empty %}
+                <li>No events were found.</li>
+            {% endfor %}
+        </ul>
+
+        <hr>
+        <h3>Add new Event</h3>
+        <form method="post">
+            {% csrf_token %}
+            {{ form|bootstrap }}
+
+            <input class="btn btn-primary" type="submit" value="Submit"/>
+        </form>
+    </div>
+</div>
+
+{% endblock content %}
diff --git a/partners/urls.py b/partners/urls.py
index 6faf56a0a..74df2bf89 100644
--- a/partners/urls.py
+++ b/partners/urls.py
@@ -27,6 +27,7 @@ urlpatterns = [
     url(r'activate/(?P<activation_key>.+)$', views.activate_account, name='activate_account'),
 
     # Partners
+    url(r'(?P<partner_id>[0-9]+)$', views.partner_view, name='partner_view'),
     url(r'(?P<partner_id>[0-9]+)/edit$', views.partner_edit, name='partner_edit'),
     url(r'(?P<partner_id>[0-9]+)/contacts/add$', views.partner_add_contact,
         name='partner_add_contact'),
diff --git a/partners/views.py b/partners/views.py
index 38c839770..a3d7b1d01 100644
--- a/partners/views.py
+++ b/partners/views.py
@@ -16,7 +16,7 @@ from .forms import ProspectivePartnerForm, ProspectiveContactForm,\
                    EmailProspectivePartnerContactForm, PromoteToPartnerForm,\
                    ProspectivePartnerEventForm, MembershipQueryForm, PromoteToContactForm,\
                    PromoteToContactFormset, PartnerForm, ContactForm, ContactFormset,\
-                   NewContactForm, InstitutionForm, ActivationForm
+                   NewContactForm, InstitutionForm, ActivationForm, PartnerEventForm
 
 from .utils import PartnerUtils
 
@@ -91,7 +91,7 @@ def promote_prospartner(request, prospartner_id):
     contact_formset = ContactModelFormset(request.POST or None,
                                           queryset=prospartner.prospective_contacts.all())
     if form.is_valid() and contact_formset.is_valid():
-        partner, institution = form.promote_to_partner()
+        partner, institution = form.promote_to_partner(request.user)
         contacts = contact_formset.promote_contacts(partner)
 
         # partner.send_mail()
@@ -107,6 +107,24 @@ def promote_prospartner(request, prospartner_id):
 ###############
 # Partner views
 ###############
+@permission_required('scipost.can_view_partners', return_403=True)
+def partner_view(request, partner_id):
+    partner = get_object_or_404(Partner, id=partner_id)
+    form = PartnerEventForm(request.POST or None)
+    if form.is_valid():
+        event = form.save(commit=False)
+        event.partner = partner
+        event.noted_by = request.user
+        event.save()
+        messages.success(request, 'Added a new event to Partner.')
+        return redirect(partner.get_absolute_url())
+    context = {
+        'partner': partner,
+        'form': form
+    }
+    return render(request, 'partners/partners_detail.html', context)
+
+
 @permission_required('scipost.can_manage_SPB', return_403=True)
 @transaction.atomic
 def partner_edit(request, partner_id):
@@ -186,7 +204,7 @@ def add_prospartner_contact(request, prospartner_id):
     if form.is_valid():
         form.save()
         messages.success(request, 'Contact successfully added to Prospective Partner')
-        return redirect(reverse('partners:manage'))
+        return redirect(reverse('partners:dashboard'))
     context = {'form': form, 'prospartner': prospartner}
     return render(request, 'partners/add_prospartner_contact.html', context)
 
@@ -216,7 +234,7 @@ def email_prospartner_contact(request, contact_id):
 
         PartnerUtils.email_prospartner_contact()
         messages.success(request, 'Email successfully sent')
-        return redirect(reverse('partners:manage'))
+        return redirect(reverse('partners:dashboard'))
     context = {'contact': contact, 'form': form}
     return render(request, 'partners/email_prospartner_contact.html', context)
 
@@ -234,7 +252,7 @@ def add_prospartner_event(request, prospartner_id):
             ppevent.save()
             prospartner.update_status_from_event(ppevent.event)
             prospartner.save()
-            return redirect(reverse('partners:manage'))
+            return redirect(reverse('partners:dashboard'))
         else:
             errormessage = 'The form was invalidly filled.'
             return render(request, 'scipost/error.html', {'errormessage': errormessage})
diff --git a/scipost/management/commands/add_groups_and_permissions.py b/scipost/management/commands/add_groups_and_permissions.py
index d8787a2d2..716e77542 100644
--- a/scipost/management/commands/add_groups_and_permissions.py
+++ b/scipost/management/commands/add_groups_and_permissions.py
@@ -53,6 +53,10 @@ class Command(BaseCommand):
             codename='can_promote_prospect_to_partner',
             name='Can promote Prospective Partner to Partner',
             content_type=content_type)
+        can_view_partners, created = Permission.objects.get_or_create(
+            codename='can_view_partners',
+            name='Can view Partner details',
+            content_type=content_type)
 
         # Registration and invitations
         can_vet_registration_requests, created = Permission.objects.get_or_create(
@@ -271,10 +275,12 @@ class Command(BaseCommand):
             can_manage_SPB,
             can_promote_prospect_to_partner,
             can_email_prospartner_contact,
+            can_view_partners
         ])
         PartnersOfficers.permissions.set([
             can_read_personal_page,
             can_manage_SPB,
+            can_view_partners,
         ])
         PartnerAccounts.permissions.set([
             can_read_personal_page
-- 
GitLab