diff --git a/lab/projects/admin.py b/lab/projects/admin.py index ea6a0479c..6a7ee44fd 100644 --- a/lab/projects/admin.py +++ b/lab/projects/admin.py @@ -17,6 +17,7 @@ from lab.models import Participation from lab.permissions import is_lab_admin, is_project_leader +from . import admin_filters from .forms import ( BaseParticipationForm, BaseProjectForm, @@ -27,36 +28,14 @@ from .models import BeamTimeRequest, Project -class ProjectStatusListFilter(admin.SimpleListFilter): - title = _("status") - - parameter_name = "status" - template = "admin/lab/project/filter.html" - - def lookups(self, request, model_admin): - return [ - (enum_member.name, enum_member.value[1]) - for enum_member in list(Project.Status) - if enum_member != Project.Status.TO_SCHEDULE - # exclude TO_SCHEDULE from filter as it is displayed in a separate table - ] - - def queryset(self, request, queryset): - if self.value(): - return queryset.filter_by_status(Project.Status[self.value()]) - return queryset - - class ProjectChangeList(ChangeList): def get_queryset(self, request: HttpRequest, exclude_parameters=None): qs = super().get_queryset(request, exclude_parameters) if request.method == "POST" and request.POST.get("action") == "delete_selected": return qs # use more general queryset for delete action - to_schedule_ids = qs.only_to_schedule().values_list( # type: ignore[attr-defined] # pylint:disable=line-too-long - "id", flat=True - ) return ( - qs.exclude(id__in=to_schedule_ids) # type: ignore[attr-defined] + # qs with projects having at least one scheduled runs + qs.filter(runs__start_date__isnull=False) # type: ignore[attr-defined] .distinct() .annotate_first_run_date() .annotate(number_of_runs=Count("runs")) @@ -238,7 +217,10 @@ class ProjectAdmin(LabPermissionMixin, ProjectDisplayMixin, ModelAdmin): search_fields = ("name",) - list_filter = [ProjectStatusListFilter] + list_filter = [ + admin_filters.ProjectStatusListFilter, + admin_filters.ProjectRunActiveEmbargoFilter, + ] list_per_page = 20 class Media: @@ -401,7 +383,7 @@ def changelist_view( # add scheduled projects on basic page (no search or pagination) to_schedule_projects = ( self.get_queryset(request) - .only_to_schedule() + .has_to_schedule_runs() .annotate( first_run_date=Value( None, diff --git a/lab/projects/admin_filters.py b/lab/projects/admin_filters.py new file mode 100644 index 000000000..0069a553a --- /dev/null +++ b/lab/projects/admin_filters.py @@ -0,0 +1,49 @@ +from django.contrib import admin +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ + +from lab.runs.models import Run + +from .models import Project + + +class ProjectStatusListFilter(admin.SimpleListFilter): + title = _("status") + + parameter_name = "status" + template = "admin/lab/project/filter.html" + + def lookups(self, request, model_admin): + return [ + (enum_member.name, enum_member.value[1]) + for enum_member in list(Project.Status) + if enum_member != Project.Status.TO_SCHEDULE + # exclude TO_SCHEDULE from filter as it is displayed in a separate table + ] + + def queryset(self, request, queryset): + if self.value(): + return queryset.filter_by_status(Project.Status[self.value()]) + return queryset + + +class ProjectRunActiveEmbargoFilter(admin.SimpleListFilter): + title = _("embargo status") + + parameter_name = "embargo" + template = "admin/lab/project/filter.html" + + def lookups(self, request, model_admin): + return [("active", _("Active")), ("over", _("Over")), ("none", _("Not set"))] + + def queryset(self, request, queryset): + if self.value(): + if self.value() == "active": + runs = Run.objects.filter(embargo_date__gte=timezone.now()) + return queryset.filter(runs__in=runs) + if self.value() == "over": + runs = Run.objects.filter(embargo_date__lt=timezone.now()) + return queryset.filter(runs__in=runs) + if self.value() == "none": + return queryset.filter(runs__embargo_date__isnull=True) + return queryset diff --git a/lab/projects/models.py b/lab/projects/models.py index d853252db..cad422f81 100644 --- a/lab/projects/models.py +++ b/lab/projects/models.py @@ -16,6 +16,9 @@ class ProjectQuerySet(models.QuerySet): + def has_to_schedule_runs(self): + return self.filter(runs__start_date__isnull=True) + def only_to_schedule(self): to_schedule_runs = Run.objects.filter(start_date__isnull=False) return self.exclude(runs__in=to_schedule_runs) diff --git a/lab/projects/tests/test_admin.py b/lab/projects/tests/test_admin.py index deefa9706..81036be30 100644 --- a/lab/projects/tests/test_admin.py +++ b/lab/projects/tests/test_admin.py @@ -426,3 +426,26 @@ def test_no_to_schedule_projects_in_ctx_when_paginated_results(self): assert ( len(cl_view.context_data["extra_qs"]) == 0 ), f"Test failed for query {url_query_hiding_qs}" + + def test_scheduled_project_in_to_schedule_qs_when_any_to_schedule_run(self): + project_both_scheduled_and_not = ProjectFactory() + RunFactory(project=project_both_scheduled_and_not, start_date=timezone.now()) + RunFactory(project=project_both_scheduled_and_not, start_date=None) + + project_scheduled = ProjectFactory() + RunFactory(project=project_scheduled, start_date=timezone.now()) + + project_not_scheduled = ProjectFactory() + RunFactory(project=project_not_scheduled, start_date=None) + + cl_view: TemplateResponse = self.admin.changelist_view(self.request) + + to_schedule_qs = cl_view.context_data["extra_qs"][0]["qs"] + scheduled_qs = cl_view.context_data["cl"].queryset + + assert to_schedule_qs.count() == 2 + assert project_both_scheduled_and_not in to_schedule_qs.all() + assert project_not_scheduled in to_schedule_qs.all() + assert scheduled_qs.count() == 2 + assert project_both_scheduled_and_not in scheduled_qs.all() + assert project_scheduled in scheduled_qs.all() diff --git a/lab/templates/admin/lab/run/change_form.html b/lab/templates/admin/lab/run/change_form.html index 47ce5c2cc..aaa611f05 100644 --- a/lab/templates/admin/lab/run/change_form.html +++ b/lab/templates/admin/lab/run/change_form.html @@ -27,18 +27,20 @@

{{ original.label }}

- {% if original.start_date %} + {% if original.start_date or original.embargo_date %}
+ {% if original.start_date %}

{% translate "Start" %}
{{ original.start_date|date:"SHORT_DATE_FORMAT" }} {{ original.start_date|time:"H:i" }}

+ {% endif %} {% if original.end_date %}

{% translate "End" %}
{{ original.end_date|date:"SHORT_DATE_FORMAT" }} {{ original.end_date|time:"H:i" }}{% endif %}

{% if request.user.is_lab_admin %} - +

{% translate "Embargo" %}
{% if original.embargo_date %}{{ original.embargo_date|date:"SHORT_DATE_FORMAT" }}{% else %}{% translate "No embargo date" %}{% endif %}

{% endif %} diff --git a/lab/templates/admin/lab/run/run_list.html b/lab/templates/admin/lab/run/run_list.html index e66c9a304..b1f8f7901 100644 --- a/lab/templates/admin/lab/run/run_list.html +++ b/lab/templates/admin/lab/run/run_list.html @@ -22,7 +22,7 @@

-
+

{% translate "Start" %}
{% if run.start_date %}{{ run.start_date|date:"SHORT_DATE_FORMAT" }} {{ run.start_date|time:"H:i" }}{% else %}-{% endif %} @@ -33,6 +33,11 @@

+

+ {% translate "Embargo date" %} +
{% if run.embargo_date %}{{ run.embargo_date|date:"SHORT_DATE_FORMAT" }}{% else %}-{% endif %} +

+
{% translate "Experimental conditions" %}

diff --git a/locale/fr/LC_MESSAGES/django.mo b/locale/fr/LC_MESSAGES/django.mo index 249c8a318..527adc1ee 100644 Binary files a/locale/fr/LC_MESSAGES/django.mo and b/locale/fr/LC_MESSAGES/django.mo differ diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 99791af93..26cc4c5ff 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-05-02 12:14+0200\n" +"POT-Creation-Date: 2024-07-18 16:55+0200\n" "PO-Revision-Date: 2021-09-09 19:04+0200\n" "Language: \n" "MIME-Version: 1.0\n" @@ -468,9 +468,6 @@ msgstr "nom" msgid "country" msgstr "pays" -msgid "status" -msgstr "statut" - msgid "Project member" msgstr "Membre du projet" @@ -514,6 +511,21 @@ msgstr "Projets" msgid "To schedule" msgstr "A planifier" +msgid "status" +msgstr "statut" + +msgid "embargo status" +msgstr "statut de l'embargo" + +msgid "Active" +msgstr "Actif" + +msgid "Over" +msgstr "Terminé" + +msgid "Not set" +msgstr "Non défini" + msgid "I have read and accepted the general conditions of Euphrosyne." msgstr "J'ai lu et accepté les conditions générales d'Euphrosyne."