From 723156a9890b007863af5324f18e00f3dfbe67d3 Mon Sep 17 00:00:00 2001 From: Jorran de Wit <jorrandewit@outlook.com> Date: Thu, 2 Nov 2017 12:59:54 +0100 Subject: [PATCH] Finish Affiliations/Institute construction --- affiliations/forms.py | 97 +++++++- affiliations/managers.py | 14 ++ .../migrations/0007_auto_20171102_1256.py | 19 ++ affiliations/models.py | 4 + common/widgets.py | 96 ++++++++ scipost/admin.py | 9 +- scipost/forms.py | 58 ++--- scipost/migrations/0071_auto_20171102_0858.py | 23 ++ .../0072_delete_affiliationobject.py | 18 ++ scipost/models.py | 14 -- .../scipost/assets/config/preconfig.scss | 3 +- scipost/static/scipost/assets/css/_form.scss | 15 ++ .../static/scipost/assets/css/_tables.scss | 7 + scipost/static/scipost/formset.js | 231 ++++++++++++++++++ .../scipost/_private_info_as_table.html | 32 ++- .../scipost/_public_info_as_table.html | 25 +- .../templates/scipost/contributor_info.html | 13 +- .../scipost/update_personal_data.html | 134 +++++----- scipost/templates/tags/bootstrap/formset.html | 16 +- scipost/views.py | 18 +- 20 files changed, 657 insertions(+), 189 deletions(-) create mode 100644 affiliations/managers.py create mode 100644 affiliations/migrations/0007_auto_20171102_1256.py create mode 100644 common/widgets.py create mode 100644 scipost/migrations/0071_auto_20171102_0858.py create mode 100644 scipost/migrations/0072_delete_affiliationobject.py create mode 100644 scipost/static/scipost/formset.js diff --git a/affiliations/forms.py b/affiliations/forms.py index dd8879a61..c475964a8 100644 --- a/affiliations/forms.py +++ b/affiliations/forms.py @@ -1,6 +1,99 @@ from django import forms +from django.forms import BaseModelFormSet, modelformset_factory +# from django.db.models import F -from .models import Institute +from django_countries import countries +from django_countries.fields import LazyTypedChoiceField +from django_countries.widgets import CountrySelectWidget + +from common.widgets import DateWidget + +from .models import Affiliation, Institute + + +class AffiliationForm(forms.ModelForm): + name = forms.CharField(label='* Affiliation', max_length=300) + country = LazyTypedChoiceField( + choices=countries, label='* Country', widget=CountrySelectWidget()) + + class Meta: + model = Affiliation + fields = ( + 'name', + 'country', + 'begin_date', + 'end_date', + ) + widgets = { + 'begin_date': DateWidget(required=False), + 'end_date': DateWidget(required=False) + } + + class Media: + js = ('scipost/formset.js',) + + def __init__(self, *args, **kwargs): + self.contributor = kwargs.pop('contributor') + affiliation = kwargs.get('instance') + if hasattr(affiliation, 'institute'): + institute = affiliation.institute + kwargs['initial'] = { + 'name': institute.name, + 'country': institute.country + } + super().__init__(*args, **kwargs) + + def save(self, commit=True): + """ + Save the Affiliation and Institute if neccessary. + """ + affiliation = super().save(commit=False) + affiliation.contributor = self.contributor + + if commit: + if hasattr(affiliation, 'institute') and affiliation.institute.affiliations.count() == 1: + # Just update if there are no other people using this Institute + institute = affiliation.institute + institute.name = self.cleaned_data['name'] + institute.country = self.cleaned_data['country'] + institute.save() + else: + institute, __ = Institute.objects.get_or_create( + name=self.cleaned_data['name'], + country=self.cleaned_data['country']) + affiliation.institute = institute + affiliation.save() + return affiliation + + +class AffiliationsFormSet(BaseModelFormSet): + """ + This formset helps update the Institutes for the Contributor at specific time periods. + """ + def __init__(self, *args, **kwargs): + self.contributor = kwargs.pop('contributor') + super().__init__(*args, **kwargs) + self.queryset = self.contributor.affiliations.all() + + def get_form_kwargs(self, *args, **kwargs): + kwargs = super().get_form_kwargs(*args, **kwargs) + kwargs['contributor'] = self.contributor + return kwargs + + def save(self, commit=True): + self.deleted_objects = [] + + for form in self.forms: + form.save(commit) + + # Delete Affiliations if needed + for form in self.deleted_forms: + self.deleted_objects.append(form.instance) + self.delete_existing(form.instance, commit=commit) + + +AffiliationsFormset = modelformset_factory(Affiliation, form=AffiliationForm, can_delete=True, + formset=AffiliationsFormSet, extra=0) class InstituteMergeForm(forms.ModelForm): @@ -17,6 +110,6 @@ class InstituteMergeForm(forms.ModelForm): def save(self, commit=True): old_institute = self.cleaned_data['institute'] if commit: - old_institute.contributors.update(institute=self.instance) + Affiliation.objects.filter(institute=old_institute).update(institute=self.instance) old_institute.delete() return self.instance diff --git a/affiliations/managers.py b/affiliations/managers.py new file mode 100644 index 000000000..1cc7840ae --- /dev/null +++ b/affiliations/managers.py @@ -0,0 +1,14 @@ +import datetime + +from django.db import models +from django.db.models import Q + + +class AffiliationQuerySet(models.QuerySet): + def active(self): + today = datetime.date.today() + return self.filter( + Q(begin_date__lte=today, end_date__isnull=True) | + Q(begin_date__isnull=True, end_date__gte=today) | + Q(begin_date__lte=today, end_date__gte=today) | + Q(begin_date__isnull=True, end_date__isnull=True)) diff --git a/affiliations/migrations/0007_auto_20171102_1256.py b/affiliations/migrations/0007_auto_20171102_1256.py new file mode 100644 index 000000000..0f044ab10 --- /dev/null +++ b/affiliations/migrations/0007_auto_20171102_1256.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-02 11:56 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('affiliations', '0006_auto_20171102_0843'), + ] + + operations = [ + migrations.AlterModelOptions( + name='affiliation', + options={'ordering': ['begin_date', 'end_date', 'institute']}, + ), + ] diff --git a/affiliations/models.py b/affiliations/models.py index edbd131b7..fddbf443c 100644 --- a/affiliations/models.py +++ b/affiliations/models.py @@ -4,6 +4,7 @@ from django.urls import reverse from django_countries.fields import CountryField from .constants import INSTITUTE_TYPES, TYPE_UNIVERSITY +from .managers import AffiliationQuerySet class Institute(models.Model): @@ -36,8 +37,11 @@ class Affiliation(models.Model): begin_date = models.DateField(null=True, blank=True) end_date = models.DateField(null=True, blank=True) + objects = AffiliationQuerySet.as_manager() + class Meta: default_related_name = 'affiliations' + ordering = ['begin_date', 'end_date', 'institute'] def __str__(self): return '{contributor} ({institute})'.format( diff --git a/common/widgets.py b/common/widgets.py new file mode 100644 index 000000000..faa904bb9 --- /dev/null +++ b/common/widgets.py @@ -0,0 +1,96 @@ +# import calendar +import datetime +import re + +from django.forms.widgets import Widget, Select, NumberInput +from django.utils.dates import MONTHS +from django.utils.safestring import mark_safe + +__all__ = ('DateWidget',) + +RE_DATE = re.compile(r'(\d{4})-(\d\d?)-(\d\d?)$') + + +class DateWidget(Widget): + """ + A Widget that splits date input into two <select> boxes for month and year, + with 'day' defaulting to the first of the month. + + Based on SelectDateWidget, in + + django/trunk/django/forms/extras/widgets.py + + + """ + none_value = (0, 'Month') + day_field = '%s_day' + month_field = '%s_month' + year_field = '%s_year' + + def __init__(self, attrs=None, end=False, required=False): + self.attrs = attrs or {} + self.required = required + self.today = datetime.date.today() + self.round_to_end = end + + # Month + self.month_choices = dict(MONTHS.items()) + if not self.required: + self.month_choices[self.none_value[0]] = self.none_value[1] + self.month_choices = sorted(self.month_choices.items()) + + def sqeeze_form_group(self, html, width=4): + return '<div class="col-md-{width}">{html}</div>'.format(width=width, html=html) + + def render(self, name, value, attrs=None): + try: + year_val, month_val, day_val = value.year, value.month, value.day + except AttributeError: + year_val = month_val = day_val = None + if isinstance(value, (str, bytes)): + match = RE_DATE.match(value) + if match: + year_val, month_val, day_val = [int(v) for v in match.groups()] + + output = [] + + if 'id' in self.attrs: + id_ = self.attrs['id'] + else: + id_ = 'id_%s' % name + + # Day input + local_attrs = self.build_attrs({'id': self.day_field % id_}) + s = NumberInput(attrs={'class': 'form-control', 'placeholder': 'Day'}) + select_html = s.render(self.day_field % name, day_val, local_attrs) + output.append(self.sqeeze_form_group(select_html)) + + # Month input + if hasattr(self, 'month_choices'): + local_attrs = self.build_attrs({'id': self.month_field % id_}) + s = Select(choices=self.month_choices, attrs={'class': 'form-control'}) + select_html = s.render(self.month_field % name, month_val, local_attrs) + output.append(self.sqeeze_form_group(select_html)) + + # Year input + local_attrs = self.build_attrs({'id': self.year_field % id_}) + s = NumberInput(attrs={'class': 'form-control', 'placeholder': 'Year'}) + select_html = s.render(self.year_field % name, year_val, local_attrs) + output.append(self.sqeeze_form_group(select_html)) + + output = '<div class="row mb-0">{0}</div>'.format(u'\n'.join(output)) + + return mark_safe(output) + + @classmethod + def id_for_label(self, id_): + return '%s_month' % id_ + + def value_from_datadict(self, data, files, name): + y = data.get(self.year_field % name) + m = data.get(self.month_field % name) + d = data.get(self.day_field % name) + + if m == "0": + return None + return '%s-%s-%s' % (y, m, d) diff --git a/scipost/admin.py b/scipost/admin.py index 8cfaffcf2..3b8059b73 100644 --- a/scipost/admin.py +++ b/scipost/admin.py @@ -6,7 +6,6 @@ from django.contrib.auth.models import User, Permission from scipost.models import Contributor, Remark,\ DraftInvitation,\ - AffiliationObject,\ RegistrationInvitation,\ AuthorshipClaim, PrecookedEmail,\ EditorialCollege, EditorialCollegeFellowship, UnavailabilityPeriod @@ -177,13 +176,6 @@ class PrecookedEmailAdmin(admin.ModelAdmin): admin.site.register(PrecookedEmail, PrecookedEmailAdmin) -class AffiliationObjectAdmin(admin.ModelAdmin): - search_fields = ['country', 'institution', 'subunit'] - - -admin.site.register(AffiliationObject, AffiliationObjectAdmin) - - class EditorialCollegeAdmin(admin.ModelAdmin): search_fields = ['discipline', 'member'] @@ -195,6 +187,7 @@ def college_fellow_is_active(fellow): '''Check if fellow is currently active.''' return fellow.is_active() + class EditorialCollegeFellowshipAdminForm(forms.ModelForm): contributor = forms.ModelChoiceField( queryset=Contributor.objects.order_by('user__last_name')) diff --git a/scipost/forms.py b/scipost/forms.py index ed25e5e0b..6a175bb12 100644 --- a/scipost/forms.py +++ b/scipost/forms.py @@ -24,7 +24,7 @@ from .decorators import has_contributor from .models import Contributor, DraftInvitation, RegistrationInvitation,\ UnavailabilityPeriod, PrecookedEmail -from affiliations.models import Institute +from affiliations.models import Affiliation, Institute from common.forms import MonthYearWidget from partners.decorators import has_contact @@ -58,12 +58,12 @@ class RegistrationForm(forms.Form): widget=forms.TextInput( {'placeholder': 'Recommended. Get one at orcid.org'})) discipline = forms.ChoiceField(choices=SCIPOST_DISCIPLINES, label='* Main discipline') - # country_of_employment = LazyTypedChoiceField( - # choices=countries, label='* Country of employment', initial='NL', - # widget=CountrySelectWidget(layout=( - # '{widget}<img class="country-select-flag" id="{flag_id}"' - # ' style="margin: 6px 4px 0" src="{country.flag}">'))) - # affiliation = forms.CharField(label='* Affiliation', max_length=300) + country_of_employment = LazyTypedChoiceField( + choices=countries, label='* Country of employment', initial='NL', + widget=CountrySelectWidget(layout=( + '{widget}<img class="country-select-flag" id="{flag_id}"' + ' style="margin: 6px 4px 0" src="{country.flag}">'))) + affiliation = forms.CharField(label='* Affiliation', max_length=300) address = forms.CharField( label='Address', max_length=1000, widget=forms.TextInput({'placeholder': 'For postal correspondence'}), @@ -116,19 +116,22 @@ class RegistrationForm(forms.Form): 'password': self.cleaned_data['password'], 'is_active': False }) - # institute, __ = Institute.objects.get_or_create( - # country=self.cleaned_data['country_of_employment'], - # name=self.cleaned_data['affiliation'], - # ) + institute, __ = Institute.objects.get_or_create( + country=self.cleaned_data['country_of_employment'], + name=self.cleaned_data['affiliation'], + ) contributor, new = Contributor.objects.get_or_create(**{ 'user': user, 'invitation_key': self.cleaned_data.get('invitation_key', ''), 'title': self.cleaned_data['title'], 'orcid_id': self.cleaned_data['orcid_id'], 'address': self.cleaned_data['address'], - # 'affiliation': institute, 'personalwebpage': self.cleaned_data['personalwebpage'], }) + affiliation, __ = Affiliation.objects.get_or_create( + contributor=contributor, + institute=institute, + ) if contributor.activation_key == '': # Seems redundant? @@ -261,9 +264,6 @@ class UpdateUserDataForm(forms.ModelForm): class UpdatePersonalDataForm(forms.ModelForm): - # country_of_employment = LazyTypedChoiceField(choices=countries, widget=CountrySelectWidget()) - # affiliation = forms.CharField(max_length=300) - class Meta: model = Contributor fields = [ @@ -275,28 +275,6 @@ class UpdatePersonalDataForm(forms.ModelForm): 'personalwebpage' ] - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # self.fields['country_of_employment'].initial = self.instance.affiliation.country - # self.fields['affiliation'].initial = self.instance.affiliation.name - - def save(self, commit=True): - contributor = super().save(commit) - # if commit: - # if contributor.affiliation.contributors.count() == 1: - # # Just update if there are no other people using this Affiliation - # affiliation = contributor.affiliation - # affiliation.name = self.cleaned_data['affiliation'] - # affiliation.country = self.cleaned_data['country_of_employment'] - # affiliation.save() - # else: - # affiliation, __ = Affiliation.objects.get_or_create( - # name=self.cleaned_data['affiliation'], - # country=self.cleaned_data['country_of_employment']) - # contributor.affiliation = affiliation - # contributor.save() - return contributor - def sync_lists(self): """ Pseudo U/S; do not remove @@ -491,12 +469,6 @@ class SearchForm(HayStackSearchForm): start = forms.DateField(widget=MonthYearWidget(), required=False) # Month end = forms.DateField(widget=MonthYearWidget(end=True), required=False) # Month - # def __init__(self, *args, **kwargs): - # super().__init__(*args, **kwargs) - # models = self.fields['models'].choices - # models = filter(lambda x: x[0] != 'sphinxdoc.document', models) - # self.fields['models'].choices = models - def search(self): sqs = super().search() diff --git a/scipost/migrations/0071_auto_20171102_0858.py b/scipost/migrations/0071_auto_20171102_0858.py new file mode 100644 index 000000000..2719f788a --- /dev/null +++ b/scipost/migrations/0071_auto_20171102_0858.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-02 07:58 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0070_remove_contributor_old_affiliation_fk'), + ] + + operations = [ + migrations.RemoveField( + model_name='contributor', + name='old_affiliation', + ), + migrations.RemoveField( + model_name='contributor', + name='old_country_of_employment', + ), + ] diff --git a/scipost/migrations/0072_delete_affiliationobject.py b/scipost/migrations/0072_delete_affiliationobject.py new file mode 100644 index 000000000..3f82fd9db --- /dev/null +++ b/scipost/migrations/0072_delete_affiliationobject.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-02 11:56 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0071_auto_20171102_0858'), + ] + + operations = [ + migrations.DeleteModel( + name='AffiliationObject', + ), + ] diff --git a/scipost/models.py b/scipost/models.py index eec7395d1..4cd14cef3 100644 --- a/scipost/models.py +++ b/scipost/models.py @@ -61,10 +61,6 @@ class Contributor(models.Model): default=True, verbose_name="I accept to receive SciPost emails") - # U/S - old_country_of_employment = CountryField() - old_affiliation = models.CharField(max_length=300, verbose_name='affiliation') - objects = ContributorManager() def __str__(self): @@ -290,16 +286,6 @@ class PrecookedEmail(models.Model): return self.email_subject -####################### -# Affiliation Objects # -####################### - -class AffiliationObject(models.Model): - country = CountryField() - institution = models.CharField(max_length=128) - subunit = models.CharField(max_length=128) - - ###################### # Static info models # ###################### diff --git a/scipost/static/scipost/assets/config/preconfig.scss b/scipost/static/scipost/assets/config/preconfig.scss index 7ddb10372..2712abe46 100644 --- a/scipost/static/scipost/assets/config/preconfig.scss +++ b/scipost/static/scipost/assets/config/preconfig.scss @@ -108,6 +108,7 @@ $font-family-base: $font-family-sans-serif; $font-size-base: 0.8rem; $font-size-sm: 0.75rem; +$font-size-lg: 1.0rem; $h1-font-size: 1.8em; $h2-font-size: 1.5em; @@ -132,7 +133,7 @@ $nav-link-padding-y: 0.4rem; $input-border-radius: 0; $btn-border-radius: $base-border-radius; $btn-border-radius-sm: $base-border-radius; -$btn-border-radius-lg: 0; +$btn-border-radius-lg: $base-border-radius; // Block quote diff --git a/scipost/static/scipost/assets/css/_form.scss b/scipost/static/scipost/assets/css/_form.scss index 25694c465..bfd15ced6 100644 --- a/scipost/static/scipost/assets/css/_form.scss +++ b/scipost/static/scipost/assets/css/_form.scss @@ -121,3 +121,18 @@ input[type="file"] { } } } + +.formset-group { + .formset-form { + border-bottom: 1px solid #ddd; + padding: 1rem 0 0.5rem; + + &.form-1 { + padding-top: 0; + } + + &.to_be_deleted { + opacity: 0.2; + } + } +} diff --git a/scipost/static/scipost/assets/css/_tables.scss b/scipost/static/scipost/assets/css/_tables.scss index 851ef9746..8873b93c3 100644 --- a/scipost/static/scipost/assets/css/_tables.scss +++ b/scipost/static/scipost/assets/css/_tables.scss @@ -21,3 +21,10 @@ table.submission td { min-width: 150px; } } + +table.contributor-info { + td { + padding: 0 0.5rem; + vertical-align: top; + } +} diff --git a/scipost/static/scipost/formset.js b/scipost/static/scipost/formset.js new file mode 100644 index 000000000..e944b74a7 --- /dev/null +++ b/scipost/static/scipost/formset.js @@ -0,0 +1,231 @@ +/** + * Copyright (c) 2009, Stanislaus Madueke + * All rights reserved. + * + * Licensed under the New BSD License + * See: http://www.opensource.org/licenses/bsd-license.php + * + * + * Modified version specific for SciPost.org + */ +;(function($) { + $.fn.formset = function(opts) + { + var options = $.extend({}, $.fn.formset.defaults, opts), + flatExtraClasses = options.extraClasses.join(' '), + totalForms = $('#id_' + options.prefix + '-TOTAL_FORMS'), + maxForms = $('#id_' + options.prefix + '-MAX_NUM_FORMS'), + minForms = $('#id_' + options.prefix + '-MIN_NUM_FORMS'), + childElementSelector = 'input,select,textarea,label,div', + $$ = $(this), + + applyExtraClasses = function(row, ndx) { + if (options.extraClasses) { + row.removeClass(flatExtraClasses); + row.addClass(options.extraClasses[ndx % options.extraClasses.length]); + } + }, + + updateElementIndex = function(elem, prefix, ndx) { + var idRegex = new RegExp(prefix + '-(\\d+|__prefix__)-'), + replacement = prefix + '-' + ndx + '-'; + if (elem.attr("for")) elem.attr("for", elem.attr("for").replace(idRegex, replacement)); + if (elem.attr('id')) elem.attr('id', elem.attr('id').replace(idRegex, replacement)); + if (elem.attr('name')) elem.attr('name', elem.attr('name').replace(idRegex, replacement)); + }, + + hasChildElements = function(row) { + return row.find(childElementSelector).length > 0; + }, + + showAddButton = function() { + return maxForms.length == 0 || // For Django versions pre 1.2 + (maxForms.val() == '' || (maxForms.val() - totalForms.val() > 0)); + }, + + /** + * Indicates whether delete link(s) can be displayed - when total forms > min forms + */ + showDeleteLinks = function() { + return minForms.length == 0 || // For Django versions pre 1.7 + (minForms.val() == '' || (totalForms.val() - minForms.val() > 0)); + }, + + insertDeleteLink = function(row) { + var delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.'), + addCssSelector = $.trim(options.addCssClass).replace(/\s+/g, '.'); + + if (row.is('TR')) { + // If the forms are laid out in table rows, insert + // the remove button into the last table cell: + row.children(':last').append('<a class="' + options.deleteCssClass +'" href="javascript:void(0)">' + options.deleteText + ' <i class="fa fa-trash" aria-hidden="true"></i></a>'); + } else if (row.is('UL') || row.is('OL')) { + // If they're laid out as an ordered/unordered list, + // insert an <li> after the last list item: + row.append('<li><a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +' <i class="fa fa-trash" aria-hidden="true"></i></a></li>'); + } else { + // Otherwise, just insert the remove button as the + // last child element of the form's container: + row.append('<a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +' <i class="fa fa-trash" aria-hidden="true"></i></a>'); + } + // Check if we're under the minimum number of forms - not to display delete link at rendering + if (!showDeleteLinks()){ + row.find('a.' + delCssSelector).hide(); + } + + row.find('a.' + delCssSelector).click(function() { + var row = $(this).parents('.' + options.formCssClass), + del = row.find('input:hidden[id $= "-DELETE"]'), + buttonRow = row.siblings("a." + addCssSelector + ', .' + options.formCssClass + '-add'), + forms; + if (del.length) { + // We're dealing with an inline formset. + // Rather than remove this form from the DOM, we'll mark it as deleted + // and hide it, then let Django handle the deleting: + del.val('on'); + row.addClass('to_be_deleted'); + forms = $('.' + options.formCssClass).not(':hidden'); + } else { + row.remove(); + // Update the TOTAL_FORMS count: + forms = $('.' + options.formCssClass).not('.formset-custom-template'); + totalForms.val(forms.length); + } + for (var i=0, formCount=forms.length; i<formCount; i++) { + // Apply `extraClasses` to form rows so they're nicely alternating: + applyExtraClasses(forms.eq(i), i); + if (!del.length) { + // Also update names and IDs for all child controls (if this isn't + // a delete-able inline formset) so they remain in sequence: + forms.eq(i).find(childElementSelector).each(function() { + updateElementIndex($(this), options.prefix, i); + }); + } + } + // Check if we've reached the minimum number of forms - hide all delete link(s) + if (!showDeleteLinks()){ + $('a.' + delCssSelector).each(function(){$(this).hide();}); + } + // Check if we need to show the add button: + if (buttonRow.is(':hidden') && showAddButton()) buttonRow.show(); + // If a post-delete callback was provided, call it with the deleted form: + if (options.removed) options.removed(row); + return false; + }); + }; + + $$.each(function(i) { + var row = $(this), + del = row.find('input:checkbox[id $= "-DELETE"]'); + + if (del.length) { + // If you specify "can_delete = True" when creating an inline formset, + // Django adds a checkbox to each form in the formset. + // Replace the default checkbox with a hidden field: + if (del.is(':checked')) { + // If an inline formset containing deleted forms fails validation, make sure + // we keep the forms hidden (thanks for the bug report and suggested fix Mike) + row.prepend('<input type="hidden" name="' + del.attr('name') +'" id="' + del.attr('id') +'" value="on" />'); + row.addClass('to_be_deleted'); + } else { + row.prepend('<input type="hidden" name="' + del.attr('name') +'" id="' + del.attr('id') +'" />'); + } + // Remove the old Bootstap row of the form + del.parents('.form-group').remove(); + } + if (hasChildElements(row)) { + row.addClass(options.formCssClass); + if (row.is(':visible')) { + insertDeleteLink(row); + applyExtraClasses(row, i); + } + } + }); + + if ($$.length) { + var hideAddButton = !showAddButton(), + addButton, template; + if (options.formTemplate) { + // If a form template was specified, we'll clone it to generate new form instances: + template = (options.formTemplate instanceof $) ? options.formTemplate : $(options.formTemplate); + template.removeAttr('id').addClass(options.formCssClass + ' formset-custom-template'); + template.find(childElementSelector).each(function() { + updateElementIndex($(this), options.prefix, '__prefix__'); + }); + insertDeleteLink(template); + } else { + // Otherwise, use the last form in the formset; this works much better if you've got + // extra (>= 1) forms (thnaks to justhamade for pointing this out): + template = $('.' + options.formCssClass + ':last').clone(true).removeAttr('id'); + template.find('input:hidden[id $= "-DELETE"]').remove(); + // Clear all cloned fields, except those the user wants to keep (thanks to brunogola for the suggestion): + template.find(childElementSelector).not(options.keepFieldValues).each(function() { + var elem = $(this); + // If this is a checkbox or radiobutton, uncheck it. + // This fixes Issue 1, reported by Wilson.Andrew.J: + if (elem.is('input:checkbox') || elem.is('input:radio')) { + elem.attr('checked', false); + } else { + elem.val(''); + } + }); + } + // FIXME: Perhaps using $.data would be a better idea? + options.formTemplate = template; + + if ($$.is('TR')) { + // If forms are laid out as table rows, insert the + // "add" button in a new table row: + var numCols = $$.eq(0).children().length, // This is a bit of an assumption :| + buttonRow = $('<tr><td colspan="' + numCols + '"><a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + ' <i class="fa fa-plus" aria-hidden="true"></i></a></tr>') + .addClass(options.formCssClass + '-add'); + $$.parent().append(buttonRow); + if (hideAddButton) buttonRow.hide(); + addButton = buttonRow.find('a'); + } else { + // Otherwise, insert it immediately after the last form: + $$.filter(':last').after('<div class="form-group row pt-2"><div class="col-12"><a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + ' <i class="fa fa-plus" aria-hidden="true"></i></a></div></div>'); + addButton = $$.filter(':last').next(); + if (hideAddButton) addButton.hide(); + } + addButton.click(function() { + var formCount = parseInt(totalForms.val()), + row = options.formTemplate.clone(true).removeClass('formset-custom-template'), + buttonRow = $($(this).parents('tr.' + options.formCssClass + '-add').get(0) || this), + delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.'); + applyExtraClasses(row, formCount); + row.insertBefore(buttonRow).show(); + row.find(childElementSelector).each(function() { + updateElementIndex($(this), options.prefix, formCount); + }); + totalForms.val(formCount + 1); + // Check if we're above the minimum allowed number of forms -> show all delete link(s) + if (showDeleteLinks()){ + $('a.' + delCssSelector).each(function(){$(this).show();}); + } + // Check if we've exceeded the maximum allowed number of forms: + if (!showAddButton()) buttonRow.hide(); + // If a post-add callback was supplied, call it with the added form: + if (options.added) options.added(row); + return false; + }); + } + + return $$; + }; + + /* Setup plugin defaults */ + $.fn.formset.defaults = { + prefix: 'form', // The form prefix for your django formset + formTemplate: null, // The jQuery selection cloned to generate new form instances + addText: 'add another', // Text for the add link + deleteText: 'remove', // Text for the delete link + addCssClass: 'add-row', // CSS class applied to the add link + deleteCssClass: 'delete-row', // CSS class applied to the delete link + formCssClass: 'dynamic-form', // CSS class applied to each form in a formset + extraClasses: [], // Additional CSS classes, which will be applied to each form in turn + keepFieldValues: '', // jQuery selector for fields whose values should be kept when the form is cloned + added: null, // Function called each time a new form is added + removed: null // Function called each time a form is deleted + }; +})(jQuery); diff --git a/scipost/templates/scipost/_private_info_as_table.html b/scipost/templates/scipost/_private_info_as_table.html index e8fc45a52..9eb68274e 100644 --- a/scipost/templates/scipost/_private_info_as_table.html +++ b/scipost/templates/scipost/_private_info_as_table.html @@ -1,13 +1,21 @@ -<table> - <tr><td>Title: </td><td> </td><td>{{ contributor.get_title_display }}</td></tr> - <tr><td>First name: </td><td> </td><td>{{ contributor.user.first_name }}</td></tr> - <tr><td>Last name: </td><td> </td><td>{{ contributor.user.last_name }}</td></tr> - <tr><td>Email: </td><td> </td><td>{{ contributor.user.email }}</td></tr> - <tr><td>ORCID id: </td><td> </td><td>{{ contributor.orcid_id }}</td></tr> - <tr><td>Country of employment: </td><td> </td> - <td>{{ contributor.affiliation.get_country_display }}</td></tr> - <tr><td>Affiliation: </td><td> </td><td>{{ contributor.affiliation.name }}</td></tr> - <tr><td>Address: </td><td> </td><td>{{ contributor.address }}</td></tr> - <tr><td>Personal web page: </td><td> </td><td>{{ contributor.personalwebpage }}</td></tr> - <tr><td>Accept SciPost emails: </td><td> </td><td>{{ contributor.accepts_SciPost_emails }}</td></tr> +<table class="contributor-info"> + <tr><td>Title:</td><td>{{ contributor.get_title_display }}</td></tr> + <tr><td>First name:</td><td>{{ contributor.user.first_name }}</td></tr> + <tr><td>Last name: </td><td>{{ contributor.user.last_name }}</td></tr> + <tr><td>Email: </td><td>{{ contributor.user.email }}</td></tr> + <tr><td>ORCID id: </td><td>{{ contributor.orcid_id }}</td></tr> + <tr> + <td>Affiliation(s):</td> + <td> + {% for affiliation in contributor.affiliations.active %} + {% if not forloop.first %} + <br> + {% endif %} + {{ affiliation.institute }} + {% endfor %} + </td> + </tr> + <tr><td>Address: </td><td>{{ contributor.address }}</td></tr> + <tr><td>Personal web page: </td><td>{{ contributor.personalwebpage }}</td></tr> + <tr><td>Accept SciPost emails: </td><td>{{ contributor.accepts_SciPost_emails }}</td></tr> </table> diff --git a/scipost/templates/scipost/_public_info_as_table.html b/scipost/templates/scipost/_public_info_as_table.html index 0b141e6b2..6e80fb2c5 100644 --- a/scipost/templates/scipost/_public_info_as_table.html +++ b/scipost/templates/scipost/_public_info_as_table.html @@ -1,9 +1,18 @@ -<table> - <tr><td>Title: </td><td> </td><td>{{ contributor.get_title_display }}</td></tr> - <tr><td>First name: </td><td> </td><td>{{ contributor.user.first_name }}</td></tr> - <tr><td>Last name: </td><td> </td><td>{{ contributor.user.last_name }}</td></tr> - <tr><td>ORCID id: </td><td> </td><td>{{ contributor.orcid_id|default:'-' }}</td></tr> - <tr><td>Country of employment: </td><td> </td><td>{{ contributor.affiliation.get_country_display|default:'-'}}</td></tr> - <tr><td>Affiliation: </td><td> </td><td>{{ contributor.affiliation.name|default:'-' }}</td></tr> - <tr><td>Personal web page: </td><td> </td><td>{{ contributor.personalwebpage|default:'-' }}</td></tr> +<table class="contributor-info"> + <tr><td>Title: </td><td>{{ contributor.get_title_display }}</td></tr> + <tr><td>First name: </td><td>{{ contributor.user.first_name }}</td></tr> + <tr><td>Last name: </td><td>{{ contributor.user.last_name }}</td></tr> + <tr><td>ORCID id: </td><td>{{ contributor.orcid_id|default:'-' }}</td></tr> + <tr> + <td>Affiliation(s):</td> + <td> + {% for affiliation in contributor.affiliations.active %} + {% if not forloop.first %} + <br> + {% endif %} + {{ affiliation.institute }} + {% endfor %} + </td> + </tr> + <tr><td>Personal web page: </td><td>{{ contributor.personalwebpage|default:'-' }}</td></tr> </table> diff --git a/scipost/templates/scipost/contributor_info.html b/scipost/templates/scipost/contributor_info.html index 7ee2c8988..26423a950 100644 --- a/scipost/templates/scipost/contributor_info.html +++ b/scipost/templates/scipost/contributor_info.html @@ -4,22 +4,13 @@ {% block content %} -<div class="row"> - <div class="col-12"> - <div class="card card-grey"> - <div class="card-body"> - <h1 class="card-title mb-0">Contributor info</h1> - <h3 class="card-subtitle text-muted">{{contributor.get_formal_display}}</h2> - </div> - </div> - </div> -</div> +<h1 class="highlight mb-4">Contributor info: {{ contributor.get_formal_display }}</h1> {% include "scipost/_public_info_as_table.html" with contributor=contributor %} <br> {% if contributor_publications %} - {# <hr>#} + <div class="row"> <div class="col-12"> <h2 class="highlight">Publications <small><a href="javascript:;" class="ml-2" data-toggle="toggle" data-target="#mypublicationslist">View/hide publications</a></small></h2> diff --git a/scipost/templates/scipost/update_personal_data.html b/scipost/templates/scipost/update_personal_data.html index ab693489e..42c0a5b38 100644 --- a/scipost/templates/scipost/update_personal_data.html +++ b/scipost/templates/scipost/update_personal_data.html @@ -15,75 +15,28 @@ <script> $(document).ready(function(){ + $('select#id_discipline').on('change', function() { + var selection = $(this).val(); + $("ul[id^='id_expertises_']").closest("li").hide(); - 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; - } - }); + switch(selection){ + case "physics": + $("#id_expertises_0").closest("li").show(); + break; + case "astrophysics": + $("#id_expertises_1").closest("li").show(); + break; + case "mathematics": + $("#id_expertises_2").closest("li").show(); + break; + case "computerscience": + $("#id_expertises_3").closest("li").show(); + break; + default: + $("ul[id^='id_expertises_']").closest("li").show(); + break; + } + }).trigger('change'); }); @@ -91,18 +44,43 @@ {% endif %} -<div class="row justify-content-center"> - <div class="col-lg-10"> - <h1 class="highlight">Update your personal data</h1> - <form action="{% url 'scipost:update_personal_data' %}" method="post"> - {% csrf_token %} - {{user_form|bootstrap}} + +<form action="{% url 'scipost:update_personal_data' %}" method="post"> + {% csrf_token %} + <div class="row justify-content-center"> + <div class="col-lg-6"> + <h1 class="mb-3">Update your personal data</h1> + {{ user_form|bootstrap }} {% if cont_form %} - {{cont_form|bootstrap}} + {{ cont_form|bootstrap }} {% endif %} - <input type="submit" class="btn btn-primary" value="Update" /> - </form> + </div> + {% if institute_formset %} + <div class="col-lg-6"> + <div id="institutes" class="formset-group"> + <h1 class="mb-3">Your Institutes</h1> + {{ institute_formset.media }} + {{ institute_formset|bootstrap }} + </div> + <div class="formset-form form-empty" style="display: none;"> + {{ institute_formset.empty_form|bootstrap }} + </div> + </div> + {% endif %} + </div> + + <div class="text-center"> + <input type="submit" class="btn btn-primary btn-lg px-3" value="Save changes" /> </div> -</div> +</form> +<script type="text/javascript"> + $(function() { + $('form #institutes > .formset-form').formset({ + addText: 'add new Institute', + deleteText: 'remove Institute', + formTemplate: 'form .form-empty', + }) + }) +</script> {% endblock content %} diff --git a/scipost/templates/tags/bootstrap/formset.html b/scipost/templates/tags/bootstrap/formset.html index eb5722f81..0f323b43a 100644 --- a/scipost/templates/tags/bootstrap/formset.html +++ b/scipost/templates/tags/bootstrap/formset.html @@ -1,11 +1,13 @@ {{ formset.management_form }} {% for form in formset %} - {% if classes.label == 'sr-only' %} - <div class="form-inline"> - {% include "tags/bootstrap/form.html" with form=form %} - </div> - {%else%} - {% include "tags/bootstrap/form.html" with form=form %} - {% endif %} + <div class="formset-form form-{{ forloop.counter }}"> + {% if classes.label == 'sr-only' %} + <div class="form-inline"> + {% include "tags/bootstrap/form.html" with form=form formset=formset %} + </div> + {% else %} + {% include "tags/bootstrap/form.html" with form=form formset=formset %} + {% endif %} + </div> {% endfor %} diff --git a/scipost/views.py b/scipost/views.py index 9b222806a..e122018ec 100644 --- a/scipost/views.py +++ b/scipost/views.py @@ -35,6 +35,7 @@ from .forms import AuthenticationForm, DraftInvitationForm, UnavailabilityPeriod EmailGroupMembersForm, EmailParticularForm, SendPrecookedEmailForm from .utils import Utils, EMAIL_FOOTER, SCIPOST_SUMMARY_FOOTER, SCIPOST_SUMMARY_FOOTER_HTML +from affiliations.forms import AffiliationsFormset from commentaries.models import Commentary from comments.models import Comment from journals.models import Publication, Journal @@ -880,7 +881,7 @@ def _update_personal_data_user_only(request): if user_form.is_valid(): user_form.save() messages.success(request, 'Your personal data has been updated.') - return redirect(reverse('partners:dashboard')) + return redirect(reverse('scipost:update_personal_data')) context = { 'user_form': user_form } @@ -891,19 +892,26 @@ 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) - if user_form.is_valid() and cont_form.is_valid(): + institute_formset = AffiliationsFormset(request.POST or None, contributor=contributor) + if user_form.is_valid() and cont_form.is_valid() and institute_formset.is_valid(): user_form.save() cont_form.save() cont_form.sync_lists() + institute_formset.save() if 'orcid_id' in cont_form.changed_data: cont_form.propagate_orcid() messages.success(request, 'Your personal data has been updated.') - return redirect(reverse('scipost:personal_page')) + return redirect(reverse('scipost:update_personal_data')) else: user_form = UpdateUserDataForm(instance=contributor.user) cont_form = UpdatePersonalDataForm(instance=contributor) - return render(request, 'scipost/update_personal_data.html', - {'user_form': user_form, 'cont_form': cont_form}) + + context = { + 'user_form': user_form, + 'cont_form': cont_form, + 'institute_formset': institute_formset, + } + return render(request, 'scipost/update_personal_data.html', context) @login_required -- GitLab