felicity-lims/felicity/apps/analysis/models/results.py
2024-06-29 22:22:00 +02:00

285 lines
11 KiB
Python

import logging
from datetime import datetime
from typing import Annotated
try:
from typing import Self
except ImportError:
from typing_extensions import Self
from sqlalchemy import (Boolean, Column, DateTime, ForeignKey, Integer, String, Text,
Table)
from sqlalchemy.orm import relationship
from felicity.apps import Auditable, BaseAuditDBModel, DBModel
from felicity.apps.analysis import conf, schemas
from felicity.apps.common import BaseMPTT
from felicity.apps.notification.utils import FelicityStreamer
from felicity.database.session import async_session_factory
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
streamer = FelicityStreamer()
"""
Many to Many Link between AnalysisResult and User
"""
result_verification = Table(
"result_verification",
DBModel.metadata,
Column("result_uid", ForeignKey("analysis_result.uid"), primary_key=True),
Column("user_uid", ForeignKey("user.uid"), primary_key=True),
)
class AnalysisResult(Auditable, BaseMPTT):
"""Test/Analysis Result
Number of analysis results per sample will be directly proportional to
the number of linked sample_analyses at minimum :)
"""
__tablename__ = "analysis_result"
sample_uid = Column(String, ForeignKey("sample.uid"), nullable=False)
sample = relationship("Sample", back_populates="analysis_results", lazy="selectin")
analysis_uid = Column(String, ForeignKey("analysis.uid"), nullable=False)
analysis = relationship("Analysis", backref="analysis_results", lazy="selectin")
laboratory_instrument_uid = Column(
String, ForeignKey("laboratory_instrument.uid"), nullable=True
)
laboratory_instrument = relationship("LaboratoryInstrument", lazy="selectin")
method_uid = Column(String, ForeignKey("method.uid"), nullable=True)
method = relationship("Method", lazy="selectin")
result = Column(Text, nullable=True)
analyst_uid = Column(String, ForeignKey("user.uid"), nullable=True)
analyst = relationship("User", foreign_keys=[analyst_uid], lazy="selectin")
submitted_by_uid = Column(String, ForeignKey("user.uid"), nullable=True)
submitted_by = relationship(
"User", foreign_keys=[submitted_by_uid], lazy="selectin"
)
submitted_by_name = Column(String, nullable=True)
date_submitted = Column(DateTime, nullable=True)
verified_by = relationship("User", secondary=result_verification, lazy="selectin")
verified_by_name = Column(String, nullable=True)
date_verified = Column(DateTime, nullable=True)
invalidated_by_uid = Column(String, ForeignKey("user.uid"), nullable=True)
invalidated_by = relationship(
"User", foreign_keys=[invalidated_by_uid], lazy="selectin"
)
date_invalidated = Column(DateTime, nullable=True)
cancelled_by_uid = Column(String, ForeignKey("user.uid"), nullable=True)
cancelled_by = relationship(
"User", foreign_keys=[cancelled_by_uid], lazy="selectin"
)
date_cancelled = Column(DateTime, nullable=True)
retest = Column(Boolean(), default=False)
reportable = Column(Boolean(), default=True) # for retests or reflex
status = Column(String, nullable=False)
due_date = Column(DateTime, nullable=True)
# reflex level
reflex_level = Column(Integer, nullable=True)
# worksheet
worksheet_uid = Column(String, ForeignKey("worksheet.uid"), nullable=True)
worksheet = relationship(
"WorkSheet", back_populates="analysis_results", lazy="selectin"
)
worksheet_position = Column(Integer, nullable=True)
assigned = Column(Boolean(), default=False)
@property
def keyword(self) -> str:
return self.analysis.keyword
async def verifications(self) -> tuple[
Annotated[int, "Total number required verifications"],
Annotated[int, "current number of verifications"]
]:
required = self.analysis.required_verifications if self.analysis.required_verifications else 1
current = len(self.verified_by)
return required, current
# async def last_verificator(self):
# _, verifications = await self.verifications()
# if verifications == 0:
# return None
# return self.verified_by[:-1]
async def retest_result(
self, retested_by, next_action="verify"
) -> tuple[
Annotated[
"AnalysisResult", "Newly Created AnalysisResult"] | None,
Annotated[
"AnalysisResult", "Retested AnalysisResult"]
] | None:
retest = None
if self.status in [conf.states.result.RESULTED]:
a_result_in = {
"sample_uid": self.sample.uid,
"analysis_uid": self.analysis_uid,
"status": conf.states.result.PENDING,
"laboratory_instrument_uid": self.laboratory_instrument_uid,
"method_uid": self.method_uid,
"parent_id": self.uid,
"retest": True,
}
a_result_schema = schemas.AnalysisResultCreate(**a_result_in)
retest = await AnalysisResult.create(a_result_schema)
await self.hide_report()
if next_action == "verify":
_, final = await self.verify(verifier=retested_by)
elif next_action == "retract":
final = await self.retract(retracted_by=retested_by)
else:
final = self
# transition sample back to received state
await self.sample.un_submit()
return retest, final
return retest, self
async def assign(self, ws_uid, position, laboratory_instrument_uid):
self.worksheet_uid = ws_uid
self.assigned = True
self.worksheet_position = position
self.laboratory_instrument_uid = (
laboratory_instrument_uid if laboratory_instrument_uid else None
)
return await self.save_async()
async def un_assign(self):
self.worksheet_uid = None
self.assigned = False
self.worksheet_position = None
self.laboratory_instrument_uid = None
return await self.save_async()
async def verify(self, verifier) -> tuple[bool, "AnalysisResult"]:
is_verified = False
required, current = await self.verifications()
self.updated_by_uid = verifier.uid # noqa
if current < required and current + 1 == required:
await self._verify(verifier_uid=verifier.uid)
self.status = conf.states.result.APPROVED
self.date_verified = datetime.now()
is_verified = True
# self.verified_by.append(verifier)
final = await self.save_async()
if final.status == conf.states.result.APPROVED:
await streamer.stream(final, verifier, "approved", "result")
return is_verified, final
async def _verify(self, verifier_uid):
await AnalysisResult.table_insert(
table=result_verification,
mappings={"result_uid": self.uid, "user_uid": verifier_uid}
)
async def retract(self, retracted_by) -> "AnalysisResult":
self.status = conf.states.result.RETRACTED
self.date_verified = datetime.now()
self.updated_by_uid = retracted_by.uid # noqa
final = await self.save_async()
if final.status == conf.states.result.RETRACTED:
await streamer.stream(final, retracted_by, "retracted", "result")
await self._verify(verifier_uid=retracted_by.uid)
return final
async def cancel(self, cancelled_by) -> "AnalysisResult":
if self.status in [conf.states.result.PENDING]:
self.status = conf.states.result.CANCELLED
self.cancelled_by_uid = cancelled_by.uid
self.date_cancelled = datetime.now()
self.updated_by_uid = cancelled_by.uid # noqa
final = await self.save_async()
if final.status == conf.states.result.CANCELLED:
await streamer.stream(final, cancelled_by, "cancelled", "result")
return final
async def re_instate(self, re_instated_by) -> "AnalysisResult":
if self.sample.status not in [
conf.states.sample.RECEIVED,
conf.states.sample.EXPECTED,
]:
raise Exception(
"You can only reinstate analytes of due and received samples"
)
if self.status in [conf.states.result.CANCELLED]:
self.status = conf.states.result.PENDING
self.cancelled_by_uid = None
self.date_cancelled = None
self.updated_by_uid = re_instated_by.uid # noqa
final = await self.save_async()
if final.status == conf.states.result.PENDING:
await streamer.stream(final, re_instated_by, "reinstated", "result")
return final
async def change_status(self, status) -> "AnalysisResult":
self.status = status
return await self.save_async()
async def hide_report(self) -> "AnalysisResult":
self.reportable = False
return await self.save_async()
@classmethod
async def filter_for_worksheet(
cls,
analyses_status: str,
analysis_uid: str,
sample_type_uid: list[str],
limit: int,
) -> list["AnalysisResult"]:
filters = {
"status__exact": analyses_status,
"assigned__exact": False,
"analysis_uid__exact": analysis_uid,
"sample___sample_type_uid__exact": sample_type_uid,
"sample___status": conf.states.sample.RECEIVED,
}
sort_attrs = ["-sample___priority", "sample___sample_id", "-created_at"]
analytes_stmt = cls.smart_query(filters=filters, sort_attrs=sort_attrs)
stmt = analytes_stmt.limit(limit)
# available: int = await cls.count_where(filters=filters)
async with async_session_factory() as session:
analyses_results = (await session.execute(stmt)).scalars().all()
return analyses_results
@classmethod
async def create(
cls, obj_in: dict | schemas.AnalysisResultCreate
) -> Self:
data = cls._import(obj_in)
return await super().create(**data)
async def update(
self, obj_in: dict | schemas.AnalysisResultUpdate
) -> Self:
data = self._import(obj_in)
return await super().update(**data)
class ResultMutation(BaseAuditDBModel):
"""Result Mutations tracker"""
__tablename__ = "result_mutation"
result_uid = Column(String, ForeignKey("analysis_result.uid"), nullable=False)
before = Column(String, nullable=False)
after = Column(String, nullable=False)
mutation = Column(String, nullable=False)
date = Column(DateTime, nullable=True)
@classmethod
async def create(cls, obj_in: dict | dict) -> Self:
data = cls._import(obj_in)
return await super().create(**data)