diff --git a/colleges/templates/colleges/_potentialfellowship_card.html b/colleges/templates/colleges/_potentialfellowship_card.html index bef5812085a189c9488bb2c8b9fb2532a7122b7c..35737a4620c79577cd08ea756b3520d51523915c 100644 --- a/colleges/templates/colleges/_potentialfellowship_card.html +++ b/colleges/templates/colleges/_potentialfellowship_card.html @@ -2,37 +2,47 @@ <div class="card-body"> <div class="row"> - <div class="col-6"> - {% include 'profiles/_profile_card.html' with profile=potfel.profile %} + <div class="col-12"> + <h3 class="highlight">Potential Fellowship details for {{ potfel }}</h3> + <div id="profileAccordion"> + <div class="card"> + <div class="card-header" id="potfelProfile"> + <h4 class="mb-0"> + <button class="btn btn-link" data-toggle="collapse" data-target="#collapseProfile" aria-expanded="true" aria-controls="collapseProfile"> + View Profile + </button> + </h4> + </div> + <div id="collapseProfile" class="collapse" aria-labelledby="potfelProfile" data-parent="#profileAccordion"> + <div class="card-body"> + {% include 'profiles/_profile_card.html' with profile=potfel.profile %} + </div> + </div> + </div> + </div> </div> - <div class="col-6"> + </div> + <div class="row"> + <div class="col-md-6"> <ul> <li><a href="{% url 'colleges:potential_fellowship_update' pk=potfel.id %}">Update</a> the data</li> <li><a href="{% url 'colleges:potential_fellowship_delete' pk=potfel.id %}">Delete</a> this Potential Fellowship</li> <li><a href="{% url 'colleges:potential_fellowship_email_initial' pk=potfel.id %}">Prepare and send initial email</a></li> </ul> - </div> - </div> - <div class="row"> - <div class="col-md-6 ml-auto"> - <h3>Events</h3> - <ul> - {% for event in potfel.potentialfellowshipevent_set.all %} - {% include 'colleges/_potentialfellowship_event_li.html' with event=event %} - {% empty %} - <li>No events found.</li> - {% endfor %} - </ul> </div> - <div class="col-md-5"> + <div class="col-md-6"> <h3>Update the status of this Potential Fellowship</h3> <form class="d-block mt-2 mb-3" action="{% url 'colleges:potential_fellowship_update_status' pk=potfel.id %}" method="post"> {% csrf_token %} {{ pfstatus_form|bootstrap }} <input type="submit" name="submit" value="Update status" class="btn btn-outline-secondary"> </form> - <hr/> + </div> + </div> + + <div class="row"> + <div class="col-md-6"> <h3>Add an event for this Potential Fellowship</h3> <form class="d-block mt-2 mb-3" action="{% url 'colleges:potential_fellowship_event_create' pk=potfel.id %}" method="post"> {% csrf_token %} @@ -40,5 +50,15 @@ <input type="submit" name="submit" value="Submit" class="btn btn-outline-secondary"> </form> </div> + <div class="col-md-6"> + <h3>Events</h3> + <ul> + {% for event in potfel.potentialfellowshipevent_set.all %} + {% include 'colleges/_potentialfellowship_event_li.html' with event=event %} + {% empty %} + <li>No events found.</li> + {% endfor %} + </ul> + </div> </div> </div> diff --git a/colleges/templates/colleges/base.html b/colleges/templates/colleges/base.html new file mode 100644 index 0000000000000000000000000000000000000000..b02dd091ee5a212cbd810f90f4554beaba8ee882 --- /dev/null +++ b/colleges/templates/colleges/base.html @@ -0,0 +1,13 @@ +{% extends 'scipost/base.html' %} + +{% block breadcrumb %} + <div class="container-outside header"> + <div class="container"> + <nav class="breadcrumb hidden-sm-down"> + {% block breadcrumb_items %} + <a href="{% url 'colleges:potential_fellowships' %}" class="breadcrumb-item">Potential Fellowships</a> + {% endblock %} + </nav> + </div> + </div> +{% endblock %} diff --git a/colleges/templates/colleges/potentialfellowship_confirm_delete.html b/colleges/templates/colleges/potentialfellowship_confirm_delete.html index 36f86e25ab8d008a53c431b18e7af9613c755846..6f010dc92acb888cecece40988193b9938ae0adb 100644 --- a/colleges/templates/colleges/potentialfellowship_confirm_delete.html +++ b/colleges/templates/colleges/potentialfellowship_confirm_delete.html @@ -1,4 +1,4 @@ -{% extends 'scipost/base.html' %} +{% extends 'colleges/base.html' %} {% load bootstrap %} diff --git a/colleges/templates/colleges/potentialfellowship_detail.html b/colleges/templates/colleges/potentialfellowship_detail.html new file mode 100644 index 0000000000000000000000000000000000000000..9357ec706277609c0fb3f563a2e8e5fc68d5e93a --- /dev/null +++ b/colleges/templates/colleges/potentialfellowship_detail.html @@ -0,0 +1,21 @@ +{% extends 'colleges/base.html' %} + +{% load bootstrap %} + +{% block breadcrumb_items %} + {{ block.super }} +<span class="breadcrumb-item">Details</span> +{% endblock %} + +{% load scipost_extras %} + +{% block pagetitle %}: Potential Fellowship details{% endblock pagetitle %} + +{% block content %} +<div class="row"> + <div class="col-12"> + {% include 'colleges/_potentialfellowship_card.html' with potfel=object pfevent_form=pfevent_form %} + </div> +</div> + +{% endblock content %} diff --git a/colleges/templates/colleges/potentialfellowship_form.html b/colleges/templates/colleges/potentialfellowship_form.html index fa4e43c4fa4388ca9edec525e47d47fb7bada765..a80217f8798440bf22e9826c05c1ab07b9242826 100644 --- a/colleges/templates/colleges/potentialfellowship_form.html +++ b/colleges/templates/colleges/potentialfellowship_form.html @@ -1,7 +1,12 @@ -{% extends 'scipost/base.html' %} +{% extends 'colleges/base.html' %} {% load bootstrap %} +{% block breadcrumb_items %} + {{ block.super }} + <span class="breadcrumb-item">{% if form.instance.id %}Update{% else %}Add new{% endif %} Potential Fellowship</span> +{% endblock %} + {% block pagetitle %}: Potential Fellowships{% endblock pagetitle %} {% block content %} diff --git a/colleges/templates/colleges/potentialfellowship_list.html b/colleges/templates/colleges/potentialfellowship_list.html index 9b3cd2b9f5cd06b8e46158f938f1b209ad8ee291..0fa8ce291258052c86a14a6dc9d20599693b536f 100644 --- a/colleges/templates/colleges/potentialfellowship_list.html +++ b/colleges/templates/colleges/potentialfellowship_list.html @@ -1,14 +1,27 @@ -{% extends 'scipost/base.html' %} +{% extends 'colleges/base.html' %} {% load scipost_extras %} +{% load colleges_extras %} {% load bootstrap %} +{% block headsup %} +<script type="text/javascript"> +$(document).ready(function($) { + $(".table-row").click(function() { + window.document.location = $(this).data("href"); + }); +}); +</script> +{% endblock headsup %} + {% block pagetitle %}: Potential Fellowships{% endblock pagetitle %} {% block content %} <div class="row"> <div class="col-12"> + <a href="{% url 'colleges:potential_fellowship_create' %}">Add a Potential Fellowship</a> + <br/><br/> <p> <ul class="list-inline"> <li class="list-inline-item"> @@ -34,8 +47,6 @@ <div class="row"> <div class="col-12"> - <a href="{% url 'colleges:potential_fellowship_create' %}">Add a Potential Fellowship</a> - <br/><br/> {% if view.kwargs.discipline %} <h3>Potential Fellowships in {{ view.kwargs.discipline }}{% if view.kwargs.expertise %}, {{ view.kwargs.expertise }}{% endif %}:</h3> <br/> @@ -49,9 +60,9 @@ <th>Status</th> </tr> </thead> - <tbody id="accordion" role="tablist" aria-multiselectable="true"> + <tbody> {% for potfel in object_list %} - <tr data-toggle="collapse" data-parent="#accordion" href="#collapse{{ potfel.id }}" aria-expanded="false" aria-controls="collapse{{ potfel.id }}" style="cursor: pointer;"> + <tr class="table-row" data-href="{% url 'colleges:potential_fellowship_detail' pk=potfel.id %}" target="_blank" style="cursor: pointer;"> <td>{{ potfel.profile.last_name }}, {{ potfel.profile.get_title_display }} {{ potfel.profile.first_name }}</td> <td>{{ potfel.profile.get_discipline_display }}</td> <td> @@ -59,12 +70,7 @@ <div class="single d-inline" data-specialization="{{expertise|lower}}" data-toggle="tooltip" data-placement="bottom" title="{{expertise|get_specialization_display}}">{{expertise|get_specialization_code}}</div> {% endfor %} </td> - <td>{{ potfel.get_status_display }}</td> - </tr> - <tr id="collapse{{ potfel.id }}" class="collapse" role="tabpanel" aria-labelledby="heading{{ potfel.id }}" style="background-color: #fff;"> - <td colspan="4"> - {% include 'colleges/_potentialfellowship_card.html' with potfel=potfel pfevent_form=pfevent_form %} - </td> + <td style="color: #ffffff; background-color:{{ potfel.status|potfelstatuscolor }};">{{ potfel.get_status_display }}</td> </tr> {% empty %} <tr> @@ -73,6 +79,13 @@ {% endfor %} </tbody> </table> + + {% if is_paginated %} + <div class="col-12"> + {% include 'partials/pagination.html' with page_obj=page_obj %} + </div> + {% endif %} + </div> </div> {% endblock content %} diff --git a/colleges/templatetags/colleges_extras.py b/colleges/templatetags/colleges_extras.py new file mode 100644 index 0000000000000000000000000000000000000000..d0a10eceb9560daad8525c3d9e6be8ebda4c75ef --- /dev/null +++ b/colleges/templatetags/colleges_extras.py @@ -0,0 +1,48 @@ +__copyright__ = "Copyright 2016-2018, Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django import template + +from ..constants import ( + POTENTIAL_FELLOWSHIP_IDENTIFIED, POTENTIAL_FELLOWSHIP_INVITED, POTENTIAL_FELLOWSHIP_REINVITED, + POTENTIAL_FELLOWSHIP_MULTIPLY_REINVITED, POTENTIAL_FELLOWSHIP_DECLINED, + POTENTIAL_FELLOWSHIP_UNRESPONSIVE, POTENTIAL_FELLOWSHIP_RETIRED, POTENTIAL_FELLOWSHIP_DECEASED, + POTENTIAL_FELLOWSHIP_INTERESTED, POTENTIAL_FELLOWSHIP_REGISTERED, + POTENTIAL_FELLOWSHIP_ACTIVE_IN_COLLEGE, POTENTIAL_FELLOWSHIP_SCIPOST_EMERITUS + ) + +from common.utils import hslColorWheel + + +register = template.Library() + + +@register.filter(name='potfelstatuscolor') +def potfelstatuscolor(status): + color = '#333333' + if status == POTENTIAL_FELLOWSHIP_IDENTIFIED: + color = hslColorWheel(12, 8) + elif status == POTENTIAL_FELLOWSHIP_INVITED: + color = hslColorWheel(12, 9) + elif status == POTENTIAL_FELLOWSHIP_REINVITED: + color = hslColorWheel(12, 10) + elif status == POTENTIAL_FELLOWSHIP_MULTIPLY_REINVITED: + color = hslColorWheel(12, 11) + elif status == POTENTIAL_FELLOWSHIP_DECLINED: + color = hslColorWheel(12, 0) + elif status == POTENTIAL_FELLOWSHIP_UNRESPONSIVE: + color = hslColorWheel(12, 1) + elif status == POTENTIAL_FELLOWSHIP_RETIRED: + color = hslColorWheel(12, 1, 75) + elif status == POTENTIAL_FELLOWSHIP_DECEASED: + color = hslColorWheel(12, 1, 10) + elif status == POTENTIAL_FELLOWSHIP_INTERESTED: + color = hslColorWheel(12, 2) + elif status == POTENTIAL_FELLOWSHIP_REGISTERED: + color = hslColorWheel(12, 3) + elif status == POTENTIAL_FELLOWSHIP_ACTIVE_IN_COLLEGE: + color = hslColorWheel(12, 4) + elif status == POTENTIAL_FELLOWSHIP_SCIPOST_EMERITUS: + color = hslColorWheel(12, 4, 40, 40) + return color diff --git a/colleges/urls.py b/colleges/urls.py index fd290b952cc27883ac2b9bdd2794fd194a69b4c8..dc6fb8c3501531edd64a093e1e439306300ff2a8 100644 --- a/colleges/urls.py +++ b/colleges/urls.py @@ -65,15 +65,20 @@ urlpatterns = [ name='potential_fellowship_delete' ), url( - r'^potentialfellowships/(?P<pk>[0-9]+)/events/add$', + r'^potentialfellowships/(?P<pk>[0-9]+)/events/add/$', views.PotentialFellowshipEventCreateView.as_view(), name='potential_fellowship_event_create' ), url( - r'^potentialfellowships/(?P<pk>[0-9]+)/email/$', + r'^potentialfellowships/(?P<pk>[0-9]+)/email_initial/$', views.PotentialFellowshipInitialEmailView.as_view(), name='potential_fellowship_email_initial' ), + url( + r'^potentialfellowships/(?P<pk>[0-9]+)/$', + views.PotentialFellowshipDetailView.as_view(), + name='potential_fellowship_detail' + ), url( r'^potentialfellowships/(?P<discipline>[a-zA-Z]+)/(?P<expertise>[a-zA-Z:]+)/$', views.PotentialFellowshipListView.as_view(), diff --git a/colleges/views.py b/colleges/views.py index 293e04dd9596f619c44a114d5924238f14114aad..d62a106bc805a2ff34c0c6ce49b1fb562254e153 100644 --- a/colleges/views.py +++ b/colleges/views.py @@ -8,6 +8,7 @@ from django.shortcuts import get_object_or_404, render, redirect from django.core.urlresolvers import reverse, reverse_lazy from django.utils import timezone from django.utils.decorators import method_decorator +from django.views.generic.detail import DetailView from django.views.generic.edit import CreateView, UpdateView, DeleteView from django.views.generic.list import ListView @@ -24,7 +25,7 @@ from .forms import FellowshipForm, FellowshipTerminateForm, FellowshipRemoveSubm from .models import Fellowship, PotentialFellowship, PotentialFellowshipEvent from scipost.constants import SCIPOST_SUBJECT_AREAS -from scipost.mixins import PermissionsMixin +from scipost.mixins import PermissionsMixin, PaginationMixin from mails.forms import EmailTemplateForm from mails.views import MailView @@ -352,7 +353,7 @@ class PotentialFellowshipDeleteView(PermissionsMixin, DeleteView): success_url = reverse_lazy('colleges:potential_fellowships') -class PotentialFellowshipListView(PermissionsMixin, ListView): +class PotentialFellowshipListView(PermissionsMixin, PaginationMixin, ListView): """ List the PotentialFellowship object instances. """ @@ -374,6 +375,15 @@ class PotentialFellowshipListView(PermissionsMixin, ListView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['subject_areas'] = SCIPOST_SUBJECT_AREAS + return context + + +class PotentialFellowshipDetailView(PermissionsMixin, DetailView): + permission_required = 'scipost.can_manage_college_composition' + model = PotentialFellowship + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) context['pfstatus_form'] = PotentialFellowshipStatusForm() context['pfevent_form'] = PotentialFellowshipEventForm() return context diff --git a/common/forms.py b/common/forms.py index 3f1b8651d2b2c3d30e5389ec3d478f3f5a156df0..5fa2d913773a61c40c068238d3a7be01ab3cce36 100644 --- a/common/forms.py +++ b/common/forms.py @@ -6,6 +6,7 @@ import calendar import datetime import re +from django import forms from django.forms.widgets import Widget, Select from django.utils.dates import MONTHS from django.utils.safestring import mark_safe @@ -115,3 +116,8 @@ class MonthYearWidget(Widget): return '%s-%s-%s' % (y, m, d) return data.get(name, None) + + +class ModelChoiceFieldwithid(forms.ModelChoiceField): + def label_from_instance(self, obj): + return '%s (id = %i)' % (super().label_from_instance(obj), obj.id) diff --git a/common/utils.py b/common/utils.py index cdbb65c174398fa1f5b1df176cd9c9a534d23240..0edde7a732a107ba3beb620cbd66ed9ff8b64c43 100644 --- a/common/utils.py +++ b/common/utils.py @@ -7,6 +7,22 @@ from django.core.mail import EmailMultiAlternatives from django.template import loader +def hslColorWheel(N=10, index=0, saturation=50, lightness=50): + """ + Distributes colors into N values around a color wheel, + according to hue-saturation-lightness (HSL). + + index takes values from 0 to N-1. + """ + hue = int(index * 360/N % 360) + saturation = max(saturation, 0) + saturation = min(saturation, 100) + lightness = max(lightness, 0) + lightness = min(lightness, 100) + + return 'hsl(%s, %s%%, %s%%)' % (str(hue), str(saturation), str(lightness)) + + def workdays_between(datetime_from, datetime_until): """Return number of complete workdays. diff --git a/cronjob_production_eachhour.sh b/cronjob_production_eachhour.sh new file mode 100644 index 0000000000000000000000000000000000000000..47ae6e28c0d5c797807e4d65d363a9ecb1791c02 --- /dev/null +++ b/cronjob_production_eachhour.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Per minute cronjobs for production area + +cd /home/scipost/webapps/scipost/scipost_v1 +source venv/bin/activate + +# Do tasks +python3 manage.py check_celery diff --git a/journals/migrations/0053_auto_20181118_1758.py b/journals/migrations/0053_auto_20181118_1758.py new file mode 100644 index 0000000000000000000000000000000000000000..f2a41d010006dd96dcdc203e13e60f4abc5de665 --- /dev/null +++ b/journals/migrations/0053_auto_20181118_1758.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-11-18 16:58 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0052_journal_refereeing_period'), + ] + + operations = [ + migrations.AlterField( + model_name='unregisteredauthor', + name='profile', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='profiles.Profile'), + ), + ] diff --git a/journals/models.py b/journals/models.py index 8cab95489805a758d49b682f57d459ef30f19b85..f9d4e06292402763872dbade328c1fe0a2c87655 100644 --- a/journals/models.py +++ b/journals/models.py @@ -36,23 +36,12 @@ from scipost.fields import ChoiceArrayField class UnregisteredAuthor(models.Model): first_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100) - profile = models.OneToOneField( + profile = models.ForeignKey( 'profiles.Profile', on_delete=models.SET_NULL, null=True, blank=True) def __str__(self): return self.last_name + ', ' + self.first_name - def merge(self, unregistered_author): - """ - Merge another UnregisteredAuthor into this object. - """ - if unregistered_author == self: # Do nothing. - return - - self.profile = unregistered_author.profile - self.save() - unregistered_author.delete() - class PublicationAuthorsTable(models.Model): publication = models.ForeignKey('journals.Publication', related_name='authors') diff --git a/mails/admin.py b/mails/admin.py index d7d26b99e714c27c50f251942892f2fe9976d30c..6967486fda3158c00f55d1233d4b8b1d568be0a8 100644 --- a/mails/admin.py +++ b/mails/admin.py @@ -8,7 +8,7 @@ from .models import MailLog class MailLogAdmin(admin.ModelAdmin): - list_display = ['__str__', 'processed'] + list_display = ['__str__', 'to_recipients', 'created', 'processed'] readonly_fields = ('created', 'latest_activity') diff --git a/preprints/migrations/0009_auto_20181123_1000.py b/preprints/migrations/0009_auto_20181123_1000.py new file mode 100644 index 0000000000000000000000000000000000000000..8ef8494faec9d72742f6d402143c113a3998ffd5 --- /dev/null +++ b/preprints/migrations/0009_auto_20181123_1000.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-11-23 09:00 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('preprints', '0008_auto_20180913_2112'), + ] + + operations = [ + migrations.AlterModelOptions( + name='preprint', + options={'ordering': ['-identifier_w_vn_nr']}, + ), + ] diff --git a/preprints/models.py b/preprints/models.py index 41e89808518abb67c5cc6adce6963b5677c18015..443f8ddc584cbeab11181939437e09eb69258a7c 100644 --- a/preprints/models.py +++ b/preprints/models.py @@ -32,6 +32,10 @@ class Preprint(models.Model): modified = models.DateTimeField(auto_now=True) created = models.DateTimeField(auto_now_add=True) + class Meta: + ordering = ['-identifier_w_vn_nr'] + + def __str__(self): return 'Preprint {}'.format(self.identifier_w_vn_nr) diff --git a/preprints/views.py b/preprints/views.py index 9c291adb42e9a6cfc4cf3e729b96ff84d8e2f293..340afc48895810952fb842b9099badb7aa184c6b 100644 --- a/preprints/views.py +++ b/preprints/views.py @@ -1,6 +1,7 @@ __copyright__ = "Copyright 2016-2018, Stichting SciPost (SciPost Foundation)" __license__ = "AGPL v3" +import os from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404, redirect @@ -28,7 +29,13 @@ def preprint_pdf(request, identifier_w_vn_nr): contributor__user=request.user).exists(): raise Http404 - response = HttpResponse(preprint._file.read(), content_type='application/pdf') - filename = '{}.pdf'.format(preprint.identifier_w_vn_nr) - response['Content-Disposition'] = ('filename=' + filename) + __, extension = os.path.splitext(preprint._file.name) + if extension == '.pdf': + response = HttpResponse(preprint._file.read(), content_type='application/pdf') + filename = '{}.pdf'.format(preprint.identifier_w_vn_nr) + response['Content-Disposition'] = ('filename=' + filename) + else: + response = HttpResponse(preprint._file.read(), content_type='application/force-download') + filename = '{}{}'.format(preprint.identifier_w_vn_nr, extension) + response['Content-Disposition'] = ('filename=' + filename) return response diff --git a/profiles/admin.py b/profiles/admin.py index d00201908914d436c4e62a62e8e45a6275378b04..771c7c3b119dc26e079bbe7e7db65b47737a9853 100644 --- a/profiles/admin.py +++ b/profiles/admin.py @@ -13,6 +13,7 @@ class ProfileEmailInline(admin.TabularInline): class ProfileAdmin(admin.ModelAdmin): + list_display = ['__str__', 'email', 'discipline', 'expertises', 'has_active_contributor'] search_fields = ['first_name', 'last_name', 'emails__email', 'orcid_id'] inlines = [ProfileEmailInline] diff --git a/profiles/forms.py b/profiles/forms.py index 92862facaf33f881125b393d8c35e6e0dd2a1da1..0ec066920a5839c5e207eb1800df5f6cb4c4dfc5 100644 --- a/profiles/forms.py +++ b/profiles/forms.py @@ -5,6 +5,7 @@ __license__ = "AGPL v3" from django import forms from django.shortcuts import get_object_or_404 +from common.forms import ModelChoiceFieldwithid from invitations.models import RegistrationInvitation from journals.models import UnregisteredAuthor from ontology.models import Topic @@ -15,7 +16,7 @@ from .models import Profile, ProfileEmail class ProfileForm(forms.ModelForm): - email = forms.EmailField() + email = forms.EmailField(required=False) # If the Profile is created from an existing object (so we can update the object): instance_from_type = forms.CharField(max_length=32, required=False) instance_pk = forms.IntegerField(required=False) @@ -36,7 +37,7 @@ class ProfileForm(forms.ModelForm): def clean_email(self): """Check that the email isn't yet associated to an existing Profile.""" cleaned_email = self.cleaned_data['email'] - if ProfileEmail.objects.filter( + if cleaned_email and ProfileEmail.objects.filter( email=cleaned_email).exclude(profile__id=self.instance.id).exists(): raise forms.ValidationError('A Profile with this email already exists.') return cleaned_email @@ -53,10 +54,11 @@ class ProfileForm(forms.ModelForm): def save(self): profile = super().save() - profile.emails.update(primary=False) - email, __ = ProfileEmail.objects.get_or_create( - profile=profile, email=self.cleaned_data['email']) - profile.emails.filter(id=email.id).update(primary=True, still_valid=True) + if self.cleaned_data['email']: + profile.emails.update(primary=False) + email, __ = ProfileEmail.objects.get_or_create( + profile=profile, email=self.cleaned_data['email']) + profile.emails.filter(id=email.id).update(primary=True, still_valid=True) instance_pk = self.cleaned_data['instance_pk'] if instance_pk: if self.cleaned_data['instance_from_type'] == 'contributor': @@ -84,11 +86,6 @@ class SimpleProfileForm(ProfileForm): self.fields['accepts_refereeing_requests'].widget = forms.HiddenInput() -class ModelChoiceFieldwithid(forms.ModelChoiceField): - def label_from_instance(self, obj): - return '%s (id = %i)' % (super().label_from_instance(obj), obj.id) - - class ProfileMergeForm(forms.Form): to_merge = ModelChoiceFieldwithid(queryset=Profile.objects.all(), empty_label=None) to_merge_into = ModelChoiceFieldwithid(queryset=Profile.objects.all(), empty_label=None) @@ -102,10 +99,11 @@ class ProfileMergeForm(forms.Form): data = super().clean() if self.cleaned_data['to_merge'] == self.cleaned_data['to_merge_into']: self.add_error(None, 'A Profile cannot be merged into itself.') - if self.cleaned_data['to_merge'].has_contributor and \ - self.cleaned_data['to_merge_into'].has_contributor: - self.add_error(None, 'Each of these two Profiles has a Contributor. ' - 'Cannot merge. If these are distinct people or if two separate ' + if self.cleaned_data['to_merge'].has_active_contributor and \ + self.cleaned_data['to_merge_into'].has_active_contributor: + self.add_error(None, 'Each of these two Profiles has an active Contributor. ' + 'Merge the Contributors first.\n' + 'If these are distinct people or if two separate ' 'accounts are needed, a ProfileNonDuplicate instance should be created; ' 'contact techsupport.') return data @@ -118,27 +116,25 @@ class ProfileMergeForm(forms.Form): profile = self.cleaned_data['to_merge_into'] profile_old = self.cleaned_data['to_merge'] - # Merge scientific information from old Profile to the new Profile. + # Merge information from old to new Profile. profile.expertises = list( set(profile_old.expertises or []) | set(profile.expertises or [])) if profile.orcid_id is None: profile.orcid_id = profile_old.orcid_id if profile.webpage is None: profile.webpage = profile_old.webpage + if profile_old.has_active_contributor and not profile.has_active_contributor: + profile.contributor = profile_old.contributor profile.save() # Save all the field updates. profile.topics.add(*profile_old.topics.all()) - if hasattr(profile_old, 'unregisteredauthor') and profile_old.unregisteredauthor: - profile.unregisteredauthor.merge(profile_old.unregisteredauthor) + UnregisteredAuthor.objects.filter(profile=profile_old).update(profile=profile) - # Merge email and Contributor information + # Merge email profile_old.emails.exclude( email__in=profile.emails.values_list('email', flat=True)).update( primary=False, profile=profile) - if hasattr(profile_old, 'contributor') and profile_old.contributor: - profile.contributor = profile_old.contributor - profile.contributor.save() # Move all invitations to the "new" profile. profile_old.refereeinvitation_set.all().update(profile=profile) @@ -152,7 +148,7 @@ class ProfileEmailForm(forms.ModelForm): class Meta: model = ProfileEmail - fields = ['email', 'still_valid'] + fields = ['email', 'still_valid', 'primary'] def __init__(self, *args, **kwargs): self.profile = kwargs.pop('profile', None) diff --git a/profiles/migrations/0015_auto_20181118_0849.py b/profiles/migrations/0015_auto_20181118_0849.py new file mode 100644 index 0000000000000000000000000000000000000000..83aff5b2ddbf1c71623d30e3463424c11335943c --- /dev/null +++ b/profiles/migrations/0015_auto_20181118_0849.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-11-18 07:49 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0014_auto_20181110_0637'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='title', + field=models.CharField(blank=True, choices=[('PR', 'Prof.'), ('DR', 'Dr'), ('MR', 'Mr'), ('MRS', 'Mrs'), ('MS', 'Ms')], max_length=4, null=True), + ), + ] diff --git a/profiles/models.py b/profiles/models.py index f840a6ca0bad5c876a5815167b71f378317e8fad..93da3c30ebafd65a0c8367848581e800581c4c27 100644 --- a/profiles/models.py +++ b/profiles/models.py @@ -45,7 +45,7 @@ class Profile(models.Model): #. mark somebody as a non-referee (if that person does not want to referee for SciPost) """ - title = models.CharField(max_length=4, choices=TITLE_CHOICES) + title = models.CharField(max_length=4, choices=TITLE_CHOICES, blank=True, null=True) first_name = models.CharField(max_length=64) last_name = models.CharField(max_length=64) discipline = models.CharField(max_length=20, choices=SCIPOST_DISCIPLINES, @@ -70,20 +70,23 @@ class Profile(models.Model): ordering = ['last_name'] def __str__(self): - return '%s, %s %s' % (self.last_name, self.get_title_display(), self.first_name) + return '%s, %s %s' % (self.last_name, + self.get_title_display() if self.title != None else '', + self.first_name) @property def email(self): return getattr(self.emails.filter(primary=True).first(), 'email', '') @property - def has_contributor(self): - has_contributor = False + def has_active_contributor(self): + has_active_contributor = False try: - has_contributor = (self.contributor is not None) + has_active_contributor = (self.contributor is not None and + self.contributor.is_active) except Contributor.DoesNotExist: pass - return has_contributor + return has_active_contributor def get_absolute_url(self): return reverse('profiles:profile_detail', kwargs={'pk': self.id}) @@ -136,9 +139,9 @@ def get_profiles(slug): if tbl.contributor is not None] unreg_id_list = [tbl.unregistered_author.id for tbl in publications.all() \ if tbl.unregistered_author is not None] - print (unreg_id_list) return Profile.objects.filter(models.Q(contributor__id__in=cont_id_list) | - models.Q(unregisteredauthor__id__in=unreg_id_list)) + models.Q(unregisteredauthor__id__in=unreg_id_list + )).distinct() class ProfileNonDuplicates(models.Model): diff --git a/profiles/templates/profiles/_profile_card.html b/profiles/templates/profiles/_profile_card.html index 49698c21080a857be7bbad6498aa1984a4d05a34..503bd04d65b0f3df805f52d734073582574cbae0 100644 --- a/profiles/templates/profiles/_profile_card.html +++ b/profiles/templates/profiles/_profile_card.html @@ -1,28 +1,31 @@ {% load bootstrap %} {% load scipost_extras %} +{% load user_groups %} +{% is_edcol_admin request.user as is_edcol_admin %} +{% is_scipost_admin request.user as is_scipost_admin %} <div class="card"> <div class="card-header"> - Details + Details for profile {{ profile.id }} </div> <div class="card-body"> <table class="table"> <tr> <td>Name:</td> - <td>{{ profile.last_name }}, {{ profile.get_title_display }} {{ profile.first_name }}</td> + <td>{{ profile }}</td> </tr> <tr> <td>Email(s)</td> <td> <table class="table table-sm"> <thead> - <tr> - <th colspan="2">Email</th> - <th>Still valid</th> - <th></th> - </tr> + <tr> + <th colspan="2">Email</th> + <th>Still valid</th> + <th></th> + </tr> </thead> {% for profile_mail in profile.emails.all %} <tr> @@ -56,7 +59,7 @@ <tr><td>Webpage</td><td><a href="{{ profile.webpage }}" target="_blank">{{ profile.webpage }}</a></td></tr> <tr><td>Accepts SciPost emails</td><td>{{ profile.accepts_SciPost_emails }}</td></tr> <tr><td>Accepts refereeing requests</td><td>{{ profile.accepts_refereeing_requests }}</td></tr> - <tr><td>Contributor</td><td>{% if profile.contributor %}Yes (<a href="{% url 'scipost:contributor_info' contributor_id=profile.contributor.id %}" target="_blank">info link</a>){% else %}No{% endif %}</td></tr> + <tr><td>Contributor</td><td>{% if profile.contributor %}Yes, id: {{ profile.contributor.pk }}, status: {{ profile.contributor.get_status_display }}, user active: {{ profile.contributor.user.is_active }} (<a href="{% url 'scipost:contributor_info' contributor_id=profile.contributor.id %}" target="_blank">info link</a>){% else %}No{% endif %}</td></tr> </table> </div> </div> @@ -65,43 +68,51 @@ <div class="card"> <div class="card-header"> - Actions + Publications </div> <div class="card-body"> <ul> - <li><a href="{% url 'profiles:profile_update' pk=profile.id %}">Update</a> this Profile</li> - <li><a href="{% url 'profiles:profile_delete' pk=profile.id %}" class="text-danger">Delete</a> this Profile</li> - {% if email_form %} - <li> - <div> - Add an email to this Profile: - <form action="{% url 'profiles:add_profile_email' profile_id=profile.id %}" method="post"> - {% csrf_token %} - {{ email_form|bootstrap }} - <input class="btn btn-outline-secondary" type="submit" value="Add"> - </form> - </div> - </li> - {% endif %} + {% for pub in profile.publications.all %} + <li><a href="{{ pub.get_absolute_url }}">{{ pub.citation }}</a></li> + {% empty %} + <li>No Publication found</li> + {% endfor %} </ul> </div> </div> <div class="card"> <div class="card-header"> - Refereeing invitations + Comments </div> <div class="card-body"> - <ul> - {% for inv in profile.refereeinvitation_set.all %} - <li>{{ inv.submission.title }}<br/>(invited {{ inv.date_invited }}; fulfilled: {% if inv.fulfilled %}<i class="fa fa-check-square text-success"></i>{% else %}<i class="fa fa-times-circle"></i>{% endif %})</li> - {% empty %} - <li>No refereeing invitation found</li> - {% endfor %} - </ul> + {% for comment in profile.comments.all %} + <li><a href="{{ comment.get_absolute_url }}">{{ comment }}</a></li> + {% empty %} + <li>No Comment found</li> + {% endfor %} </div> </div> + <div class="card"> + <div class="card-header"> + Theses + </div> + <div class="card-body"> + {% for thesis in profile.theses.all %} + <li><a href="{{ thesis.get_absolute_url }}">{{ thesis }}</a></li> + {% empty %} + <li>No Thesis found</li> + {% endfor %} + </div> + </div> + +</div> + +{% if is_scipost_admin or is_edcol_admin %} +<h4 class="highlight p-2 text-danger">Admin-level info</h4> +<div class="card-columns"> + <div class="card"> <div class="card-header"> Registration invitations @@ -109,7 +120,7 @@ <div class="card-body"> <ul> {% for reginv in profile.registrationinvitation_set.all %} - <li>{{ reginv }}</li> + <li>{{ reginv }}<br/>status: {{ reginv.get_status_display }}</li> {% empty %} <li>No invitation found</li> {% endfor %} @@ -119,43 +130,70 @@ <div class="card"> <div class="card-header"> - Publications + Fellowships and Potential Fellowships </div> <div class="card-body"> + <h5>Fellowships</h5> <ul> - {% for pub in profile.publications.all %} - <li><a href="{{ pub.get_absolute_url }}">{{ pub.citation }}</a></li> + {% for fellowship in profile.contributor.fellowships.all %} + <li><a href="{{ fellowship.get_absolute_url }}">{{ fellowship }}</a></li> {% empty %} - <li>No Publication found</li> + <li>No fellowships found</li> + {% endfor %} + </ul> + <h5>Potential Fellowships</h5> + <ul> + {% for potfellowship in profile.potentialfellowship_set.all %} + <li>{{ potfellowship }}</li> + {% empty %} + <li>No Potential Fellowships found</li> {% endfor %} </ul> </div> </div> + + <div class="card"> <div class="card-header"> - Comments + Refereeing invitations </div> <div class="card-body"> - {% for comment in profile.comments.all %} - <li><a href="{{ comment.get_absolute_url }}">{{ comment }}</a></li> - {% empty %} - <li>No Comment found</li> - {% endfor %} + <ul> + {% for inv in profile.refereeinvitation_set.all %} + <li>{{ inv.submission.title }}<br/>(invited {{ inv.date_invited }}; fulfilled: {% if inv.fulfilled %}<i class="fa fa-check-square text-success"></i>{% else %}<i class="fa fa-times-circle"></i>{% endif %})</li> + {% empty %} + <li>No refereeing invitation found</li> + {% endfor %} + </ul> </div> </div> +</div> +<div class="card-columns"> <div class="card"> <div class="card-header"> - Theses + Actions </div> <div class="card-body"> - {% for thesis in profile.theses.all %} - <li><a href="{{ thesis.get_absolute_url }}">{{ thesis }}</a></li> - {% empty %} - <li>No Thesis found</li> - {% endfor %} + <ul> + <li><a href="{% url 'profiles:profile_update' pk=profile.id %}">Update</a> this Profile</li> + <li><a href="{% url 'profiles:profile_delete' pk=profile.id %}" class="text-danger">Delete</a> this Profile</li> + {% if email_form %} + <li> + <div> + Add an email to this Profile: + <form class="form-inline" action="{% url 'profiles:add_profile_email' profile_id=profile.id %}" method="post"> + {% csrf_token %} + {{ email_form|bootstrap }} + <input class="btn btn-outline-secondary" type="submit" value="Add"> + </form> + </div> + </li> + {% endif %} + </ul> </div> </div> </div> +{% endif %} diff --git a/profiles/templates/profiles/profile_form.html b/profiles/templates/profiles/profile_form.html index 1f8b76b6cbc9edf97e51333ff71ee8bd377103b5..b5e021cfcd8da36d25df6b56fd5325c926068176 100644 --- a/profiles/templates/profiles/profile_form.html +++ b/profiles/templates/profiles/profile_form.html @@ -4,7 +4,7 @@ {% block breadcrumb_items %} {{ block.super }} - <span class="breadcrumb-item">{% if form.instance.id %}Update {{ form.instance }}{% else %}Add new Profile{% endif %}</span> + <span class="breadcrumb-item">{% if form.instance.id %}Update {{ form.instance }}{% else %}Add new Profile {% if from_type %}(from {{ from_type }}){% endif %}{% endif %}</span> {% endblock %} {% block pagetitle %}: Profiles{% endblock pagetitle %} @@ -16,7 +16,7 @@ <h4>Matching profiles found for this {{ from_type }}</h4> <ul> {% for matching_profile in matching_profiles %} - <li>{{ matching_profile }} <a href="{% url 'profiles:profile_match' profile_id=matching_profile.id from_type=from_type pk=pk %}">match this {{ from_type }} to this Profile</a> + <li>{{ matching_profile }} (id {{ matching_profile.id }}, {{ matching_profile.email }}) <a href="{% url 'profiles:profile_match' profile_id=matching_profile.id from_type=from_type pk=pk %}"><i class="fa fa-arrow-right"></i> Match this {{ from_type }} to this Profile</a> </li> {% endfor %} </ul> diff --git a/profiles/templates/profiles/profile_list.html b/profiles/templates/profiles/profile_list.html index b0539904e8a23e4d9db4146097ef799f654a0c74..a28bbeed51d5a5a44e2a17663263925a58838c94 100644 --- a/profiles/templates/profiles/profile_list.html +++ b/profiles/templates/profiles/profile_list.html @@ -3,6 +3,8 @@ {% load bootstrap %} {% load add_get_parameters %} {% load scipost_extras %} +{% load user_groups %} + {% block breadcrumb_items %} {{ block.super }} @@ -22,82 +24,102 @@ $(document).ready(function($) { {% block pagetitle %}: Profiles{% endblock pagetitle %} {% block content %} + +{% is_edcol_admin request.user as is_edcol_admin %} +{% is_scipost_admin request.user as is_scipost_admin %} + <div class="row"> <div class="col-12"> <h4>Profiles-related Actions:</h4> <ul> - <li><a href="{% url 'profiles:duplicates' %}">Check for duplicates</a></li> - {% if contributors_w_duplicate_email %} - <li class="text-danger"> - {{ contributors_w_duplicate_email|length }} Contributor duplicates (via email) identified - <ul> - {% for dup in contributors_w_duplicate_email %} - <li>{{ dup }}, {{ dup.user.email }}, id {{ dup.id }}</li> - {% empty %} - <li>No duplicates found</li> - {% endfor %} - </ul> - Please take action by merging these Contributor instances. - </li> - {% endif %} - <li><a href="{% url 'profiles:profile_create' %}">Add a Profile</a></li> - {% if next_reginv_wo_profile %} - <li>Create a Profile for <a href="{% url 'profiles:profile_create' from_type='registrationinvitation' pk=next_reginv_wo_profile.id %}">the next</a> Registration Invitation without one ({{ nr_reginv_wo_profile }} to handle)</li> - {% endif %} - {% if next_contributor_wo_profile %} - <li>Create a Profile for <a href="{% url 'profiles:profile_create' from_type='contributor' pk=next_contributor_wo_profile.id %}">the next</a> Contributor without one ({{ nr_contributors_wo_profile }} to handle)</li> - {% endif %} - {% if next_unreg_auth_wo_profile %} - <li>Create a Profile for <a href="{% url 'profiles:profile_create' from_type='unregisteredauthor' pk=next_unreg_auth_wo_profile.id %}">the next</a> UnregisteredAuthor without one ({{ nr_unreg_auth_wo_profile }} to handle)</li> - {% endif %} - {% if next_refinv_wo_profile %} - <li>Create a Profile for <a href="{% url 'profiles:profile_create' from_type='refereeinvitation' pk=next_refinv_wo_profile.id %}">the next</a> Referee Invitation without one ({{ nr_refinv_wo_profile }} to handle)</li> - {% endif %} - </ul> - <h4>Specialize the list:</h4> - <ul> - <li> - <ul class="list-inline"> - <li class="list-inline-item"> - <a href="{% url 'profiles:profiles' %}">View all</a> or view by discipline/subject area: - </li> - {% for discipline in subject_areas %} - <li class="list-inline-item"> - <div class="dropdown"> - <button class="btn btn-primary dropdown-toggle" type="button" id="dropdownMenuButton{{ discipline.0|cut:" " }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ discipline.0 }}</button> - <div class="dropdown-menu" aria-labelledby="dropdownMenuButton{{ discipline.0|cut:" " }}"> - <a class="dropdown-item" href="{% add_get_parameters discipline=discipline.0|cut:' ' %}">View all in {{ discipline.0 }}</a> - {% for area in discipline.1 %} - <a class="dropdown-item" href="{% add_get_parameters discipline=discipline.0|cut:' ' expertise=area.0 %}">{{ area.0 }}</a> - {% endfor %} - </div> + {% if is_scipost_admin or is_edcol_admin %} + {% if nr_contributors_w_duplicate_names > 0 %} + <li><i class="fa fa-exclamation-circle text-warning"></i> <a href="{% url 'scipost:contributor_duplicates' %}?kind=names">Handle Contributors with duplicate names ({{ nr_contributors_w_duplicate_names }} to handle)</a></li> + {% else %} + <li><i class="fa fa-check-circle text-success"></i> No name-duplicate Contributors found</li> + {% endif %} + {% if nr_contributors_w_duplicate_emails > 0 %} + <li><a href="{% url 'scipost:contributor_duplicates' %}?kind=names">Handle Contributors with duplicate emails ({{ nr_contributors_w_duplicate_emails }} to handle)</a></li> + {% else %} + <li><i class="fa fa-check-circle text-success"></i> No email-duplicate Contributors found</li> + {% endif %} + {% if next_contributor_wo_profile %} + <li><i class="fa fa-exclamation-circle text-warning"></i> Create a Profile for <a href="{% url 'profiles:profile_create' from_type='contributor' pk=next_contributor_wo_profile.id %}">the next</a> Contributor without one ({{ nr_contributors_wo_profile }} to handle)</li> + {% else %} + <li><i class="fa fa-check-circle text-success"></i> All registered Contributors have a Profile</li> + {% endif %} + {% if nr_potential_duplicate_profiles > 0 %} + <li><i class="fa fa-exclamation-circle text-warning"></i> <a href="{% url 'profiles:duplicates' %}">Check for duplicate Profiles ({{ nr_potential_duplicate_profiles }} to handle)</a></li> + {% else %} + <li><i class="fa fa-check-circle text-success"></i> No potential duplicate Profiles detected</li> + {% endif %} + {% if next_reginv_wo_profile %} + <li><i class="fa fa-exclamation-circle text-warning"></i> Create a Profile for <a href="{% url 'profiles:profile_create' from_type='registrationinvitation' pk=next_reginv_wo_profile.id %}">the next</a> Registration Invitation without one ({{ nr_reginv_wo_profile }} to handle)</li> + {% else %} + <li><i class="fa fa-check-circle text-success"></i> All Registration Invitations have a Profile</li> + {% endif %} + {% if next_unreg_auth_wo_profile %} + <li><i class="fa fa-exclamation-circle text-warning"></i> Create a Profile for <a href="{% url 'profiles:profile_create' from_type='unregisteredauthor' pk=next_unreg_auth_wo_profile.id %}">the next</a> UnregisteredAuthor without one ({{ nr_unreg_auth_wo_profile }} to handle)</li> + {% else %} + <li><i class="fa fa-check-circle text-success"></i> All UnregisteredAuthors have a Profile</li> + {% endif %} + {% if next_refinv_wo_profile %} + <li><i class="fa fa-exclamation-circle text-warning"></i> Create a Profile for <a href="{% url 'profiles:profile_create' from_type='refereeinvitation' pk=next_refinv_wo_profile.id %}">the next</a> Referee Invitation without one ({{ nr_refinv_wo_profile }} to handle)</li> + {% else %} + <li><i class="fa fa-check-circle text-success"></i> All Referee Invitations have a Profile</li> + {% endif %} + {% endif %} + <li><a href="{% url 'profiles:profile_create' %}">Add a Profile</a></li> + </ul> + </div> +</div> + +<div class="row"> + <div class="col-12"> + <h4>Specialize the list:</h4> + <ul> + <li> + <ul class="list-inline"> + <li class="list-inline-item"> + <a href="{% url 'profiles:profiles' %}">View all</a> or view by discipline/subject area: + </li> + {% for discipline in subject_areas %} + <li class="list-inline-item"> + <div class="dropdown"> + <button class="btn btn-primary dropdown-toggle" type="button" id="dropdownMenuButton{{ discipline.0|cut:" " }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ discipline.0 }}</button> + <div class="dropdown-menu" aria-labelledby="dropdownMenuButton{{ discipline.0|cut:" " }}"> + <a class="dropdown-item" href="{% add_get_parameters discipline=discipline.0|cut:' ' %}">View all in {{ discipline.0 }}</a> + {% for area in discipline.1 %} + <a class="dropdown-item" href="{% add_get_parameters discipline=discipline.0|cut:' ' expertise=area.0 %}">{{ area.0 }}</a> + {% endfor %} </div> - </li> - {% endfor %} - </ul> - </li> - <li>View only Profiles <a href="{% add_get_parameters contributor=True %}">with</a> or <a href="{% add_get_parameters contributor=False %}">without</a> an associated Contributor</li> - <li> - <ul class="list-inline"> - <li class="list-inline-item">Last name startswith:</li> - <li class="list-inline-item"> - <form action="" method="get">{{ searchform }} - {% if request.GET.discipline %} - <input type="hidden" name="discipline" value="{{ request.GET.discipline }}"> - {% if request.GET.expertise %} - <input type="hidden" name="expertise" value="{{ request.GET.expertise }}"> - {% endif %} - {% endif %} - {% if request.GET.contributor %} - <input type="hidden" name="contributor" value="{{ request.GET.contributor }}"> - {% endif %} - </li> - <li class="list-inline-item"><input class="btn btn-outline-secondary" type="submit" value="Search"></form> - </li> - </ul> - </li> + </div> + </li> + {% endfor %} + </ul> + </li> + <li>View only Profiles <a href="{% add_get_parameters contributor=True %}">with</a> or <a href="{% add_get_parameters contributor=False %}">without</a> an associated Contributor</li> + <li> + <ul class="list-inline"> + <li class="list-inline-item">Last name startswith:</li> + <li class="list-inline-item"> + <form action="" method="get">{{ searchform }} + {% if request.GET.discipline %} + <input type="hidden" name="discipline" value="{{ request.GET.discipline }}"> + {% if request.GET.expertise %} + <input type="hidden" name="expertise" value="{{ request.GET.expertise }}"> + {% endif %} + {% endif %} + {% if request.GET.contributor %} + <input type="hidden" name="contributor" value="{{ request.GET.contributor }}"> + {% endif %} + </li> + <li class="list-inline-item"><input class="btn btn-outline-secondary" type="submit" value="Search"></form> + </li> </ul> - </div> +</li> +</ul> +</div> </div> <div class="row"> @@ -117,14 +139,14 @@ $(document).ready(function($) { <tbody> {% for profile in object_list %} <tr class="table-row" data-href="{% url 'profiles:profile_detail' pk=profile.id %}" target="_blank" style="cursor: pointer;"> - <td>{{ profile.last_name }}, {{ profile.get_title_display }} {{ profile.first_name }}</td> + <td>{{ profile }}</td> <td>{{ profile.get_discipline_display }}</td> <td> {% for expertise in profile.expertises %} <div class="single d-inline" data-specialization="{{expertise|lower}}" data-toggle="tooltip" data-placement="bottom" title="{{expertise|get_specialization_display}}">{{expertise|get_specialization_code}}</div> {% endfor %} </td> - <td>{% if profile.contributor %}<i class="fa fa-check-circle text-success"></i>{% else %}<i class="fa fa-times-circle text-danger"></i>{% endif %}</td> + <td>{% if profile.has_active_contributor %}<i class="fa fa-check-circle text-success"></i>{% else %}<i class="fa fa-times-circle text-danger"></i>{% endif %}</td> </tr> {% empty %} <tr> diff --git a/profiles/templates/profiles/profile_merge.html b/profiles/templates/profiles/profile_merge.html index 12cc944d6516421265fcfa9e221f70e01dd338aa..3952b44fda30e416f12293ef68d3b751daab1501 100644 --- a/profiles/templates/profiles/profile_merge.html +++ b/profiles/templates/profiles/profile_merge.html @@ -16,6 +16,10 @@ <div class="row"> <div class="col-12"> <h1 class="highlight">Merge Profiles {{ profile_to_merge.id }} and {{ profile_to_merge_into.id }}</h1> + {% if profile_to_merge.has_active_contributor and not profile_to_merge_into.has_active_contributor %} + <h3 class="text-danger">Warning: the Profile to merge is associated to an active Contributor, while the one to merge into is not</h3> + <p>Consider <a href="{% url 'profiles:merge' %}?to_merge={{ profile_to_merge_into.id }}&to_merge_into={{ profile_to_merge.id }}" method="get">merging the other way around</a></p> + {% endif %} </div> </div> <div class="row"> @@ -38,6 +42,7 @@ {% csrf_token %} {{ merge_form|bootstrap }} <input class="btn btn-primary" type="submit" value="Confirm merge"> + <a class="text-warning" href="{% url 'profiles:merge' %}?to_merge={{ profile_to_merge_into.id }}&to_merge_into={{ profile_to_merge.id }}" method="get">Merge the other way around</a></p> </form> </div> </div> diff --git a/profiles/views.py b/profiles/views.py index 991e203198fdbdb9967aeb1dc76912e85e3d2f4b..4ece47f98d620478e2d9520997534d56f85ba973 100644 --- a/profiles/views.py +++ b/profiles/views.py @@ -6,7 +6,7 @@ from django.contrib import messages from django.core.urlresolvers import reverse, reverse_lazy from django.db import transaction from django.db.models import Q -from django.http import Http404 +from django.http import Http404, HttpResponseRedirect from django.shortcuts import get_object_or_404, render, redirect from django.views.decorators.http import require_POST from django.views.generic.detail import DetailView @@ -70,7 +70,8 @@ class ProfileCreateView(PermissionsMixin, CreateView): matching_profiles = matching_profiles.filter( Q(last_name=reginv.last_name) | Q(emails__email__in=reginv.email)) - context['matching_profiles'] = matching_profiles[:10] + context['matching_profiles'] = matching_profiles.distinct().order_by( + 'last_name', 'first_name') return context def get_initial(self): @@ -131,29 +132,52 @@ class ProfileCreateView(PermissionsMixin, CreateView): def profile_match(request, profile_id, from_type, pk): """ Links an existing Profile to one of existing - Contributor, UnregisteredAuthor, RefereeInvitation, RegistrationInvitation. + Contributor, UnregisteredAuthor, RefereeInvitation or RegistrationInvitation. + + Profile relates to Contributor as OneToOne. + Matching is thus only allowed if there are no duplicate objects for these elements. + + For matching the Profile to a Contributor, the following preconditions are defined: + - the Profile has no association to another Contributor + - the Contributor has no association to another Profile + If these are not met, no action is taken. """ profile = get_object_or_404(Profile, pk=profile_id) + nr_rows = 0 if from_type == 'contributor': + if hasattr(profile, 'contributor') and profile.contributor.id != pk: + messages.error(request, + 'Error: cannot math this Profile to this Contributor, ' + 'since this Profile already has a different Contributor.\n' + 'Please merge the duplicate Contributors first.') + return redirect(reverse('profiles:profiles')) contributor = get_object_or_404(Contributor, pk=pk) - contributor.profile = profile - contributor.save() - messages.success(request, 'Profile matched with Contributor') + if contributor.profile and contributor.profile.id != profile.id: + messages.error(request, + 'Error: cannot match this Profile to this Contributor, ' + 'since this Contributor already has a different Profile.\n' + 'Please merge the duplicate Profiles first.') + return redirect(reverse('profiles:profiles')) + # Preconditions are met, match: + nr_rows = Contributor.objects.filter(pk=pk).update(profile=profile) + # Give priority to the email coming from Contributor + profile.emails.update(primary=False) + email, __ = ProfileEmail.objects.get_or_create( + profile=profile, email=contributor.user.email) + profile.emails.filter(id=email.id).update(primary=True, still_valid=True) elif from_type == 'unregisteredauthor': - unreg_auth = get_object_or_404(UnregisteredAuthor, pk=pk) - unreg_auth.profile = profile - unreg_auth.save() - messages.success(request, 'Profile matched with UnregisteredAuthor') + nr_rows = UnregisteredAuthor.objects.filter(pk=pk).update(profile=profile) elif from_type == 'refereeinvitation': - ref_inv = get_object_or_404(RefereeInvitation, pk=pk) - ref_inv.profile = profile - ref_inv.save() - messages.success(request, 'Profile matched with RefereeInvitation') + nr_rows = RefereeInvitation.objects.filter(pk=pk).update(profile=profile) elif from_type == 'registrationinvitation': - reg_inv = get_object_or_404(RegistrationInvitation, pk=pk) - reg_inv.profile = profile - reg_inv.save() - messages.success(request, 'Profile matched with RegistrationInvitation') + nr_rows = RegistrationInvitation.objects.filter(pk=pk).update(profile=profile) + if nr_rows == 1: + messages.success(request, 'Profile matched with %s' % from_type) + else: + messages.error( + request, + 'Error: Profile matching with %s: updated %s rows instead of 1!' + 'Please contact techsupport' % (from_type, nr_rows)) return redirect(reverse('profiles:profiles')) @@ -214,7 +238,10 @@ class ProfileListView(PermissionsMixin, PaginationMixin, ListView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - contributors_wo_profile = Contributor.objects.filter(profile__isnull=True) + contributors_w_duplicate_email = Contributor.objects.with_duplicate_email() + contributors_w_duplicate_names = Contributor.objects.with_duplicate_names() + contributors_wo_profile = Contributor.objects.active().filter(profile__isnull=True) + nr_potential_duplicate_profiles = Profile.objects.potential_duplicates().count() unreg_auth_wo_profile = UnregisteredAuthor.objects.filter(profile__isnull=True) refinv_wo_profile = RefereeInvitation.objects.filter(profile__isnull=True) reginv_wo_profile = RegistrationInvitation.objects.filter(profile__isnull=True) @@ -222,8 +249,10 @@ class ProfileListView(PermissionsMixin, PaginationMixin, ListView): context.update({ 'subject_areas': SCIPOST_SUBJECT_AREAS, 'searchform': SearchTextForm(initial={'text': self.request.GET.get('text')}), - 'contributors_w_duplicate_email': Contributor.objects.have_duplicate_email(), + 'nr_contributors_w_duplicate_emails': contributors_w_duplicate_email.count(), + 'nr_contributors_w_duplicate_names': contributors_w_duplicate_names.count(), 'nr_contributors_wo_profile': contributors_wo_profile.count(), + 'nr_potential_duplicate_profiles': nr_potential_duplicate_profiles, 'next_contributor_wo_profile': contributors_wo_profile.first(), 'nr_unreg_auth_wo_profile': unreg_auth_wo_profile.count(), 'next_unreg_auth_wo_profile': unreg_auth_wo_profile.first(), @@ -269,15 +298,29 @@ def profile_merge(request): merge_form = ProfileMergeForm(request.POST or None, initial=request.GET) context = {'merge_form': merge_form} - if merge_form.is_valid(): - profile = merge_form.save() - messages.success(request, 'Profiles merged') - return redirect(profile.get_absolute_url()) + if request.method == 'POST': + if merge_form.is_valid(): + profile = merge_form.save() + messages.success(request, 'Profiles merged') + return redirect(profile.get_absolute_url()) + else: + try: + context.update({ + 'profile_to_merge': get_object_or_404( + Profile, pk=merge_form.cleaned_data['to_merge'].id), + 'profile_to_merge_into': get_object_or_404( + Profile, pk=merge_form.cleaned_data['to_merge_into'].id) + }) + except ValueError: + raise Http404 + elif request.method == 'GET': try: context.update({ - 'profile_to_merge': get_object_or_404(Profile, pk=int(request.GET['to_merge'])), - 'profile_to_merge_into': get_object_or_404(Profile, pk=int(request.GET['to_merge_into'])) + 'profile_to_merge': get_object_or_404(Profile, + pk=int(request.GET['to_merge'])), + 'profile_to_merge_into': get_object_or_404(Profile, + pk=int(request.GET['to_merge_into'])) }) except ValueError: raise Http404 @@ -298,7 +341,9 @@ def add_profile_email(request, profile_id): else: for field, err in form.errors.items(): messages.warning(request, err[0]) - return redirect(reverse('profiles:profiles')) + if request.POST.get('next', None): + return HttpResponseRedirect(request.POST.get('next')) + return redirect(profile.get_absolute_url()) @require_POST @@ -321,7 +366,7 @@ def toggle_email_status(request, email_id): profile_email = get_object_or_404(ProfileEmail, pk=email_id) ProfileEmail.objects.filter(id=email_id).update(still_valid=not profile_email.still_valid) messages.success(request, 'Email updated') - return redirect('profiles:profiles') + return redirect(profile_email.profile.get_absolute_url()) @require_POST @@ -331,4 +376,4 @@ def delete_profile_email(request, email_id): profile_email = get_object_or_404(ProfileEmail, pk=email_id) profile_email.delete() messages.success(request, 'Email deleted') - return redirect('profiles:profiles') + return redirect(profile_email.profile.get_absolute_url()) diff --git a/scipost/admin.py b/scipost/admin.py index 097b6c7f38f195d2998fb863965c5f2e6d085719..5eeb7a855d3aee19a0c8f6b606f1eb37610d15ae 100644 --- a/scipost/admin.py +++ b/scipost/admin.py @@ -41,8 +41,14 @@ class UserAdmin(UserAdmin): ContactToUserInline, ProductionUserInline ] + list_display = ['username', 'email', 'first_name', 'last_name', + 'is_active', 'is_staff', 'is_duplicate'] search_fields = ['last_name', 'email'] + def is_duplicate(self, obj): + return obj.contributor.is_duplicate + is_duplicate.short_description = 'Is duplicate?' + is_duplicate.boolean = True admin.site.unregister(User) admin.site.register(Contributor, ContributorAdmin) diff --git a/scipost/forms.py b/scipost/forms.py index dbef578daffee3f5bb9830ccde44ca8448360fc3..f2c7ed99a716539fdf156a0b47dd8a953f4b26a7 100644 --- a/scipost/forms.py +++ b/scipost/forms.py @@ -29,15 +29,24 @@ from .constants import ( BARRED) from .decorators import has_contributor from .fields import ReCaptchaField -from .models import Contributor, DraftInvitation, UnavailabilityPeriod, PrecookedEmail +from .models import Contributor, DraftInvitation, UnavailabilityPeriod, \ + Remark, AuthorshipClaim, PrecookedEmail from affiliations.models import Affiliation, Institution -from common.forms import MonthYearWidget +from common.forms import MonthYearWidget, ModelChoiceFieldwithid from partners.decorators import has_contact +from colleges.models import Fellowship, PotentialFellowshipEvent +from commentaries.models import Commentary from comments.models import Comment -from journals.models import Publication -from submissions.models import Report +from funders.models import Grant +from invitations.models import CitationNotification +from journals.models import PublicationAuthorsTable, Publication +from mails.utils import DirectMailUtil +from submissions.models import Submission, EditorialAssignment, RefereeInvitation, Report, \ + EditorialCommunication, EICRecommendation +from theses.models import ThesisLink +from virtualmeetings.models import Feedback, Nomination, Motion REGISTRATION_REFUSAL_CHOICES = ( @@ -415,6 +424,203 @@ class UnavailabilityPeriodForm(forms.ModelForm): return end +class ContributorMergeForm(forms.Form): + to_merge = ModelChoiceFieldwithid(queryset=Contributor.objects.all(), empty_label=None) + to_merge_into = ModelChoiceFieldwithid(queryset=Contributor.objects.all(), empty_label=None) + + def clean(self): + data = super().clean() + if self.cleaned_data['to_merge'] == self.cleaned_data['to_merge_into']: + self.add_error(None, 'A Contributor cannot be merged into itself.') + return data + + def save(self): + """ + Merge one Contributor into another. Set the previous Contributor to inactive. + """ + contrib_from = self.cleaned_data['to_merge'] + contrib_into = self.cleaned_data['to_merge_into'] + + both_contribs_active = contrib_from.is_active and contrib_into.is_active + + contrib_from_qs = Contributor.objects.filter(pk=contrib_from.id) + contrib_into_qs = Contributor.objects.filter(pk=contrib_into.id) + + # Step 1: update all fields within Contributor + if contrib_from.profile and not contrib_into.profile: + profile = contrib_from.profile + contrib_from_qs.update(profile=None) + contrib_into_qs.update(profile=profile) + User.objects.filter(pk=contrib_from.user.id).update(is_active=False) + User.objects.filter(pk=contrib_into.user.id).update(is_active=True) + if contrib_from.invitation_key and not contrib_into.invitation_key: + contrib_into_qs.update(invitation_key=contrib_into.invitation_key) + if contrib_from.activation_key and not contrib_into.activation_key: + contrib_into_qs.update(activation_key=contrib_into.activation_key) + contrib_from_qs.update(status=DOUBLE_ACCOUNT) + if contrib_from.orcid_id and not contrib_into.orcid_id: + contrib_into_qs.update(orcid_id=contrib_from.orcid_id) + if contrib_from.personalwebpage and not contrib_into.personalwebpage: + contrib_into_qs.update(personalwebpage=contrib_from.personalwebpage) + + # Specify duplicate_of for deactivated Contributor + contrib_from_qs.update(duplicate_of=contrib_into) + + # Step 2: update all ForeignKey relations + Affiliation.objects.filter(contributor=contrib_from).update(contributor=contrib_into) + Fellowship.objects.filter(contributor=contrib_from).update(contributor=contrib_into) + PotentialFellowshipEvent.objects.filter( + noted_by=contrib_from).update(noted_by=contrib_into) + Commentary.objects.filter(requested_by=contrib_from).update(requested_by=contrib_into) + Commentary.objects.filter(vetted_by=contrib_from).update(vetted_by=contrib_into) + Comment.objects.filter(vetted_by=contrib_from).update(vetted_by=contrib_into) + Comment.objects.filter(author=contrib_from).update(author=contrib_into) + Grant.objects.filter(recipient=contrib_from).update(recipient=contrib_into) + CitationNotification.objects.filter( + contributor=contrib_from).update(contributor=contrib_into) + PublicationAuthorsTable.objects.filter( + contributor=contrib_from).update(contributor=contrib_into) + UnavailabilityPeriod.objects.filter( + contributor=contrib_from).update(contributor=contrib_into) + Remark.objects.filter( + contributor=contrib_from).update(contributor=contrib_into) + DraftInvitation.objects.filter( + drafted_by=contrib_from).update(drafted_by=contrib_into) + AuthorshipClaim.objects.filter( + claimant=contrib_from).update(claimant=contrib_into) + AuthorshipClaim.objects.filter( + vetted_by=contrib_from).update(vetted_by=contrib_into) + Submission.objects.filter( + editor_in_charge=contrib_from).update(editor_in_charge=contrib_into) + Submission.objects.filter( + submitted_by=contrib_from).update(submitted_by=contrib_into) + EditorialAssignment.objects.filter(to=contrib_from).update(to=contrib_into) + RefereeInvitation.objects.filter(referee=contrib_from).update(referee=contrib_into) + RefereeInvitation.objects.filter(invited_by=contrib_from).update(invited_by=contrib_into) + Report.objects.filter(vetted_by=contrib_from).update(vetted_by=contrib_into) + Report.objects.filter(author=contrib_from).update(author=contrib_into) + EditorialCommunication.objects.filter( + referee=contrib_from).update(referee=contrib_into) + ThesisLink.objects.filter(requested_by=contrib_from).update(requested_by=contrib_into) + ThesisLink.objects.filter(vetted_by=contrib_from).update(vetted_by=contrib_into) + Feedback.objects.filter(by=contrib_from).update(by=contrib_into) + Nomination.objects.filter(by=contrib_from).update(by=contrib_into) + Motion.objects.filter(put_forward_by=contrib_from).update(put_forward_by=contrib_into) + + # Step 3: update all ManyToMany + commentaries = Commentary.objects.filter(authors__in=[contrib_from,]).all() + for commentary in commentaries: + commentary.authors.remove(contrib_from) + commentary.authors.add(contrib_into) + commentaries = Commentary.objects.filter(authors_claims__in=[contrib_from,]).all() + for commentary in commentaries: + commentary.authors_claims.remove(contrib_from) + commentary.authors_claims.add(contrib_into) + commentaries = Commentary.objects.filter(authors_false_claims__in=[contrib_from,]).all() + for commentary in commentaries: + commentary.authors_false_claims.remove(contrib_from) + commentary.authors_false_claims.add(contrib_into) + comments = Comment.objects.filter(in_agreement__in=[contrib_from,]).all() + for comment in comments: + comment.in_agreement.remove(contrib_from) + comment.in_agreement.add(contrib_into) + comments = Comment.objects.filter(in_notsure__in=[contrib_from,]).all() + for comment in comments: + comment.in_notsure.remove(contrib_from) + comment.in_notsure.add(contrib_into) + comments = Comment.objects.filter(in_disagreement__in=[contrib_from,]).all() + for comment in comments: + comment.in_disagreement.remove(contrib_from) + comment.in_disagreement.add(contrib_into) + publications = Publication.objects.filter(authors_registered__in=[contrib_from,]).all() + for publication in publications: + publication.authors_registered.remove(contrib_from) + publication.authors_registered.add(contrib_into) + publications = Publication.objects.filter(authors_claims__in=[contrib_from,]).all() + for publication in publications: + publication.authors_claims.remove(contrib_from) + publication.authors_claims.add(contrib_into) + publications = Publication.objects.filter(authors_false_claims__in=[contrib_from,]).all() + for publication in publications: + publication.authors_false_claims.remove(contrib_from) + publication.authors_false_claims.add(contrib_into) + submissions = Submission.objects.filter(authors__in=[contrib_from,]).all() + for submission in submissions: + submission.authors.remove(contrib_from) + submission.authors.add(contrib_into) + submissions = Submission.objects.filter(authors_claims__in=[contrib_from,]).all() + for submission in submissions: + submission.authors_claims.remove(contrib_from) + submission.authors_claims.add(contrib_into) + submissions = Submission.objects.filter(authors_false_claims__in=[contrib_from,]).all() + for submission in submissions: + submission.authors_false_claims.remove(contrib_from) + submission.authors_false_claims.add(contrib_into) + eicrecs = EICRecommendation.objects.filter(eligible_to_vote__in=[contrib_from,]).all() + for eicrec in eicrecs: + eicrec.eligible_to_vote.remove(contrib_from) + eicrec.eligible_to_vote.add(contrib_into) + eicrecs = EICRecommendation.objects.filter(voted_for__in=[contrib_from,]).all() + for eicrec in eicrecs: + eicrec.voted_for.remove(contrib_from) + eicrec.voted_for.add(contrib_into) + eicrecs = EICRecommendation.objects.filter(voted_against__in=[contrib_from,]).all() + for eicrec in eicrecs: + eicrec.voted_against.remove(contrib_from) + eicrec.voted_against.add(contrib_into) + eicrecs = EICRecommendation.objects.filter(voted_abstain__in=[contrib_from,]).all() + for eicrec in eicrecs: + eicrec.voted_abstain.remove(contrib_from) + eicrec.voted_abstain.add(contrib_into) + thesislinks = ThesisLink.objects.filter(author_as_cont__in=[contrib_from,]).all() + for tl in thesislinks: + tl.author_as_cont.remove(contrib_from) + tl.author_as_cont.add(contrib_into) + thesislinks = ThesisLink.objects.filter(author_claims__in=[contrib_from,]).all() + for tl in thesislinks: + tl.author_claims.remove(contrib_from) + tl.author_claims.add(contrib_into) + thesislinks = ThesisLink.objects.filter(author_false_claims__in=[contrib_from,]).all() + for tl in thesislinks: + tl.author_false_claims.remove(contrib_from) + tl.author_false_claims.add(contrib_into) + thesislinks = ThesisLink.objects.filter(supervisor_as_cont__in=[contrib_from,]).all() + for tl in thesislinks: + tl.supervisor_as_cont.remove(contrib_from) + tl.supervisor_as_cont.add(contrib_into) + nominations = Nomination.objects.filter(in_agreement__in=[contrib_from,]).all() + for nom in nominations: + nom.in_agreement.remove(contrib_from) + nom.in_agreement.add(contrib_into) + nominations = Nomination.objects.filter(in_notsure__in=[contrib_from,]).all() + for nom in nominations: + nom.in_notsure.remove(contrib_from) + nom.in_notsure.add(contrib_into) + nominations = Nomination.objects.filter(in_disagreement__in=[contrib_from,]).all() + for nom in nominations: + nom.in_disagreement.remove(contrib_from) + nom.in_disagreement.add(contrib_into) + motions = Motion.objects.filter(in_agreement__in=[contrib_from,]).all() + for nom in motions: + nom.in_agreement.remove(contrib_from) + nom.in_agreement.add(contrib_into) + motions = Motion.objects.filter(in_notsure__in=[contrib_from,]).all() + for nom in motions: + nom.in_notsure.remove(contrib_from) + nom.in_notsure.add(contrib_into) + motions = Motion.objects.filter(in_disagreement__in=[contrib_from,]).all() + for nom in motions: + nom.in_disagreement.remove(contrib_from) + nom.in_disagreement.add(contrib_into) + # If both accounts were active, inform the Contributor of the merge + if both_contribs_active: + mail_sender = DirectMailUtil( + mail_code='contributors/inform_contributor_duplicate_accounts_merged', + contrib_from=Contributor.objects.get(id=contrib_from.id)) + mail_sender.send() + return Contributor.objects.get(id=contrib_into.id) + + class RemarkForm(forms.Form): remark = forms.CharField(widget=forms.Textarea(), label='') diff --git a/scipost/management/commands/add_groups_and_permissions.py b/scipost/management/commands/add_groups_and_permissions.py index 1d13fcd2a8d05b9209234c34155c754de9421a79..c8f3fdf54204861c1df382f308db7252c1432fb7 100644 --- a/scipost/management/commands/add_groups_and_permissions.py +++ b/scipost/management/commands/add_groups_and_permissions.py @@ -403,6 +403,7 @@ class Command(BaseCommand): can_view_pool, can_take_charge_of_submissions, can_create_profiles, + can_view_profiles, can_attend_VGMs, can_view_statistics, can_manage_ontology, diff --git a/scipost/management/commands/check_celery.py b/scipost/management/commands/check_celery.py new file mode 100644 index 0000000000000000000000000000000000000000..4d5aa0dc702f437948fc5d4006fab2d9c005271f --- /dev/null +++ b/scipost/management/commands/check_celery.py @@ -0,0 +1,40 @@ +__copyright__ = "Copyright 2016-2018, Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +import datetime + +from django.core import mail +from django.core.management.base import BaseCommand +from django.utils import timezone + +from django_celery_results.models import TaskResult + + +class Command(BaseCommand): + help = 'Check if Celery is still running, or at least not failing.' + + def handle(self, *args, **kwargs): + # check failed. + compare_dt = timezone.now() - datetime.timedelta(hours=1) + results_failed = TaskResult.objects.filter( + status='FAILURE', date_done__gt=compare_dt).order_by('date_done').last() + if results_failed: + # Mail failed + body = 'Celery has failed task results. Last failed ID: {}'.format( + results_failed.id) + mail.mail_admins('Celery failed', body) + self.stdout.write( + self.style.SUCCESS('Celery failed, last ID: {}.'.format(results_failed.id))) + else: + last_result = TaskResult.objects.filter( + date_done__gt=compare_dt).order_by('date_done').last() + if last_result and last_result.date_done < compare_dt: + # Mail inactive + body = 'No results for Celery found. Celery seems to be inactive.' + body += ' Last result ID: {}'.format(last_result.id) + mail.mail_admins('Celery inactive', body) + self.stdout.write( + self.style.SUCCESS('Celery inactive, last ID: {}.'.format(last_result.id))) + if not results_failed and not last_result: + self.stdout.write(self.style.SUCCESS('Celery alive!')) diff --git a/scipost/managers.py b/scipost/managers.py index 8d698f4011d692b77452d0de652db8e234fe704f..affd24618f5f2049250628f0c1afd0798e3b4568 100644 --- a/scipost/managers.py +++ b/scipost/managers.py @@ -4,10 +4,10 @@ __license__ = "AGPL v3" from django.db import models from django.db.models import Count, Q -from django.db.models.functions import Lower +from django.db.models.functions import Concat, Lower from django.utils import timezone -from .constants import NORMAL_CONTRIBUTOR, NEWLY_REGISTERED, AUTHORSHIP_CLAIM_PENDING +from .constants import NORMAL_CONTRIBUTOR, NEWLY_REGISTERED, DOUBLE_ACCOUNT, AUTHORSHIP_CLAIM_PENDING class FellowManager(models.Manager): @@ -28,13 +28,6 @@ class ContributorQuerySet(models.QuerySet): """Return all validated and vetted Contributors.""" return self.filter(user__is_active=True, status=NORMAL_CONTRIBUTOR) - def have_duplicate_email(self): - """ Return Contributors having duplicate emails. """ - duplicates = self.values(lower_email=Lower('user__email')).annotate( - Count('id')).order_by('user__last_name').filter(id__count__gt=1) - return self.annotate(lower_email=Lower('user__email') - ).filter(lower_email__in=[dup['lower_email'] for dup in duplicates]) - def available(self): """Filter out the Contributors that have active unavailability periods.""" today = timezone.now().date() @@ -48,12 +41,38 @@ class ContributorQuerySet(models.QuerySet): def awaiting_vetting(self): """Filter Contributors that have not been vetted through.""" - return self.filter(user__is_active=True, status=NEWLY_REGISTERED) + return self.filter(user__is_active=True, status=NEWLY_REGISTERED + ).exclude(status=DOUBLE_ACCOUNT) def fellows(self): """TODO: NEEDS UPDATE TO NEW FELLOWSHIP RELATIONS.""" return self.filter(fellowships__isnull=False).distinct() + def with_duplicate_names(self): + """ + Returns only potential duplicate Contributors (as identified by first and + last names). + Admins and superusers are explicitly excluded. + """ + contribs = self.exclude(status=DOUBLE_ACCOUNT + ).exclude(user__is_superuser=True).exclude(user__is_staff=True + ).annotate(full_name=Concat('user__last_name', 'user__first_name')) + duplicates = contribs.values('full_name').annotate( + nr_count=Count('full_name')).filter(nr_count__gt=1).values_list('full_name', flat=True) + return contribs.filter( + full_name__in=duplicates).order_by('user__last_name', 'user__first_name', '-id') + + def with_duplicate_email(self): + """ + Return Contributors having duplicate emails. + """ + qs = self.exclude(status=DOUBLE_ACCOUNT + ).exclude(user__is_superuser=True).exclude( + user__is_staff=True).annotate(lower_email=Lower('user__email')) + duplicates = qs.values('lower_email').annotate( + Count('id')).filter(id__count__gt=1).values_list('lower_email', flat=True) + return qs.filter(user__email__in=duplicates) + class UnavailabilityPeriodManager(models.Manager): def today(self): diff --git a/scipost/migrations/0017_auto_20181115_2150.py b/scipost/migrations/0017_auto_20181115_2150.py new file mode 100644 index 0000000000000000000000000000000000000000..0cb61cbe8813cb8ea31cf7c58be61e428b3174f5 --- /dev/null +++ b/scipost/migrations/0017_auto_20181115_2150.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-11-15 20:50 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0016_auto_20180930_1801'), + ] + + operations = [ + migrations.AlterModelOptions( + name='contributor', + options={'ordering': ['user__last_name', 'user__first_name']}, + ), + ] diff --git a/scipost/migrations/0018_contributor_duplicate_of.py b/scipost/migrations/0018_contributor_duplicate_of.py new file mode 100644 index 0000000000000000000000000000000000000000..cbaa585ff9b61428aa0615c77c2549e726e4f1cf --- /dev/null +++ b/scipost/migrations/0018_contributor_duplicate_of.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-11-17 17:01 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0017_auto_20181115_2150'), + ] + + operations = [ + migrations.AddField( + model_name='contributor', + name='duplicate_of', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='duplicates', to='scipost.Contributor'), + ), + ] diff --git a/scipost/models.py b/scipost/models.py index a4bec62c981e70b44de3b2c03002cb395f2345e1..6fca155d743da18301cadd0a3c9d42458115da7e 100644 --- a/scipost/models.py +++ b/scipost/models.py @@ -16,7 +16,7 @@ from django.utils import timezone from .behaviors import TimeStampedModel, orcid_validator from .constants import ( - SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS, subject_areas_dict, DISABLED, + SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS, subject_areas_dict, NORMAL_CONTRIBUTOR, DISABLED, TITLE_CHOICES, INVITATION_STYLE, INVITATION_TYPE, INVITATION_CONTRIBUTOR, INVITATION_FORMAL, AUTHORSHIP_CLAIM_PENDING, AUTHORSHIP_CLAIM_STATUS, CONTRIBUTOR_STATUSES, NEWLY_REGISTERED) from .fields import ChoiceArrayField @@ -63,9 +63,15 @@ class Contributor(models.Model): related_name="contrib_vetted_by", blank=True, null=True) accepts_SciPost_emails = models.BooleanField( default=True, verbose_name="I accept to receive SciPost emails") + # If this Contributor is merged into another, then this field is set to point to the new one: + duplicate_of = models.ForeignKey('scipost.Contributor', on_delete=models.SET_NULL, + null=True, blank=True, related_name='duplicates') objects = ContributorQuerySet.as_manager() + class Meta: + ordering = ['user__last_name', 'user__first_name'] + def __str__(self): return '%s, %s' % (self.user.last_name, self.user.first_name) @@ -79,6 +85,18 @@ class Contributor(models.Model): """Return public information page url.""" return reverse('scipost:contributor_info', args=(self.id,)) + @property + def is_active(self): + """ + Checks if the Contributor is registered, vetted, + and has not been deactivated for any reason. + """ + return self.user.is_active and self.status == NORMAL_CONTRIBUTOR + + @property + def is_duplicate(self): + return self.duplicate_of is not None + @property def is_currently_available(self): """Check if Contributor is currently not marked as unavailable.""" diff --git a/scipost/templates/scipost/_public_info_as_table.html b/scipost/templates/scipost/_public_info_as_table.html index 1f0f93e93ff44d17a377c2632e39254646ebc0d8..a3b73a41a3193ec35862d22f53d11d96c0e412dc 100644 --- a/scipost/templates/scipost/_public_info_as_table.html +++ b/scipost/templates/scipost/_public_info_as_table.html @@ -15,4 +15,13 @@ </td> </tr> <tr><td>Personal web page: </td><td>{{ contributor.personalwebpage|default:'-' }}</td></tr> + + {% if perms.scipost.can_vet_registration_requests %} + <tr class="text-muted"><td>Username</td><td>{{ contributor.user.username }}</td></tr> + <tr class="text-muted"><td>Email (from User)</td><td>{{ contributor.user.email }}</td></tr> + <tr class="text-muted"><td>Date joined / last login</td><td>{{ contributor.user.date_joined }} / {{ contributor.user.last_login }}</td></tr> + <tr class="text-muted"><td>Status</td><td>{{ contributor.get_status_display }}</td></tr> + <tr class="text-muted"><td>User active?</td><td>{{ contributor.user.is_active }}</td></tr> + <tr class="text-muted"><td>Id</td><td>{{ contributor.id }}{% if contributor.profile %} <a href="{% url 'profiles:profile_detail' pk=contributor.profile.id %}">View Profile <i class="fa fa-arrow-right"></i></a>{% endif %}</td></tr> + {% endif %} </table> diff --git a/scipost/templates/scipost/contributor_duplicate_list.html b/scipost/templates/scipost/contributor_duplicate_list.html new file mode 100644 index 0000000000000000000000000000000000000000..ae58665e3dc65fff6c90b73f60603e10bf23b2e3 --- /dev/null +++ b/scipost/templates/scipost/contributor_duplicate_list.html @@ -0,0 +1,44 @@ +{% extends 'profiles/base.html' %} + +{% load bootstrap %} + +{% block breadcrumb_items %} + {{ block.super }} +<span class="breadcrumb-item">Contributor duplicates</span> +{% endblock %} + +{% load scipost_extras %} + +{% block pagetitle %}: Contributor duplicates{% endblock pagetitle %} + +{% block content %} +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Potentially duplicate Contributors</h1> + {% if merge_form %} + <form action="{% url 'scipost:contributor_merge' %}" method="get"> + {{ merge_form|bootstrap }} + <input class="btn btn-outline-secondary" type="submit" value="Check"> + </form> + {% endif %} + + <br> + <h3>All found duplicates</h3> + <ul> + {% for contrib_dup in object_list %} + <li>{{ contrib_dup }} (<em>id={{ contrib_dup.id }}</em>)</li> + {% empty %} + <li<em>No duplicates found</em></li> + {% endfor %} + </ul> + + {% if is_paginated %} + <div class="col-12"> + {% include 'partials/pagination.html' with page_obj=page_obj %} + </div> + {% endif %} + + </div> +</div> + +{% endblock content %} diff --git a/scipost/templates/scipost/contributor_info.html b/scipost/templates/scipost/contributor_info.html index d65751ed7db2636235ff7754b3725448eed99fb8..0969da038368d52d0d76039dba9bbc15143e6018 100644 --- a/scipost/templates/scipost/contributor_info.html +++ b/scipost/templates/scipost/contributor_info.html @@ -6,9 +6,16 @@ <h1 class="highlight mb-4">Contributor info: {{ contributor.get_title_display }} {{ contributor.user.first_name }} {{ contributor.user.last_name }}</h1> -{% include "scipost/_public_info_as_table.html" with contributor=contributor %} -<br> +<div class="card"> + <div class="card-header"> + Details + </div> + <div class="card-body"> + {% include "scipost/_public_info_as_table.html" with contributor=contributor %} + </div> +</div> + {% if contributor_publications %} <div class="row"> diff --git a/scipost/templates/scipost/contributor_merge.html b/scipost/templates/scipost/contributor_merge.html new file mode 100644 index 0000000000000000000000000000000000000000..8589170711c51d3a0eeb77853ac0668b7d551a5f --- /dev/null +++ b/scipost/templates/scipost/contributor_merge.html @@ -0,0 +1,50 @@ +{% extends 'scipost/base.html' %} + +{% load bootstrap %} + +{% block breadcrumb_items %} + {{ block.super }} +<span class="breadcrumb-item"><a href="{% url 'scipost:contributor_duplicates' %}">Duplicates</a></span> +<span class="breadcrumb-item">Merge Contributors {{ contributor_to_merge.id }} and {{ contributor_to_merge_into.id }}</span> +{% endblock %} + +{% load scipost_extras %} + +{% block pagetitle %}: Contributor duplicates: merge{% endblock pagetitle %} + +{% block content %} +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Merge Contributors {{ contributor_to_merge.id }} and {{ contributor_to_merge_into.id }}</h1> + {% if contributor_to_merge.user.is_active and not contributor_to_merge_into.user.is_active %} + <h3 class="text-danger">Warning: the contributor to merge is active, while the one to merge into is not</h3> + <p>Consider <a href="{% url 'scipost:contributor_merge' %}?to_merge={{ contributor_to_merge_into.id }}&to_merge_into={{ contributor_to_merge.id }}" method="get">merging the other way around</a></p> + {% endif %} + </div> +</div> +<div class="row"> + <div class="col-12"> + <h3 class="highlight">Contributor {{ contributor_to_merge.id }}</h3> + {% include "scipost/_public_info_as_table.html" with contributor=contributor_to_merge %} + </div> +</div> +<div class="row"> + <div class="col-12"> + <h3 class="highlight">Contributor {{ contributor_to_merge_into.id }}</h3> + {% include "scipost/_public_info_as_table.html" with contributor=contributor_to_merge_into %} + </div> +</div> + +<div class="row"> + <div class="col-12"> + <h3 class="highlight">Merge:</h3> + <form method="post"> + {% csrf_token %} + {{ merge_form|bootstrap }} + <input class="btn btn-primary" type="submit" value="Confirm merge"> + <a class="text-warning" href="{% url 'scipost:contributor_merge' %}?to_merge={{ contributor_to_merge_into.id }}&to_merge_into={{ contributor_to_merge.id }}" method="get">Merge the other way around</a></p> + </form> + </div> +</div> + +{% endblock content %} diff --git a/scipost/templates/scipost/footer.html b/scipost/templates/scipost/footer.html index a423be0af8357a466e7e9db24fb4d32e32b118f2..8cf25b23fb1dcc39095e0a4bb04ad9c2e06909c9 100644 --- a/scipost/templates/scipost/footer.html +++ b/scipost/templates/scipost/footer.html @@ -11,23 +11,18 @@ <a href="{% url 'scipost:terms_and_conditions' %}">Terms and conditions</a> <table class="mt-2 social-media"> - <tr> - <td> - <a href="//www.facebook.com/scipost" target="_blank" title="Facebook"> - <i class="fa fa-facebook" aria-hidden="true"></i> - </a> + <tr> + <td> + <a href="//twitter.com/scipost_dot_org" target="_blank" title="Twitter"> + <i class="fa fa-twitter" aria-hidden="true"></i> + </a> </td> - <td> - <a href="//twitter.com/scipost_dot_org" target="_blank" title="Twitter"> - <i class="fa fa-twitter" aria-hidden="true"></i> - </a> + <td> + <a href="{% url 'scipost:feeds' %}" title="RSS feeds"> + <i class="fa fa-rss" aria-hidden="true"></i> + </a> </td> - <td> - <a href="{% url 'scipost:feeds' %}" title="RSS feeds"> - <i class="fa fa-rss" aria-hidden="true"></i> - </a> - </td> - </tr> + </tr> </table> </div> <div class="col-md-4 mb-3 mb-md-0"> diff --git a/scipost/urls.py b/scipost/urls.py index e9c1f7f5742b1b7fda3a2739d85d2694d58bcb8c..59388af583e3800c51ecc0bffccdcb0bc22e4c41 100644 --- a/scipost/urls.py +++ b/scipost/urls.py @@ -158,6 +158,13 @@ urlpatterns = [ url(r'^vet_authorship_claim/(?P<claim_id>[0-9]+)/(?P<claim>[0-1])$', views.vet_authorship_claim, name='vet_authorship_claim'), + # Potential duplicates + url(r'contributor_duplicates/$', + views.ContributorDuplicateListView.as_view(), + name='contributor_duplicates'), + url(r'contributor_merge/$', + views.contributor_merge, + name='contributor_merge'), #################### # Email facilities # diff --git a/scipost/views.py b/scipost/views.py index 7fa8c1f6713e2c91efe8bc9289bbd4819d5a19d4..53ad7d48840f57eb880f8c84ab788fd0ff8a62ae 100644 --- a/scipost/views.py +++ b/scipost/views.py @@ -39,7 +39,9 @@ from .models import ( from .forms import ( AuthenticationForm, UnavailabilityPeriodForm, RegistrationForm, AuthorshipClaimForm, SearchForm, VetRegistrationForm, reg_ref_dict, UpdatePersonalDataForm, UpdateUserDataForm, - PasswordChangeForm, EmailGroupMembersForm, EmailParticularForm, SendPrecookedEmailForm) + PasswordChangeForm, ContributorMergeForm, + EmailGroupMembersForm, EmailParticularForm, SendPrecookedEmailForm) +from .mixins import PermissionsMixin, PaginationMixin from .utils import Utils, EMAIL_FOOTER, SCIPOST_SUMMARY_FOOTER, SCIPOST_SUMMARY_FOOTER_HTML from affiliations.forms import AffiliationsFormset @@ -959,6 +961,86 @@ def contributor_info(request, contributor_id): return render(request, 'scipost/contributor_info.html', context) +class ContributorDuplicateListView(PermissionsMixin, PaginationMixin, ListView): + """ + List Contributors with potential (not yet handled) duplicates. + Two sources of duplicates are separately considered: + - duplicate full names (last name + first name) + - duplicate email addresses. + + """ + permission_required = 'scipost.can_vet_registration_requests' + model = Contributor + template_name = 'scipost/contributor_duplicate_list.html' + + def get_queryset(self): + queryset = Contributor.objects.all() + if self.request.GET.get('kind') == 'names': + queryset = queryset.with_duplicate_names() + elif self.request.GET.get('kind') == 'emails': + queryset = queryset.with_duplicate_emails() + else: + queryset = queryset.with_duplicate_names() + return queryset + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + + if len(context['object_list']) > 1: + initial = { + 'to_merge': context['object_list'][0].id, + 'to_merge_into': context['object_list'][1].id + } + context['merge_form'] = ContributorMergeForm(initial=initial) + return context + + +@transaction.atomic +@permission_required('scipost.can_vet_registration_requests') +def contributor_merge(request): + """ + Handles the merging of data from one Contributor instance to another, + to solve one person - multiple registrations issues. + + Both instances are preserved, but the merge_from instance's + status is set to DOUBLE_ACCOUNT and its User is set to inactive. + + If both Contributor instances were active, then the account owner + is emailed with information about the merge. + """ + merge_form = ContributorMergeForm(request.POST or None, initial=request.GET) + context = {'merge_form': merge_form} + + if request.method == 'POST': + if merge_form.is_valid(): + contributor = merge_form.save() + messages.success(request, 'Contributors merged') + return redirect(reverse('scipost:contributor_duplicates')) + else: + try: + context.update({ + 'contributor_to_merge': get_object_or_404( + Contributor, pk=merge_form.cleaned_data['to_merge'].id), + 'contributor_to_merge_into': get_object_or_404( + Contributor, pk=merge_form.cleaned_data['to_merge_into'].id) + }) + except ValueError: + raise Http404 + + elif request.method == 'GET': + try: + context.update({ + 'contributor_to_merge': get_object_or_404(Contributor, + pk=int(request.GET['to_merge'])), + 'contributor_to_merge_into': get_object_or_404(Contributor, + pk=int(request.GET['to_merge_into'])), + }) + except ValueError: + raise Http404 + + return render(request, 'scipost/contributor_merge.html', context) + + #################### # Email facilities # #################### diff --git a/submissions/forms.py b/submissions/forms.py index 47668d7bdfdb4fc1a2dbc28feb523506391c37ba..a5b56969a6798dc6dc98db79557df8d33457cf3a 100644 --- a/submissions/forms.py +++ b/submissions/forms.py @@ -27,17 +27,14 @@ from .models import ( iThenticateReport, EditorialCommunication) from .signals import notify_manuscript_accepted -# from common.helpers import get_new_secrets_key from colleges.models import Fellowship -# from invitations.models import RegistrationInvitation from journals.models import Journal from journals.constants import SCIPOST_JOURNAL_PHYSICS_PROC, SCIPOST_JOURNAL_PHYSICS from mails.utils import DirectMailUtil from preprints.helpers import generate_new_scipost_identifier from preprints.models import Preprint from production.utils import get_or_create_production_stream -# from profiles.models import Profile -from scipost.constants import SCIPOST_SUBJECT_AREAS #, INVITATION_REFEREEING +from scipost.constants import SCIPOST_SUBJECT_AREAS from scipost.services import ArxivCaller from scipost.models import Contributor, Remark import strings @@ -1019,7 +1016,7 @@ class ReportForm(forms.ModelForm): required_fields_label = ['report', 'recommendation', 'qualification'] # If the Report is not a followup: Explicitly assign more fields as being required! - if not self.instance.is_followup_report: + if not self.instance.is_followup_report and self.submission.submitted_to.name != SCIPOST_JOURNAL_PHYSICS_PROC: required_fields_label += [ 'strengths', 'weaknesses', diff --git a/submissions/templates/submissions/select_referee.html b/submissions/templates/submissions/select_referee.html index b8a46829ea681d1aeed080e2de66e51c291710c8..bd87004f10c23be8e642f76c8134319659ae779b 100644 --- a/submissions/templates/submissions/select_referee.html +++ b/submissions/templates/submissions/select_referee.html @@ -81,13 +81,23 @@ <tr> <th>Name</th> <th>Registered<br/>Contributor?</th> - <th>Action<br/><span class="text-muted"><small>(Unregistered people will also receive a registration invitation)</small></span></th> + <th>Email<br/>known?</th> + <th>Action<br/><span class="text-muted"><small>(Unregistered people will also automatically receive a registration invitation)</small></span></th> </tr> {% for profile in profiles_found %} <tr> <td>{{ profile }}</td> <td>{% if profile.contributor %}<i class="fa fa-check-circle text-success"></i>{% else %}<i class="fa fa-times-circle text-danger"></i>{% endif %}</td> - <td>Send refereeing invitation <a href="{% url 'submissions:invite_referee' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr profile_id=profile.id auto_reminders_allowed=1 %}">with</a> or <a href="{% url 'submissions:invite_referee' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr profile_id=profile.id auto_reminders_allowed=0 %}">without</a> auto-reminders {% include 'partials/submissions/refinv_auto_reminders_tooltip.html' %}</td> + <td>{% if profile.email %}<i class="fa fa-check-circle text-success"></i>{% else %}<i class="fa fa-times-circle text-danger"></i>{% endif %}</td> + <td>{% if profile.email %}Send refereeing invitation <a href="{% url 'submissions:invite_referee' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr profile_id=profile.id auto_reminders_allowed=1 %}">with</a> or <a href="{% url 'submissions:invite_referee' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr profile_id=profile.id auto_reminders_allowed=0 %}">without</a> auto-reminders {% include 'partials/submissions/refinv_auto_reminders_tooltip.html' %}{% else %}<span class="text-danger">Cannot send an invitation without an email</span> <i class="fa fa-arrow-right"></i> Add one: + <form class="form-inline" action="{% url 'profiles:add_profile_email' profile_id=profile.id %}" method="post"> + {% csrf_token %} + {{ profile_email_form|bootstrap }} + <input type="hidden" name="next" value="{{ request.get_full_path }}"> + <input class="btn btn-outline-secondary" type="submit" value="Add"> + </form> + {% endif %} + </td> </tr> {% empty %} <tr> diff --git a/submissions/views.py b/submissions/views.py index 372989d1ec82084db63453ede72174e6addf18b0..3ae50a5702a8882bbcf74922559395f21f415c3a 100644 --- a/submissions/views.py +++ b/submissions/views.py @@ -53,7 +53,7 @@ from ontology.models import Topic from ontology.forms import SelectTopicForm from production.forms import ProofsDecisionForm from profiles.models import Profile -from profiles.forms import SimpleProfileForm +from profiles.forms import SimpleProfileForm, ProfileEmailForm from scipost.constants import INVITATION_REFEREEING from scipost.decorators import is_contributor_user from scipost.forms import RemarkForm @@ -936,6 +936,7 @@ def select_referee(request, identifier_w_vn_nr): 'workdays_left_to_report': workdays_between(timezone.now(), submission.reporting_deadline), 'referee_search_form': referee_search_form, 'queryresults': queryresults, + 'profile_email_form': ProfileEmailForm(initial={'primary': True}), }) return render(request, 'submissions/select_referee.html', context) @@ -1618,7 +1619,8 @@ def prepare_for_voting(request, rec_id): Q(contributor__expertises__contains=[recommendation.submission.subject_area]) | Q(contributor__expertises__contains=recommendation.submission.secondary_areas)).order_by( 'contributor__user__last_name') - coauthorships = recommendation.submission.flag_coauthorships_arxiv(fellows_with_expertise) + #coauthorships = recommendation.submission.flag_coauthorships_arxiv(fellows_with_expertise) + coauthorships = None context = { 'recommendation': recommendation, diff --git a/templates/email/contributors/inform_contributor_duplicate_accounts_merged.html b/templates/email/contributors/inform_contributor_duplicate_accounts_merged.html new file mode 100644 index 0000000000000000000000000000000000000000..13ddb815260a104d4936b83a1172b93bba1759f7 --- /dev/null +++ b/templates/email/contributors/inform_contributor_duplicate_accounts_merged.html @@ -0,0 +1,18 @@ +<p> + Dear {{ contrib_from.duplicate_of.get_title_display }} {{ contrib_from.duplicate_of.user.last_name }}, +</p> +<p> + We noticed that you had two separate registrations at SciPost, and have consolidated your two accounts into a single active one, namely your account with username <strong><em style="color: green;">{{ contrib_from.duplicate_of.user.username }}</em></strong>. +</p> +<p> + Your alternate account with username <strong><em style="color: red;">{{ contrib_from.user.username }}</em></strong> has been deactivated, but all the data associated to it has been transferred to your active account. +</p> +<p> + Please get in touch with us at <a href="mailto:techsupport@scipost.org">SciPost techsupport</a> (or by simply replying to this email) if you have any questions. +</p> +<p> + Many thanks, + <br><br> + The SciPost Team +</p> +{% include 'email/_footer.html' %} diff --git a/templates/email/contributors/inform_contributor_duplicate_accounts_merged.json b/templates/email/contributors/inform_contributor_duplicate_accounts_merged.json new file mode 100644 index 0000000000000000000000000000000000000000..f7987957d8141c238f624e2b2de5029267866efe --- /dev/null +++ b/templates/email/contributors/inform_contributor_duplicate_accounts_merged.json @@ -0,0 +1,8 @@ +{ + "subject": "SciPost: duplicate accounts merged", + "to_address": "user.email", + "bcc_to": "admin@scipost.org", + "from_address_name": "SciPost Admin", + "from_address": "admin@scipost.org", + "context_object": "contrib_from" +} diff --git a/templates/email/potentialfellowships/invite_potential_fellow_initial.html b/templates/email/potentialfellowships/invite_potential_fellow_initial.html index 71fae8d06fa3d724a0931b18989c4c29b4f08367..8c54bc307825d84575af609fe48ab257692f04d1 100644 --- a/templates/email/potentialfellowships/invite_potential_fellow_initial.html +++ b/templates/email/potentialfellowships/invite_potential_fellow_initial.html @@ -1,23 +1,19 @@ <p>Dear {{ potfel.profile.get_title_display }} {{ potfel.profile.last_name }},</p> -<p>Hopefully you've already come across <a href="https://scipost.org{% url 'scipost:index' %}">SciPost</a> and are aware of our mission to establish a healthier infrastructure for scientific publishing.</p> +<p>Hopefully you've already come across <a href="https://scipost.org{% url 'scipost:index' %}">SciPost</a> and are aware of our mission to establish a community-based infrastructure for scientific publishing.</p> -<p>On behalf of the SciPost Foundation and in view of your professional expertise and reputation, I hereby would like to invite you to join our Editorial College and become one of our Editorial Fellows. We are currently making a big push for expansion of our activities, see <a href="https://scipost.org{% url 'scipost:ExpSustDrive2018' %}">this page</a> for details.</p> +<p>On behalf of the SciPost Foundation and in view of your professional expertise and reputation, I hereby would like to invite you to join our Editorial College by becoming one of our Editorial Fellows.</p> <p> - Please note that only well-known and respected senior academics are being contacted for this purpose. - Academic reputation and involvement in the community are the most important criteria guiding our considerations of who should belong to the Editorial College. - The current list of Fellows can be found at <a href="https://scipost.org{% url 'scipost:about' %}">scipost.org/about</a>; on this page, you will also find basic information on SciPost and its guiding principles (you can also take a look at our <a href="https://scipost.org{% url 'scipost:FAQ' %}">frequently asked questions page</a>). + Academic reputation is the most important criterion guiding our considerations of who should belong to the Editorial College. The current list of Fellows can be found at <a href="https://scipost.org{% url 'scipost:about' %}">scipost.org/about</a>; on this page, you will also find basic information on SciPost and its guiding principles. </p> <p> - We do not pose any conditions on your involvement, and you would always remain in complete control of your level of commitment (devoting even a couple of hours per month would be enough to help out significantly). - Functioning of the College proceeds according to the by-laws set out at <a href="https://scipost.org{% url 'scipost:EdCol_by-laws' %}">scipost.org/EdCol_by-laws</a>, - and a short summary of the editorial workflow can be found at <a href="https://scipost.org{% url 'submissions:editorial_workflow' %}">this page</a>. + We do not pose any conditions on your involvement, and you will always remain in complete control of your level of commitment (a couple of hours per month would already be significant). Functioning of the College proceeds according to the by-laws set out at <a href="https://scipost.org{% url 'scipost:EdCol_by-laws' %}">scipost.org/EdCol_by-laws</a>, and a short summary of the editorial workflow can be found at <a href="https://scipost.org{% url 'submissions:editorial_workflow' %}">this page</a>. </p> <p> - I would be very happy to provide you with more information should you require it. Could I beg you to give us a response (by replying to this email) within the next couple of weeks? + I would be very happy to provide you with more information should you require it. Could I beg you to give us a response (by replying to this email) within the next couple of weeks? </p> <p>