diff --git a/SciPost_v1/settings/base.py b/SciPost_v1/settings/base.py index 54e67dda0b80013f74b3d35bfc4d2e48b1ae2bb5..eba6c214ee452d2e69be9049a5176194ac11649d 100644 --- a/SciPost_v1/settings/base.py +++ b/SciPost_v1/settings/base.py @@ -94,6 +94,7 @@ INSTALLED_APPS = ( 'theses', 'virtualmeetings', 'production', + 'partners', 'webpack_loader', ) diff --git a/SciPost_v1/urls.py b/SciPost_v1/urls.py index 36ab1c7ced277f745ad08e22922ebd4d594c6c21..7b29434e2aae982aa946d1363cfefcf35d4f4d79 100644 --- a/SciPost_v1/urls.py +++ b/SciPost_v1/urls.py @@ -43,6 +43,8 @@ urlpatterns = [ url(r'^meetings/', include('virtualmeetings.urls', namespace="virtualmeetings")), url(r'^news/', include('news.urls', namespace="news")), url(r'^production/', include('production.urls', namespace="production")), + url(r'^partners/', include('partners.urls', namespace="partners")), + url(r'^supporting_partners/', include('partners.urls', namespace="partners")), # Keep temporarily for historical reasons ] if settings.DEBUG: diff --git a/partners/__init__.py b/partners/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/partners/admin.py b/partners/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..6295519faa26f5bee354818f25a827375b6b3a94 --- /dev/null +++ b/partners/admin.py @@ -0,0 +1,23 @@ +from django.contrib import admin + +from .models import ContactPerson, Partner, Consortium,\ + ProspectivePartner, MembershipAgreement + + +admin.site.register(ContactPerson) + + +class PartnerAdmin(admin.ModelAdmin): + search_fields = ['institution', 'institution_acronym', + 'institution_address', 'contact_person'] + +admin.site.register(Partner, PartnerAdmin) + + +admin.site.register(Consortium) + + +admin.site.register(ProspectivePartner) + + +admin.site.register(MembershipAgreement) diff --git a/partners/apps.py b/partners/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..22e6fe3bc79c57abf3a8a51ccf4f6dc9ca1ff251 --- /dev/null +++ b/partners/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class PartnersConfig(AppConfig): + name = 'partners' diff --git a/partners/constants.py b/partners/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..ec05c2315596b05013b825e5c27531e75aaca543 --- /dev/null +++ b/partners/constants.py @@ -0,0 +1,43 @@ +import datetime + + +PARTNER_TYPES = ( + ('Int. Fund. Agency', 'International Funding Agency'), + ('Nat. Fund. Agency', 'National Funding Agency'), + ('Nat. Library', 'National Library'), + ('Univ. Library', 'University Library'), + ('Res. Library', 'Research Library'), + ('Foundation', 'Foundation'), + ('Individual', 'Individual'), +) + +PARTNER_STATUS = ( + ('Prospective', 'Prospective'), + ('Negotiating', 'Negotiating'), + ('Active', 'Active'), + ('Inactive', 'Inactive'), +) + + +CONSORTIUM_STATUS = ( + ('Prospective', 'Prospective'), + ('Active', 'Active'), + ('Inactive', 'Inactive'), +) + + +MEMBERSHIP_AGREEMENT_STATUS = ( + ('Submitted', 'Request submitted by Partner'), + ('Pending', 'Sent to Partner, response pending'), + ('Signed', 'Signed by Partner'), + ('Honoured', 'Honoured: payment of Partner received'), + ('Completed', 'Completed: agreement has been fulfilled'), +) + +MEMBERSHIP_DURATION = ( + (datetime.timedelta(days=365), '1 year'), + (datetime.timedelta(days=730), '2 years'), + (datetime.timedelta(days=1095), '3 years'), + (datetime.timedelta(days=1460), '4 years'), + (datetime.timedelta(days=1825), '5 years'), +) diff --git a/partners/forms.py b/partners/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..f53c3844c01dddb68c23f2eeef32489bcdb67001 --- /dev/null +++ b/partners/forms.py @@ -0,0 +1,46 @@ +from django import forms + +from captcha.fields import ReCaptchaField +from django_countries import countries +from django_countries.widgets import CountrySelectWidget +from django_countries.fields import LazyTypedChoiceField + +from .constants import PARTNER_TYPES +from .models import ContactPerson, Partner, ProspectivePartner, MembershipAgreement + +from scipost.models import TITLE_CHOICES + + +class PartnerForm(forms.ModelForm): + class Meta: + model = Partner + fields = '__all__' + + def __init__(self, *args, **kwargs): + super(PartnerForm, self).__init__(*args, **kwargs) + self.fields['institution_address'].widget = forms.Textarea({'rows': 8, }) + +class ProspectivePartnerForm(forms.ModelForm): + class Meta: + model = ProspectivePartner + exclude = ['date_received', 'date_processed', 'processed'] + + +class MembershipQueryForm(forms.Form): + """ + This form is to be used by an agent of the prospective Partner, + in order to request more information about potentially joining the SPB. + """ + title = forms.ChoiceField(choices=TITLE_CHOICES, label='* Your title') + first_name = forms.CharField(label='* Your first name', max_length=100) + last_name = forms.CharField(label='* Your last name', max_length=100) + email = forms.EmailField(label='* Your email address') + role = forms.CharField(label='* Your role in your organization') + partner_type = forms.ChoiceField(choices=PARTNER_TYPES, label='* Partner type') + institution_name = forms.CharField(label='* Name of your institution') + country = LazyTypedChoiceField( + choices=countries, label='* Country', initial='NL', + widget=CountrySelectWidget(layout=( + '{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:') diff --git a/partners/migrations/0001_initial.py b/partners/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..9f7fdd7dcfe5882c5057e8d2d8991c996d956abc --- /dev/null +++ b/partners/migrations/0001_initial.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-05-19 08:59 +from __future__ import unicode_literals + +import datetime +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django_countries.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Consortium', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128)), + ('status', models.CharField(choices=[('Prospective', 'Prospective'), ('Active', 'Active'), ('Inactive', 'Inactive')], max_length=16)), + ], + ), + migrations.CreateModel( + name='ContactPerson', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(choices=[('PR', 'Prof.'), ('DR', 'Dr'), ('MR', 'Mr'), ('MRS', 'Mrs')], max_length=4)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='MembershipAgreement', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('Submitted', 'Request submitted by Partner'), ('Pending', 'Sent to Partner, response pending'), ('Signed', 'Signed by Partner'), ('Honoured', 'Honoured: payment of Partner received'), ('Completed', 'Completed: agreement has been fulfilled')], max_length=16)), + ('date_requested', models.DateField()), + ('start_date', models.DateField()), + ('duration', models.DurationField(choices=[(datetime.timedelta(365), '1 year'), (datetime.timedelta(730), '2 years'), (datetime.timedelta(1095), '3 years'), (datetime.timedelta(1460), '4 years'), (datetime.timedelta(1825), '5 years')])), + ('offered_yearly_contribution', models.SmallIntegerField(default=0)), + ('consortium', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='partners.Consortium')), + ], + ), + migrations.CreateModel( + name='Partner', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('partner_type', models.CharField(choices=[('Int. Fund. Agency', 'International Funding Agency'), ('Nat. Fund. Agency', 'National Funding Agency'), ('Nat. Library', 'National Library'), ('Univ. Library', 'University Library'), ('Res. Library', 'Research Library'), ('Foundation', 'Foundation'), ('Individual', 'Individual')], max_length=32)), + ('status', models.CharField(choices=[('Prospective', 'Prospective'), ('Negotiating', 'Negotiating'), ('Active', 'Active'), ('Inactive', 'Inactive')], max_length=16)), + ('institution_name', models.CharField(max_length=256)), + ('institution_acronym', models.CharField(max_length=10)), + ('institution_address', models.CharField(blank=True, max_length=1000, null=True)), + ('country', django_countries.fields.CountryField(max_length=2)), + ('financial_contact', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='partner_financial_contact', to='partners.ContactPerson')), + ('main_contact', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='partner_main_contact', to='partners.ContactPerson')), + ('technical_contact', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='partner_technical_contact', to='partners.ContactPerson')), + ], + ), + migrations.AddField( + model_name='membershipagreement', + name='partner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='partners.Partner'), + ), + migrations.AddField( + model_name='consortium', + name='partners', + field=models.ManyToManyField(blank=True, to='partners.Partner'), + ), + ] diff --git a/partners/migrations/0002_auto_20170519_1335.py b/partners/migrations/0002_auto_20170519_1335.py new file mode 100644 index 0000000000000000000000000000000000000000..8bffe6c9bbe4a80f18c7533550ad3b1ebce65321 --- /dev/null +++ b/partners/migrations/0002_auto_20170519_1335.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-05-19 11:35 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone +import django_countries.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='MembershipQuery', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(choices=[('PR', 'Prof.'), ('DR', 'Dr'), ('MR', 'Mr'), ('MRS', 'Mrs')], max_length=4)), + ('first_name', models.CharField(max_length=32)), + ('last_name', models.CharField(max_length=32)), + ('email', models.EmailField(max_length=254)), + ('partner_type', models.CharField(choices=[('Int. Fund. Agency', 'International Funding Agency'), ('Nat. Fund. Agency', 'National Funding Agency'), ('Nat. Library', 'National Library'), ('Univ. Library', 'University Library'), ('Res. Library', 'Research Library'), ('Foundation', 'Foundation'), ('Individual', 'Individual')], max_length=32)), + ('institution_name', models.CharField(max_length=256)), + ('country', django_countries.fields.CountryField(max_length=2)), + ('date_received', models.DateTimeField(default=django.utils.timezone.now)), + ('date_processed', models.DateTimeField()), + ('processed', models.BooleanField(default=False)), + ], + options={ + 'verbose_name_plural': 'membership queries', + }, + ), + migrations.AlterModelOptions( + name='consortium', + options={'verbose_name_plural': 'consortia'}, + ), + ] diff --git a/partners/migrations/0003_auto_20170519_1424.py b/partners/migrations/0003_auto_20170519_1424.py new file mode 100644 index 0000000000000000000000000000000000000000..c5d9c35d8fcd8f7dc056c421fe2b0b32ceae6339 --- /dev/null +++ b/partners/migrations/0003_auto_20170519_1424.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-05-19 12:24 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone +import django_countries.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0002_auto_20170519_1335'), + ] + + operations = [ + migrations.CreateModel( + name='ProspectivePartner', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(choices=[('PR', 'Prof.'), ('DR', 'Dr'), ('MR', 'Mr'), ('MRS', 'Mrs')], max_length=4)), + ('first_name', models.CharField(max_length=32)), + ('last_name', models.CharField(max_length=32)), + ('email', models.EmailField(max_length=254)), + ('role', models.CharField(max_length=128)), + ('partner_type', models.CharField(choices=[('Int. Fund. Agency', 'International Funding Agency'), ('Nat. Fund. Agency', 'National Funding Agency'), ('Nat. Library', 'National Library'), ('Univ. Library', 'University Library'), ('Res. Library', 'Research Library'), ('Foundation', 'Foundation'), ('Individual', 'Individual')], max_length=32)), + ('institution_name', models.CharField(max_length=256)), + ('country', django_countries.fields.CountryField(max_length=2)), + ('date_received', models.DateTimeField(default=django.utils.timezone.now)), + ('date_processed', models.DateTimeField()), + ('processed', models.BooleanField(default=False)), + ], + ), + migrations.DeleteModel( + name='MembershipQuery', + ), + ] diff --git a/partners/migrations/0004_auto_20170519_1425.py b/partners/migrations/0004_auto_20170519_1425.py new file mode 100644 index 0000000000000000000000000000000000000000..f44c307494246de26b93ad9a8dc869f5a9d3a8a2 --- /dev/null +++ b/partners/migrations/0004_auto_20170519_1425.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-05-19 12:25 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0003_auto_20170519_1424'), + ] + + operations = [ + migrations.AlterField( + model_name='prospectivepartner', + name='date_processed', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/partners/migrations/__init__.py b/partners/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/partners/models.py b/partners/models.py new file mode 100644 index 0000000000000000000000000000000000000000..123b99d4cd07a9975a0fc80964068f4264e85fe3 --- /dev/null +++ b/partners/models.py @@ -0,0 +1,107 @@ +from django.contrib.auth.models import User +from django.db import models +from django.utils import timezone + +from django_countries.fields import CountryField + +from .constants import PARTNER_TYPES, PARTNER_STATUS, CONSORTIUM_STATUS,\ + MEMBERSHIP_AGREEMENT_STATUS, MEMBERSHIP_DURATION + +from scipost.constants import TITLE_CHOICES +from scipost.models import Contributor + + +class ContactPerson(models.Model): + """ + A ContactPerson is a simple form of User which is meant + to be associated to Partner objects + (main contact, financial/technical contact etc). + ContactPersons and Contributors have different rights. + """ + user = models.OneToOneField(User, on_delete=models.CASCADE, unique=True) + title = models.CharField(max_length=4, choices=TITLE_CHOICES) + + def __str__(self): + return '%s %s, %s' % (self.get_title_display(), self.user.last_name, self.user.first_name) + + +class Partner(models.Model): + """ + Supporting Partners. + These are the official Partner objects created by SciPost Admin. + """ + partner_type = models.CharField(max_length=32, choices=PARTNER_TYPES) + status = models.CharField(max_length=16, choices=PARTNER_STATUS) + institution_name = models.CharField(max_length=256) + institution_acronym = models.CharField(max_length=10) + institution_address = models.CharField(max_length=1000, blank=True, null=True) + country = CountryField() + main_contact = models.ForeignKey(ContactPerson, on_delete=models.CASCADE, + blank=True, null=True, + related_name='partner_main_contact') + financial_contact = models.ForeignKey(ContactPerson, on_delete=models.CASCADE, + blank=True, null=True, + related_name='partner_financial_contact') + technical_contact = models.ForeignKey(ContactPerson, on_delete=models.CASCADE, + blank=True, null=True, + related_name='partner_technical_contact') + + def __str__(self): + return self.institution_acronym + ' (' + self.get_status_display() + ')' + + +class Consortium(models.Model): + """ + Collection of Partners. + """ + name = models.CharField(max_length=128) + partners = models.ManyToManyField(Partner, blank=True) + status = models.CharField(max_length=16, choices=CONSORTIUM_STATUS) + + class Meta: + verbose_name_plural = 'consortia' + + + +class ProspectivePartner(models.Model): + """ + Created from the membership_request page, after submitting a query form. + """ + title = models.CharField(max_length=4, choices=TITLE_CHOICES) + first_name = models.CharField(max_length=32) + last_name = models.CharField(max_length=32) + email = models.EmailField() + role = models.CharField(max_length=128) + partner_type = models.CharField(max_length=32, choices=PARTNER_TYPES) + institution_name = models.CharField(max_length=256) + country = CountryField() + date_received = models.DateTimeField(default=timezone.now) + date_processed = models.DateTimeField(blank=True, null=True) + processed = models.BooleanField(default=False) + + def __str__(self): + resp = "processed" + if not self.processed: + resp = "unprocessed" + return '%s (received %s), %s' % (self.institution_name, + self.date_received.strftime("%Y-%m-%d"), + resp) + + +class MembershipAgreement(models.Model): + """ + Agreement for membership of the Supporting Partners Board. + A new instance is created each time an Agreement is made or renewed. + """ + partner = models.ForeignKey(Partner, on_delete=models.CASCADE, blank=True, null=True) + consortium = models.ForeignKey(Consortium, on_delete=models.CASCADE, blank=True, null=True) + status = models.CharField(max_length=16, choices=MEMBERSHIP_AGREEMENT_STATUS) + date_requested = models.DateField() + start_date = models.DateField() + duration = models.DurationField(choices=MEMBERSHIP_DURATION) + offered_yearly_contribution = models.SmallIntegerField(default=0) + + def __str__(self): + return (str(self.partner) + + ' [' + self.get_duration_display() + + ' from ' + self.start_date.strftime('%Y-%m-%d') + ']') diff --git a/partners/templates/partners/_partner_card.html b/partners/templates/partners/_partner_card.html new file mode 100644 index 0000000000000000000000000000000000000000..31175dccfad720d674489d66f3165abe4803c9af --- /dev/null +++ b/partners/templates/partners/_partner_card.html @@ -0,0 +1,31 @@ +{% load bootstrap %} + +<div class="card-block"> + <div class="row"> + <div class="col-1"> + <p>{{ partner.country }}</p> + </div> + <div class="col-4"> + <h3>{{ partner.institution_name }}</h3> + <p>{{ partner.institution_acronym }}</p> + <p>({{ pp.get_partner_type_display }})</p> + </div> + <div class="col-4"> + {% if partner.main_contact %} + <p>Main contact: {{ partner.main_contact..get_title_display }} {{ partner.main_contact.user.first_name }} {{ partner.main_contact.user.last_name }}</p> + <p>{{ partner.main_contact.user.email }}</p> + {% endif %} + {% if partner.financial_contact %} + <p>Financial contact: {{ partner.financial_contact..get_title_display }} {{ partner.financial_contact.user.first_name }} {{ partner.financial_contact.user.last_name }}</p> + <p>{{ partner.financial_contact.user.email }}</p> + {% endif %} + {% if partner.technical_contact %} + <p>Technical contact: {{ partner.technical_contact..get_title_display }} {{ partner.technical_contact.user.first_name }} {{ partner.technical_contact.user.last_name }}</p> + <p>{{ partner.technical_contact.user.email }}</p> + {% endif %} + </div> + <div class="col-3"> + <p>Edit</p> + </div> + </div> +</div> diff --git a/partners/templates/partners/_prospective_partner_card.html b/partners/templates/partners/_prospective_partner_card.html new file mode 100644 index 0000000000000000000000000000000000000000..fe2624c247b05a878622743b0c213a80467220cc --- /dev/null +++ b/partners/templates/partners/_prospective_partner_card.html @@ -0,0 +1,22 @@ +{% load bootstrap %} + +<div class="card-block"> + <div class="row"> + <div class="col-1"> + <p>{{ pp.country }}</p> + </div> + <div class="col-4"> + <h3>{{ pp.institution_name }}</h3> + <p>({{ pp.get_partner_type_display }})</p> + <p>Received {{ pp.date_received }}</p> + </div> + <div class="col-4"> + <p>Contact: {{ pp.get_title_display }} {{ pp.first_name }} {{ pp.last_name }}</p> + <p>(role: {{ pp.role }})</p> + <p>{{ pp.email }}</p> + </div> + <div class="col-3"> + <p>Edit</p> + </div> + </div> +</div> diff --git a/partners/templates/partners/add_prospective_partner.html b/partners/templates/partners/add_prospective_partner.html new file mode 100644 index 0000000000000000000000000000000000000000..737780443f5c8da12d23699d2845e666fbf1ce14 --- /dev/null +++ b/partners/templates/partners/add_prospective_partner.html @@ -0,0 +1,29 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: Supporting Partners: add{% endblock pagetitle %} + +{% load bootstrap %} + +{% block content %} + +<section> + <div class="flex-container"> + <div class="flex-greybox"> + <h1>Add a Prospective Partner</h1> + </div> + </div> + <p>Please provide contact details of an appropriate representative, and details about the potential Partner.</p> + + <form action="{% url 'partners:add_prospective_partner' %}" method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input class="btn btn-primary" type="submit" value="Submit"/> + </form> + + {% if errormessage %} + <p class="text-danger">{{ errormessage }}</p> + {% endif %} + +</section> + +{% endblock content %} diff --git a/partners/templates/partners/manage_partners.html b/partners/templates/partners/manage_partners.html new file mode 100644 index 0000000000000000000000000000000000000000..95fb172639561b9c03f3d03c7a97711b0ccdf18a --- /dev/null +++ b/partners/templates/partners/manage_partners.html @@ -0,0 +1,55 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: Supporting Partners: manage{% endblock pagetitle %} + + +{% block content %} + +<div class="flex-container"> + <div class="flex-greybox"> + <h1>Partners Management Page</h1> + </div> +</div> + +<section> + <div class="flex-container"> + <div class="flex-greybox"> + <h2>Partners</h2> + </div> + </div> + <ul class="list-group list-group-flush"> + {% for partner in partners %} + <li class="list-group-item">{% include 'partners/_partner_card.html' with partner=partner %}</li> + {% endfor %} + </ul> +</section> + +<section> + <div class="flex-container"> + <div class="flex-greybox"> + <h2>Prospective Partners (not yet processed)</h2> + </div> + </div> + <h3><a href="{% url 'partners:add_prospective_partner' %}">Add a prospective partner</a></h3> + <br/> + <ul class="list-group list-group-flush"> + {% for partner in prospective_partners %} + <li class="list-group-item">{% include 'partners/_prospective_partner_card.html' with pp=partner %}</li> + {% endfor %} + </ul> +</section> + +<section> + <div class="flex-container"> + <div class="flex-greybox"> + <h2>Agreements</h2> + </div> + </div> + <ul> + {% for agreement in agreements %} + <li>{{ agreement }}</li> + {% endfor %} + </ul> +</section> + +{% endblock content %} diff --git a/partners/templates/partners/membership_request.html b/partners/templates/partners/membership_request.html new file mode 100644 index 0000000000000000000000000000000000000000..13e312907df28f848d1eeb15705c45a45ed33cb9 --- /dev/null +++ b/partners/templates/partners/membership_request.html @@ -0,0 +1,66 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: Supporting Partners Board: Membership request{% endblock pagetitle %} + +{% load staticfiles %} +{% load bootstrap %} +{% block bodysup %} + +<script> +$(document).ready(function(){ + $("#id_consortium_members").hide() + $("label[for='id_consortium_members']").hide() + + $('select#id_partner_type').on('change', function (){ + var selection = $(this).val(); + switch(selection){ + case "Consortium": + $("#id_consortium_members").show() + $("label[for='id_consortium_members']").show() + break; + default: + $("#id_consortium_members").hide() + $("label[for='id_consortium_members']").hide() + } +}); +}); + +</script> + + +<section> + <div class="flex-container"> + <div class="flex-greybox"> + <h1>Supporting Partners Board: Membership Request</h1> + </div> + </div> + + <div class="flex-container"> + <div class="flex-whitebox"> + <p>You can hereby request further details on the process to become one + of our Supporting Partners.</p> + <p>Filling in this form does not yet constitute a binding agreement.</p> + <p>It simply expresses your interest in considering joining our Supporting Partners Board.</p> + <p>After filling this form, SciPost Administration will contact you with more details on Partnership.</p> + <p><em>Note: you will automatically be considered as the contact person for this Partner.</em></p> + + {% if errormessage %} + <p style="color: red;">{{ errormessage }}</p> + {% endif %} + + <form action="{% url 'partners:membership_request' %}" method="post"> + {% csrf_token %} + <h3>Please provide us the following relevant details:</h3> + {{ query_form|bootstrap }} + <input class="btn btn-primary" type="submit" value="Submit"/> + </form> + + {% if errormessage %} + <p class="text-danger">{{ errormessage }}</p> + {% endif %} + </div> + </div> + +</section> + +{% endblock bodysup %} diff --git a/partners/templates/partners/supporting_partners.html b/partners/templates/partners/supporting_partners.html new file mode 100644 index 0000000000000000000000000000000000000000..b1434fb8c75e7465993224ad3118362e8ac9b45b --- /dev/null +++ b/partners/templates/partners/supporting_partners.html @@ -0,0 +1,139 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: Supporting Partners{% endblock pagetitle %} + +{% load staticfiles %} + +{% load scipost_extras %} + +{% block bodysup %} + +<section> + <div class="flex-container"> + <div class="flex-greybox"> + <h1>SciPost Supporting Partners</h1> + </div> + </div> + + {% if perms.scipost.can_manage_SPB %} + <a href="{% url 'partners:manage' %}">Manage Partners</a> + {% endif %} + + <div class="flex-container"> + <div class="flex-whitebox"> + + <h3>Openness at strictly minimal cost: the role of professional academics</h3> + <p>A fundamental principle underlining all of SciPost’s activities is openness. This means in particular that SciPost guarantees free online access to all publications in all its Journals (free for readers; all articles are published under the terms of a Creative Commons license (most commonly CC-BY)), and does not charge any article processing fees for publishing (free for authors). All editorial activities are performed by professional academics as part of their normal professional duties: contributors to SciPost are demonstrably dedicated to serving their community, and to realizing the dream of true openness in scientific publishing. SciPost thus achieves the highest possible standards of academic quality and open accessibility while maintaining costs at the lowest achievable level.</p> + + <h3>Financing and sustainability: the role of institutional backers</h3> + <p>Besides relying on the dedicated service of professional scientists for many of its day-to-day operations, SciPost must additionally rely on institutional backers for sustaining its professional-level publishing facilities and other activities. This backing is sought primarily from the organizations which are positively impacted by its activities, directly or indirectly: (inter)national funding agencies, universities, national/university/research libraries, academic consortia, governments and ministries, foundations, benefactors and further interested parties. This financial backing cannot take the form of article processing charges or journal subscriptions, due to SciPost’s operating principles: rather, Supporting Partners provide operating funds to SciPost, these funds being pooled in order to cover maintenance and operation of the web infrastructure at SciPost.org, administration of all site-related activities, production of publications in SciPost Journals and development of new publishing-related services for the international scientific community.</p> + + </div> + </div> + + + + <div class="flex-container"> + <div class="flex-greybox"> + <h1>Supporting Partners Board</h1> + </div> + </div> + + <div class="flex-container"> + <div class="flex-whitebox"> + + <p>We hereby cordially invite interested parties who are supportive of SciPost's mission to join the SciPost Supporting Partners Board by signing a <a href="{% static 'scipost/SPB/SciPost_Supporting_Partner_Agreement.pdf' %}">Partner Agreement</a>.</p> + + <p>Prospective partners can query for more information about Membership by filling the <a href="{% url 'partners:membership_request' %}">online query form</a>.</p> + + <br/> + <p>The <a href="{% static 'scipost/SPB/SciPost_Supporting_Partner_Agreement.pdf' %}">Partner Agreement</a> itself contains a detailed presentation of the Foundation, its activities and financial aspects. What follows is a summary of the most important points.</p> + + <br/> + <p>The set of all Supporting Partners forms the SciPost Supporting Partners Board (SPB). Acting as a representative body, the SPB’s purpose is to provide the main financial backing without which SciPost could not continue carrying out its mission, to counsel it in all its operations and to help determine the initiative’s development priorities.</p> + + <p>The SPB has a yearly virtual general meeting, organized and chaired by a representative of the SciPost Foundation. During this meeting, SPB-related topics are discussed, recommendations to the SciPost Foundation can be put forward and voted on by Partners, and general issues on any of SciPost’s activities can be discussed.</p> + + + <h3>Types of Partners</h3> + <p>Supporting Partners can for example be: + <ul> + <li>International/national funding agencies</li> + <li>National/university/research libraries</li> + <li>Consortia (of e.g. universities or libraries)</li> + <li>Government (through e.g. education/science ministries)</li> + <li>Foundations</li> + <li>Benefactors of any other type.</li> + </ul> + </p> + + <h3>Partnership benefits</h3> + <p>All funds originating from Supporting Partners are used to provide services to the academic community as a whole: SciPost operates in a completely open fashion, and the fulfillment of the Foundation’s mission benefits academics worldwide, without any distinction.</p> + <p>Partnership nonetheless provides Partners with a number of additional benefits as compared to non-Partner parties. SciPost agrees to provide its Partners with: + <ol> + <li>A seat on the SciPost Supporting Partners Board, including access to yearly meetings, with voting rights proportional to financial contribution (up to a maximum of 20% of all votes for any individual Partner).</li> + <li>Inclusion in the online listing of Supporting Partners on the SciPost website.</li> + <li>Exclusive ability to feed the Partner’s institutional theses database to the SciPost Theses database, for inclusion in the Theses part of the portal.</li> + <li>Access to the SciPost metadata API (providing among others means to generate yearly metadata summaries relating to SciPost publications for each Contributor employed by the Partner).</li> + <li>Exclusive access to further online tools planned by SciPost during their development stage. The SPB as a whole can provide feedback and request specific features.</li> + </ol> + </p> + + <h3>Financial contribution</h3> + <p>For the financial year 2017, the contributions are set to yearly amounts of: + <table> + <tr> + <td>(Inter)national funding agency:</td><td> </td> + <td>size-dependent tailored agreement</td> + </tr> + <tr> + <td>University/library:</td><td> </td> + <td>€1000 (base; more is greatly appreciated)</td> + </tr> + <tr> + <td>National consortium of universities/libraries:</td><td> </td> + <td>10% bulk discount on the above<br/>(e.g. €3600 for 4 universities)</td> + </tr> + <tr> + <td>Foundations/benefactors:</td><td> </td> + <td>(Partner-set amount of at least €500)</td> + </tr> + </table> + + <p>Note that if the consortium itself is not a legal entity, each individual member must sign a separate Agreement.</p> + <p>All amounts are exclusive of VAT, which will be payable by the Partner where applicable.</p> + + <p><strong>Sustainability - </strong> This norm allows sustainable functioning of SciPost under the expectation that individual institutions be associated to between two and three full publications per year on average (computed using authorship fractions). A Partner who is associated to more authorships and/or who generally recognizes the value of SciPost’s activities, is of course welcome to contribute more.</p> + <p><strong>Donations - </strong>Contributions of less than €500 per year are treated as incidental donations and do not lead to the obtention of Partner benefits.</p> + <p>Note that SciPost has been designated as a Public Benefit Organisation (PBO; in Dutch: Algemeen Nut Beogende Instelling, ANBI) by the Dutch Tax Administration. Natural and legal persons making donations to a PBO may deduct their gifts from their Dutch income tax or corporate income tax (see the PBO page of the Dutch Tax Administration).</p> + + <h3>Activation procedure</h3> + <p>In order to become a Supporting Partner, one must: + <ul> + <li>Fill in the online <a href="{% url 'partners:membership_request' %}">membership request form</a> (the form must be filled in by an authorized agent employed by or associated to the prospective Partner; personal contact details of this person will be treated confidentially).</li> + <li>Wait for the email response from the SciPost administration, containing a Partnership Agreement offer including detailed terms (start date, duration, financial contribution).</li> + <li>Email a scan of the signed copy of the Partnership Agreement to SciPost.</li> + <li>Proceed with the payment of the financial contribution, following invoicing from the SciPost Foundation.</li> + </ul> + </p> + + </div> + </div> +</section> + +{% if request.user|is_in_group:'Editorial Administrators' %} +<section> + <div class="flex-container"> + <div class="flex-greybox"> + <h1>Prospective Partners</h1> + </div> + </div> + <ul> + {% for agreement in prospective_agreements %} + <li>{{ agreement }}</li> + {% endfor %} + </ul> +</section> +{% endif %} + +{% endblock bodysup %} diff --git a/partners/tests.py b/partners/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/partners/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/partners/urls.py b/partners/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..3c3dd7ebb828f446978d61c6ef781ae3a84b273f --- /dev/null +++ b/partners/urls.py @@ -0,0 +1,14 @@ +from django.conf.urls import include, url + +from . import views + +urlpatterns = [ + + url(r'^$', views.supporting_partners, + name='partners'), + 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, + name='add_prospective_partner'), +] diff --git a/partners/views.py b/partners/views.py new file mode 100644 index 0000000000000000000000000000000000000000..dd338e2325feb3b488c6a96c8f94d608eccd5704 --- /dev/null +++ b/partners/views.py @@ -0,0 +1,64 @@ +from django.contrib.auth.decorators import login_required +from django.contrib import messages +from django.shortcuts import render, reverse, redirect +from django.utils import timezone + +from guardian.decorators import permission_required + +from .models import Partner, Consortium, ProspectivePartner, MembershipAgreement +from .forms import PartnerForm, ProspectivePartnerForm, MembershipQueryForm + +def supporting_partners(request): + prospective_agreements = MembershipAgreement.objects.filter( + status='Submitted').order_by('date_requested') + context = {'prospective_partners': prospective_agreements, } + return render(request, 'partners/supporting_partners.html', context) + + +def membership_request(request): + query_form = MembershipQueryForm(request.POST or None) + if query_form.is_valid(): + query = ProspectivePartner( + title=query_form.cleaned_data['title'], + first_name=query_form.cleaned_data['first_name'], + last_name=query_form.cleaned_data['last_name'], + email=query_form.cleaned_data['email'], + partner_type=query_form.cleaned_data['partner_type'], + institution_name=query_form.cleaned_data['institution_hame'], + country=query_form.cleaned_data['country'], + date_received=timezone.now(), + ) + query.save() + ack_message = ('Thank you for your SPB Membership query. ' + 'We will get back to you in the very near future ' + 'with further details.') + context = {'ack_message': ack_message, } + return render(request, 'scipost/acknowledgement.html', context) + context = {'query_form': query_form,} + return render(request, 'partners/membership_request.html', context) + + +@permission_required('scipost.can_manage_SPB', return_403=True) +def manage(request): + """ + Lists relevant info regarding management of Supporting Partners Board. + """ + partners = Partner.objects.all().order_by('country', 'institution_name') + prospective_partners = ProspectivePartner.objects.filter( + processed=False).order_by('date_received') + agreements = MembershipAgreement.objects.all().order_by('date_requested') + context = {'partners': partners, + 'prospective_partners': prospective_partners, + 'agreements': agreements, } + return render(request, 'partners/manage_partners.html', context) + + +@permission_required('scipost.can_manage_SPB', return_403=True) +def add_prospective_partner(request): + form = ProspectivePartnerForm(request.POST or None) + if form.is_valid(): + pros_partner = form.save() + messages.success(request, 'Prospective Partners successfully added') + return redirect(reverse('partners:manage')) + context = {'form': form,} + return render(request, 'partners/add_prospective_partner.html', context) diff --git a/scipost/constants.py b/scipost/constants.py index 8b58a19e11103a9ece14ffd464f32a37316b2b1e..aa19e9b476d151d4400e2a14037b60c24b4a17b9 100644 --- a/scipost/constants.py +++ b/scipost/constants.py @@ -1,4 +1,3 @@ -import datetime DISCIPLINE_PHYSICS = 'physics' diff --git a/scipost/management/commands/add_groups_and_permissions.py b/scipost/management/commands/add_groups_and_permissions.py index f353c60f855feef314a1e4037d91d97f15432629..5a40c6b13b941194b8c15f819a3fe884b16150b2 100644 --- a/scipost/management/commands/add_groups_and_permissions.py +++ b/scipost/management/commands/add_groups_and_permissions.py @@ -14,6 +14,7 @@ class Command(BaseCommand): # Create Groups SciPostAdmin, created = Group.objects.get_or_create(name='SciPost Administrators') + FinancialAdmin, created = Group.objects.get_or_create(name='Financial Administrators') AdvisoryBoard, created = Group.objects.get_or_create(name='Advisory Board') EditorialAdmin, created = Group.objects.get_or_create(name='Editorial Administrators') EditorialCollege, created = Group.objects.get_or_create(name='Editorial College') @@ -29,6 +30,12 @@ class Command(BaseCommand): # Create Permissions content_type = ContentType.objects.get_for_model(Contributor) + # Supporting Partners + can_manage_SPB, created = Permission.objects.get_or_create( + codename='can_manage_SPB', + name='Can manage Supporting Partners Board', + content_type=content_type) + # Registration and invitations can_vet_registration_requests, created = Permission.objects.get_or_create( codename='can_vet_registration_requests',