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> · {{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