diff --git a/scipost_django/submissions/forms/appraisal.py b/scipost_django/submissions/forms/appraisal.py index 0fb3b22e1ee1e004c3b04ea17b1b7d71c13b7376..f76ae218adcf05fad5af65c8ee35f585a5c85ab2 100644 --- a/scipost_django/submissions/forms/appraisal.py +++ b/scipost_django/submissions/forms/appraisal.py @@ -5,10 +5,11 @@ __license__ = "AGPL v3" from django import forms from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, Div, Field, HTML, ButtonHolder, Button +from crispy_forms.layout import Layout, Div, Field from crispy_bootstrap5.bootstrap5 import FloatingField from ethics.models import SubmissionClearance +from submissions.models.assignment import ConditionalAssignmentOffer from ..models import Qualification, Readiness @@ -100,7 +101,8 @@ class RadioAppraisalForm(forms.Form): label="Readiness", choices=(("assign_now", "Ready to take charge now"),) + Readiness.STATUS_CHOICES[0:1] - + Readiness.STATUS_CHOICES[4:5], + + Readiness.STATUS_CHOICES[3:4] + + Readiness.STATUS_CHOICES[5:6], widget=forms.RadioSelect(), required=False, initial=None, @@ -140,6 +142,12 @@ class RadioAppraisalForm(forms.Form): self.initial["readiness"] = readiness.status + # Disable the readiness field if the fellow made an assignment offer + if self.submission.conditional_assignment_offers.filter( + offered_by=self.fellow.contributor + ).exists(): + self.fields["readiness"].disabled = True + self.helper = FormHelper() self.helper.layout = Layout( Div( @@ -169,6 +177,8 @@ class RadioAppraisalForm(forms.Form): action = "take charge" elif readiness == "desk_reject": action = "suggest a desk rejection" + elif readiness == Readiness.STATUS_CONDITIONAL: + action = "offer a conditional assignment" if action: # If readiness is set to assign_now or desk_reject if reason: # Both readiness and expertise_level are required @@ -186,10 +196,8 @@ class RadioAppraisalForm(forms.Form): qualification, _ = Qualification.objects.get_or_create( submission=self.submission, fellow=self.fellow ) - print(expertise_level) qualification.expertise_level = expertise_level qualification.save() - print(qualification) if ( readiness_status := self.cleaned_data["readiness"] @@ -197,10 +205,8 @@ class RadioAppraisalForm(forms.Form): readiness, _ = Readiness.objects.get_or_create( submission=self.submission, fellow=self.fellow ) - print(readiness_status) readiness.status = readiness_status readiness.save() - print(readiness) @property def is_qualified(self): @@ -235,3 +241,104 @@ class RadioAppraisalForm(forms.Form): """ 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 + + +class ConditionalAssignmentOfferInlineForm(forms.ModelForm): + class Meta: + model = ConditionalAssignmentOffer + fields = ["submission", "offered_by", "condition_type"] + widgets = { + "submission": forms.HiddenInput(), + "offered_by": forms.HiddenInput(), + } + + def __init__(self, *args, **kwargs): + self.submission = kwargs.pop("submission") + self.offered_by = kwargs.pop("offered_by") + self.readonly = kwargs.pop("readonly", False) + + # Create field depending on the type of condition + extra_fields = {} + + condition_type = None + if args and (data := args[0]) and data.get("condition_type"): + condition_type = data.get("condition_type") + elif instance := kwargs.get("instance"): + condition_type = instance.condition_type + elif len(ConditionalAssignmentOffer.CONDITION_CHOICES) == 1: + condition_type = ConditionalAssignmentOffer.CONDITION_CHOICES[0][0] + + if condition_type == "JournalTransfer": + alternative_journal_id = forms.ModelChoiceField( + label="Alternative journal", + queryset=self.submission.submitted_to.alternative_journals.all(), + ) + extra_fields["alternative_journal_id"] = alternative_journal_id + + self.base_fields.update(extra_fields) + super().__init__(*args, **kwargs) + + self.initial["submission"] = self.submission + self.initial["offered_by"] = self.offered_by + + self.fields["condition_type"].label = "Condition for assignment" + self.fields["condition_type"].choices = ( + ConditionalAssignmentOffer.CONDITION_CHOICES + ) + + # If the form is readonly, disable all fields + for field in self.fields: + if self.readonly: + self.fields[field].disabled = True + if field in extra_fields: + self.initial[field] = self.instance.condition_details.get(field, None) + else: + self.initial[field] = getattr(self, field, None) or getattr( + self.instance, field, None + ) + + self.helper = FormHelper() + self.helper.layout = Layout( + Field("submission"), + Field("offered_by"), + Div(FloatingField("condition_type"), css_class="col"), + *[Div(FloatingField(field), css_class="col") for field in extra_fields], + ) + + def clean(self): + qualification = Qualification.objects.filter( + submission=self.submission, fellow__contributor=self.offered_by + ).first() + has_clearance = SubmissionClearance.objects.filter( + profile=self.offered_by.profile, + submission=self.submission, + ).exists() + + if qualification is None: + self.add_error( + None, + "You must first declare your expertise level for this submission.", + ) + elif not qualification.is_qualified: + self.add_error( + None, + "You must be at least marginally qualified to make an assignment offer.", + ) + if not has_clearance: + self.add_error( + None, + "You must first declare no competing interests with this submission.", + ) + + return super().clean() + + def save(self): + instance = super().save(commit=False) + + if instance.condition_type == "JournalTransfer": + instance.condition_details = { + "alternative_journal_id": self.cleaned_data["alternative_journal_id"].id + } + + instance.save() + return instance diff --git a/scipost_django/submissions/migrations/0166_alter_readiness_status.py b/scipost_django/submissions/migrations/0166_alter_readiness_status.py new file mode 100644 index 0000000000000000000000000000000000000000..ff9add0502785bae9be8a598cecb9c7eb61e9355 --- /dev/null +++ b/scipost_django/submissions/migrations/0166_alter_readiness_status.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.15 on 2024-10-09 14:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("submissions", "0165_add_journal_transfer_conditional_offer"), + ] + + operations = [ + migrations.AlterField( + model_name="readiness", + name="status", + field=models.CharField( + choices=[ + ("perhaps_later", "Perhaps later"), + ( + "could_if_transferred", + "I could, if transferred to lower journal", + ), + ("too_busy", "I would, but I'm currently too busy"), + ("conditional", "I would, if transferred"), + ("not_interested", "I won't, I'm not interested enough"), + ("desk_reject", "I won't, and vote for desk rejection"), + ], + max_length=32, + ), + ), + ] diff --git a/scipost_django/submissions/models/readiness.py b/scipost_django/submissions/models/readiness.py index 898049f72eeba916e9e2c8f76b60caf2338055a4..3a08b3a5b17ec7a76bbb3139afbbe680d52afdc2 100644 --- a/scipost_django/submissions/models/readiness.py +++ b/scipost_django/submissions/models/readiness.py @@ -18,6 +18,7 @@ class Readiness(models.Model): STATUS_TOO_BUSY = "too_busy" STATUS_NOT_INTERESTED = "not_interested" STATUS_DESK_REJECT = "desk_reject" + STATUS_CONDITIONAL = "conditional" STATUS_CHOICES = ( (STATUS_PERHAPS_LATER, "Perhaps later"), ( @@ -25,6 +26,7 @@ class Readiness(models.Model): "I could, if transferred to lower journal", ), (STATUS_TOO_BUSY, "I would, but I'm currently too busy"), + (STATUS_CONDITIONAL, "I would, if transferred"), (STATUS_NOT_INTERESTED, "I won't, I'm not interested enough"), (STATUS_DESK_REJECT, "I won't, and vote for desk rejection"), ) diff --git a/scipost_django/submissions/templates/submissions/pool/_hx_conditional_assignment_offer_form.html b/scipost_django/submissions/templates/submissions/pool/_hx_conditional_assignment_offer_form.html new file mode 100644 index 0000000000000000000000000000000000000000..2eb490e9131353bf9aa438755630a576b21eab7e --- /dev/null +++ b/scipost_django/submissions/templates/submissions/pool/_hx_conditional_assignment_offer_form.html @@ -0,0 +1,29 @@ +{% load crispy_forms_tags %} +{% load ethics_extras %} + +<div class="row mb-0"> + + + + <form id="conditional-assignment-offer-pool-form-{{ submission.id }}" + class="col" + hx-post="{{ request.path }}" + hx-swap="outerHTML" + hx-target="closest div" + hx-trigger="change delay:250ms"> + <div class="row mb-0 align-items-center"> + + {% crispy form %} + + {% if request.method == "POST" %} + <div class="col-auto"> + <button hx-vals='{"submit": "Make offer"}' + hx-post="{{ request.path }}" + hx-confirm="Are you sure you want to make this offer? You will not be able to change it later, and you will automatically become EIC of the submission if the offer is accepted." + class="btn btn-primary btn-sm">Make offer</button> + </div> + {% endif %} + + </div> + </form> +</div> 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 index 7f593ca5c0decf8b1fe147a7a16e5f9732fc597b..67a10c4aa80f1b32bac89808c3be88b8844d6256 100644 --- a/scipost_django/submissions/templates/submissions/pool/_hx_radio_appraisal_form.html +++ b/scipost_django/submissions/templates/submissions/pool/_hx_radio_appraisal_form.html @@ -11,18 +11,26 @@ hx-post="{{ request.path }}" hx-swap="outerHTML" hx-target="closest div" - hx-trigger="change delay:1s, CI-clearance-asserted from:closest div"> + hx-sync="closest form:replace" + hx-trigger="change delay:500ms, CI-clearance-asserted from:closest div, conditional-assignment-offer-made from:closest div"> {% crispy form %} </form> <div class="col-12 col-lg">{% include "ethics/_hx_submission_ethics.html" %}</div> + {% if readiness == 'conditional' %} + <div hx-get="{% url "submissions:pool:_hx_conditional_assignment_offer_form" submission.preprint.identifier_w_vn_nr %}" + hx-trigger="intersect once"></div> + {% endif %} + {% if not clearance and readiness and readiness != "perhaps_later" %} <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 once"> - <div class="btn btn-secondary htmx-indicator">Searching CrossRef for common works ...</div> + <div class="btn btn-secondary htmx-indicator"> + Searching CrossRef for common works ... + </div> </div> {% endif %} diff --git a/scipost_django/submissions/urls/pool/base.py b/scipost_django/submissions/urls/pool/base.py index 20fb513d5330cdd1b014ab1236bfecb3f6b4048a..5617c777079414534af2a2a6c70c07e992669bd0 100644 --- a/scipost_django/submissions/urls/pool/base.py +++ b/scipost_django/submissions/urls/pool/base.py @@ -34,6 +34,11 @@ urlpatterns = [ # building on /submissions/pool/ views_appraisal._hx_radio_appraisal_form, name="_hx_radio_appraisal_form", ), + path( + "conditional_assignment_offer", + views_appraisal._hx_conditional_assignment_offer_form, + name="_hx_conditional_assignment_offer_form", + ), ] ), ), diff --git a/scipost_django/submissions/views/appraisal.py b/scipost_django/submissions/views/appraisal.py index 8b0fda23b8bfee02428c9545c4d46e6d65b9ac39..9467ce070f9c0c93e06cdaf26e308a10b96f06ff 100644 --- a/scipost_django/submissions/views/appraisal.py +++ b/scipost_django/submissions/views/appraisal.py @@ -3,12 +3,15 @@ __license__ = "AGPL v3" from django.http import HttpResponse -from django.shortcuts import get_object_or_404, render +from django.shortcuts import get_object_or_404, redirect, 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.forms.appraisal import ( + ConditionalAssignmentOfferInlineForm, + RadioAppraisalForm, +) from submissions.models import Submission, Qualification, Readiness from submissions.forms import QualificationForm, ReadinessForm @@ -61,3 +64,43 @@ def _hx_radio_appraisal_form(request, identifier_w_vn_nr=None): "submissions/pool/_hx_radio_appraisal_form.html", context, ) + + +@fellowship_required() +def _hx_conditional_assignment_offer_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) + offer = submission.conditional_assignment_offers.filter( + offered_by=fellow.contributor + ).first() + + form = ConditionalAssignmentOfferInlineForm( + request.POST or None, + instance=offer, + submission=submission, + offered_by=fellow.contributor, + readonly=request.method == "GET" and offer is not None, + ) + + if request.method == "POST" and request.POST.get("submit"): + if form.is_valid(): + form.save() + + response = HttpResponse() + response["HX-Trigger"] = "conditional-assignment-offer-made" + + return response + + context = { + "submission": submission, + "form": form, + "offer": offer, + } + return TemplateResponse( + request, + "submissions/pool/_hx_conditional_assignment_offer_form.html", + context, + )