Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
282 changes: 75 additions & 207 deletions src/genlab_bestilling/libs/genlabid.py
Copy link
Contributor

@emilte emilte Jul 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Det er en drastisk endring på så viktig funksjonalitet. Hvis denne branchen ikke er avhengig av refaktoreringen hadde det kanskje vært lurt å gjøre denne endringen i en egen dedikert branch slik at det er lettere å reversere i tilfelle noe problem skulle oppstå?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Er det skrevet tester rundt denne funksjonaliteten? Er vi sikre på at det fortsatt fungerer som forventet?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enig - da flytter jeg endringene i genlabid.py til en ny branch og tilpasser heller implementeringen som allerede er der.

Jeg ser ingen tester skrev rundt generering av genlabid.

Copy link
Contributor

@emilte emilte Jul 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jeg foreslår at dere kanskje setter dere sammen og prøver å skrive pytester på den funksjonen, i egen branch

Original file line number Diff line number Diff line change
@@ -1,215 +1,83 @@
from typing import Any

import sqlglot
import sqlglot.expressions
from django.db import connection, transaction
from sqlglot.expressions import (
EQ,
Alias,
Cast,
DataType,
Extract,
From,
Literal,
Subquery,
and_,
column,
)

from ..models import ExtractionOrder, Order, Sample, Species


def get_replica_for_sample() -> None:
"""
TODO: implement
"""
pass


def get_current_sequences(order_id: int | str) -> Any:
"""
Invoke a Postgres function to get the current sequence number
for a specific combination of year and species.
"""
samples = (
Sample.objects.select_related("order", "species")
.filter(order=order_id)
.values("order__created_at", "species__code")
.distinct()
)
from django.db import transaction
from django.db.models import Case, QuerySet, When

from ..models import Sample


# Format the genlab ID as GYYCODEXXXX
def generate_genlab_id(code: str, year: int, count: int) -> str:
return f"G{str(year)[-2:]}{code.upper()}{count:04d}"


# Order samples by their names, assuming they are numeric strings
def order(samples: QuerySet) -> QuerySet:
name_to_sample = {sample.name.strip(): sample for sample in samples}
names = list(name_to_sample.keys())

with connection.cursor() as cursor:
sequences = {}
for sample in samples:
query = sqlglot.select(
sqlglot.expressions.func(
"get_genlab_sequence_name",
sqlglot.expressions.Literal.string(sample["species__code"]),
sqlglot.expressions.Literal.number(
sample["order__created_at"].year
),
dialect="postgres",
)
).sql(dialect="postgres")
seq = cursor.execute(
query,
).fetchone()[0]
sequences[seq] = 0

for k in sequences.keys():
query = sqlglot.select("last_value").from_(k).sql(dialect="postgres")
sequences[k] = cursor.execute(query).fetchone()[0]

return sequences
# Check all are digits
if not all(name.isdigit() for name in names):
return samples

# Sort names as integers
sorted_names = sorted(names, key=lambda x: int(x))

# Build a CASE statement to preserve order in SQL
when_statements = [
When(name=name, then=pos) for pos, name in enumerate(sorted_names)
]

return samples.order_by(Case(*when_statements))


# New entry point for generating genlab IDs
@transaction.atomic
def generate(
order_id: int | str,
order_id: int,
sorting_order: list[str] | None = None,
selected_samples: list[Any] | None = None,
selected_samples: list[int] | None = None,
) -> None:
"""
wrapper to handle errors and reset the sequence to the current sequence value
"""
sequences = get_current_sequences(order_id)
print(sequences)

with connection.cursor() as cursor:
try:
with transaction.atomic():
cursor.execute(
update_genlab_id_query(order_id, sorting_order, selected_samples)
)
except Exception:
# if there is an error, reset the sequence
# NOTE: this is unsafe unless this function is executed in a queue
# by just one worker to prevent concurrency
with connection.cursor() as cursor: # noqa: PLW2901 # Was this intentional?
for k, v in sequences.items():
cursor.execute("SELECT setval(%s, %s)", [k, v])

sequences = get_current_sequences(order_id)
print(sequences)


def update_genlab_id_query(
order_id: int | str,
sorting_order: list[str] | None = None,
selected_samples: list[Any] | None = None,
) -> str:
"""
Safe generation of a SQL raw query using sqlglot
The query runs an update on all the rows with a specific order_id
and set genlab_id = generate_genlab_id(code, year)
"""
if sorting_order is None:
sorting_order = []

samples_table = Sample._meta.db_table
extraction_order_table = ExtractionOrder._meta.db_table
order_table = Order._meta.db_table
species_table = Species._meta.db_table

order_by_columns = (
[column(col, table=samples_table) for col in sorting_order]
if sorting_order
else [column("name", table=samples_table)]
if selected_samples is None:
return

# Sort order, default is by name
order_by = sorting_order or ["name"]

# Fetch samples
samples = (
Sample.objects.select_related("species", "order")
.filter(order_id=order_id, id__in=selected_samples, genlab_id__isnull=True)
.order_by(*order_by)
)

return sqlglot.expressions.update(
samples_table,
properties={
"genlab_id": column(
"genlab_id",
table="order_samples",
)
},
where=sqlglot.expressions.EQ(
this=column("id", table=samples_table),
expression="order_samples.id",
),
from_=From(
this=Alias(
this=Subquery(
this=(
sqlglot.select(
column(
"id",
table=samples_table,
),
Alias(
this=sqlglot.expressions.func(
"generate_genlab_id",
column("code", table=species_table),
Cast(
this=Extract(
this="YEAR",
expression=column(
"confirmed_at",
table=order_table,
),
),
to=DataType(
this=DataType.Type.INT, nested=False
),
),
),
alias="genlab_id",
),
)
.join(
extraction_order_table,
on=EQ(
this=column(
"order_id",
table=samples_table,
),
expression=column(
"order_ptr_id",
table=extraction_order_table,
),
),
)
.join(
order_table,
on=EQ(
this=column(
"order_ptr_id",
table=extraction_order_table,
),
expression=column(
"id",
table=order_table,
),
),
)
.from_(samples_table)
.join(
species_table,
on=EQ(
this=column(
"species_id",
table=samples_table,
),
expression=column(
"id",
table=species_table,
),
),
)
.where(
and_(
EQ(
this=column(col="order_id"),
expression=Literal.number(order_id),
),
column(col="genlab_id").is_(None),
)
)
.order_by(*order_by_columns)
),
),
alias="order_samples",
)
),
).sql(dialect="postgres")
# If sorting order is by name, we might need to order them numerically
if order_by == ["name"]:
samples = order(samples)

# Group counts by species + year
counts = {}
updates = []

# Iterate through samples to generate genlab IDs in correct order
for sample in list(samples):
# Extract species code and year from the sample
species_code = sample.species.code
year = sample.order.confirmed_at.year
key = (species_code, year)

# Initialize count
if key not in counts:
existing_count = Sample.objects.filter(
species__code=species_code,
order__confirmed_at__year=year,
genlab_id__isnull=False,
).count()
counts[key] = existing_count + 1
else:
counts[key] += 1

genlab_id = generate_genlab_id(species_code, year, counts[key])
sample.genlab_id = genlab_id
updates.append(sample)

# Bulk update samples with new genlab IDs
Sample.objects.bulk_update(updates, ["genlab_id"])
4 changes: 4 additions & 0 deletions src/genlab_bestilling/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,10 @@ def to_draft(self) -> None:
def get_type(self) -> str:
return "order"

@property
def filled_genlab_count(self) -> int:
return self.samples.filter(genlab_id__isnull=False).count()

@property
def next_status(self) -> OrderStatus | None:
current_index = self.STATUS_ORDER.index(self.status)
Expand Down
1 change: 1 addition & 0 deletions src/staff/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ class Meta:
]
attrs = {"class": "w-full table-auto tailwind-table table-sm"}
sequence = ("checked",)
order_by = ("genlab_id", "name_as_int", "species")

empty_text = "No Samples"

Expand Down
2 changes: 1 addition & 1 deletion src/staff/templates/staff/sample_filter.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{% load render_table from django_tables2 %}

{% block page-title %}
{% if order %}{{ order }} - Samples{% else %}Samples{% endif %}
{% if order %}{{ order }} - Samples {{ order.filled_genlab_count }} / {{ order.samples.count }}{% else %}Samples{% endif %}
{% endblock page-title %}

{% block page-inner %}
Expand Down
Loading