SciPost Code Repository

Skip to content
Snippets Groups Projects
forms.py 19 KiB
Newer Older
__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
Jorran de Wit's avatar
Jorran de Wit committed
import datetime
from typing import Dict
from django import forms
from django.contrib.auth import get_user_model
from django.db.models import Max, Value, Q
from django.db.models.functions import Greatest, Coalesce, NullIf
from django.contrib.sessions.backends.db import SessionStore
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Div, Field, Submit
from crispy_bootstrap5.bootstrap5 import FloatingField
from django.urls import reverse
from journals.models import Journal
from markup.widgets import TextareaWithPreview
from proceedings.models import Proceedings
from scipost.fields import UserModelChoiceField

from . import constants
from .models import (
    ProductionUser,
    ProductionStream,
    ProductionEvent,
    Proofs,
    ProductionEventAttachment,
)
Jorran de Wit's avatar
Jorran de Wit committed
today = datetime.datetime.today()


class ProductionEventForm(forms.ModelForm):
    class Meta:
        model = ProductionEvent
        widgets = {
            "comments": TextareaWithPreview(attrs={"rows": 4}),
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.layout = Layout(
            Field("comments"),
            Submit("submit", "Submit"),
        )


class ProductionEventForm_deprec(forms.ModelForm):
    class Meta:
        model = ProductionEvent
        fields = ("comments",)
            "comments": forms.Textarea(attrs={"rows": 4}),
Jorran de Wit's avatar
Jorran de Wit committed


class AssignOfficerForm(forms.ModelForm):
    class Meta:
        model = ProductionStream
        fields = ("officer",)
    def save(self, commit=True):
        stream = super().save(False)
        if commit:
            if stream.status in [
                constants.PRODUCTION_STREAM_INITIATED,
                constants.PROOFS_SOURCE_REQUESTED,
            ]:
                stream.status = constants.PROOFS_TASKED
            elif stream.status == constants.PROOFS_TASKED:
                stream.status = constants.PRODUCTION_STREAM_INITIATED
            stream.save()
        return stream

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["officer"].queryset = ProductionUser.objects.active().filter(
            user__groups__name="Production Officers"
        )

class AssignInvitationsOfficerForm(forms.ModelForm):
    class Meta:
        model = ProductionStream
        fields = ("invitations_officer",)
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields[
            "invitations_officer"
        ].queryset = ProductionUser.objects.active().filter(
            user__groups__name="Production Officers"
        )

class AssignSupervisorForm(forms.ModelForm):
    class Meta:
        model = ProductionStream
        fields = ("supervisor",)
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["supervisor"].queryset = ProductionUser.objects.active().filter(
            user__groups__name="Production Supervisor"
        )
class StreamStatusForm(forms.ModelForm):
    class Meta:
        model = ProductionStream
        fields = ("status",)

    def __init__(self, *args, **kwargs):
        self.current_production_user = kwargs.pop("production_user")
        super().__init__(*args, **kwargs)
        self.fields["status"].choices = self.get_available_statuses()

    def get_available_statuses(self):
        if self.instance.status in [
            constants.PRODUCTION_STREAM_COMPLETED,
            constants.PROOFS_SOURCE_REQUESTED,
            constants.PROOFS_ACCEPTED,
            constants.PROOFS_CITED,
        ]:
            # No status change can be made by User
            return ()
        elif self.instance.status == constants.PRODUCTION_STREAM_INITIATED:
            return ((constants.PROOFS_SOURCE_REQUESTED, "Source files requested"),)
        elif self.instance.status == constants.PROOFS_TASKED:
            return ((constants.PROOFS_PRODUCED, "Proofs have been produced"),)
        elif self.instance.status == constants.PROOFS_PRODUCED:
            return (
                (constants.PROOFS_CHECKED, "Proofs have been checked by Supervisor"),
                (constants.PROOFS_SENT, "Proofs sent to Authors"),
            )
        elif self.instance.status == constants.PROOFS_CHECKED:
            return (
                (constants.PROOFS_SENT, "Proofs sent to Authors"),
                (constants.PROOFS_CORRECTED, "Corrections implemented"),
                (constants.PROOFS_SOURCE_REQUESTED, "Source files requested"),
            )
        elif self.instance.status == constants.PROOFS_SENT:
            return (
                (constants.PROOFS_RETURNED, "Proofs returned by Authors"),
                (constants.PROOFS_ACCEPTED, "Authors have accepted proofs"),
            )
        elif self.instance.status == constants.PROOFS_RETURNED:
            return (
                (constants.PROOFS_CHECKED, "Proofs have been checked by Supervisor"),
                (constants.PROOFS_SENT, "Proofs sent to Authors"),
                (constants.PROOFS_CORRECTED, "Corrections implemented"),
                (constants.PROOFS_ACCEPTED, "Authors have accepted proofs"),
            )
        elif self.instance.status == constants.PROOFS_CORRECTED:
            return (
                (constants.PROOFS_CHECKED, "Proofs have been checked by Supervisor"),
                (constants.PROOFS_SENT, "Proofs sent to Authors"),
                (constants.PROOFS_ACCEPTED, "Authors have accepted proofs"),
            )
        elif self.instance.status == constants.PROOFS_PUBLISHED:
            return (
                (
                    constants.PROOFS_CITED,
                    "Cited people have been notified/invited to SciPost",
                ),
            )
        return ()

    def save(self, commit=True):
        stream = super().save(commit)
        if commit:
            event = ProductionEvent(
                stream=stream,
                event="status",
                comments="Stream changed status to: {status}".format(
                    status=stream.get_status_display()
                ),
                noted_by=self.current_production_user,
            )
            event.save()
        return stream


Jorran de Wit's avatar
Jorran de Wit committed
class UserToOfficerForm(forms.ModelForm):
    user = UserModelChoiceField(
        queryset=get_user_model()
        .objects.filter(production_user__isnull=True)
        .order_by("last_name"),
        required=False,
Jorran de Wit's avatar
Jorran de Wit committed
    class Meta:
        model = ProductionUser
    def save(self, commit=True):
        if user := self.cleaned_data["user"]:
            existing_production_user = ProductionUser.objects.filter(
                name=f"{user.first_name} {user.last_name}"
            ).first()
            if existing_production_user:
                existing_production_user.user = user
                existing_production_user.save()

            else:
                production_user = ProductionUser.objects.create(
                    name=f"{user.first_name} {user.last_name}", user=user
                )
                production_user.save()
Jorran de Wit's avatar
Jorran de Wit committed

Jorran de Wit's avatar
Jorran de Wit committed
class ProofsUploadForm(forms.ModelForm):
Jorran de Wit's avatar
Jorran de Wit committed
    class Meta:
Jorran de Wit's avatar
Jorran de Wit committed
        model = Proofs
        fields = ("attachment",)
Jorran de Wit's avatar
Jorran de Wit committed


Jorran de Wit's avatar
Jorran de Wit committed
class ProofsDecisionForm(forms.ModelForm):
    decision = forms.ChoiceField(
        choices=[
            (True, "Accept Proofs for publication"),
            (False, "Decline Proofs for publication"),
        ]
    )
Jorran de Wit's avatar
Jorran de Wit committed
    feedback = forms.CharField(required=False, widget=forms.Textarea)
    feedback_attachment = forms.FileField(required=False)
Jorran de Wit's avatar
Jorran de Wit committed

    class Meta:
Jorran de Wit's avatar
Jorran de Wit committed
        model = Proofs
Jorran de Wit's avatar
Jorran de Wit committed
        fields = ()

    def save(self, commit=True):
Jorran de Wit's avatar
Jorran de Wit committed
        proofs = self.instance
        decision = self.cleaned_data["decision"]
        comments = self.cleaned_data["feedback"]
Jorran de Wit's avatar
Jorran de Wit committed

        if decision in ["True", True]:
Jorran de Wit's avatar
Jorran de Wit committed
            proofs.status = constants.PROOFS_ACCEPTED
            if proofs.stream.status in [
                constants.PROOFS_PRODUCED,
                constants.PROOFS_CHECKED,
                constants.PROOFS_SENT,
                constants.PROOFS_CORRECTED,
            ]:
Jorran de Wit's avatar
Jorran de Wit committed
                # Force status change on Stream if appropriate
Jorran de Wit's avatar
Jorran de Wit committed
                proofs.stream.status = constants.PROOFS_ACCEPTED
Jorran de Wit's avatar
Jorran de Wit committed
        else:
Jorran de Wit's avatar
Jorran de Wit committed
            proofs.status = constants.PROOFS_DECLINED
            proofs.stream.status = constants.PROOFS_RETURNED
Jorran de Wit's avatar
Jorran de Wit committed

        if commit:
Jorran de Wit's avatar
Jorran de Wit committed
            proofs.save()
            proofs.stream.save()
Jorran de Wit's avatar
Jorran de Wit committed

            prodevent = ProductionEvent(
Jorran de Wit's avatar
Jorran de Wit committed
                stream=proofs.stream,
                event="status",
                comments="<em>Received feedback from the authors:</em><br>{comments}".format(
                    comments=comments
                ),
                noted_by=proofs.stream.supervisor,
Jorran de Wit's avatar
Jorran de Wit committed
            )
            prodevent.save()
            if self.cleaned_data.get("feedback_attachment"):
                attachment = ProductionEventAttachment(
                    attachment=self.cleaned_data["feedback_attachment"],
                    production_event=prodevent,
                )
                attachment.save()
Jorran de Wit's avatar
Jorran de Wit committed
        return proofs


class ProductionStreamSearchForm(forms.Form):
    author = forms.CharField(max_length=100, required=False, label="Author(s)")
    title = forms.CharField(max_length=512, required=False)
    identifier = forms.CharField(max_length=128, required=False)

    all_streams = ProductionStream.objects.ongoing()

    stream_journals = all_streams.values_list(
        "submission__editorialdecision__for_journal", flat=True
    ).distinct()
    stream_proceedings = all_streams.values_list(
        "submission__proceedings", flat=True
    ).distinct()
    stream_officers = all_streams.values_list("officer", flat=True).distinct()
    stream_supervisors = all_streams.values_list("supervisor", flat=True).distinct()

    journal = forms.MultipleChoiceField(
        choices=Journal.objects.active()
        .filter(id__in=stream_journals)
        .order_by("name")
        .values_list("id", "name"),
    proceedings = forms.MultipleChoiceField(
        choices=Proceedings.objects.all()
        .filter(id__in=stream_proceedings)
        .order_by("-submissions_close")
        # Short name is `event_suffix` if set, otherwise `event_name`
        .annotate(
            short_name=Coalesce(NullIf("event_suffix", Value("")), "event_name")
        ).values_list("id", "short_name"),
    officer = forms.MultipleChoiceField(
        choices=[(0, "Unassigned")]
        + [
            (prod_user.id, str(prod_user))
            for prod_user in ProductionUser.objects.active()
            .filter(id__in=stream_officers)
    supervisor = forms.MultipleChoiceField(
        choices=[(0, "Unassigned")]
        + [
            (prod_user.id, str(prod_user))
            for prod_user in ProductionUser.objects.active()
            .filter(id__in=stream_supervisors)
    )
    status = forms.MultipleChoiceField(
        # Use short status names from their internal (code) name
        choices=[
            (status_code_name, status_code_name.replace("_", " ").title())
            for status_code_name, _ in constants.PRODUCTION_STREAM_STATUS[:-2]
        required=False,
    )
    orderby = forms.ChoiceField(
        label="Order by",
        choices=(
            ("submission__acceptance_date", "Date accepted"),
            ("latest_activity_annot", "Latest activity"),
            (
                "status,submission__acceptance_date",
                "Status + Date accepted",
            ),
            ("status,latest_activity_annot", "Status + Latest activity"),
        ),
        required=False,
    )
    ordering = forms.ChoiceField(
        label="Ordering",
        choices=(
            # FIXME: Emperically, the ordering appers to be reversed for dates?
            ("-", "Ascending"),
            ("+", "Descending"),
        ),
        required=False,
    )

    def __init__(self, *args, **kwargs):
        self.session_key = kwargs.pop("session_key", None)
        super().__init__(*args, **kwargs)

        # Set the initial values of the form fields from the session data
        if self.session_key:
            session = SessionStore(session_key=self.session_key)

            for field in self.fields:
                if field in session:
                    self.fields[field].initial = session[field]

        self.helper = FormHelper()
        self.helper.layout = Layout(
            Div(
                        Div(FloatingField("identifier"), css_class="col-4"),
                        Div(FloatingField("author"), css_class="col-8"),
                        Div(FloatingField("title"), css_class="col-12"),
                    css_class="col-12 col-md-8",
                ),
                Div(
                        Div(FloatingField("orderby"), css_class="col-6 col-md-12"),
                        Div(FloatingField("ordering"), css_class="col-6 col-md-12"),
                    css_class="col-12 col-md-4",
                css_class="row mb-0 mt-2",
            ),
            Div(
                Div(Field("journal", size=10), css_class="col-6 col-md-4 col-lg"),
                    Field("proceedings", size=10),
                    css_class="col-6 col-md-8 col-lg d-none d-md-block",
                Div(Field("status", size=10), css_class="col-6 col-md-4 col-lg"),
                Div(Field("supervisor", size=10), css_class="col-6 col-md-4 col-lg"),
                Div(Field("officer", size=10), css_class="col-6 col-md-4 col-lg"),
    def apply_filter_set(self, filters: Dict, none_on_empty: bool = False):
        # Apply the filter set to the form
        for key in self.fields:
            if key in filters:
                self.fields[key].initial = filters[key]
            elif none_on_empty:
                if isinstance(self.fields[key], forms.MultipleChoiceField):
                    self.fields[key].initial = []
                else:
                    self.fields[key].initial = None

    def search_results(self):
        # Save the form data to the session
        if self.session_key is not None:
            session = SessionStore(session_key=self.session_key)

            for key in self.cleaned_data:
                session[key] = self.cleaned_data.get(key)

            session.save()

        streams = ProductionStream.objects.ongoing()

        streams = streams.annotate(
            latest_activity_annot=Greatest(Max("events__noted_on"), "opened", "closed")
        )

        if identifier := self.cleaned_data.get("identifier"):
            streams = streams.filter(
                submission__preprint__identifier_w_vn_nr__icontains=identifier,
        if author := self.cleaned_data.get("author"):
            streams = streams.filter(submission__author_list__icontains=author)
        if title := self.cleaned_data.get("title"):
            streams = streams.filter(submission__title__icontains=title)

        def is_in_or_null(queryset, key, value, implicit_all=True):
            """
            Filter a queryset by a list of values. If the list contains a 0, then
            also include objects where the key is null. If the list is empty, then
            include all objects if implicit_all is True.
            """
            value = self.cleaned_data.get(value)
            has_unassigned = "0" in value
            is_unassigned = Q(**{key + "__isnull": True})
            is_in_values = Q(**{key + "__in": list(filter(lambda x: x != 0, value))})

            if has_unassigned:
                return queryset.filter(is_unassigned | is_in_values)
            elif implicit_all and not value:
                return queryset
            else:
                return queryset.filter(is_in_values)

        streams = is_in_or_null(
            streams, "submission__editorialdecision__for_journal", "journal"
        )
        streams = is_in_or_null(streams, "submission__proceedings", "proceedings")
        streams = is_in_or_null(streams, "officer", "officer")
        streams = is_in_or_null(streams, "supervisor", "supervisor")
        streams = is_in_or_null(streams, "status", "status")
        if not self.user.has_perm("scipost.can_view_all_production_streams"):
            # Restrict stream queryset if user is not supervisor
            streams = streams.filter_for_user(self.user.production_user)

        # Ordering of streams
        # Only order if both fields are set
        if (orderby_value := self.cleaned_data.get("orderby")) and (
            ordering_value := self.cleaned_data.get("ordering")
        ):
            # Remove the + from the ordering value, causes a Django error
            ordering_value = ordering_value.replace("+", "")

            # Ordering string is built by the ordering (+/-), and the field name
            # from the orderby field split by "," and joined together
            streams = streams.order_by(
                *[
                    ordering_value + order_part
                    for order_part in orderby_value.split(",")
                ]
            )


class BulkAssignOfficersForm(forms.Form):
    officer = forms.ModelChoiceField(
        queryset=ProductionUser.objects.active().filter(
            user__groups__name="Production Officers"
        ),
        required=False,
        empty_label="Unchanged",
    )
    supervisor = forms.ModelChoiceField(
        queryset=ProductionUser.objects.active().filter(
            user__groups__name="Production Supervisor"
        ),
        required=False,
        empty_label="Unchanged",
    )

    def __init__(self, *args, **kwargs):
        self.productionstreams = kwargs.pop("productionstreams", None)
        super().__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.form_id = "productionstreams-bulk-action-form"
        self.helper.attrs = {
            "hx-post": reverse(
                "production:_hx_productionstream_actions_bulk_assign_officers"
            ),
            "hx-target": "#productionstream-bulk-assign-officers-container",
            "hx-swap": "outerHTML",
            "hx-confirm": "Are you sure you want to assign the selected production streams to the selected officers?",
        }
        self.helper.layout = Layout(
            Div(
                Div(Field("supervisor"), css_class="col-6 col-md-4 col-lg-3"),
                Div(Field("officer"), css_class="col-6 col-md-4 col-lg-3"),
                css_class="row mb-0",
            ),
        )