diff --git a/src/genlab_bestilling/libs/genlabid.py b/src/genlab_bestilling/libs/genlabid.py index 32af0235..1616c471 100644 --- a/src/genlab_bestilling/libs/genlabid.py +++ b/src/genlab_bestilling/libs/genlabid.py @@ -63,7 +63,11 @@ def get_current_sequences(order_id: int | str) -> Any: return sequences -def generate(order_id: int | str) -> None: +def generate( + order_id: int | str, + sorting_order: list[str] | None = None, + selected_samples: list[Any] | None = None, +) -> None: """ wrapper to handle errors and reset the sequence to the current sequence value """ diff --git a/src/genlab_bestilling/models.py b/src/genlab_bestilling/models.py index 4e5bff44..5a86aa2f 100644 --- a/src/genlab_bestilling/models.py +++ b/src/genlab_bestilling/models.py @@ -1,9 +1,10 @@ import uuid from datetime import timedelta -from typing import Any +from typing import TYPE_CHECKING, Any from django.conf import settings from django.db import models, transaction +from django.db.models import QuerySet from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext_lazy as _ @@ -12,6 +13,9 @@ from rest_framework.exceptions import ValidationError from taggit.managers import TaggableManager +if TYPE_CHECKING: + from .models import Sample + from . import managers from .libs.helpers import position_to_coordinates @@ -301,6 +305,10 @@ def to_draft(self) -> None: def get_type(self) -> str: return "order" + @property + def filled_genlab_count(self) -> int: + return self.samples.filter(genlab_id__isnull=False).count() + @property def next_status(self) -> OrderStatus | None: current_index = self.STATUS_ORDER.index(self.status) @@ -461,6 +469,27 @@ def order_manually_checked(self) -> None: self.save() app.configure_task(name="generate-genlab-ids").defer(order_id=self.id) + def order_selected_checked( + self, + sorting_order: list[str] | None = None, + selected_samples: QuerySet["Sample"] | None = None, + ) -> None: + """ + Partially set the order as checked by the lab staff, + generate a genlab id for the samples selected + """ + self.internal_status = self.Status.CHECKED + self.status = self.OrderStatus.PROCESSING + self.save() + + selected_sample_names = list(selected_samples.values_list("id", flat=True)) + + app.configure_task(name="generate-genlab-ids").defer( + order_id=self.id, + sorting_order=sorting_order, + selected_samples=selected_sample_names, + ) + class AnalysisOrder(Order): samples = models.ManyToManyField( diff --git a/src/genlab_bestilling/tasks.py b/src/genlab_bestilling/tasks.py index 607f301b..d92b0a78 100644 --- a/src/genlab_bestilling/tasks.py +++ b/src/genlab_bestilling/tasks.py @@ -1,4 +1,6 @@ # from .libs.isolation import isolate +from typing import Any + from django.db.utils import OperationalError from procrastinate import RetryStrategy from procrastinate.contrib.django import app @@ -12,6 +14,14 @@ max_attempts=5, linear_wait=5, retry_exceptions={OperationalError} ), ) -def generate_ids(order_id: str | int) -> None: - generate_genlab_id(order_id=order_id) +def generate_ids( + order_id: int | str, + sorting_order: list[str] | None = None, + selected_samples: list[Any] | None = None, +) -> None: + generate_genlab_id( + order_id=order_id, + sorting_order=sorting_order, + selected_samples=selected_samples, + ) # isolate(order_id=order_id) diff --git a/src/staff/tables.py b/src/staff/tables.py index f5f8575d..ebb149fd 100644 --- a/src/staff/tables.py +++ b/src/staff/tables.py @@ -1,6 +1,8 @@ from typing import Any import django_tables2 as tables +from django.db.models import IntegerField +from django.db.models.functions import Cast from django.utils.safestring import mark_safe from genlab_bestilling.models import ( @@ -139,6 +141,17 @@ class SampleBaseTable(tables.Table): verbose_name="", ) + checked = tables.CheckBoxColumn( + attrs={ + "th__input": {"type": "checkbox", "id": "select-all-checkbox"}, + }, + accessor="pk", + orderable=False, + empty_values=(), + ) + + name = tables.Column(order_by=("name_as_int",)) + class Meta: model = Sample fields = [ @@ -154,17 +167,41 @@ class Meta: "plate_positions", ] attrs = {"class": "w-full table-auto tailwind-table table-sm"} - sequence = ("is_prioritised", "genlab_id", "guid", "name", "species", "type") - order_by = ("-is_prioritised", "species", "genlab_id", "name") + sequence = ( + "checked", + "is_prioritised", + "genlab_id", + "guid", + "name", + "species", + "type", + ) + order_by = ( + "-is_prioritised", + "species", + "genlab_id", + "name_as_int", + ) empty_text = "No Samples" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if hasattr(self.data, "data"): + self.data.data = self.data.data.annotate( + name_as_int=Cast("name", output_field=IntegerField()) + ) + def render_plate_positions(self, value: Any) -> str: if value: return ", ".join([str(v) for v in value.all()]) return "" + def render_checked(self, record: Any) -> str: + return mark_safe(f'') # noqa: S308 + def create_sample_table(base_fields: list[str] | None = None) -> type[tables.Table]: class CustomSampleTable(tables.Table): diff --git a/src/staff/templates/staff/analysisorder_filter.html b/src/staff/templates/staff/analysisorder_filter.html index 0a0d6946..a0ac89a4 100644 --- a/src/staff/templates/staff/analysisorder_filter.html +++ b/src/staff/templates/staff/analysisorder_filter.html @@ -1,5 +1,19 @@ {% extends "staff/base_filter.html" %} +{% load crispy_forms_tags static %} +{% load render_table from django_tables2 %} {% block page-title %} Analysis Orders {% endblock page-title %} + +{% block page-inner %} +
+
+ {{ filter.form | crispy }} +
+ +
+ +{% render_table table %} + +{% endblock page-inner %} diff --git a/src/staff/templates/staff/base_filter.html b/src/staff/templates/staff/base_filter.html index 66b774cf..96f0cd28 100644 --- a/src/staff/templates/staff/base_filter.html +++ b/src/staff/templates/staff/base_filter.html @@ -5,19 +5,23 @@ {% block content %}

{% block page-title %}{% endblock page-title %}

{% block page-inner %}{% endblock page-inner %} - -
-
- {{ filter.form | crispy }} -
- -
- - {% render_table table %} {% endblock %} {% block body_javascript %} {{ block.super }} {{ filter.form.media }} + {% endblock body_javascript %} diff --git a/src/staff/templates/staff/equipmentorder_filter.html b/src/staff/templates/staff/equipmentorder_filter.html index 25b2704d..434f0c13 100644 --- a/src/staff/templates/staff/equipmentorder_filter.html +++ b/src/staff/templates/staff/equipmentorder_filter.html @@ -1,5 +1,19 @@ {% extends "staff/base_filter.html" %} +{% load crispy_forms_tags static %} +{% load render_table from django_tables2 %} {% block page-title %} Equipment Orders {% endblock page-title %} + +{% block page-inner %} +
+
+ {{ filter.form | crispy }} +
+ +
+ +{% render_table table %} + +{% endblock page-inner %} diff --git a/src/staff/templates/staff/extractionorder_filter.html b/src/staff/templates/staff/extractionorder_filter.html index f0ff30b7..e2c1e8d9 100644 --- a/src/staff/templates/staff/extractionorder_filter.html +++ b/src/staff/templates/staff/extractionorder_filter.html @@ -1,5 +1,19 @@ {% extends "staff/base_filter.html" %} +{% load crispy_forms_tags static %} +{% load render_table from django_tables2 %} {% block page-title %} Extraction Orders {% endblock page-title %} + +{% block page-inner %} +
+
+ {{ filter.form | crispy }} +
+ +
+ +{% render_table table %} + +{% endblock page-inner %} diff --git a/src/staff/templates/staff/extractionplate_filter.html b/src/staff/templates/staff/extractionplate_filter.html index 877fca41..26e046dd 100644 --- a/src/staff/templates/staff/extractionplate_filter.html +++ b/src/staff/templates/staff/extractionplate_filter.html @@ -1,4 +1,6 @@ {% extends "staff/base_filter.html" %} +{% load crispy_forms_tags static %} +{% load render_table from django_tables2 %} {% block page-title %} Extraction plates @@ -8,4 +10,13 @@
Create
+ +
+
+ {{ filter.form | crispy }} +
+ +
+ +{% render_table table %} {% endblock page-inner %} diff --git a/src/staff/templates/staff/project_filter.html b/src/staff/templates/staff/project_filter.html index 22279374..b0434b4c 100644 --- a/src/staff/templates/staff/project_filter.html +++ b/src/staff/templates/staff/project_filter.html @@ -1,5 +1,19 @@ {% extends "staff/base_filter.html" %} +{% load crispy_forms_tags static %} +{% load render_table from django_tables2 %} {% block page-title %} Projects {% endblock page-title %} + +{% block page-inner %} +
+
+ {{ filter.form | crispy }} +
+ +
+ +{% render_table table %} + +{% endblock page-inner %} diff --git a/src/staff/templates/staff/sample_filter.html b/src/staff/templates/staff/sample_filter.html index 221b70d9..55f3d5d8 100644 --- a/src/staff/templates/staff/sample_filter.html +++ b/src/staff/templates/staff/sample_filter.html @@ -1,7 +1,22 @@ {% extends "staff/base_filter.html" %} +{% load crispy_forms_tags static %} +{% load render_table from django_tables2 %} {% block page-title %} -{% if order %}{{ order }} - Samples{% else %}Samples{% endif %} +
+
+ {% if order %} + {{ order }} - Samples + {% else %} + Samples + {% endif %} +
+ {% if order %} +
+ {{ order.filled_genlab_count }} / {{ order.samples.count }} Genlabs generated +
+ {% endif %} +
{% endblock page-title %} {% block page-inner %} @@ -11,5 +26,17 @@ Download CSV Lab + +
+ {% csrf_token %} + + + +
This page is under development. The genlab IDs will generate for all, and without sorting as per now.
+ + {% render_table table %} +
{% endif %} {% endblock page-inner %} diff --git a/src/staff/templates/staff/samplemarkeranalysis_filter.html b/src/staff/templates/staff/samplemarkeranalysis_filter.html index 4f81078d..227e0a18 100644 --- a/src/staff/templates/staff/samplemarkeranalysis_filter.html +++ b/src/staff/templates/staff/samplemarkeranalysis_filter.html @@ -1,4 +1,6 @@ {% extends "staff/base_filter.html" %} +{% load crispy_forms_tags static %} +{% load render_table from django_tables2 %} {% block page-title %} {% if order %}{{ order }} - Samples{% else %}Samples{% endif %} @@ -10,4 +12,14 @@ back {% endif %} + +
+
+ {{ filter.form | crispy }} +
+ +
+ +{% render_table table %} + {% endblock page-inner %} diff --git a/src/staff/urls.py b/src/staff/urls.py index 28be320b..6dc62299 100644 --- a/src/staff/urls.py +++ b/src/staff/urls.py @@ -11,6 +11,7 @@ ExtractionPlateCreateView, ExtractionPlateDetailView, ExtractionPlateListView, + GenerateGenlabIDsView, ManaullyCheckedOrderActionView, OrderAnalysisSamplesListView, OrderExtractionSamplesListView, @@ -85,6 +86,11 @@ SampleLabView.as_view(), name="order-extraction-samples-lab", ), + path( + "orders/extraction//samples/generate-genlab-ids/", + GenerateGenlabIDsView.as_view(), + name="generate-genlab-ids", + ), path( "orders/analysis//samples/", OrderAnalysisSamplesListView.as_view(), diff --git a/src/staff/views.py b/src/staff/views.py index 9292a26b..dbcddfdd 100644 --- a/src/staff/views.py +++ b/src/staff/views.py @@ -381,11 +381,7 @@ def form_valid(self, form: Form) -> HttpResponse: _("The order was checked, GenLab IDs will be generated"), ) except Exception as e: - messages.add_message( - self.request, - messages.ERROR, - f"Error: {str(e)}", - ) + messages.error(self.request, f"Error: {str(e)}") return super().form_valid(form) @@ -513,6 +509,54 @@ def form_invalid(self, form: Form) -> HttpResponse: return HttpResponseRedirect(self.get_success_url()) +class GenerateGenlabIDsView( + SingleObjectMixin, StaffMixin, SingleTableMixin, FilterView +): + model = ExtractionOrder + + def get_object(self) -> ExtractionOrder: + return ExtractionOrder.objects.get(pk=self.kwargs["pk"]) + + def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + self.object = self.get_object() + selected_ids = request.POST.getlist("checked") + + if not selected_ids: + messages.error(request, "No samples were selected.") + return HttpResponseRedirect(self.get_return_url()) + + sort_param = request.POST.get("sort", "") + sorting_order = [s.strip() for s in sort_param.split(",") if s.strip()] + + selected_samples = Sample.objects.filter(pk__in=selected_ids) + + if sorting_order: + selected_samples = selected_samples.order_by(*sorting_order) + + try: + self.object.order_selected_checked( + sorting_order=sorting_order, selected_samples=selected_samples + ) + messages.add_message( + request, + messages.SUCCESS, + _(f"Genlab IDs generated for {selected_samples.count()} samples."), + ) + except Exception as e: + messages.add_message( + request, + messages.ERROR, + f"Error: {str(e)}", + ) + + return HttpResponseRedirect(self.get_return_url()) + + def get_return_url(self) -> str: + return reverse_lazy( + "staff:order-extraction-samples", kwargs={"pk": self.object.pk} + ) + + class ExtractionPlateCreateView(StaffMixin, CreateView): model = ExtractionPlate form_class = ExtractionPlateForm