diff --git a/SciPost_v1/settings/production.py b/SciPost_v1/settings/production.py index ac0546cab12d7d38e10f38d9249ddfe02512a164..b95b188543eec3c02a750b111877015627119244 100644 --- a/SciPost_v1/settings/production.py +++ b/SciPost_v1/settings/production.py @@ -1,3 +1,6 @@ +import sentry_sdk +from sentry_sdk.integrations.django import DjangoIntegration + from .base import * # THE MAIN THING HERE @@ -18,7 +21,8 @@ WEBPACK_LOADER['DEFAULT']['CACHE'] = True WEBPACK_LOADER['DEFAULT']['BUNDLE_DIR_NAME'] = '/home/scipost/webapps/scipost_static/bundles/' # Error reporting -ADMINS = MANAGERS = (('J.S.Caux', 'J.S.Caux@uva.nl'), ('J.de Wit', 'jorrandewit@outlook.com')) +ADMINS = [] +MANAGERS = (('J.S.Caux', 'J.S.Caux@uva.nl'), ('J.de Wit', 'jorrandewit@outlook.com')) # Cookies SESSION_COOKIE_SECURE = True @@ -57,3 +61,10 @@ LOGGING['handlers']['scipost_file_doi']['filename'] = '/home/scipost/webapps/sci # API REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'] = ('rest_framework.renderers.JSONRenderer',) + + +# Sentry +sentry_sdk.init( + dsn=get_secret('SENTRY_DSN'), + integrations=[DjangoIntegration()] +) diff --git a/affiliations/admin.py b/affiliations/admin.py index fc4aa88c963abfdd8e52f67fe6c44d7f7ba9740b..f398cbde6a702263d3a606e9073ed45bd77ebb18 100644 --- a/affiliations/admin.py +++ b/affiliations/admin.py @@ -7,5 +7,14 @@ from django.contrib import admin from .models import Affiliation, Institution -admin.site.register(Affiliation) -admin.site.register(Institution) +class AffiliationAdmin(admin.ModelAdmin): + search_fields = ['institution__name', 'institution__acronym', + 'contributor__user__last_name'] + +admin.site.register(Affiliation, AffiliationAdmin) + + +class InstitutionAdmin(admin.ModelAdmin): + search_fields =['name', 'acronym'] + +admin.site.register(Institution, InstitutionAdmin) diff --git a/affiliations/forms.py b/affiliations/forms.py index 41b281de326282a01d7c56c6ac7318af868402a0..05dc8c902e3192f72a16a1d2992c4119471bf644 100644 --- a/affiliations/forms.py +++ b/affiliations/forms.py @@ -9,6 +9,8 @@ from django_countries import countries from django_countries.fields import LazyTypedChoiceField from django_countries.widgets import CountrySelectWidget +from ajax_select.fields import AutoCompleteSelectField + from common.widgets import DateWidget from .models import Affiliation, Institution @@ -117,3 +119,11 @@ class InstitutionMergeForm(forms.ModelForm): institution=old_institution).update(institution=self.instance) old_institution.delete() return self.instance + + +class InstitutionOrganizationSelectForm(forms.ModelForm): + organization = AutoCompleteSelectField('organization_lookup') + + class Meta: + model = Institution + fields = [] diff --git a/affiliations/migrations/0003_institution_organization.py b/affiliations/migrations/0003_institution_organization.py new file mode 100644 index 0000000000000000000000000000000000000000..169975618df4f7454d6c1c52bf9a8cfbc488171e --- /dev/null +++ b/affiliations/migrations/0003_institution_organization.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2019-03-29 06:44 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('organizations', '0010_auto_20190223_1406'), + ('affiliations', '0002_auto_20171229_1435'), + ] + + operations = [ + migrations.AddField( + model_name='institution', + name='organization', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='institutions', to='organizations.Organization'), + ), + ] diff --git a/affiliations/models.py b/affiliations/models.py index 5bb05f92ecc0971936f30f21818613315aa7372c..ed9e2d280e00ddef8f6fe5a089849e6fd18ef92d 100644 --- a/affiliations/models.py +++ b/affiliations/models.py @@ -22,6 +22,8 @@ class Institution(models.Model): acronym = models.CharField(max_length=16, blank=True) country = CountryField() type = models.CharField(max_length=16, choices=INSTITUTION_TYPES, default=TYPE_UNIVERSITY) + organization = models.ForeignKey('organizations.Organization', on_delete=models.CASCADE, + blank=True, null=True) objects = InstitutionQuerySet.as_manager() diff --git a/affiliations/templates/affiliations/institution_confirm_delete.html b/affiliations/templates/affiliations/institution_confirm_delete.html new file mode 100644 index 0000000000000000000000000000000000000000..bc40d6ef792d923965578137ada5341e5dfa3cfc --- /dev/null +++ b/affiliations/templates/affiliations/institution_confirm_delete.html @@ -0,0 +1,46 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: delete Institution{% endblock pagetitle %} + +{% load bootstrap %} + +{% block breadcrumb_items %} +{{ block.super }} +<span class="breadcrumb-item">{{ institution.name }}</span> +{% endblock %} + +{% block content %} + +<h1>Institution: confirm delete</h1> +<div class="row"> + <div class="col-4"> + <table class="table"> + <tbody> + <tr><td>Name:</td><td>{{ institution.name }}</td></tr> + <tr><td>Acronym:</td><td>{{ institution.acronym }}</td></tr> + <tr><td>Country:</td><td>{{ institution.get_country_display }}</td></tr> + <tr><td>Type:</td><td>{{ institution.get_type_display }}</td></tr> + <tr><td>Organization:</td><td>{{ institution.organization }}</td></tr> + <tr> + <td>(aff)Affiliations:</td> + <td> + <ul class="list-unstyled"> + {% for aff in institution.affiliations.all %} + <li>{{ aff }}</li> + {% endfor %} + </ul> + </td> + </tr> + </tbody> + </table> + </div> + <div class="col-12"> + <form method="post"> + {% csrf_token %} + <h3 class="mb-2">Are you sure you want to delete this Institution?</h3> + <input type="submit" class="btn btn-danger" value="Yes, delete it" /> + </form> + </div> +</div> + +{% endblock content %} diff --git a/affiliations/templates/affiliations/institution_link_organization.html b/affiliations/templates/affiliations/institution_link_organization.html new file mode 100644 index 0000000000000000000000000000000000000000..9bd1517aed943bdfeed68558ec5d54161da0e4d6 --- /dev/null +++ b/affiliations/templates/affiliations/institution_link_organization.html @@ -0,0 +1,45 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: link Institution to Organization{% endblock pagetitle %} + +{% load bootstrap %} + +{% block breadcrumb_items %} +{{ block.super }} +<span class="breadcrumb-item">{{ institution.name }}</span> +{% endblock %} + +{% block content %} + +<h1>Institution: link to Organization</h1> +<div class="row"> + <div class="col-4"> + <table class="table"> + <tbody> + <tr><td>Name:</td><td>{{ institution.name }}</td></tr> + <tr><td>Acronym:</td><td>{{ institution.acronym }}</td></tr> + <tr><td>Country:</td><td>{{ institution.get_country_display }}</td></tr> + <tr><td>Type:</td><td>{{ institution.get_type_display }}</td></tr> + <tr><td>Organization:</td><td>{{ institution.organization }}</td></tr> + </tbody> + </table> + </div> + <div class="col-6"> + <h3>Link to:</h3> + <form action="{% url 'affiliations:link_to_organization' pk=institution.pk %}" method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input type="submit" value="Link" class="btn btn-primary"> + </form> + </div> + <div class="col-2"> + <p>Can't find it in the selector? <a href="{% url 'organizations:organization_create' %}" target="_blank">Add a new organization to our database</a> (opens in new window)</p> + </div> +</div> + +{% endblock content %} + +{% block footer_script %} +{{ block.super }} +{{ form.media }} +{% endblock footer_script %} diff --git a/affiliations/templates/affiliations/institutions_without_organization_list.html b/affiliations/templates/affiliations/institutions_without_organization_list.html new file mode 100644 index 0000000000000000000000000000000000000000..76be007587d5853abc746ad625c4633b132f29e9 --- /dev/null +++ b/affiliations/templates/affiliations/institutions_without_organization_list.html @@ -0,0 +1,39 @@ +{% extends 'affiliations/base.html' %} + + +{% block pagetitle %}: Institutions{% endblock pagetitle %} + + +{% block breadcrumb_items %} + <span class="breadcrumb-item">Institutions without Organization</span> +{% endblock %} + +{% block content %} + +<h1 class="highlight">Institutions without Organization</h1> + +{% if is_paginated %} + {% include 'partials/pagination.html' with page_obj=page_obj %} +{% endif %} + +<ul> + {% for institution in object_list %} + <li> + {% if perms.scipost.can_manage_affiliations %} + <a href="{% url 'affiliations:link_to_organization' pk=institution.pk %}">Link to Org</a> +   + <a href="{% url 'affiliations:institution_delete' institution_id=institution.pk %}" class="text-danger">Delete</a> +   + {% endif %} + <a href="{{ institution.get_absolute_url }}">{{ institution }}</a> + </li> + {% empty %} + <li><em>There are no Institutions without an Organization.</em><li> + {% endfor %} +</ul> +{% if is_paginated %} + {% include 'partials/pagination.html' with page_obj=page_obj %} +{% endif %} + + +{% endblock content %} diff --git a/affiliations/urls.py b/affiliations/urls.py index 5221f5d8ea7ab48e491885a2f2cacac743ff0bf4..db9ce2d7f92548a0168c9f5b61f01d8e95c31d32 100644 --- a/affiliations/urls.py +++ b/affiliations/urls.py @@ -12,6 +12,14 @@ urlpatterns = [ name='institution_details'), url(r'^(?P<institution_id>[0-9]+)/edit', views.InstitutionUpdateView.as_view(), name='institution_edit'), + url(r'^(?P<institution_id>[0-9]+)/delete/', views.InstitutionDeleteView.as_view(), + name='institution_delete'), url(r'^(?P<institution_id>[0-9]+)/merge$', views.merge_institutions, name='merge_institutions'), + url(r'^institutions_without_organization/$', + views.InstitutionWithoutOrganizationListView.as_view(), + name='institutions_without_organization'), + url(r'^(?P<pk>[0-9]+)/link_to_organization/$', + views.LinkInstitutionToOrganizationView.as_view(), + name='link_to_organization'), ] diff --git a/affiliations/views.py b/affiliations/views.py index 3107fdf2cfda6f972a654697dafaac8ff7db4d97..2f7d98203a8a6c69a90970db4fed049203f57efd 100644 --- a/affiliations/views.py +++ b/affiliations/views.py @@ -5,14 +5,16 @@ __license__ = "AGPL v3" from django.shortcuts import redirect from django.contrib import messages from django.contrib.auth.decorators import permission_required -from django.urls import reverse +from django.urls import reverse, reverse_lazy from django.utils.decorators import method_decorator from django.views.generic.detail import DetailView -from django.views.generic.edit import UpdateView +from django.views.generic.edit import UpdateView, DeleteView from django.views.generic.list import ListView from django.shortcuts import get_object_or_404 -from .forms import InstitutionMergeForm +from scipost.mixins import PermissionsMixin + +from .forms import InstitutionMergeForm, InstitutionOrganizationSelectForm from .models import Institution @@ -26,6 +28,13 @@ class InstitutionDetailView(DetailView): pk_url_kwarg = 'institution_id' +class InstitutionDeleteView(PermissionsMixin, DeleteView): + model = Institution + permission_required = 'scipost.can_manage_affiliations' + pk_url_kwarg = 'institution_id' + success_url = reverse_lazy('affiliations:institutions_without_organization') + + @method_decorator(permission_required('scipost.can_manage_affiliations'), name='dispatch') class InstitutionUpdateView(UpdateView): model = Institution @@ -59,3 +68,24 @@ def merge_institutions(request, institution_id): a=form.cleaned_data.get('institution', '?'), b=institution)) return redirect(reverse('affiliations:institution_edit', args=(institution.id,))) + + +class InstitutionWithoutOrganizationListView(ListView): + queryset = Institution.objects.filter(organization=None) + paginate_by = 20 + template_name = 'affiliations/institutions_without_organization_list.html' + + +class LinkInstitutionToOrganizationView(PermissionsMixin, UpdateView): + """ + For an existing Institution instance, specify the link to an Organization. + """ + permission_required = 'scipost.can_manage_affiliations' + model = Institution + form_class = InstitutionOrganizationSelectForm + template_name = 'affiliations/institution_link_organization.html' + success_url = reverse_lazy('affiliations:institutions_without_organization') + + def form_valid(self, form): + form.instance.organization = form.cleaned_data['organization'] + return super().form_valid(form) diff --git a/colleges/managers.py b/colleges/managers.py index 67e450e0cd057ac2ac532955a241317339b4e59a..e07b7f0fc0ced30b4262a66f3ef1309bdc1934b3 100644 --- a/colleges/managers.py +++ b/colleges/managers.py @@ -25,8 +25,32 @@ class FellowQuerySet(models.QuerySet): Q(start_date__isnull=True, until_date__isnull=True) ).ordered() + def specialties_overlap(self, discipline, expertises=[]): + """ + Returns all Fellows specialized in the given discipline + and any of the (optional) expertises. + + This method is also separately implemented for Contributor and Profile objects. + """ + qs = self.filter(contributor__profile__discipline=discipline) + if expertises and len(expertises) > 0: + qs = qs.filter(contributor__profile__expertises__overlap=expertises) + return qs + + def specialties_contain(self, discipline, expertises=[]): + """ + Returns all Fellows specialized in the given discipline + and all of the (optional) expertises. + + This method is also separately implemented for Contributor and Profile objects. + """ + qs = self.filter(contributor__profile__discipline=discipline) + if expertises and len(expertises) > 0: + qs = qs.filter(contributor__profile__expertises__contains=expertises) + return qs + def ordered(self): - """Return ordered queryset explicitly, since this may have big affect on performance.""" + """Return ordered queryset explicitly, since this may have big effect on performance.""" return self.order_by('contributor__user__last_name') def return_active_for_submission(self, submission): @@ -56,7 +80,7 @@ class PotentialFellowshipQuerySet(models.QuerySet): return self.filter( profile__discipline=contributor.profile.discipline, status=POTENTIAL_FELLOWSHIP_ELECTION_VOTE_ONGOING - ).order_by('profile__last_name') + ).distinct().order_by('profile__last_name') def to_vote_on(self, contributor): return self.vote_needed(contributor).exclude( diff --git a/colleges/templates/colleges/potentialfellowship_list.html b/colleges/templates/colleges/potentialfellowship_list.html index 6cc4b4e90102462f1bb52f3dfbea6e246360a355..7131ac1ef2e61b584a557c93e3a23e9ef742ad47 100644 --- a/colleges/templates/colleges/potentialfellowship_list.html +++ b/colleges/templates/colleges/potentialfellowship_list.html @@ -117,7 +117,7 @@ $(document).ready(function($) { <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 style="color: #ffffff; background-color:{{ potfel.status|potfelstatuscolor }};">{{ potfel.get_status_display }} <small>{% voting_results_display potfel %}</small></td> + <td style="color: #ffffff; background-color:{{ potfel.status|potfelstatuscolor }};">{{ potfel.get_status_display }}<br/><small>{% voting_results_display potfel %}</small></td> <td>{{ potfel.latest_event_details }}</td> </tr> {% empty %} diff --git a/colleges/templatetags/colleges_extras.py b/colleges/templatetags/colleges_extras.py index 13a510c612de262bff017503d69c40950cb34f5d..3a9b26c02527abc5020090a14ffb1c3fcc5b3935 100644 --- a/colleges/templatetags/colleges_extras.py +++ b/colleges/templatetags/colleges_extras.py @@ -3,6 +3,7 @@ __license__ = "AGPL v3" from django import template +from django.utils.html import format_html, mark_safe from ..constants import ( POTENTIAL_FELLOWSHIP_IDENTIFIED, POTENTIAL_FELLOWSHIP_NOMINATED, @@ -14,6 +15,7 @@ from ..constants import ( POTENTIAL_FELLOWSHIP_INTERESTED, POTENTIAL_FELLOWSHIP_REGISTERED, POTENTIAL_FELLOWSHIP_ACTIVE_IN_COLLEGE, POTENTIAL_FELLOWSHIP_SCIPOST_EMERITUS ) +from ..models import Fellowship from common.utils import hslColorWheel @@ -62,8 +64,33 @@ def potfelstatuscolor(status): @register.simple_tag def voting_results_display(potfel): if potfel.status == POTENTIAL_FELLOWSHIP_ELECTION_VOTE_ONGOING: - return ' Agree: %s, Abstain: %s, Disagree: %s' % ( - potfel.in_agreement.count(), - potfel.in_abstain.count(), - potfel.in_disagreement.count()) + nr_agree = potfel.in_agreement.count() + nr_abstain = potfel.in_abstain.count() + nr_disagree = potfel.in_disagreement.count() + nr_spec_agree = potfel.in_agreement.all().specialties_overlap( + potfel.profile.discipline, potfel.profile.expertises).count() + nr_spec_abstain = potfel.in_abstain.all().specialties_overlap( + potfel.profile.discipline, potfel.profile.expertises).count() + nr_spec_disagree = potfel.in_disagreement.all().specialties_overlap( + potfel.profile.discipline, potfel.profile.expertises).count() + nr_specialists = Fellowship.objects.active().specialties_overlap( + potfel.profile.discipline, potfel.profile.expertises).count() + nr_Fellows = Fellowship.objects.active().specialties_overlap( + potfel.profile.discipline).count() + # Establish whether election criterion has been met. + # Rule is: spec Agree must be >= 3/4 of (total nr of spec - nr abstain) + election_agree_percentage = int( + 100 * nr_spec_agree/(max(1, nr_specialists - nr_spec_abstain))) + election_criterion_met = nr_spec_agree > 0 and election_agree_percentage >= 75 + if election_criterion_met: + election_text = (' <strong class="bg-success p-1 text-white">' + 'Elected (%s% in favour)</strong>' % str(election_agree_percentage)) + else: + election_text = (' <strong class="bg-warning p-1 text-white">' + '%s% in favour</strong>') % str(election_agree_percentage) + return format_html('Specialists ({}):<br/>Agree: {}, Abstain: {}, Disagree: {} {}<br/>' + 'All: ({} Fellows)<br/>Agree: {}, Abstain: {}, Disagree: {}', + nr_specialists, nr_spec_agree, nr_spec_abstain, nr_spec_disagree, + mark_safe(election_text), + nr_Fellows, nr_agree, nr_abstain, nr_disagree) return '' diff --git a/conflicts/management/commands/check_submission_metadata.py b/conflicts/management/commands/check_submission_metadata.py new file mode 100644 index 0000000000000000000000000000000000000000..fc7bbbbd49cd36d43a7cc7199061e47c6f7ff01d --- /dev/null +++ b/conflicts/management/commands/check_submission_metadata.py @@ -0,0 +1,24 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +import traceback + +from django.core.management.base import BaseCommand + +from submissions.models import Submission + + +class Command(BaseCommand): + """Verify the metadata formatting and flag errors.""" + + def handle(self, *args, **kwargs): + for sub in Submission.objects.all(): + # Check that the author list is properly formatted + try: + if 'entries' in sub.metadata: + author_str_list = [ + a['name'].split()[-1] for a in sub.metadata['entries'][0]['authors']] + except: + print('Error for %s' % sub.preprint) + traceback.print_exc() diff --git a/conflicts/management/commands/update_coi_via_arxiv.py b/conflicts/management/commands/update_coi_via_arxiv.py index 288a63e4ad5d9ee8f810d3bf96e28f371ca7a2b4..9c49692a6fb5d010e89ffab5045871e6630c8f0b 100644 --- a/conflicts/management/commands/update_coi_via_arxiv.py +++ b/conflicts/management/commands/update_coi_via_arxiv.py @@ -35,15 +35,13 @@ class Command(BaseCommand): # Get all possibly relevant Profiles author_str_list = [a.split()[-1] for a in sub.author_list.split(',')] if 'entries' in sub.metadata: - sub.metadata['entries'][0]['authors'] - # last_names = [] author_str_list += [ a['name'].split()[-1] for a in sub.metadata['entries'][0]['authors']] - author_str_list = set(author_str_list) # Distinct operator + author_str_list = set(author_str_list) # Distinct operator author_profiles = Profile.objects.filter( Q(contributor__in=sub.authors.all()) | Q(last_name__in=author_str_list)).distinct() n_new_conflicts += caller.compare(author_profiles, fellow_profiles, submission=sub) Submission.objects.filter(id=sub.id).update(needs_conflicts_update=False) - return n_new_conflicts + return str(n_new_conflicts) diff --git a/forums/models.py b/forums/models.py index 47b56585aee6dec7b173de21acf4f2c1d8efb798..39f9ff08ae684e832ab6455f74ff109d1e4e7a2f 100644 --- a/forums/models.py +++ b/forums/models.py @@ -208,7 +208,7 @@ class Post(models.Model): self.posted_by.last_name, self.subject[:32]) def get_absolute_url(self): - return '%s#post%s' % (self.get_forum().get_absolute_url(), self.id) + return '%s#post%s' % (self.get_anchor_forum_or_meeting().get_absolute_url(), self.id) @property def nr_followups(self): @@ -226,16 +226,17 @@ class Post(models.Model): print ('post %s id_list: %s' % (self.id, id_list)) return id_list - def get_forum(self): + def get_anchor_forum_or_meeting(self): """ Climb back the hierarchy up to the original Forum. If no Forum is found, return None. """ type_forum = ContentType.objects.get_by_natural_key('forums', 'forum') - if self.parent_content_type == type_forum: + type_meeting = ContentType.objects.get_by_natural_key('forums', 'meeting') + if self.parent_content_type == type_forum or self.parent_content_type == type_meeting: return self.parent else: - return self.parent.get_forum() + return self.parent.get_anchor_forum_or_meeting() class Motion(Post): diff --git a/forums/templates/forums/forum_detail.html b/forums/templates/forums/forum_detail.html index 6b1be878e9c83d273d1a00e606d6eb62c6f83a0b..2d3d0626f9e85dd704bed50b123960a334167464 100644 --- a/forums/templates/forums/forum_detail.html +++ b/forums/templates/forums/forum_detail.html @@ -19,7 +19,7 @@ <div class="row"> <div class="col-12"> - <h3 class="highlight"> + <h2 class="highlight"> {% if forum.meeting %} {% with context_colors=forum.meeting.context_colors %} <span class="badge badge-{{ context_colors.bg }} mx-0 mb-2 p-2 text-{{ context_colors.text }}"> @@ -34,7 +34,7 @@ <a href="{{ forum.get_absolute_url }}">{{ forum }}</a> <span class="badge badge-primary badge-pill">{% with nr_posts=forum.nr_posts %}{{ nr_posts }} post{{ nr_posts|pluralize }}{% endwith %}</span> </span> - </h3> + </h2> {% if forum.parent %} <p>Parent: <a href="{{ forum.parent.get_absolute_url }}">{{ forum.parent }}</a></p> @@ -92,39 +92,45 @@ {% endif %} - <h3>Table of Contents</h3> - <ul> - <li><a href="#Description">Description</a></li> - {% if forum.meeting %} - <li><a href="#Preamble">Preamble</a></li> - <li><a href="#Motions">Motions</a></li> - {% endif %} - <li><a href="#Posts">Posts</a></li> - {% if forum.meeting %} - <li><a href="#Minutes">Minutes</a></li> - {% endif %} - </ul> + <h2>Table of Contents</h2> + <div class="m-2"> + <ul> + <li><a href="#Description">Description</a></li> + {% if forum.meeting %} + <li><a href="#Preamble">Preamble</a></li> + <li><a href="#Motions">Motions</a></li> + {% endif %} + <li><a href="#Posts">Posts</a></li> + {% if forum.meeting %} + <li><a href="#Minutes">Minutes</a></li> + {% endif %} + </ul> + </div> </div> </div> <div class="row"> <div class="col-12"> - <h3 class="highlight" id="Description">Description</h3> - {{ forum.description|restructuredtext }} + <h2 class="highlight" id="Description">Description</h2> + <div class="m-2"> + {{ forum.descripteion|restructuredtext }} + </div> </div> </div> {% if forum.meeting %} <div class="row"> <div class="col-12"> - <h3 class="highlight" id="Preamble">Preamble</h3> - {{ forum.meeting.preamble|restructuredtext }} + <h2 class="highlight" id="Preamble">Preamble</h2> + <div class="m-2"> + {{ forum.meeting.preamble|restructuredtext }} + </div> </div> </div> <div class="row"> <div class="col-12"> - <h3 class="highlight" id="Motions">Motions</h3> + <h2 class="highlight" id="Motions">Motions</h2> <ul> {% if forum.meeting.future %} <li>Adding Motions will be activated once the meeting starts</li> @@ -143,7 +149,7 @@ <div class="row"> <div class="col-12"> - <h3 class="highlight" id="Posts">Posts</h3> + <h2 class="highlight" id="Posts">Posts</h2> <ul> <li><a href="{% url 'forums:post_create' slug=forum.slug parent_model='forum' parent_id=forum.id %}">Add a new Post</a></li> </ul> @@ -158,8 +164,10 @@ {% if forum.meeting %} <div class="row"> <div class="col-12"> - <h3 class="highlight" id="Minutes">Minutes</h3> - {{ forum.meeting.minutes|restructuredtext }} + <h2 class="highlight" id="Minutes">Minutes</h2> + <div class="m-2"> + {{ forum.meeting.minutes|restructuredtext }} + </div> </div> </div> {% endif %} diff --git a/forums/templates/forums/motion_confirm_create.html b/forums/templates/forums/motion_confirm_create.html new file mode 100644 index 0000000000000000000000000000000000000000..3d626c7063acf74c3642f169f781058a5d746042 --- /dev/null +++ b/forums/templates/forums/motion_confirm_create.html @@ -0,0 +1,58 @@ +{% extends 'forums/base.html' %} + +{% load bootstrap %} +{% load restructuredtext %} + + +{% block breadcrumb_items %} +{{ block.super }} +<span class="breadcrumb-item">Confirm Motion creation</span> +{% endblock %} + +{% block pagetitle %}: Motion: confirm creation{% endblock pagetitle %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h3 class="highlight">Preview</h3> + + <div class="card"> + <div class="card-header"> + {{ form.initial.subject }} + </div> + <div class="card-body"> + {% if form.initial.text %} + {{ form.initial.text|restructuredtext }} + {% else %} + <span class="text-danger">No text given</span> + {% endif %} + </div> + </div> + + {% if form.errors %} + {% for field in form %} + {% for error in field.errors %} + <div class="alert alert-danger"> + <strong>{{ field.name }} - {{ error|escape }}</strong> + </div> + {% endfor %} + {% endfor %} + {% for error in form.non_field_errors %} + <div class="alert alert-danger"> + <strong>{{ error|escape }}</strong> + </div> + {% endfor %} + {% endif %} + + <form action="" method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input type="submit" value="Confirm Motion creation" class="btn btn-primary"> + <span class="text-danger"> <strong>Not satisfied?</strong> Hit your browser's back button and redraft your Motion</span> + </form> + + </div> +</div> + +{% endblock content %} diff --git a/forums/templates/forums/motion_form.html b/forums/templates/forums/motion_form.html new file mode 100644 index 0000000000000000000000000000000000000000..ce4f3e20e1d9dee884a9beed919c0ac70660e26c --- /dev/null +++ b/forums/templates/forums/motion_form.html @@ -0,0 +1,27 @@ +{% extends 'forums/base.html' %} + +{% load bootstrap %} +{% load restructuredtext %} + + +{% block breadcrumb_items %} +{{ block.super }} +<span class="breadcrumb-item">{% if form.instance.id %}Update {{ form.instance }}{% else %}New Motion{% endif %}</span> +{% endblock %} + +{% block pagetitle %}: Motion{% endblock pagetitle %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h3 class="highlight">Create a new Motion</h3> + <form action="" method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input type="submit" value="Run preview" class="btn btn-primary"> + </form> + </div> +</div> + +{% endblock content %} diff --git a/forums/templates/forums/post_card.html b/forums/templates/forums/post_card.html index 78da1449f3b3de3c22d50cd9f098583781fc83f5..6abd15a0b6c62b46c98390a7636a313a4c27c3a6 100644 --- a/forums/templates/forums/post_card.html +++ b/forums/templates/forums/post_card.html @@ -1,6 +1,6 @@ {% load restructuredtext %} -<div class="card {% if post.motion %}text-white bg-dark{% else %}text-body{% endif %}" id="post{{ post.id }}"> +<div class="card m-2 {% if post.motion %}text-white bg-dark{% else %}text-body{% endif %}" id="post{{ post.id }}"> <div class="card-header"> {{ post.subject }} <div class="postInfo"> diff --git a/forums/views.py b/forums/views.py index dc9eb86728d7b1d623d1779db96f6280ec8aa782..6f9df6f59780bd3d1f8e3e37385d247b3e5e93bd 100644 --- a/forums/views.py +++ b/forums/views.py @@ -236,7 +236,7 @@ class MotionCreateView(PostCreateView): """ model = Motion form_class = MotionForm - template_name = 'forums/post_form.html' + template_name = 'forums/motion_form.html' def get_initial(self, *args, **kwargs): initial = super().get_initial(*args, **kwargs) @@ -310,6 +310,7 @@ class MotionConfirmCreateView(PostConfirmCreateView): Specialization of PostConfirmCreateView to Motion-class objects. """ form_class = MotionForm + template_name = 'forums/motion_confirm_create.html' def get_initial(self, *args, **kwargs): initial = super().get_initial(*args, **kwargs) diff --git a/journals/services.py b/journals/services.py index 4cd4bb77ab7941818a5bfc36c720b25bdeb25fd1..5d8ea0d78b70bfca1b5c6917017fc492482623d8 100644 --- a/journals/services.py +++ b/journals/services.py @@ -61,7 +61,12 @@ def update_citedby(doi_label): 'Please contact the SciPost Admin.') return - response_deserialized = ET.fromstring(r.text) + try: + response_deserialized = ET.fromstring(r.text) + except ET.ParseError: # something went wrong, abort + logger.info('Response parsing failed for doi: %s', publication.doi_string) + return + prefix = '{http://www.crossref.org/qrschema/2.0}' citations = [] for link in response_deserialized.iter(prefix + 'forward_link'): diff --git a/journals/templates/journals/base.html b/journals/templates/journals/base.html index 48517580e9c8cd51edadc640d79f12fa546c2631..5e3cc60aefa15fff3ac6e39f5d97002fb6b88573 100644 --- a/journals/templates/journals/base.html +++ b/journals/templates/journals/base.html @@ -40,11 +40,20 @@ <div class="row my-1"> <div class="col-12"> {% if journal.active %} - <p>{{journal}} is published by the SciPost Foundation under the journal doi: 10.21468/{{journal.name}}{% if journal.issn %} and ISSN {{journal.issn}}{% endif %}.</p> + <p>{{journal}} is published by the SciPost Foundation under the journal doi: 10.21468/{{journal.name}}{% if journal.issn %} and ISSN {{journal.issn}}{% endif %}.</p> {% endif %} {% if journal.doi_label == 'SciPostPhys' %} - <p>SciPost Physics has been awarded the DOAJ Seal <img src="{% static 'scipost/images/DOAJ_Seal_logo_big.png' %}" alt="DOAJ Seal" width="40"> from the <a href="https://doaj.org">Directory of Open Access Journals</a></p> - <p class="mb-0">All content in {{ journal }} is deposited and permanently preserved in the CLOCKSS archive <a href="https://www.clockss.org/clockss/Home" target="_blank"><img src="{% static 'scipost/images/clockss_original_logo_boxed_ai-cropped-90.png' %}" alt="CLOCKSS logo" width="40"></a></p> + <p class="mb-1"> + SciPost Physics has been awarded the DOAJ Seal <img src="{% static 'scipost/images/DOAJ_Seal_logo_big.png' %}" alt="DOAJ Seal" width="40"> from the <a href="https://doaj.org">Directory of Open Access Journals</a> + </p> + <p> + All content in {{ journal }} is deposited and permanently preserved in the CLOCKSS archive <a href="https://www.clockss.org/clockss/Home" target="_blank"><img src="{% static 'scipost/images/clockss_original_logo_boxed_ai-cropped-90.png' %}" alt="CLOCKSS logo" width="40"></a> + </p> + <p class="mb-1"> + Self-computed impact factor + <small><sup><i class="fa fa-question-circle" data-toggle="tooltip" data-html="true" title="Number of citations in year N for papers published in years N-1 and N-2,<br/>divided by the number of papers in those years.<br/>Data obtained from Crossref's Cited-by service"></i></sup></small> for 2018 (using <a href="https://www.crossref.org/services/cited-by/" target="_blank">Crossref Cited-by</a> data) +: <strong>5.25</strong> + </p> {% endif %} </div> </div> diff --git a/journals/templatetags/lookup.py b/journals/templatetags/lookup.py index 0cdcd41b33540a993e5f23bbdedf3931f4ae882b..cdb5e7fd3ca81cd803bc0b19bfde70239e1d32d9 100644 --- a/journals/templatetags/lookup.py +++ b/journals/templatetags/lookup.py @@ -10,7 +10,6 @@ from ajax_select import register, LookupChannel from ..models import Publication from funders.models import Funder, Grant -from organizations.models import Organization @register('publication_lookup') @@ -43,30 +42,6 @@ class PublicationLookup(LookupChannel): raise PermissionDenied -@register('organization_lookup') -class OrganizationLookup(LookupChannel): - model = Organization - - def get_query(self, q, request): - return (self.model.objects.order_by('name') - .filter(Q(name__icontains=q) | - Q(acronym__icontains=q) | - Q(name_original__icontains=q))[:10]) - - def format_item_display(self, item): - """(HTML) format item for displaying item in the selected deck area.""" - return u"<span class='auto_lookup_display'>%s</span>" % item.full_name_with_acronym - - def format_match(self, item): - """(HTML) Format item for displaying in the dropdown.""" - return item.full_name_with_acronym - - def check_auth(self, request): - """Check if has organization administrative permissions.""" - if not request.user.has_perm('scipost.can_manage_organizations'): - raise PermissionDenied - - @register('funder_lookup') class FunderLookup(LookupChannel): model = Funder diff --git a/notifications/views.py b/notifications/views.py index 955e23ab384be6c2759f35f6241ce61c6f9d8d55..a98a37183f284f429ea851bfbe2898c1248c7ca4 100644 --- a/notifications/views.py +++ b/notifications/views.py @@ -65,57 +65,15 @@ def live_unread_notification_count(request): def live_notification_list(request): """Return JSON of unread count and content of messages.""" - if not request.user.is_authenticated(): - data = { - 'unread_count': 0, - 'list': [] - } - return JsonResponse(data) - - try: - # Default to 5 as a max number of notifications - num_to_fetch = max(int(request.GET.get('max', 10)), 1) - num_to_fetch = min(num_to_fetch, 100) - except ValueError: - num_to_fetch = 5 - - try: - offset = int(request.GET.get('offset', 0)) - except ValueError: - offset = 0 - - list = [] - - #for n in request.user.notifications.all()[offset:offset + num_to_fetch]: - # Kill notifications for now - for n in None: - struct = model_to_dict(n) - # struct['unread'] = struct['pseudo_unread'] - struct['slug'] = id2slug(n.id) - if n.actor: - if isinstance(n.actor, User): - # Humanize if possible - struct['actor'] = '{f} {l}'.format(f=n.actor.first_name, l=n.actor.last_name) - else: - struct['actor'] = str(n.actor) - if n.target: - if hasattr(n.target, 'notification_name'): - struct['target'] = n.target.notification_name - else: - struct['target'] = str(n.target) - struct['forward_link'] = n.get_absolute_url() - if n.action_object: - struct['action_object'] = str(n.action_object) - struct['timesince'] = n.timesince() - - list.append(struct) - - if request.GET.get('mark_as_read'): - # Mark all as read - request.user.notifications.mark_all_as_read() + # if not request.user.is_authenticated(): + # data = { + # 'unread_count': 0, + # 'list': [] + # } + # return JsonResponse(data) data = { - 'unread_count': request.user.notifications.unread().count(), - 'list': list + 'unread_count': 0, + 'list': [] } return JsonResponse(data) diff --git a/organizations/management/commands/gobble_institutions.py b/organizations/management/commands/gobble_institutions.py new file mode 100644 index 0000000000000000000000000000000000000000..45a21fc8a7f36ae80ecfac4f87e34b59efb36f1e --- /dev/null +++ b/organizations/management/commands/gobble_institutions.py @@ -0,0 +1,30 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django.core.management.base import BaseCommand + +from affiliations.models import Affiliation as deprec_Affiliation +from affiliations.models import Institution +from organizations.models import Organization +from profiles.models import Affiliation + + +class Command(BaseCommand): + help = ('For affiliations.Institution objects with a defined organization ' + 'field, update the user Profiles to the new profiles.Affiliation objects ' + 'and delete the deprecated Institution and Affiliation objects.') + + def handle(self, *args, **kwargs): + for inst in Institution.objects.exclude(organization=None): + print('Handling institution %s' % str(inst)) + for deprec_aff in deprec_Affiliation.objects.filter(institution=inst): + Affiliation.objects.create( + profile=deprec_aff.contributor.profile, + organization=deprec_aff.institution.organization, + date_from=deprec_aff.begin_date, + date_until=deprec_aff.end_date) + print('\t\tDeleting affiliation %s' % str(deprec_aff)) + deprec_aff.delete() + print('\tDeleting institution %s' % str(inst)) + inst.delete() diff --git a/organizations/templates/organizations/organization_list.html b/organizations/templates/organizations/organization_list.html index 4cce72c17bbb585be1092f2a5172fbaf4ce6ab1d..eca989ccb45d1f9a0f2c485fbccfebce1757b9d2 100644 --- a/organizations/templates/organizations/organization_list.html +++ b/organizations/templates/organizations/organization_list.html @@ -36,6 +36,7 @@ $(document).ready(function($) { <li><a href="{% url 'organizations:dashboard' %}">Go to the dashboard</a></li> <li><a href="{% url 'organizations:organization_create' %}">Create a new Organization instance</a></li> <li><a href="{% url 'funders:funders_dashboard' %}">Link Funders to Organizations</a> ({{ nr_funders_wo_organization }} found in need of linking)</li> + <li><a href="{% url 'affiliations:institutions_without_organization' %}">Link (deprecated) affiliations.Institutions to Organizations</a> ({{ nr_institutions_wo_organization }} found in need of linking)</li> </ul> {% endif %} </div> diff --git a/organizations/templatetags/lookup.py b/organizations/templatetags/lookup.py new file mode 100644 index 0000000000000000000000000000000000000000..a57ad364a16de5531dce5487b85fd13bdbef24d5 --- /dev/null +++ b/organizations/templatetags/lookup.py @@ -0,0 +1,33 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django.core.exceptions import PermissionDenied +from django.db.models import Q + +from ajax_select import register, LookupChannel + +from ..models import Organization + + +@register('organization_lookup') +class OrganizationLookup(LookupChannel): + model = Organization + + def get_query(self, q, request): + return (self.model.objects.order_by('name') + .filter(Q(name__icontains=q) | + Q(acronym__icontains=q) | + Q(name_original__icontains=q))[:10]) + + def format_item_display(self, item): + """(HTML) format item for displaying item in the selected deck area.""" + return u"<span class='auto_lookup_display'>%s</span>" % item.full_name_with_acronym + + def format_match(self, item): + """(HTML) Format item for displaying in the dropdown.""" + return item.full_name_with_acronym + + def check_auth(self, request): + """Allow use by everybody (this is used in the registration form).""" + pass diff --git a/organizations/views.py b/organizations/views.py index 78d73153fd2b314985a98ebc5db0f60f57751cb8..ee7e856be60608d4865329f549d61d8672ff3a0c 100644 --- a/organizations/views.py +++ b/organizations/views.py @@ -23,6 +23,7 @@ from .forms import OrganizationEventForm, ContactPersonForm,\ NewContactForm, ContactActivationForm, ContactRoleForm from .models import Organization, OrganizationEvent, ContactPerson, Contact, ContactRole +from affiliations.models import Institution from funders.models import Funder from mails.utils import DirectMailUtil from mails.views import MailEditorSubview @@ -70,6 +71,9 @@ class OrganizationListView(PaginationMixin, ListView): context = super().get_context_data(*args, **kwargs) if self.request.user.has_perm('scipost.can_manage_organizations'): context['nr_funders_wo_organization'] = Funder.objects.filter(organization=None).count() + if self.request.user.has_perm('scipost.can_manage_organizations'): + context['nr_institutions_wo_organization'] = Institution.objects.filter( + organization=None).count() context['pubyears'] = range(int(timezone.now().strftime('%Y')), 2015, -1) context['countrycodes'] = [code['country'] for code in list( Organization.objects.all().distinct('country').values('country'))] diff --git a/profiles/admin.py b/profiles/admin.py index 1b54b2c2a72c5d9253e0c89c62ec59d0c5cce768..d0e860a7fbdcafc4a33b4e80d029d04ef8a3dd3a 100644 --- a/profiles/admin.py +++ b/profiles/admin.py @@ -4,7 +4,7 @@ __license__ = "AGPL v3" from django.contrib import admin -from .models import Profile, ProfileEmail, ProfileNonDuplicates +from .models import Profile, ProfileEmail, ProfileNonDuplicates, Affiliation class ProfileEmailInline(admin.TabularInline): @@ -12,10 +12,15 @@ class ProfileEmailInline(admin.TabularInline): extra = 0 +class AffiliationInline(admin.TabularInline): + model = Affiliation + extra = 0 + + 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] + inlines = [ProfileEmailInline, AffiliationInline] admin.site.register(Profile, ProfileAdmin) diff --git a/profiles/constants.py b/profiles/constants.py index fb7c120de3db2c927f5fbab193a24b4adffff12c..f318b40b24f8914b7b9f17bf88a0470712fc8442 100644 --- a/profiles/constants.py +++ b/profiles/constants.py @@ -9,3 +9,34 @@ PROFILE_NON_DUPLICATE_REASONS = ( (DIFFERENT_PEOPLE, 'These are different people'), (MULTIPLE_ALLOWED, 'Multiple Profiles allowed for this person'), ) + + +AFFILIATION_CATEGORY_EMPLOYED_PROF_FULL = 'employed_prof_full' +AFFILIATION_CATEGORY_EMPLOYED_PROF_ASSOCIATE = 'employed_prof_associate' +AFFILIATION_CATEGORY_EMPLOYED_PROF_ASSISTANT = 'employed_prof_assistant' +AFFILIATION_CATEGORY_EMPLOYED_PROF_EMERITUS = 'employed_prof_emeritus' +AFFILIATION_CATEGORY_EMPLOYED_PERMANENT_STAFF = 'employed_permanent_staff' +AFFILIATION_CATEGORY_EMPLOYED_FIXED_TERM_STAFF = 'employed_fixed_term_staff' +AFFILIATION_CATEGORY_EMPLOYED_TENURE_TRACK = 'employed_tenure_track' +AFFILIATION_CATEGORY_EMPLOYED_POSTDOC = 'employed_postdoc' +AFFILIATION_CATEGORY_EMPLOYED_PhD = 'employed_phd' +AFFILIATION_CATEGORY_ASSOCIATE_SCIENTIST = 'associate_scientist' +AFFILIATION_CATEGORY_CONSULTANT = 'consultant' +AFFILIATION_CATEGORY_VISITOR = 'visitor' +AFFILIATION_CATEGORY_UNSPECIFIED = 'unspecified' + +AFFILIATION_CATEGORIES = ( + (AFFILIATION_CATEGORY_EMPLOYED_PROF_FULL, 'Full Professor'), + (AFFILIATION_CATEGORY_EMPLOYED_PROF_ASSOCIATE, 'Associate Professor'), + (AFFILIATION_CATEGORY_EMPLOYED_PROF_ASSISTANT, 'Assistant Professor'), + (AFFILIATION_CATEGORY_EMPLOYED_PROF_EMERITUS, 'Emeritus Professor'), + (AFFILIATION_CATEGORY_EMPLOYED_PERMANENT_STAFF, 'Permanent Staff'), + (AFFILIATION_CATEGORY_EMPLOYED_FIXED_TERM_STAFF, 'Fixed Term Staff'), + (AFFILIATION_CATEGORY_EMPLOYED_TENURE_TRACK, 'Tenure Tracker'), + (AFFILIATION_CATEGORY_EMPLOYED_POSTDOC, 'Postdoctoral Researcher'), + (AFFILIATION_CATEGORY_EMPLOYED_PhD, 'PhD candidate'), + (AFFILIATION_CATEGORY_ASSOCIATE_SCIENTIST, 'Associate Scientist'), + (AFFILIATION_CATEGORY_CONSULTANT, 'Consultant'), + (AFFILIATION_CATEGORY_VISITOR, 'Visitor'), + (AFFILIATION_CATEGORY_UNSPECIFIED, 'Unspecified'), +) diff --git a/profiles/forms.py b/profiles/forms.py index dfba810721e71273c78feab0bfdf397948b161e2..59dd7b319ed49297de9138a3f2e9c9586e52239b 100644 --- a/profiles/forms.py +++ b/profiles/forms.py @@ -5,6 +5,8 @@ __license__ = "AGPL v3" from django import forms from django.shortcuts import get_object_or_404 +from ajax_select.fields import AutoCompleteSelectField + from common.forms import ModelChoiceFieldwithid from invitations.models import RegistrationInvitation from journals.models import UnregisteredAuthor @@ -12,7 +14,7 @@ from ontology.models import Topic from scipost.models import Contributor from submissions.models import RefereeInvitation -from .models import Profile, ProfileEmail +from .models import Profile, ProfileEmail, Affiliation class ProfileForm(forms.ModelForm): @@ -136,7 +138,10 @@ class ProfileMergeForm(forms.Form): email__in=profile.emails.values_list('email', flat=True)).update( primary=False, profile=profile) - # Move all invitations to the "new" profile. + # Move all affiliations to the "new" profile + profile_old.affiliations.all().update(profile=profile) + + # Move all invitations to the "new" profile profile_old.refereeinvitation_set.all().update(profile=profile) profile_old.registrationinvitation_set.all().update(profile=profile) @@ -165,3 +170,16 @@ class ProfileEmailForm(forms.ModelForm): """Save to a profile.""" self.instance.profile = self.profile return super().save() + + +class AffiliationForm(forms.ModelForm): + organization = AutoCompleteSelectField('organization_lookup') + + class Meta: + model = Affiliation + fields = ['profile', 'organization', 'category', + 'description', 'date_from', 'date_until'] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['profile'].widget = forms.HiddenInput() diff --git a/profiles/managers.py b/profiles/managers.py index 556d1af12dd01dd665df986926f745aab885043f..25212f24ab3d5163ba8467926f572967114997cc 100644 --- a/profiles/managers.py +++ b/profiles/managers.py @@ -31,3 +31,27 @@ class ProfileQuerySet(models.QuerySet): ).filter(nr_count__gt=1).exclude(full_name__in=nonduplicate_full_names) return profiles.filter(full_name__in=[item['full_name'] for item in duplicates] ).order_by('last_name', 'first_name', '-id') + + def specialties_overlap(self, discipline, expertises=[]): + """ + Returns all Profiles specialized in the given discipline + and any of the (optional) expertises. + + This method is also separately implemented for Contributor and Fellowship objects. + """ + qs = self.filter(discipline=discipline) + if expertises and len(expertises) > 0: + qs = qs.filter(expertises__overlap=expertises) + return qs + + def specialties_contain(self, discipline, expertises=[]): + """ + Returns all Profiles specialized in the given discipline + and all of the (optional) expertises. + + This method is also separately implemented for Contributor and Fellowship objects. + """ + qs = self.filter(discipline=discipline) + if expertises and len(expertises) > 0: + qs = qs.filter(expertises__contains=expertises) + return qs diff --git a/profiles/migrations/0018_affiliation.py b/profiles/migrations/0018_affiliation.py new file mode 100644 index 0000000000000000000000000000000000000000..83b3d0301775c1b37145ac5e706c477daba79e23 --- /dev/null +++ b/profiles/migrations/0018_affiliation.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2019-03-27 14:14 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('organizations', '0010_auto_20190223_1406'), + ('profiles', '0017_auto_20190126_2058'), + ] + + operations = [ + migrations.CreateModel( + name='Affiliation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('category', models.CharField(choices=[('employed_prof_full', 'Full Professor'), ('employed_prof_associate', 'Associate Professor'), ('employed_prof_assistant', 'Assistant Professor'), ('employed_prof_emeritus', 'Emeritus Professor'), ('employed_permanent_staff', 'Permanent Staff'), ('employed_fixed_term_staff', 'Fixed Term Staff'), ('employed_tenure_track', 'Tenure Tracker'), ('employed_postdoc', 'Postdoctoral Researcher'), ('employed_phd', 'PhD candidate'), ('associate_scientist', 'Associate Scientist'), ('consultant', 'Consultant'), ('visitor', 'Visotor')], help_text='Select the most suitable category', max_length=64)), + ('description', models.CharField(max_length=256)), + ('date_from', models.DateField(blank=True, null=True)), + ('date_until', models.DateField(blank=True, null=True)), + ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='affiliations', to='organizations.Organization')), + ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='affiliations', to='profiles.Profile')), + ], + options={ + 'ordering': ['profile__user__last_name', 'profile__user__first_name', 'date_until'], + 'default_related_name': 'affiliations', + }, + ), + ] diff --git a/profiles/migrations/0019_auto_20190327_1520.py b/profiles/migrations/0019_auto_20190327_1520.py new file mode 100644 index 0000000000000000000000000000000000000000..ee2a8810c796494d4e7f5922013c2019a266a264 --- /dev/null +++ b/profiles/migrations/0019_auto_20190327_1520.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2019-03-27 14:20 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0018_affiliation'), + ] + + operations = [ + migrations.AlterModelOptions( + name='affiliation', + options={'ordering': ['profile__last_name', 'profile__first_name', 'date_until']}, + ), + migrations.AlterField( + model_name='affiliation', + name='category', + field=models.CharField(choices=[('employed_prof_full', 'Full Professor'), ('employed_prof_associate', 'Associate Professor'), ('employed_prof_assistant', 'Assistant Professor'), ('employed_prof_emeritus', 'Emeritus Professor'), ('employed_permanent_staff', 'Permanent Staff'), ('employed_fixed_term_staff', 'Fixed Term Staff'), ('employed_tenure_track', 'Tenure Tracker'), ('employed_postdoc', 'Postdoctoral Researcher'), ('employed_phd', 'PhD candidate'), ('associate_scientist', 'Associate Scientist'), ('consultant', 'Consultant'), ('visitor', 'Visitor')], help_text='Select the most suitable category', max_length=64), + ), + ] diff --git a/profiles/migrations/0020_auto_20190327_1713.py b/profiles/migrations/0020_auto_20190327_1713.py new file mode 100644 index 0000000000000000000000000000000000000000..b9d9b127c257ee4098f097bc1ac9dcb57d979ca3 --- /dev/null +++ b/profiles/migrations/0020_auto_20190327_1713.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2019-03-27 16:13 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0019_auto_20190327_1520'), + ] + + operations = [ + migrations.AlterModelOptions( + name='affiliation', + options={'ordering': ['profile__last_name', 'profile__first_name', '-date_until']}, + ), + ] diff --git a/profiles/migrations/0021_auto_20190329_0744.py b/profiles/migrations/0021_auto_20190329_0744.py new file mode 100644 index 0000000000000000000000000000000000000000..28fd2593584769f68a491869f264b9dbcfb70ed7 --- /dev/null +++ b/profiles/migrations/0021_auto_20190329_0744.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2019-03-29 06:44 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0020_auto_20190327_1713'), + ] + + operations = [ + migrations.AlterField( + model_name='affiliation', + name='category', + field=models.CharField(choices=[('employed_prof_full', 'Full Professor'), ('employed_prof_associate', 'Associate Professor'), ('employed_prof_assistant', 'Assistant Professor'), ('employed_prof_emeritus', 'Emeritus Professor'), ('employed_permanent_staff', 'Permanent Staff'), ('employed_fixed_term_staff', 'Fixed Term Staff'), ('employed_tenure_track', 'Tenure Tracker'), ('employed_postdoc', 'Postdoctoral Researcher'), ('employed_phd', 'PhD candidate'), ('associate_scientist', 'Associate Scientist'), ('consultant', 'Consultant'), ('visitor', 'Visitor'), ('unspecified', 'Unspecified')], default='unspecified', help_text='Select the most suitable category', max_length=64), + ), + ] diff --git a/profiles/migrations/0022_auto_20190331_1926.py b/profiles/migrations/0022_auto_20190331_1926.py new file mode 100644 index 0000000000000000000000000000000000000000..aaed115c4ba9240c0f6ac932133b795da4d385a2 --- /dev/null +++ b/profiles/migrations/0022_auto_20190331_1926.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2019-03-31 17:26 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0021_auto_20190329_0744'), + ] + + operations = [ + migrations.AlterField( + model_name='affiliation', + name='description', + field=models.CharField(blank=True, max_length=256, null=True), + ), + ] diff --git a/profiles/models.py b/profiles/models.py index 2a7e9b5af27a07ebd2069a4c7ec0265d2082896c..0a4fafa693d19d57fe0b6f6eafa0f44bd0059b95 100644 --- a/profiles/models.py +++ b/profiles/models.py @@ -18,7 +18,8 @@ from journals.models import Publication, PublicationAuthorsTable from ontology.models import Topic from theses.models import ThesisLink -from .constants import PROFILE_NON_DUPLICATE_REASONS +from .constants import (PROFILE_NON_DUPLICATE_REASONS, + AFFILIATION_CATEGORIES, AFFILIATION_CATEGORY_UNSPECIFIED) from .managers import ProfileQuerySet @@ -164,3 +165,48 @@ class ProfileNonDuplicates(models.Model): @property def full_name(self): return '%s%s' % (self.profiles.first().last_name, self.profiles.first().first_name) + + + +################ +# Affiliations # +################ + +class Affiliation(models.Model): + """ + Link between a Profile and an Organization, for a specified time interval. + + Fields: + * profile + * organization + * description + * date_from + * date_until + + Affiliations can overlap in time. + + Ideally, each Profile would have at least one valid Affiliation at each moment + of time during the whole duration of that person's career. + """ + profile = models.ForeignKey('profiles.Profile', on_delete=models.CASCADE, + related_name='affiliations') + organization = models.ForeignKey('organizations.Organization', on_delete=models.CASCADE, + related_name='affiliations') + category = models.CharField(max_length=64, choices=AFFILIATION_CATEGORIES, + default=AFFILIATION_CATEGORY_UNSPECIFIED, + help_text='Select the most suitable category') + description = models.CharField(max_length=256, blank=True, null=True) + date_from = models.DateField(blank=True, null=True) + date_until = models.DateField(blank=True, null=True) + + class Meta: + default_related_name = 'affiliations' + ordering = ['profile__last_name', 'profile__first_name', + '-date_until'] + + def __str__(self): + return '%s, %s [%s to %s]' % ( + str(self.profile), + str(self.organization), + self.date_from.strftime('%Y-%m-%d') if self.date_from else 'Undefined', + self.date_until.strftime('%Y-%m-%d') if self.date_until else 'Undefined') diff --git a/profiles/templates/profiles/_affiliations_table.html b/profiles/templates/profiles/_affiliations_table.html new file mode 100644 index 0000000000000000000000000000000000000000..9c3303f2b0ec6fc319eb20f8f93810ccfad5a9b0 --- /dev/null +++ b/profiles/templates/profiles/_affiliations_table.html @@ -0,0 +1,36 @@ +<table class="table"> + <thead class="thead-default"> + <tr> + <th>Organization</th> + <th>Category</th> + <th>From</th> + <th>Until</th> + {% if actions %} + <td>Actions</td> + {% endif %} + </tr> + </thead> + <tbody> + {% for aff in profile.affiliations.all %} + <tr> + <td> + {{ aff.organization }} + {% if aff.description %}<br/> <em>{{ aff.description }}</em>{% endif %} + </td> + <td>{{ aff.get_category_display }}</td> + <td>{% if aff.date_from %}{{ aff.date_from|date:'Y-m-d' }}{% else %}Undefined{% endif %}</td> + <td>{% if aff.date_until %}{{ aff.date_until|date:'Y-m-d' }}{% else %}Undefined{% endif %}</td> + {% if actions %} + <td> + <ul class="list-unstyled"> + <li><a href="{% url 'profiles:affiliation_update' profile_id=profile.id pk=aff.id %}" class="text-warning">Update</a></li> + <li><a href="{% url 'profiles:affiliation_delete' profile_id=profile.id pk=aff.id %}" class="text-danger">Delete</a></li> + </ul> + </td> + {% endif %} + </tr> + {% empty %} + <tr><td colspan="4">No Affiliation has been defined</td></tr> + {% endfor %} + </tbody> +</table> diff --git a/profiles/templates/profiles/_profile_card.html b/profiles/templates/profiles/_profile_card.html index 503bd04d65b0f3df805f52d734073582574cbae0..448842decfcb031d27ef066efda59fad491b1287 100644 --- a/profiles/templates/profiles/_profile_card.html +++ b/profiles/templates/profiles/_profile_card.html @@ -14,35 +14,44 @@ <table class="table"> <tr> <td>Name:</td> - <td>{{ profile }}</td> + <td>{{ profile }}</td> </tr> + <tr> + <td>Affiliations + <ul> + <li><a href="{% url 'profiles:affiliation_create' profile_id=profile.id %}">Add a new Affiliation</a></li> + </ul> + </td> + <td> + {% include 'profiles/_affiliations_table.html' with profile=profile actions=True %} + </td> <tr> <td>Email(s)</td> <td> <table class="table table-sm"> - <thead> + <thead> + <tr> + <th colspan="2">Email</th> + <th>Still valid</th> + <th></th> + </tr> + </thead> + {% for profile_mail in profile.emails.all %} <tr> - <th colspan="2">Email</th> - <th>Still valid</th> - <th></th> + <td>{% if profile_mail.primary %}<strong>{% endif %}{{ profile_mail.email }}{% if profile_mail.primary %}</strong>{% endif %}</td> + <td>{{ profile_mail.primary|yesno:'Primary,Alternative' }}</td> + <td> + <i class="fa {{ profile_mail.still_valid|yesno:'fa-check-circle text-success,fa-times-circle text-danger' }}"></i> + + </td> + <td class="d-flex"> + <form method="post" action="{% url 'profiles:toggle_email_status' profile_mail.id %}">{% csrf_token %}<button type="submit" class="btn btn-link">{{ profile_mail.still_valid|yesno:'Deprecate,Mark valid' }}</button></form> + <form method="post" action="{% url 'profiles:email_make_primary' profile_mail.id %}">{% csrf_token %}<button type="submit" class="btn btn-link">Make primary</button></form> + <form method="post" action="{% url 'profiles:delete_profile_email' profile_mail.id %}">{% csrf_token %}<button type="submit" class="btn btn-link text-danger ml-2" onclick="return confirm('Sure you want to delete {{ profile_mail.email }}?')"><i class="fa fa-trash"></i></button></form> + </td> </tr> - </thead> - {% for profile_mail in profile.emails.all %} - <tr> - <td>{% if profile_mail.primary %}<strong>{% endif %}{{ profile_mail.email }}{% if profile_mail.primary %}</strong>{% endif %}</td> - <td>{{ profile_mail.primary|yesno:'Primary,Alternative' }}</td> - <td> - <i class="fa {{ profile_mail.still_valid|yesno:'fa-check-circle text-success,fa-times-circle text-danger' }}"></i> - - </td> - <td class="d-flex"> - <form method="post" action="{% url 'profiles:toggle_email_status' profile_mail.id %}">{% csrf_token %}<button type="submit" class="btn btn-link">{{ profile_mail.still_valid|yesno:'Deprecate,Mark valid' }}</button></form> - <form method="post" action="{% url 'profiles:email_make_primary' profile_mail.id %}">{% csrf_token %}<button type="submit" class="btn btn-link">Make primary</button></form> - <form method="post" action="{% url 'profiles:delete_profile_email' profile_mail.id %}">{% csrf_token %}<button type="submit" class="btn btn-link text-danger ml-2" onclick="return confirm('Sure you want to delete {{ profile_mail.email }}?')"><i class="fa fa-trash"></i></button></form> - </td> - </tr> {% endfor %} - </table> + </table> </td> </tr> <tr> diff --git a/profiles/templates/profiles/affiliation_confirm_delete.html b/profiles/templates/profiles/affiliation_confirm_delete.html new file mode 100644 index 0000000000000000000000000000000000000000..b7eeec911df8a8ef14a9eb0b0270c024d97be404 --- /dev/null +++ b/profiles/templates/profiles/affiliation_confirm_delete.html @@ -0,0 +1,29 @@ +{% extends 'profiles/base.html' %} + +{% load bootstrap %} + +{% block breadcrumb_items %} + {{ block.super }} + <span class="breadcrumb-item">Delete {{ affiliation }}</span> +{% endblock %} + +{% block pagetitle %}: Delete Affiliation{% endblock pagetitle %} + +{% block content %} +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Delete Affiliation</h1> + {% include 'profiles/_affiliations_table.html' with contributor=affiliation.profile.contributor actions=False %} + </div> +</div> +<div class="row"> + <div class="col-12"> + <form method="post"> + {% csrf_token %} + <h3 class="mb-2">Are you sure you want to delete this Affiliation?</h3> + <input type="submit" class="btn btn-danger" value="Yes, delete it" /> + </form> + </div> +</div> + +{% endblock content %} diff --git a/profiles/templates/profiles/affiliation_form.html b/profiles/templates/profiles/affiliation_form.html new file mode 100644 index 0000000000000000000000000000000000000000..aefa75e6b83bba23ec9df9ebe3f3920dc0fcd408 --- /dev/null +++ b/profiles/templates/profiles/affiliation_form.html @@ -0,0 +1,46 @@ +{% extends 'profiles/base.html' %} + +{% load bootstrap %} + +{% load scipost_extras %} + +{% block breadcrumb_items %} + {{ block.super }} + <span class="breadcrumb-item">{% if form.instance.id %}Update{% else %}Add new{% endif %} Affiliation</span> +{% endblock %} + +{% block pagetitle %}: Affiliation{% endblock pagetitle %} + +{% block content %} +<div class="row"> + <div class="col-12"> + <h3 class="highlight">Add a new Affiliation to your Profile</h3> + <p class="text-danger">Don't find the organization you need in our list? Please <a href="{% url 'helpdesk:ticket_create' %}">create a Ticket</a> providing us with the details, we'll get back to you!</p> + <form action="" method="post"> + {% csrf_token %} + {{ form|bootstrap }} + + {% if form.errors %} + {% for field in form %} + {% for error in field.errors %} + <div class="alert alert-danger"> + <strong>{{ field.name }} - {{ error|escape }}</strong> + </div> + {% endfor %} + {% endfor %} + {% for error in form.non_field_errors %} + <div class="alert alert-danger"> + <strong>{{ error|escape }}</strong> + </div> + {% endfor %} + {% endif %} + <input type="submit" value="Submit" class="btn btn-primary"> + </form> + </div> +</div> +{% endblock content %} + +{% block footer_script %} + {{ block.super }} + {{ form.media }} +{% endblock footer_script %} diff --git a/profiles/urls.py b/profiles/urls.py index 5fc21faf8468a389094722ae9bd8478528b61237..9e248b56d0dc569406301b812873db07a3723f7e 100644 --- a/profiles/urls.py +++ b/profiles/urls.py @@ -72,4 +72,19 @@ urlpatterns = [ views.delete_profile_email, name='delete_profile_email' ), + url( + r'^(?P<profile_id>[0-9]+)/affiliation/add/$', + views.AffiliationCreateView.as_view(), + name='affiliation_create' + ), + url( + r'^(?P<profile_id>[0-9]+)/affiliation/(?P<pk>[0-9]+)/update/$', + views.AffiliationUpdateView.as_view(), + name='affiliation_update' + ), + url( + r'^(?P<profile_id>[0-9]+)/affiliation/(?P<pk>[0-9]+)/delete/$', + views.AffiliationDeleteView.as_view(), + name='affiliation_delete' + ), ] diff --git a/profiles/views.py b/profiles/views.py index 33aee843b4fb8fe2244c4c8ca20c079d7654b5e4..096725e392741ae85c02d11ddddc6441221d136a 100644 --- a/profiles/views.py +++ b/profiles/views.py @@ -3,6 +3,7 @@ __license__ = "AGPL v3" from django.contrib import messages +from django.contrib.auth.mixins import UserPassesTestMixin from django.core.urlresolvers import reverse, reverse_lazy from django.db import transaction from django.db.models import Q @@ -24,8 +25,8 @@ from invitations.models import RegistrationInvitation from journals.models import UnregisteredAuthor from submissions.models import RefereeInvitation -from .models import Profile, ProfileEmail -from .forms import ProfileForm, ProfileMergeForm, ProfileEmailForm +from .models import Profile, ProfileEmail, Affiliation +from .forms import ProfileForm, ProfileMergeForm, ProfileEmailForm, AffiliationForm @@ -240,7 +241,7 @@ class ProfileListView(PermissionsMixin, PaginationMixin, ListView): context = super().get_context_data(**kwargs) 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) + contributors_wo_profile = Contributor.objects.nonduplicates().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) @@ -377,3 +378,84 @@ def delete_profile_email(request, email_id): profile_email.delete() messages.success(request, 'Email deleted') return redirect(profile_email.profile.get_absolute_url()) + + +class AffiliationCreateView(UserPassesTestMixin, CreateView): + model = Affiliation + form_class = AffiliationForm + template_name = 'profiles/affiliation_form.html' + + def test_func(self): + """ + Allow creating an Affiliation if user is Admin, EdAdmin or is + the Contributor to which this Profile is related. + """ + if self.request.user.has_perm('scipost.can_create_profiles'): + return True + return self.request.user.contributor.profile.id == int(self.kwargs.get('profile_id')) + + def get_initial(self, *args, **kwargs): + initial = super().get_initial(*args, **kwargs) + profile = get_object_or_404(Profile, pk=self.kwargs.get('profile_id')) + initial.update({ + 'profile': profile + }) + return initial + + def get_success_url(self): + """ + If request.user is Admin or EdAdmin, redirect to profile detail view. + Otherwise if request.user is Profile owner, return to personal page. + """ + if self.request.user.has_perm('scipost.can_create_profiles'): + return reverse_lazy('profiles:profile_detail', + kwargs={'pk': self.object.profile.id}) + return reverse_lazy('scipost:personal_page') + + +class AffiliationUpdateView(UserPassesTestMixin, UpdateView): + model = Affiliation + form_class = AffiliationForm + template_name = 'profiles/affiliation_form.html' + + def test_func(self): + """ + Allow updating an Affiliation if user is Admin, EdAdmin or is + the Contributor to which this Profile is related. + """ + if self.request.user.has_perm('scipost.can_create_profiles'): + return True + return self.request.user.contributor.profile.id == int(self.kwargs.get('profile_id')) + + def get_success_url(self): + """ + If request.user is Admin or EdAdmin, redirect to profile detail view. + Otherwise if request.user is Profile owner, return to personal page. + """ + if self.request.user.has_perm('scipost.can_create_profiles'): + return reverse_lazy('profiles:profile_detail', + kwargs={'pk': self.object.profile.id}) + return reverse_lazy('scipost:personal_page') + + +class AffiliationDeleteView(UserPassesTestMixin, DeleteView): + model = Affiliation + + def test_func(self): + """ + Allow deleting an Affiliation if user is Admin, EdAdmin or is + the Contributor to which this Profile is related. + """ + if self.request.user.has_perm('scipost.can_create_profiles'): + return True + return self.request.user.contributor.profile.id == int(self.kwargs.get('profile_id')) + + def get_success_url(self): + """ + If request.user is Admin or EdAdmin, redirect to profile detail view. + Otherwise if request.user is Profile owner, return to personal page. + """ + if self.request.user.has_perm('scipost.can_create_profiles'): + return reverse_lazy('profiles:profile_detail', + kwargs={'pk': self.object.profile.id}) + return reverse_lazy('scipost:personal_page') diff --git a/requirements.txt b/requirements.txt index 5f8150f36acdde695738326547f04b47cbd0be9e..d7e634573575b5f14057c1bcae8036a706af36ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,6 +32,8 @@ Pygments==2.2.0 # Syntax highlighter Sphinx==1.4.9 sphinx-rtd-theme==0.1.9 # Sphinx theme +# Sentry +sentry-sdk==0.7.8 # Testing factory-boy==2.10.0 diff --git a/scipost/forms.py b/scipost/forms.py index 23094e18e6c17dbdf9c423f3231f214f9a41ebf3..d490ba10fc88291e20f11a8bd2890b390eeee209 100644 --- a/scipost/forms.py +++ b/scipost/forms.py @@ -6,6 +6,7 @@ import datetime import pyotp from django import forms +from django.contrib.auth import authenticate from django.contrib.auth.models import User, Group from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.password_validation import validate_password @@ -16,7 +17,6 @@ from django.utils.dates import MONTHS from django_countries import countries from django_countries.widgets import CountrySelectWidget -from django_countries.fields import LazyTypedChoiceField from ajax_select.fields import AutoCompleteSelectField from haystack.forms import ModelSearchForm as HayStackSearchForm @@ -31,7 +31,7 @@ from .models import Contributor, DraftInvitation, UnavailabilityPeriod, \ Remark, AuthorshipClaim, PrecookedEmail, TOTPDevice from .totp import TOTPVerification -from affiliations.models import Affiliation, Institution +from affiliations.models import Affiliation as deprec_Affiliation from common.forms import MonthYearWidget, ModelChoiceFieldwithid from organizations.decorators import has_contact @@ -42,6 +42,7 @@ from funders.models import Grant from invitations.models import CitationNotification from journals.models import PublicationAuthorsTable, Publication from mails.utils import DirectMailUtil +from profiles.models import Profile, ProfileEmail, Affiliation from submissions.models import Submission, EditorialAssignment, RefereeInvitation, Report, \ EditorialCommunication, EICRecommendation from theses.models import ThesisLink @@ -96,15 +97,18 @@ class RegistrationForm(forms.Form): widget=forms.TextInput({ 'placeholder': 'Recommended. Get one at orcid.org'})) discipline = forms.ChoiceField(choices=SCIPOST_DISCIPLINES, label='* Main discipline') - country_of_employment = LazyTypedChoiceField( - choices=countries, label='* Country of employment', initial='NL', - widget=CountrySelectWidget(layout=( - '{widget}<img class="country-select-flag" id="{flag_id}"' - ' style="margin: 6px 4px 0" src="{country.flag}">'))) - affiliation = forms.CharField(label='* Affiliation', max_length=300) + current_affiliation = AutoCompleteSelectField( + 'organization_lookup', + help_text=('Start typing, then select in the popup; ' + 'if you do not find the organization you seek, ' + 'please fill in your institution name and address instead.'), + show_help_text=False, + required=False, + label='* Current affiliation') address = forms.CharField( - label='Address', max_length=1000, - widget=forms.TextInput({'placeholder': 'For postal correspondence'}), required=False) + label='Institution name and address', max_length=1000, + widget=forms.TextInput({'placeholder': '[only if you did not find your affiliation above]'}), + required=False) personalwebpage = forms.URLField( label='Personal web page', required=False, widget=forms.TextInput({'placeholder': 'full URL, e.g. https://www.[yourpage].com'})) @@ -113,10 +117,24 @@ class RegistrationForm(forms.Form): password = forms.CharField(label='* Password', widget=forms.PasswordInput()) password_verif = forms.CharField(label='* Verify password', widget=forms.PasswordInput(), help_text='Your password must contain at least 8 characters') - captcha = ReCaptchaField(label='*Please verify to continue:') + captcha = ReCaptchaField(label='* Please verify to continue:') subscribe = forms.BooleanField( required=False, initial=False, label='Stay informed, subscribe to the SciPost newsletter.') + def clean(self): + """ + Check that either an organization or an address are provided. + """ + cleaned_data = super(RegistrationForm, self).clean() + current_affiliation = cleaned_data.get('current_affiliation', None) + address = cleaned_data.get('address', '') + + if current_affiliation is None and address == '': + raise forms.ValidationError( + 'You must either specify a Current Affiliation, or ' + 'fill in the institution name and address field' + ) + def clean_password(self): password = self.cleaned_data.get('password', '') user = User( @@ -155,11 +173,34 @@ class RegistrationForm(forms.Form): 'password': self.cleaned_data['password'], 'is_active': False }) - institution, __ = Institution.objects.get_or_create( - country=self.cleaned_data['country_of_employment'], - name=self.cleaned_data['affiliation'], - ) + # Get or create a Profile + profile = Profile.objects.filter( + title=self.cleaned_data['title'], + first_name=self.cleaned_data['first_name'], + last_name=self.cleaned_data['last_name'], + discipline=self.cleaned_data['discipline']).first() + if profile is None: + profile = Profile.objects.create( + title=self.cleaned_data['title'], + first_name=self.cleaned_data['first_name'], + last_name=self.cleaned_data['last_name'], + discipline=self.cleaned_data['discipline'], + orcid_id=self.cleaned_data['orcid_id'], + webpage=self.cleaned_data['personalwebpage']) + # Add a ProfileEmail to this Profile + profile_email = ProfileEmail.objects.get_or_create( + profile=profile, email=self.cleaned_data['email']) + profile.emails.update(primary=False) + profile.emails.filter(id=profile_email.id).update(primary=True, still_valid=True) + # Create an Affiliation for this Profile + current_affiliation = self.cleaned_data.get('current_affiliation', None) + if current_affiliation: + Affiliation.objects.create( + profile=profile, + organization=self.cleaned_data['current_affiliation']) + # Create the Contributor object contributor, __ = Contributor.objects.get_or_create(**{ + 'profile': profile, 'user': user, 'invitation_key': self.cleaned_data.get('invitation_key', ''), 'title': self.cleaned_data['title'], @@ -168,10 +209,6 @@ class RegistrationForm(forms.Form): 'personalwebpage': self.cleaned_data['personalwebpage'], 'accepts_SciPost_emails': self.cleaned_data['subscribe'], }) - affiliation, __ = Affiliation.objects.get_or_create( - contributor=contributor, - institution=institution, - ) contributor.save() return contributor @@ -297,30 +334,58 @@ class SciPostAuthenticationForm(AuthenticationForm): Inherits from django.contrib.auth.forms:AuthenticationForm. Extra fields: - - next: url for the next page, obtainable via POST + * next: url for the next page, obtainable via POST Overriden methods: - - confirm_login_allowed: disallow inactive or unvetted accounts. + * clean: allow either username, or email as substitute for username + * confirm_login_allowed: disallow inactive or unvetted accounts. """ next = forms.CharField(widget=forms.HiddenInput(), required=False) code = forms.CharField( required=False, widget=forms.TextInput(attrs={'autocomplete': 'off'}), help_text="Please type in the code displayed on your authenticator app from your device") + def clean(self): + """Allow either username, or email as substitute for username.""" + username = self.cleaned_data.get('username') + password = self.cleaned_data.get('password') + + if username is not None and password: + self.user_cache = authenticate(self.request, username=username, password=password) + if self.user_cache is None: + try: + _user = User.objects.get(email=username) + self.user_cache = authenticate( + self.request, username=_user.username, password=password) + except: + pass + if self.user_cache is None: + raise forms.ValidationError( + ("Please enter a correct %(username)s and password. " + "Note that both fields may be case-sensitive. " + "Your can use your email instead of your username."), + code='invalid_login', + params={'username': self.username_field.verbose_name}, + ) + else: + self.confirm_login_allowed(self.user_cache) + + return self.cleaned_data + def confirm_login_allowed(self, user): if not user.is_active: raise forms.ValidationError( ('Your account is not yet activated. ' - 'Please first activate your account by clicking on the ' - 'activation link we emailed you.'), + 'Please first activate your account by clicking on the ' + 'activation link we emailed you.'), code='inactive', - ) + ) if not user.groups.exists(): raise forms.ValidationError( ('Your account has not yet been vetted.\n' - 'Our admins will verify your credentials very soon, ' - 'and if vetted (your will receive an information email) ' - 'you will then be able to login.'), + 'Our admins will verify your credentials very soon, ' + 'and if vetted (your will receive an information email) ' + 'you will then be able to login.'), code='unvetted', ) if user.devices.exists(): @@ -467,7 +532,7 @@ class ContributorMergeForm(forms.Form): contrib_from_qs.update(duplicate_of=contrib_into) # Step 2: update all ForeignKey relations - Affiliation.objects.filter(contributor=contrib_from).update(contributor=contrib_into) + deprec_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) diff --git a/scipost/managers.py b/scipost/managers.py index 8fe31e821eacc7ed6574fad3811bafdfbdb38ba6..d3844a2a288b370c6f1498f6ff6518f1c02ff479 100644 --- a/scipost/managers.py +++ b/scipost/managers.py @@ -57,6 +57,30 @@ class ContributorQuerySet(models.QuerySet): """TODO: NEEDS UPDATE TO NEW FELLOWSHIP RELATIONS.""" return self.filter(fellowships__isnull=False).distinct() + def specialties_overlap(self, discipline, expertises=[]): + """ + Returns all Contributors specialized in the given discipline + and any of the (optional) expertises. + + This method is also separately implemented for Profile and Fellowship objects. + """ + qs = self.filter(profile__discipline=discipline) + if expertises and len(expertises) > 0: + qs = qs.filter(profile__expertises__overlap=expertises) + return qs + + def specialties_contain(self, discipline, expertises=[]): + """ + Returns all Contributors specialized in the given discipline + and all of the (optional) expertises. + + This method is also separately implemented for Profile and Fellowship objects. + """ + qs = self.filter(profile__discipline=discipline) + if expertises and len(expertises) > 0: + qs = qs.filter(profile__expertises__contains=expertises) + return qs + def with_duplicate_names(self): """ Returns only potential duplicate Contributors (as identified by first and diff --git a/scipost/templates/partials/scipost/personal_page/account.html b/scipost/templates/partials/scipost/personal_page/account.html index 39af7836ac87e1909d63e586b37bc88a92218460..a5d6d423a459c7b4c4e7e9701904f588ad0d2512 100644 --- a/scipost/templates/partials/scipost/personal_page/account.html +++ b/scipost/templates/partials/scipost/personal_page/account.html @@ -57,7 +57,7 @@ {% if recommend_totp %} <div class="border border-danger p-2 mb-3"> - <h3 class="text-warningx"> + <h3> <i class="fa fa-exclamation-triangle text-danger"></i> Please increase your account's security</h3> <div> @@ -162,6 +162,16 @@ </div> </div> +<div class="row"> + <div class="col-12"> + <h3>Your Affiliations:</h3> + <ul> + <li><a href="{% url 'profiles:affiliation_create' profile_id=contributor.profile.id %}">Add a new Affiliation</a></li> + </ul> + {% include 'profiles/_affiliations_table.html' with profile=contributor.profile actions=True %} + </div> +</div> + {% if unavailability_form %} <hr> <div class="row"> diff --git a/scipost/templates/scipost/_private_info_as_table.html b/scipost/templates/scipost/_private_info_as_table.html index 5e2450b5f83d399961cc4a7f0714c1ecfa3be515..ea9cdd7b69cbb1e4c2301dca6d268d1d8ed23a6f 100644 --- a/scipost/templates/scipost/_private_info_as_table.html +++ b/scipost/templates/scipost/_private_info_as_table.html @@ -5,15 +5,10 @@ <tr><td>Email: </td><td>{{ contributor.user.email }}</td></tr> <tr><td>ORCID id: </td><td>{{ contributor.orcid_id }}</td></tr> <tr> - <td>Affiliation(s):</td> - <td> - {% for affiliation in contributor.affiliations.active %} - {% if not forloop.first %} - <br> - {% endif %} - {{ affiliation.institution }} - {% endfor %} - </td> + <td>Affiliation(s):</td> + <td> + {% include 'profiles/_affiliations_table.html' with contributor=contributor actions=False %} + </td> </tr> <tr><td>Address: </td><td>{{ contributor.address }}</td></tr> <tr><td>Personal web page: </td><td>{{ contributor.personalwebpage }}</td></tr> diff --git a/scipost/templates/scipost/_public_info_as_table.html b/scipost/templates/scipost/_public_info_as_table.html index a3b73a41a3193ec35862d22f53d11d96c0e412dc..af335e2362dea207721c17f08091e9e8b2894972 100644 --- a/scipost/templates/scipost/_public_info_as_table.html +++ b/scipost/templates/scipost/_public_info_as_table.html @@ -3,17 +3,6 @@ <tr><td>First name: </td><td>{{ contributor.user.first_name }}</td></tr> <tr><td>Last name: </td><td>{{ contributor.user.last_name }}</td></tr> <tr><td>ORCID id: </td><td>{{ contributor.orcid_id|default:'-' }}</td></tr> - <tr> - <td>Affiliation(s):</td> - <td> - {% for affiliation in contributor.affiliations.active %} - {% if not forloop.first %} - <br> - {% endif %} - {{ affiliation.institution }} - {% endfor %} - </td> - </tr> <tr><td>Personal web page: </td><td>{{ contributor.personalwebpage|default:'-' }}</td></tr> {% if perms.scipost.can_vet_registration_requests %} diff --git a/scipost/templates/scipost/navbar.html b/scipost/templates/scipost/navbar.html index aa54892400d8988b7bb2ee49fc929a9b4c512250..aec258392e028312046eae70fc67cd0c3492d0ff 100644 --- a/scipost/templates/scipost/navbar.html +++ b/scipost/templates/scipost/navbar.html @@ -64,6 +64,8 @@ </div> <div class="col-md-6"> <div class="dropdown-item"><a href="{% url 'submissions:submit_manuscript' %}">Submit a manuscript</a> <i class="fa fa-angle-right" aria-hidden="true"></i></div> + <div class="dropdown-item"><a href="{% url 'submissions:author_guidelines' %}">Author guidelines</a> <i class="fa fa-angle-right" aria-hidden="true"></i></div> + <div class="dropdown-item"><a href="{% url 'submissions:referee_guidelines' %}">Referee guidelines</a> <i class="fa fa-angle-right" aria-hidden="true"></i></div> </div> </div> <div class="dropdown-divider"></div> diff --git a/scipost/templates/scipost/register.html b/scipost/templates/scipost/register.html index 1a77c273be8194416e99a555dd65adeaba6229ff..50dd299da035e04838313ca5a7350fcc81bf9d75 100644 --- a/scipost/templates/scipost/register.html +++ b/scipost/templates/scipost/register.html @@ -44,3 +44,8 @@ {% endblock content %} + +{% block footer_script %} + {{ block.super }} + {{ form.media }} +{% endblock footer_script %} diff --git a/scipost/views.py b/scipost/views.py index 72400cedb28c51630ae0d15187b9a4502b7ea7e0..c1c0f843c9d054d34b75b6b88b5c1506f35fe73d 100644 --- a/scipost/views.py +++ b/scipost/views.py @@ -402,12 +402,12 @@ class SciPostLoginView(LoginView): Inherits from django.contrib.auth.views:LoginView. Overriden fields: - - template_name - - authentication_form + * template_name + * authentication_form Overriden methods: - - get initial: allow prefilling with GET data, for 'next' - - get redirect url + * get initial: allow prefilling with GET data, for 'next' + * get redirect url """ template_name = 'scipost/login.html' @@ -466,10 +466,10 @@ class SciPostPasswordChangeView(PasswordChangeView): Inherits from django.contrib.auth.views:PasswordChangeView. Overriden fields: - - template_name + * template_name Overriden methods: - - get_success_url + * get_success_url """ template_name = 'scipost/password_change.html' @@ -491,12 +491,12 @@ class SciPostPasswordResetView(PasswordResetView): Derived from django.contrib.auth.views:PasswordResetView. Overriden fields: - - template_name - - email_template_name - - subject_template_name + * template_name + * email_template_name + * subject_template_name Overriden methods: - - get_success_url + * get_success_url """ template_name = 'scipost/password_reset.html' @@ -520,10 +520,10 @@ class SciPostPasswordResetConfirmView(PasswordResetConfirmView): Derived from django.contrib.auth.views:PasswordResetConfirmView. Overriden fields: - - template_name + * template_name Overriden methods: - - get_success_url + * get_success_url """ template_name = 'scipost/password_reset_confirm.html' diff --git a/submissions/admin.py b/submissions/admin.py index 3d27be9bce38695553eadfe5f0e549f27b2eaf2a..ddf6b838a5e32fa1101a8ea86780d8625bca0841 100644 --- a/submissions/admin.py +++ b/submissions/admin.py @@ -48,7 +48,7 @@ class SubmissionAdminForm(forms.ModelForm): class SubmissionAdmin(GuardedModelAdmin): date_hierarchy = 'submission_date' form = SubmissionAdminForm - list_display = ('title', 'author_list', 'submitted_to', + list_display = ('title', 'author_list', 'preprint', 'submitted_to', 'status', 'visible_public', 'visible_pool', 'refereeing_cycle', 'submission_date', 'publication') list_filter = ('status', 'discipline', 'submission_type', 'submitted_to') diff --git a/submissions/management/commands/email_fellows_tasklist.py b/submissions/management/commands/email_fellows_tasklist.py index 738473672d0b4a7b3ddad6426d6556ed4bd04f74..7425635d5187a336d97fa9832f2b8cd2261f6e10 100644 --- a/submissions/management/commands/email_fellows_tasklist.py +++ b/submissions/management/commands/email_fellows_tasklist.py @@ -28,13 +28,16 @@ class Command(BaseCommand): assignments_upcoming_deadline = assignments_ongoing.refereeing_deadline_within(days=7) if recs_to_vote_on or assignments_ongoing or assignments_to_consider or assignments_upcoming_deadline: mail_sender = DirectMailUtil( - 'fellows/email_fellow_tasklist', delayed_processing=False, # Render immediately, because m2m/querysets cannot be saved for later rendering. + 'fellows/email_fellow_tasklist', + # Render immediately, because m2m/querysets cannot be saved for later rendering: + delayed_processing=False, + object=fellow, fellow=fellow, nr_potfels_to_vote_on=nr_potfels_to_vote_on, recs_to_vote_on=recs_to_vote_on, assignments_ongoing=assignments_ongoing, assignments_to_consider=assignments_to_consider, assignments_upcoming_deadline=assignments_upcoming_deadline) - mail_sender.send() + mail_sender.send_mail() count += 1 self.stdout.write(self.style.SUCCESS('Emailed {} fellows.'.format(count))) diff --git a/submissions/refereeing_cycles.py b/submissions/refereeing_cycles.py index 21e264835c5d845773a31cfaeda67110c2e7a142..d89ee510bc42789af338f52e958c857459d29d53 100644 --- a/submissions/refereeing_cycles.py +++ b/submissions/refereeing_cycles.py @@ -239,7 +239,7 @@ class BaseCycle: self.add_action(action) # Submission is a resubmission: EIC has to determine which cycle to proceed with. - comments_to_vet = self._submission.comments.awaiting_vetting().values_list('id') + comments_to_vet = self._submission.comments.awaiting_vetting() for comment in comments_to_vet: self.add_action(VettingAction(comment)) diff --git a/submissions/templates/submissions/_guidelines_dl.html b/submissions/templates/submissions/_guidelines_dl.html new file mode 100644 index 0000000000000000000000000000000000000000..237f8856a230b01905fd2058e79d2bde1ba63a13 --- /dev/null +++ b/submissions/templates/submissions/_guidelines_dl.html @@ -0,0 +1,41 @@ +<div class="card"> + <div class="card-header"> + Useful further links + </div> + <div class="card-body pb-0"> + <div class="small"> + <dl class="row"> + <dt class="col-sm-6 col-md-5">Page</dt> + <dd class="col-sm-6 col-md-7"><em>... where you will find info on:</em></dd> + <dt class="col-sm-6 col-md-5"> + {% if 'by-laws' in request.path %}<i class="fa fa-arrow-circle-right text-success"></i>{% endif %} + <a href="{% url 'scipost:EdCol_by-laws' %}">Editorial College by-laws</a></dt> + <dd class="col-sm-6 col-md-7"><em>Official rules for all our editorial workflows</em></dd> + <dt class="col-sm-6 col-md-5"> + {% if 'journals_terms_and_conditions' in request.path %}<i class="fa fa-arrow-circle-right text-success"></i>{% endif %} + <a href="{% url 'journals:journals_terms_and_conditions' %}">Journals Terms and Conditions</a></dt> + <dd class="col-sm-6 col-md-7"><em>Expectations, Open Access policy, license and copyright, author obligations, referee code of conduct, corrections and retractions</em></dd> + <dt class="col-sm-6 col-md-5"> + {% if '/terms_and_conditions' in request.path %}<i class="fa fa-arrow-circle-right text-success"></i>{% endif %} + <a href="{% url 'scipost:terms_and_conditions' %}">SciPost Terms and Conditions</a></dt> + <dd class="col-sm-6 col-md-7"><em>General terms and conditions pertaining to ownership, license to use, contributions, impermissible uses, etc.</em></dd> + <dt class="col-sm-6 col-md-5"> + {% if 'author_guidelines' in request.path %}<i class="fa fa-arrow-circle-right text-success"></i>{% endif %} + <a href="{% url 'submissions:author_guidelines' %}">Author guidelines</a> + </dt> + <dd class="col-sm-6 col-md-7"><em>A simple guide on how to proceed as an author</em> + </dd> + <dt class="col-sm-6 col-md-5"> + {% if 'sub_and_ref_procedure' in request.path %}<i class="fa fa-arrow-circle-right text-success"></i>{% endif %} + <a href="{% url 'submissions:sub_and_ref_procedure' %}">Submission and refereeing procedure</a> + </dt> + <dd class="col-sm-6 col-md-7"><em>More details about submission procedure and refereeing protocols</em> + </dd> + <dt class="col-sm-6 col-md-5"> + {% if 'referee_guidelines' in request.path %}<i class="fa fa-arrow-circle-right text-success"></i>{% endif %} + <a href="{% url 'submissions:referee_guidelines' %}">Refereeing guidelines</a></dt> + <dd class="col-sm-6 col-md-7"><em>A simple guide on how (if you are a referee) to act professionally, and (if you are an author) react constructively</em></dd> + </dl> + </div> + </div> +</div> diff --git a/submissions/templates/submissions/author_guidelines.html b/submissions/templates/submissions/author_guidelines.html index 2c452d8b28bef015dfcd382cf901080b410c4e09..10fa8b389a8bbc768d58cd592f9d149277e824d2 100644 --- a/submissions/templates/submissions/author_guidelines.html +++ b/submissions/templates/submissions/author_guidelines.html @@ -18,30 +18,21 @@ </div> <div class="row"> - <div class="col-lg-6"> - -<h4>On this page:</h4> -<ul> - <li><a href="#manuprep">Manuscript preparation</a></li> - <li><a href="#manusub">Manuscript submission</a></li> - <li><a href="#refphase">The refereeing phase</a></li> - <li><a href="#resub">Resubmission</a></li> - <li><a href="#postacceptance">Post-acceptance</a></li> - <li><a href="#FAQ">Frequently asked questions</a></li> -</ul> - + <div class="col-lg-5"> + <h4>The following is a quick summary guide to authoring at SciPost.</h4> + <p>It is not meant to replace our official rules and other helpful pages, which you will find in the Useful links box next to this menu.</p> + <h4>On this page:</h4> + <ul> + <li><a href="#manuprep">Manuscript preparation</a></li> + <li><a href="#manusub">Manuscript submission</a></li> + <li><a href="#refphase">The refereeing phase</a></li> + <li><a href="#resub">Resubmission</a></li> + <li><a href="#postacceptance">Post-acceptance</a></li> + <li><a href="#FAQ">Frequently asked questions</a></li> + </ul> </div> - <div class="col-lg-6"> - <p> - The following is a general guide to authoring at SciPost. - It does not replace - our Journals' <a href="{% url 'journals:journals_terms_and_conditions' %}">Terms and Conditions</a> - (which extend the general <a href="{% url 'scipost:terms_and_conditions' %}">SciPost Terms and Conditions</a>), and <a href="{% url 'scipost:EdCol_by-laws' %}">Editorial College by-laws</a> which remain in force as official rules. - </p> - <p> - Before submitting to SciPost, you should familiarize yourself with the - <a href="{% url 'journals:journals_terms_and_conditions' %}">SciPost Journals Terms and Conditions</a>. - </p> + <div class="col-lg-7"> + {% include 'submissions/_guidelines_dl.html' %} </div> </div> diff --git a/submissions/templates/submissions/referee_guidelines.html b/submissions/templates/submissions/referee_guidelines.html index 712cd750cc112a5c35aea38e27c3d27d5839a469..1b918c46a879ecdbf6c0e61dcd0e4af2cddb0709 100644 --- a/submissions/templates/submissions/referee_guidelines.html +++ b/submissions/templates/submissions/referee_guidelines.html @@ -16,49 +16,48 @@ <h2 class="highlight-x">A guide for referees and authors</h2> <div class="row"> - <div class="col-lg-6"> + <div class="col-lg-5"> + <p> + The following is a general guide to refereeing at SciPost. + Slightly informal in style, it is meant to introduce you to how our peer review system works. + </p> + <p>It is not meant to replace our official rules and other helpful pages, which you will find in the Useful links box next to this menu.</p> -<h4>On this page:</h4> -<ul> - <li><a href="#forReferees">For Referees</a> - <ul> - <li><a href="#refereeInvited">Have you been invited to write a Report?</a></li> - <li><a href="#refereeContributor">Do you wish to contribute a Report?</a></li> - <li><a href="#refereeWhatIsAsked">What do we ask you in our Report form?</a></li> - <li><a href="#refereeBeforeWritingReport">Before you write your Report</a></li> - <li><a href="#refereeWritingReport">Writing your Report</a></li> - <li><a href="#refereeGoodReport">The characteristics of a good Report</a></li> - <li><a href="#refereeFAQ">FAQ</a></li> - </ul> - </li> + <p> + You should already be somewhat familiar with our <a href="{% url 'submissions:sub_and_ref_procedure' %}">submission and refereeing procedure</a>. + </p> + <p> + Referees should in particular be familiar with + our <a href="{% url 'journals:journals_terms_and_conditions' %}#referee_code_of_conduct">referee code of conduct</a>. + Authors should in particular be familiar with + our <a href="{% url 'journals:journals_terms_and_conditions' %}#author_obligations">author obligations</a>. + </p> - <li><a href="#forAuthors">For Authors</a> + <h4>On this page:</h4> <ul> - <li><a href="#authorsReact">How to react to a Report?</a></li> - </ul> - </li> -</ul> + <li><a href="#forReferees">For Referees</a> + <ul> + <li><a href="#refereeInvited">Have you been invited to write a Report?</a></li> + <li><a href="#refereeContributor">Do you wish to contribute a Report?</a></li> + <li><a href="#refereeWhatIsAsked">What do we ask you in our Report form?</a></li> + <li><a href="#refereeBeforeWritingReport">Before you write your Report</a></li> + <li><a href="#refereeWritingReport">Writing your Report</a></li> + <li><a href="#refereeGoodReport">The characteristics of a good Report</a></li> + <li><a href="#refereeFAQ">FAQ</a></li> + </ul> + </li> - </div> - <div class="col-lg-6"> -<p> - The following is a general guide to refereeing at SciPost. - Slightly informal in style, it is meant to introduce you to how our peer review system works. - It does not replace - our Journals' <a href="{% url 'journals:journals_terms_and_conditions' %}">Terms and Conditions</a> - (which extend the general <a href="{% url 'scipost:terms_and_conditions' %}">SciPost Terms and Conditions</a>), and <a href="{% url 'scipost:EdCol_by-laws' %}">Editorial College by-laws</a> which remain in force as official rules. -</p> -<p> - You should already be somewhat familiar with our <a href="{% url 'submissions:sub_and_ref_procedure' %}">submission and refereeing procedure</a>. -</p> -<p> - Referees should in particular be familiar with - our <a href="{% url 'journals:journals_terms_and_conditions' %}#referee_code_of_conduct">referee code of conduct</a>. - Authors should in particular be familiar with - our <a href="{% url 'journals:journals_terms_and_conditions' %}#author_obligations">author obligations</a>. -</p> + <li><a href="#forAuthors">For Authors</a> + <ul> + <li><a href="#authorsReact">How to react to a Report?</a></li> + </ul> + </li> + </ul> </div> + <div class="col-lg-7"> + {% include 'submissions/_guidelines_dl.html' %} + </div> </div> <hr> diff --git a/submissions/templates/submissions/sub_and_ref_procedure.html b/submissions/templates/submissions/sub_and_ref_procedure.html index a57a79d4ae5cd9a842eb1de9d5e73f94e86ce2b4..a9e67fdf4b734500a86c6fb224c22c148c407896 100644 --- a/submissions/templates/submissions/sub_and_ref_procedure.html +++ b/submissions/templates/submissions/sub_and_ref_procedure.html @@ -15,6 +15,12 @@ </div> </div> +<div class="row"> + <div class="col-lg-7"> + {% include 'submissions/_guidelines_dl.html' %} + </div> +</div> + {% if perms.scipost.can_attend_VGMs %} <div class="container border border-danger"> <span class="text-danger">DRAFT (POOLVIEW ONLY) - <a href="mailto:admin@scipost.org">COMMENTS WELCOME</a></span> diff --git a/templates/email/fellows/email_fellow_tasklist.html b/templates/email/fellows/email_fellow_tasklist.html index adb315f44f39f6a1142e3077195e38aae1e82031..6b10719dd889956f88f51c02d569868af74e86a0 100644 --- a/templates/email/fellows/email_fellow_tasklist.html +++ b/templates/email/fellows/email_fellow_tasklist.html @@ -45,12 +45,14 @@ {% if assignment.submission.cycle.has_required_actions %} <h3>Required actions (go to the <a href="https://scipost.org{% url 'submissions:editorial_page' assignment.submission.preprint.identifier_w_vn_nr %}">Editorial page</a> to carry them out):</h3> <ul> - {% for action in assignment.submission.cycle.get_required_actions %} - <li>{{action.1}}</li> + {% for code, action in assignment.submission.cycle.required_actions.items %} + <li>{{ action|safe }}</li> {% empty %} <li>No action required. Great job!</li> {% endfor %} </ul> + {% else %} + <p>No action required. Great job!</p> {% endif %} </li> {% endfor %} diff --git a/virtualmeetings/management/commands/transfer_old_motion_to_new.py b/virtualmeetings/management/commands/transfer_old_motion_to_new.py new file mode 100644 index 0000000000000000000000000000000000000000..e65b27169a01876ae670e9a195cd2fa5be3698ea --- /dev/null +++ b/virtualmeetings/management/commands/transfer_old_motion_to_new.py @@ -0,0 +1,40 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django.core.management.base import BaseCommand +from django.contrib.contenttypes.models import ContentType + +from virtualmeetings.models import Motion as deprec_Motion +from forums.models import Post, Motion + +class Command(BaseCommand): + help = ('Temporary method to transfer old virtualmeetings.Motions ' + 'to new forums.Motion ones.') + + def add_arguments(self, parser): + parser.add_argument( + '--old_pk', action='store', default=0, type=int, dest='old_pk', help='Old Motion id') + parser.add_argument( + '--new_pk', action='store', default=0, type=int, dest='new_pk', help='New Motion id') + + def handle(self, *args, **kwargs): + old_motion = deprec_Motion.objects.get(pk=kwargs['old_pk']) + new_motion = Motion.objects.get(pk=kwargs['new_pk']) + # Transfer the votes + for voter in old_motion.in_agreement.all(): + new_motion.in_agreement.add(voter.user) + for voter in old_motion.in_notsure.all(): + new_motion.in_doubt.add(voter.user) + for voter in old_motion.in_disagreement.all(): + new_motion.in_disagreement.add(voter.user) + # Transfer the old remarks to Post objects + type_motion = ContentType.objects.get_by_natural_key('forums', 'post') + for remark in old_motion.remarks.all(): + Post.objects.get_or_create( + posted_by=remark.contributor.user, + posted_on=remark.date, + parent_content_type=type_motion, + parent_object_id=new_motion.id, + subject='Remark', + text=remark.remark)