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..0d58dff2 100644 --- a/src/genlab_bestilling/admin.py +++ b/src/genlab_bestilling/admin.py @@ -19,6 +19,7 @@ Location, LocationType, Marker, + Order, Organization, Sample, SampleMarkerAnalysis, @@ -48,6 +49,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"] 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..76dac513 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.db.models import Exists, OuterRef, QuerySet +from django.http import HttpResponse from django.views import View from drf_spectacular.utils import extend_schema from rest_framework.decorators import action @@ -33,6 +33,7 @@ Marker, Sample, SampleMarkerAnalysis, + SampleStatusAssignment, SampleType, Species, ) @@ -77,6 +78,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#", + "marked": "Marked", + "plucked": "Plucked", + "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", + "marked", + "plucked", + "isolated", + ], + "Elvemusling": [ + "genlab_id", + "fish_id", + "guid", + "location.name", + "year", + "name", + "station", + "type.name", + "length", + "notes", + "isolation_method", + "qiagen_number", + "placement_in_fridge", + "marked", + "plucked", + "isolated", + ], + "Terrestrisk": [ + "genlab_id", + "guid", + "name", + "type.name", + "species.name", + "location.name", + "delivered_to_lab", + "order", + "analysis_orders", + "notes", + "marked", + "plucked", + "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", + "marked", + "plucked", + "isolated", + "isolation_method", + "qiagen_number", + ], + } + def get_queryset(self) -> QuerySet: return ( super() @@ -89,7 +199,24 @@ def get_queryset(self) -> QuerySet: "order__genrequest__area", "location", ) - .order_by("id") + .annotate( + is_marked=Exists( + SampleStatusAssignment.objects.filter( + sample=OuterRef("pk"), status="marked" + ) + ), + is_plucked=Exists( + SampleStatusAssignment.objects.filter( + sample=OuterRef("pk"), status="plucked" + ) + ), + is_isolated=Exists( + SampleStatusAssignment.objects.filter( + sample=OuterRef("pk"), status="isolated" + ) + ), + ) + .order_by("genlab_id", "type") ) def get_serializer_class(self) -> type[BaseSerializer]: @@ -99,21 +226,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/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/models.py b/src/genlab_bestilling/models.py index 8211e20f..47f646c3 100644 --- a/src/genlab_bestilling/models.py +++ b/src/genlab_bestilling/models.py @@ -495,14 +495,6 @@ 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, @@ -827,7 +819,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 %}
{% if genrequest %} - back - Equipment order + Back + Equipment order {% endif %}
{% 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
{% if object.id %} - back + Back {% else %} - back + Back {% endif %}
{% 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 %}

{{ filter.form | crispy }} +
-
{% 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 }}

- back + Back
{% 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 %}
{% if genrequest %} - back - Equipment order + Back + Equipment order {% endif %}
{% 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 %}

{% if object.id %} - back + Back {% else %} - back + Back {% 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 %}

- back + Back
{% 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 %}
{% if genrequest %} - back - Equipment order + Back + Equipment order {% endif %}
{% 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
{% if object.id %} - back + Back {% else %} - back + Back {% endif %}
{% 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 }}?

- back + Back

Are you sure you want to delete this request?

{% csrf_token %} - +
{% 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 %}

{% if object.id %} - back + Back {% else %} - back - Register UBW project + Back + Register UBW project {% 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?

{% csrf_token %} - +
{% 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/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 %} +
+ Back + Edit + Members + {% if object.verified_at %} + Genetic projects + {% if object.active %} + New Genetic project + {% endif %} + {% endif %} +
+
{% object-detail object=object %} @@ -22,27 +44,5 @@

Members

{% render_table table %}
- -
- back - Edit - Members - {% if object.verified_at %} - Genetic projects - {% if object.active %} - new Genetic project - {% endif %} - {% endif %} -
{% 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" %} - + {% 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 }}

- back + Back
{% 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

- Register a project + Register a project
{% render_table table %} diff --git a/src/staff/filters.py b/src/staff/filters.py index 067247b7..271d13a8 100644 --- a/src/staff/filters.py +++ b/src/staff/filters.py @@ -2,18 +2,56 @@ 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): + 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__area"].field.label = "Area" + self.filters["genrequest__area"].field.widget = autocomplete.ModelSelect2( + url="autocomplete:area", + attrs={ + "class": "w-full", + }, + ) + + +class OrderFilter(filters.FilterSet): def __init__( self, data: dict[str, Any] | None = None, @@ -31,7 +69,7 @@ def __init__( ) class Meta: - model = AnalysisOrder + model = ExtractionOrder fields = [ "id", "status", @@ -41,6 +79,24 @@ class Meta: 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 +112,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/mixins.py b/src/staff/mixins.py new file mode 100644 index 00000000..95b5b918 --- /dev/null +++ b/src/staff/mixins.py @@ -0,0 +1,52 @@ +import django_tables2 as tables +from django.utils.safestring import mark_safe + +from genlab_bestilling.models import ( + AnalysisOrder, + EquipmentOrder, + ExtractionOrder, + Order, +) + + +def render_status_helper(value: Order.OrderStatus) -> str: + status_colors = { + Order.OrderStatus.PROCESSING: "bg-yellow-100 text-yellow-800", + Order.OrderStatus.COMPLETED: "bg-green-100 text-green-800", + Order.OrderStatus.DELIVERED: "bg-red-100 text-red-800", + } + status_text = { + Order.OrderStatus.PROCESSING: "Processing", + Order.OrderStatus.COMPLETED: "Completed", + Order.OrderStatus.DELIVERED: "Not started", + Order.OrderStatus.DRAFT: "Draft", + } + 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 + ) + + +class StatusMixinTable(tables.Table): + status = tables.Column( + orderable=False, + verbose_name="Status", + ) + + def render_status(self, value: Order.OrderStatus, record: Order) -> str: + return render_status_helper(record.status) + + +class StaffIDMixinTable(tables.Table): + id = tables.Column( + orderable=False, + empty_values=(), + ) + + def render_id( + self, record: ExtractionOrder | AnalysisOrder | EquipmentOrder + ) -> str: + url = record.get_absolute_staff_url() + + return mark_safe(f'{record}') # noqa: S308 diff --git a/src/staff/tables.py b/src/staff/tables.py index a9ce9222..c70373de 100644 --- a/src/staff/tables.py +++ b/src/staff/tables.py @@ -1,3 +1,5 @@ +from dataclasses import dataclass +from datetime import datetime from typing import Any import django_tables2 as tables @@ -16,6 +18,28 @@ ) from nina.models import Project +from .mixins import StaffIDMixinTable, StatusMixinTable, render_status_helper + + +@dataclass +class CombinedOrder: + extraction_order: ExtractionOrder + analysis_order: AnalysisOrder | None = None + priority: Order.OrderPriority = Order.OrderPriority.NORMAL + assigned_staff: list[str] | None = None + + def status(self) -> Order.OrderStatus: + """Returns the lowest status of the extraction and analysis orders.""" + order = Order.STATUS_ORDER + if self.analysis_order: + return min( + self.extraction_order.status, + self.analysis_order.status, + key=order.index, + ) + + return self.extraction_order.status + class ProjectTable(tables.Table): number = tables.Column( @@ -31,107 +55,148 @@ class Meta: class OrderTable(tables.Table): - id = tables.Column( - linkify=True, + priority = tables.Column( orderable=False, - empty_values=(), + verbose_name="Priority", + accessor="priority", ) - is_urgent = tables.Column( - orderable=True, - visible=True, - verbose_name="", + def render_priority(self, value: Order.OrderPriority) -> str: + if value == Order.OrderPriority.URGENT: + return mark_safe( + '' # noqa: E501 + ) + return "" + + def get_extraction_link(record: CombinedOrder) -> str | None: + return record.extraction_order.get_absolute_staff_url() + + ext_id = tables.Column( + linkify=get_extraction_link, + orderable=False, + empty_values=(), + verbose_name="EXT ID", + accessor="extraction_order", ) - status = tables.Column( - verbose_name="Status", + def render_ext_id(self, record: CombinedOrder) -> str: + return str(record.extraction_order) + + def get_analysis_link(record: CombinedOrder) -> str | None: + if record.analysis_order is not None: + return record.analysis_order.get_absolute_staff_url() + return None + + anl_id = tables.Column( + linkify=get_analysis_link, orderable=False, + empty_values=(), + verbose_name="ANL ID", + accessor="analysis_order", ) - is_seen = tables.Column( + def render_anl_id(self, record: CombinedOrder) -> str: + if record.analysis_order: + return str(record.analysis_order) + return "-" + + ext_status = tables.Column( + accessor="extraction_order__status", + verbose_name="EXT status", orderable=False, - visible=True, - verbose_name="", ) - class Meta: - fields = [ - "name", - "status", - "genrequest", - "genrequest__name", - "genrequest__project", - "genrequest__area", - "genrequest__samples_owner", - "created_at", - "last_modified_at", - "is_urgent", - "is_seen", - ] - sequence = ("is_seen", "is_urgent", "status", "id", "name") - empty_text = "No Orders" - order_by = ("-is_urgent", "last_modified_at", "created_at") + def render_ext_status(self, value: Order.OrderStatus, record: CombinedOrder) -> str: + return render_status_helper(value) - def render_id(self, record: Any) -> str: - return str(record) + anl_status = tables.Column( + verbose_name="ANL status", + orderable=False, + accessor="analysis_order__status", + empty_values=(), + ) - def render_is_urgent(self, value: bool) -> str: - html_exclaimation_mark = ( - "" - ) - if value: - return mark_safe(html_exclaimation_mark) # noqa: S308 - else: - return "" + def render_anl_status(self, value: Order.OrderStatus, record: CombinedOrder) -> str: + if record.analysis_order: + return render_status_helper(value) + return "-" - def render_is_seen(self, value: bool) -> str: - if not value: - return mark_safe( - '' - ) - return "" + area = tables.Column( + accessor="extraction_order__genrequest__area__name", + verbose_name="Area", + orderable=False, + ) + description = tables.Column( + accessor="extraction_order__genrequest__name", + verbose_name="Description", + orderable=False, + ) -class AnalysisOrderTable(OrderTable): - id = tables.Column( - linkify=("staff:order-analysis-detail", {"pk": tables.A("id")}), + total_samples_extraction = tables.Column( + accessor="extraction_order__sample_count", + verbose_name="Total samples EXT", orderable=False, - empty_values=(), ) - class Meta(OrderTable.Meta): - model = AnalysisOrder - fields = OrderTable.Meta.fields + ["return_samples"] + total_samples_analysis = tables.Column( + accessor="analysis_order__sample_count", + verbose_name="Total samples ANL", + orderable=False, + ) + samples_isolated = tables.Column( + accessor="extraction_order__sample_isolated_count", + verbose_name="Samples isolated", + orderable=False, + ) -class ExtractionOrderTable(OrderTable): - id = tables.Column( - linkify=("staff:order-extraction-detail", {"pk": tables.A("id")}), + markers = tables.ManyToManyColumn( + accessor="extraction_order__genrequest__markers", + transform=lambda x: x.name, + ) + + assigned_staff = tables.Column( + accessor="assigned_staff", + verbose_name="Assigned staff", orderable=False, empty_values=(), ) - sample_count = tables.Column( - accessor="sample_count", - verbose_name="Sample Count", + def render_assigned_staff(self, value: list[str] | None) -> str: + if value: + return ", ".join(value) + return "-" + + delivery_date = tables.Column( + accessor="analysis_order__expected_delivery_date", + verbose_name="Delivery date", orderable=False, + empty_values=(), ) - class Meta(OrderTable.Meta): - model = ExtractionOrder - fields = OrderTable.Meta.fields + [ - "species", - "sample_types", - "internal_status", - "needs_guid", - "return_samples", - "pre_isolated", - ] - sequence = OrderTable.Meta.sequence + ("sample_count",) + def render_delivery_date(self, value: datetime) -> str: + if value: + return value.strftime("%d/%m/%Y") + return "-" - def render_sample_count(self, record: Any) -> str: - return record.sample_count or "0" + class Meta: + fields = [ + "priority", + "ext_id", + "anl_id", + "ext_status", + "anl_status", + "area", + "description", + "total_samples_extraction", + "total_samples_analysis", + "samples_isolated", + "markers", + "assigned_staff", + "delivery_date", + ] + empty_text = "No Orders" class EquipmentOrderTable(OrderTable): @@ -284,10 +349,15 @@ class Meta: "isolation_method", ] + 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", "location") class OrderAnalysisSampleTable(tables.Table): @@ -346,45 +416,14 @@ class Meta: empty_text = "No Plates" -class StatusMixinTable(tables.Table): - status = tables.Column( - orderable=False, - verbose_name="Status", - ) - - def render_status(self, value: Order.OrderStatus, record: Order) -> str: - status_colors = { - "Processing": "bg-yellow-100 text-yellow-800", - "Completed": "bg-green-100 text-green-800", - "Delivered": "bg-red-100 text-red-800", - } - status_text = { - "Processing": "Processing", - "Completed": "Completed", - "Delivered": "Not started", - } - 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 - ) - - -class StaffIDMixinTable(tables.Table): - id = tables.Column( +class UrgentOrderTable(StaffIDMixinTable, StatusMixinTable): + priority = tables.TemplateColumn( orderable=False, - empty_values=(), + verbose_name="Priority", + accessor="priority", + template_name="staff/components/priority_column.html", ) - def render_id( - self, record: ExtractionOrder | AnalysisOrder | EquipmentOrder - ) -> str: - url = record.get_absolute_staff_url() - - return mark_safe(f'{record}') # noqa: S308 - - -class UrgentOrderTable(StaffIDMixinTable, StatusMixinTable): description = tables.Column( accessor="genrequest__name", verbose_name="Description", @@ -392,29 +431,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 +462,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 +508,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 +566,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 +593,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..bf6df0b0 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 %}
{% csrf_token %} - +
{% 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 %} +
+
+ Assign staff
{% 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 @@
{{ filter.form | crispy }} +
-
{% render_table table %} diff --git a/src/staff/templates/staff/base.html b/src/staff/templates/staff/base.html index 80cb99de..0951c392 100644 --- a/src/staff/templates/staff/base.html +++ b/src/staff/templates/staff/base.html @@ -7,9 +7,8 @@
Menu
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 %}
{% csrf_token %} - +
+{% 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 }}

- back + Back
{% 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 @@
{{ filter.form | crispy }} +
-
{% render_table table %} diff --git a/src/staff/templates/staff/extractionorder_detail.html b/src/staff/templates/staff/extractionorder_detail.html index cf21b4c0..4bb4777c 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 %}
{% 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 %}
{% csrf_token %} - +
{% 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 %}
+
+ Assign staff +
{% 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..b2327ebe 100644 --- a/src/staff/templates/staff/extractionorder_filter.html +++ b/src/staff/templates/staff/extractionorder_filter.html @@ -3,15 +3,15 @@ {% load render_table from django_tables2 %} {% block page-title %} -Extraction Orders +Orders {% endblock page-title %} {% block page-inner %}
{{ filter.form | crispy }} +
-
{% 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 %}
- back + Back
{% 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 %}
- Create + Create
{{ filter.form | crispy }}
- +
{% 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 %}

{% if object.id %} - back + Back {% else %} - back + Back {% endif %}
{{ form|crispy }} {% csrf_token %} -
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 }}

{% if model_type == "genrequest" %} - Back to Genrequest + Back to Genrequest {% else %} - Back to Order + Back to Order {% endif %}
@@ -43,7 +43,7 @@

Assign Staff to Order

- +
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 @@
{{ filter.form | crispy }} +
-
{% 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 %}
- back + Back
{% 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 - Download CSV - Lab -
+
+ 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 %} +
-
- {% csrf_token %} - - + +
+ {{ filter.form | crispy }} + +
+
+ +
+ {% csrf_token %} + +
+ + Download CSV -
This page is under development. The genlab IDs will generate for all, and without sorting as per now.
+
+
+ {{ order.filled_genlab_count }} / {{ order.samples.count }} samples with genlabID +
+
+
+
+
+
- {% render_table table %} + {% render_table table %}
{% endcomment %} +
{% csrf_token %} {% for status in statuses %} - {% endfor %} @@ -18,7 +27,7 @@

{% block page-title %}{% if order %}{{ order }} - Samp - {% render_table table %} diff --git a/src/staff/templatetags/order_tags.py b/src/staff/templatetags/order_tags.py index 77f63f48..f0c520a5 100644 --- a/src/staff/templatetags/order_tags.py +++ b/src/staff/templatetags/order_tags.py @@ -1,7 +1,8 @@ from django import template from django.db import models -from genlab_bestilling.models import Area, Order +from capps.users.models import User +from genlab_bestilling.models import Area, Order, SampleStatusAssignment from ..tables import ( AssignedOrderTable, @@ -14,14 +15,33 @@ register = template.Library() +@register.filter +def is_responsible(staff_queryset: models.QuerySet, user: User) -> bool: + return staff_queryset.filter(id=user.id).exists() + + @register.inclusion_tag("staff/components/order_table.html", takes_context=True) def urgent_orders_table(context: dict, area: Area | None = None) -> dict: urgent_orders = ( Order.objects.filter( is_urgent=True, ) - .exclude(status=Order.OrderStatus.DRAFT) + .exclude(status__in=[Order.OrderStatus.DRAFT, Order.OrderStatus.COMPLETED]) .select_related("genrequest") + .annotate( + priority=models.Case( + models.When(is_urgent=True, then=Order.OrderPriority.URGENT), + models.When(is_prioritized=True, then=Order.OrderPriority.PRIORITIZED), + default=1, + ), + delivery_date=models.Case( + models.When( + analysisorder__isnull=False, + then="analysisorder__expected_delivery_date", + ), + default=models.Value(None, output_field=models.DateField()), + ), + ) ) if area: @@ -69,6 +89,13 @@ def new_seen_orders_table(context: dict, area: Area | None = None) -> dict: models.When(is_prioritized=True, then=Order.OrderPriority.PRIORITIZED), default=1, ), + delivery_date=models.Case( + models.When( + analysisorder__isnull=False, + then="analysisorder__expected_delivery_date", + ), + default=models.Value(None, output_field=models.DateField()), + ), ) ) @@ -102,7 +129,14 @@ def new_unseen_orders_table(context: dict, area: Area | None = None) -> dict: then=models.Count("analysisorder__samples", distinct=True), ), default=0, - ) + ), + delivery_date=models.Case( + models.When( + analysisorder__isnull=False, + then="analysisorder__expected_delivery_date", + ), + default=models.Value(None, output_field=models.DateField()), + ), ) ) @@ -128,13 +162,25 @@ def assigned_orders_table(context: dict) -> dict: status__in=[ Order.OrderStatus.PROCESSING, Order.OrderStatus.DELIVERED, - Order.OrderStatus.COMPLETED, ], responsible_staff=user, ) .select_related("genrequest") .annotate( - sample_count=models.Count("extractionorder__samples", distinct=True), + isolated_sample_count=models.Count( + "sample_status_assignments", + distinct=True, + filter=models.Q( + sample_status_assignments__status=SampleStatusAssignment.SampleStatus.ISOLATED, + ), + ), + sample_count=models.Case( + models.When( + extractionorder__isnull=False, + then=models.Count("extractionorder__samples", distinct=True), + ), + default=0, + ), priority=models.Case( models.When(is_urgent=True, then=Order.OrderPriority.URGENT), models.When(is_prioritized=True, then=Order.OrderPriority.PRIORITIZED), @@ -173,6 +219,13 @@ def draft_orders_table(context: dict, area: Area) -> dict: models.When(is_urgent=True, then=Order.OrderPriority.URGENT), default=1, ), + delivery_date=models.Case( + models.When( + analysisorder__isnull=False, + then="analysisorder__expected_delivery_date", + ), + default=models.Value(None, output_field=models.DateField()), + ), ) .order_by("-priority", "-created_at") ) diff --git a/src/staff/urls.py b/src/staff/urls.py index b43c512e..65482634 100644 --- a/src/staff/urls.py +++ b/src/staff/urls.py @@ -2,20 +2,18 @@ from .views import ( AnalysisOrderDetailView, - AnalysisOrderListView, DashboardView, EquipmentOrderDetailView, EqupimentOrderListView, ExtractionOrderDetailView, - ExtractionOrderListView, ExtractionPlateCreateView, ExtractionPlateDetailView, ExtractionPlateListView, GenerateGenlabIDsView, - ManaullyCheckedOrderActionView, MarkAsSeenView, OrderAnalysisSamplesListView, OrderExtractionSamplesListView, + OrderListView, OrderPrioritizedAdminView, OrderToDraftActionView, OrderToNextStatusActionView, @@ -41,19 +39,12 @@ ProjectValidateActionView.as_view(), name="projects-verify", ), - path( - "orders/analysis/", AnalysisOrderListView.as_view(), name="order-analysis-list" - ), + path("orders/", OrderListView.as_view(), name="order-list"), path( "orders/equipment/", EqupimentOrderListView.as_view(), name="order-equipment-list", ), - path( - "orders/extraction/", - ExtractionOrderListView.as_view(), - name="order-extraction-list", - ), path( "orders/analysis//", AnalysisOrderDetailView.as_view(), @@ -69,11 +60,6 @@ OrderToNextStatusActionView.as_view(), name="order-to-next-status", ), - path( - "orders//manually-checked/", - ManaullyCheckedOrderActionView.as_view(), - name="order-manually-checked", - ), path( "//add-staff/", StaffEditView.as_view(), diff --git a/src/staff/views.py b/src/staff/views.py index 7b44831f..775e7ca2 100644 --- a/src/staff/views.py +++ b/src/staff/views.py @@ -4,7 +4,8 @@ from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.db import models -from django.db.models import Count +from django.db.models import Exists, OuterRef +from django.db.models.functions import Concat from django.forms import Form from django.http import HttpRequest, HttpResponse, HttpResponseRedirect, JsonResponse from django.shortcuts import get_object_or_404 @@ -34,19 +35,19 @@ from shared.views import ActionView from .filters import ( - AnalysisOrderFilter, ExtractionPlateFilter, + OrderFilter, OrderSampleFilter, SampleFilter, SampleMarkerOrderFilter, ) from .forms import ExtractionPlateForm, OrderStaffForm from .tables import ( - AnalysisOrderTable, + CombinedOrder, EquipmentOrderTable, - ExtractionOrderTable, OrderAnalysisSampleTable, OrderExtractionSampleTable, + OrderTable, PlateTable, ProjectTable, SampleStatusTable, @@ -92,44 +93,93 @@ def get_context_data(self, **kwargs) -> dict[str, Any]: return context -class AnalysisOrderListView(StaffMixin, SingleTableMixin, FilterView): - model = AnalysisOrder - table_class = AnalysisOrderTable - filterset_class = AnalysisOrderFilter +class OrderListView(StaffMixin, SingleTableMixin, FilterView): + model = ExtractionOrder + table_class = OrderTable + filterset_class = OrderFilter - def get_queryset(self) -> models.QuerySet[AnalysisOrder]: - return ( + def get_queryset(self) -> models.QuerySet[ExtractionOrder]: + queryset = ( super() .get_queryset() .select_related( "genrequest", - "polymorphic_ctype", - "genrequest__samples_owner", "genrequest__project", "genrequest__area", ) + .annotate( + sample_count=models.Count("samples", distinct=True), + sample_isolated_count=models.Count( + "sample_status_assignments", + distinct=True, + filter=models.Q( + sample_status_assignments__status=SampleStatusAssignment.SampleStatus.ISOLATED, + ), + ), + ) + .prefetch_related("analysis_orders") ) + # TODO: Filter + return queryset -class ExtractionOrderListView(StaffMixin, SingleTableMixin, FilterView): - model = ExtractionOrder - table_class = ExtractionOrderTable - filterset_class = AnalysisOrderFilter - - def get_queryset(self) -> models.QuerySet[ExtractionOrder]: - return ( - super() - .get_queryset() - .select_related( - "genrequest", - "genrequest__samples_owner", - "polymorphic_ctype", - "genrequest__project", - "genrequest__area", + def get_table_data(self) -> list[CombinedOrder]: + flattened = [] + for extraction_order in self.get_queryset(): + assigned = list( + extraction_order.responsible_staff.all() + .annotate( + full_name=Concat("first_name", models.Value(" "), "last_name") + ) + .values_list("full_name", flat=True) ) - .prefetch_related("species", "sample_types") - .annotate(sample_count=Count("samples")) - ) + priority = Order.OrderPriority.NORMAL + if extraction_order.is_urgent: + priority = Order.OrderPriority.URGENT + + analysis_orders = extraction_order.analysis_orders.annotate( + sample_count=models.Count("samples", distinct=True), + ).all() + + if analysis_orders: + for analysis_order in analysis_orders: + assigned.extend( + analysis_order.responsible_staff.all() + .annotate( + full_name=Concat( + "first_name", models.Value(" "), "last_name" + ) + ) + .values_list("full_name", flat=True) + .iterator() + ) + if analysis_order.is_urgent: + priority = Order.OrderPriority.URGENT + + combined_order = CombinedOrder( + extraction_order=extraction_order, + analysis_order=analysis_order, + priority=priority, + assigned_staff=assigned, + ) + flattened.append(combined_order) + else: + flattened.append( + CombinedOrder( + extraction_order=extraction_order, + priority=priority, + assigned_staff=assigned, + ) + ) + + status_order = [ + Order.OrderStatus.DELIVERED, + Order.OrderStatus.DRAFT, + Order.OrderStatus.PROCESSING, + Order.OrderStatus.COMPLETED, + ] + flattened.sort(key=lambda x: (status_order.index(x.status()))) + return flattened class ExtractionPlateListView(StaffMixin, SingleTableMixin, FilterView): @@ -150,7 +200,7 @@ def get_queryset(self) -> models.QuerySet[ExtractionPlate]: class EqupimentOrderListView(StaffMixin, SingleTableMixin, FilterView): model = EquipmentOrder table_class = EquipmentOrderTable - filterset_class = AnalysisOrderFilter + filterset_class = OrderFilter def get_queryset(self) -> models.QuerySet[EquipmentOrder]: return ( @@ -190,6 +240,17 @@ def get_object(self) -> Order: def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: try: order = self.get_object() + + if not order.genrequest.responsible_staff.filter( + id=request.user.id + ).exists(): + messages.error( + request, _("You are not authorized to mark this order as seen.") + ) + return HttpResponseRedirect( + self.get_return_url(request.POST.get("return_to")) + ) + order.toggle_seen() messages.success(request, _("Order is marked as seen")) except Exception as e: @@ -353,8 +414,10 @@ def get_isolation_methods(self) -> list[str]: samples = Sample.objects.filter(order=order) species_ids = samples.values_list("species_id", flat=True).distinct() - return IsolationMethod.objects.filter(species_id__in=species_ids).values_list( - "name", flat=True + return ( + IsolationMethod.objects.filter(species_id__in=species_ids) + .values_list("name", flat=True) + .distinct() ) def get_base_fields(self) -> list[str]: @@ -376,7 +439,7 @@ def get_success_url(self) -> str: def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: status_name = request.POST.get("status") - selected_ids = request.POST.getlist("checked") + selected_ids = request.POST.getlist(f"checked-{self.get_order().pk}") isolation_method = request.POST.get("isolation_method") if not selected_ids: @@ -390,6 +453,10 @@ def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: if status_name: self.assign_status_to_samples(samples, status_name, order, request) + if status_name == "isolated": + # Cannot use "samples" here + # because we need to check all samples in the order + self.check_all_isolated(Sample.objects.filter(order=order)) if isolation_method: self.update_isolation_methods(samples, isolation_method, request) return HttpResponseRedirect(self.get_success_url()) @@ -436,6 +503,25 @@ def assign_status_to_samples( request, f"{samples.count()} samples updated with status '{status_name}'." ) + # Checks if all samples in the order are isolated + # If they are, it updates the order status to completed + def check_all_isolated(self, samples: models.QuerySet) -> None: + samples_with_flag = samples.annotate( + has_isolated=Exists( + SampleStatusAssignment.objects.filter( + sample=OuterRef("pk"), + status=SampleStatusAssignment.SampleStatus.ISOLATED, + ) + ) + ) + + if not samples_with_flag.filter(has_isolated=False).exists(): + self.get_order().to_next_status() + messages.success( + self.request, + "All samples are isolated. The order status is updated to completed.", + ) + def update_isolation_methods( self, samples: models.QuerySet, isolation_method: str, request: HttpRequest ) -> None: @@ -444,7 +530,9 @@ def update_isolation_methods( ).first() try: - im = IsolationMethod.objects.get(name=selected_isolation_method.name) + im = IsolationMethod.objects.filter( + name=selected_isolation_method.name + ).first() except IsolationMethod.DoesNotExist: messages.error( request, @@ -485,40 +573,6 @@ def post(self, request: HttpRequest, *args, **kwargs) -> JsonResponse: return JsonResponse({"error": "Sample not found"}, status=404) -class ManaullyCheckedOrderActionView(SingleObjectMixin, ActionView): - model = ExtractionOrder - - def get_queryset(self) -> models.QuerySet[ExtractionOrder]: - return ExtractionOrder.objects.filter(status=Order.OrderStatus.DELIVERED) - - def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: - self.object = self.get_object() - return super().post(request, *args, **kwargs) - - def form_valid(self, form: Form) -> HttpResponse: - try: - # TODO: check state transition - self.object.order_manually_checked() - messages.add_message( - self.request, - messages.SUCCESS, - _("The order was checked, GenLab IDs will be generated"), - ) - except Exception as e: - messages.error(self.request, f"Error: {str(e)}") - - return super().form_valid(form) - - def get_success_url(self) -> str: - return reverse_lazy( - f"staff:order-{self.object.get_type()}-detail", - kwargs={"pk": self.object.id}, - ) - - def form_invalid(self, form: Form) -> HttpResponse: - return HttpResponseRedirect(self.get_success_url()) - - class StaffEditView(StaffMixin, SingleObjectMixin, TemplateView): form_class = OrderStaffForm template_name = "staff/order_staff_edit.html" @@ -527,7 +581,7 @@ def get_queryset(self) -> models.QuerySet[Order] | models.QuerySet[Genrequest]: model_type = self._get_model_type() if model_type == "genrequest": return Genrequest.objects.all() - return Order.objects.filter(status=Order.OrderStatus.DELIVERED) + return Order.objects.all() def _get_model_type(self) -> str: """Returns model type based on request data.""" @@ -640,7 +694,10 @@ def form_valid(self, form: Form) -> HttpResponse: return super().form_valid(form) def get_success_url(self) -> str: - return reverse_lazy(f"staff:order-{self.object.get_type()}-list") + return reverse_lazy( + f"staff:order-{self.object.get_type()}-detail", + kwargs={"pk": self.object.pk}, + ) def form_invalid(self, form: Form) -> HttpResponse: return HttpResponseRedirect(self.get_success_url()) diff --git a/src/templates/components/action-button.html b/src/templates/components/action-button.html index 664403c3..11c2e216 100644 --- a/src/templates/components/action-button.html +++ b/src/templates/components/action-button.html @@ -6,5 +6,5 @@
{% csrf_token %} {% if form %}{{ form }}{% endif %} - +
diff --git a/src/templates/components/formset.html b/src/templates/components/formset.html index 8578faf1..e9576d8e 100644 --- a/src/templates/components/formset.html +++ b/src/templates/components/formset.html @@ -7,8 +7,8 @@ {% if form_collection %}{{ form_collection }}{% endif %} {% if form %}{{ form }}{% endif %} - - + + diff --git a/src/templates/users/user_detail.html b/src/templates/users/user_detail.html index 098dab6c..4e4a2e27 100644 --- a/src/templates/users/user_detail.html +++ b/src/templates/users/user_detail.html @@ -17,8 +17,8 @@

{{ object }}

- My Info - My Info + E-Mail diff --git a/src/templates/users/user_form.html b/src/templates/users/user_form.html index a932af99..fd63f9a8 100644 --- a/src/templates/users/user_form.html +++ b/src/templates/users/user_form.html @@ -15,7 +15,7 @@

{{ user }}

{{ form|crispy }}
- +
diff --git a/src/theme/static_src/src/styles.css b/src/theme/static_src/src/styles.css index 702e889d..3caab71c 100644 --- a/src/theme/static_src/src/styles.css +++ b/src/theme/static_src/src/styles.css @@ -231,6 +231,70 @@ ol.breadcrumb > li + ::before { background-color: transparent; } +.select2-container { + width: 100% !important; +} + .select2-container .select2-selection--single { - height: 3.2rem !important; + height: 2.6rem !important; + position: relative; /* Needed for absolute positioning inside */ + padding-right: 2.5rem; + display: flex; + align-items: center; + border-color: rgb(209 213 219); + opacity: 80%; +} + +.select2-container .select2-selection__arrow { + position: absolute; + top: 50%; + right: 0.75rem; + transform: translateY(20%); + width: 1rem; + height: 1rem; + pointer-events: none; +} + +.custom_order_button { + background-color: #F2F2F2; /* Fill */ + border: 1px solid #BABABA; /* Stroke */ + color: #4A4A4A; /* Text & icon color */ +} + +.custom_order_button:hover { + background-color: #e0e0e0; /* Optional: darker on hover */ + border: 1px solid #A0A0A0; /* Stroke */ +} + +.custom_order_button_blue { + background-color: #E6F0FA; /* Fill */ + border: 1px solid #8CA5BE; /* Stroke */ + color: #0F3D6A; /* Text & icon color */ +} + +.custom_order_button_blue:hover { + background-color: #D2E4F5; /* Optional: darker on hover */ + border: 1px solid #7792AB; /* Stroke */ +} + +.custom_order_button_green { + background-color: #DFF7CA; /* Fill */ + border: 1px solid #93AD7D; /* Stroke */ + color: #001D3A; /* Text & icon color */ +} + +.custom_order_button_green:hover { + background-color: #C9EBB0; /* Optional: darker on hover */ + border: 1px solid #7F996C; /* Stroke */ +} + +.custom_order_button_red { + background-color: #f7caca; /* Fill */ + border: 1px solid #ad7d7d; /* Stroke */ + color: #3a0000; /* Text & icon color */ +} + +.custom_order_button_red:hover { + background-color: #ebb0b0; /* Optional: darker on hover */ + border: 1px solid #996c6c; /* Stroke */ }