diff --git a/.bootstraprc b/.bootstraprc index 6c92d1c231dd79fcd038f0f96c1504f9756e0b75..34dcfd0ae1a6e3d04f0e5dcce547594b437e8e5d 100644 --- a/.bootstraprc +++ b/.bootstraprc @@ -23,6 +23,7 @@ "grid": true, "input-group": true, "list-group": true, + "media": true, "modal": true, "mixins": true, "nav": true, diff --git a/SciPost_v1/settings/local_jorran.py b/SciPost_v1/settings/local_jorran.py index 1d47922efff81ddbc6b2157ce74b9fee93f697a6..9740c928c59730c5e5e807600a23514eb6ea0d80 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/SciPost_v1/urls.py b/SciPost_v1/urls.py index 7b29434e2aae982aa946d1363cfefcf35d4f4d79..adf26e3aa41c52e07475da80c70f1e99700c13c7 100644 --- a/SciPost_v1/urls.py +++ b/SciPost_v1/urls.py @@ -1,20 +1,6 @@ -"""SciPost_v1 URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/1.8/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') -Including another URLconf - 1. Add an import: from blog import urls as blog_urls - 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) -""" from django.conf import settings from django.conf.urls import include, url +from django.conf.urls.static import static from django.contrib import admin from ajax_select import urls as ajax_select_urls @@ -49,6 +35,5 @@ urlpatterns = [ if settings.DEBUG: import debug_toolbar - urlpatterns += [ - url(r'^__debug__/', include(debug_toolbar.urls)), - ] + urlpatterns += [url(r'^__debug__/', include(debug_toolbar.urls))] + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/common/utils.py b/common/utils.py index 4a778d1002428a3cd1c2da6823a7c137df54f39c..de3b345c51ad4f671999254ed9f1c8ed595b2fa9 100644 --- a/common/utils.py +++ b/common/utils.py @@ -13,7 +13,7 @@ class BaseMailUtil(object): for var_name in _dict: setattr(cls, var_name, _dict[var_name]) - def _send_mail(cls, template_name, recipients, subject, extra_bcc=None): + def _send_mail(cls, template_name, recipients, subject, extra_bcc=None, extra_context={}): """ Call this method from a classmethod to send emails. The template will have context variables defined appended from the `load` method. @@ -21,13 +21,14 @@ class BaseMailUtil(object): Arguments: template_name -- The .html template to use in the mail. The name be used to get the following two templates: - `email/<template_name>.html` (non-HTML) - `email/<template_name>_html.html` + `email/<template_name>.txt` (non-HTML) + `email/<template_name>.html` recipients -- List of mailaddresses to send to mail to. subject -- The subject of the mail. """ - template = loader.get_template('email/%s.html' % template_name) - html_template = loader.get_template('email/%s_html.html' % template_name) + template = loader.get_template('email/%s.txt' % template_name) + html_template = loader.get_template('email/%s.html' % template_name) + cls._context.update(extra_context) message = template.render(Context(cls._context)) html_message = html_template.render(Context(cls._context)) bcc_list = [cls.mail_sender] diff --git a/partners/admin.py b/partners/admin.py index 50290d597a3c74dc13f05feb92afa0e769e2724f..ccb1ffbd115c8115883e9213827520e3ddb2d89c 100644 --- a/partners/admin.py +++ b/partners/admin.py @@ -1,15 +1,37 @@ from django.contrib import admin -from .models import Contact, Partner, PartnerEvent, Consortium,\ +from .models import Contact, Partner, Consortium, Institution,\ ProspectivePartner, ProspectiveContact, ProspectivePartnerEvent,\ - MembershipAgreement + MembershipAgreement, ContactRequest, PartnersAttachment + + +class AttachmentInline(admin.TabularInline): + model = PartnersAttachment + + +class ContactToPartnerInline(admin.TabularInline): + model = Contact.partners.through + extra = 0 + verbose_name = 'Contact' + verbose_name_plural = 'Contacts' + + +class ContactToUserInline(admin.StackedInline): + model = Contact + extra = 0 + min_num = 0 + verbose_name = 'Contact (Partners)' + class ProspectiveContactInline(admin.TabularInline): model = ProspectiveContact extra = 0 + class ProspectivePartnerEventInline(admin.TabularInline): model = ProspectivePartnerEvent + extra = 0 + class ProspectivePartnerAdmin(admin.ModelAdmin): inlines = (ProspectiveContactInline, ProspectivePartnerEventInline,) @@ -19,10 +41,21 @@ class ProspectivePartnerAdmin(admin.ModelAdmin): class PartnerAdmin(admin.ModelAdmin): search_fields = ('institution', ) + inlines = ( + ContactToPartnerInline, + ) + + +class MembershipAgreementAdmin(admin.ModelAdmin): + inlines = ( + AttachmentInline, + ) admin.site.register(Partner, PartnerAdmin) -admin.site.register(Contact) 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 148ac6556ebd5852c5aeaaac739086933b4ad51f..e3f6224cd7e124253e16f7e7787731d6c72f0b24 100644 --- a/partners/constants.py +++ b/partners/constants.py @@ -12,6 +12,7 @@ PARTNER_KINDS = ( (PARTNER_KIND_UNI_LIBRARY, 'University (and its Library)'), ('Res. Library', 'Research Library'), ('Prof. Soc.', 'Professional Society'), + ('Nat. Consor.', 'National Consortium'), ('Foundation', 'Foundation'), ('Individual', 'Individual'), ) @@ -47,8 +48,9 @@ PROSPECTIVE_PARTNER_EVENTS = ( ) +PARTNER_INITIATED = 'Initiated' PARTNER_STATUS = ( - ('Initiated', 'Initiated'), + (PARTNER_INITIATED, 'Initiated'), ('Contacted', 'Contacted'), ('Negotiating', 'Negotiating'), ('Uninterested', 'Uninterested'), @@ -56,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'), @@ -63,13 +74,22 @@ CONSORTIUM_STATUS = ( ('Inactive', 'Inactive'), ) - +PARTNER_STATUS_UPDATE = 'status_update' PARTNER_EVENTS = ( ('initial', 'Contacted (initial)'), - ('status_update', 'Status updated'), + (PARTNER_STATUS_UPDATE, 'Status updated'), ('comment', 'Comment added'), ) + +CONTACT_TYPES = ( + ('gen', 'General Contact'), + ('tech', 'Technical Contact'), + ('fin', 'Financial Contact'), + ('leg', 'Legal Contact') +) + + MEMBERSHIP_SUBMITTED = 'Submitted' MEMBERSHIP_AGREEMENT_STATUS = ( (MEMBERSHIP_SUBMITTED, 'Request submitted by Partner'), diff --git a/partners/decorators.py b/partners/decorators.py new file mode 100644 index 0000000000000000000000000000000000000000..cbfeffd3505e34cb9ab77443cdac4f57a798e18d --- /dev/null +++ b/partners/decorators.py @@ -0,0 +1,10 @@ +from .models import Contact + + +def has_contact(user): + """Requires user to be related to any Contact.""" + try: + user.partner_contact + return True + except Contact.DoesNotExist: + return False diff --git a/partners/forms.py b/partners/forms.py index a09a6b424c8a93b01a17ef287fc432dc4ac27fd9..6454802bb98667250a90b45b6a7ff141ce98d0de 100644 --- a/partners/forms.py +++ b/partners/forms.py @@ -1,24 +1,420 @@ from django import forms +from django.contrib.auth.models import User, Group +from django.contrib.auth.password_validation import validate_password +from django.core.exceptions import ValidationError +from django.db import transaction +from django.db.models import Q 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_KINDS -from .models import Partner, ProspectivePartner, ProspectiveContact, ProspectivePartnerEvent +from .constants import PARTNER_KINDS, PROSPECTIVE_PARTNER_PROCESSED, CONTACT_TYPES,\ + PARTNER_STATUS_UPDATE, REQUEST_PROCESSED, REQUEST_DECLINED +from .models import Partner, ProspectivePartner, ProspectiveContact, ProspectivePartnerEvent,\ + Institution, Contact, PartnerEvent, MembershipAgreement, ContactRequest,\ + PartnersAttachment +from .utils import PartnerUtils from scipost.models import TITLE_CHOICES +class MembershipAgreementForm(forms.ModelForm): + class Meta: + model = MembershipAgreement + fields = ( + 'partner', + 'status', + 'date_requested', + 'start_date', + 'duration', + 'offered_yearly_contribution' + ) + widgets = { + 'start_date': forms.TextInput(attrs={'placeholder': 'YYYY-MM-DD'}), + 'date_requested': forms.TextInput(attrs={'placeholder': 'YYYY-MM-DD'}), + } + + def save(self, current_user, commit=True): + agreement = super().save(commit=False) + if commit: + if agreement.partner and not self.instance.id: + # Create PartnerEvent if Agreement is new + event = PartnerEvent( + partner=agreement.partner, + event=PARTNER_STATUS_UPDATE, + comments='Membership Agreement added with start date %s' % agreement.start_date, + noted_by=current_user + ) + event.save() + # Save agreement afterwards to be able to detect edit/add difference + agreement.save() + return agreement + + +class ActivationForm(forms.ModelForm): + class Meta: + 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 + except Contact.DoesNotExist: + self.add_error(None, 'Your account is invalid, please contact the administrator.') + return super().clean(*args, **kwargs) + + def clean_password(self): + password = self.cleaned_data.get('password_new', '') + try: + validate_password(password, self.instance) + except ValidationError as error_message: + self.add_error('password_new', error_message) + return password + + def clean_password_verif(self): + if self.cleaned_data.get('password_new', '') != self.cleaned_data.get('password_verif', ''): + self.add_error('password_verif', 'Your password entries must match') + return self.cleaned_data.get('password_verif', '') + + @transaction.atomic + def activate_user(self): + if self.errors: + return forms.ValidationError + + # Activate account + self.instance.is_active = True + self.instance.set_password(self.cleaned_data['password_new']) + self.instance.save() + + # 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) + return self.instance + + +class PartnerEventForm(forms.ModelForm): + class Meta: + model = PartnerEvent + fields = ( + 'event', + 'comments', + ) + + +class InstitutionForm(forms.ModelForm): + class Meta: + model = Institution + fields = ( + 'kind', + 'name', + 'acronym', + 'address', + 'country', + 'logo' + ) + + class PartnerForm(forms.ModelForm): class Meta: model = Partner - fields = '__all__' + fields = ( + 'institution', + 'status', + 'main_contact' + ) def __init__(self, *args, **kwargs): - super(PartnerForm, self).__init__(*args, **kwargs) - self.fields['institution_address'].widget = forms.Textarea({'rows': 8, }) + super().__init__(*args, **kwargs) + 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, current_user): + 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(current_user=current_user) + elif self.cleaned_data['decision'] == 'decline': + self.instance.status = REQUEST_DECLINED + self.instance.save() + + +class RequestContactFormSet(forms.BaseModelFormSet): + def process_requests(self, current_user): + """ + Process all requests if status is eithter accept or decline. + """ + for form in self.forms: + form.process_request(current_user=current_user) + + +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 + + @transaction.atomic + def save(self, current_user, 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) + 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) + 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.generate_key() + contact.save() + contact.partners.add(self.partner) + + # Send email for activation + PartnerUtils.load({'contact': contact}) + PartnerUtils.email_contact_new_for_activation(current_user=current_user) + 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(max_length=16) + + class Meta: + model = ProspectivePartner + fields = ( + 'kind', + 'institution_name', + 'country', + ) + + def promote_to_partner(self, current_user): + # Create new instances + institution = Institution( + kind=self.cleaned_data['kind'], + name=self.cleaned_data['institution_name'], + acronym=self.cleaned_data['acronym'], + address=self.cleaned_data['address'], + country=self.cleaned_data['country'] + ) + institution.save() + partner = Partner( + institution=institution, + main_contact=None + ) + partner.save() + event = PartnerEvent( + partner=partner, + event=PARTNER_STATUS_UPDATE, + comments='ProspectivePartner has been upgraded to Partner by %s %s' + % (current_user.first_name, current_user.last_name), + noted_by=current_user + ) + event.save() + + # Close Prospect + self.instance.status = PROSPECTIVE_PARTNER_PROCESSED + self.instance.save() + return (partner, institution,) + + +class PromoteToContactForm(forms.ModelForm): + """ + This form is used to create a new `partners.Contact` + """ + kind = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple, + label='Contact types', choices=CONTACT_TYPES, required=False) + + class Meta: + model = ProspectiveContact + fields = ( + 'title', + 'first_name', + 'last_name', + 'email', + ) + + def clean_email(self): + """ + Check if email address is already used. + """ + email = self.cleaned_data['email'] + if User.objects.filter(Q(email=email) | Q(username=email)).exists(): + self.add_error('email', 'This emailadres has already been used.') + return email + + @transaction.atomic + def promote_contact(self, partner, current_user): + """ + Promote ProspectiveContact's to Contact's related to a certain Partner. + The status update after promotion is handled outside this method, in the Partner model. + """ + # How to handle empty instances? + + if self.errors: + return forms.ValidationError # Is this a valid exception? + + # Create a new User and Contact linked to the partner given + contact_form = NewContactForm(self.cleaned_data, partner=partner) + if contact_form.is_valid(): + return contact_form.save(current_user=current_user) + r = contact_form.errors + raise forms.ValidationError('NewContactForm invalid. Please contact Admin.') + + +class PromoteToContactFormset(forms.BaseModelFormSet): + """ + This is a formset to process multiple `PromoteToContactForm`s at the same time + designed for the 'promote prospect to partner' action. + """ + def save(self, *args, **kwargs): + raise DeprecationWarning(("This formset is not meant to used with the default" + " `save` method. User the `promote_contacts` instead.")) + + @transaction.atomic + def promote_contacts(self, partner, current_user): + """ + Promote ProspectiveContact's to Contact's related to a certain Partner. + """ + contacts = [] + for form in self.forms: + contacts.append(form.promote_contact(partner, current_user)) + partner.main_contact = contacts[0] + partner.save() + return contacts class ProspectivePartnerForm(forms.ModelForm): @@ -41,10 +437,10 @@ class ProspectiveContactForm(forms.ModelForm): class EmailProspectivePartnerContactForm(forms.Form): email_subject = forms.CharField(widget=forms.Textarea(), - initial='SciPost Supporting Partners Board') + initial='Supporting Partners Board') message = forms.CharField(widget=forms.Textarea(), required=False) include_SPB_summary = forms.BooleanField( - required=False, initial=False, + required=False, initial=True, label='include SPB summary with message') def __init__(self, *args, **kwargs): @@ -55,15 +451,8 @@ class EmailProspectivePartnerContactForm(forms.Form): {'placeholder': 'Write your message in this box (optional).'}) -# class ProspectivePartnerContactSelectForm(forms.Form): - -# def __init__(self, *args, **kwargs): -# prospartner_id = kwargs.pop('prospartner.id') -# super(ProspectivePartnerContactSelectForm, self).__init(*args, **kwargs) -# self.fields['contact'] = forms.ModelChoiceField( -# queryset=ProspectiveContact.objects.filter( -# prospartner__pk=prospartner_id).order_by('last_name'), -# required=True) +class EmailProspectivePartnerGenericForm(EmailProspectivePartnerContactForm): + email = forms.EmailField(label='Generic address for emailing') class ProspectivePartnerEventForm(forms.ModelForm): @@ -93,3 +482,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 53729dc305a9312d29b98d3de6198e17c10f6084..abd47528ec58a4c2cc8203cc742ab7710fbae32a 100644 --- a/partners/managers.py +++ b/partners/managers.py @@ -1,8 +1,37 @@ from django.db import models -from .constants import MEMBERSHIP_SUBMITTED +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): + def not_yet_partner(self): + 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/0012_auto_20170612_2006.py b/partners/migrations/0012_auto_20170612_2006.py new file mode 100644 index 0000000000000000000000000000000000000000..67e286ffce50fd56ddae743487abef996c3d7b4d --- /dev/null +++ b/partners/migrations/0012_auto_20170612_2006.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-12 18:06 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0011_auto_20170609_2234'), + ] + + operations = [ + migrations.AlterField( + model_name='institution', + name='kind', + field=models.CharField(choices=[('Res. Inst.', 'Research Institute'), ('Int. Fund. Agency', 'International Funding Agency'), ('Nat. Fund. Agency', 'National Funding Agency'), ('Nat. Lab.', 'National Laboratory'), ('Nat. Library', 'National Library'), ('Nat. Acad.', 'National Academy'), ('Univ. Library', 'University (and its Library)'), ('Res. Library', 'Research Library'), ('Prof. Soc.', 'Professional Society'), ('Nat. Consor.', 'National Consortium'), ('Foundation', 'Foundation'), ('Individual', 'Individual')], max_length=32), + ), + migrations.AlterField( + model_name='prospectivepartner', + name='kind', + field=models.CharField(choices=[('Res. Inst.', 'Research Institute'), ('Int. Fund. Agency', 'International Funding Agency'), ('Nat. Fund. Agency', 'National Funding Agency'), ('Nat. Lab.', 'National Laboratory'), ('Nat. Library', 'National Library'), ('Nat. Acad.', 'National Academy'), ('Univ. Library', 'University (and its Library)'), ('Res. Library', 'Research Library'), ('Prof. Soc.', 'Professional Society'), ('Nat. Consor.', 'National Consortium'), ('Foundation', 'Foundation'), ('Individual', 'Individual')], default='Univ. Library', max_length=32), + ), + migrations.AlterField( + model_name='prospectivepartnerevent', + name='event', + field=models.CharField(choices=[('requested', 'Requested (from online form)'), ('comment', 'Comment added'), ('email_sent', 'Email sent'), ('negotiating', 'Initiated negotiation'), ('marked_as_uninterested', 'Marked as uninterested'), ('promoted', 'Promoted to Partner')], max_length=64), + ), + ] diff --git a/partners/migrations/0012_auto_20170620_1526.py b/partners/migrations/0012_auto_20170620_1526.py new file mode 100644 index 0000000000000000000000000000000000000000..302836f563c2eedbd345f067bc90e5fbf896ee3e --- /dev/null +++ b/partners/migrations/0012_auto_20170620_1526.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-20 13:26 +from __future__ import unicode_literals + +from django.db import migrations, models +import scipost.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0011_auto_20170609_2234'), + ] + + operations = [ + migrations.RemoveField( + model_name='partner', + name='financial_contact', + ), + migrations.RemoveField( + model_name='partner', + name='technical_contact', + ), + migrations.AddField( + model_name='contact', + name='consortia', + field=models.ManyToManyField(help_text='All Consortia for which the Contact has explicit permission to view/edit its data.', to='partners.Consortium'), + ), + migrations.AddField( + model_name='contact', + name='partners', + field=models.ManyToManyField(help_text='All Partners (+related Institutions) the Contact is related to.', to='partners.Partner'), + ), + migrations.AlterField( + model_name='contact', + name='kind', + field=scipost.fields.ChoiceArrayField(base_field=models.CharField(choices=[('tech', 'Technical Contact'), ('fin', 'Financial Contact')], max_length=4), size=None), + ), + migrations.AlterField( + model_name='prospectivepartnerevent', + name='event', + field=models.CharField(choices=[('requested', 'Requested (from online form)'), ('comment', 'Comment added'), ('email_sent', 'Email sent'), ('negotiating', 'Initiated negotiation'), ('marked_as_uninterested', 'Marked as uninterested'), ('promoted', 'Promoted to Partner')], max_length=64), + ), + ] diff --git a/partners/migrations/0013_auto_20170620_1551.py b/partners/migrations/0013_auto_20170620_1551.py new file mode 100644 index 0000000000000000000000000000000000000000..d0626aef8410083b3bcc0429b7857862e4380bce --- /dev/null +++ b/partners/migrations/0013_auto_20170620_1551.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-20 13:51 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0012_auto_20170620_1526'), + ] + + operations = [ + migrations.AlterField( + model_name='contact', + name='consortia', + field=models.ManyToManyField(blank=True, help_text='All Consortia for which the Contact has explicit permission to view/edit its data.', to='partners.Consortium'), + ), + ] diff --git a/partners/migrations/0013_auto_20170623_0806.py b/partners/migrations/0013_auto_20170623_0806.py new file mode 100644 index 0000000000000000000000000000000000000000..e0e22f9c96fee922d5bd7f4e2d62679d8fbe6261 --- /dev/null +++ b/partners/migrations/0013_auto_20170623_0806.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-23 06:06 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0012_auto_20170612_2006'), + ] + + operations = [ + migrations.AlterField( + model_name='contact', + name='title', + field=models.CharField(choices=[('PR', 'Prof.'), ('DR', 'Dr'), ('MR', 'Mr'), ('MRS', 'Mrs'), ('MS', 'Ms')], max_length=4), + ), + migrations.AlterField( + model_name='prospectivecontact', + name='title', + field=models.CharField(choices=[('PR', 'Prof.'), ('DR', 'Dr'), ('MR', 'Mr'), ('MRS', 'Mrs'), ('MS', 'Ms')], max_length=4), + ), + ] diff --git a/partners/migrations/0014_auto_20170620_1554.py b/partners/migrations/0014_auto_20170620_1554.py new file mode 100644 index 0000000000000000000000000000000000000000..42da59192a1b08571b36b9c5ac81e5295145005f --- /dev/null +++ b/partners/migrations/0014_auto_20170620_1554.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-20 13:54 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0013_auto_20170620_1551'), + ] + + operations = [ + migrations.AlterField( + model_name='contact', + name='user', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='partner_contact', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/partners/migrations/0015_auto_20170620_1634.py b/partners/migrations/0015_auto_20170620_1634.py new file mode 100644 index 0000000000000000000000000000000000000000..e5378392c14ee18714b466f6d8723df5f1c030f9 --- /dev/null +++ b/partners/migrations/0015_auto_20170620_1634.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-20 14:34 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0014_auto_20170620_1554'), + ] + + operations = [ + migrations.AlterField( + model_name='institution', + name='address', + field=models.TextField(blank=True), + ), + ] diff --git a/partners/migrations/0016_auto_20170624_0905.py b/partners/migrations/0016_auto_20170624_0905.py new file mode 100644 index 0000000000000000000000000000000000000000..e44535151dfb413e5aea7db04c29a82f36dea374 --- /dev/null +++ b/partners/migrations/0016_auto_20170624_0905.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-24 07:05 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0015_auto_20170620_1634'), + ] + + operations = [ + migrations.AddField( + model_name='contact', + name='activation_key', + field=models.CharField(blank=True, max_length=40), + ), + migrations.AddField( + model_name='contact', + name='key_expires', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='membershipagreement', + name='partner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='agreements', to='partners.Partner'), + ), + migrations.AlterField( + model_name='partner', + name='status', + field=models.CharField(choices=[('Initiated', 'Initiated'), ('Contacted', 'Contacted'), ('Negotiating', 'Negotiating'), ('Uninterested', 'Uninterested'), ('Active', 'Active'), ('Inactive', 'Inactive')], default='Initiated', max_length=16), + ), + migrations.AlterField( + model_name='prospectivecontact', + name='prospartner', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='prospective_contacts', to='partners.ProspectivePartner'), + ), + ] diff --git a/partners/migrations/0017_auto_20170624_1358.py b/partners/migrations/0017_auto_20170624_1358.py new file mode 100644 index 0000000000000000000000000000000000000000..bd94e15247190ef2d51f7df5f0e1ed6f4241783e --- /dev/null +++ b/partners/migrations/0017_auto_20170624_1358.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-24 11:58 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0016_auto_20170624_0905'), + ] + + operations = [ + migrations.AlterField( + model_name='partnerevent', + name='noted_by', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='partnerevent', + name='partner', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='events', to='partners.Partner'), + ), + ] diff --git a/partners/migrations/0018_merge_20170624_1943.py b/partners/migrations/0018_merge_20170624_1943.py new file mode 100644 index 0000000000000000000000000000000000000000..04770cb31326110a1c70c27ab6c06a133c9ece45 --- /dev/null +++ b/partners/migrations/0018_merge_20170624_1943.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-24 17:43 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0017_auto_20170624_1358'), + ('partners', '0013_auto_20170623_0806'), + ] + + operations = [ + ] diff --git a/partners/migrations/0019_auto_20170624_2003.py b/partners/migrations/0019_auto_20170624_2003.py new file mode 100644 index 0000000000000000000000000000000000000000..60b6f60c561b3223abee03878709c60bf83d20af --- /dev/null +++ b/partners/migrations/0019_auto_20170624_2003.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-24 18:03 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0018_merge_20170624_1943'), + ] + + operations = [ + migrations.AlterField( + model_name='membershipagreement', + name='offered_yearly_contribution', + field=models.SmallIntegerField(default=0, help_text="Yearly contribution in euro's (€)"), + ), + ] diff --git a/partners/migrations/0020_auto_20170624_2013.py b/partners/migrations/0020_auto_20170624_2013.py new file mode 100644 index 0000000000000000000000000000000000000000..5a69c81a3a25fa09f78f1d1a67e3b2f2ac903a83 --- /dev/null +++ b/partners/migrations/0020_auto_20170624_2013.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-24 18:13 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0019_auto_20170624_2003'), + ] + + operations = [ + migrations.AlterField( + model_name='partner', + name='main_contact', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='partner_main_contact', to='partners.Contact'), + ), + ] diff --git a/partners/migrations/0021_auto_20170626_2014.py b/partners/migrations/0021_auto_20170626_2014.py new file mode 100644 index 0000000000000000000000000000000000000000..c55b21c250a1d7647b726b2c142afa1fef18bcd2 --- /dev/null +++ b/partners/migrations/0021_auto_20170626_2014.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-26 18:14 +from __future__ import unicode_literals + +from django.db import migrations, models +import scipost.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0020_auto_20170624_2013'), + ] + + operations = [ + migrations.AddField( + model_name='institution', + name='logo', + field=models.ImageField(blank=True, upload_to='UPLOADS/INSTITUTIONS/LOGOS/%Y/%m/'), + ), + migrations.AlterField( + model_name='contact', + name='kind', + field=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), + ), + ] diff --git a/partners/migrations/0022_auto_20170626_2104.py b/partners/migrations/0022_auto_20170626_2104.py new file mode 100644 index 0000000000000000000000000000000000000000..678678be652fa3951b010c20d82ee2f7a89d0b2b --- /dev/null +++ b/partners/migrations/0022_auto_20170626_2104.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-26 19:04 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0021_auto_20170626_2014'), + ] + + operations = [ + migrations.AlterField( + model_name='institution', + name='logo', + field=models.ImageField(blank=True, upload_to='institutions/logo/%Y/'), + ), + ] diff --git a/partners/migrations/0023_contact_description.py b/partners/migrations/0023_contact_description.py new file mode 100644 index 0000000000000000000000000000000000000000..771cf824f4c0bd1cab2e2437fdc000a96afa7f15 --- /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 0000000000000000000000000000000000000000..5d7d385a552dcd4c6074808c709afbfa6df3084d --- /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 0000000000000000000000000000000000000000..2a99b4e6ab802ef6a0833e86d89ca02bc6a25c7b --- /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 0000000000000000000000000000000000000000..c67c14a229fd369a038b11d8c003f6a2dff104df --- /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 723b0316c8b761045dd9d00e47322d198db597ea..3a2451424bffc50c4b972cc5926cf78c6861b809 100644 --- a/partners/models.py +++ b/partners/models.py @@ -1,5 +1,12 @@ +import datetime +import hashlib +import random +import string + from django.contrib.auth.models import User from django.db import models +from django.utils import timezone +from django.urls import reverse from django_countries.fields import CountryField @@ -14,12 +21,15 @@ from .constants import PROSPECTIVE_PARTNER_EVENT_EMAIL_SENT,\ PROSPECTIVE_PARTNER_EVENT_MARKED_AS_UNINTERESTED,\ PROSPECTIVE_PARTNER_UNINTERESTED,\ PROSPECTIVE_PARTNER_EVENT_PROMOTED,\ - PROSPECTIVE_PARTNER_PROCESSED + PROSPECTIVE_PARTNER_PROCESSED, CONTACT_TYPES,\ + PARTNER_INITIATED, REQUEST_STATUSES, REQUEST_INITIATED -from .managers import MembershipAgreementManager +from .managers import MembershipAgreementManager, ProspectivePartnerManager, PartnerManager,\ + ContactRequestManager, PartnersAttachmentManager from scipost.constants import TITLE_CHOICES -from scipost.models import get_sentinel_user +from scipost.fields import ChoiceArrayField +from scipost.models import get_sentinel_user, Contributor ######################## @@ -38,6 +48,8 @@ class ProspectivePartner(models.Model): status = models.CharField(max_length=32, choices=PROSPECTIVE_PARTNER_STATUS, default=PROSPECTIVE_PARTNER_ADDED) + objects = ProspectivePartnerManager() + def __str__(self): return '%s (received %s), %s' % (self.institution_name, self.date_received.strftime("%Y-%m-%d"), @@ -62,7 +74,8 @@ class ProspectiveContact(models.Model): It does not have a corresponding User object. It is meant to be used internally at SciPost, during Partner mining. """ - prospartner = models.ForeignKey('partners.ProspectivePartner', on_delete=models.CASCADE) + prospartner = models.ForeignKey('partners.ProspectivePartner', on_delete=models.CASCADE, + related_name='prospective_contacts') title = models.CharField(max_length=4, choices=TITLE_CHOICES) first_name = models.CharField(max_length=64) last_name = models.CharField(max_length=64) @@ -96,14 +109,36 @@ class Institution(models.Model): """ kind = models.CharField(max_length=32, choices=PARTNER_KINDS) name = models.CharField(max_length=256) + logo = models.ImageField(upload_to='institutions/logo/%Y/', blank=True) acronym = models.CharField(max_length=16) - address = models.CharField(max_length=1000, blank=True) + address = models.TextField(blank=True) country = CountryField() def __str__(self): 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 @@ -111,13 +146,60 @@ class Contact(models.Model): (main contact, financial/technical contact etc). Contacts and Contributors have different rights. """ - user = models.OneToOneField(User, on_delete=models.CASCADE, unique=True) - kind = models.CharField(max_length=128) + user = models.OneToOneField(User, on_delete=models.CASCADE, unique=True, + 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.')) + consortia = models.ManyToManyField('partners.Consortium', blank=True, + help_text=('All Consortia for which the Contact has' + ' explicit permission to view/edit its data.')) + activation_key = models.CharField(max_length=40, blank=True) + key_expires = models.DateTimeField(default=timezone.now) def __str__(self): return '%s %s, %s' % (self.get_title_display(), self.user.last_name, self.user.first_name) + def generate_key(self, feed=''): + """ + Generate and save a new activation_key for the Contact, given a certain feed. + """ + for i in range(5): + feed += random.choice(string.ascii_letters) + feed = feed.encode('utf8') + salt = self.user.username.encode('utf8') + self.activation_key = hashlib.sha1(salt+salt).hexdigest() + self.key_expires = datetime.datetime.now() + datetime.timedelta(days=2) + + 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 + try: + # User also has a Contributor-side, do not remove complete User + self.user.contributor + return super().delete(*args, **kwargs) + except Contributor.DoesNotExist: + # Remove User; casade-remove this Contact + self.user.delete() + return self + + @property + def kind_display(self): + """ + Due to a lack of support to use get_FOO_display in a ArrayField, one has to create + one 'manually'. + """ + choices = dict(CONTACT_TYPES) + return ', '.join([choices[value] for index, value in enumerate(self.kind)]) + class Partner(models.Model): """ @@ -126,29 +208,35 @@ class Partner(models.Model): """ institution = models.ForeignKey('partners.Institution', on_delete=models.CASCADE, blank=True, null=True) - status = models.CharField(max_length=16, choices=PARTNER_STATUS) - main_contact = models.ForeignKey('partners.Contact', on_delete=models.CASCADE, - blank=True, null=True, - related_name='partner_main_contact') - financial_contact = models.ForeignKey('partners.Contact', on_delete=models.CASCADE, - blank=True, null=True, - related_name='partner_financial_contact') - technical_contact = models.ForeignKey('partners.Contact', on_delete=models.CASCADE, - blank=True, null=True, - related_name='partner_technical_contact') + status = models.CharField(max_length=16, choices=PARTNER_STATUS, default=PARTNER_INITIATED) + 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() + ')' return self.get_status_display() + 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) + partner = models.ForeignKey('partners.Partner', on_delete=models.CASCADE, + related_name='events') event = models.CharField(max_length=64, choices=PARTNER_EVENTS) comments = models.TextField(blank=True) noted_on = models.DateTimeField(auto_now_add=True) - noted_by = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE) + noted_by = models.ForeignKey(User, on_delete=models.CASCADE) def __str__(self): return '%s: %s' % (str(self.partner), self.get_event_display()) @@ -172,14 +260,14 @@ class MembershipAgreement(models.Model): A new instance is created each time an Agreement is made or renewed. """ partner = models.ForeignKey('partners.Partner', on_delete=models.CASCADE, - blank=True, null=True) + blank=True, null=True, related_name='agreements') consortium = models.ForeignKey('partners.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) + offered_yearly_contribution = models.SmallIntegerField(default=0, help_text="Yearly contribution in euro's (€)") objects = MembershipAgreementManager() @@ -187,3 +275,23 @@ class MembershipAgreement(models.Model): return (str(self.partner) + ' [' + self.get_duration_display() + ' from ' + self.start_date.strftime('%Y-%m-%d') + ']') + + 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/_agreement_card.html b/partners/templates/partners/_agreement_card.html new file mode 100644 index 0000000000000000000000000000000000000000..5e014103f312446d24d9b71e1163163d4c4d4c71 --- /dev/null +++ b/partners/templates/partners/_agreement_card.html @@ -0,0 +1,20 @@ +<div class="card-block"> + <div class="row"> + <div class="col-md-4"> + <address> + <h3>{{ agreement.partner.institution.name }}</h3> + <strong>{{ agreement.partner.institution.acronym }} ({{ agreement.partner.institution.get_kind_display }})</strong><br> + {{ agreement.partner.institution.address|linebreaks }} + {{ agreement.partner.institution.get_country_display }} + </address> + </div> + <div class="col-md-4"> + <p>Agreement Status: <span class="label label-sm label-secondary">{{ agreement.get_status_display }}</span></p> + <p>Yearly Contribution: € {{ agreement.offered_yearly_contribution }}</p> + </div> + <div class="col-md-4"> + <p>Start Date: {{ agreement.start_date }}</p> + <p>Duration: {{ agreement.get_duration_display }}</p> + </div> + </div> +</div> diff --git a/partners/templates/partners/_agreement_table.html b/partners/templates/partners/_agreement_table.html new file mode 100644 index 0000000000000000000000000000000000000000..5b6c9abaea39845da4fc7748a8bf7c97e584517c --- /dev/null +++ b/partners/templates/partners/_agreement_table.html @@ -0,0 +1,8 @@ +<table class="table mt-2"> + <tr><td>Partner: </td><td><a href="{{agreement.partner.get_absolute_url}}">{{ agreement.partner }}</a></td></tr> + <tr><td>Status: </td><td>{{ agreement.status }}</td></tr> + <tr><td>Request date: </td><td>{{ agreement.date_requested }}</td></tr> + <tr><td>Start date: </td><td>{{ agreement.start_date }}</td></tr> + <tr><td>Duration: </td><td>{{ agreement.get_duration_display }}</td></tr> + <tr><td>Yearly contribution: </td><td>€ {{ agreement.offered_yearly_contribution }}</td></tr> +</table> diff --git a/partners/templates/partners/_contact_info_table.html b/partners/templates/partners/_contact_info_table.html new file mode 100644 index 0000000000000000000000000000000000000000..327605c92077e499ed1736617e9d5ec632035927 --- /dev/null +++ b/partners/templates/partners/_contact_info_table.html @@ -0,0 +1,8 @@ +<table> + <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 0000000000000000000000000000000000000000..ae833db1ab029878a230dc7f306045e4bbfefeac --- /dev/null +++ b/partners/templates/partners/_contact_li.html @@ -0,0 +1,8 @@ +<li> + <h4 class="pb-0"><strong>{{ contact.get_title_display }} {{ contact.user.first_name }} {{ contact.user.last_name }}</strong> {% if not contact.user.is_active %}<span class="label label-sm label-warning">Inactive</span>{% endif %}</h4> + <p class="text-muted mb-1">{{contact.description}}</p> + <p> + {{ contact.kind_display }}<br> + <a href="mailto:{{ contact.user.email }}">{{ contact.user.email }}</a> + </p> +</li> diff --git a/partners/templates/partners/_partner_card.html b/partners/templates/partners/_partner_card.html index 0a5e7df5986d015e49134ef685943f99ef01ffee..426d248b4e800a6b5a115057d443b0b5f151b303 100644 --- a/partners/templates/partners/_partner_card.html +++ b/partners/templates/partners/_partner_card.html @@ -2,30 +2,36 @@ <div class="card-block"> <div class="row"> - <div class="col-1"> + <div class="col-md-1"> <p>{{ partner.institution.country }}</p> </div> - <div class="col-4"> - <h3>{{ partner.institution.name }}</h3> - <p>{{ partner.institution.acronym }}</p> - <p>({{ pp.get_kind_display }})</p> + <div class="col-md-4"> + <address> + <h3><a href="{{partner.get_absolute_url}}">{{ partner.institution.name }}</a></h3> + <strong>{{ partner.institution.acronym }} ({{ partner.institution.get_kind_display }})</strong><br> + {{ partner.institution.address|linebreaks }} + {{ partner.institution.get_country_display }}<br> + Main contact: {{ partner.main_contact|default_if_none:'<em>Unknown</em>' }} + </address> </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 class="col-md-4"> + + <h3>Contacts</h3> + <ul> + {% for contact in partner.contact_set.all %} + {% include 'partners/_contact_li.html' with contact=contact %} + {% endfor %} + </ul> </div> - <div class="col-3"> - <p>Edit</p> + <div class="col-md-3"> + <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> + <li><a href="{{partner.get_absolute_url}}">View events ({{partner.events.count}})</a></li> + </ul> + </div> </div> </div> diff --git a/partners/templates/partners/_partners_page_base.html b/partners/templates/partners/_partners_page_base.html new file mode 100644 index 0000000000000000000000000000000000000000..c0e00cd2a5c44e143ff7c344e02ff5ca39a03b3c --- /dev/null +++ b/partners/templates/partners/_partners_page_base.html @@ -0,0 +1,14 @@ +{% extends 'scipost/base.html' %} + +{% block breadcrumb %} + <nav class="breadcrumb py-md-2 px-0"> + <div class="container"> + {% block breadcrumb_items %} + <a href="{% url 'partners:dashboard' %}" class="breadcrumb-item">Partner Page</a> + {% endblock %} + </div> + </nav> +{% endblock %} + + +{% block pagetitle %}: Supporting Partners:{% endblock pagetitle %} diff --git a/partners/templates/partners/_prospective_partner_card.html b/partners/templates/partners/_prospective_partner_card.html index 5496f483c4504fa5c1916c9abef54c4e34111f8b..5b9ad8805860fc5384f17034504efdc6cc0a44dd 100644 --- a/partners/templates/partners/_prospective_partner_card.html +++ b/partners/templates/partners/_prospective_partner_card.html @@ -15,6 +15,7 @@ <p>{{ pp.get_status_display }}</p> </div> <div class="col-md-7"> + <a href="{% url 'partners:email_prospartner_generic' prospartner_id=pp.id %}">Compose email to a generic address</a> <h3>Contacts:</h3> <a class="d-inline-block mb-2" href="{% url 'partners:add_prospartner_contact' prospartner_id=pp.id %}">Add a contact</a> <table class="table"> @@ -25,7 +26,7 @@ <th>Actions</th> </thead> <tbody> - {% for contact in pp.prospectivecontact_set.all %} + {% for contact in pp.prospective_contacts.all %} <tr> <td>{{ contact.role }}</td> <td>{{ contact.get_title_display }} {{ contact.first_name }} {{ contact.last_name }}</td> @@ -43,8 +44,7 @@ </div> <div class="row"> - <div class="col-1"></div> - <div class="col-6"> + <div class="col-md-6 offset-md-1"> <h3>Events</h3> <ul> {% for event in pp.prospectivepartnerevent_set.all %} @@ -54,13 +54,18 @@ {% endfor %} </ul> </div> - <div class="col-5"> + <div class="col-md-5"> <h3>Add an event for this Prospective Partner</h3> - <form action="{% url 'partners:add_prospartner_event' prospartner_id=pp.id %}" method="post"> - {% csrf_token %} - {{ ppevent_form|bootstrap }} - <input type="submit" name="submit" value="Submit" class="btn btn-secondary"> + <form class="d-block mt-2 mb-3" action="{% url 'partners:add_prospartner_event' prospartner_id=pp.id %}" method="post"> + {% csrf_token %} + {{ ppevent_form|bootstrap }} + <input type="submit" name="submit" value="Submit" class="btn btn-secondary"> </form> + + <h3>Partner status</h3> + <ul> + <li><a href="{% url 'partners:promote_prospartner' pp.id %}">Upgrade prospect to partner</a></li> + </ul> </div> </div> </div> diff --git a/partners/templates/partners/activate_account.html b/partners/templates/partners/activate_account.html new file mode 100644 index 0000000000000000000000000000000000000000..0e3aef12f1e53ddb919c6a2fc5f06620e70f6f36 --- /dev/null +++ b/partners/templates/partners/activate_account.html @@ -0,0 +1,31 @@ +{% extends 'scipost/base.html' %} + + +{% block pagetitle %}{{block.super}} Activate Account{% endblock pagetitle %} + +{% load bootstrap %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Activate Account</h1> + </div> + <div class="col-md-8 offset-md-2"> + <h2>{{contact.get_title_display}} {{contact.user.first_name}} {{contact.user.last_name}}</h2> + <h3>{{contact.user.email}}</h3> + </div> +</div> + +<div class="row"> + <div class="col-md-8 offset-md-2 mb-5"> + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + + <input class="btn btn-primary" type="submit" value="Activate"/> + </form> + </div> +</div> + +{% endblock content %} diff --git a/partners/templates/partners/add_prospective_partner.html b/partners/templates/partners/add_prospective_partner.html index 8475f83747ab690382c23741fa560f66f28e0986..28abef26c9c5798619a3b18590ec3a74d315e180 100644 --- a/partners/templates/partners/add_prospective_partner.html +++ b/partners/templates/partners/add_prospective_partner.html @@ -1,4 +1,10 @@ -{% extends 'scipost/base.html' %} +{% extends 'partners/_partners_page_base.html' %} + +{% block breadcrumb_items %} + {{block.super}} + <span class="breadcrumb-item">Add a Prospective Partner</span> +{% endblock %} + {% block pagetitle %}: Supporting Partners: add{% endblock pagetitle %} diff --git a/partners/templates/partners/agreements_add.html b/partners/templates/partners/agreements_add.html new file mode 100644 index 0000000000000000000000000000000000000000..98116017a27ebba14dbc767a464186916c327a50 --- /dev/null +++ b/partners/templates/partners/agreements_add.html @@ -0,0 +1,33 @@ +{% extends 'partners/_partners_page_base.html' %} + +{% block breadcrumb_items %} + {{block.super}} + <span class="breadcrumb-item">Add Membership Agreement</span> +{% endblock %} + +{% block pagetitle %}{{block.super}} Add Membership Agreement{% endblock pagetitle %} + +{% load bootstrap %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Add Membership Agreement</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/agreements_details.html b/partners/templates/partners/agreements_details.html new file mode 100644 index 0000000000000000000000000000000000000000..e522ea8ab2e3a58a5b02065e300bc8bcb6448d6a --- /dev/null +++ b/partners/templates/partners/agreements_details.html @@ -0,0 +1,51 @@ +{% extends 'partners/_partners_page_base.html' %} + +{% block breadcrumb_items %} + {{block.super}} + <span class="breadcrumb-item">Membership Agreement details</span> +{% endblock %} + +{% block pagetitle %}{{block.super}} Membership Agreement details{% endblock pagetitle %} + +{% load bootstrap %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Membership Agreement details</h1> + </div> +</div> + +<div class="row"> + <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> + + {% 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 }} + + <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 new file mode 100644 index 0000000000000000000000000000000000000000..e99dd7b9629655d3c64698e8b5e4f03708525e7b --- /dev/null +++ b/partners/templates/partners/dashboard.html @@ -0,0 +1,216 @@ +{% extends 'scipost/base.html' %} + +{% load partners_extras %} +{% load bootstrap %} + +{% block pagetitle %}: partner page{% endblock pagetitle %} + +{% block content %} + + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Welcome to your SciPost Partner Page, {{ request.user.first_name }} {{ request.user.last_name }}</h1> + </div> +</div> + +<div class="row"> + <div class="col-12"> + <div class="tab-nav-container"> + <div class="tab-nav-inner"> + <!-- Nav tabs --> + <ul class="nav btn-group personal-page-nav" role="tablist"> + <li class="nav-item btn btn-secondary"> + <a href="#account" class="nav-link active" data-toggle="tab">Account</a> + </li> + <li class="nav-item btn btn-secondary"> + <a href="#agreements" class="nav-link" data-toggle="tab">Membership Agreements</a> + </li> + {% if perms.scipost.can_manage_SPB %} + <li class="nav-item btn btn-secondary"> + <a href="#prospartners" class="nav-link" data-toggle="tab">Prospective Partners</a> + </li> + <li class="nav-item btn btn-secondary"> + <a href="#partners" class="nav-link" data-toggle="tab">Partners</a> + </li> + <li class="nav-item btn btn-secondary"> + <a href="#global_agreements" class="nav-link" data-toggle="tab">Agreements</a> + </li> + {% endif %} + </ul> + </div> + </div> + </div> +</div> + +<div class="tab-content"> + <!-- Tab: Account --> + <div class="tab-pane active" id="account" role="tabpanel"> + <div class="row"> + <div class="col-12"> + <h2 class="highlight">Your Account</h2> + </div> + </div> + <div class="row"> + <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> + <ul class="list-unstyled mb-5"> + {% for partner in request.user.partner_contact.partners.all %} + <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"><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 %} + <li>No partners found. Please contact the SciPost admin.</li> + {% endfor %} + </ul> + + <h3>Update your personal data or password</h3> + + <ul> + <li><a href="{% url 'scipost:update_personal_data' %}">Update your personal data</a></li> + <li><a href="{% url 'scipost:change_password' %}">Change your password</a></li> + </ul> + </div> + </div> + </div><!-- End tab --> + + <!-- Tab: Agreements --> + <div class="tab-pane" id="agreements" role="tabpanel"> + <div class="row"> + <div class="col-12"> + <h2 class="highlight">Membership Agreements</h2> + </div> + </div> + <div class="row"> + <div class="col-12"> + <ul class="list-group list-group-flush"> + {% for agreement in personal_agreements %} + <li class="list-group-item">{% include 'partners/_agreement_card.html' with agreement=agreement %}</li> + {% empty %} + <li class="list-group-item">No Membership Agreements found.</li> + {% endfor %} + </ul> + </div> + </div> + </div><!-- End tab --> + + {% if perms.scipost.can_manage_SPB %} + <!-- Tab: prospective partners --> + <div class="tab-pane" id="prospartners" role="tabpanel"> + <div class="row"> + <div class="col-12"> + <h2 class="highlight">Prospective Partners</h2> + <h3><a href="{% url 'partners:add_prospective_partner' %}">Add a prospective partner</a></h3> + </div> + </div> + + <table class="table table-hover mb-5"> + <thead class="thead-default"> + <tr> + <th>Country</th> + <th>Institution name</th> + <th>Kind</th> + <th>Status</th> + <th>Date received</th> + </tr> + </thead> + + <tbody id="accordion" role="tablist" aria-multiselectable="true"> + {% for partner in prospective_partners %} + <tr data-toggle="collapse" data-parent="#accordion" href="#collapse{{ partner.id }}" aria-expanded="true" aria-controls="collapse{{ partner.id }}" style="cursor: pointer;"> + <td>{{ partner.get_country_display }}</td> + <td>{{ partner.institution_name }}</td> + <td>{{ partner.get_kind_display }}</td> + <td style="background-color:{{ partner.status|partnerstatuscolor }}">{{ partner.get_status_display }}</td> + <td>{{ partner.date_received|date:"Y-m-d" }}</td> + </tr> + <tr id="collapse{{ partner.id }}" class="collapse" role="tabpanel" aria-labelledby="heading{{ partner.id}}" style="background-color: #fff;"> + <td colspan="5"> + {% include 'partners/_prospective_partner_card.html' with pp=partner %} + </td> + </tr> + {% empty %} + <tr> + <td colspan="5">No prospects found.</td> + </tr> + {% endfor %} + </tbody> + </table> + </div><!-- End tab --> + + <!-- Tab: Partners --> + <div class="tab-pane" id="partners" role="tabpanel"> + <div class="row"> + <div class="col-12"> + <h2 class="highlight">Partners</h2> + </div> + </div> + <div class="row"> + <div class="col-12"> + <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> + </div> + </div> + </div><!-- End tab --> + + <!-- Tab: Agreements --> + <div class="tab-pane" id="global_agreements" role="tabpanel"> + <div class="row"> + <div class="col-12"> + <h2 class="highlight">SciPost's Membership Agreements</h2> + <h3><a href="{% url 'partners:add_agreement' %}">Add Membership Agreement</a></h3> + </div> + </div> + + <table class="table"> + <thead> + <tr> + <th>Partner</th> + <th>Status</th> + <th>Duration</th> + <th>Start Date</th> + </tr> + </thead> + <tbody> + {% for agreement in agreements %} + <tr> + <td><a href="{{agreement.get_absolute_url}}">{{ agreement.partner }}</a></td> + <td>{{ agreement.get_status_display }}</td> + <td>{{ agreement.get_duration_display }}</td> + <td>{{ agreement.start_date }}</td> + </tr> + {% empty %} + <tr> + <td colspan="4">No Agreements found</td> + </tr> + {% endfor %} + </tbody> + </table> + </div><!-- End tab --> + {% endif %} +</div> + + +{% endblock content %} diff --git a/partners/templates/partners/email_prospartner_generic.html b/partners/templates/partners/email_prospartner_generic.html new file mode 100644 index 0000000000000000000000000000000000000000..6732aa816ef9eb0ffc243267a11102538add437f --- /dev/null +++ b/partners/templates/partners/email_prospartner_generic.html @@ -0,0 +1,29 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: Supporting Partners: email contact{% endblock pagetitle %} + +{% load bootstrap %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Email a Prospective Partner Generic Address</h1> + </div> +</div> + +<div class="row"> + <div class="col-12"> + <form action="{% url 'partners:email_prospartner_generic' prospartner_id=prospartner.id %}" 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 %} + </div> +</div> + +{% endblock content %} diff --git a/partners/templates/partners/institution_edit.html b/partners/templates/partners/institution_edit.html new file mode 100644 index 0000000000000000000000000000000000000000..46fed7ad380de950082effef4fd9a292c37b1310 --- /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" enctype="multipart/form-data"> + {% 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/manage_partners.html b/partners/templates/partners/manage_partners.html deleted file mode 100644 index 903a78b0501082011c6ce97c8d598eacdabbab64..0000000000000000000000000000000000000000 --- a/partners/templates/partners/manage_partners.html +++ /dev/null @@ -1,104 +0,0 @@ -{% extends 'scipost/base.html' %} - -{% load partners_extras %} - -{% block pagetitle %}: Supporting Partners: manage{% endblock pagetitle %} - - -{% block content %} - -<div class="row"> - <div class="col-12"> - <h1 class="highlight">Partners Management Page</h1> - </div> -</div> - - -<div class="row"> - <div class="col-12"> - <div class="tab-nav-container"> - <div class="tab-nav-inner"> - <ul class="nav btn-group personal-page-nav" role="tablist"> - <li class="nav-item btn btn-secondary"> - <a href="#prospartners" class="nav-link active" data-toggle="tab">Prospective Partners</a> - </li> - <li class="nav-item btn btn-secondary"> - <a href="#partners" class="nav-link" data-toggle="tab">Partners</a> - </li> - <li class="nav-item btn btn-secondary"> - <a href="#agreements" class="nav-link" data-toggle="tab">Agreements</a> - </li> - </ul> - </div> - </div> - </div> -</div> - - <div class="tab-content"> - <div class="tab-pane active" id="prospartners" role="tabpanel"> - <div class="row"> - <div class="col-12"> - <h2 class="highlight">Prospective Partners</h2> - </div> - </div> - <h3><a href="{% url 'partners:add_prospective_partner' %}">Add a prospective partner</a></h3> - <br/> - - <table class="table table-hover"> - <thead class="thead-default"> - <tr> - <th>Country</th> - <th>Institution name</th> - <th>Kind</th> - <th>Status</th> - <th>Date received</th> - </tr> - </thead> - - <tbody id="accordion" role="tablist" aria-multiselectable="true"> - {% for partner in prospective_partners %} - <tr data-toggle="collapse" data-parent="#accordion" href="#collapse{{ partner.id }}" aria-expanded="true" aria-controls="collapse{{ partner.id }}" style="cursor: pointer;"> - <td>{{ partner.get_country_display }}</td> - <td>{{ partner.institution_name }}</td> - <td>{{ partner.get_kind_display }}</td> - <td style="background-color:{{ partner.status|partnerstatuscolor }}">{{ partner.get_status_display }}</td> - <td>{{ partner.date_received|date:"Y-m-d" }}</td> - </tr> - <tr id="collapse{{ partner.id }}" class="collapse" role="tabpanel" aria-labelledby="heading{{ partner.id}}" style="background-color: #fff;"> - <td colspan="5"> - {% include 'partners/_prospective_partner_card.html' with pp=partner %} - </td> - </tr> - {% endfor %} - </tbody> - </table> - </div> - - <div class="tab-pane" id="partners" role="tabpanel"> - <div class="row"> - <div class="col-12"> - <h2 class="highlight">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> - </div> - - <div class="tab-pane" id="agreements" role="tabpanel"> - <div class="row"> - <div class="col-12"> - <h2 class="highlight">Agreements</h2> - </div> - </div> - <ul> - {% for agreement in agreements %} - <li>{{ agreement }}</li> - {% endfor %} - </ul> - </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..e074cb3ef9c0b7d1f709e119f424157ed6eb3109 --- /dev/null +++ b/partners/templates/partners/partner_edit.html @@ -0,0 +1,65 @@ +{% extends 'partners/_partners_page_base.html' %} + +{% 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 %} + +{% 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/templates/partners/partner_request_contact.html b/partners/templates/partners/partner_request_contact.html new file mode 100644 index 0000000000000000000000000000000000000000..1ef1498a5d646205b965baae4f7780cb28b0077f --- /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 new file mode 100644 index 0000000000000000000000000000000000000000..da9797ea4e456ea387cc74fd56825f2af7dd265e --- /dev/null +++ b/partners/templates/partners/partners_detail.html @@ -0,0 +1,82 @@ +{% extends 'partners/_partners_page_base.html' %} + +{% block breadcrumb_items %} + {{block.super}} + <span class="breadcrumb-item">Partner details</span> +{% endblock %} + +{% block pagetitle %}{{block.super}} Partner details{% endblock pagetitle %} + +{% load bootstrap %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Partner {{partner}}</h1> + </div> +</div> + +<div class="row"> + <div class="col-md-6"> + {% 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> + {{ partner.institution.address|linebreaks }} + {{ partner.institution.get_country_display }}<br> + Main contact: {{ partner.main_contact|default_if_none:'<em>Unknown</em>' }} + </address> + + <h3>Membership Agreements</h3> + <ul> + {% for agreement in partner.agreements.all %} + <li><a href="{{agreement.get_absolute_url}}">{{agreement}}</a></li> + {% empty %} + <li>No agreements found.</li> + {% endfor %} + </ul> + + <h3>Contacts</h3> + <ul> + {% for contact in partner.contact_set.all %} + {% include 'partners/_contact_li.html' with contact=contact %} + {% endfor %} + </ul> + + <h3>Requested Contacts</h3> + <ul> + {% for contact_request in partner.contactrequest_set.awaiting_processing %} + <li><strong>{{contact_request}}</strong> ({{contact_request.email}})</li> + {% empty %} + <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 }} + + <input class="btn btn-primary" type="submit" value="Submit"/> + </form> + {% endif %} + </div> +</div> + +{% endblock content %} diff --git a/partners/templates/partners/process_contact_requests.html b/partners/templates/partners/process_contact_requests.html new file mode 100644 index 0000000000000000000000000000000000000000..489b9a7e56120566fec2b11d419bb31342d76bcc --- /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/templates/partners/promote_prospartner.html b/partners/templates/partners/promote_prospartner.html new file mode 100644 index 0000000000000000000000000000000000000000..437ad8ca295b966aff18eb5371c9591093701287 --- /dev/null +++ b/partners/templates/partners/promote_prospartner.html @@ -0,0 +1,37 @@ +{% extends 'partners/_partners_page_base.html' %} + +{% block breadcrumb_items %} + {{block.super}} + <span class="breadcrumb-item">Promote Prospect</span> +{% endblock %} + +{% block pagetitle %}{{block.super}} Promote Prospect{% endblock pagetitle %} + +{% load bootstrap %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Promote Prospect to Partner</h1> + </div> +</div> + +<div class="row"> + <div class="col-12"> + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + {{ contact_formset.management_form }} + + {% for form in contact_formset %} + <h3>Contact {{forloop.counter}}</h3> + {{ form|bootstrap }} + {% endfor %} + + <input class="btn btn-primary" type="submit" value="Submit"/> + </form> + </div> +</div> + +{% endblock content %} diff --git a/partners/templates/partners/supporting_partners.html b/partners/templates/partners/supporting_partners.html index e7ee9a584c71293a392f65cd3249abaad3bb7bc1..2c0d931cf4eda4a2eab1dba6112cfa39cdcdc7f4 100644 --- a/partners/templates/partners/supporting_partners.html +++ b/partners/templates/partners/supporting_partners.html @@ -12,7 +12,7 @@ <div class="col-12"> <h1 class="highlight">SciPost Supporting Partners</h1> {% if perms.scipost.can_manage_SPB %} - <a href="{% url 'partners:manage' %}">Manage Partners</a> + <a href="{% url 'partners:dashboard' %}">Manage Partners</a> {% endif %} </div> </div> diff --git a/partners/urls.py b/partners/urls.py index ed50ba7595ef25515bfe6da02467211056449627..f376c8de27594f1f53fa970b3d4b201c62518d90 100644 --- a/partners/urls.py +++ b/partners/urls.py @@ -4,14 +4,44 @@ from . import views 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'^manage$', views.manage, name='manage'), - url(r'^add_prospective_partner$', views.add_prospective_partner, + url(r'^process_contact_requests$', views.process_contact_requests, name='process_contact_requests'), + + # Prospects + url(r'^prospects/add$', views.add_prospective_partner, name='add_prospective_partner'), - url(r'^add_prospartner_contact/(?P<prospartner_id>[0-9]+)$', - views.add_prospartner_contact, name='add_prospartner_contact'), - url(r'^email_prospartner_contact/(?P<contact_id>[0-9]+)$', + url(r'^prospects/contacts/(?P<contact_id>[0-9]+)/email$', views.email_prospartner_contact, name='email_prospartner_contact'), - url(r'^add_prospartner_event/(?P<prospartner_id>[0-9]+)$', + + url(r'^prospects/(?P<prospartner_id>[0-9]+)/contacts/add$', + views.add_prospartner_contact, name='add_prospartner_contact'), + url(r'^prospects/(?P<prospartner_id>[0-9]+)/promote$', + views.promote_prospartner, name='promote_prospartner'), + url(r'^prospects/(?P<prospartner_id>[0-9]+)/email_generic', + views.email_prospartner_generic, name='email_prospartner_generic'), + url(r'^prospects/(?P<prospartner_id>[0-9]+)/events/add$', views.add_prospartner_event, name='add_prospartner_event'), + + # Agreements + 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, + name='institution_edit'), + + # Users + url(r'activate/(?P<activation_key>.+)$', views.activate_account, name='activate_account'), + + # Partners + url(r'(?P<partner_id>[0-9]+)$', views.partner_view, name='partner_view'), + url(r'(?P<partner_id>[0-9]+)/edit$', views.partner_edit, name='partner_edit'), + url(r'(?P<partner_id>[0-9]+)/contacts/add$', views.partner_add_contact, + name='partner_add_contact'), + url(r'(?P<partner_id>[0-9]+)/contacts/request$', views.partner_request_contact, + name='partner_request_contact'), ] diff --git a/partners/utils.py b/partners/utils.py index fc40882c002bf695b7e219a08a302faf44e966f0..2d5241c44c89bcd779f7f0b0e992a634b06c4b9a 100644 --- a/partners/utils.py +++ b/partners/utils.py @@ -1,7 +1,6 @@ from common.utils import BaseMailUtil - class PartnerUtils(BaseMailUtil): mail_sender = 'partners@scipost.org' mail_sender_title = 'SciPost Supporting Partners' @@ -14,5 +13,27 @@ class PartnerUtils(BaseMailUtil): and invite participation to the Supporting Partners Board. """ cls._send_mail(cls, 'email_prospartner_contact', - [cls._context['contact'].email,], - cls._context['email_subject']) + [cls._context['contact'].email], + cls._context['email_subject']) + + @classmethod + def email_prospartner_generic(cls): + """ + Email a generic address for a ProspectivePartner + for which no Contact could be defined. + """ + cls._send_mail(cls, 'email_prospartner_contact', + [cls._context['email']], + cls._context['email_subject']) + + @classmethod + def email_contact_new_for_activation(cls, current_user): + """ + Email a generic address for a Contact. + + current_contact -- Contact object of the User who activated/created the new Contact object. + """ + cls._send_mail(cls, 'email_contact_new_for_activation', + [cls._context['contact'].user.email], + 'Welcome to the SciPost Supporting Partner Board', + extra_context={'sent_by': current_user}) diff --git a/partners/views.py b/partners/views.py index 111dda8d8190b60812b4991328984d0519ee8984..fb9f563ecbac768ba26484e15e88a10c470f0ab2 100644 --- a/partners/views.py +++ b/partners/views.py @@ -1,22 +1,30 @@ 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 from guardian.decorators import permission_required -from .constants import PROSPECTIVE_PARTNER_REQUESTED, PROSPECTIVE_PARTNER_ADDED,\ +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 +from .models import Partner, ProspectivePartner, ProspectiveContact, ContactRequest,\ + ProspectivePartnerEvent, MembershipAgreement, Contact, Institution,\ + PartnersAttachment from .forms import ProspectivePartnerForm, ProspectiveContactForm,\ - EmailProspectivePartnerContactForm,\ - ProspectivePartnerEventForm, MembershipQueryForm - -from common.utils import BaseMailUtil + EmailProspectivePartnerContactForm, PromoteToPartnerForm,\ + ProspectivePartnerEventForm, MembershipQueryForm, PromoteToContactForm,\ + PromoteToContactFormset, PartnerForm, ContactForm, ContactFormset,\ + NewContactForm, InstitutionForm, ActivationForm, PartnerEventForm,\ + MembershipAgreementForm, RequestContactForm, RequestContactFormSet,\ + ProcessRequestContactForm, PartnersAttachmentFormSet, PartnersAttachmentForm,\ + EmailProspectivePartnerGenericForm from .utils import PartnerUtils + def supporting_partners(request): context = {} if request.user.groups.filter(name='Editorial Administrators').exists(): @@ -26,6 +34,31 @@ def supporting_partners(request): return render(request, 'partners/supporting_partners.html', context) +@login_required +@permission_required('scipost.can_read_partner_page', return_403=True) +def dashboard(request): + ''' + This page is meant as a personal page for Partners, where they will for example be able + to read their personal data and agreements. + ''' + context = {} + try: + context['personal_agreements'] = (MembershipAgreement.objects.open_to_partner() + .filter(partner__contact=request.user.partner_contact)) + except Contact.DoesNotExist: + pass + + 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')) + context['ppevent_form'] = ProspectivePartnerEventForm() + context['agreements'] = MembershipAgreement.objects.order_by('date_requested') + return render(request, 'partners/dashboard.html', context) + + @transaction.atomic def membership_request(request): query_form = MembershipQueryForm(request.POST or None) @@ -47,8 +80,8 @@ def membership_request(request): ) contact.save() prospartnerevent = ProspectivePartnerEvent( - prospartner = prospartner, - event = PROSPECTIVE_PARTNER_EVENT_REQUESTED,) + prospartner=prospartner, + event=PROSPECTIVE_PARTNER_EVENT_REQUESTED) prospartnerevent.save() ack_message = ('Thank you for your SPB Membership query. ' 'We will get back to you in the very near future ' @@ -59,22 +92,146 @@ def membership_request(request): return render(request, 'partners/membership_request.html', context) +@permission_required('scipost.can_promote_prospect_to_partner', return_403=True) +@transaction.atomic +def promote_prospartner(request, prospartner_id): + prospartner = get_object_or_404(ProspectivePartner.objects.not_yet_partner(), + pk=prospartner_id) + form = PromoteToPartnerForm(request.POST or None, instance=prospartner) + ContactModelFormset = modelformset_factory(ProspectiveContact, PromoteToContactForm, + formset=PromoteToContactFormset, extra=0) + contact_formset = ContactModelFormset(request.POST or None, + queryset=prospartner.prospective_contacts.all()) + if form.is_valid() and contact_formset.is_valid(): + partner, institution = form.promote_to_partner(request.user) + contacts = contact_formset.promote_contacts(partner, request.user) + messages.success(request, ('<h3>Upgraded Partner %s</h3>' + '%i contacts have received a validation mail.') % + (str(partner), len(contacts))) + return redirect(reverse('partners:dashboard')) + context = {'form': form, 'contact_formset': contact_formset} + return render(request, 'partners/promote_prospartner.html', context) + + +############### +# Partner views +############### +@permission_required('scipost.can_view_own_partner_details', return_403=True) +def partner_view(request, 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) + event.partner = partner + event.noted_by = request.user + event.save() + messages.success(request, 'Added a new event to Partner.') + return redirect(partner.get_absolute_url()) + context = { + 'partner': partner, + 'form': form + } + return render(request, 'partners/partners_detail.html', context) + + @permission_required('scipost.can_manage_SPB', return_403=True) -def manage(request): - """ - Lists relevant info regarding management of Supporting Partners Board. - """ - partners = Partner.objects.all() - prospective_partners = ProspectivePartner.objects.order_by('country', 'institution_name') - ppevent_form = ProspectivePartnerEventForm() - agreements = MembershipAgreement.objects.order_by('date_requested') - context = {'partners': partners, - 'prospective_partners': prospective_partners, - 'ppevent_form': ppevent_form, - 'agreements': agreements, } - return render(request, 'partners/manage_partners.html', context) +@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(current_user=request.user) + 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) + + +@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(current_user=request.user) + 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 +################### +@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, request.FILES or None, instance=institution) + if form.is_valid(): + form.save() + messages.success(request, 'Institution has been updated.') + 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) @@ -94,7 +251,7 @@ def add_prospartner_contact(request, prospartner_id): if form.is_valid(): form.save() messages.success(request, 'Contact successfully added to Prospective Partner') - return redirect(reverse('partners:manage')) + return redirect(reverse('partners:dashboard')) context = {'form': form, 'prospartner': prospartner} return render(request, 'partners/add_prospartner_contact.html', context) @@ -107,11 +264,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]: @@ -124,11 +281,41 @@ def email_prospartner_contact(request, contact_id): PartnerUtils.email_prospartner_contact() messages.success(request, 'Email successfully sent') - return redirect(reverse('partners:manage')) + return redirect(reverse('partners:dashboard')) context = {'contact': contact, 'form': form} return render(request, 'partners/email_prospartner_contact.html', context) +@permission_required('scipost.can_email_prospartner_contact', return_403=True) +@transaction.atomic +def email_prospartner_generic(request, prospartner_id): + prospartner = get_object_or_404(ProspectivePartner, pk=prospartner_id) + form = EmailProspectivePartnerGenericForm(request.POST or None) + if form.is_valid(): + comments = 'Email sent to %s.' % form.cleaned_data['email'] + prospartnerevent = ProspectivePartnerEvent( + prospartner=prospartner, + event=PROSPECTIVE_PARTNER_EVENT_EMAIL_SENT, + comments=comments, + noted_on=timezone.now(), + noted_by=request.user.contributor) + prospartnerevent.save() + if prospartner.status in [PROSPECTIVE_PARTNER_REQUESTED, + PROSPECTIVE_PARTNER_ADDED]: + prospartner.status = PROSPECTIVE_PARTNER_APPROACHED + prospartner.save() + PartnerUtils.load({'institution_name': prospartner.institution_name, + 'email': form.cleaned_data['email'], + 'email_subject': form.cleaned_data['email_subject'], + 'message': form.cleaned_data['message'], + 'include_SPB_summary': form.cleaned_data['include_SPB_summary']}) + + PartnerUtils.email_prospartner_generic() + messages.success(request, 'Email successfully sent') + return redirect(reverse('partners:manage')) + context = {'prospartner': prospartner, 'form': form} + return render(request, 'partners/email_prospartner_generic.html', context) + @permission_required('scipost.can_manage_SPB', return_403=True) @transaction.atomic @@ -143,9 +330,82 @@ def add_prospartner_event(request, prospartner_id): ppevent.save() prospartner.update_status_from_event(ppevent.event) prospartner.save() - return redirect(reverse('partners:manage')) + return redirect(reverse('partners:dashboard')) else: errormessage = 'The form was invalidly filled.' return render(request, 'scipost/error.html', {'errormessage': errormessage}) errormessage = 'This view can only be posted to.' return render(request, 'scipost/error.html', {'errormessage': errormessage}) + + +############ +# Agreements +############ +@permission_required('scipost.can_manage_SPB', return_403=True) +def add_agreement(request): + form = MembershipAgreementForm(request.POST or None, initial=request.GET) + if request.POST and form.is_valid(): + agreement = form.save(request.user) + messages.success(request, 'Membership Agreement created.') + return redirect(agreement.get_absolute_url()) + context = { + 'form': form + } + return render(request, 'partners/agreements_add.html', context) + + +@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) + 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 +######### +def activate_account(request, activation_key): + contact = get_object_or_404(Contact, user__is_active=False, + activation_key=activation_key, + user__email__icontains=request.GET.get('email', None)) + + # TODO: Key Expires fallback + form = ActivationForm(request.POST or None, instance=contact.user) + if form.is_valid(): + form.activate_user() + messages.success(request, '<h3>Thank you for registration</h3>') + return redirect(reverse('partners:dashboard')) + context = { + 'contact': contact, + 'form': form + } + return render(request, 'partners/activate_account.html', context) diff --git a/production/admin.py b/production/admin.py index 16774de5fb420d01ec1770127df34b489cdae89d..7969db8ec5fb3bbc2427fc3751b86fc871763e62 100644 --- a/production/admin.py +++ b/production/admin.py @@ -1,43 +1,25 @@ from django.contrib import admin -from django import forms - from .models import ProductionStream, ProductionEvent -from submissions.models import Submission +def event_count(obj): + return obj.productionevent_set.count() -class ProductionStreamAdminForm(forms.ModelForm): - submission = forms.ModelChoiceField( - queryset=Submission.objects.order_by('-arxiv_identifier_w_vn_nr')) - class Meta: - model = ProductionStream - fields = '__all__' +class ProductionEventInline(admin.TabularInline): + model = ProductionEvent + extra = 1 + readonly_fields = () class ProductionStreamAdmin(admin.ModelAdmin): search_fields = ['submission'] - list_display = ['submission', 'opened', 'status'] - form = ProductionStreamAdminForm + list_filter = ['status'] + list_display = ['submission', 'opened', 'status', event_count] + inlines = ( + ProductionEventInline, + ) admin.site.register(ProductionStream, ProductionStreamAdmin) - - -class ProductionEventAdminForm(forms.ModelForm): - stream = forms.ModelChoiceField( - queryset=ProductionStream.objects.order_by('-submission.arxiv_identifier_w_vn_nr')) - - class Meta: - model = ProductionEvent - fields = '__all__' - - -class ProductionEventAdmin(admin.ModelAdmin): - search_field = ['stream', 'event', 'comment', 'noted_by'] - list_display = ['stream', 'event', 'noted_on', 'noted_by'] - form = ProductionEventAdminForm - - -admin.site.register(ProductionEvent, ProductionEventAdmin) diff --git a/production/migrations/0008_auto_20170623_0833.py b/production/migrations/0008_auto_20170623_0833.py new file mode 100644 index 0000000000000000000000000000000000000000..0f539586ada2418a86d38c75cf419f90a5c55f92 --- /dev/null +++ b/production/migrations/0008_auto_20170623_0833.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-23 06:33 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('production', '0007_auto_20170610_1202'), + ] + + operations = [ + migrations.AlterModelOptions( + name='productionevent', + options={'ordering': ['noted_on']}, + ), + ] diff --git a/production/migrations/0009_auto_20170701_1356.py b/production/migrations/0009_auto_20170701_1356.py new file mode 100644 index 0000000000000000000000000000000000000000..9aa834befe8297f68135a13d1799b90375d50d78 --- /dev/null +++ b/production/migrations/0009_auto_20170701_1356.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-07-01 11:56 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('production', '0008_auto_20170623_0833'), + ] + + operations = [ + migrations.AlterField( + model_name='productionevent', + name='noted_on', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/production/models.py b/production/models.py index 27e8c6a15785bcbae6c3d711889e7117f97b08e7..6b515e8c2b2f8c2c11e8396f6969210cb5554106 100644 --- a/production/models.py +++ b/production/models.py @@ -38,12 +38,15 @@ class ProductionEvent(models.Model): stream = models.ForeignKey(ProductionStream, on_delete=models.CASCADE) event = models.CharField(max_length=64, choices=PRODUCTION_EVENTS) comments = models.TextField(blank=True, null=True) - noted_on = models.DateTimeField(auto_now_add=True) + noted_on = models.DateTimeField(default=timezone.now) noted_by = models.ForeignKey(Contributor, on_delete=models.CASCADE) duration = models.DurationField(blank=True, null=True) objects = ProductionEventManager() + class Meta: + ordering = ['noted_on'] + def __str__(self): return '%s: %s' % (str(self.stream.submission), self.get_event_display()) diff --git a/scipost/admin.py b/scipost/admin.py index 5db931f3ef878d4e7cd3e23cd82a5ed1e2964acb..4fe9c27cd08971c8949375475d8e7bc850906f45 100644 --- a/scipost/admin.py +++ b/scipost/admin.py @@ -1,5 +1,3 @@ -import datetime - from django.contrib import admin from django import forms @@ -11,19 +9,26 @@ from scipost.models import Contributor, Remark,\ AffiliationObject,\ RegistrationInvitation,\ AuthorshipClaim, PrecookedEmail,\ - EditorialCollege, EditorialCollegeFellowship + EditorialCollege, EditorialCollegeFellowship, UnavailabilityPeriod from journals.models import Publication +from partners.admin import ContactToUserInline from submissions.models import Submission +admin.site.register(UnavailabilityPeriod) + + class ContributorInline(admin.StackedInline): model = Contributor + extra = 0 + min_num = 0 class UserAdmin(UserAdmin): inlines = [ ContributorInline, + ContactToUserInline, ] search_fields = ['last_name', 'email'] @@ -84,6 +89,7 @@ def get_remark_type(remark): return 'Recommendation' return '' + class RemarkAdminForm(forms.ModelForm): submission = forms.ModelChoiceField( required=False, @@ -93,6 +99,7 @@ class RemarkAdminForm(forms.ModelForm): model = Remark fields = '__all__' + class RemarkAdmin(admin.ModelAdmin): search_fields = ['contributor', 'remark'] list_display = [remark_text, 'contributor', 'date', get_remark_type] @@ -100,6 +107,7 @@ class RemarkAdmin(admin.ModelAdmin): list_filter = [RemarkTypeListFilter] form = RemarkAdminForm + admin.site.register(Remark, RemarkAdmin) @@ -119,6 +127,7 @@ class DraftInvitationAdmin(admin.ModelAdmin): search_fields = ['first_name', 'last_name', 'email', 'processed'] form = DraftInvitationAdminForm + admin.site.register(DraftInvitation, DraftInvitationAdmin) @@ -134,6 +143,7 @@ class RegistrationInvitationAdminForm(forms.ModelForm): model = RegistrationInvitation fields = '__all__' + class RegistrationInvitationAdmin(admin.ModelAdmin): search_fields = ['first_name', 'last_name', 'email', 'invitation_key'] list_display = ['__str__', 'invitation_type', 'invited_by', 'responded'] @@ -141,9 +151,8 @@ class RegistrationInvitationAdmin(admin.ModelAdmin): date_hierarchy = 'date_sent' form = RegistrationInvitationAdminForm -admin.site.register(RegistrationInvitation, RegistrationInvitationAdmin) - +admin.site.register(RegistrationInvitation, RegistrationInvitationAdmin) admin.site.register(AuthorshipClaim) admin.site.register(Permission) @@ -162,7 +171,6 @@ class AffiliationObjectAdmin(admin.ModelAdmin): admin.site.register(AffiliationObject, AffiliationObjectAdmin) - class EditorialCollegeAdmin(admin.ModelAdmin): search_fields = ['discipline', 'member'] diff --git a/scipost/constants.py b/scipost/constants.py index 3f8ef98b446e7553635d2424c16d040dc986c2ea..7a37244c270c6ec72a85cc3312c5695332e9cc8f 100644 --- a/scipost/constants.py +++ b/scipost/constants.py @@ -149,6 +149,7 @@ TITLE_CHOICES = ( ('DR', 'Dr'), ('MR', 'Mr'), ('MRS', 'Mrs'), + ('MS', 'Ms'), ) INVITATION_EDITORIAL_FELLOW = 'F' diff --git a/scipost/decorators.py b/scipost/decorators.py new file mode 100644 index 0000000000000000000000000000000000000000..7f0ab750e58d257dcb4db82c2acd67422bde0bec --- /dev/null +++ b/scipost/decorators.py @@ -0,0 +1,10 @@ +from .models import Contributor + + +def has_contributor(user): + """Requires user to be related to any Contributor.""" + try: + user.contributor + return True + except Contributor.DoesNotExist: + return False diff --git a/scipost/forms.py b/scipost/forms.py index 61677101d231d2acdf4d79f1666a498d3c53fc68..8273339f4df468b302262e7b6f311a026deec94c 100644 --- a/scipost/forms.py +++ b/scipost/forms.py @@ -15,9 +15,11 @@ from captcha.fields import ReCaptchaField from ajax_select.fields import AutoCompleteSelectField from .constants import SCIPOST_DISCIPLINES, TITLE_CHOICES, SCIPOST_FROM_ADDRESSES +from .decorators import has_contributor from .models import Contributor, DraftInvitation, RegistrationInvitation,\ UnavailabilityPeriod, PrecookedEmail +from partners.decorators import has_contact from journals.models import Publication # from mailing_lists.models import MailchimpList, MailchimpSubscription @@ -312,10 +314,14 @@ class AuthenticationForm(forms.Form): Check the url being valid the current request, else return to the default link (personal page). """ - personal_page_url = reverse_lazy('scipost:personal_page') redirect_to = self.cleaned_data['next'] if not is_safe_url(redirect_to, request.get_host()) or not redirect_to: - return personal_page_url + if has_contributor(request.user): + return reverse_lazy('scipost:personal_page') + elif has_contact(request.user): + return reverse_lazy('partners:dashboard') + else: + return reverse_lazy('scipost:index') return redirect_to diff --git a/scipost/management/commands/add_groups_and_permissions.py b/scipost/management/commands/add_groups_and_permissions.py index a2459c66f6c19abe466410911a372e06d94e666e..35c942debfea0ce2b0ac7025bef904e99b56acf2 100644 --- a/scipost/management/commands/add_groups_and_permissions.py +++ b/scipost/management/commands/add_groups_and_permissions.py @@ -3,6 +3,7 @@ from django.core.management.base import BaseCommand from django.contrib.auth.models import Group, Permission from django.contrib.contenttypes.models import ContentType +from partners.models import Contact from scipost.models import Contributor @@ -29,10 +30,11 @@ class Command(BaseCommand): PartnersAdmin, created = Group.objects.get_or_create(name='Partners Administrators') PartnersOfficers, created = Group.objects.get_or_create(name='Partners Officers') - + PartnerAccounts, created = Group.objects.get_or_create(name='Partners Accounts') # Create Permissions content_type = ContentType.objects.get_for_model(Contributor) + content_type_contact = ContentType.objects.get_for_model(Contact) # Supporting Partners can_manage_SPB, created = Permission.objects.get_or_create( @@ -43,6 +45,22 @@ class Command(BaseCommand): codename='can_email_prospartner_contact', name='Can email Prospective Partner Contact', content_type=content_type) + can_read_partner_page, created = Permission.objects.get_or_create( + codename='can_read_partner_page', + name='Can read Prospective Partner personal page', + content_type=content_type) + can_promote_prospect_to_partner, created = Permission.objects.get_or_create( + codename='can_promote_prospect_to_partner', + name='Can promote Prospective Partner to Partner', + content_type=content_type) + can_view_partners, created = Permission.objects.get_or_create( + codename='can_view_partners', + name='Can view Partner details 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 can_vet_registration_requests, created = Permission.objects.get_or_create( @@ -264,11 +282,22 @@ class Command(BaseCommand): ]) PartnersAdmin.permissions.set([ + can_read_partner_page, + can_view_own_partner_details, can_manage_SPB, + can_promote_prospect_to_partner, can_email_prospartner_contact, + can_view_partners, ]) PartnersOfficers.permissions.set([ + can_read_partner_page, + can_view_own_partner_details, can_manage_SPB, + can_view_partners, + ]) + PartnerAccounts.permissions.set([ + can_read_partner_page, + can_view_own_partner_details, ]) if verbose: diff --git a/scipost/migrations/0056_auto_20170623_0806.py b/scipost/migrations/0056_auto_20170623_0806.py new file mode 100644 index 0000000000000000000000000000000000000000..ec3170909dac35018b943ce12cfb9de6d1da7301 --- /dev/null +++ b/scipost/migrations/0056_auto_20170623_0806.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-23 06:06 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0055_auto_20170519_0937'), + ] + + operations = [ + migrations.AlterField( + model_name='contributor', + name='title', + field=models.CharField(choices=[('PR', 'Prof.'), ('DR', 'Dr'), ('MR', 'Mr'), ('MRS', 'Mrs'), ('MS', 'Ms')], max_length=4), + ), + migrations.AlterField( + model_name='draftinvitation', + name='title', + field=models.CharField(choices=[('PR', 'Prof.'), ('DR', 'Dr'), ('MR', 'Mr'), ('MRS', 'Mrs'), ('MS', 'Ms')], max_length=4), + ), + migrations.AlterField( + model_name='registrationinvitation', + name='title', + field=models.CharField(choices=[('PR', 'Prof.'), ('DR', 'Dr'), ('MR', 'Mr'), ('MRS', 'Mrs'), ('MS', 'Ms')], max_length=4), + ), + ] diff --git a/scipost/migrations/0057_merge_20170624_1943.py b/scipost/migrations/0057_merge_20170624_1943.py new file mode 100644 index 0000000000000000000000000000000000000000..bb2fb8d667353d8c91605f69c4d072e5cbd8060a --- /dev/null +++ b/scipost/migrations/0057_merge_20170624_1943.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-24 17:43 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0056_auto_20170623_0806'), + ('scipost', '0056_auto_20170619_2049'), + ] + + operations = [ + ] diff --git a/scipost/migrations/0058_auto_20170624_2003.py b/scipost/migrations/0058_auto_20170624_2003.py new file mode 100644 index 0000000000000000000000000000000000000000..cbd739d2d53765a419f67473fe8c9da61b4e00d5 --- /dev/null +++ b/scipost/migrations/0058_auto_20170624_2003.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-24 18:03 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0057_merge_20170624_1943'), + ] + + operations = [ + migrations.AlterField( + model_name='contributor', + name='user', + field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/scipost/migrations/0059_auto_20170701_1356.py b/scipost/migrations/0059_auto_20170701_1356.py new file mode 100644 index 0000000000000000000000000000000000000000..de6151cfd5103244b72b6b101fca52cfa1077d4d --- /dev/null +++ b/scipost/migrations/0059_auto_20170701_1356.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-07-01 11:56 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0058_auto_20170624_2003'), + ] + + operations = [ + migrations.AlterModelOptions( + name='unavailabilityperiod', + options={'ordering': ['-start']}, + ), + ] diff --git a/scipost/models.py b/scipost/models.py index 8c477ff6b07f1aea1fa6b5628f7bb7f9066e3636..a59ea8df52301965ee597ab3f795dff1048dfe02 100644 --- a/scipost/models.py +++ b/scipost/models.py @@ -37,7 +37,7 @@ class Contributor(models.Model): Permissions determine the sub-types. username, password, email, first_name and last_name are inherited from User. """ - user = models.OneToOneField(User, on_delete=models.CASCADE, unique=True) + user = models.OneToOneField(User, on_delete=models.PROTECT, unique=True) invitation_key = models.CharField(max_length=40, blank=True) activation_key = models.CharField(max_length=40, blank=True) key_expires = models.DateTimeField(default=timezone.now) @@ -164,6 +164,12 @@ class UnavailabilityPeriod(models.Model): start = models.DateField() end = models.DateField() + class Meta: + ordering = ['-start'] + + def __str__(self): + return '%s (%s to %s)' % (self.contributor, self.start, self.end) + class Remark(models.Model): contributor = models.ForeignKey(Contributor, on_delete=models.CASCADE) diff --git a/scipost/static/scipost/assets/css/_form.scss b/scipost/static/scipost/assets/css/_form.scss index 46777f8099ba93a25eceef4208c93b3d7bb19824..60f2289b07992d78f8c5cb1eed829d31add22677 100644 --- a/scipost/static/scipost/assets/css/_form.scss +++ b/scipost/static/scipost/assets/css/_form.scss @@ -10,6 +10,11 @@ border-color: #d9534f; } +.has-error .multiple-checkbox .help-block { + color: $brand-danger; + font-weight: 600; +} + .form-control + .help-block { margin-top: 3px; display: inline-block; @@ -42,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; + } + } +} diff --git a/scipost/templates/scipost/navbar.html b/scipost/templates/scipost/navbar.html index 181b9fc24d293afc173a6867e34d718185d20881..c972b3ae8a9efe50aa22f9f66b5c7393f7e84162 100644 --- a/scipost/templates/scipost/navbar.html +++ b/scipost/templates/scipost/navbar.html @@ -21,9 +21,16 @@ <li class="nav-item highlighted"> <span class="nav-link">Logged in as {{ user.username }}</span> </li> - <li class="nav-item{% if '/personal_page' in request.path %} active{% endif %}"> - <a class="nav-link" href="{% url 'scipost:personal_page' %}">Personal Page</a> - </li> + {% if user.contributor %} + <li class="nav-item{% if '/personal_page' in request.path %} active{% endif %}"> + <a class="nav-link" href="{% url 'scipost:personal_page' %}">Personal Page</a> + </li> + {% endif %} + {% if user.partner_contact %} + <li class="nav-item{% if '/partners/dashboard' in request.path %} active{% endif %}"> + <a class="nav-link" href="{% url 'partners:dashboard' %}">Partner Page</a> + </li> + {% endif %} <li class="nav-item"> <a class="nav-link" href="{% url 'scipost:logout' %}">Logout</a> </li> diff --git a/scipost/templates/scipost/update_personal_data.html b/scipost/templates/scipost/update_personal_data.html index 1c3875d45e6994f439e882566b975c5e63702004..c78cffd259ee0119827e4d310ae2760db81812f1 100644 --- a/scipost/templates/scipost/update_personal_data.html +++ b/scipost/templates/scipost/update_personal_data.html @@ -6,82 +6,85 @@ {% block content %} -<script> +{% if cont_form %} + <script> -$(document).ready(function(){ + $(document).ready(function(){ - switch ($('select#id_discipline').val()) { - case "physics": - $("#id_expertises_0").closest("li").show(); - $("#id_expertises_1").closest("li").hide(); - $("#id_expertises_2").closest("li").hide(); - $("#id_expertises_3").closest("li").hide(); - break; - case "astrophysics": - $("#id_expertises_0").closest("li").hide(); - $("#id_expertises_1").closest("li").show(); - $("#id_expertises_2").closest("li").hide(); - $("#id_expertises_3").closest("li").hide(); - break; - case "mathematics": - $("#id_expertises_0").closest("li").hide(); - $("#id_expertises_1").closest("li").hide(); - $("#id_expertises_2").closest("li").show(); - $("#id_expertises_3").closest("li").hide(); - break; - case "computerscience": - $("#id_expertises_0").closest("li").hide(); - $("#id_expertises_1").closest("li").hide(); - $("#id_expertises_2").closest("li").hide(); - $("#id_expertises_3").closest("li").show(); - break; - default: - $("#id_expertises_0").closest("li").show(); - $("#id_expertises_1").closest("li").show(); - $("#id_expertises_2").closest("li").show(); - $("#id_expertises_3").closest("li").show(); - break; - } + switch ($('select#id_discipline').val()) { + case "physics": + $("#id_expertises_0").closest("li").show(); + $("#id_expertises_1").closest("li").hide(); + $("#id_expertises_2").closest("li").hide(); + $("#id_expertises_3").closest("li").hide(); + break; + case "astrophysics": + $("#id_expertises_0").closest("li").hide(); + $("#id_expertises_1").closest("li").show(); + $("#id_expertises_2").closest("li").hide(); + $("#id_expertises_3").closest("li").hide(); + break; + case "mathematics": + $("#id_expertises_0").closest("li").hide(); + $("#id_expertises_1").closest("li").hide(); + $("#id_expertises_2").closest("li").show(); + $("#id_expertises_3").closest("li").hide(); + break; + case "computerscience": + $("#id_expertises_0").closest("li").hide(); + $("#id_expertises_1").closest("li").hide(); + $("#id_expertises_2").closest("li").hide(); + $("#id_expertises_3").closest("li").show(); + break; + default: + $("#id_expertises_0").closest("li").show(); + $("#id_expertises_1").closest("li").show(); + $("#id_expertises_2").closest("li").show(); + $("#id_expertises_3").closest("li").show(); + break; + } - $('select#id_discipline').on('change', function() { - var selection = $(this).val(); - switch(selection){ - case "physics": - $("#id_expertises_0").closest("li").show(); - $("#id_expertises_1").closest("li").hide(); - $("#id_expertises_2").closest("li").hide(); - $("#id_expertises_3").closest("li").hide(); - break; - case "astrophysics": - $("#id_expertises_0").closest("li").hide(); - $("#id_expertises_1").closest("li").show(); - $("#id_expertises_2").closest("li").hide(); - $("#id_expertises_3").closest("li").hide(); - break; - case "mathematics": - $("#id_expertises_0").closest("li").hide(); - $("#id_expertises_1").closest("li").hide(); - $("#id_expertises_2").closest("li").show(); - $("#id_expertises_3").closest("li").hide(); - break; - case "computerscience": - $("#id_expertises_0").closest("li").hide(); - $("#id_expertises_1").closest("li").hide(); - $("#id_expertises_2").closest("li").hide(); - $("#id_expertises_3").closest("li").show(); - break; - default: - $("#id_expertises_0").closest("li").show(); - $("#id_expertises_1").closest("li").show(); - $("#id_expertises_2").closest("li").show(); - $("#id_expertises_3").closest("li").show(); - break; - } - }); + $('select#id_discipline').on('change', function() { + var selection = $(this).val(); + switch(selection){ + case "physics": + $("#id_expertises_0").closest("li").show(); + $("#id_expertises_1").closest("li").hide(); + $("#id_expertises_2").closest("li").hide(); + $("#id_expertises_3").closest("li").hide(); + break; + case "astrophysics": + $("#id_expertises_0").closest("li").hide(); + $("#id_expertises_1").closest("li").show(); + $("#id_expertises_2").closest("li").hide(); + $("#id_expertises_3").closest("li").hide(); + break; + case "mathematics": + $("#id_expertises_0").closest("li").hide(); + $("#id_expertises_1").closest("li").hide(); + $("#id_expertises_2").closest("li").show(); + $("#id_expertises_3").closest("li").hide(); + break; + case "computerscience": + $("#id_expertises_0").closest("li").hide(); + $("#id_expertises_1").closest("li").hide(); + $("#id_expertises_2").closest("li").hide(); + $("#id_expertises_3").closest("li").show(); + break; + default: + $("#id_expertises_0").closest("li").show(); + $("#id_expertises_1").closest("li").show(); + $("#id_expertises_2").closest("li").show(); + $("#id_expertises_3").closest("li").show(); + break; + } + }); -}); + }); + + </script> +{% endif %} -</script> <div class="row"> <div class="col-lg-10 offset-lg-1"> @@ -89,7 +92,9 @@ $(document).ready(function(){ <form action="{% url 'scipost:update_personal_data' %}" method="post"> {% csrf_token %} {{user_form|bootstrap}} - {{cont_form|bootstrap}} + {% if cont_form %} + {{cont_form|bootstrap}} + {% endif %} <input type="submit" class="btn btn-secondary" value="Update" /> </form> </div> diff --git a/scipost/views.py b/scipost/views.py index 629438ab2846f013feff52ad3582f3e2ce315b6a..47f7a2027ab138e9c9882d1e8eaf90c90b00ba52 100644 --- a/scipost/views.py +++ b/scipost/views.py @@ -4,7 +4,7 @@ from django.utils import timezone from django.shortcuts import get_object_or_404, render from django.contrib import messages from django.contrib.auth import login, logout, update_session_auth_hash -from django.contrib.auth.decorators import login_required +from django.contrib.auth.decorators import login_required, user_passes_test from django.contrib.auth.models import Group from django.contrib.auth.views import password_reset, password_reset_confirm from django.core import mail @@ -22,6 +22,7 @@ from django.db.models import Prefetch from guardian.decorators import permission_required from .constants import SCIPOST_SUBJECT_AREAS, subject_areas_raw_dict, SciPost_from_addresses_dict +from .decorators import has_contributor from .models import Contributor, CitationNotification, UnavailabilityPeriod,\ DraftInvitation, RegistrationInvitation,\ AuthorshipClaim, EditorialCollege, EditorialCollegeFellowship @@ -46,7 +47,11 @@ from theses.models import ThesisLink ############## def is_registered(user): - return user.groups.filter(name='Registered Contributors').exists() + """ + This method checks if user is activated assuming an validated user + has at least one permission group (`Registered Contributor` or `Partner Accounts`). + """ + return user.groups.exists() # Global search @@ -737,6 +742,7 @@ def logout_view(request): @login_required +@user_passes_test(has_contributor) def mark_unavailable_period(request): ''' Mark period unavailable for Contributor using this view. @@ -756,6 +762,7 @@ def mark_unavailable_period(request): @require_POST @login_required +@user_passes_test(has_contributor) def delete_unavailable_period(request, period_id): ''' Delete period unavailable registered. @@ -768,6 +775,7 @@ def delete_unavailable_period(request, period_id): @login_required +@user_passes_test(has_contributor) def personal_page(request): """ The Personal Page is the main view for accessing user functions. @@ -909,7 +917,11 @@ def change_password(request): # Update user's session hash to stay logged in. update_session_auth_hash(request, request.user) messages.success(request, 'Your SciPost password has been successfully changed') - return redirect(reverse('scipost:personal_page')) + try: + request.user.contributor + return redirect(reverse('scipost:personal_page')) + except Contributor.DoesNotExist: + return redirect(reverse('partners:dashboard')) return render(request, 'scipost/change_password.html', {'form': form}) @@ -926,8 +938,19 @@ def reset_password(request): post_reset_redirect=reverse('scipost:login')) -@login_required -def update_personal_data(request): +def _update_personal_data_user_only(request): + user_form = UpdateUserDataForm(request.POST or None, instance=request.user) + if user_form.is_valid(): + user_form.save() + messages.success(request, 'Your personal data has been updated.') + return redirect(reverse('partners:dashboard')) + context = { + 'user_form': user_form + } + return render(request, 'scipost/update_personal_data.html', context) + + +def _update_personal_data_contributor(request): contributor = Contributor.objects.get(user=request.user) user_form = UpdateUserDataForm(request.POST or None, instance=request.user) cont_form = UpdatePersonalDataForm(request.POST or None, instance=contributor) @@ -945,6 +968,14 @@ def update_personal_data(request): @login_required +def update_personal_data(request): + if has_contributor(request.user): + return _update_personal_data_contributor(request) + return _update_personal_data_user_only(request) + + +@login_required +@user_passes_test(has_contributor) def claim_authorships(request): """ The system auto-detects potential authorships (of submissions, @@ -984,6 +1015,7 @@ def claim_authorships(request): @login_required +@user_passes_test(has_contributor) def claim_sub_authorship(request, submission_id, claim): if request.method == 'POST': contributor = Contributor.objects.get(user=request.user) @@ -999,6 +1031,7 @@ def claim_sub_authorship(request, submission_id, claim): @login_required +@user_passes_test(has_contributor) def claim_com_authorship(request, commentary_id, claim): if request.method == 'POST': contributor = Contributor.objects.get(user=request.user) @@ -1014,6 +1047,7 @@ def claim_com_authorship(request, commentary_id, claim): @login_required +@user_passes_test(has_contributor) def claim_thesis_authorship(request, thesis_id, claim): if request.method == 'POST': contributor = Contributor.objects.get(user=request.user) diff --git a/submissions/migrations/0046_auto_20170623_0806.py b/submissions/migrations/0046_auto_20170623_0806.py new file mode 100644 index 0000000000000000000000000000000000000000..a0249efe9ae3f066ccb08d5b9e30cde68bdd74fb --- /dev/null +++ b/submissions/migrations/0046_auto_20170623_0806.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-23 06:06 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0045_auto_20170608_1710'), + ] + + operations = [ + migrations.AlterField( + model_name='refereeinvitation', + name='title', + field=models.CharField(choices=[('PR', 'Prof.'), ('DR', 'Dr'), ('MR', 'Mr'), ('MRS', 'Mrs'), ('MS', 'Ms')], max_length=4), + ), + ] diff --git a/submissions/migrations/0047_submission_acceptance_date.py b/submissions/migrations/0047_submission_acceptance_date.py new file mode 100644 index 0000000000000000000000000000000000000000..ec64c3acddce25595b8b1ea7b84440e9849447a3 --- /dev/null +++ b/submissions/migrations/0047_submission_acceptance_date.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-26 19:38 +from __future__ import unicode_literals + +from django.db import migrations, models + + +def do_nothing(apps, schema_editor): + pass + + +def auto_fill_acceptance_dates(apps, schema_editor): + Publication = apps.get_model('journals', 'Publication') + for pub in Publication.objects.all(): + submission = pub.accepted_submission + submission.acceptance_date = pub.acceptance_date + submission.save() + print("Auto-filled Acceptance dates for accepted Submissions") + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0046_auto_20170623_0806'), + ] + + operations = [ + migrations.AddField( + model_name='submission', + name='acceptance_date', + field=models.DateField(blank=True, null=True, verbose_name='acceptance date'), + ), + migrations.RunPython(auto_fill_acceptance_dates, do_nothing), + ] diff --git a/submissions/models.py b/submissions/models.py index f6465d3595b5aba5f4b869bf4964f8a151e46ebc..6dc55f8d05b55ff3c5110602c039f7608b4aef5c 100644 --- a/submissions/models.py +++ b/submissions/models.py @@ -78,6 +78,7 @@ class Submission(ArxivCallable, models.Model): # Metadata metadata = JSONField(default={}, blank=True, null=True) submission_date = models.DateField(verbose_name='submission date', default=datetime.date.today) + acceptance_date = models.DateField(verbose_name='acceptance date', null=True, blank=True) latest_activity = models.DateTimeField(auto_now=True) objects = SubmissionManager() diff --git a/submissions/templates/submissions/_submission_summary_short.html b/submissions/templates/submissions/_submission_summary_short.html index ca045cbf8b37536f29828c769d0d9fec1d764a9e..bf6915e89f54fe758948d39f7ae3d7c438025498 100644 --- a/submissions/templates/submissions/_submission_summary_short.html +++ b/submissions/templates/submissions/_submission_summary_short.html @@ -24,6 +24,12 @@ <a href="{{submission.arxiv_link}}" target="_blank">{{submission.arxiv_link}}</a> </td> </tr> + {% if submission.acceptance_date %} + <tr> + <td>Date accepted:</td> + <td>{{submission.acceptance_date}}</td> + </tr> + {% endif %} <tr> <td>Date submitted:</td> <td>{{submission.submission_date}}</td> diff --git a/submissions/views.py b/submissions/views.py index 4dc7746fcf955b373f19e75da4f1b72fa80eb2ee..a672ef347c0997e85f140df871142abf16982869 100644 --- a/submissions/views.py +++ b/submissions/views.py @@ -1261,6 +1261,7 @@ def fix_College_decision(request, rec_id): if recommendation.recommendation in [1, 2, 3]: # Publish as Tier I, II or III recommendation.submission.status = 'accepted' + recommendation.submission.acceptance_date = datetime.date.today() # Create a ProductionStream object prodstream = ProductionStream(submission=recommendation.submission) prodstream.save() diff --git a/templates/email/email_contact_new_for_activation.html b/templates/email/email_contact_new_for_activation.html new file mode 100644 index 0000000000000000000000000000000000000000..16bfbdb90ad5d4f5ff43e0e13cb041f6c38ef3b6 --- /dev/null +++ b/templates/email/email_contact_new_for_activation.html @@ -0,0 +1,27 @@ +<p>Dear {{contact.get_title_display}} {{contact.user.first_name}} {{contact.user.last_name}},</p> + +<p> + Many thanks for joining the SciPost Supporting Partners Board. We have now created an account for you on scipost.org, which will allow you to access all relevant information and functionalities related to the SPB. +</p> +<p> + This is a personal account for you, which is linked to the Partner(s) representing your institution(s). +</p> +<p> + In order to activate your account, please navigate to <a href="https://scipost.org{% url 'partners:activate_account' contact.activation_key %}?email={{contact.user.email}}">this link</a>. You will be asked to choose a password, after which you will be able to login. +</p> +<p> + After logging in, you will find the main Partners page by clicking on the “Partner Page†link in the top menu on the site. +</p> +<p> + The institution you represent will be listed under the “My Partners†column. Basic information about your Membership is displayed on this page. You can add additional Contacts (legal, financial, technical etc) by following the “view/edit†and then “Request new Contact†links. +</p> +<p> + We are very pleased to welcome you on Board, and will be happy to answer any questions you might have. +</p> +<p> + Sincerely,<br><br> + By {% if sent_by.partner_contact %}{{sent_by.partner_contact.get_title_display}}{% elif sent_by.contributor %}{{sent_by.contributor.get_title_display}}{% else %}{{sent_by.first_name}}{% endif %} {{ sent_by.last_name }} + on behalf of SciPost and its Supporting Partners Board +</p> + +{% include 'email/_footer.html' %} diff --git a/templates/email/email_contact_new_for_activation.txt b/templates/email/email_contact_new_for_activation.txt new file mode 100644 index 0000000000000000000000000000000000000000..ee83ea8c1d84a58292906826ce439b5cd063e135 --- /dev/null +++ b/templates/email/email_contact_new_for_activation.txt @@ -0,0 +1,22 @@ +Dear {{contact.get_title_display}} {{contact.user.first_name}} {{contact.user.last_name}},\n\n + +Many thanks for joining the SciPost Supporting Partners Board. We have now created an account for you on scipost.org, which will allow you to access all relevant information and functionalities related to the SPB.\n\n + +This is a personal account for you, which is linked to the Partner(s) representing your institution(s).\n\n + +In order to activate your account, please navigate to:\n\n + +https://scipost.org{% url 'partners:activate_account' contact.activation_key %}?email={{contact.user.email}} \n\n + +You will be asked to choose a password, after which you will be able to login.\n\n + +After logging in, you will find the main Partners page by clicking on the “Partner Page†link in the top menu on the site.\n\n + +The institution you represent will be listed under the “My Partners†column. Basic information about your Membership is displayed on this page. You can add additional Contacts (legal, financial, technical etc) by following the “view/edit†and then “Request new Contact†links.\n\n + +We are very pleased to welcome you on Board, and will be happy to answer any questions you might have.\n\n + +Sincerely,\n\n + +By {% if sent_by.partner_contact %}{{sent_by.partner_contact.get_title_display}}{% elif sent_by.contributor %}{{sent_by.contributor.get_title_display}}{% else %}{{sent_by.first_name}}{% endif %} {{ sent_by.last_name }}\n +on behalf of SciPost and its Supporting Partners Board diff --git a/templates/email/email_prospartner_contact.html b/templates/email/email_prospartner_contact.html index c971d4d2f0eeb4ccd3ec31937e2840db6b2c4ea3..2d68fdbbb7aaaa87a717834e67702d76b8f3e5d8 100644 --- a/templates/email/email_prospartner_contact.html +++ b/templates/email/email_prospartner_contact.html @@ -1,30 +1,52 @@ -Dear {{ contact.get_title_display }} {{ contact.last_name }}, \n\n - -{% if message %}{{ message }}{% endif %} - +{% load staticfiles %} +{% if contact %} +<p>Dear {{ contact.get_title_display }} {{ contact.last_name }},</p> +{% else %} +<p>Dear colleagues,</p> +{% endif %} +{% if message %} +<p> + {{ message|linebreaks }} +</p> +{% endif %} {% if include_SPB_summary %} -You might by now have heard of SciPost, a recently-launched initiative aiming to bring disruptive change to current academic publishing practices. -\n\nIn summary, SciPost is a publication portal managed by professional scientists, offering (among others) high-quality Open Access journals with innovative forms of refereeing, and a means of commenting on all existing literature. SciPost is established as a not-for-profit foundation devoted to serving the interests of the international scientific community. -\n\nThe site is anchored at https://scipost.org. Many further details about SciPost, its principles, ideals and implementation can be found at https://scipost.org/about and https://scipost.org/FAQ. - -\n\nCrucially, as explained on our Partners page at https://scipost.org/partners, SciPost follows a completely different funding model than traditional publishers, and provides a cost-slashing alternative to existing platforms. SciPost charges neither subscription fees, nor article processing charges; its activities are instead to be collectively financed through a Supporting Partners Board, formed by a worldwide consortium of institutions and organizations which directly or indirectly benefit from SciPost’s activities. - -\n\nSupport takes the form of a small financial commitment, collectively pooled to enable SciPost to perform all its publication-related activities, maintain its online portal and implement its long-term development plan. - -\n\nIn the agreement template, which you can find online at https://scipost.org/static/scipost/SPB/SciPost_Supporting_Partner_Agreement.pdf, you will find many more specific details about our operations, requirements and funding strategy. I would greatly appreciate if you took a few minutes to read through this document. - -\n\nIt would be a privilege to welcome you as members of our Supporting Partners Board. I am hereby contacting you to enquire whether your institution would consider joining. Your support at this time is crucially required to make our initiative sustainable, and to help make it possible for the community to reap all the benefits deriving form its viable implementation. - -\n\nI will be happy to provide any required further details. I sincerely hope that SciPost will be able to count on your support. - -\n\nOn behalf of the SciPost Foundation, -\nProf. dr Jean-Sébastien Caux -\nhttp://jscaux.org -\n--------------------------------------------- -\nInstitute for Theoretial Physics\nUniversity of Amsterdam -\nScience Park 904\n1098 XH Amsterdam\nThe Netherlands -\n--------------------------------------------- -\ntel.: +31 (0)20 5255775\nfax: +31 (0)20 5255778 -\n--------------------------------------------- - +<p> + You might by now have heard of SciPost, a recently-launched initiative aiming to bring disruptive change to current academic publishing practices. +</p> +<p> + In summary, SciPost is a publication portal managed by professional scientists, offering (among others) high-quality Open Access journals with innovative forms of refereeing, and a means of commenting on all existing literature. SciPost is established as a not-for-profit foundation devoted to serving the interests of the international scientific community. +</p> +<p> + The site is anchored at <a href="https://scipost.org">SciPost.org</a>. Many further details about SciPost, its principles, ideals and implementation can be found on the <a href="https://scipost.org/about">about</a> and <a href="https://scipost.org/FAQ">FAQ</a> pages. +</p> +<p> + Crucially, as explained on our <a href="https://scipost.org/partners">Partners page</a>, SciPost follows a completely different funding model than traditional publishers, and provides a cost-slashing alternative to existing platforms. SciPost charges neither subscription fees, nor article processing charges; its activities are instead to be collectively financed through a Supporting Partners Board, formed by a worldwide consortium of institutions and organizations which directly or indirectly benefit from SciPost’s activities. +</p> +<p> + Support takes the form of a small financial commitment, collectively pooled to enable SciPost to perform all its publication-related activities, maintain its online portal and implement its long-term development plan. +</p> +<p> + In the <a href="https://scipost.org{% static 'scipost/SPB/SciPost_Supporting_Partner_Agreement.pdf' %}">agreement template</a>, you will find many more specific details about our operations, requirements and funding strategy. I would greatly appreciate if you took a few minutes to read through this document. +</p> +<p> + It would be a privilege to welcome you as members of our Supporting Partners Board. I am hereby contacting you to enquire whether your institution would consider joining. Your support at this time is crucially required to make our initiative sustainable, and to help make it possible for the community to reap all the benefits deriving form its viable implementation. +</p> +<p> +I will be happy to provide any required further details. If you are interested, you can simply get in touch via this address (<a href="mailto:partners@scipost.org">partners@scipost.org</a>). I sincerely hope that SciPost will be able to count on your support. +</p> +<p>On behalf of the SciPost Foundation,</p> +Prof. dr Jean-Sébastien Caux +<br/> +<br/><a href="mailto:J.S.Caux@uva.nl">J.S.Caux@uva.nl</a> +<br/><a href="http://jscaux.org">jscaux.org</a> +<br/>--------------------------------------------- +<br/>Institute for Theoretical Physics +<br/>University of Amsterdam +<br/>Science Park 904 +<br/>1098 XH Amsterdam +<br/>The Netherlands +<br/>--------------------------------------------- +<br/>tel.: +31 (0)20 5255775 +<br/>fax: +31 (0)20 5255778 +<br/>--------------------------------------------- {% endif %} diff --git a/templates/email/email_prospartner_contact.txt b/templates/email/email_prospartner_contact.txt new file mode 100644 index 0000000000000000000000000000000000000000..5f0455f8b1f0420e757364b23bc35ca5ac90a31c --- /dev/null +++ b/templates/email/email_prospartner_contact.txt @@ -0,0 +1,33 @@ +{% if contact %} +Dear {{ contact.get_title_display }} {{ contact.last_name }}, \n\n +{% else %} +Dear colleagues, \n\n +{% endif %} +{% if message %}{{ message }}{% endif %} +{% if include_SPB_summary %} +You might by now have heard of SciPost, a recently-launched initiative aiming to bring disruptive change to current academic publishing practices. +\n\nIn summary, SciPost is a publication portal managed by professional scientists, offering (among others) high-quality Open Access journals with innovative forms of refereeing, and a means of commenting on all existing literature. SciPost is established as a not-for-profit foundation devoted to serving the interests of the international scientific community. +\n\nThe site is anchored at https://scipost.org. Many further details about SciPost, its principles, ideals and implementation can be found at https://scipost.org/about and https://scipost.org/FAQ. + +\n\nCrucially, as explained on our Partners page at https://scipost.org/partners, SciPost follows a completely different funding model than traditional publishers, and provides a cost-slashing alternative to existing platforms. SciPost charges neither subscription fees, nor article processing charges; its activities are instead to be collectively financed through a Supporting Partners Board, formed by a worldwide consortium of institutions and organizations which directly or indirectly benefit from SciPost’s activities. + +\n\nSupport takes the form of a small financial commitment, collectively pooled to enable SciPost to perform all its publication-related activities, maintain its online portal and implement its long-term development plan. + +\n\nIn the agreement template, which you can find online at https://scipost.org/static/scipost/SPB/SciPost_Supporting_Partner_Agreement.pdf, you will find many more specific details about our operations, requirements and funding strategy. I would greatly appreciate if you took a few minutes to read through this document. + +\n\nIt would be a privilege to welcome you as members of our Supporting Partners Board. I am hereby contacting you to enquire whether your institution would consider joining. Your support at this time is crucially required to make our initiative sustainable, and to help make it possible for the community to reap all the benefits deriving form its viable implementation. + +\n\nI will be happy to provide any required further details. If you are interested, you can simply get in touch via this address (partners@scipost.org). I sincerely hope that SciPost will be able to count on your support. + +\n\nOn behalf of the SciPost Foundation, +\nProf. dr Jean-Sébastien Caux +\n\nJ.S.Caux@uva.nl +\nhttp://jscaux.org +\n--------------------------------------------- +\nInstitute for Theoretical Physics\nUniversity of Amsterdam +\nScience Park 904\n1098 XH Amsterdam\nThe Netherlands +\n--------------------------------------------- +\ntel.: +31 (0)20 5255775\nfax: +31 (0)20 5255778 +\n--------------------------------------------- + +{% endif %} diff --git a/templates/email/email_prospartner_contact_html.html b/templates/email/email_prospartner_contact_html.html deleted file mode 100644 index f1dcf34b6b240931b5deb8430f02b561c224e6fb..0000000000000000000000000000000000000000 --- a/templates/email/email_prospartner_contact_html.html +++ /dev/null @@ -1,48 +0,0 @@ -{% load staticfiles %} -<p>Dear {{ contact.get_title_display }} {{ contact.last_name }},</p> - -{% if message %} -<p> - {{ message|linebreaks }} -</p> -{% endif %} - -{% if include_SPB_summary %} -<p> - You might by now have heard of SciPost, a recently-launched initiative aiming to bring disruptive change to current academic publishing practices. -</p> -<p> - In summary, SciPost is a publication portal managed by professional scientists, offering (among others) high-quality Open Access journals with innovative forms of refereeing, and a means of commenting on all existing literature. SciPost is established as a not-for-profit foundation devoted to serving the interests of the international scientific community. -</p> -<p> - The site is anchored at <a href="https://scipost.org">SciPost.org</a>. Many further details about SciPost, its principles, ideals and implementation can be found on the <a href="https://scipost.org/about">about</a> and <a href="https://scipost.org/FAQ">FAQ</a> pages. -</p> -<p> - Crucially, as explained on our <a href="https://scipost.org/partners">Partners page</a>, SciPost follows a completely different funding model than traditional publishers, and provides a cost-slashing alternative to existing platforms. SciPost charges neither subscription fees, nor article processing charges; its activities are instead to be collectively financed through a Supporting Partners Board, formed by a worldwide consortium of institutions and organizations which directly or indirectly benefit from SciPost’s activities. -</p> -<p> - Support takes the form of a small financial commitment, collectively pooled to enable SciPost to perform all its publication-related activities, maintain its online portal and implement its long-term development plan. -</p> -<p> - In the <a href="{% static 'scipost/SPB/SciPost_Supporting_Partner_Agreement.pdf' %}">agreement template</a>, you will find many more specific details about our operations, requirements and funding strategy. I would greatly appreciate if you took a few minutes to read through this document. -</p> -<p> - It would be a privilege to welcome you as members of our Supporting Partners Board. I am hereby contacting you to enquire whether your institution would consider joining. Your support at this time is crucially required to make our initiative sustainable, and to help make it possible for the community to reap all the benefits deriving form its viable implementation. -</p> -<p> -I will be happy to provide any required further details. I sincerely hope that SciPost will be able to count on your support. -</p> -<p>On behalf of the SciPost Foundation,</p> -Prof. dr Jean-Sébastien Caux -<br/><a href="http://jscaux.org">jscaux.org</a> -<br/>--------------------------------------------- -<br/>Institute for Theoretial Physics -<br/>University of Amsterdam -<br/>Science Park 904 -<br/>1098 XH Amsterdam -<br/>The Netherlands -<br/>--------------------------------------------- -<br/>tel.: +31 (0)20 5255775 -<br/>fax: +31 (0)20 5255778 -<br/>--------------------------------------------- -{% endif %} diff --git a/templates/email/new_activation_link.html b/templates/email/new_activation_link.html index 85df025ad6f2933dbd91318177a87baf192e0912..0098af24f11521c6001aa11120b557c0c0b95487 100644 --- a/templates/email/new_activation_link.html +++ b/templates/email/new_activation_link.html @@ -1,9 +1,15 @@ -Dear {{contributor.get_title_display}} {{contributor.user.last_name}},\n\n +<p>Dear {{contributor.get_title_display}} {{contributor.user.last_name}},</p> -Your request for a new email activation link for registration to the SciPost publication portal has been received. You now need to visit this link within the next 48 hours: \n\n +<p> + Your request for a new email activation link for registration to the SciPost publication portal has been received. You now need to visit this link within the next 48 hours: +</p> +<p> + <a href="https://scipost.org{% url 'scipost:activation' contributor.id contributor.activation_key %}">Activate your account</a> +</p> -https://scipost.org{% url 'scipost:activation' contributor.id contributor.activation_key %} -\n\n +<p> + Your registration will thereafter be vetted. Many thanks for your interest.<br> + The SciPost Team. +</p> -Your registration will thereafter be vetted. Many thanks for your interest.\n -The SciPost Team. +{% include 'email/_footer.html' %} diff --git a/templates/email/new_activation_link.txt b/templates/email/new_activation_link.txt new file mode 100644 index 0000000000000000000000000000000000000000..85df025ad6f2933dbd91318177a87baf192e0912 --- /dev/null +++ b/templates/email/new_activation_link.txt @@ -0,0 +1,9 @@ +Dear {{contributor.get_title_display}} {{contributor.user.last_name}},\n\n + +Your request for a new email activation link for registration to the SciPost publication portal has been received. You now need to visit this link within the next 48 hours: \n\n + +https://scipost.org{% url 'scipost:activation' contributor.id contributor.activation_key %} +\n\n + +Your registration will thereafter be vetted. Many thanks for your interest.\n +The SciPost Team. diff --git a/templates/email/new_activation_link_html.html b/templates/email/new_activation_link_html.html deleted file mode 100644 index 0098af24f11521c6001aa11120b557c0c0b95487..0000000000000000000000000000000000000000 --- a/templates/email/new_activation_link_html.html +++ /dev/null @@ -1,15 +0,0 @@ -<p>Dear {{contributor.get_title_display}} {{contributor.user.last_name}},</p> - -<p> - Your request for a new email activation link for registration to the SciPost publication portal has been received. You now need to visit this link within the next 48 hours: -</p> -<p> - <a href="https://scipost.org{% url 'scipost:activation' contributor.id contributor.activation_key %}">Activate your account</a> -</p> - -<p> - Your registration will thereafter be vetted. Many thanks for your interest.<br> - The SciPost Team. -</p> - -{% include 'email/_footer.html' %} diff --git a/templates/email/referee_in_response_to_decision.html b/templates/email/referee_in_response_to_decision.html index bda9d8047d9b26663921017842aba421d7b75274..b8363b5b524d6dae43ea5b8ac9d27cbc4325bff1 100644 --- a/templates/email/referee_in_response_to_decision.html +++ b/templates/email/referee_in_response_to_decision.html @@ -1,14 +1,23 @@ -Dear {{ invitation.referee.get_title_display }} {{ invitation.referee.user.last_name }},\n\n +<p>Dear {{ invitation.referee.get_title_display }} {{ invitation.referee.user.last_name }},</p> -We hereby confirm your choice to {% if invitation.accepted %}accept{% else %}decline (due to reason: {{ invitation.get_refusal_reason_display }}){% endif %} to referee Submission\n\n +<p> + We hereby confirm your choice to {% if invitation.accepted %}accept{% else %}decline (due to reason: {{ invitation.get_refusal_reason_display }}){% endif %} to referee Submission +</p> +<p> + {{ invitation.submission.title }} + <br/> + by {{ invitation.submission.author_list }}. +</p> -{{invitation.submission.title}}\n -by {{ invitation.submission.author_list }}\n\n +<p> + {% if invitation.accepted %} + We will look forward to receiving your Report by the reporting deadline {{ invitation.submission.reporting_deadline|date:'Y-m-d' }}. + <br/> + Many thanks for your collaboration, + {% else %} + Nonetheless, we thank you very much for considering this refereeing invitation, + {% endif %} + <br/>The SciPost Team. +</p> -{% if invitation.accepted %} -We will look forward to receiving your Report by the reporting deadline {{ invitation.submission.reporting_deadline|date:'Y-m-d' }}.\n\n -Many thanks for your collaboration,\n -{% else %} -Nonetheless, we thank you very much for considering this refereeing invitation,\n -{% endif %} -The SciPost Team. +{% include 'email/_footer.html' %} diff --git a/templates/email/referee_in_response_to_decision.txt b/templates/email/referee_in_response_to_decision.txt new file mode 100644 index 0000000000000000000000000000000000000000..bda9d8047d9b26663921017842aba421d7b75274 --- /dev/null +++ b/templates/email/referee_in_response_to_decision.txt @@ -0,0 +1,14 @@ +Dear {{ invitation.referee.get_title_display }} {{ invitation.referee.user.last_name }},\n\n + +We hereby confirm your choice to {% if invitation.accepted %}accept{% else %}decline (due to reason: {{ invitation.get_refusal_reason_display }}){% endif %} to referee Submission\n\n + +{{invitation.submission.title}}\n +by {{ invitation.submission.author_list }}\n\n + +{% if invitation.accepted %} +We will look forward to receiving your Report by the reporting deadline {{ invitation.submission.reporting_deadline|date:'Y-m-d' }}.\n\n +Many thanks for your collaboration,\n +{% else %} +Nonetheless, we thank you very much for considering this refereeing invitation,\n +{% endif %} +The SciPost Team. diff --git a/templates/email/referee_in_response_to_decision_html.html b/templates/email/referee_in_response_to_decision_html.html deleted file mode 100644 index b8363b5b524d6dae43ea5b8ac9d27cbc4325bff1..0000000000000000000000000000000000000000 --- a/templates/email/referee_in_response_to_decision_html.html +++ /dev/null @@ -1,23 +0,0 @@ -<p>Dear {{ invitation.referee.get_title_display }} {{ invitation.referee.user.last_name }},</p> - -<p> - We hereby confirm your choice to {% if invitation.accepted %}accept{% else %}decline (due to reason: {{ invitation.get_refusal_reason_display }}){% endif %} to referee Submission -</p> -<p> - {{ invitation.submission.title }} - <br/> - by {{ invitation.submission.author_list }}. -</p> - -<p> - {% if invitation.accepted %} - We will look forward to receiving your Report by the reporting deadline {{ invitation.submission.reporting_deadline|date:'Y-m-d' }}. - <br/> - Many thanks for your collaboration, - {% else %} - Nonetheless, we thank you very much for considering this refereeing invitation, - {% endif %} - <br/>The SciPost Team. -</p> - -{% include 'email/_footer.html' %} diff --git a/templates/email/referee_response_to_EIC.html b/templates/email/referee_response_to_EIC.html index baca1fbcedfd7e29c9b470c4b4f5e9518571566e..99dc01a0f92abb424b098ab7e9fc15fc94a510a0 100644 --- a/templates/email/referee_response_to_EIC.html +++ b/templates/email/referee_response_to_EIC.html @@ -1,12 +1,23 @@ -Dear {{ invitation.submission.editor_in_charge.get_title_display }} {{ invitation.submission.editor_in_charge.user.last_name }},\n\n +<p>Dear {{ invitation.submission.editor_in_charge.get_title_display }} {{ invitation.submission.editor_in_charge.user.last_name }},</p> -Referee {% if invitation.referee %}{{ invitation.referee.get_title_display }} {{ invitation.referee.user.last_name }}{% else %}{{ invitation.get_title_display }} {{ invitation.first_name }} {{ invitation.last_name }}{% endif %} has {% if invitation.accepted %}accepted{% else %}declined (due to reason: {{ invitation.get_refusal_reason_display }}){% endif %} to referee Submission\n\n - -{{ invitation.submission.title }}\n by {{ invitation.submission.author_list }}\n\n +<p> + Referee {% if invitation.referee %}{{ invitation.referee.get_title_display }} {{ invitation.referee.user.last_name }}{% else %}{{ invitation.get_title_display }} {{ invitation.first_name }} {{ invitation.last_name }}{% endif %} has {% if invitation.accepted %}accepted{% else %}declined (due to reason: {{ invitation.get_refusal_reason_display }}){% endif %} to referee Submission +</p> +<p> + {{ invitation.submission.title }} + <br/> + by {{ invitation.submission.author_list }}. +</p> {% if not invitation.accepted %} -Please invite another referee from the Submission\'s editorial page at https://scipost.org{% url 'submissions:editorial_page' invitation.submission.arxiv_identifier_w_vn_nr %}.\n\n + <p> + Please invite another referee from the Submission's <a href="https://scipost.org{% url 'submissions:editorial_page' invitation.submission.arxiv_identifier_w_vn_nr %}">editorial page</a>. + </p> {% endif %} -Many thanks for your collaboration,\n -The SciPost Team. +<p> + Many thanks for your collaboration,<br/> + The SciPost Team. +</p> + +{% include 'email/_footer.html' %} diff --git a/templates/email/referee_response_to_EIC.txt b/templates/email/referee_response_to_EIC.txt new file mode 100644 index 0000000000000000000000000000000000000000..baca1fbcedfd7e29c9b470c4b4f5e9518571566e --- /dev/null +++ b/templates/email/referee_response_to_EIC.txt @@ -0,0 +1,12 @@ +Dear {{ invitation.submission.editor_in_charge.get_title_display }} {{ invitation.submission.editor_in_charge.user.last_name }},\n\n + +Referee {% if invitation.referee %}{{ invitation.referee.get_title_display }} {{ invitation.referee.user.last_name }}{% else %}{{ invitation.get_title_display }} {{ invitation.first_name }} {{ invitation.last_name }}{% endif %} has {% if invitation.accepted %}accepted{% else %}declined (due to reason: {{ invitation.get_refusal_reason_display }}){% endif %} to referee Submission\n\n + +{{ invitation.submission.title }}\n by {{ invitation.submission.author_list }}\n\n + +{% if not invitation.accepted %} +Please invite another referee from the Submission\'s editorial page at https://scipost.org{% url 'submissions:editorial_page' invitation.submission.arxiv_identifier_w_vn_nr %}.\n\n +{% endif %} + +Many thanks for your collaboration,\n +The SciPost Team. diff --git a/templates/email/referee_response_to_EIC_html.html b/templates/email/referee_response_to_EIC_html.html deleted file mode 100644 index 99dc01a0f92abb424b098ab7e9fc15fc94a510a0..0000000000000000000000000000000000000000 --- a/templates/email/referee_response_to_EIC_html.html +++ /dev/null @@ -1,23 +0,0 @@ -<p>Dear {{ invitation.submission.editor_in_charge.get_title_display }} {{ invitation.submission.editor_in_charge.user.last_name }},</p> - -<p> - Referee {% if invitation.referee %}{{ invitation.referee.get_title_display }} {{ invitation.referee.user.last_name }}{% else %}{{ invitation.get_title_display }} {{ invitation.first_name }} {{ invitation.last_name }}{% endif %} has {% if invitation.accepted %}accepted{% else %}declined (due to reason: {{ invitation.get_refusal_reason_display }}){% endif %} to referee Submission -</p> -<p> - {{ invitation.submission.title }} - <br/> - by {{ invitation.submission.author_list }}. -</p> - -{% if not invitation.accepted %} - <p> - Please invite another referee from the Submission's <a href="https://scipost.org{% url 'submissions:editorial_page' invitation.submission.arxiv_identifier_w_vn_nr %}">editorial page</a>. - </p> -{% endif %} - -<p> - Many thanks for your collaboration,<br/> - The SciPost Team. -</p> - -{% include 'email/_footer.html' %} diff --git a/templates/email/registration_request_received.html b/templates/email/registration_request_received.html index 88264d5ff1c2d459d8f79606ffa73850487b877e..f18064bf55b004ce93676b6b561b8d4ca632428b 100644 --- a/templates/email/registration_request_received.html +++ b/templates/email/registration_request_received.html @@ -1,9 +1,15 @@ -Dear {{contributor.get_title_display}} {{contributor.user.last_name}},\n\n +<p>Dear {{contributor.get_title_display}} {{contributor.user.last_name}},</p> -Your request for registration to the SciPost publication portal has been received. You now need to validate your email by visiting this link within the next 48 hours: \n\n +<p> + Your request for registration to the SciPost publication portal has been received. You now need to validate your email by visiting this link within the next 48 hours: +</p> +<p> + <a href="https://scipost.org{% url 'scipost:activation' contributor.id contributor.activation_key %}">Activate your account</a> +</p> -https://scipost.org{% url 'scipost:activation' contributor.id contributor.activation_key %} -\n\n +<p> + Your registration will thereafter be vetted. Many thanks for your interest.<br> + The SciPost Team. +</p> -Your registration will thereafter be vetted. Many thanks for your interest.\n -The SciPost Team. +{% include 'email/_footer.html' %} diff --git a/templates/email/registration_request_received.txt b/templates/email/registration_request_received.txt new file mode 100644 index 0000000000000000000000000000000000000000..88264d5ff1c2d459d8f79606ffa73850487b877e --- /dev/null +++ b/templates/email/registration_request_received.txt @@ -0,0 +1,9 @@ +Dear {{contributor.get_title_display}} {{contributor.user.last_name}},\n\n + +Your request for registration to the SciPost publication portal has been received. You now need to validate your email by visiting this link within the next 48 hours: \n\n + +https://scipost.org{% url 'scipost:activation' contributor.id contributor.activation_key %} +\n\n + +Your registration will thereafter be vetted. Many thanks for your interest.\n +The SciPost Team. diff --git a/templates/email/registration_request_received_html.html b/templates/email/registration_request_received_html.html deleted file mode 100644 index f18064bf55b004ce93676b6b561b8d4ca632428b..0000000000000000000000000000000000000000 --- a/templates/email/registration_request_received_html.html +++ /dev/null @@ -1,15 +0,0 @@ -<p>Dear {{contributor.get_title_display}} {{contributor.user.last_name}},</p> - -<p> - Your request for registration to the SciPost publication portal has been received. You now need to validate your email by visiting this link within the next 48 hours: -</p> -<p> - <a href="https://scipost.org{% url 'scipost:activation' contributor.id contributor.activation_key %}">Activate your account</a> -</p> - -<p> - Your registration will thereafter be vetted. Many thanks for your interest.<br> - The SciPost Team. -</p> - -{% include 'email/_footer.html' %} diff --git a/templates/email/report_delivered_eic.html b/templates/email/report_delivered_eic.html index fe01130b3d6c39bc41a02242ebecfe67d395da0a..11d7d9f82e48e1ca640829b453dd495d531c386f 100644 --- a/templates/email/report_delivered_eic.html +++ b/templates/email/report_delivered_eic.html @@ -1,13 +1,19 @@ -Dear {{ report.submission.editor_in_charge.get_title_display }} {{ report.submission.editor_in_charge.user.last_name }},\n\n +<p>Dear {{ report.submission.editor_in_charge.get_title_display }} {{ report.submission.editor_in_charge.user.last_name }},</p> -Referee {{ report.author.get_title_display }} {{ report.author.user.last_name }} has delivered a Report for Submission\n\n -{{ report.submission.title }}\n -by {{ report.submission.author_list }}. -\n\n +<p> + Referee {{ report.author.get_title_display }} {{ report.author.user.last_name }} has delivered a Report for Submission: +</p> +<p> + {{report.submission.title}} + <br/> + by {{ report.submission.author_list }}. +</p> +<p> + Please vet this Report via your <a href="https://scipost.org{% url 'scipost:personal_page' %}">personal page</a> under the Editorial Actions tab. +</p> +<p> + Many thanks in advance for your collaboration,<br> + The SciPost Team. +</p> -Please vet this Report via your personal page at -https://scipost.org{% url 'scipost:personal_page' %}, under the Editorial Actions tab.\n -\n\n - -Many thanks in advance for your collaboration,\n -The SciPost Team. +{% include 'email/_footer.html' %} diff --git a/templates/email/report_delivered_eic.txt b/templates/email/report_delivered_eic.txt new file mode 100644 index 0000000000000000000000000000000000000000..fe01130b3d6c39bc41a02242ebecfe67d395da0a --- /dev/null +++ b/templates/email/report_delivered_eic.txt @@ -0,0 +1,13 @@ +Dear {{ report.submission.editor_in_charge.get_title_display }} {{ report.submission.editor_in_charge.user.last_name }},\n\n + +Referee {{ report.author.get_title_display }} {{ report.author.user.last_name }} has delivered a Report for Submission\n\n +{{ report.submission.title }}\n +by {{ report.submission.author_list }}. +\n\n + +Please vet this Report via your personal page at +https://scipost.org{% url 'scipost:personal_page' %}, under the Editorial Actions tab.\n +\n\n + +Many thanks in advance for your collaboration,\n +The SciPost Team. diff --git a/templates/email/report_delivered_eic_html.html b/templates/email/report_delivered_eic_html.html deleted file mode 100644 index 11d7d9f82e48e1ca640829b453dd495d531c386f..0000000000000000000000000000000000000000 --- a/templates/email/report_delivered_eic_html.html +++ /dev/null @@ -1,19 +0,0 @@ -<p>Dear {{ report.submission.editor_in_charge.get_title_display }} {{ report.submission.editor_in_charge.user.last_name }},</p> - -<p> - Referee {{ report.author.get_title_display }} {{ report.author.user.last_name }} has delivered a Report for Submission: -</p> -<p> - {{report.submission.title}} - <br/> - by {{ report.submission.author_list }}. -</p> -<p> - Please vet this Report via your <a href="https://scipost.org{% url 'scipost:personal_page' %}">personal page</a> under the Editorial Actions tab. -</p> -<p> - Many thanks in advance for your collaboration,<br> - The SciPost Team. -</p> - -{% include 'email/_footer.html' %} diff --git a/templates/email/report_delivered_referee.html b/templates/email/report_delivered_referee.html index ecff9be7a20b5597ccc9684b7d849915b8e7a610..9e50b5f4b9c9063c83433de22bd000034faa27fd 100644 --- a/templates/email/report_delivered_referee.html +++ b/templates/email/report_delivered_referee.html @@ -1,11 +1,21 @@ -Dear {{ report.author.get_title_display }} {{ report.author.user.last_name }},\n\n +<p> + Dear {{ report.author.get_title_display }} {{ report.author.user.last_name }}, +</p> -We hereby confirm reception of your Report on Submission\n\n +<p>We hereby confirm reception of your Report on Submission</p> -{{ report.submission.title }}\n -by {{ report.submission.author_list }}.\n\n +<p> + {{ report.submission.title }} + <br/> + by {{ report.submission.author_list }}. +</p> -We are immensely grateful for your time and effort. Your Report will soon be vetted by the Submission's Editor-in-charge, at which point you will receive an email update from us.\n\n +<p> + We are immensely grateful for your time and effort. Your Report will soon be vetted by the Submission's Editor-in-charge, at which point you will receive an email update from us. +</p> -Many thanks again,\n -The SciPost Team. +<p> + Many thanks again, + <br/> + The SciPost Team. +</p> diff --git a/templates/email/report_delivered_referee.txt b/templates/email/report_delivered_referee.txt new file mode 100644 index 0000000000000000000000000000000000000000..ecff9be7a20b5597ccc9684b7d849915b8e7a610 --- /dev/null +++ b/templates/email/report_delivered_referee.txt @@ -0,0 +1,11 @@ +Dear {{ report.author.get_title_display }} {{ report.author.user.last_name }},\n\n + +We hereby confirm reception of your Report on Submission\n\n + +{{ report.submission.title }}\n +by {{ report.submission.author_list }}.\n\n + +We are immensely grateful for your time and effort. Your Report will soon be vetted by the Submission's Editor-in-charge, at which point you will receive an email update from us.\n\n + +Many thanks again,\n +The SciPost Team. diff --git a/templates/email/report_delivered_referee_html.html b/templates/email/report_delivered_referee_html.html deleted file mode 100644 index 9e50b5f4b9c9063c83433de22bd000034faa27fd..0000000000000000000000000000000000000000 --- a/templates/email/report_delivered_referee_html.html +++ /dev/null @@ -1,21 +0,0 @@ -<p> - Dear {{ report.author.get_title_display }} {{ report.author.user.last_name }}, -</p> - -<p>We hereby confirm reception of your Report on Submission</p> - -<p> - {{ report.submission.title }} - <br/> - by {{ report.submission.author_list }}. -</p> - -<p> - We are immensely grateful for your time and effort. Your Report will soon be vetted by the Submission's Editor-in-charge, at which point you will receive an email update from us. -</p> - -<p> - Many thanks again, - <br/> - The SciPost Team. -</p> diff --git a/templates/email/submission_cycle_reinvite_referee.html b/templates/email/submission_cycle_reinvite_referee.html index f776b1bd3329c58dd2f8d3fa303403328adf1fe4..08db162758be0ab0ed123a02ccd3b0667456d828 100644 --- a/templates/email/submission_cycle_reinvite_referee.html +++ b/templates/email/submission_cycle_reinvite_referee.html @@ -1,15 +1,28 @@ -Dear {{invitation.get_title_display}} {{invitation.last_name}},\n\n\n +<p>Dear {{invitation.get_title_display}} {{invitation.last_name}},</p> -The authors of submission\n\n +<p> + The authors of submission +</p> -{{invitation.submission.title}} by {{invitation.submission.author_list}} \n\n +<p> + {{invitation.submission.title}} + <br> + by {{invitation.submission.author_list}} + <br> + (<a href="https://scipost.org{{invitation.submission.get_absolute_url}}">see on SciPost.org</a>) +<p> + have resubmitted their manuscript to SciPost. On behalf of the Editor-in-charge {{invitation.submission.editor_in_charge.get_title_display}} {{invitation.submission.editor_in_charge.user.last_name}}, we would like to invite you to quickly review this new version. + Please accept or decline the invitation (login required) as soon as possible (ideally within the next 2 days). +</p> +<p> + If you accept, your report can be submitted by simply clicking on the "Contribute a Report" link on the Submission's Page before the reporting deadline (currently set at {{invitation.submission.reporting_deadline|date:'N j, Y'}}; your report will be automatically recognized as an invited report). +</p> +<p> + You might want to make sure you are familiar with our refereeing code of conduct and with the refereeing procedure. +</p> +<p> + We would be extremely grateful for your contribution, and thank you in advance for your consideration.<br> + The SciPost Team. +</p> -have resubmitted their manuscript to SciPost. On behalf of the Editor-in-charge {{invitation.submission.editor_in_charge.get_title_display}} {{invitation.submission.editor_in_charge.user.last_name}}, we would like to invite you to quickly review this new version.\n -Please accept or decline the invitation (login required) as soon as possible (ideally within the next 2 days).\n\n - -If you accept, your report can be submitted by simply clicking on the "Contribute a Report" link on the Submission's Page before the reporting deadline (currently set at {{invitation.submission.reporting_deadline|date:'N j, Y'}}; your report will be automatically recognized as an invited report).\n\n - -You might want to make sure you are familiar with our refereeing code of conduct and with the refereeing procedure.\n\n - -We would be extremely grateful for your contribution, and thank you in advance for your consideration.\n -The SciPost Team. +{% include 'email/_footer.html' %} diff --git a/templates/email/submission_cycle_reinvite_referee.txt b/templates/email/submission_cycle_reinvite_referee.txt new file mode 100644 index 0000000000000000000000000000000000000000..f776b1bd3329c58dd2f8d3fa303403328adf1fe4 --- /dev/null +++ b/templates/email/submission_cycle_reinvite_referee.txt @@ -0,0 +1,15 @@ +Dear {{invitation.get_title_display}} {{invitation.last_name}},\n\n\n + +The authors of submission\n\n + +{{invitation.submission.title}} by {{invitation.submission.author_list}} \n\n + +have resubmitted their manuscript to SciPost. On behalf of the Editor-in-charge {{invitation.submission.editor_in_charge.get_title_display}} {{invitation.submission.editor_in_charge.user.last_name}}, we would like to invite you to quickly review this new version.\n +Please accept or decline the invitation (login required) as soon as possible (ideally within the next 2 days).\n\n + +If you accept, your report can be submitted by simply clicking on the "Contribute a Report" link on the Submission's Page before the reporting deadline (currently set at {{invitation.submission.reporting_deadline|date:'N j, Y'}}; your report will be automatically recognized as an invited report).\n\n + +You might want to make sure you are familiar with our refereeing code of conduct and with the refereeing procedure.\n\n + +We would be extremely grateful for your contribution, and thank you in advance for your consideration.\n +The SciPost Team. diff --git a/templates/email/submission_cycle_reinvite_referee_html.html b/templates/email/submission_cycle_reinvite_referee_html.html deleted file mode 100644 index 08db162758be0ab0ed123a02ccd3b0667456d828..0000000000000000000000000000000000000000 --- a/templates/email/submission_cycle_reinvite_referee_html.html +++ /dev/null @@ -1,28 +0,0 @@ -<p>Dear {{invitation.get_title_display}} {{invitation.last_name}},</p> - -<p> - The authors of submission -</p> - -<p> - {{invitation.submission.title}} - <br> - by {{invitation.submission.author_list}} - <br> - (<a href="https://scipost.org{{invitation.submission.get_absolute_url}}">see on SciPost.org</a>) -<p> - have resubmitted their manuscript to SciPost. On behalf of the Editor-in-charge {{invitation.submission.editor_in_charge.get_title_display}} {{invitation.submission.editor_in_charge.user.last_name}}, we would like to invite you to quickly review this new version. - Please accept or decline the invitation (login required) as soon as possible (ideally within the next 2 days). -</p> -<p> - If you accept, your report can be submitted by simply clicking on the "Contribute a Report" link on the Submission's Page before the reporting deadline (currently set at {{invitation.submission.reporting_deadline|date:'N j, Y'}}; your report will be automatically recognized as an invited report). -</p> -<p> - You might want to make sure you are familiar with our refereeing code of conduct and with the refereeing procedure. -</p> -<p> - We would be extremely grateful for your contribution, and thank you in advance for your consideration.<br> - The SciPost Team. -</p> - -{% include 'email/_footer.html' %} diff --git a/templates/email/submission_eic_reappointment.html b/templates/email/submission_eic_reappointment.html index 01c9463755a1254e5397d6a529f5904212a4c13b..6d2d4ee38dd3dae48e4f74641cd00b1bdef37b80 100644 --- a/templates/email/submission_eic_reappointment.html +++ b/templates/email/submission_eic_reappointment.html @@ -1,17 +1,25 @@ -Dear {{ submission.editor_in_charge.get_title_display }} {{ submission.editor_in_charge.user.last_name }},\n\n +<p>Dear {{ submission.editor_in_charge.get_title_display }} {{ submission.editor_in_charge.user.last_name }},</p> -The authors of the SciPost Submission\n\n -{{ submission.title }} -\n\n -by {{ submission.author_list }} -\n\n -have resubmitted their manuscript.\n\n +<p> + The authors of the SciPost Submission +</p> +<p> + {{submission.title}} + <br/> + by {{submission.author_list}} +</p> +<p> + have resubmitted their manuscript. +</p> +<p> + As Editor-in-charge, you can take your editorial actions from the submission's <a href="https://scipost.org{% url 'submissions:editorial_page' submission.arxiv_identifier_w_vn_nr %}">editorial page</a>, which is also accessible from your <a href="https://scipost.org{% url 'scipost:personal_page' %}">personal page</a> under the Editorial Actions tab. +</p> +<p> + You can either take an immediate acceptance/rejection decision, quickly consult previous referees or run a new refereeing round, in which case you should now invite at least 3 referees; you might want to make sure you are aware of the detailed procedure described in the <a href="https://scipost.org{% url 'scipost:EdCol_by-laws' %}">Editorial College by-laws</a>. +</p> +<p> + Many thanks in advance for your collaboration,<br> + The SciPost Team. +</p> -As Editor-in-charge, you can take your editorial actions from the submission\'s editorial page https://scipost.org{% url 'submissions:editorial_page' submission.arxiv_identifier_w_vn_nr %}, -which is also accessible from your personal page at https://scipost.org{% url 'scipost:personal_page' %} under the Editorial Actions tab.\n\n - -You can either take an immediate acceptance/rejection decision, quickly consult previous referees or run a new refereeing round, in which case you should now invite at least 3 referees; you might want to make sure you are aware of the detailed procedure described in the Editorial College by-laws. See https://scipost.org{% url 'scipost:EdCol_by-laws' %}. -\n\n - -Many thanks in advance for your collaboration,\n -The SciPost Team. +{% include 'email/_footer.html' %} diff --git a/templates/email/submission_eic_reappointment.txt b/templates/email/submission_eic_reappointment.txt new file mode 100644 index 0000000000000000000000000000000000000000..01c9463755a1254e5397d6a529f5904212a4c13b --- /dev/null +++ b/templates/email/submission_eic_reappointment.txt @@ -0,0 +1,17 @@ +Dear {{ submission.editor_in_charge.get_title_display }} {{ submission.editor_in_charge.user.last_name }},\n\n + +The authors of the SciPost Submission\n\n +{{ submission.title }} +\n\n +by {{ submission.author_list }} +\n\n +have resubmitted their manuscript.\n\n + +As Editor-in-charge, you can take your editorial actions from the submission\'s editorial page https://scipost.org{% url 'submissions:editorial_page' submission.arxiv_identifier_w_vn_nr %}, +which is also accessible from your personal page at https://scipost.org{% url 'scipost:personal_page' %} under the Editorial Actions tab.\n\n + +You can either take an immediate acceptance/rejection decision, quickly consult previous referees or run a new refereeing round, in which case you should now invite at least 3 referees; you might want to make sure you are aware of the detailed procedure described in the Editorial College by-laws. See https://scipost.org{% url 'scipost:EdCol_by-laws' %}. +\n\n + +Many thanks in advance for your collaboration,\n +The SciPost Team. diff --git a/templates/email/submission_eic_reappointment_html.html b/templates/email/submission_eic_reappointment_html.html deleted file mode 100644 index 6d2d4ee38dd3dae48e4f74641cd00b1bdef37b80..0000000000000000000000000000000000000000 --- a/templates/email/submission_eic_reappointment_html.html +++ /dev/null @@ -1,25 +0,0 @@ -<p>Dear {{ submission.editor_in_charge.get_title_display }} {{ submission.editor_in_charge.user.last_name }},</p> - -<p> - The authors of the SciPost Submission -</p> -<p> - {{submission.title}} - <br/> - by {{submission.author_list}} -</p> -<p> - have resubmitted their manuscript. -</p> -<p> - As Editor-in-charge, you can take your editorial actions from the submission's <a href="https://scipost.org{% url 'submissions:editorial_page' submission.arxiv_identifier_w_vn_nr %}">editorial page</a>, which is also accessible from your <a href="https://scipost.org{% url 'scipost:personal_page' %}">personal page</a> under the Editorial Actions tab. -</p> -<p> - You can either take an immediate acceptance/rejection decision, quickly consult previous referees or run a new refereeing round, in which case you should now invite at least 3 referees; you might want to make sure you are aware of the detailed procedure described in the <a href="https://scipost.org{% url 'scipost:EdCol_by-laws' %}">Editorial College by-laws</a>. -</p> -<p> - Many thanks in advance for your collaboration,<br> - The SciPost Team. -</p> - -{% include 'email/_footer.html' %}