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