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
10 changes: 5 additions & 5 deletions src/genlab_bestilling/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
ExtractionPlate,
ExtractPlatePosition,
Genrequest,
IsolationMethod,
Location,
LocationType,
Marker,
Organization,
Sample,
SampleMarkerAnalysis,
SampleStatus,
SampleStatusAssignment,
SampleType,
Species,
Expand Down Expand Up @@ -526,9 +526,9 @@ class AnalysisResultAdmin(ModelAdmin):
]


@admin.register(SampleStatus)
class SampleStatusAdmin(ModelAdmin): ...


@admin.register(SampleStatusAssignment)
class SampleStatusAssignmentAdmin(ModelAdmin): ...


@admin.register(IsolationMethod)
class IsolationMethodAdmin(ModelAdmin): ...
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Generated by Django 5.2.3 on 2025-07-11 10:16

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("genlab_bestilling", "0025_genrequest_responsible_staff"),
]

operations = [
migrations.AlterField(
model_name="samplestatusassignment",
name="status",
field=models.CharField(
blank=True,
choices=[
("marked", "Marked"),
("plucked", "Plucked"),
("isolated", "Isolated"),
],
help_text="The status of the sample in the lab",
null=True,
verbose_name="Sample status",
),
),
migrations.RemoveField(
model_name="sample",
name="assigned_statuses",
),
migrations.AddField(
model_name="isolationmethod",
name="species",
field=models.ForeignKey(
default=None,
help_text="The species this isolation method is related to.",
on_delete=django.db.models.deletion.CASCADE,
related_name="species_isolation_methods",
to="genlab_bestilling.species",
),
),
migrations.CreateModel(
name="SampleIsolationMethod",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"isolation_method",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="sample_isolation_methods",
to="genlab_bestilling.isolationmethod",
),
),
(
"sample",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="isolation_methods",
to="genlab_bestilling.sample",
),
),
],
options={
"unique_together": {("sample", "isolation_method")},
},
),
migrations.AddField(
model_name="sample",
name="isolation_method",
field=models.ManyToManyField(
blank=True,
help_text="The isolation method used for this sample",
related_name="samples",
through="genlab_bestilling.SampleIsolationMethod",
to="genlab_bestilling.isolationmethod",
),
),
migrations.DeleteModel(
name="SampleStatus",
),
]
59 changes: 39 additions & 20 deletions src/genlab_bestilling/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,11 +657,13 @@ class Sample(models.Model):

extractions = models.ManyToManyField(f"{an}.ExtractionPlate", blank=True)
parent = models.ForeignKey("self", on_delete=models.PROTECT, null=True, blank=True)
assigned_statuses = models.ManyToManyField(
f"{an}.SampleStatus",
through=f"{an}.SampleStatusAssignment",

isolation_method = models.ManyToManyField(
f"{an}.IsolationMethod",
related_name="samples",
through=f"{an}.SampleIsolationMethod",
blank=True,
help_text="The isolation method used for this sample",
)

objects = managers.SampleQuerySet.as_manager()
Expand Down Expand Up @@ -776,29 +778,23 @@ def generate_genlab_id(self, commit: bool = True) -> str:
# assignee (one or plus?)


class SampleStatus(models.Model):
name = models.CharField(max_length=255)
weight = models.IntegerField(
default=0,
)
area = models.ForeignKey(
f"{an}.Area",
on_delete=models.CASCADE,
related_name="area_statuses",
help_text="The area this status is related to.",
)


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.ForeignKey(
f"{an}.SampleStatus",
on_delete=models.CASCADE,
related_name="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",
Expand All @@ -814,8 +810,31 @@ class Meta:
unique_together = ("sample", "status", "order")


class SampleIsolationMethod(models.Model):
sample = models.ForeignKey(
f"{an}.Sample",
on_delete=models.CASCADE,
related_name="isolation_methods",
)
isolation_method = models.ForeignKey(
f"{an}.IsolationMethod",
on_delete=models.CASCADE,
related_name="sample_isolation_methods",
)

class Meta:
unique_together = ("sample", "isolation_method")


class IsolationMethod(models.Model):
name = models.CharField(max_length=255, unique=True)
species = models.ForeignKey(
f"{an}.Species",
on_delete=models.CASCADE,
related_name="species_isolation_methods",
help_text="The species this isolation method is related to.",
default=None,
)

def __str__(self) -> str:
return self.name
Expand Down
95 changes: 58 additions & 37 deletions src/staff/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,47 +219,68 @@ def render_checked(self, record: Any) -> str:
return mark_safe(f'<input type="checkbox" name="checked" value="{record.id}">') # noqa: S308


def create_sample_table(base_fields: list[str] | None = None) -> type[tables.Table]:
class CustomSampleTable(tables.Table):
"""
This shows a checkbox in the header.
To display text in the header alongside the checkbox
override the header-property in the CheckBoxColumn class.
"""

checked = tables.CheckBoxColumn(
accessor="pk",
orderable=True,
attrs={
"th__input": {
"id": "select-all-checkbox",
},
"td__input": {
"name": "checked",
},
},
empty_values=(),
verbose_name="Mark",
)
class SampleStatusTable(tables.Table):
"""
This shows a checkbox in the header.
To display text in the header alongside the checkbox
override the header-property in the CheckBoxColumn class.
"""

for field in base_fields:
locals()[field] = tables.BooleanColumn(
verbose_name=field.capitalize(),
orderable=True,
yesno="✔,-",
default=False,
)
checked = tables.CheckBoxColumn(
accessor="pk",
orderable=False,
attrs={
"th__input": {
"id": "select-all-checkbox",
},
"td__input": {
"name": "checked",
},
},
empty_values=(),
verbose_name="Mark",
)

internal_note = tables.TemplateColumn(
template_name="staff/note_input_column.html", orderable=False
)
internal_note = tables.TemplateColumn(
template_name="staff/note_input_column.html", orderable=False
)

class Meta:
model = Sample
fields = ["checked", "genlab_id", "internal_note"] + list(base_fields)
sequence = ["checked", "genlab_id"] + list(base_fields) + ["internal_note"]
marked = tables.BooleanColumn(
verbose_name="Marked",
orderable=True,
yesno="✔,-",
default=False,
)
plucked = tables.BooleanColumn(
verbose_name="Plucked",
orderable=True,
yesno="✔,-",
default=False,
)
isolated = tables.BooleanColumn(
verbose_name="Isolated",
orderable=True,
yesno="✔,-",
default=False,
)

return CustomSampleTable
class Meta:
model = Sample
fields = [
"checked",
"genlab_id",
"internal_note",
"isolation_method",
]
sequence = [
"checked",
"genlab_id",
"marked",
"plucked",
"isolated",
"internal_note",
"isolation_method",
]


class OrderExtractionSampleTable(SampleBaseTable):
Expand Down
63 changes: 46 additions & 17 deletions src/staff/templates/staff/sample_lab.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,51 @@
{% load render_table from django_tables2 %}

{% block content %}
<h3 class="text-4xl mb-5">{% if order %}{{ order }} - Samples{% else %}Samples{% endif %}</h3>

<div class="flex gap-5 mb-5">
<a class="btn bg-primary" href="../"><i class="fas fa-arrow-left"></i> back</a>
</div>

<form method="post" action="{% url 'staff:order-extraction-samples-lab' order.pk %}">
{% csrf_token %}
{% for status in statuses %}
<button class="btn bg-blue-500 text-white mt-4 mr-2" type="submit" name="status" value="{{ status.name }}">
<i class="fa-solid fa-id-badge"></i> {{ status.name }}
</button>
{% endfor %}

{% render_table table %}
</form>
<h3 class="text-4xl mb-5">{% block page-title %}{% if order %}{{ order }} - Samples{% else %}Samples{% endif %}{% endblock page-title %}</h3>
{% block page-inner %}
<div class="flex gap-5 mb-5">
<a class="btn bg-primary" href="../"><i class="fas fa-arrow-left"></i> back</a>
</div>
<form method="post" action="{% url 'staff:order-extraction-samples-lab' order.pk %}">
{% csrf_token %}
{% for status in statuses %}
<button class="btn bg-blue-500 text-white mt-4 mr-2" type="submit" name="status" value="{{ status }}">
{{ status|capfirst }}
</button>
{% endfor %}
<div class="inline-block text-left">
<button type="button"
id="parent-dropdown-button"
onclick="document.getElementById('parent-dropdown-menu').classList.toggle('hidden')"
class="btn bg-slate-100 w-64 h-10 px-4">
<span id="selected-method-{{ record.pk }}" class="truncate block w-full text-center">
{{ record.selected_isolation_method.name|default:"Isolation method" }}
<i class="fa-solid fa-chevron-down ml-2"></i>
</span>
</button>
<div id="parent-dropdown-menu"
class="absolute z-10 mt-2 w-100 origin-top-right rounded-md bg-white shadow-lg hidden"
role="menu" aria-orientation="vertical" aria-labelledby="parent-dropdown-button">
{% if not isolation_methods %}
<p class="px-4 py-2 text-sm text-gray-700">No isolation methods available</p>
{% else %}
{% for im in isolation_methods %}
<button
type="submit"
name="isolation_method"
value="{{ im }}"
class="update-isolation-button block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-gray-700"
role="menuitem"
>
{{ im }}
</button>
{% endfor %}
{% endif %}
</div>
</div>
{% render_table table %}
</form>
{% endblock page-inner %}

<script>
document.addEventListener("DOMContentLoaded", function () {
Expand All @@ -44,7 +73,7 @@ <h3 class="text-4xl mb-5">{% if order %}{{ order }} - Samples{% else %}Samples{%

clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(function () {
fetch("{% url 'staff:update-sample' %}", {
fetch("{% url 'staff:update-internal-note' %}", {
method: "POST",
body: formData
});
Expand Down
Loading