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
82 changes: 79 additions & 3 deletions src/genlab_bestilling/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
from django import forms
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.db import transaction
from django.db.models import Model
from django.utils.html import strip_tags
from formset.renderers.tailwind import FormRenderer
from formset.utils import FormMixin
from formset.widgets import DualSortableSelector, Selectize, TextInput
Expand All @@ -14,6 +16,7 @@
from .libs.formset import ContextFormCollection
from .models import (
AnalysisOrder,
AnalysisOrderResultsCommunication,
EquimentOrderQuantity,
EquipmentOrder,
ExtractionOrder,
Expand Down Expand Up @@ -122,6 +125,12 @@ def __init__(self, *args, genrequest: Genrequest, **kwargs):

# self.fields["species"].queryset = genrequest.species.all()
self.fields["sample_types"].queryset = genrequest.sample_types.all() # type: ignore[attr-defined]
self.fields[
"contact_person"
].help_text = "Person to contact with questions about this order"
self.fields[
"contact_email"
].help_text = "Email to contact with questions about this order"

def save(self, commit: bool = True) -> EquipmentOrder:
obj = super().save(commit=False)
Expand Down Expand Up @@ -258,9 +267,8 @@ def __init__(self, *args, genrequest: Genrequest, **kwargs):

self.fields["species"].queryset = genrequest.species.all() # type: ignore[attr-defined]
self.fields["sample_types"].queryset = genrequest.sample_types.all() # type: ignore[attr-defined]
# self.fields["markers"].queryset = Marker.objects.filter(
# species__genrequests__id=genrequest.id
# ).distinct()
self.fields["contact_person"].label = "Responsible genetic researcher"
self.fields["contact_email"].label = "Responsible genetic researcher email"

def save(self, commit: bool = True) -> ExtractionOrder:
obj = super().save(commit=False)
Expand Down Expand Up @@ -329,10 +337,37 @@ def __init__(self, *args, genrequest: Genrequest, **kwargs):
" with the sample selection by pressing Submit"
)

self.fields["contact_person"].label = "Responsible genetic researcher"
self.fields["contact_email"].label = "Responsible genetic researcher email"

def clean_contact_email_results(self) -> str:
emails_raw = self.cleaned_data.get("contact_email_results", "")
emails = [e.strip() for e in emails_raw.split(",") if e.strip()]

try:
for email in emails:
validate_email(email)
except ValidationError:
msg = f"Invalid email: {email}"
raise forms.ValidationError(msg) from ValidationError(msg)
return ", ".join(emails)

def clean_contact_person_results(self) -> str:
names_raw = self.cleaned_data.get("contact_person_results", "")
names = [n.strip() for n in names_raw.split(",") if n.strip()]

for name in names:
# Optionally allow hyphens and apostrophes in names
if not all(c.isalpha() or c.isspace() or c in "-'" for c in name):
msg = f"Invalid name: {name}"
raise forms.ValidationError(msg) from ValidationError(msg)
return ", ".join(names)

def save(self, commit: bool = True) -> Model:
if not commit:
msg = "This form is always committed"
raise NotImplementedError(msg)

with transaction.atomic():
obj = super().save(commit=False)
obj.genrequest = self.genrequest
Expand All @@ -342,9 +377,38 @@ def save(self, commit: bool = True) -> Model:

if obj.from_order and not obj.name and obj.from_order.name:
obj.name = obj.from_order.name + " - Analysis"

obj.save()
self.save_m2m()
obj.populate_from_order()

# Save AnalysisOrderResultsCommunication objects
# Delete old entries first (in case of resubmission)
obj.results_contacts.all().delete()

names = [
strip_tags(n.strip())
for n in self.cleaned_data["contact_person_results"].split(",")
if n.strip()
]
emails = [
strip_tags(e.strip())
for e in self.cleaned_data["contact_email_results"].split(",")
if e.strip()
]

if names and emails:
if len(names) != len(emails):
msg = "The number of names must match the number of emails."
raise ValidationError(msg)

for name, email in zip(names, emails, strict=False):
AnalysisOrderResultsCommunication.objects.create(
analysis_order=obj,
contact_person_results=name,
contact_email_results=email,
)

return obj

def clean(self) -> None:
Expand All @@ -354,6 +418,18 @@ def clean(self) -> None:
msg = "An extraction order must be selected"
raise ValidationError(msg)

contact_person_results = forms.CharField(
label="Contact person(s) for results",
help_text="Comma-separated list of names to contact with results",
required=True,
)

contact_email_results = forms.CharField(
label="Contact email(s) for results",
help_text="Comma-separated list of emails to contact with results (must match order of names)", # noqa: E501
required=True,
)

field_order = [
"name",
"use_all_samples",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Generated by Django 5.2.3 on 2025-07-24 08:45

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


class Migration(migrations.Migration):
dependencies = [
(
"genlab_bestilling",
"0032_alter_isolationmethod_remove_species_add_type_20250722_1226",
),
]

operations = [
migrations.AlterField(
model_name="order",
name="contact_person",
field=models.CharField(
help_text="Responsible for genetic bioinformatics analysis", null=True
),
),
migrations.CreateModel(
name="AnalysisOrderResultsCommunication",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"contact_person_results",
models.CharField(
help_text="Person to contact for analysis resuls", null=True
),
),
(
"contact_email_results",
models.EmailField(
help_text="Email to send analysis results",
max_length=254,
null=True,
),
),
(
"analysis_order",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="results_contacts",
to="genlab_bestilling.analysisorder",
),
),
],
),
]
23 changes: 22 additions & 1 deletion src/genlab_bestilling/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ class OrderPriority:
contact_person = models.CharField(
null=True,
blank=False,
help_text="Person to contact with questions about this order",
help_text="Responsible for genetic bioinformatics analysis",
)
contact_email = models.EmailField(
null=True,
Expand Down Expand Up @@ -532,6 +532,27 @@ def order_selected_checked(
)


class AnalysisOrderResultsCommunication(models.Model):
analysis_order = models.ForeignKey(
f"{an}.AnalysisOrder",
on_delete=models.CASCADE,
related_name="results_contacts",
)
contact_person_results = models.CharField(
null=True,
blank=False,
help_text="Person to contact for analysis resuls",
)
contact_email_results = models.EmailField(
null=True,
blank=False,
help_text="Email to send analysis results",
)

def __str__(self):
return f"{str(self.analysis_order)} {str(self.contact_person_results)} {str(self.contact_email_results)}" # noqa: E501


class AnalysisOrder(Order):
samples = models.ManyToManyField(
f"{an}.Sample", blank=True, through="SampleMarkerAnalysis"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,27 @@ <h3 class="text-4xl mb-5">Order {{ object }}</h3>

{% object-detail object=object %}

{% if results_contacts %}
<h5 class="text-2xl my-5">Contacts for Analysis Results</h5>
<div class="bg-white p-4">
<ul class="list-disc pl-5">
{% for contact in results_contacts %}
<li>
{{ contact.contact_person_results }} —
<a href="mailto:{{ contact.contact_email_results }}" class="underline text-blue-700">
{{ contact.contact_email_results }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% else %}
<h5 class="text-2xl my-5">Contacts for Analysis Results</h5>
<div class="bg-white p-4">
<p>No contacts provided for analysis results.</p>
</div>
{% endif %}


<h5 class="text-2xl my-5">Samples to analyze</h5>
<div class="bg-white p-4">
Expand All @@ -48,7 +69,7 @@ <h5 class="text-2xl my-5">Samples to analyze</h5>
<a class="btn custom_order_button_red" href="{% url 'genrequest-analysis-delete' genrequest_id=object.genrequest_id pk=object.id %}">Delete</a>
{% endif %}
{% elif object.status == object.OrderStatus.DELIVERED %}
<a class="btn custom_order_button" href="{% url 'genrequest-analysis-samples' genrequest_id=object.genrequest_id pk=object.id %}">Samples</a>
<a class="btn custom_order_button" href="{% url 'genrequest-analysis-samples' genrequest_id=object.genrequest_id pk=object.id %}">Samples</a>
{% endif %}
</div>
{% endblock %}
30 changes: 21 additions & 9 deletions src/genlab_bestilling/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,15 @@ def gen_crumbs(self) -> list[tuple]:
(str(self.object), ""),
]

def get_context_data(self, **kwargs) -> dict[str, Any]:
context = super().get_context_data(**kwargs)
all_samples_have_no_genlab_id = not self.object.samples.exclude(
genlab_id__isnull=True
).exists()
context["all_samples_have_no_genlab_id"] = all_samples_have_no_genlab_id
context["results_contacts"] = self.object.results_contacts.all()
return context

def get_queryset(self) -> QuerySet:
return (
super()
Expand All @@ -475,15 +484,6 @@ def get_queryset(self) -> QuerySet:
.prefetch_related("sample_markers", "markers")
)

def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
context = super().get_context_data(**kwargs)
order = self.object
all_samples_have_no_genlab_id = not order.samples.exclude(
genlab_id__isnull=True
).exists()
context["all_samples_have_no_genlab_id"] = all_samples_have_no_genlab_id
return context


class ExtractionOrderDetailView(GenrequestNestedMixin, DetailView):
model = ExtractionOrder
Expand Down Expand Up @@ -826,6 +826,9 @@ def gen_crumbs(self) -> list[tuple]:
]

def get_success_url(self) -> str:
# Clear any leftover error messages before redirect
list(messages.get_messages(self.request))

obj: AnalysisOrder = self.object # type: ignore[assignment] # Possibly None
if obj.from_order:
return reverse(
Expand All @@ -843,6 +846,15 @@ def get_success_url(self) -> str:
},
)

# Override form_invalid to show errors in the form
def form_invalid(self, form: Form) -> HttpResponse:
for field, errors in form.errors.items():
field_obj = form.fields.get(field)
label = field_obj.label if field_obj is not None else field
for error in errors:
messages.error(self.request, f"{label}: {error}")
return super().form_invalid(form)


class ExtractionOrderCreateView(
GenrequestNestedMixin,
Expand Down
4 changes: 2 additions & 2 deletions src/staff/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,13 +704,13 @@ class DraftOrderTable(StaffIDMixinTable):

contact_person = tables.Column(
accessor="contact_person",
verbose_name="Contact Person",
verbose_name="Responsible genetic researcher",
orderable=False,
)

contact_email = tables.Column(
accessor="contact_email",
verbose_name="Contact Email",
verbose_name="Responsible genetic researcher email",
orderable=False,
)

Expand Down
31 changes: 28 additions & 3 deletions src/staff/templatetags/order_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@

from django import template
from django.db import models
from django.utils.html import format_html_join
from django.utils.safestring import mark_safe

from genlab_bestilling.models import Area, Order
from genlab_bestilling.models import (
AnalysisOrder,
AnalysisOrderResultsCommunication,
Area,
Order,
)

from ..tables import (
AssignedOrderTable,
Expand Down Expand Up @@ -339,11 +345,30 @@ def analysis_order_samples_detail_table(order: Order, extraction_orders: dict) -

@register.inclusion_tag("../templates/components/order-detail.html")
def contact_detail_table(order: Order) -> dict:
# Default values
result_contacts_html = "—"

# Only fetch contacts if it's an AnalysisOrder instance
if isinstance(order, AnalysisOrder):
result_contacts = (
AnalysisOrderResultsCommunication.objects.filter(analysis_order=order)
.values_list("contact_person_results", "contact_email_results")
.distinct()
)
if result_contacts:
result_contacts_html = format_html_join(
"\n",
'<div>{} — <a href="mailto:{}" class="text-blue-700 underline !text-blue-700">{}</a></div>', # noqa: E501
[(name, email, email) for name, email in result_contacts],
)

fields = {
"Samples owner of genetic project": order.genrequest.samples_owner,
"Contact person": order.contact_person,
"Contact Email": order.contact_email,
"Responsible genetic researcher": order.contact_person,
"Responsible genetic researcher email": order.contact_email,
"Contact name and email for analysis results": result_contacts_html,
}

return {
"fields": fields,
"header": "Contact",
Expand Down