diff --git a/src/genlab_bestilling/forms.py b/src/genlab_bestilling/forms.py index dcb382c7..d28798dd 100644 --- a/src/genlab_bestilling/forms.py +++ b/src/genlab_bestilling/forms.py @@ -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 @@ -14,6 +16,7 @@ from .libs.formset import ContextFormCollection from .models import ( AnalysisOrder, + AnalysisOrderResultsCommunication, EquimentOrderQuantity, EquipmentOrder, ExtractionOrder, @@ -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) @@ -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) @@ -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 @@ -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: @@ -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", diff --git a/src/genlab_bestilling/migrations/0033_alter_order_contact_person_and_more.py b/src/genlab_bestilling/migrations/0033_alter_order_contact_person_and_more.py new file mode 100644 index 00000000..051aa388 --- /dev/null +++ b/src/genlab_bestilling/migrations/0033_alter_order_contact_person_and_more.py @@ -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", + ), + ), + ], + ), + ] diff --git a/src/genlab_bestilling/models.py b/src/genlab_bestilling/models.py index 783e3607..c83f9f12 100644 --- a/src/genlab_bestilling/models.py +++ b/src/genlab_bestilling/models.py @@ -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, @@ -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" diff --git a/src/genlab_bestilling/templates/genlab_bestilling/analysisorder_detail.html b/src/genlab_bestilling/templates/genlab_bestilling/analysisorder_detail.html index 982a072d..9e264540 100644 --- a/src/genlab_bestilling/templates/genlab_bestilling/analysisorder_detail.html +++ b/src/genlab_bestilling/templates/genlab_bestilling/analysisorder_detail.html @@ -26,6 +26,27 @@
No contacts provided for analysis results.
+