diff --git a/SciPost_v1/settings/base.py b/SciPost_v1/settings/base.py index 44532bc63c162b25e9049c152ba72e079bfeee4a..5a5468d6df9aa34e0204a18d1029a0e75d713f41 100644 --- a/SciPost_v1/settings/base.py +++ b/SciPost_v1/settings/base.py @@ -84,6 +84,7 @@ INSTALLED_APPS = ( 'haystack', 'rest_framework', 'sphinxdoc', + 'affiliations', 'colleges', 'commentaries', 'comments', diff --git a/SciPost_v1/urls.py b/SciPost_v1/urls.py index 5e06ce57cc97bd5caf2b866a23a16bbb4ccaf333..5122b6590b62f946fcac51070dbc94fff95a3b50 100644 --- a/SciPost_v1/urls.py +++ b/SciPost_v1/urls.py @@ -29,6 +29,7 @@ urlpatterns = [ url(r'^10.21468/%s/' % JOURNAL_REGEX, include('journals.urls.journal', namespace="journal")), url(r'^%s/' % JOURNAL_REGEX, include('journals.urls.journal', namespace="_journal")), url(r'^', include('scipost.urls', namespace="scipost")), + url(r'^affiliations/', include('affiliations.urls', namespace="affiliations")), url(r'^colleges/', include('colleges.urls', namespace="colleges")), url(r'^commentaries/', include('commentaries.urls', namespace="commentaries")), url(r'^commentary/', include('commentaries.urls', namespace="_commentaries")), diff --git a/affiliations/__init__.py b/affiliations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..82323b3dfc6109a67eb86b3f5207b5736e368fa2 --- /dev/null +++ b/affiliations/__init__.py @@ -0,0 +1 @@ +default_app_config = 'affiliations.apps.AffiliationsConfig' diff --git a/affiliations/admin.py b/affiliations/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..c5caa196a71f9055181c39f64ce325781942d563 --- /dev/null +++ b/affiliations/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin + +from .models import Affiliation, Institute + + +admin.site.register(Affiliation) +admin.site.register(Institute) diff --git a/affiliations/apps.py b/affiliations/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..f0e52fdbca89da2fb6af3a3f746f97a019459e75 --- /dev/null +++ b/affiliations/apps.py @@ -0,0 +1,13 @@ +from django.apps import AppConfig +from django.db.models.signals import post_save + + +class AffiliationsConfig(AppConfig): + name = 'affiliations' + + def ready(self): + super().ready() + + from . import models, signals + post_save.connect(signals.notify_new_affiliation, + sender=models.Institute) diff --git a/affiliations/constants.py b/affiliations/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..d1da522fd4d6cc9dad61d159d840fb114ef05d7a --- /dev/null +++ b/affiliations/constants.py @@ -0,0 +1,4 @@ +TYPE_UNIVERSITY = 'university' +INSTITUTE_TYPES = ( + (TYPE_UNIVERSITY, 'University'), + ) diff --git a/affiliations/forms.py b/affiliations/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..c475964a874231ba9841e819d6255f47e60f68cc --- /dev/null +++ b/affiliations/forms.py @@ -0,0 +1,115 @@ +from django import forms +from django.forms import BaseModelFormSet, modelformset_factory +# from django.db.models import F + +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): + institute = forms.ModelChoiceField(queryset=Institute.objects.none()) + + class Meta: + model = Institute + fields = [] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['institute'].queryset = Institute.objects.exclude(id=self.instance.id) + + def save(self, commit=True): + old_institute = self.cleaned_data['institute'] + if commit: + 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 0000000000000000000000000000000000000000..1cc7840aedaba9741dbf02be686327f3967dcbaa --- /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/0001_initial.py b/affiliations/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..c6f23fbf3012655430243cadf43e90b45ba37f55 --- /dev/null +++ b/affiliations/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-01 19:00 +from __future__ import unicode_literals + +from django.db import migrations, models +import django_countries.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Institute', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('country', django_countries.fields.CountryField(max_length=2)), + ('type', models.CharField(choices=[('university', 'University')], default='university', max_length=16)), + ], + ), + ] diff --git a/affiliations/migrations/0002_affiliation_acronym.py b/affiliations/migrations/0002_affiliation_acronym.py new file mode 100644 index 0000000000000000000000000000000000000000..01d4fb2775e7a720a601742f3cbbec47857629af --- /dev/null +++ b/affiliations/migrations/0002_affiliation_acronym.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-01 19:06 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('affiliations', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='institute', + name='acronym', + field=models.CharField(blank=True, max_length=16), + ), + ] diff --git a/affiliations/migrations/0003_auto_20171101_2022.py b/affiliations/migrations/0003_auto_20171101_2022.py new file mode 100644 index 0000000000000000000000000000000000000000..65eda86a2566d86f96ad9d7cadda678b4c865b39 --- /dev/null +++ b/affiliations/migrations/0003_auto_20171101_2022.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-01 19:22 +from __future__ import unicode_literals + +from django.db import migrations + + +def fill_institutes(apps, schema_editor): + Contributor = apps.get_model('scipost', 'Contributor') + Institute = apps.get_model('affiliations', 'Institute') + for contributor in Contributor.objects.all(): + affiliation, __ = Institute.objects.get_or_create( + name=contributor.affiliation, country=contributor.country_of_employment) + contributor._affiliation = affiliation + contributor.save() + + +def return_none(*args, **kwargs): + return + + +class Migration(migrations.Migration): + + dependencies = [ + ('affiliations', '0002_affiliation_acronym'), + ('scipost', '0066_contributor__affiliation') + ] + + operations = [ + migrations.RunPython(fill_institutes, return_none), + ] diff --git a/affiliations/migrations/0004_auto_20171101_2208.py b/affiliations/migrations/0004_auto_20171101_2208.py new file mode 100644 index 0000000000000000000000000000000000000000..be2b86fd29e02e05d65e33b75b6c5b1672f57ef9 --- /dev/null +++ b/affiliations/migrations/0004_auto_20171101_2208.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-01 21:08 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('affiliations', '0003_auto_20171101_2022'), + ] + + operations = [ + migrations.AlterModelOptions( + name='institute', + options={'ordering': ['country']}, + ), + ] diff --git a/affiliations/migrations/0005_affiliation.py b/affiliations/migrations/0005_affiliation.py new file mode 100644 index 0000000000000000000000000000000000000000..7d4a84394dd3b11b752ff9295448beae27837c0a --- /dev/null +++ b/affiliations/migrations/0005_affiliation.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-02 07:41 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0069_auto_20171102_0840'), + ('affiliations', '0004_auto_20171101_2208'), + ] + + operations = [ + migrations.CreateModel( + name='Affiliation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('begin_date', models.DateField(blank=True, null=True)), + ('end_date', models.DateField(blank=True, null=True)), + ('contributor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='affiliations', to='scipost.Contributor')), + ('institute', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='affiliations', to='affiliations.Institute')), + ], + options={ + 'default_related_name': 'affiliations', + }, + ), + ] diff --git a/affiliations/migrations/0006_auto_20171102_0843.py b/affiliations/migrations/0006_auto_20171102_0843.py new file mode 100644 index 0000000000000000000000000000000000000000..bf8745a366ed9df5067bb99ca24254415596790f --- /dev/null +++ b/affiliations/migrations/0006_auto_20171102_0843.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-02 07:43 +from __future__ import unicode_literals + +from django.db import migrations + + +def fill_affiliations(apps, schema_editor): + Contributor = apps.get_model('scipost', 'Contributor') + Affiliation = apps.get_model('affiliations', 'Affiliation') + for contributor in Contributor.objects.all(): + Affiliation.objects.get_or_create( + institute=contributor.old_affiliation_fk, contributor=contributor) + + +def return_none(*args, **kwargs): + return + + +class Migration(migrations.Migration): + + dependencies = [ + ('affiliations', '0005_affiliation'), + ] + + operations = [ + migrations.RunPython(fill_affiliations, return_none), + ] diff --git a/affiliations/migrations/0007_auto_20171102_1256.py b/affiliations/migrations/0007_auto_20171102_1256.py new file mode 100644 index 0000000000000000000000000000000000000000..0f044ab100678bee7a9753772f7b68506dbacb60 --- /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/migrations/__init__.py b/affiliations/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/affiliations/models.py b/affiliations/models.py new file mode 100644 index 0000000000000000000000000000000000000000..fddbf443c2a86de9f5703abc3d4d026aab3edc98 --- /dev/null +++ b/affiliations/models.py @@ -0,0 +1,48 @@ +from django.db import models +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): + """ + Any (scientific) Institute in the world should ideally have a SciPost registration. + """ + name = models.CharField(max_length=255) + acronym = models.CharField(max_length=16, blank=True) + country = CountryField() + type = models.CharField(max_length=16, choices=INSTITUTE_TYPES, default=TYPE_UNIVERSITY) + + class Meta: + default_related_name = 'institutes' + ordering = ['country'] + + def __str__(self): + return '{name} ({country})'.format(name=self.name, country=self.get_country_display()) + + def get_absolute_url(self): + return reverse('affiliations:institute_details', args=(self.id,)) + + +class Affiliation(models.Model): + """ + An Affiliation is a (time dependent) connection between an Institute and a Contributor. + This could thus be changed over time and history will be preserved. + """ + institute = models.ForeignKey('affiliations.Institute') + contributor = models.ForeignKey('scipost.Contributor') + 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( + contributor=self.contributor, institute=self.institute.name) diff --git a/affiliations/signals.py b/affiliations/signals.py new file mode 100644 index 0000000000000000000000000000000000000000..2d583e8df78604bb2c2cef5cdb9d3c7737db6902 --- /dev/null +++ b/affiliations/signals.py @@ -0,0 +1,17 @@ +from django.contrib.auth.models import User + +from notifications.models import FakeActors +from notifications.signals import notify + + +def notify_new_affiliation(sender, instance, created, **kwargs): + """ + Notify the SciPost Administration about a new Affiliation created to check it. + """ + if created: + administrators = User.objects.filter(groups__name='SciPost Administrators') + actor, __ = FakeActors.objects.get_or_create(name='A SciPost user') + for user in administrators: + notify.send(sender=sender, recipient=user, actor=actor, + verb=' created a new Institute instance. You may want to validate it.', + target=instance) diff --git a/affiliations/templates/affiliations/institute_form.html b/affiliations/templates/affiliations/institute_form.html new file mode 100644 index 0000000000000000000000000000000000000000..57d68b57f9c1b361ceb937b8790d1fec092da1fe --- /dev/null +++ b/affiliations/templates/affiliations/institute_form.html @@ -0,0 +1,55 @@ +{% extends 'scipost/_personal_page_base.html' %} + +{% load bootstrap %} + +{% block pagetitle %}: Institutes details{% endblock pagetitle %} + +{% block breadcrumb_items %} + {{ block.super }} + <a href="{% url 'affiliations:institutes' %}" class="breadcrumb-item">Institutes</a> + <span class="breadcrumb-item">Institute detail</span> +{% endblock %} + +{% block content %} + +<h1>Institute {{ institute }}</h1> + +<form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input class="btn btn-primary" type="submit" value="Update Institute"> +</form> + +<br> +<a href="javascript:;" data-toggle="toggle" data-target="#merging">Merge another institute into {{ institute }}?</a> +<div id="merging" style="display: none;"> + <h3>Merge institutes</h3> + <div class="card border-danger"> + <div class="card-body d-flex flex-row"> + <div class="p-2"> + <i class="fa fa-2x fa-exclamation-triangle text-warning" aria-hidden="true"></i> + </div> + <div class="px-2"> + You better be sure what you are doing, this is a one-way street. This will merge the chosen institute into {{ institute }}. The fields of the chosen institute <strong>will be lost</strong> however. + </div> + + </div> + </div> + <br> + <form action="{% url 'affiliations:merge_institutes' institute.id %}" method="post"> + {% csrf_token %} + {{ merge_form|bootstrap }} + <input class="btn btn-primary" type="submit" value="Merge institutes" onclick="return confirm('Are you sure this is what you want?')"> + </form> +</div> + +<br> +<br> +<h3>Contributors of {{ institute }}</h3> +<ul> + {% for contributor in institute.contributors.all %} + <li>{{ contributor }}</li> + {% endfor %} +</ul> + +{% endblock content %} diff --git a/affiliations/templates/affiliations/institute_list.html b/affiliations/templates/affiliations/institute_list.html new file mode 100644 index 0000000000000000000000000000000000000000..c40b5a8c9f9e50339f5f07e1e228810cf148d711 --- /dev/null +++ b/affiliations/templates/affiliations/institute_list.html @@ -0,0 +1,30 @@ +{% extends 'scipost/_personal_page_base.html' %} + + +{% block pagetitle %}: Institutes{% endblock pagetitle %} + +{% block breadcrumb_items %} + {{ block.super }} + <span class="breadcrumb-item">Institutes</span> +{% endblock %} + +{% block content %} + +<h1>All Institutes in the database</h1> + +{% if is_paginated %} + {% include 'partials/pagination.html' with page_obj=page_obj %} +{% endif %} +<ul> + {% for institute in object_list %} + <li><a href="{% url 'affiliations:institute_details' institute.id %}">{{ institute }}</a></li> + {% empty %} + <li><em>There are no Institutes known yet.</em><li> + {% endfor %} +</ul> +{% if is_paginated %} + {% include 'partials/pagination.html' with page_obj=page_obj %} +{% endif %} + + +{% endblock content %} diff --git a/affiliations/tests.py b/affiliations/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/affiliations/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/affiliations/urls.py b/affiliations/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..6cc8b42ba3ce945719a8b6ca27bfd52eb9f2da1a --- /dev/null +++ b/affiliations/urls.py @@ -0,0 +1,11 @@ +from django.conf.urls import url + +from . import views + +urlpatterns = [ + url(r'^$', views.InstituteListView.as_view(), name='institutes'), + url(r'^(?P<institute_id>[0-9]+)/$', views.InstituteUpdateView.as_view(), + name='institute_details'), + url(r'^(?P<institute_id>[0-9]+)/merge$', views.merge_institutes, + name='merge_institutes'), +] diff --git a/affiliations/views.py b/affiliations/views.py new file mode 100644 index 0000000000000000000000000000000000000000..dd028053c37ba0fb1c8e77d1b463ad6a16155854 --- /dev/null +++ b/affiliations/views.py @@ -0,0 +1,52 @@ +from django.shortcuts import redirect +from django.contrib import messages +from django.contrib.auth.decorators import permission_required +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.views.generic.edit import UpdateView +from django.views.generic.list import ListView +from django.shortcuts import get_object_or_404 + +from .forms import InstituteMergeForm +from .models import Institute + + +@method_decorator(permission_required('scipost.can_manage_affiliations'), name='dispatch') +class InstituteListView(ListView): + model = Institute + paginate_by = 100 + + +@method_decorator(permission_required('scipost.can_manage_affiliations'), name='dispatch') +class InstituteUpdateView(UpdateView): + model = Institute + pk_url_kwarg = 'institute_id' + fields = [ + 'name', + 'acronym', + 'country', + ] + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context['merge_form'] = InstituteMergeForm() + return context + + def form_valid(self, *args, **kwargs): + messages.success(self.request, 'Institute saved') + return super().form_valid(*args, **kwargs) + + +@permission_required('scipost.can_manage_affiliations') +def merge_institutes(request, institute_id): + """ + Merge Affiliation (affiliation_id) into the Affliation chosen in the form. + """ + institute = get_object_or_404(Institute, id=institute_id) + form = InstituteMergeForm(request.POST or None, instance=institute) + if form.is_valid(): + form.save() + messages.success(request, 'Institute {a} merged into {b}'.format( + a=form.cleaned_data.get('institute', '?'), b=institute)) + + return redirect(reverse('affiliations:institute_details', args=(institute.id,))) diff --git a/common/widgets.py b/common/widgets.py new file mode 100644 index 0000000000000000000000000000000000000000..faa904bb9279dc95b3b5e9f1cfd8f0a85b51a28f --- /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/funders/views.py b/funders/views.py index 8a2593f66dbad45d914f89e3efd0150c15f8749d..1b3d3b8c20ab40e39a335be12f57d250615c24f6 100644 --- a/funders/views.py +++ b/funders/views.py @@ -10,7 +10,7 @@ from .models import Funder, Grant from .forms import FunderRegistrySearchForm, FunderForm, GrantForm -@permission_required('scipost.can_publish_accepted_submission', raise_exception=True) +@permission_required('scipost.can_view_all_funding_info', raise_exception=True) def funders(request): funders = Funder.objects.all() form = FunderRegistrySearchForm() @@ -21,7 +21,7 @@ def funders(request): return render(request, 'funders/funders.html', context) -@permission_required('scipost.can_publish_accepted_submission', raise_exception=True) +@permission_required('scipost.can_view_all_funding_info', raise_exception=True) def query_crossref_for_funder(request): """ Checks Crossref's Fundref Registry for an entry @@ -41,7 +41,7 @@ def query_crossref_for_funder(request): return render(request, 'funders/query_crossref_for_funder.html', context) -@permission_required('scipost.can_publish_accepted_submission', raise_exception=True) +@permission_required('scipost.can_view_all_funding_info', raise_exception=True) def add_funder(request): form = FunderForm(request.POST or None) if form.is_valid(): @@ -63,7 +63,7 @@ def funder_publications(request, funder_id): return render(request, 'funders/funder_details.html', context) -@permission_required('scipost.can_publish_accepted_submission', raise_exception=True) +@permission_required('scipost.can_view_all_funding_info', raise_exception=True) def add_grant(request): grant_form = GrantForm(request.POST or None) if grant_form.is_valid(): diff --git a/journals/migrations/0049_auto_20171101_2000.py b/journals/migrations/0049_auto_20171101_2000.py new file mode 100644 index 0000000000000000000000000000000000000000..d16635f0b7243a049ac2bf60984c551937bf31c8 --- /dev/null +++ b/journals/migrations/0049_auto_20171101_2000.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-01 19:00 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0048_auto_20171101_1028'), + ] + + operations = [ + migrations.AlterField( + model_name='publication', + name='funders_generic', + field=models.ManyToManyField(blank=True, related_name='publications', to='funders.Funder'), + ), + migrations.AlterField( + model_name='publication', + name='grants', + field=models.ManyToManyField(blank=True, related_name='publications', to='funders.Grant'), + ), + ] diff --git a/journals/migrations/0050_publication_institutes.py b/journals/migrations/0050_publication_institutes.py new file mode 100644 index 0000000000000000000000000000000000000000..71f4c2adfd91e8d086d6d8d5df34f5226ab7d015 --- /dev/null +++ b/journals/migrations/0050_publication_institutes.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-02 12:05 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('affiliations', '0007_auto_20171102_1256'), + ('journals', '0049_auto_20171101_2000'), + ] + + operations = [ + migrations.AddField( + model_name='publication', + name='institutes', + field=models.ManyToManyField(blank=True, related_name='publications', to='affiliations.Institute'), + ), + ] diff --git a/journals/migrations/0051_auto_20171102_1307.py b/journals/migrations/0051_auto_20171102_1307.py new file mode 100644 index 0000000000000000000000000000000000000000..aa16b353181771fa0bd88287991f6ec99322d596 --- /dev/null +++ b/journals/migrations/0051_auto_20171102_1307.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-02 12:07 +from __future__ import unicode_literals + +from django.db import migrations + + +def fill_publications(apps, schema_editor): + """ + Add all Institutes to a Publication, assuming all Contributors Affiliations are + active at moment of publication. + """ + Publication = apps.get_model('journals', 'Publication') + for publication in Publication.objects.all(): + for author in publication.authors.all(): + for affiliation in author.affiliations.all(): + publication.institutes.add(affiliation.institute) + + +def return_none(*args, **kwargs): + return + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0050_publication_institutes'), + ] + + operations = [ + migrations.RunPython(fill_publications, return_none), + ] diff --git a/journals/models.py b/journals/models.py index f32b6386af8fc7c9c2e0241af54f67bd41711888..a7ac541c86f2cf72f6150e39ac764d264132dcf0 100644 --- a/journals/models.py +++ b/journals/models.py @@ -123,20 +123,28 @@ class Issue(models.Model): class Publication(models.Model): + """ + A Publication is an object directly related to an accepted Submission. It contains metadata, + the actual publication file, author data, etc. etc. + """ + # Publication data accepted_submission = models.OneToOneField('submissions.Submission', on_delete=models.CASCADE) in_issue = models.ForeignKey(Issue, on_delete=models.CASCADE) paper_nr = models.PositiveSmallIntegerField() + + # Core fields + title = models.CharField(max_length=300) + author_list = models.CharField(max_length=1000, verbose_name="author list") + abstract = models.TextField() + pdf_file = models.FileField(upload_to='UPLOADS/PUBLICATIONS/%Y/%m/', max_length=200) discipline = models.CharField(max_length=20, choices=SCIPOST_DISCIPLINES, default='physics') domain = models.CharField(max_length=3, choices=SCIPOST_JOURNALS_DOMAINS) subject_area = models.CharField(max_length=10, choices=SCIPOST_SUBJECT_AREAS, verbose_name='Primary subject area', default='Phys:QP') - secondary_areas = ChoiceArrayField(models.CharField(max_length=10, - choices=SCIPOST_SUBJECT_AREAS), - blank=True, null=True) - title = models.CharField(max_length=300) - author_list = models.CharField(max_length=1000, verbose_name="author list") + secondary_areas = ChoiceArrayField( + models.CharField(max_length=10, choices=SCIPOST_SUBJECT_AREAS), blank=True, null=True) - # Authors which have been mapped to contributors: + # Authors authors = models.ManyToManyField('scipost.Contributor', blank=True, related_name='publications') authors_unregistered = models.ManyToManyField(UnregisteredAuthor, blank=True, @@ -151,26 +159,33 @@ class Publication(models.Model): related_name='claimed_publications') authors_false_claims = models.ManyToManyField('scipost.Contributor', blank=True, related_name='false_claimed_publications') - abstract = models.TextField() - pdf_file = models.FileField(upload_to='UPLOADS/PUBLICATIONS/%Y/%m/', max_length=200) + cc_license = models.CharField(max_length=32, choices=CC_LICENSES, default=CCBY4) + + # Funders grants = models.ManyToManyField('funders.Grant', blank=True, related_name="publications") - funders_generic = models.ManyToManyField('funders.Funder', blank=True, - related_name="publications") # not linked to a grant + funders_generic = models.ManyToManyField( + 'funders.Funder', blank=True, related_name="publications") # not linked to a grant + institutes = models.ManyToManyField('affiliations.Institute', + blank=True, related_name="publications") + + # Metadata metadata = JSONField(default={}, blank=True, null=True) metadata_xml = models.TextField(blank=True, null=True) # for Crossref deposit - latest_metadata_update = models.DateTimeField(blank=True, null=True) metadata_DOAJ = JSONField(default={}, blank=True, null=True) - BiBTeX_entry = models.TextField(blank=True, null=True) doi_label = models.CharField(max_length=200, unique=True, db_index=True, validators=[doi_publication_validator]) + BiBTeX_entry = models.TextField(blank=True, null=True) doideposit_needs_updating = models.BooleanField(default=False) + citedby = JSONField(default={}, blank=True, null=True) + + # Date fields submission_date = models.DateField(verbose_name='submission date') acceptance_date = models.DateField(verbose_name='acceptance date') publication_date = models.DateField(verbose_name='publication date') latest_citedby_update = models.DateTimeField(null=True, blank=True) + latest_metadata_update = models.DateTimeField(blank=True, null=True) latest_activity = models.DateTimeField(default=timezone.now) - citedby = JSONField(default={}, blank=True, null=True) objects = PublicationManager() diff --git a/journals/templates/journals/publication_detail.html b/journals/templates/journals/publication_detail.html index a3f4acd26f431be4c3c53f37d2a446c3bb910aa9..64a0039f85cbb602a1f8da5a5053912a8c8357ea 100644 --- a/journals/templates/journals/publication_detail.html +++ b/journals/templates/journals/publication_detail.html @@ -3,8 +3,9 @@ {% load journals_extras %} {% load staticfiles %} {% load scipost_extras %} +{% load user_groups %} -{% block pagetitle %}: Publication detail{% endblock pagetitle %} +{% block pagetitle %}: {{ publication.citation }} - {{ publication.title }}{% endblock pagetitle %} {% block breadcrumb_items %} {{block.super}} @@ -46,6 +47,7 @@ {% endblock headsup %} {% block content %} + {% is_edcol_admin request.user as is_edcol_admin %} {% include 'journals/_publication_details.html' with publication=publication %} @@ -85,10 +87,20 @@ {{ author }} {% if not forloop.last %} · {% endif %} {% endfor %} </p> + + {% if is_edcol_admin %} + {# This function is not available for public yet! #} + <h3>Institutes related to this Publication: <small>(adminstrator only)</small></h3> + <ul> + {% for institute in publication.institutes.all %} + <li>{{ institute }}</li> + {% endfor %} + </ul> + {% endif %} </div> </div> - {% if request.user|is_in_group:'Editorial Administrators' %} + {% if is_edcol_admin %} <hr> <div class="row"> <div class="col-12"> diff --git a/journals/views.py b/journals/views.py index 792062e6b868e99ac9f1f681f05f768a39ded83f..bbd2b074b7cbd282d582d9f23d05a7f2828b2d10 100644 --- a/journals/views.py +++ b/journals/views.py @@ -253,6 +253,13 @@ def validate_publication(request): publication.authors_unregistered.add(publication.first_author_unregistered) publication.authors_claims.add(*submission.authors_claims.all()) publication.authors_false_claims.add(*submission.authors_false_claims.all()) + + # Add Institutes to the publication + for author in publication.authors.all(): + for institute in author.affiliations.active(): + publication.institutes.add(institute) + + # Save the beast publication.save() # Move file to final location diff --git a/notifications/managers.py b/notifications/managers.py index 4ec6b22b904ec620247661c0314472a25ade2ba7..1ac9a2e2e0a9f527127475ed4906cdca2477b07c 100644 --- a/notifications/managers.py +++ b/notifications/managers.py @@ -13,6 +13,10 @@ class NotificationQuerySet(models.query.QuerySet): """Return only unread items in the current queryset""" return self.filter(unread=True) + def pseudo_unread(self): + """Return only unread items in the current queryset""" + return self.filter(pseudo_unread=True) + def read(self): """Return only read items in the current queryset""" return self.filter(unread=False) @@ -27,6 +31,16 @@ class NotificationQuerySet(models.query.QuerySet): return qs.update(unread=False) + def mark_all_as_pseudo_read(self, recipient=None): + """Mark as read any unread messages in the current queryset.""" + # We want to filter out read ones, as later we will store + # the time they were marked as read. + qs = self.pseudo_unread() + if recipient: + qs = qs.filter(recipient=recipient) + + return qs.update(pseudo_unread=False) + def mark_all_as_unread(self, recipient=None): """Mark as unread any read messages in the current queryset.""" qs = self.read() diff --git a/notifications/migrations/0003_fakeactors.py b/notifications/migrations/0003_fakeactors.py new file mode 100644 index 0000000000000000000000000000000000000000..d254da9d3748ba9bb62c850f48a8f3b696304b02 --- /dev/null +++ b/notifications/migrations/0003_fakeactors.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-01 21:08 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('notifications', '0002_auto_20171021_1821'), + ] + + operations = [ + migrations.CreateModel( + name='FakeActors', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=256)), + ], + ), + ] diff --git a/notifications/migrations/0003_notification_pseudo_unread.py b/notifications/migrations/0003_notification_pseudo_unread.py new file mode 100644 index 0000000000000000000000000000000000000000..6e6953bef863e3fd5fd39d9818696faffc1cefa5 --- /dev/null +++ b/notifications/migrations/0003_notification_pseudo_unread.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-29 20:37 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('notifications', '0002_auto_20171021_1821'), + ] + + operations = [ + migrations.AddField( + model_name='notification', + name='pseudo_unread', + field=models.BooleanField(default=True), + ), + ] diff --git a/notifications/migrations/0004_merge_20171103_1132.py b/notifications/migrations/0004_merge_20171103_1132.py new file mode 100644 index 0000000000000000000000000000000000000000..d1e1181dc2afeed0f959be72acb45789d4e141e7 --- /dev/null +++ b/notifications/migrations/0004_merge_20171103_1132.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-03 10:32 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('notifications', '0003_fakeactors'), + ('notifications', '0003_notification_pseudo_unread'), + ] + + operations = [ + ] diff --git a/notifications/migrations/0005_auto_20171105_1004.py b/notifications/migrations/0005_auto_20171105_1004.py new file mode 100644 index 0000000000000000000000000000000000000000..6fa563d7794c73ef134263774bd4dc39936b5a60 --- /dev/null +++ b/notifications/migrations/0005_auto_20171105_1004.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-05 09:04 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('notifications', '0004_merge_20171103_1132'), + ] + + operations = [ + migrations.AddField( + model_name='notification', + name='unread_datetime', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='notification', + name='created', + field=models.DateTimeField(auto_now_add=True), + ), + ] diff --git a/notifications/migrations/0006_remove_notification_unread_datetime.py b/notifications/migrations/0006_remove_notification_unread_datetime.py new file mode 100644 index 0000000000000000000000000000000000000000..8de0a18add4020f5ac0f0715a493efb758f09adf --- /dev/null +++ b/notifications/migrations/0006_remove_notification_unread_datetime.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-05 09:09 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('notifications', '0005_auto_20171105_1004'), + ] + + operations = [ + migrations.RemoveField( + model_name='notification', + name='unread_datetime', + ), + ] diff --git a/notifications/models.py b/notifications/models.py index d7d9fef894b28bc7af060517cb9d136c502f0940..e05b53710ade32063ef292107119e6f9b0d4835c 100644 --- a/notifications/models.py +++ b/notifications/models.py @@ -9,6 +9,16 @@ from .constants import NOTIFICATION_TYPES from .managers import NotificationQuerySet +class FakeActors(models.Model): + """ + This Model acts as a surrogate person that either is unknown, deceased, fake, etc. etc. + """ + name = models.CharField(max_length=256) + + def __str__(self): + return self.name + + class Notification(models.Model): """ Action model describing the actor acting out a verb (on an optional @@ -33,6 +43,7 @@ class Notification(models.Model): recipient = models.ForeignKey(settings.AUTH_USER_MODEL, blank=False, related_name='notifications') unread = models.BooleanField(default=True) + pseudo_unread = models.BooleanField(default=True) # Used to keep notification-bg "active" actor_content_type = models.ForeignKey(ContentType, related_name='notify_actor') actor_object_id = models.CharField(max_length=255) @@ -51,7 +62,7 @@ class Notification(models.Model): action_object_object_id = models.CharField(max_length=255, blank=True, null=True) action_object = GenericForeignKey('action_object_content_type', 'action_object_object_id') - created = models.DateTimeField(default=timezone.now) + created = models.DateTimeField(auto_now_add=True) # This field is for internal use only. It is used to prevent duplicate sending # of notifications. @@ -95,19 +106,22 @@ class Notification(models.Model): return id2slug(self.id) def mark_toggle(self): - if self.unread: + if self.pseudo_unread: self.unread = False - self.save() + self.pseudo_unread = False else: self.unread = True - self.save() + self.pseudo_unread = True + self.save() def mark_as_read(self): - if self.unread: + if self.unread or self.pseudo_unread: self.unread = False + self.pseudo_unread = False self.save() def mark_as_unread(self): - if not self.unread: + if not self.unread or not self.pseudo_unread: self.unread = True + self.pseudo_unread = True self.save() diff --git a/notifications/templatetags/notifications_tags.py b/notifications/templatetags/notifications_tags.py index 42c62d09bfa42bf4735008e895623171f7f500ec..8d8bccd2402cd1f213000b4597ba82bc67390833 100644 --- a/notifications/templatetags/notifications_tags.py +++ b/notifications/templatetags/notifications_tags.py @@ -1,35 +1,60 @@ # -*- coding: utf-8 -*- +from django.core.urlresolvers import reverse from django.template import Library from django.utils.html import format_html register = Library() -@register.assignment_tag(takes_context=True) -def notifications_unread(context): - user = user_context(context) - if not user: - return '' - return user.notifications.unread().count() +@register.simple_tag(takes_context=True) +def live_notify_badge(context, classes=''): + html = "<span class='live_notify_badge {classes}'>0</span>".format(classes=classes) + return format_html(html) @register.simple_tag(takes_context=True) -def live_notify_badge(context, badge_class='live_notify_badge', classes=''): +def live_notify_list(context): user = user_context(context) if not user: return '' - html = "<span class='{badge_class} {classes}' data-count='{unread}'>{unread}</span>".format( - badge_class=badge_class, unread=user.notifications.unread().count(), - classes=classes - ) - return format_html(html) + html = '<div class="popover-template popover">' + html += '<div class="popover notifications" role="tooltip">' + + # User default links + html += '<h6 class="header">Welcome {first_name} {last_name}</h6>'.format( + first_name=user.first_name, last_name=user.last_name) + + if hasattr(user, 'contributor'): + html += '<a class="item" href="{url}">Personal Page</a>'.format( + url=reverse('scipost:personal_page')) + + # User specific links + if user.has_perm('scipost.can_read_partner_page'): + html += '<a class="item" href="{url}">Partner Page</a>'.format( + url=reverse('partners:dashboard')) + if user.has_perm('scipost.can_view_timesheets'): + html += '<a class="item" href="{url}">Financial Administration</a>'.format( + url=reverse('finances:finance')) + if user.has_perm('scipost.can_view_all_funding_info'): + html += '<a class="item" href="{url}">Funders</a>'.format( + url=reverse('funders:funders')) + if user.has_perm('scipost.can_view_production'): + html += '<a class="item" href="{url}">Production</a>'.format( + url=reverse('production:production')) + if user.has_perm('scipost.can_view_pool'): + html += '<a class="item" href="{url}">Submission Pool</a>'.format( + url=reverse('submissions:pool')) + # Logout links + html += '<div class="divider"></div>' + html += '<a class="item" href="{url}">Logout</a>'.format( + url=reverse('scipost:logout')) -@register.simple_tag -def live_notify_list(list_class='live_notify_list', classes=''): - html = "<ul class='{list_class} {classes}'></ul>".format(list_class=list_class, - classes=classes) + # Notifications + html += '<div class="divider"></div><h6 class="header">Inbox</h6>' + html += '<div class="live_notify_list"></div></div>' + html += '<div class="popover-body"></div></div>' return format_html(html) diff --git a/notifications/urls.py b/notifications/urls.py index 7fd8627126d17a50579d3d39bdd523c1841ab137..a80a7a6769ffa3f1c17e446232a00976a5840819 100644 --- a/notifications/urls.py +++ b/notifications/urls.py @@ -5,9 +5,7 @@ from . import views urlpatterns = [ url(r'^redirect/(?P<slug>\d+)$', views.forward, name='forward'), - url(r'^mark-all-as-read/$', views.mark_all_as_read, name='mark_all_as_read'), url(r'^mark-toggle/(?P<slug>\d+)/$', views.mark_toggle, name='mark_toggle'), - url(r'^delete/(?P<slug>\d+)/$', views.delete, name='delete'), url(r'^api/unread_count/$', views.live_unread_notification_count, name='live_unread_notification_count'), url(r'^api/list/$', views.live_notification_list, name='live_unread_notification_list'), diff --git a/notifications/views.py b/notifications/views.py index bcdf7e3b03fc93946b0d254b3136d9c9b342fc5a..b36eaab4806253da91d5c18cfc1aa033983249b9 100644 --- a/notifications/views.py +++ b/notifications/views.py @@ -24,22 +24,6 @@ def forward(request, slug): return redirect(notification.target.get_absolute_url()) -@login_required -@user_passes_test(is_test_user) -def mark_all_as_read(request): - request.user.notifications.mark_all_as_read() - - _next = request.GET.get('next') - - if _next: - return redirect(_next) - - if request.GET.get('json'): - return JsonResponse({'unread': 0}) - - return redirect('notifications:all') - - @login_required @user_passes_test(is_test_user) def mark_toggle(request, slug=None): @@ -58,22 +42,6 @@ def mark_toggle(request, slug=None): return redirect('notifications:all') -@login_required -@user_passes_test(is_test_user) -def delete(request, slug=None): - id = slug2id(slug) - - notification = get_object_or_404(Notification, recipient=request.user, id=id) - notification.delete() - - _next = request.GET.get('next') - - if _next: - return redirect(_next) - - return redirect('notifications:all') - - def live_unread_notification_count(request): if not request.user.is_authenticated(): data = {'unread_count': 0} @@ -101,6 +69,7 @@ def live_notification_list(request): for n in request.user.notifications.all()[:num_to_fetch]: struct = model_to_dict(n) + struct['unread'] = struct['pseudo_unread'] struct['slug'] = id2slug(n.id) if n.actor: if isinstance(n.actor, User): @@ -119,8 +88,11 @@ def live_notification_list(request): struct['timesince'] = n.timesince() list.append(struct) - if request.GET.get('mark_as_read'): - n.mark_as_read() + + if request.GET.get('mark_as_read'): + # Mark all as read + request.user.notifications.mark_all_as_read() + data = { 'unread_count': request.user.notifications.unread().count(), 'list': list diff --git a/petitions/views.py b/petitions/views.py index b702d80784f700c223fe6be5da4f6228a3955790..86c527ccc405f6270909f55622607d7cfff08f70 100644 --- a/petitions/views.py +++ b/petitions/views.py @@ -26,8 +26,8 @@ def petition(request, slug): 'first_name': request.user.first_name, 'last_name': request.user.last_name, 'email': request.user.email, - 'country_of_employment': request.user.contributor.country_of_employment, - 'affiliation': request.user.contributor.affiliation, + 'country_of_employment': request.user.contributor.affiliation.country_of_employment, + 'affiliation': request.user.contributor.affiliation.name, } form = SignPetitionForm(request.POST or None, initial=initial, petition=petition, diff --git a/proceedings/templates/partials/proceedings/description.html b/proceedings/templates/partials/proceedings/description.html index 0375ee4b73bdb7eaac45f7e0d4536fa493246e74..a28c66442a8810b3d8123707b9754709246434db 100644 --- a/proceedings/templates/partials/proceedings/description.html +++ b/proceedings/templates/partials/proceedings/description.html @@ -15,7 +15,7 @@ <h3>Guest Fellows responsible for this Issue</h3> <ul> {% for fellow in proceedings.fellowships.guests %} - <li>{{ fellow.contributor.get_title_display }} {{ fellow.contributor.user.first_name }} {{ fellow.contributor.user.last_name }}{% if fellow.contributor.affiliation %}, {{ fellow.contributor.affiliation }}{% endif %}</li> + <li>{{ fellow.contributor.get_title_display }} {{ fellow.contributor.user.first_name }} {{ fellow.contributor.user.last_name }}{% if fellow.contributor.affiliation.name %}, {{ fellow.contributor.affiliation.name }}{% endif %}</li> {% endfor %} </ul> {% endif %} diff --git a/production/views.py b/production/views.py index 49bfa8926e18bd7ba4c338287cb27d08c1c6110d..cc7e0d4063e42b191d0b4074367e3d58ee4b259e 100644 --- a/production/views.py +++ b/production/views.py @@ -1,4 +1,3 @@ -import datetime import mimetypes from django.contrib import messages diff --git a/scipost/admin.py b/scipost/admin.py index 8cfaffcf2da4ef93af08a909d043e8164df872fe..3b8059b73e28851d0ebc3e071091ae2b846fc0b2 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 614c14f44fad3314c1e6333c434e8ad634e56d21..6a175bb129bf87ef00e2c342fcb4f324793724bc 100644 --- a/scipost/forms.py +++ b/scipost/forms.py @@ -24,6 +24,7 @@ from .decorators import has_contributor from .models import Contributor, DraftInvitation, RegistrationInvitation,\ UnavailabilityPeriod, PrecookedEmail +from affiliations.models import Affiliation, Institute from common.forms import MonthYearWidget from partners.decorators import has_contact @@ -115,16 +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'], + ) 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'], - 'country_of_employment': self.cleaned_data['country_of_employment'], 'address': self.cleaned_data['address'], - 'affiliation': self.cleaned_data['affiliation'], 'personalwebpage': self.cleaned_data['personalwebpage'], }) + affiliation, __ = Affiliation.objects.get_or_create( + contributor=contributor, + institute=institute, + ) if contributor.activation_key == '': # Seems redundant? @@ -259,37 +266,20 @@ class UpdateUserDataForm(forms.ModelForm): class UpdatePersonalDataForm(forms.ModelForm): class Meta: model = Contributor - fields = ['title', 'discipline', 'expertises', 'orcid_id', 'country_of_employment', - 'affiliation', 'address', 'personalwebpage' - ] - widgets = {'country_of_employment': CountrySelectWidget()} - - # def __init__(self, *args, **kwargs): - # super().__init__(*args, **kwargs) - # self.fields['mail_subscription'] = forms.ModelMultipleChoiceField( - # queryset=MailchimpList.objects.open_to_subscribe(kwargs['instance']).distinct(), - # widget=forms.CheckboxSelectMultiple(), - # label='Subscribe to the following mailing lists:', - # required=False) - # self.fields['mailing_lists'] = forms.ModelMultipleChoiceField( - # queryset=MailchimpList.objects.open_to_subscribe(kwargs['instance']).distinct(), - # widget=forms.CheckboxSelectMultiple(), - # label='Subscribe to the following mailing lists:', - # required=False) + fields = [ + 'title', + 'discipline', + 'expertises', + 'orcid_id', + 'address', + 'personalwebpage' + ] def sync_lists(self): + """ + Pseudo U/S; do not remove + """ return - # contributor = self.instance - # original_lists = list(self.fields['mailing_lists'].queryset) - # - # # Subscribe to lists - # for _list in self.cleaned_data['mailing_lists']: - # _list.update_membership([contributor]) - # original_lists.remove(_list) - # - # # Unsubscribe from the leftovers - # for _list in original_lists: - # _list.update_membership([contributor], status='unsubscribed') def propagate_orcid(self): """ @@ -479,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/management/commands/add_groups_and_permissions.py b/scipost/management/commands/add_groups_and_permissions.py index f94c680845f9d5a85f29aa35bb9b238e1e9124f3..1b99ccf50734665e25491a3b6b0e8934057d33d0 100644 --- a/scipost/management/commands/add_groups_and_permissions.py +++ b/scipost/management/commands/add_groups_and_permissions.py @@ -262,6 +262,12 @@ class Command(BaseCommand): name='Can view timesheets', content_type=content_type) + # Affiliations administration + can_manage_affiliations, created = Permission.objects.get_or_create( + codename='can_manage_affiliations', + name='Can manage affiliations', + content_type=content_type) + # Mailchimp can_manage_mailchimp, created = Permission.objects.get_or_create( codename='can_manage_mailchimp', @@ -293,6 +299,7 @@ class Command(BaseCommand): can_manage_mailchimp, can_view_all_production_streams, can_promote_to_production_team, + can_manage_affiliations, ]) FinancialAdmin.permissions.set([ diff --git a/scipost/migrations/0066_contributor__affiliation.py b/scipost/migrations/0066_contributor__affiliation.py new file mode 100644 index 0000000000000000000000000000000000000000..345021dd99dabb24f7eaebd34da3c943d0aa9ee4 --- /dev/null +++ b/scipost/migrations/0066_contributor__affiliation.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-01 19:28 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('affiliations', '0002_affiliation_acronym'), + ('scipost', '0065_authorshipclaim_publication'), + ] + + operations = [ + migrations.AddField( + model_name='contributor', + name='_affiliation', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='affiliations.Institute'), + ), + ] diff --git a/scipost/migrations/0067_auto_20171101_2132.py b/scipost/migrations/0067_auto_20171101_2132.py new file mode 100644 index 0000000000000000000000000000000000000000..43e5ed004e74df1fcd53f6e1044a92cb8b98a43f --- /dev/null +++ b/scipost/migrations/0067_auto_20171101_2132.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-01 20:32 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0066_contributor__affiliation'), + ] + + operations = [ + migrations.RenameField( + model_name='contributor', + old_name='affiliation', + new_name='old_affiliation', + ), + migrations.RenameField( + model_name='contributor', + old_name='country_of_employment', + new_name='old_country_of_employment', + ), + migrations.AlterField( + model_name='contributor', + name='_affiliation', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='contributors', to='affiliations.Institute'), + ), + ] diff --git a/scipost/migrations/0068_auto_20171101_2132.py b/scipost/migrations/0068_auto_20171101_2132.py new file mode 100644 index 0000000000000000000000000000000000000000..f59223bee41208b98d73e363ccea8ab890db2be7 --- /dev/null +++ b/scipost/migrations/0068_auto_20171101_2132.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-01 20:32 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0067_auto_20171101_2132'), + ] + + operations = [ + migrations.RenameField( + model_name='contributor', + old_name='_affiliation', + new_name='affiliation', + ), + ] diff --git a/scipost/migrations/0069_auto_20171102_0840.py b/scipost/migrations/0069_auto_20171102_0840.py new file mode 100644 index 0000000000000000000000000000000000000000..1de2b91a9358bde40dc595dd50e8484e6f89841c --- /dev/null +++ b/scipost/migrations/0069_auto_20171102_0840.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-02 07:40 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0068_auto_20171101_2132'), + ] + + operations = [ + migrations.RenameField( + model_name='contributor', + old_name='affiliation', + new_name='old_affiliation_fk', + ), + ] diff --git a/scipost/migrations/0070_remove_contributor_old_affiliation_fk.py b/scipost/migrations/0070_remove_contributor_old_affiliation_fk.py new file mode 100644 index 0000000000000000000000000000000000000000..01a7bf2698a49cb00331d27fb633a29c7301fcdb --- /dev/null +++ b/scipost/migrations/0070_remove_contributor_old_affiliation_fk.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-02 07:55 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('affiliations', '0006_auto_20171102_0843'), + ('scipost', '0069_auto_20171102_0840'), + ] + + operations = [ + migrations.RemoveField( + model_name='contributor', + name='old_affiliation_fk', + ), + ] diff --git a/scipost/migrations/0071_auto_20171102_0858.py b/scipost/migrations/0071_auto_20171102_0858.py new file mode 100644 index 0000000000000000000000000000000000000000..2719f788adab65209a7313825393ea9c855d160d --- /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 0000000000000000000000000000000000000000..3f82fd9db28aa1045add819d8df6332c999b344c --- /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 92d9f20429454017cf9634938617bd2ee781c849..4cd14cef37edb1e16bc8e44f6e1247cf3849a428 100644 --- a/scipost/models.py +++ b/scipost/models.py @@ -50,8 +50,6 @@ class Contributor(models.Model): blank=True, null=True) orcid_id = models.CharField(max_length=20, verbose_name="ORCID id", blank=True) - country_of_employment = CountryField() - affiliation = models.CharField(max_length=300, verbose_name='affiliation') address = models.CharField(max_length=1000, verbose_name="address", blank=True) personalwebpage = models.URLField(verbose_name='personal web page', @@ -225,7 +223,7 @@ class RegistrationInvitation(models.Model): class CitationNotification(models.Model): - contributor = models.ForeignKey(Contributor, on_delete=models.CASCADE) + contributor = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE) cited_in_submission = models.ForeignKey('submissions.Submission', on_delete=models.CASCADE, blank=True, null=True) @@ -288,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 7ddb10372dd8fed7fd054288653e0a21ca3e1024..67b14aebb660f6e677e7fb8e820338a89786ea26 100644 --- a/scipost/static/scipost/assets/config/preconfig.scss +++ b/scipost/static/scipost/assets/config/preconfig.scss @@ -65,6 +65,7 @@ $card-shadow-color: #ccc; $card-grey-border-bottom-color: #d0d1d5; $card-grey-border-color: #e5e6e9 #dfe0e4 $card-grey-border-bottom-color; + // breadcrumb // $breadcrumb-bg: #f9f9f9; @@ -108,6 +109,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 +134,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/_breadcrumb.scss b/scipost/static/scipost/assets/css/_breadcrumb.scss index cbb8d8e6be656517b8849ca6ee3e78ea67bd6646..4e3915ebcfbd361517c401b2fa6e389afcc6d538 100644 --- a/scipost/static/scipost/assets/css/_breadcrumb.scss +++ b/scipost/static/scipost/assets/css/_breadcrumb.scss @@ -9,6 +9,7 @@ .container { width: 100%; display: flex; + flex-wrap: nowrap; } .breadcrumb-item { diff --git a/scipost/static/scipost/assets/css/_form.scss b/scipost/static/scipost/assets/css/_form.scss index 25694c4652bcedaceed0f28c56a540a518ed0b14..bfd15ced62c30b8de7bbcdd4edcc652ad5452517 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/_homepage.scss b/scipost/static/scipost/assets/css/_homepage.scss index 16da6e2fd48b0948f08dec3ed1fe31c34d22b41c..2940939bb54bee8eaa06fa6383e895e80e042b68 100644 --- a/scipost/static/scipost/assets/css/_homepage.scss +++ b/scipost/static/scipost/assets/css/_homepage.scss @@ -24,8 +24,6 @@ max-height: 100%; height: 100%; -webkit-overflow-scrolling: touch; - padding-left: 1rem; - padding-right: 1rem; } .main-panel { @@ -44,6 +42,8 @@ border-left: 1px solid #ddd; position: relative; padding-top: 0.25rem; + padding-left: 1rem; + padding-right: 1rem; margin-top: 2rem; .card { @@ -70,10 +70,15 @@ padding-right: 0; } } +} + - @media (min-width: 768px) { +@media (min-width: 768px) { + .has-sidebar { .main-panel { width: calc(100% - 350px); + padding-left: 1rem; + padding-right: 1rem; } .sidebar { width: 350px; @@ -94,8 +99,10 @@ } } } +} - @media (min-width: 1280px) { +@media (min-width: 1280px) { + .has-sidebar { .main-panel { width: calc(100% - 400px); } diff --git a/scipost/static/scipost/assets/css/_nav.scss b/scipost/static/scipost/assets/css/_nav.scss index 9551795390051441481d3a22f222af65cf3f28a7..a5e59525ef3c8b4ee262156b3af58e73b546fc0a 100644 --- a/scipost/static/scipost/assets/css/_nav.scss +++ b/scipost/static/scipost/assets/css/_nav.scss @@ -68,6 +68,10 @@ nav.main-nav { } } +.nav { + flex-wrap: nowrap; +} + nav.submenu { margin-top: -1.5rem; margin-bottom: 1rem; @@ -76,9 +80,15 @@ nav.submenu { border-radius: 0; border-top: 1px solid #fff; border-bottom: 1px solid #ddd; + display: flex; + flex-wrap: nowrap; + flex-direction: row; + overflow-x: scroll; .item { padding: 0 0.5rem; + white-space: nowrap; + display: block; border-right: 2px solid #fff; } .item:first-child, diff --git a/scipost/static/scipost/assets/css/_navbar.scss b/scipost/static/scipost/assets/css/_navbar.scss index a1ffa8e02c7f39a7152f6c0ff12fa3e724f444cd..211d612f9d49f42ea5ea0de34305cc0f567830f8 100644 --- a/scipost/static/scipost/assets/css/_navbar.scss +++ b/scipost/static/scipost/assets/css/_navbar.scss @@ -4,6 +4,9 @@ */ .navbar { margin-bottom: 1.5rem; + display: flex; + flex-wrap: nowrap; + overflow-x: scroll; &.main-nav { border-bottom: 1px solid #ddd; @@ -25,10 +28,6 @@ } } - .navbar-nav { - flex-direction: row; - } - .active > .nav-link { border-color: $scipost-darkblue; box-shadow: 0 1px 0 0 #ccc; @@ -38,8 +37,26 @@ background-color: rgba(255, 255, 255, 0.6); } - .nav-item { - margin-right: 0.5rem; + .navbar-nav { + display: flex; + flex-direction: row; + } +} + +@media (min-width: 768px) { + .navbar { + + .navbar-nav { + margin-right: auto; + } + + .nav-item { + margin-right: 0.5rem; + } + + [data-toggle="collapse"] { + display: none; + } } } @@ -77,6 +94,10 @@ .navbar-counter { position: relative; + .nav-link.notifications_container:hover { + background-color: #fff; + } + a.dropdown-toggle { min-width: 45px; } @@ -92,47 +113,59 @@ content: "\f0f3"; } } +} - .badge { - vertical-align: top; - margin-left: -15px; - margin-top: -5px; - height: 16px; - min-width: 16px; - line-height: 10px; - display: none; - padding: 0.25em; - border-radius: 99px; - border: 1px solid #f9f9f9; - } +.live_notify_badge { + vertical-align: top; + margin-left: -15px; + margin-top: -5px; + height: 16px; + min-width: 16px; + line-height: 10px; + display: none; + padding: 0.25em; + border-radius: 99px; + border: 1px solid #f9f9f9; } .notifications_container { - color: $scipost-lightestblue; + .badge_link { + color: $scipost-lightestblue; + + // &::after { + // content: none; + // } + } .user { color: $scipost-darkblue; } - .fa { - font-size: 150%; + .fa-inbox { vertical-align: bottom; margin: 0 0.25rem; + min-width: 17px; + + &:before { + font-size: 150%; + } + } + + &.show .fa-inbox { + color: $scipost-darkblue; } &.positive_count { - color: $scipost-orange; + .badge_link { + color: $scipost-orange; + } .user { color: $scipost-darkblue; } - .badge { + .live_notify_badge { display: inline-block; } } - - &::after { - content: none; - } } diff --git a/scipost/static/scipost/assets/css/_popover.scss b/scipost/static/scipost/assets/css/_notifications.scss similarity index 52% rename from scipost/static/scipost/assets/css/_popover.scss rename to scipost/static/scipost/assets/css/_notifications.scss index 5ef240e8b434fe68d4c9fd09f3c5052a61ad69f6..8670e2f38dbbc8047210e2cf016255b074506a33 100644 --- a/scipost/static/scipost/assets/css/_popover.scss +++ b/scipost/static/scipost/assets/css/_notifications.scss @@ -1,43 +1,28 @@ -.popover { - width: 500px; - box-shadow: #ccc 0px 1px 2px 1px; -} .navbar-counter .nav-link:hover { background-color: $white; } -.popover.bs-popover-bottom, -.popover.bs-popover-auto[x-placement^="bottom"] { - .arrow::before { - border-bottom-color: rgb(221, 221, 221); - } - - .arrow::after { - border-bottom-color: #f7f7f7; - } +.popover-template { + display: none; } .notifications { - border: 0; - border-radius: 0; - - .popover-header { - font-size: 100%; - padding: 0.3rem 1rem; - font-weight: 600; - text-transform: uppercase; + padding: 0; + min-width: 500px; - a { - color: $scipost-darkblue; - } + .header { + padding: 1rem 1rem 0.5rem 1rem; + background-color: #f9f9f9; } - .popover-body { - padding: 0; + li.item { + &[href]:hover { + background-color: #f9f9f9; + } } - &.popover .list-group-item { + .item { padding: 0.4rem 1rem; border-radius: 0; border-top: 1px solid #fff; @@ -47,18 +32,42 @@ justify-content: space-between; display: flex; + &[href] { + cursor: pointer; + } + + &.active, + &.active[href]:hover { + background-color: $scipost-lightestblue; + } + + > div { + white-space: normal; + } + &:last-child { border-bottom: 0; } } + a.item, + .item a { + color: $scipost-lightblue; + + &:hover { + text-decoration: underline; + } + } + .actions { - display: block; + // display: none; opacity: 0.0; transition: opacity 0.1s; width: 20px; height: 100%; - padding-left: 0.25rem; + padding-left: 1rem; + padding-right: 0.5rem; + padding-bottom: 0.1rem; .fa[data-toggle="tooltip"] { font-size: 1em; @@ -75,11 +84,7 @@ } } - .list-group-item:hover .actions { + .item:hover .actions { opacity: 1.0; } } - -.popover-header { - font-size: initial; -} diff --git a/scipost/static/scipost/assets/css/_tables.scss b/scipost/static/scipost/assets/css/_tables.scss index 851ef97467fe1cc81ff3c3a867511b57278a32ba..8873b93c394e15c1ef958f4509818a1f193e5c59 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/assets/css/_type.scss b/scipost/static/scipost/assets/css/_type.scss index 056fe6dcb5bb8c21f77aee7affb8a68a0cfec105..fbe387a334e6b23a3e336d1cedcb3930c08a2171 100644 --- a/scipost/static/scipost/assets/css/_type.scss +++ b/scipost/static/scipost/assets/css/_type.scss @@ -31,7 +31,7 @@ h4 { line-height: normal; } -h5, h6 { +h5 { font-weight: 300; } diff --git a/scipost/static/scipost/assets/css/style.scss b/scipost/static/scipost/assets/css/style.scss index be68d319863f3b5d962c823aca10301be2f28b73..5b2d4702fc9744c9f3ab74936d10a968850e6049 100644 --- a/scipost/static/scipost/assets/css/style.scss +++ b/scipost/static/scipost/assets/css/style.scss @@ -30,9 +30,9 @@ @import "modal"; @import "navbar"; @import "nav"; +@import "notifications"; @import "page_header"; @import "pool"; -@import "popover"; @import "tables"; @import "tooltip"; @import "type"; diff --git a/scipost/static/scipost/assets/js/notifications.js b/scipost/static/scipost/assets/js/notifications.js index 49fbe7f24e3f96a1d8a2c82cd9191f42a3f1cfd6..a20d3a146c7ff98bf3ac7a5642dce137ecd2dd2b 100644 --- a/scipost/static/scipost/assets/js/notifications.js +++ b/scipost/static/scipost/assets/js/notifications.js @@ -1,195 +1,172 @@ -var notify_container_class = "notifications_container"; -var notify_badge_class = "live_notify_badge"; -var notify_menu_class = "live_notify_list"; -var notify_api_url_count = "/notifications/api/unread_count/"; -var notify_api_url_list = "/notifications/api/list/"; -var notify_api_url_toggle_read = "/notifications/mark-toggle/"; -var notify_api_url_mark_all_read = "/notifications/mark-all-as-read/"; -var notify_fetch_count = "5"; -var notify_refresh_period = 60000; -var consecutive_misfires = 0; -var registered_functions = [fill_notification_badge]; - - -function initiate_popover(reinitiate) { - if(typeof reinitiate == 'undefined') { - reinitiate = false; +function fetch_api_data(callback, url, args) { + if (!url) { + var url = notify_api_url_count; } - var notification_template = '<div class="popover notifications" role="tooltip"><div class="arrow"></div><p class="popover-header"></p><div class="popover-body"></div></div>'; - - function get_notifications_title() { - return 'My inbox'; + if (callback) { + //only fetch data if a function is setup + var r = new XMLHttpRequest(); + r.addEventListener('readystatechange', function(event){ + if (this.readyState === 4){ + if (this.status === 200){ + consecutive_misfires = 0; + var data = JSON.parse(r.responseText); + return callback(data, args); + } else { + consecutive_misfires++; + } + } + }) + r.open("GET", url + '?max=5', true); + r.send(); } +} - function get_notifications() { - var _str = '<ul id="notification-list" class="update_notifications list-group"><div class="w-100 text-center py-4"><i class="fa fa-circle-o-notch fa-2x fa-spin fa-fw"></i><span class="sr-only">Loading...</span></div></ul>'; - get_notification_list(); - return _str; - } - $('.popover [data-toggle="tooltip"]').tooltip('dispose') - $('#notifications_badge').popover('dispose').popover({ - animation: false, - trigger: 'click', - title: get_notifications_title, - template: notification_template, - content: get_notifications, - container: 'body', - offset: '0, 9px', - placement: "bottom", - html: true - }).on('inserted.bs.popover', function() { - // Bloody js - setTimeout(function() { - $('.popover [data-toggle="tooltip"]').tooltip({ - animation: false, - delay: {"show": 500, "hide": 100}, - fallbackPlacement: 'clockwise', - placement: 'bottom' - }); - $('.popover .actions a').on('click', function() { - mark_toggle(this) - }) - $('.popover a.mark_all_read').on('click', function() { - mark_all_read(this) - }) - }, 1000); - }); - if (reinitiate) { - $('#notifications_badge').popover('show') - } -} +function update_counter_callback(data, args) { + var counter = data['unread_count']; + var el = $(args['element']); -function request_reinitiate(url) { - var r = new XMLHttpRequest(); - r.addEventListener('readystatechange', function(event){ - if (this.readyState == 4 && this.status == 200) { - fetch_api_data() - initiate_popover(reinitiate=true) - } - }) - r.open("GET", url, true); - r.send(); -} + if (typeof counter == 'undefined') { + counter = 0; + } -function mark_all_read(el) { - request_reinitiate(notify_api_url_mark_all_read + '?json=1') + el.html(counter); + if (counter > 0) { + el.parents('.notifications_container').addClass('positive_count') + } else { + el.parents('.notifications_container').removeClass('positive_count') + } } -function mark_toggle(el) { - request_reinitiate(notify_api_url_toggle_read + $(el).data('slug') + '/?json=1') -} +function update_list_callback(data, args) { + var items = data['list']; + var el = $(args['element']); + var messages = items.map(function (item) { + // Notification content + var message = '', + link = ''; -function fill_notification_badge(data) { - var badges = document.getElementsByClassName(notify_badge_class); - var container = $('.' + notify_container_class); - if (badges) { - for(var i = 0; i < badges.length; i++){ - badges[i].innerHTML = data.unread_count; - if (data.unread_count > 0) { - container.addClass('positive_count'); + if(typeof item.actor !== 'undefined'){ + message += '<strong>' + item.actor + '</strong>'; + } + if(typeof item.verb !== 'undefined'){ + message += " " + item.verb; + } + if(typeof item.target !== 'undefined'){ + if(typeof item.forward_link !== 'undefined') { + link = item.forward_link; + message += " <a href='" + item.forward_link + "'>" + item.target + "</a>"; } else { - container.removeClass('positive_count'); + message += " " + item.target; + } + } + if(typeof item.timesince !== 'undefined'){ + message += "<br><small>"; + if(typeof item.forward_link !== 'undefined') { + message += " <a href='" + item.forward_link + "'>Direct link</a> · "; } + message += "<span class='text-muted'>" + item.timesince + " ago</span></small>"; } + + // Notification actions + if(item.unread) { + var mark_toggle = '<a href="javascript:;" class="mark-toggle" data-slug="' + item.slug + '"><i class="fa fa-circle" data-toggle="tooltip" data-placement="auto" title="Mark as read" aria-hidden="true"></i></a>'; + } else { + var mark_toggle = '<a href="javascript:;" class="mark-toggle" data-slug="' + item.slug + '"><i class="fa fa-circle-o" data-toggle="tooltip" data-placement="auto" title="Mark as unread" aria-hidden="true"></i></a>'; + } + + if(typeof item.forward_link !== 'undefined') { + mark_toggle += "<br><a href='" + item.forward_link + "' data-toggle='tooltip' data-placement='auto' title='Go to item'><i class='fa fa-share' aria-hidden='true'></i></a>"; + } + + // Complete list html + if(link !== '') { + return '<li href="' + link + '" class="item ' + (item.unread ? ' active' : '') + '"><div>' + message + '</div><div class="actions">' + mark_toggle + '</div></li>'; + } else { + return '<li class="item ' + (item.unread ? ' active' : '') + '"><div>' + message + '</div><div class="actions">' + mark_toggle + '</div></li>'; + } + + }).join(''); + + if (messages == '') { + messages = '<li class="item px-5"><em>You have no new notifications</em></li>' } + + // Fill DOM + el.find('.live_notify_list').html(messages).parents('body').trigger('refresh_notify_list'); } -function get_notification_list() { - fetch_api_data(notify_api_url_list, true, function(data) { +function update_mark_callback(data, args) { + var el = $(args['element']); + $(el).parents('.item').toggleClass('active'); + trigger_badge(); +} - var messages = data.list.map(function (item) { - var message = "<div>"; - if(typeof item.actor !== 'undefined'){ - message += '<strong>' + item.actor + '</strong>'; - } - if(typeof item.verb !== 'undefined'){ - message += " " + item.verb; - } - if(typeof item.target !== 'undefined'){ - if(typeof item.forward_link !== 'undefined') { - message += " <a href='" + item.forward_link + "'>" + item.target + "</a>"; - } else { - message += " " + item.target; - } - } - if(typeof item.timesince !== 'undefined'){ - message += "<br><small class='text-muted'>" + item.timesince + " ago</small>"; - } - message += "</div>"; - if(item.unread) { - var mark_as_read = '<div class="actions"><a href="javascript:;" data-slug="' + item.slug + '"><i class="fa fa-circle" data-toggle="tooltip" data-placement="auto" title="Mark as read" aria-hidden="true"></i></a></div>'; - } else { - var mark_as_read = '<div class="actions"><a href="javascript:;" data-slug="' + item.slug + '"><i class="fa fa-circle-o" data-toggle="tooltip" data-placement="auto" title="Mark as unread" aria-hidden="true"></i></a></div>'; - } - return '<li class="list-group-item ' + (item.unread ? ' active' : '') + '">' + message + mark_as_read + '</li>'; - }).join(''); +function update_counter(el) { + fetch_api_data(update_counter_callback, "/notifications/api/unread_count/", {'element': el}); +} - if (messages == '') { - messages = '<div class="text-center px-2 py-3"><i class="fa fa-star-o fa-2x" aria-hidden="true"></i><h3>You have no new notifications</h3></div>' - } +function mark_toggle(el) { + var url = "/notifications/mark-toggle/" + $(el).data('slug') + "?json=1"; + fetch_api_data(update_mark_callback, url, {'element': el}); +} - document.getElementById('notification-list').innerHTML = messages; +function update_list(el) { + fetch_api_data(update_list_callback, "/notifications/api/list/?mark_as_read=1", {'element': el}); +} + +function trigger_badge() { + $('.live_notify_badge').trigger('notification_count_updated'); +} +// Update Badge count every minute +var badge_timer = setInterval(trigger_badge, 60000); + +function initiate_popover() { + var template = $('.notifications_container .popover-template').html(); + $('.notifications_container a[data-toggle="popover"]').popover({ + trigger: 'focus', + template: template, + placement: 'bottom', + title: 'empty-on-purpose' + }) + .on('inserted.bs.popover', function() { + $('body').trigger('notification_open_list'); }); } -function fetch_api_data(url, once, callback) { - if (!url) { - var url = notify_api_url_count; - } - if (!once) { - var once = false; - } +$(function(){ + $('body').on('notification_open_list', function() { + update_list(this); + }) - if (registered_functions.length > 0) { - //only fetch data if a function is setup - var r = new XMLHttpRequest(); - r.addEventListener('readystatechange', function(event){ - if (this.readyState === 4){ - if (this.status === 200){ - consecutive_misfires = 0; - var data = JSON.parse(r.responseText); - registered_functions.forEach(function (func) { func(data); }); - if (callback) { - return callback(data); - } - }else{ - consecutive_misfires++; - } - } - }) - r.open("GET", url + '?max=' + notify_fetch_count, true); - r.send(); - } - var timer = null; - if (!once) { - if (consecutive_misfires < 10 && !once) { - timer = setTimeout(fetch_api_data, notify_refresh_period); - } else { - var badges = document.getElementsByClassName(notify_badge_class); - if (badges) { - for (var i=0; i < badges.length; i++){ - badges[i].innerHTML = "!"; - badges[i].title = "Connection lost!" - } - } - } - } + $('.live_notify_badge').on('notification_count_updated', function() { + update_counter(this); + }).trigger('notification_count_updated'); - return stop; - function stop() { - if (timer) { - clearTimeout(timer); - timer = 0; - } - } -} -setTimeout(fetch_api_data, 1000); + $('body').on('refresh_notify_list', function() { + // Bloody js + var list = $('.live_notify_list'); + list.find('li.item').on('click', function(e) { + e.stopPropagation(); + }) + .filter('[href]').on('click', function(e) { + window.location.href = $(this).attr('href') + }); + list.find('[data-toggle="tooltip"]').tooltip({ + animation: false, + delay: {"show": 500, "hide": 100}, + fallbackPlacement: 'clockwise', + placement: 'bottom' + }); + list.find('.actions a.mark-toggle').on('click', function(e) { + e.stopPropagation(); + mark_toggle(this); + }); + }); -$(function(){ initiate_popover(); }); diff --git a/scipost/static/scipost/formset.js b/scipost/static/scipost/formset.js new file mode 100644 index 0000000000000000000000000000000000000000..e944b74a779fecef708f2b55528da23b79f17139 --- /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 04d525681719b0e696584c1584c9baeaaa12d04b..9eb68274e47459fb4068a8e92476bd75733ef994 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.country_of_employment.name }}</td></tr> - <tr><td>Affiliation: </td><td> </td><td>{{ contributor.affiliation }}</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 e41461ee178b3bdd9e01b9b0ba92804abf6b04ca..6e80fb2c51545076559b06b46310b145ab17cae6 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.country_of_employment.name|default:'-'}}</td></tr> - <tr><td>Affiliation: </td><td> </td><td>{{ contributor.affiliation|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 7ee2c8988a5f5d791b97c4ff50740fd03d0968cf..26423a9502582d3579c1ea106e3e1e9752c701fa 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/navbar.html b/scipost/templates/scipost/navbar.html index 93ccffea2081b2623fabd849169e3cf6546e36a8..6c0bea234470a3afe91bf080a4bf276f6978e84c 100644 --- a/scipost/templates/scipost/navbar.html +++ b/scipost/templates/scipost/navbar.html @@ -3,74 +3,74 @@ {% load scipost_extras %} -<nav class="navbar navbar-scroll navbar-light main-nav navbar-expand-lg"> - <div class="navbar-scroll-inner"> - <ul class="navbar-nav mr-auto"> - <li class="nav-item{% if request.path == '/' %} active{% endif %}"> - <a href="{% url 'scipost:index' %}" class="nav-link">Home</a> - </li> - <li class="nav-item{% if '/journals/' in request.path %} active{% endif %}"> - <a href="{% url 'journals:journals' %}" class="nav-link">Journals</a> - </li> - <li class="nav-item{% if '/submissions/' in request.path %} active{% endif %}"> - <a class="nav-link" href="{% url 'submissions:submissions' %}">Submissions</a> - </li> - <li class="nav-item{% if '/commentaries/' in request.path %} active{% endif %}"> - <a class="nav-link" href="{% url 'commentaries:commentaries' %}">Commentaries</a> +<nav class="navbar navbar-light main-nav navbar-expand-lg"> + <ul id="menu-navbar" class="navbar-nav"> + <li class="nav-item{% if request.path == '/' %} active{% endif %}"> + <a href="{% url 'scipost:index' %}" class="nav-link">Home</a> + </li> + <li class="nav-item{% if '/journals/' in request.path %} active{% endif %}"> + <a href="{% url 'journals:journals' %}" class="nav-link">Journals</a> + </li> + <li class="nav-item{% if '/submissions/' in request.path %} active{% endif %}"> + <a class="nav-link" href="{% url 'submissions:submissions' %}">Submissions</a> + </li> + <li class="nav-item{% if '/commentaries/' in request.path %} active{% endif %}"> + <a class="nav-link" href="{% url 'commentaries:commentaries' %}">Commentaries</a> + </li> + <li class="nav-item{% if '/theses/' in request.path %} active{% endif %}"> + <a class="nav-link" href="{% url 'theses:theses' %}">Theses</a> + </li> + <li class="nav-item{% if '/about' in request.path %} active{% endif %}"> + <a class="nav-link" href="{% url 'scipost:about' %}">About SciPost</a> + </li> + + + + {% if user.is_authenticated %} + {% if request.user|is_in_group:'Testers' %} + <li class="nav-item highlighted navbar-counter"> + <div class="nav-link notifications_container"> + <a href="javascript:;" class="d-inline-block ml-1 badge_link" id="notifications_badge" data-toggle="popover"> + <span class="user">{% if user.last_name %}{% if user.contributor %}{{ user.contributor.get_title_display }} {% endif %}{{ user.first_name }} {{ user.last_name }}{% else %}{{ user.username }}{% endif %}</span> + <i class="fa fa-inbox" aria-hidden="true"></i> + {% live_notify_badge classes="badge badge-pill badge-primary" %} + </a> + {% live_notify_list %} + </div> + </li> - <li class="nav-item{% if '/theses/' in request.path %} active{% endif %}"> - <a class="nav-link" href="{% url 'theses:theses' %}">Theses</a> + {% else %} + <li class="nav-item highlighted"> + <span class="nav-link">Logged in as {{ user.username }}</span> </li> - <li class="nav-item{% if '/about' in request.path %} active{% endif %}"> - <a class="nav-link" href="{% url 'scipost:about' %}">About SciPost</a> + {% endif %} + <li class="nav-item"> + <a class="nav-link" href="{% url 'scipost:logout' %}">Logout</a> </li> - - {% if user.is_authenticated %} - {% if request.user|is_in_group:'Testers' %} - <li class="nav-item highlighted dropdown navbar-counter"> - <div class="nav-link"> - <a href="javascript:;" class="d-inline-block ml-1 dropdown-toggle notifications_container" id="notifications_badge"> - <span class="user">{% if user.last_name %}{% if user.contributor %}{{ user.contributor.get_title_display }} {% endif %}{{ user.first_name }} {{ user.last_name }}{% else %}{{ user.username }}{% endif %}</span> - <i class="fa fa-inbox" aria-hidden="true"></i> - {% live_notify_badge classes="badge badge-pill badge-primary" %} - </a> - {% live_notify_list classes="update_notifications d-none" %} - </div> - - </li> - {% else %} - <li class="nav-item highlighted"> - <span class="nav-link">Logged in as {{ user.username }}</span> + {% if perms.scipost.can_view_production %} + <li class="nav-item{% if '/production' in request.path %} active{% endif %}"> + <a class="nav-link" href="{% url 'production:production' %}">Production</a> </li> {% endif %} - <li class="nav-item"> - <a class="nav-link" href="{% url 'scipost:logout' %}">Logout</a> + {% 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> - {% if perms.scipost.can_view_production %} - <li class="nav-item{% if '/production' in request.path %} active{% endif %}"> - <a class="nav-link" href="{% url 'production:production' %}">Production</a> - </li> - {% endif %} - {% 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 %} - {% else %} - <li class="nav-item{% if request.path == '/login/' %} active{% endif %}"> - <a class="nav-link" href="{% url 'scipost:login' %}">Login</a> + {% 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 %} + {% else %} + <li class="nav-item{% if request.path == '/login/' %} active{% endif %}"> + <a class="nav-link" href="{% url 'scipost:login' %}">Login</a> + </li> + {% endif %} - </ul> - <form action="{% url 'scipost:search' %}" method="get" class="form-inline search-nav-form"> - <input class="form-control" id="id_q" maxlength="100" name="q" type="text" required="required" value="{{search_query|default:''}}"> - <input class="btn btn-secondary" type="submit" value="Search"> - </form> - </div> + </ul> + <form action="{% url 'scipost:search' %}" method="get" class="form-inline search-nav-form"> + <input class="form-control" id="id_q" maxlength="100" name="q" type="text" required="required" value="{{search_query|default:''}}"> + <input class="btn btn-secondary" type="submit" value="Search"> + </form> </nav> diff --git a/scipost/templates/scipost/personal_page.html b/scipost/templates/scipost/personal_page.html index 19522be6222256824b92c3703e6088d23d5ef881..90218f977a81b0820918adf894cdf10e7f7e6835 100644 --- a/scipost/templates/scipost/personal_page.html +++ b/scipost/templates/scipost/personal_page.html @@ -314,6 +314,11 @@ <li><a href="{% url 'mailing_lists:overview' %}">Manage mailing lists</a></li> {% endif %} </ul> + + <h3>SciPost Administation</h3> + <ul> + <li><a href="{% url 'affiliations:institutes' %}">Manage Institutes database</a></li> + </ul> {% endif %} {% if perms.scipost.can_view_timesheets %} @@ -354,10 +359,10 @@ <li><a href="{% url 'submissions:pool' %}">Submissions Pool</a></li> <li><a href="{% url 'submissions:treated_submissions_list' %}">Fully treated Submissions</a>{% if nr_treated_submissions_without_pdf %} ({{nr_treated_submissions_without_pdf}} unfinished){% endif %}</li> <li><a href="{% url 'journals:harvest_citedby_list' %}">Harvest citedby data</a></li> - <li><a href="{% url 'journals:manage_metadata' %}">Manage Publication metadata</a></li> - <li><a href="{% url 'journals:manage_report_metadata' %}">Manage Report metadata</a></li> <li><a href="{% url 'journals:manage_comment_metadata' %}">Manage Comment metadata</a></li> <li><a href="{% url 'colleges:fellowships' %}">Manage Fellowships</a></li> + <li><a href="{% url 'journals:manage_metadata' %}">Manage Publication metadata</a></li> + <li><a href="{% url 'journals:manage_report_metadata' %}">Manage Report metadata</a></li> <li><a href="{% url 'proceedings:proceedings' %}">Manage Proceedings Issues</a></li> </ul> {% endif %} diff --git a/scipost/templates/scipost/update_personal_data.html b/scipost/templates/scipost/update_personal_data.html index ab693489e3a48571ac07aba497bc6e9053c71445..42c0a5b383643ecb8169aea8ceb7ab6fa7dc51be 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 eb5722f819c06ea17cfc9bf104411aa73c30b2e6..0f323b43a26307842d431fa10cbc0ad1f8f25e32 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 9b222806a95855071b71a348e0b44614e44c1699..e122018ec1a75591f46e303ff89214c56b984525 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 diff --git a/submissions/templates/submissions/_report_tex_template.html b/submissions/templates/submissions/_report_tex_template.html index ceb16ce3c1644e379d0b935a45f29b7034851fc6..264dbc3a367163d594c6c1cc759e570a9325fb8b 100644 --- a/submissions/templates/submissions/_report_tex_template.html +++ b/submissions/templates/submissions/_report_tex_template.html @@ -48,7 +48,7 @@ Report by {% if report.anonymous %}Anonymous{% else %}{{report.author.user.first \begin{center} %%%%%%%%%% AFFILIATIONS -{\bf 1} {{report.author.affiliation}}\\ +{\bf 1} {{report.author.affiliation.name}}\\ \end{center} {% endif %}