diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4061410b..3e3c6eab 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,12 +7,12 @@ env:
on:
pull_request:
- branches: ['main', 'summer25']
- paths-ignore: ['docs/**']
+ branches: ["main", "summer25", "summer25-*"]
+ paths-ignore: ["docs/**"]
push:
- branches: ['main', 'summer25']
- paths-ignore: ['docs/**']
+ branches: ["main", "summer25", "summer25-*"]
+ paths-ignore: ["docs/**"]
concurrency:
group: ${{ github.head_ref || github.run_id }}
@@ -28,7 +28,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
- python-version: '3.11'
+ python-version: "3.11"
# Consider using pre-commit.ci for open source project
- name: Run pre-commit
uses: pre-commit/action@v3.0.0
diff --git a/.gitignore b/.gitignore
index b5fac182..b48164c8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -335,3 +335,6 @@ staticfiles/
# Local History for devcontainer
.devcontainer/bash_history
+
+### macOS ###
+.DS_Store
diff --git a/src/config/autocomplete.py b/src/config/autocomplete.py
index 72e981c0..9fd60470 100644
--- a/src/config/autocomplete.py
+++ b/src/config/autocomplete.py
@@ -12,6 +12,7 @@
OrderAutocomplete,
SampleTypeAutocomplete,
SpeciesAutocomplete,
+ StatusAutocomplete,
)
from nina.autocomplete import ProjectAutocomplete
@@ -20,6 +21,7 @@
path("area/", AreaAutocomplete.as_view(), name="area"),
path("species/", SpeciesAutocomplete.as_view(), name="species"),
path("sample-type/", SampleTypeAutocomplete.as_view(), name="sample-type"),
+ path("order-status/", StatusAutocomplete.as_view(), name="order-status"),
path("project/", ProjectAutocomplete.as_view(), name="project"),
path("marker/", MarkerAutocomplete.as_view(), name="marker"),
path("user/", UserAutocomplete.as_view(), name="user"),
diff --git a/src/genlab_bestilling/admin.py b/src/genlab_bestilling/admin.py
index a9a7839f..227e6064 100644
--- a/src/genlab_bestilling/admin.py
+++ b/src/genlab_bestilling/admin.py
@@ -19,10 +19,10 @@
Location,
LocationType,
Marker,
+ Order,
Organization,
Sample,
SampleMarkerAnalysis,
- SampleStatusAssignment,
SampleType,
Species,
)
@@ -48,6 +48,13 @@ class AreaAdmin(ModelAdmin):
list_filter_sheet = False
+@admin.register(Order)
+class OrderAdmin(admin.ModelAdmin):
+ list_display = ["id", "name", "status", "created_at"]
+ list_filter = ["status"]
+ search_fields = ["id", "name"]
+
+
@admin.register(LocationType)
class LocationTypeAdmin(ModelAdmin):
search_fields = ["name"]
@@ -526,9 +533,5 @@ class AnalysisResultAdmin(ModelAdmin):
]
-@admin.register(SampleStatusAssignment)
-class SampleStatusAssignmentAdmin(ModelAdmin): ...
-
-
@admin.register(IsolationMethod)
class IsolationMethodAdmin(ModelAdmin): ...
diff --git a/src/genlab_bestilling/api/serializers.py b/src/genlab_bestilling/api/serializers.py
index d8d285d3..e47cfb43 100644
--- a/src/genlab_bestilling/api/serializers.py
+++ b/src/genlab_bestilling/api/serializers.py
@@ -1,7 +1,3 @@
-from collections.abc import Mapping
-from typing import Any
-
-from django.forms import Field
from rest_framework import exceptions, serializers
from ..models import (
@@ -103,10 +99,15 @@ class SampleCSVSerializer(serializers.ModelSerializer):
species = SpeciesSerializer()
location = LocationSerializer(allow_null=True, required=False)
fish_id = serializers.SerializerMethodField()
+ analysis_orders = serializers.SerializerMethodField()
+ project = serializers.SerializerMethodField()
+ isolation_method = serializers.SerializerMethodField()
+ marked = serializers.SerializerMethodField()
+ plucked = serializers.SerializerMethodField()
+ isolated = serializers.SerializerMethodField()
class Meta:
model = Sample
- # Make fields as a list to enable the removal of fish_id dynamically
fields = [
"order",
"guid",
@@ -119,20 +120,43 @@ class Meta:
"notes",
"genlab_id",
"fish_id",
+ "analysis_orders",
+ "project",
+ "isolation_method",
+ "marked",
+ "plucked",
+ "isolated",
]
- def get_field_names(
- self, declared_fields: Mapping[str, Field], info: Any
- ) -> list[str]:
- field_names = super().get_field_names(declared_fields, info)
- if not self.context.get("include_fish_id", False):
- # Remove fish_id if the area is not aquatic (only relevant for aquatic area)
- field_names.remove("fish_id")
- return field_names
-
def get_fish_id(self, obj: Sample) -> str:
return obj.fish_id or "-"
+ def get_analysis_orders(self, obj: Sample) -> list[str]:
+ if obj.order and obj.order.analysis_orders.exists():
+ return [str(anl.id) for anl in obj.order.analysis_orders.all()]
+ return []
+
+ def get_project(self, obj: Sample) -> str:
+ if obj.order and obj.order.genrequest and obj.order.genrequest.project:
+ return str(obj.order.genrequest.project)
+ return ""
+
+ def get_isolation_method(self, obj: Sample) -> str:
+ method = obj.isolation_method.first()
+ return method.name if method else ""
+
+ def _flag(self, value: bool) -> str:
+ return "x" if value else ""
+
+ def get_marked(self, obj: Sample) -> str:
+ return self._flag(obj.is_marked)
+
+ def get_plucked(self, obj: Sample) -> str:
+ return self._flag(obj.is_plucked)
+
+ def get_isolated(self, obj: Sample) -> str:
+ return self._flag(obj.is_isolated)
+
class SampleUpdateSerializer(serializers.ModelSerializer):
has_error = serializers.SerializerMethodField()
diff --git a/src/genlab_bestilling/api/views.py b/src/genlab_bestilling/api/views.py
index 7f914bfc..9ea27c65 100644
--- a/src/genlab_bestilling/api/views.py
+++ b/src/genlab_bestilling/api/views.py
@@ -1,8 +1,8 @@
import uuid
-from typing import Any
from django.db import transaction
from django.db.models import QuerySet
+from django.http import HttpResponse
from django.views import View
from drf_spectacular.utils import extend_schema
from rest_framework.decorators import action
@@ -77,6 +77,115 @@ class SampleViewset(ModelViewSet):
pagination_class = IDCursorPagination
permission_classes = [AllowSampleDraft, IsAuthenticated]
+ CSV_FIELD_LABELS: dict[str, str] = {
+ "genlab_id": "Genlab ID",
+ "fish_id": "Old Genlab ID",
+ "guid": "GUID",
+ "name": "Name",
+ "species.name": "Species",
+ "location.name": "Location",
+ "order": "EXT_order",
+ "analysis_orders": "ANL_order",
+ "pop_id": "PopID",
+ "type.name": "Sample Type",
+ "gender": "Gender",
+ "length": "Length",
+ "weight": "Weight",
+ "classification": "Classification",
+ "year": "Date",
+ "notes": "Remarks",
+ "project": "Projectnumber",
+ "isolation_method": "Isolation Method",
+ "qiagen_number": "Qiagen#",
+ "is_marked": "Marked",
+ "is_plucked": "Plucked",
+ "is_isolated": "Isolated",
+ "station": "Station",
+ "placement_in_fridge": "Placement in fridge",
+ "delivered_to_lab": "Delivered to lab",
+ }
+
+ # NOTE: This can be modified to include more fields based on species or area.
+ CSV_FIELDS_BY_AREA: dict[str, list[str]] = {
+ "Akvatisk": [
+ "genlab_id",
+ "fish_id",
+ "guid",
+ "order",
+ "analysis_orders",
+ "location.name",
+ "pop_id",
+ "name",
+ "species.name",
+ "gender",
+ "length",
+ "weight",
+ "classification",
+ "year",
+ "notes",
+ "project",
+ "type.name",
+ "isolation_method",
+ "qiagen_number",
+ "is_marked",
+ "is_plucked",
+ "is_isolated",
+ ],
+ "Elvemusling": [
+ "genlab_id",
+ "fish_id",
+ "guid",
+ "location.name",
+ "year",
+ "name",
+ "station",
+ "type.name",
+ "length",
+ "notes",
+ "isolation_method",
+ "qiagen_number",
+ "placement_in_fridge",
+ "is_marked",
+ "is_plucked",
+ "is_isolated",
+ ],
+ "Terrestrisk": [
+ "genlab_id",
+ "guid",
+ "name",
+ "type.name",
+ "species.name",
+ "location.name",
+ "delivered_to_lab",
+ "order",
+ "analysis_orders",
+ "notes",
+ "is_marked",
+ "is_plucked",
+ "is_isolated",
+ "isolation_method",
+ "qiagen_number",
+ ],
+ # Same as "Terrestrisk" for now, can be modified later if needed.
+ "default": [
+ "genlab_id",
+ "guid",
+ "name",
+ "type.name",
+ "species.name",
+ "location.name",
+ "delivered_to_lab",
+ "order",
+ "analysis_orders",
+ "notes",
+ "is_marked",
+ "is_plucked",
+ "is_isolated",
+ "isolation_method",
+ "qiagen_number",
+ ],
+ }
+
def get_queryset(self) -> QuerySet:
return (
super()
@@ -89,7 +198,7 @@ def get_queryset(self) -> QuerySet:
"order__genrequest__area",
"location",
)
- .order_by("id")
+ .order_by("genlab_id", "type")
)
def get_serializer_class(self) -> type[BaseSerializer]:
@@ -99,21 +208,78 @@ def get_serializer_class(self) -> type[BaseSerializer]:
return SampleCSVSerializer
return super().get_serializer_class()
- def get_serializer_context(self, *args, **kwargs) -> dict[str, Any]:
- context = super().get_serializer_context(*args, **kwargs)
- queryset = self.filter_queryset(self.get_queryset())
- is_aquatic = queryset.filter(order__genrequest__area__name="Akvatisk").exists()
- context["include_fish_id"] = is_aquatic
- return context
+ def get_area_name(self, queryset: QuerySet) -> str:
+ return (
+ queryset.values_list("order__genrequest__area__name", flat=True).first()
+ or "default"
+ )
+
+ # NOTE: If the headers differ from species to species, we can add more headers
+ # to the CSV_FIELDS_BY_AREA dict, and then use the species name to determine
+ # which headers to use.
+ def get_csv_fields_and_labels(
+ self, area_name: str, queryset: QuerySet
+ ) -> tuple[list[str], list[str]]:
+ get_fields = area_name
+ if area_name == "Akvatisk":
+ species = queryset.values_list("species__name", flat=True).distinct()
+ if species.first() == "Elvemusling":
+ get_fields = "Elvemusling"
+
+ fields = self.CSV_FIELDS_BY_AREA.get(
+ get_fields, self.CSV_FIELDS_BY_AREA["default"]
+ )
+ labels = [self.CSV_FIELD_LABELS.get(f, f) for f in fields]
+ return fields, labels
+
+ # Helper function to get nested values from a dict using dotted notation.
+ def get_nested(self, obj: dict, dotted: str) -> str | None:
+ for part in dotted.split("."):
+ obj = obj.get(part) if isinstance(obj, dict) else None
+ return obj
+
+ def build_csv_data(
+ self, serialized_data: list[dict], fields: list[str]
+ ) -> list[dict[str, str]]:
+ return [
+ {
+ self.CSV_FIELD_LABELS[f]: (
+ ", ".join(v)
+ if isinstance(v := self.get_nested(item, f), list)
+ else v or ""
+ )
+ for f in fields
+ }
+ for item in serialized_data
+ ]
@action(
- methods=["GET"], url_path="csv", detail=False, renderer_classes=[CSVRenderer]
+ methods=["GET"],
+ url_path="csv",
+ detail=False,
+ renderer_classes=[CSVRenderer],
)
- def csv(self, request: Request) -> Response:
+ def csv(self, request: Request) -> HttpResponse:
queryset = self.filter_queryset(self.get_queryset())
- serializer = self.get_serializer(queryset, many=True)
- return Response(
- serializer.data,
+ area_name = self.get_area_name(queryset)
+
+ serializer = self.get_serializer(
+ queryset,
+ many=True,
+ )
+
+ fields, headers = self.get_csv_fields_and_labels(area_name, queryset)
+ data = self.build_csv_data(serializer.data, fields)
+
+ csv_data = CSVRenderer().render(
+ data,
+ media_type="text/csv",
+ renderer_context={"header": headers},
+ )
+
+ return HttpResponse(
+ csv_data,
+ content_type="text/csv; charset=utf-8",
headers={"Content-Disposition": "attachment; filename=samples.csv"},
)
diff --git a/src/genlab_bestilling/autocomplete.py b/src/genlab_bestilling/autocomplete.py
index 120d2cd3..2a3bdb63 100644
--- a/src/genlab_bestilling/autocomplete.py
+++ b/src/genlab_bestilling/autocomplete.py
@@ -1,4 +1,5 @@
from dal import autocomplete
+from django.http import HttpRequest, JsonResponse
from .models import (
AnalysisOrder,
@@ -18,6 +19,17 @@ class AreaAutocomplete(autocomplete.Select2QuerySetView):
model = Area
+class StatusAutocomplete(autocomplete.Select2QuerySetView):
+ def get(self, request: "HttpRequest", *args, **kwargs) -> JsonResponse:
+ term = request.GET.get("q", "").lower()
+ results = [
+ {"id": choice[0], "text": choice[1]}
+ for choice in Order.OrderStatus.choices
+ if term in choice[1].lower()
+ ]
+ return JsonResponse({"results": results})
+
+
class SpeciesAutocomplete(autocomplete.Select2QuerySetView):
model = Species
diff --git a/src/genlab_bestilling/managers.py b/src/genlab_bestilling/managers.py
index 9b2b2319..987b2b5d 100644
--- a/src/genlab_bestilling/managers.py
+++ b/src/genlab_bestilling/managers.py
@@ -4,7 +4,6 @@
from django.db import models, transaction
from django.db.models import QuerySet
-from django.db.models.expressions import RawSQL
from polymorphic.managers import PolymorphicManager, PolymorphicQuerySet
from capps.users.models import User
@@ -54,9 +53,6 @@ def filter_in_draft(self) -> QuerySet:
)
-DEFAULT_SORTING_FIELDS = ["name_as_int", "name"]
-
-
class SampleQuerySet(models.QuerySet):
def filter_allowed(self, user: User) -> QuerySet:
"""
@@ -76,7 +72,6 @@ def filter_in_draft(self) -> QuerySet:
def generate_genlab_ids(
self,
order_id: int,
- sorting_order: list[str] | None = DEFAULT_SORTING_FIELDS,
selected_samples: list[int] | None = None,
) -> None:
"""
@@ -84,28 +79,23 @@ def generate_genlab_ids(
"""
- # Lock the samples
- samples = (
- self.select_related("species")
- .filter(order_id=order_id, genlab_id__isnull=True)
- .select_for_update()
- )
+ selected_sample_ids = [int(s) for s in selected_samples]
- if selected_samples:
- samples = samples.filter(id__in=selected_samples)
-
- if sorting_order == DEFAULT_SORTING_FIELDS:
- # create an annotation containg all integer values
- # of "name", so that it's possible to sort numerically and alphabetically
- samples = samples.annotate(
- name_as_int=RawSQL(
- r"substring(%s from '^\d+$')::int",
- params=["name"],
- output_field=models.IntegerField(),
+ samples = list(
+ (
+ self.select_related("species")
+ .filter(
+ order_id=order_id,
+ genlab_id__isnull=True,
+ id__in=selected_sample_ids,
)
- ).order_by(*sorting_order)
- else:
- samples = samples.order_by(*sorting_order)
+ .select_for_update()
+ ).all()
+ )
+
+ # Sort samples in the order of selected_samples
+ id_pos = {id_: i for i, id_ in enumerate(selected_sample_ids)}
+ samples.sort(key=lambda sample: id_pos.get(sample.id, 99999)) # Safe fallback
updates = []
for sample in samples:
diff --git a/src/genlab_bestilling/migrations/0027_alter_isolationmethod_name.py b/src/genlab_bestilling/migrations/0027_alter_isolationmethod_name.py
new file mode 100644
index 00000000..80790b0a
--- /dev/null
+++ b/src/genlab_bestilling/migrations/0027_alter_isolationmethod_name.py
@@ -0,0 +1,17 @@
+# Generated by Django 5.2.3 on 2025-07-15 07:00
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("genlab_bestilling", "0026_alter_samplestatusassignment_status_and_more"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="isolationmethod",
+ name="name",
+ field=models.CharField(max_length=255),
+ ),
+ ]
diff --git a/src/genlab_bestilling/migrations/0028_sample_is_isolated_sample_is_marked_and_more.py b/src/genlab_bestilling/migrations/0028_sample_is_isolated_sample_is_marked_and_more.py
new file mode 100644
index 00000000..7204ccb1
--- /dev/null
+++ b/src/genlab_bestilling/migrations/0028_sample_is_isolated_sample_is_marked_and_more.py
@@ -0,0 +1,30 @@
+# Generated by Django 5.2.3 on 2025-07-17 08:16
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("genlab_bestilling", "0027_alter_isolationmethod_name"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="sample",
+ name="is_isolated",
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name="sample",
+ name="is_marked",
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name="sample",
+ name="is_plucked",
+ field=models.BooleanField(default=False),
+ ),
+ migrations.DeleteModel(
+ name="SampleStatusAssignment",
+ ),
+ ]
diff --git a/src/genlab_bestilling/models.py b/src/genlab_bestilling/models.py
index 8211e20f..747faa89 100644
--- a/src/genlab_bestilling/models.py
+++ b/src/genlab_bestilling/models.py
@@ -4,7 +4,6 @@
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 _
@@ -320,9 +319,14 @@ def clone(self) -> None:
def to_draft(self) -> None:
self.status = Order.OrderStatus.DRAFT
+ self.is_seen = False
self.confirmed_at = None
self.save()
+ def to_processing(self) -> None:
+ self.status = Order.OrderStatus.PROCESSING
+ self.save()
+
def toggle_seen(self) -> None:
self.is_seen = not self.is_seen
self.save()
@@ -495,19 +499,10 @@ def confirm_order(self, persist: bool = True) -> None:
if persist:
super().confirm_order()
- def order_manually_checked(self) -> None:
- """
- Set the order as checked by the lab staff, generate a genlab id
- """
- self.internal_status = self.Status.CHECKED
- self.status = self.OrderStatus.PROCESSING
- self.save(update_fields=["internal_status", "status"])
-
@transaction.atomic
def order_selected_checked(
self,
- sorting_order: list[str] | None = None,
- selected_samples: QuerySet["Sample"] | None = None,
+ selected_samples: list[int] | None = None,
) -> None:
"""
Partially set the order as checked by the lab staff,
@@ -517,12 +512,11 @@ def order_selected_checked(
self.status = self.OrderStatus.PROCESSING
self.save(update_fields=["internal_status", "status"])
- if not selected_samples.exists():
+ if not selected_samples:
return
Sample.objects.generate_genlab_ids(
order_id=self.id,
- sorting_order=sorting_order,
selected_samples=selected_samples,
)
@@ -646,6 +640,10 @@ class Sample(models.Model):
year = models.IntegerField()
notes = models.TextField(null=True, blank=True)
+ is_marked = models.BooleanField(default=False)
+ is_plucked = models.BooleanField(default=False)
+ is_isolated = models.BooleanField(default=False)
+
# "Merknad" in the Excel sheet.
internal_note = models.TextField(null=True, blank=True)
pop_id = models.CharField(max_length=150, null=True, blank=True)
@@ -778,38 +776,6 @@ def generate_genlab_id(self, commit: bool = True) -> str:
# assignee (one or plus?)
-class SampleStatusAssignment(models.Model):
- class SampleStatus(models.TextChoices):
- MARKED = "marked", _("Marked")
- PLUCKED = "plucked", _("Plucked")
- ISOLATED = "isolated", _("Isolated")
-
- sample = models.ForeignKey(
- f"{an}.Sample",
- on_delete=models.CASCADE,
- related_name="sample_status_assignments",
- )
- status = models.CharField(
- choices=SampleStatus.choices,
- null=True,
- blank=True,
- verbose_name="Sample status",
- help_text="The status of the sample in the lab",
- )
- order = models.ForeignKey(
- f"{an}.Order",
- on_delete=models.CASCADE,
- related_name="sample_status_assignments",
- null=True,
- blank=True,
- )
-
- assigned_at = models.DateTimeField(auto_now_add=True)
-
- class Meta:
- unique_together = ("sample", "status", "order")
-
-
class SampleIsolationMethod(models.Model):
sample = models.ForeignKey(
f"{an}.Sample",
@@ -827,7 +793,7 @@ class Meta:
class IsolationMethod(models.Model):
- name = models.CharField(max_length=255, unique=True)
+ name = models.CharField(max_length=255, unique=False)
species = models.ForeignKey(
f"{an}.Species",
on_delete=models.CASCADE,
diff --git a/src/genlab_bestilling/templates/genlab_bestilling/analysisorder_detail.html b/src/genlab_bestilling/templates/genlab_bestilling/analysisorder_detail.html
index a23995de..11812109 100644
--- a/src/genlab_bestilling/templates/genlab_bestilling/analysisorder_detail.html
+++ b/src/genlab_bestilling/templates/genlab_bestilling/analysisorder_detail.html
@@ -33,20 +33,20 @@
Samples to analyze
-
back
+
Back
{% if object.status == 'draft' %}
-
Edit Order
+
Edit Order
{% if not object.from_order %}
-
Edit Samples
+
Edit Samples
{% endif %}
-
Summary Samples
+
Summary Samples
{% url 'genrequest-order-confirm' genrequest_id=object.genrequest_id pk=object.id as confirm_order_url %}
{% url 'genrequest-order-clone' genrequest_id=object.genrequest_id pk=object.id as clone_order_url %}
- {% action-button action=confirm_order_url class="bg-secondary text-white" submit_text="Deliver order" csrf_token=csrf_token %}
- {% action-button action=clone_order_url class="bg-secondary text-white" submit_text="Clone Order" csrf_token=csrf_token %}
-
Delete
+ {% action-button action=confirm_order_url class="btn custom_order_button" submit_text="Deliver order" csrf_token=csrf_token %}
+ {% action-button action=clone_order_url class="btn custom_order_button" submit_text="Clone Order" csrf_token=csrf_token %}
+
Delete
{% elif object.status == object.OrderStatus.DELIVERED %}
-
Samples
+
Samples
{% endif %}
{% endblock %}
diff --git a/src/genlab_bestilling/templates/genlab_bestilling/analysisorder_filter.html b/src/genlab_bestilling/templates/genlab_bestilling/analysisorder_filter.html
index 23259bf5..9dc763bb 100644
--- a/src/genlab_bestilling/templates/genlab_bestilling/analysisorder_filter.html
+++ b/src/genlab_bestilling/templates/genlab_bestilling/analysisorder_filter.html
@@ -6,8 +6,8 @@
{% block page-inner %}
{% endblock page-inner %}
diff --git a/src/genlab_bestilling/templates/genlab_bestilling/analysisorder_form.html b/src/genlab_bestilling/templates/genlab_bestilling/analysisorder_form.html
index 8379e70f..c201072b 100644
--- a/src/genlab_bestilling/templates/genlab_bestilling/analysisorder_form.html
+++ b/src/genlab_bestilling/templates/genlab_bestilling/analysisorder_form.html
@@ -6,9 +6,9 @@ {% if object.id %}{{ object }}{% else %}Create {{ view
{% formset endpoint=request.path csrf_token=csrf_token form=form %}
diff --git a/src/genlab_bestilling/templates/genlab_bestilling/base_filter.html b/src/genlab_bestilling/templates/genlab_bestilling/base_filter.html
index 0f2c0924..e27e6f3b 100644
--- a/src/genlab_bestilling/templates/genlab_bestilling/base_filter.html
+++ b/src/genlab_bestilling/templates/genlab_bestilling/base_filter.html
@@ -9,8 +9,8 @@ {% block page-title %}{% endblock page-title %}
{% render_table table %}
diff --git a/src/genlab_bestilling/templates/genlab_bestilling/equipmentorder_detail.html b/src/genlab_bestilling/templates/genlab_bestilling/equipmentorder_detail.html
index c4f3c066..77f9fbaf 100644
--- a/src/genlab_bestilling/templates/genlab_bestilling/equipmentorder_detail.html
+++ b/src/genlab_bestilling/templates/genlab_bestilling/equipmentorder_detail.html
@@ -12,7 +12,7 @@
Order {{ object }}
{% object-detail object=object %}
@@ -35,13 +35,13 @@ Requested Equipment
{% if object.status == 'draft' %}
-
Edit
-
Edit requested equipment
+
Edit
+
Edit requested equipment
{% url 'genrequest-order-confirm' genrequest_id=object.genrequest_id pk=object.id as confirm_order_url %}
- {% action-button action=confirm_order_url class="bg-secondary text-white" submit_text="Deliver order" csrf_token=csrf_token %}
-
Delete
+ {% action-button action=confirm_order_url class="btn custom_order_button" submit_text="Deliver order" csrf_token=csrf_token %}
+
Delete
{% endif %}
{% url 'genrequest-order-clone' genrequest_id=object.genrequest_id pk=object.id as clone_order_url %}
- {% action-button action=clone_order_url class="bg-secondary text-white" submit_text="Clone Order" csrf_token=csrf_token %}
+ {% action-button action=clone_order_url class="btn custom_order_button" submit_text="Clone Order" csrf_token=csrf_token %}
{% endblock %}
diff --git a/src/genlab_bestilling/templates/genlab_bestilling/equipmentorder_filter.html b/src/genlab_bestilling/templates/genlab_bestilling/equipmentorder_filter.html
index 9b328e39..d0dad657 100644
--- a/src/genlab_bestilling/templates/genlab_bestilling/equipmentorder_filter.html
+++ b/src/genlab_bestilling/templates/genlab_bestilling/equipmentorder_filter.html
@@ -6,8 +6,8 @@
{% block page-inner %}
{% endblock page-inner %}
diff --git a/src/genlab_bestilling/templates/genlab_bestilling/equipmentorder_form.html b/src/genlab_bestilling/templates/genlab_bestilling/equipmentorder_form.html
index 0b9e874f..cc0cc260 100644
--- a/src/genlab_bestilling/templates/genlab_bestilling/equipmentorder_form.html
+++ b/src/genlab_bestilling/templates/genlab_bestilling/equipmentorder_form.html
@@ -5,9 +5,9 @@
{% if object.id %}{{ object }}{% else %}Create {{ view.model|verbose_name }}{% endif %}
{% formset endpoint=request.path csrf_token=csrf_token form=form %}
diff --git a/src/genlab_bestilling/templates/genlab_bestilling/equipmentorderquantity_form.html b/src/genlab_bestilling/templates/genlab_bestilling/equipmentorderquantity_form.html
index e9f38114..2593b8b5 100644
--- a/src/genlab_bestilling/templates/genlab_bestilling/equipmentorderquantity_form.html
+++ b/src/genlab_bestilling/templates/genlab_bestilling/equipmentorderquantity_form.html
@@ -4,7 +4,7 @@
{% block content %}
{% if object.id %}{{ object }}{% else %}Create {{ view.model|verbose_name }}{% endif %}
{% formset endpoint=request.path csrf_token=csrf_token form_collection=form_collection %}
{% endblock %}
diff --git a/src/genlab_bestilling/templates/genlab_bestilling/extractionorder_detail.html b/src/genlab_bestilling/templates/genlab_bestilling/extractionorder_detail.html
index ae3a5a89..0342d567 100644
--- a/src/genlab_bestilling/templates/genlab_bestilling/extractionorder_detail.html
+++ b/src/genlab_bestilling/templates/genlab_bestilling/extractionorder_detail.html
@@ -16,30 +16,33 @@ Order {{ object }}
- {% object-detail object=object %}
-
-
- Delivered Samples
-
-
Uploaded {{ object.samples.count }} samples
-
-
-
back
+
Back
{% if object.status == 'draft' %}
-
Edit Order
-
Edit Samples
- {% url 'genrequest-order-confirm' genrequest_id=object.genrequest_id pk=object.id as confirm_order_url %}
- {% action-button action=confirm_order_url class="bg-secondary text-white" submit_text="Deliver order" csrf_token=csrf_token %}
-
Delete
+
Edit Order
+
Edit Samples
+
+ {% url 'genrequest-order-confirm' genrequest_id=object.genrequest_id pk=object.id as confirm_order_url %}
+ {% action-button action=confirm_order_url class="btn custom_order_button" submit_text="Deliver order" csrf_token=csrf_token %}
+
+
Delete
{% endif %}
-
Samples
+
+
Samples
{% if object.status != 'draft' %}
-
Analyze these samples
+
Analyze these samples
{% endif %}
{% url 'genrequest-order-clone' genrequest_id=object.genrequest_id pk=object.id as clone_order_url %}
- {% action-button action=clone_order_url class="bg-secondary text-white" submit_text="Clone Order" csrf_token=csrf_token %}
+ {% action-button action=clone_order_url class="btn custom_order_button" submit_text="Clone Order" csrf_token=csrf_token %}
+
+
+ {% object-detail object=object %}
+
+
+ Delivered Samples
+
+
Uploaded {{ object.samples.count }} samples
{% endblock %}
diff --git a/src/genlab_bestilling/templates/genlab_bestilling/extractionorder_filter.html b/src/genlab_bestilling/templates/genlab_bestilling/extractionorder_filter.html
index 6668d719..b0676d9b 100644
--- a/src/genlab_bestilling/templates/genlab_bestilling/extractionorder_filter.html
+++ b/src/genlab_bestilling/templates/genlab_bestilling/extractionorder_filter.html
@@ -6,8 +6,8 @@
{% block page-inner %}
{% endblock page-inner %}
diff --git a/src/genlab_bestilling/templates/genlab_bestilling/extractionorder_form.html b/src/genlab_bestilling/templates/genlab_bestilling/extractionorder_form.html
index 15e837a9..1f96a6e8 100644
--- a/src/genlab_bestilling/templates/genlab_bestilling/extractionorder_form.html
+++ b/src/genlab_bestilling/templates/genlab_bestilling/extractionorder_form.html
@@ -6,9 +6,9 @@ {% if object.id %}{{ object }}{% else %}Create {{ view
{% formset endpoint=request.path csrf_token=csrf_token form=form %}
diff --git a/src/genlab_bestilling/templates/genlab_bestilling/genrequest_confirm_delete.html b/src/genlab_bestilling/templates/genlab_bestilling/genrequest_confirm_delete.html
index e57a4a4f..b3c0b53b 100644
--- a/src/genlab_bestilling/templates/genlab_bestilling/genrequest_confirm_delete.html
+++ b/src/genlab_bestilling/templates/genlab_bestilling/genrequest_confirm_delete.html
@@ -5,14 +5,14 @@
{% block content %}
Delete request {{ object }}?
Are you sure you want to delete this request?
{% endblock %}
diff --git a/src/genlab_bestilling/templates/genlab_bestilling/genrequest_detail.html b/src/genlab_bestilling/templates/genlab_bestilling/genrequest_detail.html
index 191aefc5..a24452f8 100644
--- a/src/genlab_bestilling/templates/genlab_bestilling/genrequest_detail.html
+++ b/src/genlab_bestilling/templates/genlab_bestilling/genrequest_detail.html
@@ -15,49 +15,49 @@ {{ object.project_id }} - {{ object.name|d
{% endif %}
-
- {% object-detail object=object %}
+
+
+ {% object-detail object=object %}
{% endblock %}
diff --git a/src/genlab_bestilling/templates/genlab_bestilling/genrequest_filter.html b/src/genlab_bestilling/templates/genlab_bestilling/genrequest_filter.html
index 9f64aa39..49382992 100644
--- a/src/genlab_bestilling/templates/genlab_bestilling/genrequest_filter.html
+++ b/src/genlab_bestilling/templates/genlab_bestilling/genrequest_filter.html
@@ -5,6 +5,6 @@
{% block page-title %}Genetic Project{% endblock page-title %}
{% block page-inner %}
{% endblock page-inner %}
diff --git a/src/genlab_bestilling/templates/genlab_bestilling/genrequest_form.html b/src/genlab_bestilling/templates/genlab_bestilling/genrequest_form.html
index 2ad667d3..d06e6743 100644
--- a/src/genlab_bestilling/templates/genlab_bestilling/genrequest_form.html
+++ b/src/genlab_bestilling/templates/genlab_bestilling/genrequest_form.html
@@ -5,10 +5,10 @@
{% if object.id %}{{ object }}{% else %}Create {{ view.model|verbose_name }}{% endif %}
{% formset endpoint=request.path csrf_token=csrf_token form=form %}
diff --git a/src/genlab_bestilling/templates/genlab_bestilling/order_confirm_delete.html b/src/genlab_bestilling/templates/genlab_bestilling/order_confirm_delete.html
index 8f762754..76eea90c 100644
--- a/src/genlab_bestilling/templates/genlab_bestilling/order_confirm_delete.html
+++ b/src/genlab_bestilling/templates/genlab_bestilling/order_confirm_delete.html
@@ -5,14 +5,14 @@
{% block content %}
Delete Order {{ object }}?
Are you sure you want to delete this order?
{% endblock %}
diff --git a/src/genlab_bestilling/templates/genlab_bestilling/order_filter.html b/src/genlab_bestilling/templates/genlab_bestilling/order_filter.html
index 5e32f0dd..80b5169a 100644
--- a/src/genlab_bestilling/templates/genlab_bestilling/order_filter.html
+++ b/src/genlab_bestilling/templates/genlab_bestilling/order_filter.html
@@ -6,10 +6,10 @@
{% block page-inner %}
{% endblock page-inner %}
diff --git a/src/genlab_bestilling/templates/genlab_bestilling/sample_list.html b/src/genlab_bestilling/templates/genlab_bestilling/sample_list.html
index dee3ee67..1ebbb60d 100644
--- a/src/genlab_bestilling/templates/genlab_bestilling/sample_list.html
+++ b/src/genlab_bestilling/templates/genlab_bestilling/sample_list.html
@@ -6,16 +6,16 @@
Samples for Extraction #{{ view.kwargs.pk }}
{% render_table table %}
-
back to order
+
Back to order
{% if extraction.status == 'draft' %}
{% url 'genrequest-order-confirm' genrequest_id=view.kwargs.genrequest_id pk=view.kwargs.pk as confirm_order_url %}
- {% action-button action=confirm_order_url class="bg-secondary text-white" submit_text="Deliver order" csrf_token=csrf_token %}
-
edit samples
+ {% action-button action=confirm_order_url class="btn custom_order_button" submit_text="Deliver order" csrf_token=csrf_token %}
+
Edit samples
{% endif %}
{% if extraction.status != 'draft' %}
-
Analyze these samples
+
Analyze these samples
{% endif %}
-
Download CSV
+
Download CSV
{% endblock %}
diff --git a/src/genlab_bestilling/templates/genlab_bestilling/samplemarkeranalysis_list.html b/src/genlab_bestilling/templates/genlab_bestilling/samplemarkeranalysis_list.html
index ede4ff29..5b81034a 100644
--- a/src/genlab_bestilling/templates/genlab_bestilling/samplemarkeranalysis_list.html
+++ b/src/genlab_bestilling/templates/genlab_bestilling/samplemarkeranalysis_list.html
@@ -13,12 +13,12 @@
{{ analysis }} - Samples
{% render_table table %}
-
back to order
+
Back to order
{% if analysis.status == 'draft' %}
{% url 'genrequest-order-confirm' genrequest_id=view.kwargs.genrequest_id pk=view.kwargs.pk as confirm_order_url %}
{% action-button action=confirm_order_url class="bg-secondary text-white" submit_text="Deliver order" csrf_token=csrf_token %}
{% if not analysis.from_order %}
-
edit samples
+
Edit samples
{% endif %}
{% endif %}
diff --git a/src/genlab_bestilling/tests/test_models.py b/src/genlab_bestilling/tests/test_models.py
index 6e1dd7e3..f015df43 100644
--- a/src/genlab_bestilling/tests/test_models.py
+++ b/src/genlab_bestilling/tests/test_models.py
@@ -142,11 +142,20 @@ def test_full_order_ids_generation(extraction):
"""
extraction.confirm_order()
- Sample.objects.generate_genlab_ids(extraction.id)
+ sample_ids = list(
+ Sample.objects.filter(order_id=extraction.id).values_list(
+ "id", flat=True
+ ) # selected_samples is always a list
+ )
+
+ Sample.objects.generate_genlab_ids(
+ extraction.id,
+ selected_samples=[str(pk) for pk in sample_ids],
+ )
assertQuerySetEqual(
Sample.objects.filter(genlab_id__isnull=False),
- Sample.objects.all(),
+ Sample.objects.filter(order_id=extraction.id),
ordered=False,
)
@@ -159,7 +168,7 @@ def test_order_selected_ids_generation(extraction):
Sample.objects.generate_genlab_ids(
extraction.id,
- selected_samples=extraction.samples.all().values("id")[
+ selected_samples=extraction.samples.all().values_list("id", flat=True)[
: extraction.samples.count() - 1
],
)
@@ -170,6 +179,14 @@ def test_order_selected_ids_generation(extraction):
)
+def natural_sort_key(s):
+ """Return a key that sorts numbers numerically and strings lexicographically"""
+ try:
+ return (0, int(s.name))
+ except (ValueError, TypeError):
+ return (1, str(s.name))
+
+
def test_ids_generation_with_only_numeric_names(genlab_setup):
"""
Test that by default the ordering is done on the column name
@@ -226,7 +243,14 @@ def test_ids_generation_with_only_numeric_names(genlab_setup):
extraction.confirm_order()
- Sample.objects.generate_genlab_ids(order_id=extraction.id)
+ samples = [s1, s2, s3, s4]
+ samples.sort(key=natural_sort_key)
+ sample_ids = [str(s.id) for s in samples]
+
+ Sample.objects.generate_genlab_ids(
+ order_id=extraction.id,
+ selected_samples=sample_ids,
+ )
gid = GIDSequence.objects.get_sequence_for_species_year(
species=combo[0][0], year=extraction.confirmed_at.year, lock=False
@@ -302,8 +326,13 @@ def test_ids_generation_order_by_pop_id(genlab_setup):
extraction.confirm_order()
+ samples = list(Sample.objects.filter(order_id=extraction.id))
+ samples.sort(key=lambda s: (s.pop_id, str(s.name)))
+ sample_ids = [str(s.id) for s in samples]
+
Sample.objects.generate_genlab_ids(
- order_id=extraction.id, sorting_order=["pop_id", "name"]
+ order_id=extraction.id,
+ selected_samples=sample_ids,
)
gid = GIDSequence.objects.get_sequence_for_species_year(
diff --git a/src/nina/templates/nina/project_detail.html b/src/nina/templates/nina/project_detail.html
index 6e5f3acc..bd38e40d 100644
--- a/src/nina/templates/nina/project_detail.html
+++ b/src/nina/templates/nina/project_detail.html
@@ -14,6 +14,28 @@
{{ object }}
{% endif %}
+
+
{% object-detail object=object %}
@@ -22,27 +44,5 @@
Members
{% render_table table %}
-
-
{% endblock %}
diff --git a/src/nina/templates/nina/project_form.html b/src/nina/templates/nina/project_form.html
index 90d3d1f2..55da8b8d 100644
--- a/src/nina/templates/nina/project_form.html
+++ b/src/nina/templates/nina/project_form.html
@@ -9,6 +9,6 @@ {% if object.id %}{{ object }}{% else %}Create {{ view
{% render_form form "tailwind" %}
- Submit
+ Submit
{% endblock %}
diff --git a/src/nina/templates/nina/projectmembership_form.html b/src/nina/templates/nina/projectmembership_form.html
index 792de137..fbdfb9e1 100644
--- a/src/nina/templates/nina/projectmembership_form.html
+++ b/src/nina/templates/nina/projectmembership_form.html
@@ -4,7 +4,7 @@
{% block content %}
{{ object }}
{% formset endpoint=request.path csrf_token=csrf_token form_collection=form_collection %}
{% endblock %}
diff --git a/src/nina/templates/nina/projectmembership_list.html b/src/nina/templates/nina/projectmembership_list.html
index 4a0790c1..d626c8fe 100644
--- a/src/nina/templates/nina/projectmembership_list.html
+++ b/src/nina/templates/nina/projectmembership_list.html
@@ -4,7 +4,7 @@
{% block content %}
NINA Projects you are involved in
{% render_table table %}
diff --git a/src/staff/filters.py b/src/staff/filters.py
index 067247b7..f5d09d43 100644
--- a/src/staff/filters.py
+++ b/src/staff/filters.py
@@ -2,45 +2,116 @@
import django_filters as filters
from dal import autocomplete
+from django import forms
from django.db.models import QuerySet
from django.http import HttpRequest
+from django_filters import CharFilter
from genlab_bestilling.models import (
AnalysisOrder,
+ ExtractionOrder,
ExtractionPlate,
+ Order,
Sample,
SampleMarkerAnalysis,
)
class AnalysisOrderFilter(filters.FilterSet):
- def __init__(
- self,
- data: dict[str, Any] | None = None,
- queryset: QuerySet | None = None,
- *,
- request: HttpRequest | None = None,
- prefix: str | None = None,
- ) -> None:
- super().__init__(data, queryset, request=request, prefix=prefix)
- self.filters["genrequest__project"].extra["widget"] = autocomplete.ModelSelect2(
- url="autocomplete:project"
+ class Meta:
+ model = AnalysisOrder
+ fields = ["id", "status", "genrequest__area"]
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.filters["id"].field.label = "Order ID"
+ self.filters["id"].field.widget = forms.TextInput(
+ attrs={
+ "class": "bg-white border border-gray-300 rounded-lg py-2 px-4 w-full text-gray-700", # noqa: E501
+ "placeholder": "Enter Order ID",
+ }
+ )
+
+ self.filters["status"].field.label = "Order Status"
+ self.filters["status"].field.choices = Order.OrderStatus.choices
+ self.filters["status"].field.widget = autocomplete.ListSelect2(
+ url="autocomplete:order-status",
+ attrs={
+ "class": "w-full",
+ },
)
- self.filters["genrequest"].extra["widget"] = autocomplete.ModelSelect2(
- url="autocomplete:genrequest"
+
+ self.filters["genrequest__area"].field.label = "Area"
+ self.filters["genrequest__area"].field.widget = autocomplete.ModelSelect2(
+ url="autocomplete:area",
+ attrs={
+ "class": "w-full",
+ },
)
+
+class ExtractionOrderFilter(filters.FilterSet):
class Meta:
- model = AnalysisOrder
- fields = [
- "id",
- "status",
- "genrequest",
- "genrequest__project",
- ]
+ model = ExtractionOrder
+ fields = ["id", "status", "genrequest__area", "sample_types"]
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.filters["id"].field.label = "Order ID"
+ self.filters["id"].field.widget = forms.TextInput(
+ attrs={
+ "class": "bg-white border border-gray-300 rounded-lg py-2 px-4 w-full text-gray-700", # noqa: E501
+ "placeholder": "Enter Order ID",
+ }
+ )
+
+ self.filters["status"].field.label = "Order Status"
+ self.filters["status"].field.choices = Order.OrderStatus.choices
+ self.filters["status"].field.widget = autocomplete.ListSelect2(
+ url="autocomplete:order-status",
+ attrs={
+ "class": "w-full",
+ },
+ )
+
+ self.filters["genrequest__area"].field.label = "Area"
+ self.filters["genrequest__area"].field.widget = autocomplete.ModelSelect2(
+ url="autocomplete:area",
+ attrs={
+ "class": "w-full",
+ },
+ )
+
+ self.filters["sample_types"].field.label = "Sample types"
+ self.filters["sample_types"].field.widget = autocomplete.ModelSelect2Multiple(
+ url="autocomplete:sample-type",
+ attrs={
+ "class": "w-full",
+ },
+ )
class OrderSampleFilter(filters.FilterSet):
+ genlab_id = CharFilter(
+ label="GenlabID",
+ widget=forms.TextInput(
+ attrs={
+ "placeholder": "Type here",
+ }
+ ),
+ )
+
+ name = CharFilter(
+ label="Name",
+ widget=forms.TextInput(
+ attrs={
+ "placeholder": "Type here",
+ }
+ ),
+ )
+
def __init__(
self,
data: dict[str, Any] | None = None,
@@ -56,23 +127,20 @@ def __init__(
self.filters["type"].extra["widget"] = autocomplete.ModelSelect2(
url="autocomplete:sample-type"
)
- self.filters["location"].extra["widget"] = autocomplete.ModelSelect2(
- url="autocomplete:location"
- )
class Meta:
model = Sample
fields = [
# "order",
- "guid",
- "name",
+ # "guid",
"genlab_id",
+ "name",
"species",
"type",
- "year",
- "location",
- "pop_id",
- "type",
+ # "year",
+ # "location",
+ # "pop_id",
+ # "type",
# "desired_extractions",
]
diff --git a/src/staff/tables.py b/src/staff/tables.py
index a9ce9222..b91b3efb 100644
--- a/src/staff/tables.py
+++ b/src/staff/tables.py
@@ -1,8 +1,9 @@
+import re
+from collections.abc import Sequence
+from datetime import datetime
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 (
@@ -166,7 +167,7 @@ class SampleBaseTable(tables.Table):
empty_values=(),
)
- name = tables.Column(order_by=("name_as_int",))
+ name = tables.Column()
class Meta:
model = Sample
@@ -192,23 +193,10 @@ class Meta:
"species",
"type",
)
- order_by = (
- "-is_prioritised",
- "species",
- "genlab_id",
- "name_as_int",
- )
+ order_by = ("-is_prioritised", "species", "genlab_id")
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()])
@@ -218,6 +206,24 @@ def render_plate_positions(self, value: Any) -> str:
def render_checked(self, record: Any) -> str:
return mark_safe(f' ') # noqa: S308
+ def order_name(
+ self, records: Sequence[Any], is_descending: bool
+ ) -> tuple[list[Any], bool]:
+ def natural_sort_key(record: Any) -> list[str]:
+ name = record.name or ""
+ parts = re.findall(r"\d+|\D+", name)
+ key = []
+ for part in parts:
+ if part.isdigit():
+ # Pad numbers with zeros for proper string compare, e.g., '000012' > '000001' # noqa: E501
+ key.append(f"{int(part):010d}")
+ else:
+ key.append(part.lower())
+ return key
+
+ sorted_records = sorted(records, key=natural_sort_key, reverse=is_descending)
+ return (sorted_records, True)
+
class SampleStatusTable(tables.Table):
"""
@@ -250,18 +256,21 @@ class SampleStatusTable(tables.Table):
orderable=True,
yesno="✔,-",
default=False,
+ accessor="is_marked",
)
plucked = tables.BooleanColumn(
verbose_name="Plucked",
orderable=True,
yesno="✔,-",
default=False,
+ accessor="is_plucked",
)
isolated = tables.BooleanColumn(
verbose_name="Isolated",
orderable=True,
yesno="✔,-",
default=False,
+ accessor="is_isolated",
)
class Meta:
@@ -269,6 +278,9 @@ class Meta:
fields = [
"checked",
"genlab_id",
+ "marked",
+ "plucked",
+ "isolated",
"internal_note",
"isolation_method",
"type",
@@ -283,11 +295,17 @@ class Meta:
"internal_note",
"isolation_method",
]
+ order_by = ("genlab_id",)
+
+ def render_checked(self, record: Any) -> str:
+ return mark_safe( # noqa: S308
+ f' ' # noqa: E501
+ )
class OrderExtractionSampleTable(SampleBaseTable):
class Meta(SampleBaseTable.Meta):
- fields = SampleBaseTable.Meta.fields
+ exclude = ("pop_id", "guid", "plate_positions")
class OrderAnalysisSampleTable(tables.Table):
@@ -366,7 +384,7 @@ def render_status(self, value: Order.OrderStatus, record: Order) -> str:
color_class = status_colors.get(value, "bg-gray-100 text-gray-800")
status_text = status_text.get(value, "Unknown")
return mark_safe( # noqa: S308
- f'{status_text} ' # noqa: E501
+ f'{status_text} ' # noqa: E501
)
@@ -385,6 +403,13 @@ def render_id(
class UrgentOrderTable(StaffIDMixinTable, StatusMixinTable):
+ priority = tables.TemplateColumn(
+ orderable=False,
+ verbose_name="Priority",
+ accessor="priority",
+ template_name="staff/components/priority_column.html",
+ )
+
description = tables.Column(
accessor="genrequest__name",
verbose_name="Description",
@@ -392,29 +417,28 @@ class UrgentOrderTable(StaffIDMixinTable, StatusMixinTable):
)
delivery_date = tables.Column(
- accessor="genrequest__expected_samples_delivery_date",
verbose_name="Delivery date",
orderable=False,
)
- def render_delivery_date(self, value: Any) -> str:
+ def render_delivery_date(self, value: datetime | None) -> str:
if value:
return value.strftime("%d/%m/%Y")
return "-"
class Meta:
model = Order
- fields = ["id", "description", "delivery_date", "status"]
+ fields = ["priority", "id", "description", "delivery_date", "status"]
empty_text = "No urgent orders"
template_name = "django_tables2/tailwind_inner.html"
class NewUnseenOrderTable(StaffIDMixinTable):
seen = tables.TemplateColumn(
+ verbose_name="",
orderable=False,
- verbose_name="Seen",
- template_name="staff/components/seen_column.html",
empty_values=(),
+ template_name="staff/components/seen_column.html",
)
description = tables.Column(
@@ -424,12 +448,11 @@ class NewUnseenOrderTable(StaffIDMixinTable):
)
delivery_date = tables.Column(
- accessor="genrequest__expected_samples_delivery_date",
verbose_name="Delivery date",
orderable=False,
)
- def render_delivery_date(self, value: Any) -> str:
+ def render_delivery_date(self, value: datetime | None) -> str:
if value:
return value.strftime("%d/%m/%Y")
return "-"
@@ -471,12 +494,11 @@ class NewSeenOrderTable(StaffIDMixinTable):
)
delivery_date = tables.Column(
- accessor="genrequest__expected_samples_delivery_date",
verbose_name="Delivery date",
orderable=False,
)
- def render_delivery_date(self, value: Any) -> str:
+ def render_delivery_date(self, value: datetime | None) -> str:
if value:
return value.strftime("%d/%m/%Y")
return "-"
@@ -530,9 +552,9 @@ class AssignedOrderTable(StatusMixinTable, StaffIDMixinTable):
orderable=False,
)
- def render_samples_completed(self, value: int) -> str:
+ def render_samples_completed(self, value: int, record: Order) -> str:
if value > 0:
- return "- / " + str(value)
+ return str(record.isolated_sample_count) + " / " + str(value)
return "-"
class Meta:
@@ -557,12 +579,11 @@ class DraftOrderTable(StaffIDMixinTable):
)
delivery_date = tables.Column(
- accessor="genrequest__expected_samples_delivery_date",
verbose_name="Delivery date",
orderable=False,
)
- def render_delivery_date(self, value: Any) -> str:
+ def render_delivery_date(self, value: datetime | None) -> str:
if value:
return value.strftime("%d/%m/%Y")
return "-"
diff --git a/src/staff/templates/staff/analysisorder_detail.html b/src/staff/templates/staff/analysisorder_detail.html
index fc2ae045..ba9ece3a 100644
--- a/src/staff/templates/staff/analysisorder_detail.html
+++ b/src/staff/templates/staff/analysisorder_detail.html
@@ -1,5 +1,6 @@
{% extends "staff/base.html" %}
{% load i18n %}
+{% load order_tags %}
{% block content %}
@@ -7,35 +8,43 @@ Order {{ object }}
-
back
-
Samples
+
Back
+
Samples
{% if extraction_order %}
-
Go to {{ extraction_order}}
+
Go to {{ extraction_order}}
{% endif %}
-
Assign staff
-
- {% if not object.is_seen %}
+ {% if object.genrequest.responsible_staff.all|is_responsible:request.user and not object.is_seen %}
{% endif %}
- {% if object.status == object.OrderStatus.DELIVERED %}
+ {% if object.status != object.OrderStatus.DRAFT %}
{% url 'staff:order-to-draft' pk=object.id as to_draft_url %}
- {% action-button action=to_draft_url class="bg-secondary text-white" submit_text="Convert to draft" csrf_token=csrf_token %}
+ {% action-button action=to_draft_url class="custom_order_button" submit_text="
Convert to draft"|safe csrf_token=csrf_token %}
{% endif %}
- {% if object.status != object.OrderStatus.DRAFT and object.next_status %}
+ {% if object.status == object.OrderStatus.DELIVERED %}
{% url 'staff:order-to-next-status' pk=object.id as to_next_status_url %}
- {% with "Set as "|add:object.next_status as btn_name %}
- {% action-button action=to_next_status_url class="bg-secondary text-white" submit_text=btn_name csrf_token=csrf_token %}
+ {% with "
Set as "|add:object.next_status as btn_name %}
+ {% action-button action=to_next_status_url class="bg-yellow-200 text-yellow-800 border border-yellow-700 hover:bg-yellow-300" submit_text=btn_name csrf_token=csrf_token %}
{% endwith %}
{% endif %}
+
+ {% if object.status == object.OrderStatus.PROCESSING %}
+ {% url 'staff:order-to-next-status' pk=object.id as to_next_status_url %}
+ {% with "
Set as "|add:object.next_status as btn_name %}
+ {% action-button action=to_next_status_url class="custom_order_button_green" submit_text=btn_name csrf_token=csrf_token %}
+ {% endwith %}
+ {% endif %}
+
+
{% fragment as table_header %}
diff --git a/src/staff/templates/staff/analysisorder_filter.html b/src/staff/templates/staff/analysisorder_filter.html
index a0ac89a4..c388eb48 100644
--- a/src/staff/templates/staff/analysisorder_filter.html
+++ b/src/staff/templates/staff/analysisorder_filter.html
@@ -10,8 +10,8 @@
{% render_table table %}
diff --git a/src/staff/templates/staff/components/seen_column.html b/src/staff/templates/staff/components/seen_column.html
index 4e318c3c..4b5deec7 100644
--- a/src/staff/templates/staff/components/seen_column.html
+++ b/src/staff/templates/staff/components/seen_column.html
@@ -1,5 +1,11 @@
+{% load order_tags %}
+
+{% if record.genrequest.responsible_staff.all|is_responsible:request.user %}
+{% endif %}
diff --git a/src/staff/templates/staff/equipmentorder_detail.html b/src/staff/templates/staff/equipmentorder_detail.html
index 7f76f07b..061dee90 100644
--- a/src/staff/templates/staff/equipmentorder_detail.html
+++ b/src/staff/templates/staff/equipmentorder_detail.html
@@ -11,7 +11,7 @@
Order {{ object }}
{% object-detail object=object %}
@@ -33,8 +33,8 @@ Requested Equipment
{% /table %}
-
back
-
Assign staff
+
Back
+
Assign staff
{% comment %}
{% if object.status == 'draft' %}
Edit
diff --git a/src/staff/templates/staff/equipmentorder_filter.html b/src/staff/templates/staff/equipmentorder_filter.html
index 434f0c13..07928ac6 100644
--- a/src/staff/templates/staff/equipmentorder_filter.html
+++ b/src/staff/templates/staff/equipmentorder_filter.html
@@ -10,8 +10,8 @@
{% render_table table %}
diff --git a/src/staff/templates/staff/extractionorder_detail.html b/src/staff/templates/staff/extractionorder_detail.html
index cf21b4c0..77317877 100644
--- a/src/staff/templates/staff/extractionorder_detail.html
+++ b/src/staff/templates/staff/extractionorder_detail.html
@@ -1,5 +1,7 @@
{% extends "staff/base.html" %}
{% load i18n %}
+{% load order_tags %}
+{% load static %}
{% block content %}
@@ -7,13 +9,13 @@
Order {{ object }}
-
back
-
Samples
+
Back
+
Samples
{% if analysis_orders|length > 1 %}
Go to Analysis Order
@@ -32,36 +34,33 @@ Order {{ object }}
{% elif analysis_orders|length == 1 %}
-
Go to {{ analysis_orders.first}}
+
Go to {{ analysis_orders.first}}
{% endif %}
-
Assign staff
- {% if not object.is_seen %}
+ {% if object.genrequest.responsible_staff.all|is_responsible:request.user and not object.is_seen %}
{% endif %}
- {% if object.status == object.OrderStatus.DELIVERED %}
- {% url 'staff:order-manually-checked' pk=object.id as confirm_check_url %}
- {% action-button action=confirm_check_url class="bg-secondary text-white" submit_text="Confirm - Order checked" csrf_token=csrf_token %}
- {% endif %}
-
{% if object.status != object.OrderStatus.DRAFT %}
{% url 'staff:order-to-draft' pk=object.id as to_draft_url %}
- {% action-button action=to_draft_url class="bg-secondary text-white" submit_text="Convert to draft" csrf_token=csrf_token %}
+ {% action-button action=to_draft_url class="custom_order_button" submit_text="
Convert to draft"|safe csrf_token=csrf_token %}
{% endif %}
- {% if object.next_status %}
+ {% if object.status == object.OrderStatus.DELIVERED and object.internal_status == "checked" %}
{% url 'staff:order-to-next-status' pk=object.id as to_next_status_url %}
- {% with "Set as "|add:object.next_status as btn_name %}
- {% action-button action=to_next_status_url class="bg-secondary text-white" submit_text=btn_name csrf_token=csrf_token %}
+ {% with "
Set as "|add:object.next_status as btn_name %}
+ {% action-button action=to_next_status_url class="bg-yellow-200 text-yellow-800 border border-yellow-700 hover:bg-yellow-300" submit_text=btn_name csrf_token=csrf_token %}
{% endwith %}
{% endif %}
+
{% fragment as table_header %}
{% #table-cell header=True %}GUID{% /table-cell %}
diff --git a/src/staff/templates/staff/extractionorder_filter.html b/src/staff/templates/staff/extractionorder_filter.html
index e2c1e8d9..c85fc272 100644
--- a/src/staff/templates/staff/extractionorder_filter.html
+++ b/src/staff/templates/staff/extractionorder_filter.html
@@ -10,8 +10,8 @@
{% render_table table %}
diff --git a/src/staff/templates/staff/extractionplate_detail.html b/src/staff/templates/staff/extractionplate_detail.html
index 07c5904c..e688e864 100644
--- a/src/staff/templates/staff/extractionplate_detail.html
+++ b/src/staff/templates/staff/extractionplate_detail.html
@@ -10,6 +10,6 @@ Plate {{ object }}
{% object-detail object=object %}
{% endblock %}
diff --git a/src/staff/templates/staff/extractionplate_filter.html b/src/staff/templates/staff/extractionplate_filter.html
index 26e046dd..4463ae68 100644
--- a/src/staff/templates/staff/extractionplate_filter.html
+++ b/src/staff/templates/staff/extractionplate_filter.html
@@ -8,14 +8,14 @@
{% block page-inner %}
{% render_table table %}
diff --git a/src/staff/templates/staff/extractionplate_form.html b/src/staff/templates/staff/extractionplate_form.html
index 83ce49f6..8274f222 100644
--- a/src/staff/templates/staff/extractionplate_form.html
+++ b/src/staff/templates/staff/extractionplate_form.html
@@ -5,15 +5,15 @@
{% if object.id %}{{ object }}{% else %}Create {{ view.model|verbose_name }}{% endif %}
diff --git a/src/staff/templates/staff/order_staff_edit.html b/src/staff/templates/staff/order_staff_edit.html
index efeab57b..9ea3ef7d 100644
--- a/src/staff/templates/staff/order_staff_edit.html
+++ b/src/staff/templates/staff/order_staff_edit.html
@@ -17,9 +17,9 @@ Manage Responsible Staff - {{ object }}
@@ -43,7 +43,7 @@ Assign Staff to Order
- Update staff assignment
+ Update staff assignment
diff --git a/src/staff/templates/staff/project_detail.html b/src/staff/templates/staff/project_detail.html
index 1a6b19e3..4aa8eb2a 100644
--- a/src/staff/templates/staff/project_detail.html
+++ b/src/staff/templates/staff/project_detail.html
@@ -7,7 +7,7 @@ Project {{ object }}
-
back
+
Back
{% url 'staff:projects-verify' pk=object.pk as verify_url %}
{% if object.verified_at is null %}
{% action-button action=verify_url class="btn-secondary text-white" submit_text="Mark as verified" csrf_token=csrf_token %}
diff --git a/src/staff/templates/staff/project_filter.html b/src/staff/templates/staff/project_filter.html
index b0434b4c..eb62d2fe 100644
--- a/src/staff/templates/staff/project_filter.html
+++ b/src/staff/templates/staff/project_filter.html
@@ -10,8 +10,8 @@
{% render_table table %}
diff --git a/src/staff/templates/staff/sample_detail.html b/src/staff/templates/staff/sample_detail.html
index 6b6c7c77..477ba47d 100644
--- a/src/staff/templates/staff/sample_detail.html
+++ b/src/staff/templates/staff/sample_detail.html
@@ -10,6 +10,6 @@
Sample {{ object }}
{% object-detail object=object %}
{% endblock %}
diff --git a/src/staff/templates/staff/sample_filter.html b/src/staff/templates/staff/sample_filter.html
index 430cc1b9..4d085073 100644
--- a/src/staff/templates/staff/sample_filter.html
+++ b/src/staff/templates/staff/sample_filter.html
@@ -11,32 +11,47 @@
Samples
{% endif %}
- {% if order %}
-
- {{ order.filled_genlab_count }} / {{ order.samples.count }} Genlabs generated
-
- {% endif %}
{% endblock page-title %}
{% block page-inner %}
{% if order %}
-
+
+
Back
+
Lab
+ {% if order.status == order.OrderStatus.DELIVERED and order.internal_status == "checked" %}
+ {% url 'staff:order-to-next-status' pk=order.pk as to_next_status_url %}
+ {% action-button action=to_next_status_url class="bg-secondary text-white" submit_text="Set as processing" csrf_token=csrf_token %}
+ {% endif %}
+
-
+
+
{% endcomment %}
+