mirror of
https://github.com/beak-insights/felicity-lims.git
synced 2025-02-25 01:13:01 +08:00
285 lines
11 KiB
Python
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)
|