diff --git a/scipost_django/production/forms.py b/scipost_django/production/forms.py index 1f0c1611f84e62b26a98d55995a340638e4c4777..79862d403abce58dd966b5f5a4d8d6a757342cb3 100644 --- a/scipost_django/production/forms.py +++ b/scipost_django/production/forms.py @@ -6,8 +6,8 @@ import datetime from django import forms from django.contrib.auth import get_user_model -from django.db.models import Max -from django.db.models.functions import Greatest +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 @@ -280,36 +280,59 @@ class ProofsDecisionForm(forms.ModelForm): class ProductionStreamSearchForm(forms.Form): - accepted_in = forms.ModelMultipleChoiceField( - queryset=Journal.objects.active(), + 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() + journal = forms.MultipleChoiceField( + choices=Journal.objects.active() + .filter( + id__in=all_streams.values_list( + "submission__editorialdecision__for_journal", flat=True + ) + ) + .order_by("name") + .values_list("id", "name"), required=False, ) - proceedings = forms.ModelMultipleChoiceField( - queryset=Proceedings.objects.order_by("-submissions_close"), + proceedings = forms.MultipleChoiceField( + choices=Proceedings.objects.all() + .filter(id__in=all_streams.values_list("submission__proceedings", flat=True)) + .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") + .distinct(), required=False, ) - 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) - officer = forms.ModelChoiceField( - queryset=ProductionUser.objects.active().filter( - user__groups__name="Production Officers" - ), + officer = forms.MultipleChoiceField( + choices=[(0, "Unassigned")] + + [ + (prod_user.id, str(prod_user)) + for prod_user in ProductionUser.objects.active() + .filter(id__in=all_streams.values_list("officer", flat=True)) + .order_by("-user__id") + .distinct() + ], required=False, - empty_label="Any", ) - supervisor = forms.ModelChoiceField( - queryset=ProductionUser.objects.active().filter( - user__groups__name="Production Supervisor" - ), + supervisor = forms.MultipleChoiceField( + choices=[(0, "Unassigned")] + + [ + (prod_user.id, str(prod_user)) + for prod_user in ProductionUser.objects.active() + .filter(id__in=all_streams.values_list("supervisor", flat=True)) + .order_by("-user__id") + .distinct() + ], required=False, - empty_label="Any", ) 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 + for status_code_name, _ in constants.PRODUCTION_STREAM_STATUS[:-2] ], required=False, ) @@ -351,35 +374,35 @@ class ProductionStreamSearchForm(forms.Form): self.helper = FormHelper() self.helper.layout = Layout( - Div( - Div(FloatingField("identifier"), css_class="col-md-3 col-4"), - Div(FloatingField("author"), css_class="col-md-3 col-8"), - Div(FloatingField("title"), css_class="col-md-6"), - css_class="row mb-0 mt-2", - ), Div( Div( Div( - Div(Field("accepted_in", size=5), css_class="col-sm-8"), - Div(Field("proceedings", size=5), css_class="col-sm-4"), - css_class="row mb-0", - ), - Div( - Div(Field("supervisor"), css_class="col-6"), - Div(Field("officer"), css_class="col-6"), + 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( - Div(Field("orderby"), css_class="col-6"), - Div(Field("ordering"), css_class="col-6"), + 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-sm-9", + 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"), Div( - Field("status", size=len(constants.PRODUCTION_STREAM_STATUS)), - css_class="col-sm-3", + 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", ), ) @@ -392,23 +415,6 @@ class ProductionStreamSearchForm(forms.Form): for key in self.cleaned_data: session[key] = self.cleaned_data.get(key) - session["accepted_in"] = ( - [journal.id for journal in session.get("accepted_in")] - if (session.get("accepted_in")) - else [] - ) - session["proceedings"] = ( - [proceedings.id for proceedings in session.get("proceedings")] - if (session.get("proceedings")) - else [] - ) - session["officer"] = ( - officer.id if (officer := session.get("officer")) else None - ) - session["supervisor"] = ( - supervisor.id if (supervisor := session.get("supervisor")) else None - ) - session.save() streams = ProductionStream.objects.ongoing() @@ -417,13 +423,6 @@ class ProductionStreamSearchForm(forms.Form): latest_activity_annot=Greatest(Max("events__noted_on"), "opened", "closed") ) - if accepted_in := self.cleaned_data.get("accepted_in"): - streams = streams.filter( - submission__editorialdecision__for_journal__in=accepted_in, - ) - if proceedings := self.cleaned_data.get("proceedings"): - streams = streams.filter(submission__proceedings__in=proceedings) - if identifier := self.cleaned_data.get("identifier"): streams = streams.filter( submission__preprint__identifier_w_vn_nr__icontains=identifier, @@ -433,12 +432,31 @@ class ProductionStreamSearchForm(forms.Form): if title := self.cleaned_data.get("title"): streams = streams.filter(submission__title__icontains=title) - if officer := self.cleaned_data.get("officer"): - streams = streams.filter(officer=officer) - if supervisor := self.cleaned_data.get("supervisor"): - streams = streams.filter(supervisor=supervisor) - if status := self.cleaned_data.get("status"): - streams = streams.filter(status__in=status) + 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