diff --git a/partners/forms.py b/partners/forms.py index 0d62654327d6f48d2e3b9524ed0ab1ffdfdef485..32e4956b75281473bfb567a54da5ed1266126017 100644 --- a/partners/forms.py +++ b/partners/forms.py @@ -14,6 +14,140 @@ from .models import Partner, ProspectivePartner, ProspectiveContact, Prospective from scipost.models import TITLE_CHOICES +class InstitutionForm(forms.ModelForm): + class Meta: + model = Institution + fields = ( + 'kind', + 'name', + 'acronym', + 'address', + 'country' + ) + + +class PartnerForm(forms.ModelForm): + class Meta: + model = Partner + fields = ( + 'institution', + 'status', + 'main_contact' + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['main_contact'].queryset = self.instance.contact_set.all() + + +class ContactForm(forms.ModelForm): + """ + This Contact form is mainly used for editing Contact instances. + """ + class Meta: + model = Contact + fields = ( + 'kind', + ) + + +class NewContactForm(ContactForm): + """ + This Contact form is used to create new Contact instances, as it will also handle + possible sending and activation of User instances coming with the new Contact. + """ + title = forms.ChoiceField(choices=TITLE_CHOICES, label='Title') + first_name = forms.CharField() + last_name = forms.CharField() + email = forms.CharField() + existing_user = None + + def __init__(self, *args, **kwargs): + """ + Partner is a required argument to tell the formset which Partner the Contact + is being edited for in the current form. + """ + self.partner = kwargs.pop('partner') + super().__init__(*args, **kwargs) + + def clean_email(self): + """ + Check if User already is known in the system. + """ + email = self.cleaned_data['email'] + try: + self.existing_user = User.objects.get(email=email) + if not self.data.get('confirm_use_existing', '') == 'on': + # Do not give error if user wants to use existing User + self.add_error('email', 'This User is already registered.') + self.fields['confirm_use_existing'] = forms.BooleanField( + required=False, initial=False, label='Use the existing user instead: %s %s' + % (self.existing_user.first_name, + self.existing_user.last_name)) + except User.DoesNotExist: + pass + return email + + def save(self, commit=True): + """ + If existing user is found, add it to the Partner. + """ + if self.existing_user and self.data.get('confirm_use_existing', '') == 'on': + # Do not create new Contact + try: + # Link Contact to new Partner + contact = self.existing_user.partner_contact + contact.partners.add(self.partner) + # TODO: Send mail to contact informing him/her about the new Partner + except Contact.DoesNotExist: + # Not yet a 'Contact-User' + contact = super().save(commit=False) + contact.title = self.existing_user.contributor.title + contact.user = self.existing_user + contact.save() + contact.partners.add(self.partner) + # TODO: Send mail to contact informing him/her about the new Partner + return contact + + # Create complete new Account (User + Contact) + user = User( + first_name=self.cleaned_data['first_name'], + last_name=self.cleaned_data['last_name'], + email=self.cleaned_data['email'], + username=self.cleaned_data['email'], + is_active=False, + ) + user.save() + contact = Contact( + user=user, + title=self.cleaned_data['title'], + kind=self.cleaned_data['kind'] + ) + contact.save() + contact.partners.add(self.partner) + # TODO: Send mail to contact to let him/her activate account + return contact + + +class ContactFormset(forms.BaseModelFormSet): + """ + Use custom formset to make sure the delete action will not delete an entire Contact + if the Contact still has relations with other Partners. + """ + def __init__(self, *args, **kwargs): + """ + Partner is a required argument to tell the formset which Partner the Contact + is being edited for in the current form. + """ + self.partner = kwargs.pop('partner') + super().__init__(*args, **kwargs) + + def delete_existing(self, obj, commit=True): + '''Deletes an existing model instance.''' + if commit: + obj.delete_or_remove_partner(self.partner) + + class PromoteToPartnerForm(forms.ModelForm): address = forms.CharField(widget=forms.Textarea(), required=False) acronym = forms.CharField() diff --git a/partners/models.py b/partners/models.py index 7148f51e6a55dcf32611f58856f2b94cdaba649a..5989cd4abc133c8fffa644bd2073f5cb5aed4de9 100644 --- a/partners/models.py +++ b/partners/models.py @@ -130,6 +130,16 @@ class Contact(models.Model): def __str__(self): return '%s %s, %s' % (self.get_title_display(), self.user.last_name, self.user.first_name) + def delete_or_remove_partner(self, partner, *args, **kwargs): + """ + Custom `delete` method as the contact does not always need to be deleted, + but sometimes just the link with a specific partner needs to be removed. + """ + self.partners.remove(partner) + if self.partners.exists(): + return self + return super().delete(*args, **kwargs) + @property def kind_display(self): """ diff --git a/partners/templates/partners/_partner_card.html b/partners/templates/partners/_partner_card.html index bfc788abf946493146460ad73181ab23166b94ae..e9f858ee11268356861f7e56be0c1b43ed2d69b7 100644 --- a/partners/templates/partners/_partner_card.html +++ b/partners/templates/partners/_partner_card.html @@ -20,7 +20,7 @@ <ul> {% for contact in partner.contact_set.all %} <li> - <h4>{{ contact.get_title_display }} {{ contact.user.first_name }} {{ contact.user.last_name }}</h4> + <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> @@ -28,7 +28,13 @@ </ul> </div> <div class="col-md-3"> - <p>Edit</p> + <h3>Actions</h3> + <ul> + <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> + </ul> + </div> </div> </div> diff --git a/partners/templates/partners/dashboard.html b/partners/templates/partners/dashboard.html index a7b5c0a0a7227efd51264bfaa989f41379139275..30a53fb49169641a39d0b862ad8ae7de2041ab84 100644 --- a/partners/templates/partners/dashboard.html +++ b/partners/templates/partners/dashboard.html @@ -59,8 +59,8 @@ <div class="col-md-6"> <h3>Partners</h3> <ul> - {% for partner in request.user.partners.all %} - <li>{{partner}}</li> + {% for partner in request.user.partner_contact.partners.all %} + <li>{{partner.institution}}</li> {% empty %} <li>No partners found. Please contact the SciPost admin.</li> {% endfor %} diff --git a/partners/templates/partners/institution_edit.html b/partners/templates/partners/institution_edit.html new file mode 100644 index 0000000000000000000000000000000000000000..69133f2840bc995a941f4390bf080becdbadcec4 --- /dev/null +++ b/partners/templates/partners/institution_edit.html @@ -0,0 +1,33 @@ +{% extends 'partners/_partners_page_base.html' %} + +{% block breadcrumb_items %} + {{block.super}} + <span class="breadcrumb-item">Edit Institution</span> +{% endblock %} + +{% block pagetitle %}{{block.super}} Edit Institution{% endblock pagetitle %} + +{% load bootstrap %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Edit Institution {{institution}}</h1> + </div> +</div> + +<div class="row"> + <div class="col-12"> + <form method="post"> + {% csrf_token %} + <div class="mb-5"> + {{ form|bootstrap }} + </div> + + <input class="btn btn-primary" type="submit" value="Submit"/> + </form> + </div> +</div> + +{% endblock content %} diff --git a/partners/templates/partners/partner_add_contact.html b/partners/templates/partners/partner_add_contact.html new file mode 100644 index 0000000000000000000000000000000000000000..cde33236f75bc734284971ed5f8805edaf364115 --- /dev/null +++ b/partners/templates/partners/partner_add_contact.html @@ -0,0 +1,33 @@ +{% extends 'partners/_partners_page_base.html' %} + +{% block breadcrumb_items %} + {{block.super}} + <span class="breadcrumb-item">Add Contact</span> +{% endblock %} + +{% block pagetitle %}{{block.super}} Add Contact{% endblock pagetitle %} + +{% load bootstrap %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Add Contact for Partner {{partner}}</h1> + </div> +</div> + +<div class="row"> + <div class="col-12"> + <form method="post"> + {% csrf_token %} + <div class="mb-5"> + {{ form|bootstrap }} + </div> + + <input class="btn btn-primary" type="submit" value="Submit"/> + </form> + </div> +</div> + +{% endblock content %} diff --git a/partners/templates/partners/partner_edit.html b/partners/templates/partners/partner_edit.html new file mode 100644 index 0000000000000000000000000000000000000000..434e5832b21f8c7581476c22b250a3197db434af --- /dev/null +++ b/partners/templates/partners/partner_edit.html @@ -0,0 +1,64 @@ +{% extends 'partners/_partners_page_base.html' %} + +{% block breadcrumb_items %} + {{block.super}} + <span class="breadcrumb-item">Edit Partner</span> +{% endblock %} + +{% block pagetitle %}{{block.super}} Edit Partner{% endblock pagetitle %} + +{% load bootstrap %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Edit Partner</h1> + </div> +</div> + +<div class="row"> + <div class="col-12"> + <form method="post"> + {% csrf_token %} + <div class="mb-5"> + {{ form|bootstrap }} + </div> + + <h2>Contacts</h2> + {{ contact_formset.management_form }} + {% for form in contact_formset %} + <div class="contact-form-group"> + <h3>{{form.instance}}</h3> + <p>{{ form.instance.user.email }}</p> + <div class="mb-3">{{ form|bootstrap }}</div> + </div> + {% endfor %} + + <input class="btn btn-primary" type="submit" value="Submit"/> + </form> + </div> +</div> + +{% endblock content %} + +{% block footer_script %} +<script> + function delete_hide_contact_groups(delete_input) { + input_el = $(delete_input); + if( input_el.prop('checked') ) { + input_el + .parents('.contact-form-group') + .addClass('delete-form-group'); + } else { + input_el + .parents('.contact-form-group') + .removeClass('delete-form-group'); + } + } + + $('.contact-form-group [name$="DELETE"]').on('change click', function() { + delete_hide_contact_groups(this); + }); +</script> +{% endblock %} diff --git a/partners/urls.py b/partners/urls.py index 3297d6d0d184e9c10bf1f87d1fc3af244a47aff6..3c3e1d14fc31304bd9cb181604353b4e7ea66f43 100644 --- a/partners/urls.py +++ b/partners/urls.py @@ -6,14 +6,25 @@ urlpatterns = [ url(r'^$', views.supporting_partners, name='partners'), url(r'^dashboard$', views.dashboard, name='dashboard'), url(r'^membership_request$', views.membership_request, name='membership_request'), - url(r'^prospect_partners/add$', views.add_prospective_partner, + + # Prospects + url(r'^prospects/add$', views.add_prospective_partner, name='add_prospective_partner'), - url(r'^prospect_partners/contacts/(?P<contact_id>[0-9]+)/email$', + url(r'^prospects/contacts/(?P<contact_id>[0-9]+)/email$', views.email_prospartner_contact, name='email_prospartner_contact'), - url(r'^prospect_partner/(?P<prospartner_id>[0-9]+)/contacts/add$', + url(r'^prospects/(?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$', + url(r'^prospects/(?P<prospartner_id>[0-9]+)/promote$', views.promote_prospartner, name='promote_prospartner'), - url(r'^prospect_partner/(?P<prospartner_id>[0-9]+)/events/add$', + url(r'^prospects/(?P<prospartner_id>[0-9]+)/events/add$', views.add_prospartner_event, name='add_prospartner_event'), + + # Institutions + url(r'institutions/(?P<institution_id>[0-9]+)/edit$', views.institution_edit, + name='institution_edit'), + + # Partners + 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 83b383e5f3c9f10cd19a541e433425a148c6a119..6d0265f2042387775ccae563f2ebf3af7bedf3c4 100644 --- a/partners/views.py +++ b/partners/views.py @@ -1,6 +1,6 @@ from django.contrib import messages from django.db import transaction -from django.forms import modelformset_factory, formset_factory +from django.forms import modelformset_factory from django.shortcuts import get_object_or_404, render, reverse, redirect from django.utils import timezone @@ -10,10 +10,12 @@ from .constants import PROSPECTIVE_PARTNER_REQUESTED,\ PROSPECTIVE_PARTNER_APPROACHED, PROSPECTIVE_PARTNER_ADDED,\ PROSPECTIVE_PARTNER_EVENT_REQUESTED, PROSPECTIVE_PARTNER_EVENT_EMAIL_SENT from .models import Partner, ProspectivePartner, ProspectiveContact,\ - ProspectivePartnerEvent, MembershipAgreement + ProspectivePartnerEvent, MembershipAgreement, Contact, Institution from .forms import ProspectivePartnerForm, ProspectiveContactForm,\ - EmailProspectivePartnerContactForm, PromoteToPartnerForm,\ - ProspectivePartnerEventForm, MembershipQueryForm, PromoteToContactForm, PromoteToContactFormset + EmailProspectivePartnerContactForm, PromoteToPartnerForm,\ + ProspectivePartnerEventForm, MembershipQueryForm, PromoteToContactForm,\ + PromoteToContactFormset, PartnerForm, ContactForm, ContactFormset,\ + NewContactForm, InstitutionForm from .utils import PartnerUtils @@ -100,6 +102,69 @@ def promote_prospartner(request, prospartner_id): return render(request, 'partners/promote_prospartner.html', context) +############### +# Partner views +############### +@permission_required('scipost.can_manage_SPB', return_403=True) +@transaction.atomic +def partner_edit(request, partner_id): + partner = get_object_or_404(Partner, id=partner_id) + + # Start/fill forms + form = PartnerForm(request.POST or None, instance=partner) + ContactModelFormset = modelformset_factory(Contact, ContactForm, can_delete=True, extra=0, + formset=ContactFormset) + contact_formset = ContactModelFormset(request.POST or None, partner=partner, + queryset=partner.contact_set.all()) + + # Validate forms for POST request + if form.is_valid() and contact_formset.is_valid(): + form.save() + contact_formset.save() + messages.success(request, 'Partner saved') + return redirect(reverse('partners:partner_edit', args=(partner.id,))) + context = { + 'form': form, + 'contact_formset': contact_formset + } + return render(request, 'partners/partner_edit.html', context) + + +@permission_required('scipost.can_manage_SPB', return_403=True) +def partner_add_contact(request, partner_id): + partner = get_object_or_404(Partner, id=partner_id) + form = NewContactForm(request.POST or None, partner=partner) + if form.is_valid(): + contact = form.save() + messages.success(request, '<h3>Created contact: %s</h3>Email has been sent.' + % str(contact)) + return redirect(reverse('partners:dashboard')) + context = { + 'partner': partner, + 'form': form + } + return render(request, 'partners/partner_add_contact.html', context) + + +################### +# Institution Views +################### +@permission_required('scipost.can_manage_SPB', return_403=True) +def institution_edit(request, institution_id): + institution = get_object_or_404(Institution, id=institution_id) + form = InstitutionForm(request.POST or None, instance=institution) + if form.is_valid(): + form.save() + return redirect(reverse('partners:dashboard')) + context = { + 'form': form + } + return render(request, 'partners/institution_edit.html', context) + + +########################### +# Prospective Partner Views +########################### @permission_required('scipost.can_manage_SPB', return_403=True) def add_prospective_partner(request): form = ProspectivePartnerForm(request.POST or None) @@ -132,11 +197,11 @@ def email_prospartner_contact(request, contact_id): if form.is_valid(): comments = 'Email sent to %s.' % str(contact) prospartnerevent = ProspectivePartnerEvent( - prospartner = contact.prospartner, - event = PROSPECTIVE_PARTNER_EVENT_EMAIL_SENT, - comments = comments, - noted_on = timezone.now(), - noted_by = request.user.contributor) + prospartner=contact.prospartner, + event=PROSPECTIVE_PARTNER_EVENT_EMAIL_SENT, + comments=comments, + noted_on=timezone.now(), + noted_by=request.user.contributor) prospartnerevent.save() if contact.prospartner.status in [PROSPECTIVE_PARTNER_REQUESTED, PROSPECTIVE_PARTNER_ADDED]: diff --git a/scipost/static/scipost/assets/css/_form.scss b/scipost/static/scipost/assets/css/_form.scss index 8c268068e91ec2143644abb1e9a91646fa8435f2..60f2289b07992d78f8c5cb1eed829d31add22677 100644 --- a/scipost/static/scipost/assets/css/_form.scss +++ b/scipost/static/scipost/assets/css/_form.scss @@ -47,3 +47,27 @@ input[type="file"] { border: 1px solid rgba(0, 0, 0, 0.15); border-radius: 0.15rem; } + +// Formset +// +.delete-form-group { + > * { + opacity: 0.5; + color: $brand-danger; + + &:last-child { + // The delete button should always be visible + opacity: 1.0; + color: inherit; + } + } + + .form-group { + display: none; + + &:last-child { + // The delete button should always be visible + display: block; + } + } +}