Newer
Older
__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
__license__ = "AGPL v3"
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 .models import (
ProductionUser,
ProductionStream,
ProductionEvent,
Proofs,
ProductionEventAttachment,
)
class ProductionEventForm(forms.ModelForm):
class Meta:
model = ProductionEvent
fields = (
"stream",
"comments",
"noted_by",
)
widgets = {
"comments": TextareaWithPreview(attrs={"rows": 4}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.layout = Layout(
Field("stream", type="hidden"),
Field("noted_by", type="hidden"),
Submit("submit", "Submit"),
)
class ProductionEventForm_deprec(forms.ModelForm):
class Meta:
model = ProductionEvent
"comments": forms.Textarea(attrs={"rows": 4}),
class AssignOfficerForm(forms.ModelForm):
class Meta:
model = ProductionStream
def save(self, commit=True):
stream = super().save(False)
if commit:
if stream.status in [
constants.PRODUCTION_STREAM_INITIATED,
constants.PROOFS_SOURCE_REQUESTED,
]:
elif stream.status == constants.PROOFS_TASKED:
stream.status = constants.PRODUCTION_STREAM_INITIATED
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
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
def __init__(self, *args, **kwargs):
self.current_production_user = kwargs.pop("production_user")
self.fields["status"].choices = self.get_available_statuses()
if self.instance.status in [
constants.PRODUCTION_STREAM_COMPLETED,
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,
)
user = UserModelChoiceField(
queryset=get_user_model()
.objects.filter(production_user__isnull=True)
.order_by("last_name"),
required=False,
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()
decision = forms.ChoiceField(
choices=[
(True, "Accept Proofs for publication"),
(False, "Decline Proofs for publication"),
]
)
feedback = forms.CharField(required=False, widget=forms.Textarea)
feedback_attachment = forms.FileField(required=False)
decision = self.cleaned_data["decision"]
comments = self.cleaned_data["feedback"]
if decision in ["True", True]:
if proofs.stream.status in [
constants.PROOFS_PRODUCED,
constants.PROOFS_CHECKED,
constants.PROOFS_SENT,
constants.PROOFS_CORRECTED,
]:
proofs.status = constants.PROOFS_DECLINED
proofs.stream.status = constants.PROOFS_RETURNED
event="status",
comments="<em>Received feedback from the authors:</em><br>{comments}".format(
comments=comments
),
noted_by=proofs.stream.supervisor,
if self.cleaned_data.get("feedback_attachment"):
attachment=self.cleaned_data["feedback_attachment"],
production_event=prodevent,
)
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()
.order_by("name")
.values_list("id", "name"),
proceedings = forms.MultipleChoiceField(
choices=Proceedings.objects.all()
.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()
.order_by("-user__id")
],
supervisor = forms.MultipleChoiceField(
choices=[(0, "Unassigned")]
+ [
(prod_user.id, str(prod_user))
for prod_user in ProductionUser.objects.active()
.order_by("-user__id")
],
)
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.user = kwargs.pop("user")
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="row mb-0",
),
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="row mb-0",
),
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"),
css_class="row mb-0",
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
# 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"):
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(",")
]
)
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
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",
),
)