From 89713a4fb7e61c5a10084a957e67646b437e3fe7 Mon Sep 17 00:00:00 2001 From: Jorran de Wit <jorrandewit@outlook.com> Date: Tue, 27 Jun 2017 18:41:48 +0200 Subject: [PATCH] Lengthy commit, see comment Main points added: - File Attachments (Agreement only for now) - Logo to Institution - Extra ContactTypes - Subtitle for Contact - Contact can choose subtitle/kind during activation - RequestContact - Including view/form to submit new request for Partners - Including processing view/form for the Admins - More stuff.... --- SciPost_v1/settings/local_jorran.py | 1 + partners/admin.py | 15 ++- partners/constants.py | 9 ++ partners/forms.py | 110 +++++++++++++++++- partners/managers.py | 23 +++- .../migrations/0023_contact_description.py | 20 ++++ partners/migrations/0024_contactrequest.py | 31 +++++ .../migrations/0025_partnersattachment.py | 25 ++++ .../migrations/0026_auto_20170627_1809.py | 21 ++++ partners/models.py | 53 ++++++++- .../partners/_contact_info_table.html | 10 +- partners/templates/partners/_contact_li.html | 5 + .../partners/agreements_details.html | 30 +++-- partners/templates/partners/dashboard.html | 15 ++- partners/templates/partners/partner_edit.html | 1 + .../partners/partner_request_contact.html | 32 +++++ .../templates/partners/partners_detail.html | 48 +++++--- .../partners/process_contact_requests.html | 59 ++++++++++ partners/urls.py | 6 + partners/views.py | 95 ++++++++++++--- .../commands/add_groups_and_permissions.py | 13 ++- 21 files changed, 561 insertions(+), 61 deletions(-) create mode 100644 partners/migrations/0023_contact_description.py create mode 100644 partners/migrations/0024_contactrequest.py create mode 100644 partners/migrations/0025_partnersattachment.py create mode 100644 partners/migrations/0026_auto_20170627_1809.py create mode 100644 partners/templates/partners/_contact_li.html create mode 100644 partners/templates/partners/partner_request_contact.html create mode 100644 partners/templates/partners/process_contact_requests.html diff --git a/SciPost_v1/settings/local_jorran.py b/SciPost_v1/settings/local_jorran.py index 1d47922ef..9740c928c 100644 --- a/SciPost_v1/settings/local_jorran.py +++ b/SciPost_v1/settings/local_jorran.py @@ -11,6 +11,7 @@ MIDDLEWARE_CLASSES += ( 'debug_toolbar.middleware.DebugToolbarMiddleware', ) INTERNAL_IPS = ['127.0.0.1', '::1'] +DATABASES['default']['PORT'] = '5433' # Static and media STATIC_ROOT = '/Users/jorranwit/Develop/SciPost/scipost_v1/local_files/static/' diff --git a/partners/admin.py b/partners/admin.py index 533597a4c..ccb1ffbd1 100644 --- a/partners/admin.py +++ b/partners/admin.py @@ -2,7 +2,11 @@ from django.contrib import admin from .models import Contact, Partner, Consortium, Institution,\ ProspectivePartner, ProspectiveContact, ProspectivePartnerEvent,\ - MembershipAgreement + MembershipAgreement, ContactRequest, PartnersAttachment + + +class AttachmentInline(admin.TabularInline): + model = PartnersAttachment class ContactToPartnerInline(admin.TabularInline): @@ -42,9 +46,16 @@ class PartnerAdmin(admin.ModelAdmin): ) +class MembershipAgreementAdmin(admin.ModelAdmin): + inlines = ( + AttachmentInline, + ) + + admin.site.register(Partner, PartnerAdmin) admin.site.register(Consortium) admin.site.register(Contact) +admin.site.register(ContactRequest) admin.site.register(Institution) admin.site.register(ProspectivePartner, ProspectivePartnerAdmin) -admin.site.register(MembershipAgreement) +admin.site.register(MembershipAgreement, MembershipAgreementAdmin) diff --git a/partners/constants.py b/partners/constants.py index 3349c8aff..e3f6224cd 100644 --- a/partners/constants.py +++ b/partners/constants.py @@ -58,6 +58,15 @@ PARTNER_STATUS = ( ('Inactive', 'Inactive'), ) +REQUEST_INITIATED = 'init' +REQUEST_PROCESSED = 'proc' +REQUEST_DECLINED = 'decl' +REQUEST_STATUSES = ( + (REQUEST_INITIATED, 'Request submitted by Contact'), + (REQUEST_PROCESSED, 'Processed'), + (REQUEST_DECLINED, 'Declined'), +) + CONSORTIUM_STATUS = ( ('Prospective', 'Prospective'), diff --git a/partners/forms.py b/partners/forms.py index 96e898acb..b9d472fe0 100644 --- a/partners/forms.py +++ b/partners/forms.py @@ -10,9 +10,10 @@ from django_countries.widgets import CountrySelectWidget from django_countries.fields import LazyTypedChoiceField from .constants import PARTNER_KINDS, PROSPECTIVE_PARTNER_PROCESSED, CONTACT_TYPES,\ - PARTNER_STATUS_UPDATE + PARTNER_STATUS_UPDATE, REQUEST_PROCESSED, REQUEST_DECLINED from .models import Partner, ProspectivePartner, ProspectiveContact, ProspectivePartnerEvent,\ - Institution, Contact, PartnerEvent, MembershipAgreement + Institution, Contact, PartnerEvent, MembershipAgreement, ContactRequest,\ + PartnersAttachment from scipost.models import TITLE_CHOICES @@ -22,7 +23,6 @@ class MembershipAgreementForm(forms.ModelForm): model = MembershipAgreement fields = ( 'partner', - # 'consortium', 'status', 'date_requested', 'start_date', @@ -56,10 +56,22 @@ class ActivationForm(forms.ModelForm): model = User fields = [] + description = forms.CharField(max_length=256, label="Title", required=False, + widget=forms.TextInput(attrs={ + 'placeholder': 'E.g.: Legal Agent at Stanford University'})) + kind = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple, label="Contact type", + choices=CONTACT_TYPES) password_new = forms.CharField(label='* Password', widget=forms.PasswordInput()) password_verif = forms.CharField(label='* Verify password', widget=forms.PasswordInput(), help_text='Your password must contain at least 8 characters') + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + try: + self.fields['kind'].initial = self.instance.partner_contact.kind + except Contact.DoesNotExist: + pass + def clean(self, *args, **kwargs): try: self.instance.partner_contact @@ -89,6 +101,11 @@ class ActivationForm(forms.ModelForm): self.instance.set_password(self.cleaned_data['password_new']) self.instance.save() + # Set fields for Contact + self.instance.partner_contact.description = self.cleaned_data['description'] + self.instance.partner_contact.kind = self.cleaned_data['kind'] + self.instance.partner_contact.save() + # Add permission groups to user group = Group.objects.get(name='Partners Accounts') self.instance.groups.add(group) @@ -131,6 +148,53 @@ class PartnerForm(forms.ModelForm): self.fields['main_contact'].queryset = self.instance.contact_set.all() +class RequestContactForm(forms.ModelForm): + class Meta: + model = ContactRequest + fields = ( + 'email', + 'title', + 'first_name', + 'last_name', + 'kind', + ) + + +class ProcessRequestContactForm(RequestContactForm): + decision = forms.ChoiceField(choices=((None, 'No decision'), ('accept', 'Accept'), ('decline', 'Decline')), + widget=forms.RadioSelect, label='Accept or Decline') + + class Meta: + model = ContactRequest + fields = RequestContactForm.Meta.fields + ('partner',) + + def process_request(self): + if self.cleaned_data['decision'] == 'accept': + self.instance.status = REQUEST_PROCESSED + self.instance.save() + contactForm = NewContactForm({ + 'title': self.cleaned_data['title'], + 'email': self.cleaned_data['email'], + 'first_name': self.cleaned_data['first_name'], + 'last_name': self.cleaned_data['last_name'], + 'kind': self.cleaned_data['kind'], + }, partner=self.cleaned_data['partner']) + contactForm.is_valid() + contactForm.save() + elif self.cleaned_data['decision'] == 'decline': + self.instance.status = REQUEST_DECLINED + self.instance.save() + + +class RequestContactFormSet(forms.BaseModelFormSet): + def process_requests(self): + """ + Process all requests if status is eithter accept or decline. + """ + for form in self.forms: + form.process_request() + + class ContactForm(forms.ModelForm): """ This Contact form is mainly used for editing Contact instances. @@ -444,3 +508,43 @@ class MembershipQueryForm(forms.Form): '{widget}<img class="country-select-flag" id="{flag_id}"' ' style="margin: 6px 4px 0" src="{country.flag}">'))) captcha = ReCaptchaField(attrs={'theme': 'clean'}, label='*Please verify to continue:') + + +class PartnersAttachmentForm(forms.ModelForm): + class Meta: + model = PartnersAttachment + fields = ( + 'name', + 'attachment', + ) + + def save(self, to_object, commit=True): + """ + This custom save method will automatically assign the file to the object + given when its a valid instance type. + """ + attachment = super().save(commit=False) + + # Formset's might save an empty Instance + if not attachment.name or not attachment.attachment: + return None + + if isinstance(to_object, MembershipAgreement): + attachment.agreement = to_object + else: + raise forms.ValidationError('You cannot save Attachment to this type of object.') + if commit: + attachment.save() + return attachment + + +class PartnersAttachmentFormSet(forms.BaseModelFormSet): + def save(self, to_object, commit=True): + """ + This custom save method will automatically assign the file to the object + given when its a valid instance type. + """ + returns = [] + for form in self.forms: + returns.append(form.save(to_object)) + return returns diff --git a/partners/managers.py b/partners/managers.py index 243dbf81f..abd47528e 100644 --- a/partners/managers.py +++ b/partners/managers.py @@ -1,6 +1,11 @@ from django.db import models -from .constants import MEMBERSHIP_SUBMITTED, PROSPECTIVE_PARTNER_PROCESSED +from .constants import MEMBERSHIP_SUBMITTED, PROSPECTIVE_PARTNER_PROCESSED, REQUEST_INITIATED + + +class ContactRequestManager(models.Manager): + def awaiting_processing(self): + return self.filter(status=REQUEST_INITIATED) class ProspectivePartnerManager(models.Manager): @@ -8,9 +13,25 @@ class ProspectivePartnerManager(models.Manager): return self.exclude(status=PROSPECTIVE_PARTNER_PROCESSED) +class PartnerManager(models.Manager): + def my_partners(self, current_user): + """ + Filter out my Partners if user is not a PartnerAdmin. + """ + if current_user.has_perm('scipost.can_view_partners'): + return self.all() + return self.filter(contact=current_user.partner_contact) + + class MembershipAgreementManager(models.Manager): def submitted(self): return self.filter(status=MEMBERSHIP_SUBMITTED) def open_to_partner(self): return self.exclude(status=MEMBERSHIP_SUBMITTED) + + +class PartnersAttachmentManager(models.Manager): + def my_attachments(self, current_user): + if current_user.has_perm('scipost.can_view_partners'): + return self.all() diff --git a/partners/migrations/0023_contact_description.py b/partners/migrations/0023_contact_description.py new file mode 100644 index 000000000..771cf824f --- /dev/null +++ b/partners/migrations/0023_contact_description.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-27 06:24 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0022_auto_20170626_2104'), + ] + + operations = [ + migrations.AddField( + model_name='contact', + name='description', + field=models.CharField(blank=True, max_length=256), + ), + ] diff --git a/partners/migrations/0024_contactrequest.py b/partners/migrations/0024_contactrequest.py new file mode 100644 index 000000000..5d7d385a5 --- /dev/null +++ b/partners/migrations/0024_contactrequest.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-27 07:29 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import scipost.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0023_contact_description'), + ] + + operations = [ + migrations.CreateModel( + name='ContactRequest', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('email', models.EmailField(max_length=254)), + ('kind', scipost.fields.ChoiceArrayField(base_field=models.CharField(choices=[('gen', 'General Contact'), ('tech', 'Technical Contact'), ('fin', 'Financial Contact'), ('leg', 'Legal Contact')], max_length=4), size=None)), + ('first_name', models.CharField(max_length=64)), + ('last_name', models.CharField(max_length=64)), + ('title', models.CharField(choices=[('PR', 'Prof.'), ('DR', 'Dr'), ('MR', 'Mr'), ('MRS', 'Mrs'), ('MS', 'Ms')], max_length=4)), + ('description', models.CharField(blank=True, max_length=256)), + ('status', models.CharField(choices=[('init', 'Request submitted by Contact'), ('proc', 'Processed'), ('decl', 'Declined')], default='init', max_length=4)), + ('partner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='partners.Partner')), + ], + ), + ] diff --git a/partners/migrations/0025_partnersattachment.py b/partners/migrations/0025_partnersattachment.py new file mode 100644 index 000000000..2a99b4e6a --- /dev/null +++ b/partners/migrations/0025_partnersattachment.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-27 16:08 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0024_contactrequest'), + ] + + operations = [ + migrations.CreateModel( + name='PartnersAttachment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('attachment', models.FileField(upload_to='UPLOADS/PARTNERS/ATTACHMENTS')), + ('name', models.CharField(max_length=128)), + ('agreement', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, to='partners.MembershipAgreement')), + ], + ), + ] diff --git a/partners/migrations/0026_auto_20170627_1809.py b/partners/migrations/0026_auto_20170627_1809.py new file mode 100644 index 000000000..c67c14a22 --- /dev/null +++ b/partners/migrations/0026_auto_20170627_1809.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-27 16:09 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0025_partnersattachment'), + ] + + operations = [ + migrations.AlterField( + model_name='partnersattachment', + name='agreement', + field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='partners.MembershipAgreement'), + ), + ] diff --git a/partners/models.py b/partners/models.py index 65652eca6..3a2451424 100644 --- a/partners/models.py +++ b/partners/models.py @@ -22,9 +22,10 @@ from .constants import PROSPECTIVE_PARTNER_EVENT_EMAIL_SENT,\ PROSPECTIVE_PARTNER_UNINTERESTED,\ PROSPECTIVE_PARTNER_EVENT_PROMOTED,\ PROSPECTIVE_PARTNER_PROCESSED, CONTACT_TYPES,\ - PARTNER_INITIATED + PARTNER_INITIATED, REQUEST_STATUSES, REQUEST_INITIATED -from .managers import MembershipAgreementManager, ProspectivePartnerManager +from .managers import MembershipAgreementManager, ProspectivePartnerManager, PartnerManager,\ + ContactRequestManager, PartnersAttachmentManager from scipost.constants import TITLE_CHOICES from scipost.fields import ChoiceArrayField @@ -117,6 +118,27 @@ class Institution(models.Model): return '%s (%s)' % (self.name, self.get_kind_display()) +class ContactRequest(models.Model): + """ + A ContactRequest request for a new Contact usually made by another Contact. + The requests are saved to this separate model to also be able to request new + Contact links if a Contact is already registered, but not linked to a specific Partner. + """ + email = models.EmailField() + kind = ChoiceArrayField(models.CharField(max_length=4, choices=CONTACT_TYPES)) + first_name = models.CharField(max_length=64) + last_name = models.CharField(max_length=64) + title = models.CharField(max_length=4, choices=TITLE_CHOICES) + description = models.CharField(max_length=256, blank=True) + partner = models.ForeignKey('partners.Partner', on_delete=models.CASCADE) + status = models.CharField(max_length=4, choices=REQUEST_STATUSES, default=REQUEST_INITIATED) + + objects = ContactRequestManager() + + def __str__(self): + return '%s %s %s' % (self.get_title_display(), self.first_name, self.last_name) + + class Contact(models.Model): """ A Contact is a simple form of User which is meant @@ -128,6 +150,7 @@ class Contact(models.Model): related_name='partner_contact') kind = ChoiceArrayField(models.CharField(max_length=4, choices=CONTACT_TYPES)) title = models.CharField(max_length=4, choices=TITLE_CHOICES) + description = models.CharField(max_length=256, blank=True) partners = models.ManyToManyField('partners.Partner', help_text=('All Partners (+related Institutions)' ' the Contact is related to.')) @@ -189,6 +212,8 @@ class Partner(models.Model): main_contact = models.ForeignKey('partners.Contact', on_delete=models.SET_NULL, blank=True, null=True, related_name='partner_main_contact') + objects = PartnerManager() + def __str__(self): if self.institution: return self.institution.acronym + ' (' + self.get_status_display() + ')' @@ -197,6 +222,13 @@ class Partner(models.Model): def get_absolute_url(self): return reverse('partners:partner_view', args=(self.id,)) + @property + def has_all_contacts(self): + """ + Determine if Partner has all available Contact Types available. + """ + raise NotImplemented + class PartnerEvent(models.Model): partner = models.ForeignKey('partners.Partner', on_delete=models.CASCADE, @@ -246,3 +278,20 @@ class MembershipAgreement(models.Model): def get_absolute_url(self): return reverse('partners:agreement_details', args=(self.id,)) + + +class PartnersAttachment(models.Model): + """ + An Attachment which can (in the future) be related to a Partner, Contact, MembershipAgreement, + etc. + """ + attachment = models.FileField(upload_to='UPLOADS/PARTNERS/ATTACHMENTS') + name = models.CharField(max_length=128) + agreement = models.ForeignKey('partners.MembershipAgreement', related_name='attachments', + blank=True) + + objects = PartnersAttachmentManager() + + def get_absolute_url(self): + if self.agreement: + return reverse('partners:agreement_attachments', args=(self.agreement.id, self.id)) diff --git a/partners/templates/partners/_contact_info_table.html b/partners/templates/partners/_contact_info_table.html index e9adc3645..327605c92 100644 --- a/partners/templates/partners/_contact_info_table.html +++ b/partners/templates/partners/_contact_info_table.html @@ -1,6 +1,8 @@ <table> - <tr><td>Title: </td><td> </td><td>{{ contact.get_title_display }}</td></tr> - <tr><td>First name: </td><td> </td><td>{{ contact.user.first_name }}</td></tr> - <tr><td>Last name: </td><td> </td><td>{{ contact.user.last_name }}</td></tr> - <tr><td>Email: </td><td> </td><td>{{ contact.user.email }}</td></tr> + <tbody> + <tr><td class="pr-4">Name: </td><td> </td><td>{{ contact.get_title_display }} {{ contact.user.first_name }} {{ contact.user.last_name }}</td></tr> + <tr><td class="pr-4">Description: </td><td> </td><td>{{ contact.description }}</td></tr> + <tr><td class="pr-4">Contact type: </td><td> </td><td>{{ contact.kind_display }}</td></tr> + <tr><td class="pr-4">Email: </td><td> </td><td>{{ contact.user.email }}</td></tr> + </tbody> </table> diff --git a/partners/templates/partners/_contact_li.html b/partners/templates/partners/_contact_li.html new file mode 100644 index 000000000..0d0885f6d --- /dev/null +++ b/partners/templates/partners/_contact_li.html @@ -0,0 +1,5 @@ +<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> diff --git a/partners/templates/partners/agreements_details.html b/partners/templates/partners/agreements_details.html index b7971b72d..e522ea8ab 100644 --- a/partners/templates/partners/agreements_details.html +++ b/partners/templates/partners/agreements_details.html @@ -21,17 +21,31 @@ <div class="col-md-6"> <h2>Membership Agreement</h2> {% include 'partners/_agreement_table.html' with agreement=agreement %} + + <h3>Attachments</h3> + <ul> + {% for file in agreement.attachments.all %} + <li><a href="{{file.get_absolute_url}}" target="_blank">{{file.name}}</a></li> + {% empty %} + <li>No Attachments found.</li> + {% endfor %} + </ul> </div> - <div class="col-12"> - <h2>Update Agreement</h2> - <form method="post"> - {% csrf_token %} - {{ form|bootstrap }} + {% if perms.scipost.can_manage_SPB %} + <div class="col-12"> + <h2>Update Agreement</h2> + <form method="post" enctype="multipart/form-data"> + {% csrf_token %} + {{ form|bootstrap }} - <input class="btn btn-primary" type="submit" value="Update"/> - </form> - </div> + <h3>Attachments</h3> + {{ attachment_formset|bootstrap }} + + <input class="btn btn-primary" type="submit" value="Update"/> + </form> + </div> + {% endif %} </div> {% endblock content %} diff --git a/partners/templates/partners/dashboard.html b/partners/templates/partners/dashboard.html index 48809ccd7..1a79ef615 100644 --- a/partners/templates/partners/dashboard.html +++ b/partners/templates/partners/dashboard.html @@ -55,6 +55,14 @@ <div class="col-md-6"> <h3>Your personal details:</h3> {% include "partners/_contact_info_table.html" with contact=request.user.partner_contact %} + + {% if perms.scipost.can_manage_SPB %} + <h3 class="mt-4">Administrative actions</h3> + <ul> + <li><a href="{% url 'partners:process_contact_requests' %}">Open Contact requests</a> ({{contact_requests_count}})</li> + <li>Contacts awaiting validation ({{inactivate_contacts_count}})</a></li> + </ul> + {% endif %} </div> <div class="col-md-6"> <h3 class="mb-2">My Partners</h3> @@ -63,8 +71,11 @@ <li class="media mb-2"> <img class="d-flex mr-3" width="64" src="{% if partner.institution.logo %}{{partner.institution.logo.url}}{% endif %}" alt="Partner Logo"> <div class="media-body"> - <h3 class="mt-0 mb-1"><strong>{{partner.institution.name}}</strong></h3> - {{partner.institution.acronym}} ({{partner.institution.get_kind_display}}) + <h3 class="mt-0"><strong>{{partner.institution.name}}</strong></h3> + <p> + {{partner.institution.acronym}} ({{partner.institution.get_kind_display}})<br> + <a href="{{partner.get_absolute_url}}">View/edit</a> + </p> </div> </li> {% empty %} diff --git a/partners/templates/partners/partner_edit.html b/partners/templates/partners/partner_edit.html index 434e5832b..e074cb3ef 100644 --- a/partners/templates/partners/partner_edit.html +++ b/partners/templates/partners/partner_edit.html @@ -2,6 +2,7 @@ {% block breadcrumb_items %} {{block.super}} + <a href="{{form.instance.get_absolute_url}}" class="breadcrumb-item">Partner details</a> <span class="breadcrumb-item">Edit Partner</span> {% endblock %} diff --git a/partners/templates/partners/partner_request_contact.html b/partners/templates/partners/partner_request_contact.html new file mode 100644 index 000000000..1ef1498a5 --- /dev/null +++ b/partners/templates/partners/partner_request_contact.html @@ -0,0 +1,32 @@ +{% extends 'partners/_partners_page_base.html' %} + +{% block breadcrumb_items %} + {{block.super}} + <a href="{{partner.get_absolute_url}}" class="breadcrumb-item">Partner details</a> + <span class="breadcrumb-item">Request new Contact</span> +{% endblock %} + +{% block pagetitle %}{{block.super}} Request new Contact{% endblock pagetitle %} + +{% load bootstrap %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Request new Contact for Partner {{partner}}</h1> + </div> +</div> + +<div class="row"> + <div class="col-12"> + <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/templates/partners/partners_detail.html b/partners/templates/partners/partners_detail.html index 797f9359a..da9797ea4 100644 --- a/partners/templates/partners/partners_detail.html +++ b/partners/templates/partners/partners_detail.html @@ -19,7 +19,9 @@ <div class="row"> <div class="col-md-6"> - <a href="{% url 'partners:partner_edit' partner.id %}">Edit partner</a> + {% if perms.scipost.can_manage_SPB %} + <a href="{% url 'partners:partner_edit' partner.id %}">Edit partner</a> + {% endif %} <address> <h3>{{ partner.institution.name }}</h3> <strong>{{ partner.institution.acronym }} ({{ partner.institution.get_kind_display }})</strong><br> @@ -40,32 +42,40 @@ <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> + {% include 'partners/_contact_li.html' with contact=contact %} {% endfor %} </ul> - </div> - <div class="col-md-6"> - <h3>Partner Events</h3> + + <h3>Requested Contacts</h3> <ul> - {% for event in partner.events.all %} - {% include 'partners/_prospartner_event_li.html' with event=event %} + {% for contact_request in partner.contactrequest_set.awaiting_processing %} + <li><strong>{{contact_request}}</strong> ({{contact_request.email}})</li> {% empty %} - <li>No events were found.</li> + <li>All requests are processed</li> {% endfor %} </ul> + <a href="{% url 'partners:partner_request_contact' partner.id %}">Request new Contact</a> + </div> + <div class="col-md-6"> + {% if perms.scipost.can_view_partners %} + <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 }} + <hr> + <h3>Add new Event</h3> + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} - <input class="btn btn-primary" type="submit" value="Submit"/> - </form> + <input class="btn btn-primary" type="submit" value="Submit"/> + </form> + {% endif %} </div> </div> diff --git a/partners/templates/partners/process_contact_requests.html b/partners/templates/partners/process_contact_requests.html new file mode 100644 index 000000000..489b9a7e5 --- /dev/null +++ b/partners/templates/partners/process_contact_requests.html @@ -0,0 +1,59 @@ +{% extends 'partners/_partners_page_base.html' %} + +{% block breadcrumb_items %} + {{block.super}} + <span class="breadcrumb-item">Process Contact Requests</span> +{% endblock %} + +{% block pagetitle %}{{block.super}} Process Contact Requests{% endblock pagetitle %} + +{% load bootstrap %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Process Contact Requests</h1> + </div> +</div> + +<div class="row"> + <div class="col-12"> + <form method="post"> + {% csrf_token %} + {{ formset.management_form }} + {% for form in 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 5db245e99..19075e492 100644 --- a/partners/urls.py +++ b/partners/urls.py @@ -6,6 +6,8 @@ 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'^process_contact_requests$', views.process_contact_requests, name='process_contact_requests'), + # Prospects url(r'^prospects/add$', views.add_prospective_partner, @@ -23,6 +25,8 @@ urlpatterns = [ url(r'agreements/new$', views.add_agreement, name='add_agreement'), url(r'agreements/(?P<agreement_id>[0-9]+)$', views.agreement_details, name='agreement_details'), + url(r'agreements/(?P<agreement_id>[0-9]+)/attachments/(?P<attachment_id>[0-9]+)$', + views.agreement_attachments, name='agreement_attachments'), # Institutions url(r'institutions/(?P<institution_id>[0-9]+)/edit$', views.institution_edit, @@ -36,4 +40,6 @@ urlpatterns = [ 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'), + url(r'(?P<partner_id>[0-9]+)/contacts/request$', views.partner_request_contact, + name='partner_request_contact'), ] diff --git a/partners/views.py b/partners/views.py index 2580012c3..ed28b1c2e 100644 --- a/partners/views.py +++ b/partners/views.py @@ -2,6 +2,7 @@ from django.contrib import messages from django.contrib.auth.decorators import login_required from django.db import transaction from django.forms import modelformset_factory +from django.http import HttpResponse from django.shortcuts import get_object_or_404, render, reverse, redirect from django.utils import timezone @@ -10,14 +11,16 @@ from guardian.decorators import permission_required 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, Contact, Institution +from .models import Partner, ProspectivePartner, ProspectiveContact, ContactRequest,\ + ProspectivePartnerEvent, MembershipAgreement, Contact, Institution,\ + PartnersAttachment from .forms import ProspectivePartnerForm, ProspectiveContactForm,\ EmailProspectivePartnerContactForm, PromoteToPartnerForm,\ ProspectivePartnerEventForm, MembershipQueryForm, PromoteToContactForm,\ PromoteToContactFormset, PartnerForm, ContactForm, ContactFormset,\ NewContactForm, InstitutionForm, ActivationForm, PartnerEventForm,\ - MembershipAgreementForm + MembershipAgreementForm, RequestContactForm, RequestContactFormSet,\ + ProcessRequestContactForm, PartnersAttachmentFormSet, PartnersAttachmentForm from .utils import PartnerUtils @@ -44,6 +47,8 @@ def dashboard(request): 'personal_agreements': personal_agreements } if request.user.has_perm('scipost.can_manage_SPB'): + context['contact_requests_count'] = ContactRequest.objects.awaiting_processing().count() + context['inactivate_contacts_count'] = Contact.objects.filter(user__is_active=False).count() context['partners'] = Partner.objects.all() context['prospective_partners'] = (ProspectivePartner.objects.not_yet_partner() .order_by('country', 'institution_name')) @@ -112,9 +117,9 @@ def promote_prospartner(request, prospartner_id): ############### # Partner views ############### -@permission_required('scipost.can_view_partners', return_403=True) +@permission_required('scipost.can_view_own_partner_details', return_403=True) def partner_view(request, partner_id): - partner = get_object_or_404(Partner, id=partner_id) + partner = get_object_or_404(Partner.objects.my_partners(request.user), id=partner_id) form = PartnerEventForm(request.POST or None) if form.is_valid(): event = form.save(commit=False) @@ -171,6 +176,43 @@ def partner_add_contact(request, partner_id): return render(request, 'partners/partner_add_contact.html', context) +@permission_required('scipost.can_view_own_partner_details', return_403=True) +def partner_request_contact(request, partner_id): + partner = get_object_or_404(Partner.objects.my_partners(request.user), id=partner_id) + form = RequestContactForm(request.POST or None) + if form.is_valid(): + contact_request = form.save(commit=False) + contact_request.partner = partner + contact_request.save() + messages.success(request, ('<h3>Request sent</h3>' + 'We will process your request as soon as possible.')) + return redirect(partner.get_absolute_url()) + context = { + 'partner': partner, + 'form': form + } + return render(request, 'partners/partner_request_contact.html', context) + + +@permission_required('scipost.can_manage_SPB', return_403=True) +def process_contact_requests(request): + form = RequestContactForm(request.POST or None) + + RequestContactModelFormSet = modelformset_factory(ContactRequest, ProcessRequestContactForm, + formset=RequestContactFormSet, extra=0) + formset = RequestContactModelFormSet(request.POST or None, + queryset=ContactRequest.objects.awaiting_processing()) + if formset.is_valid(): + formset.process_requests() + messages.success(request, 'Processing completed') + return redirect(reverse('partners:process_contact_requests')) + context = { + 'form': form, + 'formset': formset + } + return render(request, 'partners/process_contact_requests.html', context) + + ################### # Institution Views ################### @@ -178,8 +220,6 @@ def partner_add_contact(request, partner_id): def institution_edit(request, institution_id): institution = get_object_or_404(Institution, id=institution_id) form = InstitutionForm(request.POST or None, request.FILES or None, instance=institution) - r = request.FILES - # raise if form.is_valid(): form.save() messages.success(request, 'Institution has been updated.') @@ -284,21 +324,42 @@ def add_agreement(request): return render(request, 'partners/agreements_add.html', context) -@permission_required('scipost.can_manage_SPB', return_403=True) +@permission_required('scipost.can_view_own_partner_details', return_403=True) def agreement_details(request, agreement_id): agreement = get_object_or_404(MembershipAgreement, id=agreement_id) - form = MembershipAgreementForm(request.POST or None, instance=agreement) - if form.is_valid(): - agreement = form.save(request.user) - messages.success(request, 'Membership Agreement updated.') - return redirect(agreement.get_absolute_url()) - context = { - 'agreement': agreement, - 'form': form - } + context = {} + + if request.user.has_perm('scipost.can_manage_SPB'): + form = MembershipAgreementForm(request.POST or None, instance=agreement) + PartnersAttachmentFormSet + + PartnersAttachmentFormset = modelformset_factory(PartnersAttachment, + PartnersAttachmentForm, + formset=PartnersAttachmentFormSet) + attachment_formset = PartnersAttachmentFormset(request.POST or None, request.FILES or None, + queryset=agreement.attachments.all()) + + context['form'] = form + context['attachment_formset'] = attachment_formset + if form.is_valid() and attachment_formset.is_valid(): + agreement = form.save(request.user) + attachment_formset.save(agreement) + messages.success(request, 'Membership Agreement updated.') + return redirect(agreement.get_absolute_url()) + + context['agreement'] = agreement return render(request, 'partners/agreements_details.html', context) +@permission_required('scipost.can_view_own_partner_details', return_403=True) +def agreement_attachments(request, agreement_id, attachment_id): + attachment = get_object_or_404(PartnersAttachment.objects.my_attachments(request.user), + agreement__id=agreement_id, id=attachment_id) + response = HttpResponse(attachment.attachment.read(), content_type='application/pdf') + response['Content-Disposition'] = ('filename=%s' % attachment.name) + return response + + ######### # Account ######### diff --git a/scipost/management/commands/add_groups_and_permissions.py b/scipost/management/commands/add_groups_and_permissions.py index 716e77542..4d9c8a02f 100644 --- a/scipost/management/commands/add_groups_and_permissions.py +++ b/scipost/management/commands/add_groups_and_permissions.py @@ -55,7 +55,11 @@ class Command(BaseCommand): content_type=content_type) can_view_partners, created = Permission.objects.get_or_create( codename='can_view_partners', - name='Can view Partner details', + name='Can view Partner details of all Partners', + content_type=content_type) + can_view_own_partner_details, created = Permission.objects.get_or_create( + codename='can_view_own_partner_details', + name='Can view (its own) partner details', content_type=content_type) # Registration and invitations @@ -272,18 +276,21 @@ class Command(BaseCommand): PartnersAdmin.permissions.set([ can_read_personal_page, + can_view_own_partner_details, can_manage_SPB, can_promote_prospect_to_partner, can_email_prospartner_contact, - can_view_partners + can_view_partners, ]) PartnersOfficers.permissions.set([ can_read_personal_page, + can_view_own_partner_details, can_manage_SPB, can_view_partners, ]) PartnerAccounts.permissions.set([ - can_read_personal_page + can_read_personal_page, + can_view_own_partner_details, ]) if verbose: -- GitLab