diff --git a/scipost_django/submissions/forms/appraisal.py b/scipost_django/submissions/forms/appraisal.py index e5d9c9495e99ad07270199355c822a9b78b6e9cb..fbb7697c8e1f1a0bf8cde2190a7986729184e94f 100644 --- a/scipost_django/submissions/forms/appraisal.py +++ b/scipost_django/submissions/forms/appraisal.py @@ -5,9 +5,11 @@ __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_forms.layout import Layout, Div, Field, HTML, ButtonHolder, Button from crispy_bootstrap5.bootstrap5 import FloatingField +from ethics.models import SubmissionClearance + from ..models import Qualification, Readiness @@ -28,9 +30,9 @@ class QualificationForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper() - self.fields[ - "expertise_level" - ].label = "Your expertise level for this Submission" + self.fields["expertise_level"].label = ( + "Your expertise level for this Submission" + ) self.helper.layout = Layout( Field("submission"), Field("fellow"), @@ -80,3 +82,124 @@ class ReadinessForm(forms.ModelForm): instance.status = self.cleaned_data["choice"] instance.save() return instance + + +class RadioAppraisalForm(forms.Form): + """ + A collection of radios for the full appraisal of a submission. + """ + + expertise_level = forms.ChoiceField( + label="Expertise level", + choices=Qualification.EXPERTISE_LEVEL_CHOICES[1::2], #! Hack: skip some choices + widget=forms.RadioSelect(), + required=False, + initial=None, + ) + readiness = forms.ChoiceField( + label="Readiness", + choices=(("assign_now", "Ready to take charge now"),) + + Readiness.STATUS_CHOICES[0:1] + + Readiness.STATUS_CHOICES[4:5], + widget=forms.RadioSelect(), + required=False, + initial=None, + ) + + def __init__(self, *args, **kwargs): + self.submission = kwargs.pop("submission") + self.fellow = kwargs.pop("fellow") + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.layout = Layout( + Div( + Div( + Field("readiness", id=f"{self.submission.id}-readiness"), + css_class="col", + ), + Div( + Field("expertise_level", id=f"{self.submission.id}-expertise"), + css_class="col", + ), + css_class="row mb-0", + ) + ) + + def clean(self): + readiness = self.cleaned_data["readiness"] + expertise_level = self.cleaned_data["expertise_level"] + + if readiness == "assign_now" and not self.has_clearance: + self.add_error( + "readiness", + "You must declare no competing interests to take charge of this submission.", + ) + + action = reason = None + if not expertise_level: + reason = "You must declare a level of expertise" + elif not self.is_qualified: + reason = "You must be at least marginally qualified" + + if readiness == "assign_now": + action = "take charge" + elif readiness == "desk_reject": + action = "suggest a desk rejection" + + if action and reason: + self.add_error("expertise_level", f"{reason} to {action}.") + + def save(self): + """ + Create the individual Qualification and Readiness objects from the form data. + """ + if expertise_level := self.cleaned_data["expertise_level"]: + qualification, _ = Qualification.objects.get_or_create( + submission=self.submission, fellow=self.fellow + ) + qualification.expertise_level = expertise_level + qualification.save() + + if ( + readiness_status := self.cleaned_data["readiness"] + ) and readiness_status != "assign_now": + readiness, _ = Readiness.objects.get_or_create( + submission=self.submission, fellow=self.fellow + ) + + readiness.status = readiness_status + readiness.save() + + @property + def is_qualified(self): + """ + Return True if the form data indicates that the fellow is qualified, + i.e. has an expertise level in: + - Expert + - Very knowledgeable + - Knowledgeable + - Marginally qualified + """ + + is_qualified = self.cleaned_data["expertise_level"] in [ + choice[0] for choice in Qualification.EXPERTISE_LEVEL_CHOICES[:4] + ] + + return is_qualified + + @property + def has_clearance(self): + """ + Returns True if the fellow has clearance (no Competing Interest) with the submission. + """ + return SubmissionClearance.objects.filter( + profile=self.fellow.contributor.profile, + submission=self.submission, + ).exists() + + def should_redirect_to_editorial_assignment(self): + """ + Return True if the form data indicates that the fellow is ready to take charge now. + """ + is_ready_to_take_charge = self.cleaned_data["readiness"] == "assign_now" + return is_ready_to_take_charge and self.is_qualified and self.has_clearance diff --git a/scipost_django/submissions/models/qualification.py b/scipost_django/submissions/models/qualification.py index 25de301d661e9e20c4df2b5467680bd867c7445a..b67738bafc443af81785f4354da345a08ee5c9ce 100644 --- a/scipost_django/submissions/models/qualification.py +++ b/scipost_django/submissions/models/qualification.py @@ -21,8 +21,8 @@ class Qualification(models.Model): 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"), + (VERY_KNOWLEDGEABLE, "Very knowledgeable"), + (KNOWLEDGEABLE, "Knowledgeable"), (MARGINALLY_QUALIFIED, "Marginally qualified"), (NOT_REALLY_QUALIFIED, "Not really qualified"), (NOT_AT_ALL_QUALIFIED, "Not at all qualified"), diff --git a/scipost_django/submissions/templates/submissions/pool/_hx_appraisal.html b/scipost_django/submissions/templates/submissions/pool/_hx_appraisal.html index a4978cd0172dca41ad5c0a7578ad465b24a095d1..27f31e82238f341ae9633a96442e363226f8960e 100644 --- a/scipost_django/submissions/templates/submissions/pool/_hx_appraisal.html +++ b/scipost_django/submissions/templates/submissions/pool/_hx_appraisal.html @@ -4,53 +4,19 @@ {% get_fellow_qualification submission session_fellowship as qualification %} {% get_profile_clearance submission.clearances request.user.contributor.profile as clearance %} -<div class="row"> +<div class="col" hx-get="{% url "submissions:pool:_hx_radio_appraisal_form" submission.preprint.identifier_w_vn_nr %}" hx-trigger="intersect once"></div> - {% if qualification and qualification.is_qualified and not clearance %} - <div id="submission-{{ submission.id }}-crossref-CI-audit" - class="col-12" - hx-get="{% url 'ethics:_hx_submission_competing_interest_crossref_audit' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr %}" - hx-trigger="revealed"> - <div class="btn btn-secondary htmx-indicator"> - Searching CrossRef for common works ... - </div> - </div> - {% endif %} - -</div> -<div class="row mb-0"> - <div id="submission-{{ submission.id }}-qualification-form" - class="col-lg-4" - hx-get="{% url 'submissions:pool:_hx_qualification_form' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr %}" - hx-trigger="revealed"></div> - - - {% if qualification %} - - <div class="col-lg-{% if clearance %}4{% else %}8{% endif %}"> - - {% if qualification.is_qualified %} - - <div id="submission-{{ submission.pk }}-ethics" - hx-get="{% url 'ethics:_hx_submission_ethics' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr %}" - hx-trigger="revealed"></div> - {% else %} - - <div class="border border-danger bg-danger bg-opacity-10 p-2 d-inline-flex"> - <span class="mx-auto">Other experts will handle this Submission</span> - - </div> - {% endif %} +<div id="submission-{{ submission.pk }}-ethics" + class="col-7" + hx-get="{% url 'ethics:_hx_submission_ethics' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr %}" + hx-trigger="revealed"></div> +{% if not clearance %} + <div id="submission-{{ submission.id }}-crossref-CI-audit" + hx-get="{% url 'ethics:_hx_submission_competing_interest_crossref_audit' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr %}" + hx-trigger="revealed"> + <div class="btn btn-secondary htmx-indicator"> + Searching CrossRef for common works ... </div> - {% endif %} - - {% if qualification and qualification.is_qualified and clearance %} - <div class="col-lg-4"> - <div id="submission-{{ submission.id }}-readiness-form" - hx-get="{% url 'submissions:pool:_hx_readiness_form' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr %}" - hx-trigger="revealed"></div> - </div> - {% endif %} - -</div> + </div> +{% endif %} \ No newline at end of file diff --git a/scipost_django/submissions/templates/submissions/pool/_hx_radio_appraisal_form.html b/scipost_django/submissions/templates/submissions/pool/_hx_radio_appraisal_form.html new file mode 100644 index 0000000000000000000000000000000000000000..a60ac4be89bda42009c7f0cbd1016cc259f9e34f --- /dev/null +++ b/scipost_django/submissions/templates/submissions/pool/_hx_radio_appraisal_form.html @@ -0,0 +1,8 @@ +{% load crispy_forms_tags %} + +<form id="radio-appraisal-form-{{ submission.id }}" + hx-post="{{ request.path }}" + hx-swap="outerHTML" + hx-trigger="change delay 1s"> + {% crispy form %} +</form> 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 c09857d3aad831093adfcec636235118c0307aae..22f852a4f6b0edcb59df50bdd6918b4209c2ea34 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 @@ -1,4 +1,7 @@ {% load submissions_pool %} +{% load ethics_extras %} + +{% get_profile_clearance submission.clearances request.user.contributor.profile as clearance %} <div class="row mb-0"> <div class="col col-md-9"> @@ -131,11 +134,9 @@ {% endif %} {% if session_fellowship %} - <div id="submission-{{ submission.id }}-appraisal" - class="mb-0" - > + <section id="submission-{{ submission.id }}-appraisal" class="row mb-0 border-top border-2 pt-2"> {% include "submissions/pool/_hx_appraisal.html" with submission=submission %} - </div> + </section> {% endif %} {% endif %} diff --git a/scipost_django/submissions/urls/pool/base.py b/scipost_django/submissions/urls/pool/base.py index 40d579e70922ded3e2774f82dab57877b0558544..7a62edd10fedc02df470c3977221819904e7baca 100644 --- a/scipost_django/submissions/urls/pool/base.py +++ b/scipost_django/submissions/urls/pool/base.py @@ -44,6 +44,11 @@ urlpatterns = [ # building on /submissions/pool/ views_appraisal._hx_readiness_form, name="_hx_readiness_form", ), + path( + "radio_form", + views_appraisal._hx_radio_appraisal_form, + name="_hx_radio_appraisal_form", + ), ] ), ), diff --git a/scipost_django/submissions/views/appraisal.py b/scipost_django/submissions/views/appraisal.py index 5e9f7f4c6de3aa314639e512c905fa9378792517..74822ea0509f08667dff23949a4aa39076736279 100644 --- a/scipost_django/submissions/views/appraisal.py +++ b/scipost_django/submissions/views/appraisal.py @@ -4,9 +4,11 @@ __license__ = "AGPL v3" from django.http import HttpResponse from django.shortcuts import get_object_or_404, render +from django.template.response import TemplateResponse from django.urls import reverse from colleges.permissions import fellowship_required +from submissions.forms.appraisal import RadioAppraisalForm from submissions.models import Submission, Qualification, Readiness from submissions.forms import QualificationForm, ReadinessForm @@ -113,3 +115,53 @@ def _hx_readiness_form(request, identifier_w_vn_nr=None): "submissions/pool/_hx_readiness_form.html", context, ) + + +@fellowship_required() +def _hx_radio_appraisal_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: + qualification = Qualification.objects.get(submission=submission, fellow=fellow) + readiness = Readiness.objects.get(submission=submission, fellow=fellow) + except (Qualification.DoesNotExist, Readiness.DoesNotExist): + qualification = readiness = None + + form = RadioAppraisalForm( + request.POST or None, + submission=submission, + fellow=fellow, + initial={ + "expertise_level": qualification.expertise_level if qualification else None, + "readiness": readiness.status if readiness else None, + }, + ) + + if request.method == "POST": + if form.is_valid(): + if form.should_redirect_to_editorial_assignment(): + response = HttpResponse() + response["HX-Redirect"] = reverse( + "submissions:pool:editorial_assignment", + kwargs={ + "identifier_w_vn_nr": identifier_w_vn_nr, + }, + ) + return response + else: + form.save() + + context = { + "submission": submission, + "fellow": fellow, + "form": form, + } + return TemplateResponse( + request, + "submissions/pool/_hx_radio_appraisal_form.html", + context, + )