Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 5.2.3 on 2025-07-22 13:00

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
(
"genlab_bestilling",
"0029_alter_order_contact_email_alter_order_contact_person",
),
]

operations = [
migrations.AddField(
model_name="samplemarkeranalysis",
name="has_pcr",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="samplemarkeranalysis",
name="is_analysed",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="samplemarkeranalysis",
name="is_outputted",
field=models.BooleanField(default=False),
),
]
5 changes: 5 additions & 0 deletions src/genlab_bestilling/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,11 @@ class SampleMarkerAnalysis(models.Model):
marker = models.ForeignKey(f"{an}.Marker", on_delete=models.PROTECT)
transaction = models.UUIDField(blank=True, null=True)

# Fields for status tracking
has_pcr = models.BooleanField(default=False)
is_analysed = models.BooleanField(default=False)
is_outputted = models.BooleanField(default=False)

objects = managers.SampleAnalysisMarkerQuerySet.as_manager()

class Meta:
Expand Down
29 changes: 9 additions & 20 deletions src/staff/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,10 +401,6 @@ class Meta(SampleBaseTable.Meta):


class OrderAnalysisSampleTable(tables.Table):
sample__plate_positions = tables.Column(
empty_values=(), orderable=False, verbose_name="Extraction position"
)

checked = tables.CheckBoxColumn(
accessor="pk",
orderable=False,
Expand All @@ -424,29 +420,22 @@ class Meta:
model = SampleMarkerAnalysis
fields = [
"checked",
"sample__genlab_id",
"sample__type",
"sample.genlab_id",
"sample.type",
"marker",
"sample__plate_positions",
"sample__isolation_method",
# "sample__pcr",
# "sample__analysis",
# "sample__output",
"sample__notes",
"sample__order",
"has_pcr",
"is_analysed",
"is_outputted",
"sample.internal_note",
"sample.order",
]
attrs = {"class": "w-full table-auto tailwind-table table-sm"}
empty_text = "No Samples"

def render_sample__plate_positions(self, value: Any) -> str:
if value:
return ", ".join([str(v) for v in value.all()])

return ""

def render_checked(self, record: Any) -> str:
order_pk = getattr(self, "order_pk", None)
return mark_safe( # noqa: S308
f'<input type="checkbox" name="checked-{record.order.id}" value="{record.id}">' # noqa: E501
f'<input type="checkbox" name="checked-analysis-{order_pk}" value="{record.id}">' # noqa: E501
)


Expand Down
40 changes: 37 additions & 3 deletions src/staff/templates/staff/samplemarkeranalysis_filter.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,54 @@
</div>
</form>

{% render_table table %}
<form method="post" action="{% url 'staff:order-analysis-samples' order.pk %}">
{% csrf_token %}
{% for status in statuses %}
<button class="btn custom_order_button_green" type="submit" name="status" value="{{ status }}">
{% if status == "pcr" %}
PCR
{% else %}
{{ status|capfirst }}
{% endif %}
</button>
{% endfor %}

{% render_table table %}
</form>

<script>
document.addEventListener("DOMContentLoaded", function () {
const selectAll = document.getElementById('select-all-checkbox');
const noteInputs = document.querySelectorAll('.internal_note-input');

selectAll?.addEventListener('change', function() {
const checkboxes = document.querySelectorAll(`input[name="checked-{{ order.pk }}"]`);
const checkboxes = document.querySelectorAll(`input[name="checked-analysis-{{ order.pk }}"]`);
checkboxes.forEach((cb) => {
cb.checked = selectAll.checked;
})
});
})

let debounceTimeout;
noteInputs.forEach(function (noteInput) {
noteInput.addEventListener("input", function (event) {
const sampleId = event.target.dataset.sampleId;
const value = event.target.value;
const formData = new FormData();
formData.append("sample_id", sampleId);
formData.append("field_name", "internal_note-input");
formData.append("field_value", value);
formData.append("csrfmiddlewaretoken", "{{ csrf_token }}");

clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(function () {
fetch("{% url 'staff:update-internal-note' %}", {
method: "POST",
body: formData
});
}, 500);
});
});
});
</script>

{% endblock page-inner %}
133 changes: 131 additions & 2 deletions src/staff/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,11 +355,19 @@ def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:


class OrderAnalysisSamplesListView(StaffMixin, SingleTableMixin, FilterView):
table_pagination = False
PCR = "pcr"
ANALYSED = "analysed"
OUTPUT = "output"
VALID_STATUSES = [PCR, ANALYSED, OUTPUT]

table_pagination = False
model = SampleMarkerAnalysis
table_class = OrderAnalysisSampleTable
filterset_class = SampleMarkerOrderFilter
template_name = "staff/samplemarkeranalysis_filter.html"

def get_order(self) -> AnalysisOrder:
return get_object_or_404(AnalysisOrder, pk=self.kwargs["pk"])

def get_queryset(self) -> QuerySet[SampleMarkerAnalysis]:
return (
Expand All @@ -378,11 +386,132 @@ def get_queryset(self) -> QuerySet[SampleMarkerAnalysis]:
)
)

def get_table_data(self) -> list[Sample]:
order = self.get_order()
return SampleMarkerAnalysis.objects.filter(order=order).select_related(
"sample__type", "sample__location", "sample__species", "marker"
)

def get_table(self, **kwargs) -> Any:
table = super().get_table(**kwargs)
table.order_pk = self.kwargs["pk"] # Inject the pk into the table
return table

def get_base_fields(self) -> list[str]:
return self.VALID_STATUSES

def get_context_data(self, **kwargs) -> dict[str, Any]:
context = super().get_context_data(**kwargs)
context["order"] = AnalysisOrder.objects.get(pk=self.kwargs.get("pk"))
order = self.get_order()
samples = self.get_table_data()

# Instantiate the filter with the current GET parameters
filterset = self.filterset_class(self.request.GET, queryset=samples)
self.object_list = filterset.qs # Ensures get_table uses the filtered queryset
table = self.get_table()

context.update(
{
"order": order,
"statuses": self.get_base_fields(),
"filter": filterset,
"table": table,
}
)
return context

def get_success_url(self) -> str:
return reverse(
"staff:order-analysis-samples", kwargs={"pk": self.get_order().pk}
)

def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
status_name = request.POST.get("status")
selected_ids = request.POST.getlist(f"checked-analysis-{self.get_order().pk}")

if not selected_ids:
messages.error(request, "No samples selected.")
return HttpResponseRedirect(self.get_success_url())

order = self.get_order()

# Get the selected samples
analyses = SampleMarkerAnalysis.objects.filter(id__in=selected_ids)

if status_name:
self.assign_status_to_samples(analyses, status_name, request)
if status_name == self.OUTPUT:
self.check_all_output(SampleMarkerAnalysis.objects.filter(order=order))
else:
self.get_order().to_processing()
return HttpResponseRedirect(self.get_success_url())

def statuses_with_lower_or_equal_priority(self, status_name: str) -> list[str]:
index = self.VALID_STATUSES.index(status_name)
return self.VALID_STATUSES[: index + 1]

def assign_status_to_samples(
self,
analyses: models.QuerySet,
status_name: str,
request: HttpRequest,
) -> None:
if status_name not in self.VALID_STATUSES:
messages.error(request, f"Status '{status_name}' is not valid.")
return

statuses_to_turn_on = self.statuses_with_lower_or_equal_priority(status_name)

if status_name == self.PCR:
field_name = "has_pcr"
elif status_name == self.ANALYSED:
field_name = "is_analysed"
else:
field_name = "is_outputted"

samples_to_turn_off_ids = list(
analyses.filter(**{field_name: True}).values_list("id", flat=True)
)
samples_to_turn_on_ids = list(
analyses.filter(**{field_name: False}).values_list("id", flat=True)
)

SampleMarkerAnalysis.objects.filter(id__in=samples_to_turn_off_ids).update(
**{field_name: False}
)

update_dict = {}
if self.PCR in statuses_to_turn_on:
update_dict["has_pcr"] = True
if self.ANALYSED in statuses_to_turn_on:
update_dict["is_analysed"] = True
if self.OUTPUT in statuses_to_turn_on:
update_dict["is_outputted"] = True

SampleMarkerAnalysis.objects.filter(id__in=samples_to_turn_on_ids).update(
**update_dict
)
messages.success(
request,
f"Set statuses {', '.join(statuses_to_turn_on)} for {analyses.count()} analyses.", # noqa: E501
)

# Checks if all samples in the order have output
# If they are, it updates the order status to completed
def check_all_output(self, analyses: models.QuerySet) -> None:
if not analyses.filter(is_outputted=False).exists():
self.get_order().to_next_status()
messages.success(
self.request,
"All samples have an output. The order status is updated to completed.",
)
elif self.get_order().status == Order.OrderStatus.COMPLETED:
self.get_order().to_processing()
messages.success(
self.request,
"Not all samples have output. The order status is updated to processing.", # noqa: E501
)


class SamplesListView(StaffMixin, SingleTableMixin, FilterView):
table_pagination = False
Expand Down