From ff846a4de8a0ee764400bd4360b9202f9c5d02c8 Mon Sep 17 00:00:00 2001
From: Jorran de Wit <jorrandewit@outlook.com>
Date: Wed, 21 Jun 2017 21:55:57 +0200
Subject: [PATCH] Work in process, see comments

---
 partners/admin.py                             |   1 +
 partners/constants.py                         |   3 +-
 partners/forms.py                             | 116 +++++++++++++++---
 partners/managers.py                          |   7 +-
 partners/models.py                            |  12 +-
 .../partners/_partners_page_base.html         |  14 +++
 .../partners/_prospective_partner_card.html   |  20 +--
 .../partners/promote_prospartner.html         |  45 +++++++
 partners/urls.py                              |  12 +-
 partners/views.py                             |  27 +++-
 scipost/static/scipost/assets/css/_form.scss  |   5 +
 11 files changed, 220 insertions(+), 42 deletions(-)
 create mode 100644 partners/templates/partners/_partners_page_base.html
 create mode 100644 partners/templates/partners/promote_prospartner.html

diff --git a/partners/admin.py b/partners/admin.py
index fc4f71b22..533597a4c 100644
--- a/partners/admin.py
+++ b/partners/admin.py
@@ -26,6 +26,7 @@ class ProspectiveContactInline(admin.TabularInline):
 
 class ProspectivePartnerEventInline(admin.TabularInline):
     model = ProspectivePartnerEvent
+    extra = 0
 
 
 class ProspectivePartnerAdmin(admin.ModelAdmin):
diff --git a/partners/constants.py b/partners/constants.py
index af2004f47..9c40cf5aa 100644
--- a/partners/constants.py
+++ b/partners/constants.py
@@ -47,8 +47,9 @@ PROSPECTIVE_PARTNER_EVENTS = (
 )
 
 
+PARTNER_INITIATED = 'Initiated'
 PARTNER_STATUS = (
-    ('Initiated', 'Initiated'),
+    (PARTNER_INITIATED, 'Initiated'),
     ('Contacted', 'Contacted'),
     ('Negotiating', 'Negotiating'),
     ('Uninterested', 'Uninterested'),
diff --git a/partners/forms.py b/partners/forms.py
index a09a6b424..4d6431b53 100644
--- a/partners/forms.py
+++ b/partners/forms.py
@@ -5,20 +5,109 @@ from django_countries import countries
 from django_countries.widgets import CountrySelectWidget
 from django_countries.fields import LazyTypedChoiceField
 
-from .constants import PARTNER_KINDS
-from .models import Partner, ProspectivePartner, ProspectiveContact, ProspectivePartnerEvent
+from .constants import PARTNER_KINDS, PROSPECTIVE_PARTNER_PROCESSED, CONTACT_TYPES
+from .models import Partner, ProspectivePartner, ProspectiveContact, ProspectivePartnerEvent,\
+                    Institution, Contact
 
 from scipost.models import TITLE_CHOICES
 
 
-class PartnerForm(forms.ModelForm):
+class PromoteToPartnerForm(forms.ModelForm):
+    address = forms.CharField(widget=forms.Textarea(), required=False)
+    acronym = forms.CharField()
+
     class Meta:
-        model = Partner
-        fields = '__all__'
+        model = ProspectivePartner
+        fields = (
+            'kind',
+            'institution_name',
+            'country',
+        )
+
+    def promote_to_partner(self):
+        # Create new instances
+        institution = Institution(
+            kind=self.cleaned_data['kind'],
+            name=self.cleaned_data['institution_name'],
+            acronym=self.cleaned_data['acronym'],
+            address=self.cleaned_data['address'],
+            country=self.cleaned_data['country']
+        )
+        institution.save()
+        partner = Partner(
+            institution=institution,
+            main_contact=None
+        )
+        partner.save()
+
+        # Close Prospect
+        self.instance.status = PROSPECTIVE_PARTNER_PROCESSED
+        self.instance.save()
+        return (partner, institution,)
+
+
+class PromoteToContactForm(forms.ModelForm):
+    """
+    This form is used to create a new `partners.Contact`
+    """
+    contact_types = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple,
+                                              choices=CONTACT_TYPES, required=False)
 
-    def __init__(self, *args, **kwargs):
-        super(PartnerForm, self).__init__(*args, **kwargs)
-        self.fields['institution_address'].widget = forms.Textarea({'rows': 8, })
+    class Meta:
+        model = ProspectiveContact
+        fields = (
+            'title',
+            'first_name',
+            'last_name',
+            'email',
+        )
+
+    def promote_contacts(self, partner):
+        """
+        Promote ProspectiveContact's to Contact's related to a certain Partner.
+        """
+        raise NotImplemented
+
+
+class PromoteToContactFormset(forms.BaseModelFormSet):
+    """
+    This is a formset to process multiple `PromoteToContactForm`s at the same time
+    designed for the 'promote prospect to partner' action.
+    """
+    def clean(self):
+        """
+        Check if all CONTACT_TYPES are assigned to at least one contact.
+        """
+        contact_type_keys = list(dict(CONTACT_TYPES).keys())
+        for form in self.forms:
+            try:
+                contact_types = form.cleaned_data['contact_types']
+            except KeyError:
+                # Form invalid for `contact_types`
+                continue
+
+            for _type in contact_types:
+                try:
+                    contact_type_keys.remove(_type)
+                except ValueError:
+                    # Type-key already removed
+                    continue
+            if not contact_type_keys:
+                break
+        if contact_type_keys:
+            # Add error to all forms if not all CONTACT_TYPES are assigned
+            for form in self.forms:
+                form.add_error('contact_types', "Not all contact types have been selected yet.")
+
+    def save(self, *args, **kwargs):
+        raise DeprecationWarning(("This formset is not meant to used with the default"
+                                  " `save` method. User the `promote_contacts` instead."))
+
+    def promote_contacts(self, partner):
+        """
+        Promote ProspectiveContact's to Contact's related to a certain Partner.
+        """
+        raise NotImplemented
 
 
 class ProspectivePartnerForm(forms.ModelForm):
@@ -55,17 +144,6 @@ class EmailProspectivePartnerContactForm(forms.Form):
             {'placeholder': 'Write your message in this box (optional).'})
 
 
-# class ProspectivePartnerContactSelectForm(forms.Form):
-
-#     def __init__(self, *args, **kwargs):
-#         prospartner_id = kwargs.pop('prospartner.id')
-#         super(ProspectivePartnerContactSelectForm, self).__init(*args, **kwargs)
-#         self.fields['contact'] = forms.ModelChoiceField(
-#             queryset=ProspectiveContact.objects.filter(
-#                 prospartner__pk=prospartner_id).order_by('last_name'),
-#             required=True)
-
-
 class ProspectivePartnerEventForm(forms.ModelForm):
     class Meta:
         model = ProspectivePartnerEvent
diff --git a/partners/managers.py b/partners/managers.py
index 53729dc30..fd9f6980b 100644
--- a/partners/managers.py
+++ b/partners/managers.py
@@ -1,6 +1,11 @@
 from django.db import models
 
-from .constants import MEMBERSHIP_SUBMITTED
+from .constants import MEMBERSHIP_SUBMITTED, PROSPECTIVE_PARTNER_PROCESSED
+
+
+class ProspectivePartnerManager(models.Manager):
+    def not_yet_partner(self):
+        return self.exclude(status=PROSPECTIVE_PARTNER_PROCESSED)
 
 
 class MembershipAgreementManager(models.Manager):
diff --git a/partners/models.py b/partners/models.py
index 211f64a6c..71f6fc10d 100644
--- a/partners/models.py
+++ b/partners/models.py
@@ -14,9 +14,10 @@ from .constants import PROSPECTIVE_PARTNER_EVENT_EMAIL_SENT,\
                        PROSPECTIVE_PARTNER_EVENT_MARKED_AS_UNINTERESTED,\
                        PROSPECTIVE_PARTNER_UNINTERESTED,\
                        PROSPECTIVE_PARTNER_EVENT_PROMOTED,\
-                       PROSPECTIVE_PARTNER_PROCESSED, CONTACT_TYPES
+                       PROSPECTIVE_PARTNER_PROCESSED, CONTACT_TYPES,\
+                       PARTNER_INITIATED
 
-from .managers import MembershipAgreementManager
+from .managers import MembershipAgreementManager, ProspectivePartnerManager
 
 from scipost.constants import TITLE_CHOICES
 from scipost.fields import ChoiceArrayField
@@ -39,6 +40,8 @@ class ProspectivePartner(models.Model):
     status = models.CharField(max_length=32, choices=PROSPECTIVE_PARTNER_STATUS,
                               default=PROSPECTIVE_PARTNER_ADDED)
 
+    objects = ProspectivePartnerManager()
+
     def __str__(self):
         return '%s (received %s), %s' % (self.institution_name,
                                          self.date_received.strftime("%Y-%m-%d"),
@@ -63,7 +66,8 @@ class ProspectiveContact(models.Model):
     It does not have a corresponding User object.
     It is meant to be used internally at SciPost, during Partner mining.
     """
-    prospartner = models.ForeignKey('partners.ProspectivePartner', on_delete=models.CASCADE)
+    prospartner = models.ForeignKey('partners.ProspectivePartner', on_delete=models.CASCADE,
+                                    related_name='prospective_contacts')
     title = models.CharField(max_length=4, choices=TITLE_CHOICES)
     first_name = models.CharField(max_length=64)
     last_name = models.CharField(max_length=64)
@@ -134,7 +138,7 @@ class Partner(models.Model):
     """
     institution = models.ForeignKey('partners.Institution', on_delete=models.CASCADE,
                                     blank=True, null=True)
-    status = models.CharField(max_length=16, choices=PARTNER_STATUS)
+    status = models.CharField(max_length=16, choices=PARTNER_STATUS, default=PARTNER_INITIATED)
     main_contact = models.ForeignKey('partners.Contact', on_delete=models.CASCADE,
                                      blank=True, null=True,
                                      related_name='partner_main_contact')
diff --git a/partners/templates/partners/_partners_page_base.html b/partners/templates/partners/_partners_page_base.html
new file mode 100644
index 000000000..c0e00cd2a
--- /dev/null
+++ b/partners/templates/partners/_partners_page_base.html
@@ -0,0 +1,14 @@
+{% extends 'scipost/base.html' %}
+
+{% block breadcrumb %}
+    <nav class="breadcrumb py-md-2 px-0">
+        <div class="container">
+            {% block breadcrumb_items %}
+                <a href="{% url 'partners:dashboard' %}" class="breadcrumb-item">Partner Page</a>
+            {% endblock %}
+        </div>
+    </nav>
+{% endblock %}
+
+
+{% block pagetitle %}: Supporting Partners:{% endblock pagetitle %}
diff --git a/partners/templates/partners/_prospective_partner_card.html b/partners/templates/partners/_prospective_partner_card.html
index 5496f483c..b2f74e7a4 100644
--- a/partners/templates/partners/_prospective_partner_card.html
+++ b/partners/templates/partners/_prospective_partner_card.html
@@ -25,7 +25,7 @@
 	  <th>Actions</th>
         </thead>
         <tbody>
-          {% for contact in pp.prospectivecontact_set.all %}
+          {% for contact in pp.prospective_contacts.all %}
           <tr>
             <td>{{ contact.role }}</td>
             <td>{{ contact.get_title_display }} {{ contact.first_name }} {{ contact.last_name }}</td>
@@ -43,8 +43,7 @@
   </div>
 
   <div class="row">
-    <div class="col-1"></div>
-    <div class="col-6">
+    <div class="col-md-6 offset-md-1">
       <h3>Events</h3>
       <ul>
 	{% for event in pp.prospectivepartnerevent_set.all %}
@@ -54,13 +53,18 @@
 	{% endfor %}
       </ul>
     </div>
-    <div class="col-5">
+    <div class="col-md-5">
       <h3>Add an event for this Prospective Partner</h3>
-      <form action="{% url 'partners:add_prospartner_event' prospartner_id=pp.id %}" method="post">
-	{% csrf_token %}
-	{{ ppevent_form|bootstrap }}
-	<input type="submit" name="submit" value="Submit" class="btn btn-secondary">
+      <form class="d-block mt-2 mb-3" action="{% url 'partners:add_prospartner_event' prospartner_id=pp.id %}" method="post">
+        {% csrf_token %}
+        {{ ppevent_form|bootstrap }}
+        <input type="submit" name="submit" value="Submit" class="btn btn-secondary">
       </form>
+
+      <h3>Partner status</h3>
+      <ul>
+          <li><a href="{% url 'partners:promote_prospartner' pp.id %}">Upgrade prospect to partner</a></li>
+      </ul>
     </div>
   </div>
 </div>
diff --git a/partners/templates/partners/promote_prospartner.html b/partners/templates/partners/promote_prospartner.html
new file mode 100644
index 000000000..c7aca96ad
--- /dev/null
+++ b/partners/templates/partners/promote_prospartner.html
@@ -0,0 +1,45 @@
+{% extends 'partners/_partners_page_base.html' %}
+
+{% block breadcrumb_items %}
+    {{block.super}}
+    <span class="breadcrumb-item">Promote Prospect</span>
+{% endblock %}
+
+{% block pagetitle %}{{block.super}} Promote Prospect{% endblock pagetitle %}
+
+{% load bootstrap %}
+
+{% block content %}
+
+<div class="row">
+    <div class="col-12">
+        <h1 class="highlight">Promote Prospect to Partner</h1>
+    </div>
+</div>
+
+<div class="row">
+    <div class="col-12">
+      <form method="post">
+        {% csrf_token %}
+        {{ form|bootstrap }}
+        {{ contact_formset.management_form }}
+
+        {% for form in contact_formset %}
+            <h3>Contact {{forloop.counter}}</h3>
+            {{ form|bootstrap }}
+        {% endfor %}
+
+        {% for error in contact_formset.non_form_errors %}
+            <div class="form-group row">
+                <div class="alert alert-danger show">
+                    <strong>Form error</strong> &middot; {{error}}
+                </div>
+            </div>
+        {% endfor %}
+
+        <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 e86eae194..1c092ae45 100644
--- a/partners/urls.py
+++ b/partners/urls.py
@@ -7,12 +7,14 @@ urlpatterns = [
     url(r'^dashboard$', views.dashboard, name='dashboard'),
     url(r'^membership_request$', views.membership_request, name='membership_request'),
     url(r'^manage$', views.manage, name='manage'),
-    url(r'^add_prospective_partner$', views.add_prospective_partner,
+    url(r'^prospect_partners/add$', views.add_prospective_partner,
         name='add_prospective_partner'),
-    url(r'^add_prospartner_contact/(?P<prospartner_id>[0-9]+)$',
-        views.add_prospartner_contact, name='add_prospartner_contact'),
-    url(r'^email_prospartner_contact/(?P<contact_id>[0-9]+)$',
+    url(r'^prospect_partners/contacts/(?P<contact_id>[0-9]+)/email$',
         views.email_prospartner_contact, name='email_prospartner_contact'),
-    url(r'^add_prospartner_event/(?P<prospartner_id>[0-9]+)$',
+    url(r'^prospect_partner/(?P<prospartner_id>[0-9]+)/contacts/add$',
+        views.add_prospartner_contact, name='add_prospartner_contact'),
+    url(r'^prospect_partner/(?P<prospartner_id>[0-9]+)/promote$',
+        views.promote_prospartner, name='promote_prospartner'),
+    url(r'^prospect_partner/(?P<prospartner_id>[0-9]+)/events/add$',
         views.add_prospartner_event, name='add_prospartner_event'),
 ]
diff --git a/partners/views.py b/partners/views.py
index e233fe5b8..1d863549c 100644
--- a/partners/views.py
+++ b/partners/views.py
@@ -1,5 +1,6 @@
 from django.contrib import messages
 from django.db import transaction
+from django.forms import modelformset_factory, formset_factory
 from django.shortcuts import get_object_or_404, render, reverse, redirect
 from django.utils import timezone
 
@@ -11,8 +12,8 @@ from .constants import PROSPECTIVE_PARTNER_REQUESTED,\
 from .models import Partner, ProspectivePartner, ProspectiveContact,\
     ProspectivePartnerEvent, MembershipAgreement
 from .forms import ProspectivePartnerForm, ProspectiveContactForm,\
-    EmailProspectivePartnerContactForm,\
-    ProspectivePartnerEventForm, MembershipQueryForm
+    EmailProspectivePartnerContactForm, PromoteToPartnerForm,\
+    ProspectivePartnerEventForm, MembershipQueryForm, PromoteToContactForm, PromoteToContactFormset
 
 from .utils import PartnerUtils
 
@@ -57,8 +58,8 @@ def membership_request(request):
         )
         contact.save()
         prospartnerevent = ProspectivePartnerEvent(
-            prospartner = prospartner,
-            event = PROSPECTIVE_PARTNER_EVENT_REQUESTED,)
+            prospartner=prospartner,
+            event=PROSPECTIVE_PARTNER_EVENT_REQUESTED)
         prospartnerevent.save()
         ack_message = ('Thank you for your SPB Membership query. '
                        'We will get back to you in the very near future '
@@ -85,6 +86,24 @@ def manage(request):
     return render(request, 'partners/manage_partners.html', context)
 
 
+@permission_required('scipost.can_manage_SPB', return_403=True)
+@transaction.atomic
+def promote_prospartner(request, prospartner_id):
+    prospartner = get_object_or_404(ProspectivePartner.objects.not_yet_partner(),
+                                    pk=prospartner_id)
+    form = PromoteToPartnerForm(request.POST or None, instance=prospartner)
+    ContactModelFormset = modelformset_factory(ProspectiveContact, PromoteToContactForm,
+                                               formset=PromoteToContactFormset)
+    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()
+        contact_formset.promote_contacts(partner)
+        raise NotImplemented
+    context = {'form': form, 'contact_formset': contact_formset}
+    return render(request, 'partners/promote_prospartner.html', context)
+
+
 @permission_required('scipost.can_manage_SPB', return_403=True)
 def add_prospective_partner(request):
     form = ProspectivePartnerForm(request.POST or None)
diff --git a/scipost/static/scipost/assets/css/_form.scss b/scipost/static/scipost/assets/css/_form.scss
index 46777f809..8c268068e 100644
--- a/scipost/static/scipost/assets/css/_form.scss
+++ b/scipost/static/scipost/assets/css/_form.scss
@@ -10,6 +10,11 @@
     border-color: #d9534f;
 }
 
+.has-error .multiple-checkbox .help-block {
+    color: $brand-danger;
+    font-weight: 600;
+}
+
 .form-control + .help-block {
     margin-top: 3px;
     display: inline-block;
-- 
GitLab