From a97962c0d078e6c2f9e08263428fa2fde59346b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Caux?= <git@jscaux.org> Date: Wed, 18 Jan 2023 05:58:13 +0100 Subject: [PATCH] Add Qualification and appraisal --- scipost_django/submissions/admin.py | 12 +++ scipost_django/submissions/forms/__init__.py | 5 +- scipost_django/submissions/forms/appraisal.py | 37 ++++++++++ .../submissions/managers/__init__.py | 2 + .../submissions/managers/qualification.py | 25 +++++++ .../migrations/0133_auto_20230116_1943.py | 34 +++++++++ ...me_status_qualification_expertise_level.py | 18 +++++ scipost_django/submissions/models/__init__.py | 2 + .../submissions/models/qualification.py | 73 +++++++++++++++++++ .../submissions/pool/_hx_appraisal.html | 28 +++++++ .../pool/_hx_qualification_form.html | 9 +++ .../submissions/pool/_hx_submission_tab.html | 24 ++++++ .../_submission_details_summary_contents.html | 27 ++++--- .../pool/_submission_tab_link.html | 5 ++ .../templatetags/submissions_pool.py | 13 +++- scipost_django/submissions/urls/pool.py | 17 ++++- scipost_django/submissions/views/appraisal.py | 65 +++++++++++++++++ 17 files changed, 383 insertions(+), 13 deletions(-) create mode 100644 scipost_django/submissions/forms/appraisal.py create mode 100644 scipost_django/submissions/managers/qualification.py create mode 100644 scipost_django/submissions/migrations/0133_auto_20230116_1943.py create mode 100644 scipost_django/submissions/migrations/0134_rename_status_qualification_expertise_level.py create mode 100644 scipost_django/submissions/models/qualification.py create mode 100644 scipost_django/submissions/templates/submissions/pool/_hx_appraisal.html create mode 100644 scipost_django/submissions/templates/submissions/pool/_hx_qualification_form.html create mode 100644 scipost_django/submissions/templates/submissions/pool/_submission_tab_link.html create mode 100644 scipost_django/submissions/views/appraisal.py diff --git a/scipost_django/submissions/admin.py b/scipost_django/submissions/admin.py index c7b83302b..e7c927af9 100644 --- a/scipost_django/submissions/admin.py +++ b/scipost_django/submissions/admin.py @@ -22,6 +22,7 @@ from submissions.models import ( iThenticateReport, InternalPlagiarismAssessment, iThenticatePlagiarismAssessment, + Qualification, PreprintServer, ) from scipost.models import Contributor @@ -62,6 +63,16 @@ class iThenticatePlagiarismAssessmentInline(admin.StackedInline): model = iThenticatePlagiarismAssessment +class QualificationInline(admin.StackedInline): + model = Qualification + extra = 0 + min_num = 0 + autocomplete_fields = [ + "submission", + "fellow", + ] + + class SubmissionTieringInline(admin.StackedInline): model = SubmissionTiering extra = 0 @@ -112,6 +123,7 @@ class SubmissionAdmin(GuardedModelAdmin): inlines = [ InternalPlagiarismAssessmentInline, iThenticatePlagiarismAssessmentInline, + QualificationInline, SubmissionTieringInline, ] diff --git a/scipost_django/submissions/forms/__init__.py b/scipost_django/submissions/forms/__init__.py index fde48f721..81d3b7409 100644 --- a/scipost_django/submissions/forms/__init__.py +++ b/scipost_django/submissions/forms/__init__.py @@ -2,6 +2,9 @@ __copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" __license__ = "AGPL v3" +from .appraisal import QualificationForm + + import datetime from django import forms @@ -12,7 +15,7 @@ from django.forms.formsets import ORDERING_FIELD_NAME from django.utils import timezone from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, Div, Field, Fieldset, ButtonHolder, Submit +from crispy_forms.layout import Layout, Div, Field, ButtonHolder, Submit from crispy_forms.bootstrap import InlineRadios from crispy_bootstrap5.bootstrap5 import FloatingField diff --git a/scipost_django/submissions/forms/appraisal.py b/scipost_django/submissions/forms/appraisal.py new file mode 100644 index 000000000..410af05bd --- /dev/null +++ b/scipost_django/submissions/forms/appraisal.py @@ -0,0 +1,37 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django import forms + +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, Div, Field, ButtonHolder, Submit +from crispy_bootstrap5.bootstrap5 import FloatingField + +from ..models import Qualification + + +class QualificationForm(forms.ModelForm): + + class Meta: + model = Qualification + fields = [ + "submission", + "fellow", + "expertise_level", + # "comments", + ] + widgets = { + "submission": forms.HiddenInput(), + "fellow": forms.HiddenInput(), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.fields["expertise_level"].label = "Your expertise level for this Submission" + self.helper.layout = Layout( + Field("submission"), + Field("fellow"), + FloatingField("expertise_level"), + ) diff --git a/scipost_django/submissions/managers/__init__.py b/scipost_django/submissions/managers/__init__.py index 927735b89..67b1ce5ec 100644 --- a/scipost_django/submissions/managers/__init__.py +++ b/scipost_django/submissions/managers/__init__.py @@ -8,6 +8,8 @@ from .communication import EditorialCommunicationQuerySet from .decision import EditorialDecisionQuerySet +from .qualification import QualificationQuerySet + from .recommendation import EICRecommendationQuerySet from .referee_invitation import RefereeInvitationQuerySet diff --git a/scipost_django/submissions/managers/qualification.py b/scipost_django/submissions/managers/qualification.py new file mode 100644 index 000000000..dab50c99c --- /dev/null +++ b/scipost_django/submissions/managers/qualification.py @@ -0,0 +1,25 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django.db import models + + +class QualificationQuerySet(models.QuerySet): + + def qualified(self): + """ + Filter for Fellows which are at least marginally qualified. + """ + return self.filter(status__in=[ + self.model.STATUS_EXPERT, + self.model.STATUS_VERY_KNOWLEDGEABLE, + self.model.STATUS_KNOWLEDGEABLE, + self.model.STATUS_MARGINALLY_QUALIFIED, + ]) + + def not_qualified(self): + return self.filter(status__in=[ + self.model.STATUS_NOT_REALLY_QUALIFIED, + self.model.STATUS_NOT_AT_ALL_QUALIFIED, + ]) diff --git a/scipost_django/submissions/migrations/0133_auto_20230116_1943.py b/scipost_django/submissions/migrations/0133_auto_20230116_1943.py new file mode 100644 index 000000000..826dae197 --- /dev/null +++ b/scipost_django/submissions/migrations/0133_auto_20230116_1943.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.16 on 2023-01-16 18:43 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('colleges', '0039_nomination_add_events'), + ('submissions', '0132_auto_20221215_2034'), + ] + + operations = [ + migrations.CreateModel( + name='Qualification', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('expert', 'Expert in this subject'), ('very_knowledgeable', 'Very knowledgeable in this subject'), ('knowledgeable', 'Knowledgeable in this subject'), ('marginally_qualified', 'Marginally qualified'), ('not_really_qualified', 'Not really qualified'), ('not_at_all_qualified', 'Not at all qualified')], max_length=32)), + ('comments', models.TextField(blank=True)), + ('datetime', models.DateTimeField(default=django.utils.timezone.now)), + ('fellow', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='colleges.fellowship')), + ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='submissions.submission')), + ], + options={ + 'ordering': ['submission', 'fellow'], + }, + ), + migrations.AddConstraint( + model_name='qualification', + constraint=models.UniqueConstraint(fields=('submission', 'fellow'), name='unique_together_submission_fellow'), + ), + ] diff --git a/scipost_django/submissions/migrations/0134_rename_status_qualification_expertise_level.py b/scipost_django/submissions/migrations/0134_rename_status_qualification_expertise_level.py new file mode 100644 index 000000000..68d3c8026 --- /dev/null +++ b/scipost_django/submissions/migrations/0134_rename_status_qualification_expertise_level.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2023-01-17 08:09 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0133_auto_20230116_1943'), + ] + + operations = [ + migrations.RenameField( + model_name='qualification', + old_name='status', + new_name='expertise_level', + ), + ] diff --git a/scipost_django/submissions/models/__init__.py b/scipost_django/submissions/models/__init__.py index 5bd066ac4..0add63fac 100644 --- a/scipost_django/submissions/models/__init__.py +++ b/scipost_django/submissions/models/__init__.py @@ -18,6 +18,8 @@ from .communication import EditorialCommunication from .preprint_server import PreprintServer +from .qualification import Qualification + from .referee_invitation import RefereeInvitation from .report import Report diff --git a/scipost_django/submissions/models/qualification.py b/scipost_django/submissions/models/qualification.py new file mode 100644 index 000000000..8a7ef753e --- /dev/null +++ b/scipost_django/submissions/models/qualification.py @@ -0,0 +1,73 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django.db import models +from django.utils import timezone + +from ..managers import QualificationQuerySet + + +class Qualification(models.Model): + """ + Specification of a Fellow's qualification for handlind a Submission. + """ + + EXPERT = "expert" + VERY_KNOWLEDGEABLE = "very_knowledgeable" + KNOWLEDGEABLE = "knowledgeable" + MARGINALLY_QUALIFIED = "marginally_qualified" + NOT_REALLY_QUALIFIED = "not_really_qualified" + NOT_AT_ALL_QUALIFIED = "not_at_all_qualified" + EXPERTISE_LEVEL_CHOICES = ( + (EXPERT, "Expert in this subject"), + (VERY_KNOWLEDGEABLE, "Very knowledgeable in this subject"), + (KNOWLEDGEABLE, "Knowledgeable in this subject"), + (MARGINALLY_QUALIFIED, "Marginally qualified"), + (NOT_REALLY_QUALIFIED, "Not really qualified"), + (NOT_AT_ALL_QUALIFIED, "Not at all qualified"), + ) + + submission = models.ForeignKey( + "submissions.Submission", + on_delete=models.CASCADE, + ) + + fellow = models.ForeignKey( + "colleges.Fellowship", + on_delete=models.CASCADE, + ) + + expertise_level = models.CharField( + max_length=32, + choices=EXPERTISE_LEVEL_CHOICES, + ) + + comments = models.TextField(blank=True) + + datetime = models.DateTimeField(default=timezone.now) + + objects = QualificationQuerySet.as_manager() + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["submission", "fellow"], + name="unique_together_submission_fellow", + ), + ] + ordering =["submission", "fellow"] + + + def __str__(self): + return (f"{self.fellow}: {self.get_expertise_level_display()} " + f"(for {self.submission})") + + @property + def is_qualified(self): + return self.expertise_level in [ + self.EXPERT, + self.VERY_KNOWLEDGEABLE, + self.KNOWLEDGEABLE, + self.MARGINALLY_QUALIFIED, + ] diff --git a/scipost_django/submissions/templates/submissions/pool/_hx_appraisal.html b/scipost_django/submissions/templates/submissions/pool/_hx_appraisal.html new file mode 100644 index 000000000..58902f996 --- /dev/null +++ b/scipost_django/submissions/templates/submissions/pool/_hx_appraisal.html @@ -0,0 +1,28 @@ +{% load submissions_pool %} + +{% get_fellow_qualification submission session_fellowship as qualification %} + +<div class="row"> + <div id="submission-{{ submission.id }}-qualification-form" + class="col-lg-6" + hx-get="{% url 'submissions:pool:_hx_qualification_form' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr %}" + hx-trigger="revealed" + > + </div> + {% if qualification and qualification.is_qualified %} + <div class="col-lg-6"> + <div> + <a class="btn btn-sm btn-success text-white" href="{% url 'submissions:pool:editorial_assignment' submission.preprint.identifier_w_vn_nr %}">I will take charge of this Submission</a> + </div> + <div> + <a class="btn btn-sm btn-danger text-white" href=""> + I have a conflict of interest + </a> + </div> + <div> + readiness form + </div> + </div> + + {% endif %} +</div> diff --git a/scipost_django/submissions/templates/submissions/pool/_hx_qualification_form.html b/scipost_django/submissions/templates/submissions/pool/_hx_qualification_form.html new file mode 100644 index 000000000..dd3dc7d57 --- /dev/null +++ b/scipost_django/submissions/templates/submissions/pool/_hx_qualification_form.html @@ -0,0 +1,9 @@ +{% load crispy_forms_tags %} + +<form + hx-post="{% url 'submissions:pool:_hx_qualification_form' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr %}" + hx-target="#submission-{{ submission.id }}-qualification-form" + hx-trigger="change" +> + {% crispy form %} +</form> diff --git a/scipost_django/submissions/templates/submissions/pool/_hx_submission_tab.html b/scipost_django/submissions/templates/submissions/pool/_hx_submission_tab.html index c0cd6b24b..5ecc3e285 100644 --- a/scipost_django/submissions/templates/submissions/pool/_hx_submission_tab.html +++ b/scipost_django/submissions/templates/submissions/pool/_hx_submission_tab.html @@ -7,6 +7,9 @@ <li class="nav-item"> {% include "submissions/pool/_submission_tab_link.html" with submission=submission tab=tab target="info" text="Submission information" %} </li> + <li class="nav-item"> + {% include "submissions/pool/_submission_tab_link.html" with submission=submission tab=tab target="qualifications" text="Fellow qualifications" %} + </li> <li class="nav-item"> {% include "submissions/pool/_submission_tab_link.html" with submission=submission tab=tab target="refereeing" text="Refereeing history" %} </li> @@ -38,6 +41,27 @@ {% if tab == "info" %} {% include 'submissions/_submission_summary.html' with submission=submission hide_title=1 show_abstract=1 %} + {% elif tab == "qualifications" %} + <table class="table table-bordered"> + <thead> + <tr> + <th>Fellow</th><th>Qualification</th> + </tr> + </thead> + <tbody> + {% for qualification in submission.qualification_set.all %} + <tr> + <td>{{ qualification.fellow }}</td> + <td>{{ qualification.get_expertise_level_display }}</td> + </tr> + {% empty %} + <tr> + <td colspan="2">No Fellow has specified their qualification</td> + </tr> + {% endfor %} + </tbody> + </table> + {% elif tab == "remarks" %} {% if remark_form %} {% include 'submissions/pool/_remark_form.html' with submission=submission form=remark_form auto_show=1 %} diff --git a/scipost_django/submissions/templates/submissions/pool/_submission_details_summary_contents.html b/scipost_django/submissions/templates/submissions/pool/_submission_details_summary_contents.html index 2dd00d188..d2a51079b 100644 --- a/scipost_django/submissions/templates/submissions/pool/_submission_details_summary_contents.html +++ b/scipost_django/submissions/templates/submissions/pool/_submission_details_summary_contents.html @@ -105,15 +105,15 @@ </span> </a></li> {% endif %} + {% if submission.cycle.has_required_actions %} + <li> + <button class="btn btn-sm btn-danger text-white"> + Required actions + </button> + {% if request.user.contributor.is_ed_admin %}{% include 'submissions/pool/_required_actions_tooltip.html' with submission=submission classes='text-white' %}{% endif %} + </li> + {% endif %} </ul> - {% if submission.cycle.has_required_actions %} - <li> - <button class="btn btn-sm btn-danger text-white"> - Required actions - </button> - {% if request.user.contributor.is_ed_admin %}{% include 'submissions/pool/_required_actions_tooltip.html' with submission=submission classes='text-white' %}{% endif %} - </ul> - {% endif %} </div> </div> @@ -131,8 +131,15 @@ {% get_editor_invitations submission request.user as invitations %} {% if invitations %} <div class="border border-warning mt-1 py-1 px-2"> - <span class="mt-1 px-1 text-danger">{% include 'bi/exclamation.html' %}</i> - You are invited to become Editor-in-charge of this Submission. <a href="{% url 'submissions:pool:editorial_assignment' submission.preprint.identifier_w_vn_nr %}">You can reply to this invitation here</a>. + <span class="mt-1 px-1 text-danger">{% include 'bi/exclamation.html' %}</span> + You are invited to become Editor-in-charge of this Submission. <a href="{% url 'submissions:pool:editorial_assignment' submission.preprint.identifier_w_vn_nr %}">You can reply to this invitation here</a>. + </div> + {% endif %} + + {% if session_fellowship %} + <div id="submission-{{ submission.id }}-appraisal"> + {% include "submissions/pool/_hx_appraisal.html" with submission=submission %} </div> {% endif %} + {% endif %} diff --git a/scipost_django/submissions/templates/submissions/pool/_submission_tab_link.html b/scipost_django/submissions/templates/submissions/pool/_submission_tab_link.html new file mode 100644 index 000000000..97f0e2a5c --- /dev/null +++ b/scipost_django/submissions/templates/submissions/pool/_submission_tab_link.html @@ -0,0 +1,5 @@ +<a class="nav-link{% if tab == target %} active{% endif %}" + hx-get="{% url "submissions:pool:_hx_submission_tab" identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr tab=target %}" + hx-target="#tabs-{{ submission.id }}" + hx-indicator="#tabs-{{ submission.id }}-indicator" +>{{ text }}</a> diff --git a/scipost_django/submissions/templatetags/submissions_pool.py b/scipost_django/submissions/templatetags/submissions_pool.py index de12a89a1..84cea1b85 100644 --- a/scipost_django/submissions/templatetags/submissions_pool.py +++ b/scipost_django/submissions/templatetags/submissions_pool.py @@ -4,7 +4,7 @@ __license__ = "AGPL v3" from django import template -from ..models import EditorialAssignment +from ..models import EditorialAssignment, Qualification register = template.Library() @@ -17,3 +17,14 @@ def get_editor_invitations(submission, user): return EditorialAssignment.objects.filter( to__user=user, submission=submission ).invited() + + +@register.simple_tag +def get_fellow_qualification(submission, fellow): + """ + Return the Qualification for this Submission, Fellow parameters. + """ + try: + return Qualification.objects.get(submission=submission, fellow=fellow) + except Qualification.DoesNotExist: + return None diff --git a/scipost_django/submissions/urls/pool.py b/scipost_django/submissions/urls/pool.py index 63eb273f6..cfc4cd960 100644 --- a/scipost_django/submissions/urls/pool.py +++ b/scipost_django/submissions/urls/pool.py @@ -5,11 +5,11 @@ __license__ = "AGPL v3" from django.urls import include, path import submissions.views.pool as views_pool +import submissions.views.appraisal as views_appraisal app_name = "pool" - urlpatterns = [ # building on /submissions/pool/ path( "", @@ -24,6 +24,21 @@ urlpatterns = [ # building on /submissions/pool/ views_pool.pool, name="pool", ), + path( + "appraisal/", + include([ + path( + "", + views_appraisal._hx_appraisal, + name="_hx_appraisal", + ), + path( + "qualification_form", + views_appraisal._hx_qualification_form, + name="_hx_qualification_form", + ), + ]), + ), path( "tab/<slug:tab>", views_pool._hx_submission_tab, diff --git a/scipost_django/submissions/views/appraisal.py b/scipost_django/submissions/views/appraisal.py new file mode 100644 index 000000000..1a2b95f6d --- /dev/null +++ b/scipost_django/submissions/views/appraisal.py @@ -0,0 +1,65 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django.shortcuts import get_object_or_404, render, redirect +from django.urls import reverse + +from colleges.permissions import fellowship_required +from submissions.models import Submission, Qualification +from submissions.forms import QualificationForm + + +@fellowship_required() +def _hx_appraisal(request, identifier_w_vn_nr=None): + submission = get_object_or_404( + Submission.objects.in_pool(request.user), + preprint__identifier_w_vn_nr=identifier_w_vn_nr, + ) + context = { "submission": submission} + fellowship = request.user.contributor.session_fellowship(request) + return render( + request, + "submissions/pool/_hx_appraisal.html", + context, + ) + + +@fellowship_required() +def _hx_qualification_form(request, identifier_w_vn_nr=None): + submission = get_object_or_404( + Submission.objects.in_pool(request.user), + preprint__identifier_w_vn_nr=identifier_w_vn_nr, + ) + fellow = request.user.contributor.session_fellowship(request) + try: + instance = Qualification.objects.get(submission=submission, fellow=fellow) + except Qualification.DoesNotExist: + instance = None + if request.method == "POST": + form = QualificationForm(request.POST, instance=instance) + if form.is_valid(): + form.save() + response = render( + request, + "submissions/pool/_hx_appraisal.html", + context={"submission": submission}, + ) + response["HX-Retarget"] = f"#submission-{submission.id}-appraisal" + return response + else: + if instance: + form = QualificationForm(instance=instance) + else: + form = QualificationForm( + initial={"submission": submission, "fellow": fellow}, + ) + context = { + "submission": submission, + "form": form, + } + return render( + request, + "submissions/pool/_hx_qualification_form.html", + context, + ) -- GitLab