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