Can now calculate turnaround time including holidays.
This commit is contained in:
@@ -41,9 +41,9 @@ from .models import *
|
||||
def update_log(mapper, connection, target):
|
||||
logger.debug("\n\nBefore update\n\n")
|
||||
state = inspect(target)
|
||||
logger.debug(state)
|
||||
# logger.debug(state)
|
||||
update = dict(user=getuser(), time=datetime.now(), object=str(state.object), changes=[])
|
||||
logger.debug(update)
|
||||
# logger.debug(update)
|
||||
for attr in state.attrs:
|
||||
hist = attr.load_history()
|
||||
if not hist.has_changes():
|
||||
@@ -51,24 +51,24 @@ def update_log(mapper, connection, target):
|
||||
added = [str(item) for item in hist.added]
|
||||
deleted = [str(item) for item in hist.deleted]
|
||||
change = dict(field=attr.key, added=added, deleted=deleted)
|
||||
logger.debug(f"Adding: {pformat(change)}")
|
||||
try:
|
||||
update['changes'].append(change)
|
||||
except Exception as e:
|
||||
logger.error(f"Something went horribly wrong adding attr: {attr.key}: {e}")
|
||||
continue
|
||||
|
||||
logger.debug(f"Adding to audit logs: {pformat(update)}")
|
||||
# logger.debug(f"Adding: {pformat(change)}")
|
||||
if added != deleted:
|
||||
try:
|
||||
update['changes'].append(change)
|
||||
except Exception as e:
|
||||
logger.error(f"Something went wrong adding attr: {attr.key}: {e}")
|
||||
continue
|
||||
# logger.debug(f"Adding to audit logs: {pformat(update)}")
|
||||
if update['changes']:
|
||||
# Note: must use execute as the session will be busy at this point.
|
||||
# https://medium.com/@singh.surbhicse/creating-audit-table-to-log-insert-update-and-delete-changes-in-flask-sqlalchemy-f2ca53f7b02f
|
||||
table = AuditLog.__table__
|
||||
logger.debug(f"Adding to {table}")
|
||||
# logger.debug(f"Adding to {table}")
|
||||
connection.execute(table.insert().values(**update))
|
||||
# logger.debug("Here is where I would insert values, if I was able.")
|
||||
else:
|
||||
logger.info(f"No changes detected, not updating logs.")
|
||||
|
||||
|
||||
# event.listen(LogMixin, 'after_update', update_log, propagate=True)
|
||||
# event.listen(LogMixin, 'after_insert', update_log, propagate=True)
|
||||
event.listen(LogMixin, 'after_update', update_log, propagate=True)
|
||||
event.listen(LogMixin, 'after_insert', update_log, propagate=True)
|
||||
|
||||
67
src/submissions/backend/db/models/audit.py
Normal file
67
src/submissions/backend/db/models/audit.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from dateutil.parser import parse
|
||||
from sqlalchemy.orm import declarative_base, DeclarativeMeta, Query
|
||||
from . import BaseClass
|
||||
from sqlalchemy import Column, INTEGER, String, JSON, TIMESTAMP, func
|
||||
from datetime import date, datetime, timedelta
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
Base: DeclarativeMeta = declarative_base()
|
||||
|
||||
class AuditLog(Base):
|
||||
|
||||
__tablename__ = "_auditlog"
|
||||
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
user = Column(String(64))
|
||||
time = Column(TIMESTAMP)
|
||||
object = Column(String(64))
|
||||
changes = Column(JSON)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.user} @ {self.time}>"
|
||||
|
||||
@classmethod
|
||||
def query(cls, start_date: date | str | int | None = None, end_date: date | str | int | None = None, ):
|
||||
session = BaseClass.__database_session__
|
||||
query: Query = session.query(cls)
|
||||
if start_date is not None and end_date is None:
|
||||
logger.warning(f"Start date with no end date, using today.")
|
||||
end_date = date.today()
|
||||
if end_date is not None and start_date is None:
|
||||
logger.warning(f"End date with no start date, using Jan 1, 2023")
|
||||
start_date = session.query(cls, func.min(cls.time)).first()[1]
|
||||
if start_date is not None:
|
||||
# logger.debug(f"Querying with start date: {start_date} and end date: {end_date}")
|
||||
match start_date:
|
||||
case date():
|
||||
# logger.debug(f"Lookup BasicSubmission by start_date({start_date})")
|
||||
start_date = start_date.strftime("%Y-%m-%d")
|
||||
case int():
|
||||
# logger.debug(f"Lookup BasicSubmission by ordinal start_date {start_date}")
|
||||
start_date = datetime.fromordinal(
|
||||
datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d")
|
||||
case _:
|
||||
# logger.debug(f"Lookup BasicSubmission by parsed str start_date {start_date}")
|
||||
start_date = parse(start_date).strftime("%Y-%m-%d")
|
||||
match end_date:
|
||||
case date() | datetime():
|
||||
# logger.debug(f"Lookup BasicSubmission by end_date({end_date})")
|
||||
end_date = end_date + timedelta(days=1)
|
||||
end_date = end_date.strftime("%Y-%m-%d")
|
||||
case int():
|
||||
# logger.debug(f"Lookup BasicSubmission by ordinal end_date {end_date}")
|
||||
end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date() + timedelta(days=1)
|
||||
end_date = end_date.strftime("%Y-%m-%d")
|
||||
case _:
|
||||
# logger.debug(f"Lookup BasicSubmission by parsed str end_date {end_date}")
|
||||
end_date = parse(end_date) + timedelta(days=1)
|
||||
end_date = end_date.strftime("%Y-%m-%d")
|
||||
# logger.debug(f"Compensating for same date by using time")
|
||||
if start_date == end_date:
|
||||
start_date = datetime.strptime(start_date, "%Y-%m-%d").strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||
query = query.filter(cls.time == start_date)
|
||||
else:
|
||||
query = query.filter(cls.time.between(start_date, end_date))
|
||||
return query.all()
|
||||
@@ -142,97 +142,97 @@ class Control(BaseClass):
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.controltype_name}({self.name})>"
|
||||
|
||||
# @classmethod
|
||||
# @setup_lookup
|
||||
# def query(cls,
|
||||
# submission_type: str | None = None,
|
||||
# subtype: str | None = None,
|
||||
# start_date: date | str | int | None = None,
|
||||
# end_date: date | str | int | None = None,
|
||||
# control_name: str | None = None,
|
||||
# limit: int = 0, **kwargs
|
||||
# ) -> Control | List[Control]:
|
||||
# """
|
||||
# Lookup control objects in the database based on a number of parameters.
|
||||
#
|
||||
# Args:
|
||||
# submission_type (str | None, optional): Control archetype. Defaults to None.
|
||||
# start_date (date | str | int | None, optional): Beginning date to search by. Defaults to 2023-01-01 if end_date not None.
|
||||
# end_date (date | str | int | None, optional): End date to search by. Defaults to today if start_date not None.
|
||||
# control_name (str | None, optional): Name of control. Defaults to None.
|
||||
# limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
|
||||
#
|
||||
# Returns:
|
||||
# PCRControl|List[PCRControl]: Control object of interest.
|
||||
# """
|
||||
# from backend.db import SubmissionType
|
||||
# query: Query = cls.__database_session__.query(cls)
|
||||
# match submission_type:
|
||||
# case str():
|
||||
# from backend import BasicSubmission, SubmissionType
|
||||
# # logger.debug(f"Lookup controls by SubmissionType str: {submission_type}")
|
||||
# query = query.join(BasicSubmission).join(SubmissionType).filter(SubmissionType.name == submission_type)
|
||||
# case SubmissionType():
|
||||
# from backend import BasicSubmission
|
||||
# # logger.debug(f"Lookup controls by SubmissionType: {submission_type}")
|
||||
# query = query.join(BasicSubmission).filter(BasicSubmission.submission_type_name == submission_type.name)
|
||||
# case _:
|
||||
# pass
|
||||
# # NOTE: by control type
|
||||
# match subtype:
|
||||
# case str():
|
||||
# if cls.__name__ == "Control":
|
||||
# raise ValueError(f"Cannot query base class Control with subtype.")
|
||||
# elif cls.__name__ == "IridaControl":
|
||||
# query = query.filter(cls.subtype == subtype)
|
||||
# else:
|
||||
# try:
|
||||
# query = query.filter(cls.subtype == subtype)
|
||||
# except AttributeError as e:
|
||||
# logger.error(e)
|
||||
# case _:
|
||||
# pass
|
||||
# # NOTE: by date range
|
||||
# if start_date is not None and end_date is None:
|
||||
# logger.warning(f"Start date with no end date, using today.")
|
||||
# end_date = date.today()
|
||||
# if end_date is not None and start_date is None:
|
||||
# logger.warning(f"End date with no start date, using 90 days ago.")
|
||||
# # start_date = date(2023, 1, 1)
|
||||
# start_date = date.today() - timedelta(days=90)
|
||||
# if start_date is not None:
|
||||
# match start_date:
|
||||
# case date():
|
||||
# # logger.debug(f"Lookup control by start date({start_date})")
|
||||
# start_date = start_date.strftime("%Y-%m-%d")
|
||||
# case int():
|
||||
# # logger.debug(f"Lookup control by ordinal start date {start_date}")
|
||||
# start_date = datetime.fromordinal(
|
||||
# datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d")
|
||||
# case _:
|
||||
# # logger.debug(f"Lookup control with parsed start date {start_date}")
|
||||
# start_date = parse(start_date).strftime("%Y-%m-%d")
|
||||
# match end_date:
|
||||
# case date():
|
||||
# # logger.debug(f"Lookup control by end date({end_date})")
|
||||
# end_date = end_date.strftime("%Y-%m-%d")
|
||||
# case int():
|
||||
# # logger.debug(f"Lookup control by ordinal end date {end_date}")
|
||||
# end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date().strftime(
|
||||
# "%Y-%m-%d")
|
||||
# case _:
|
||||
# # logger.debug(f"Lookup control with parsed end date {end_date}")
|
||||
# end_date = parse(end_date).strftime("%Y-%m-%d")
|
||||
# # logger.debug(f"Looking up BasicSubmissions from start date: {start_date} and end date: {end_date}")
|
||||
# query = query.filter(cls.submitted_date.between(start_date, end_date))
|
||||
# match control_name:
|
||||
# case str():
|
||||
# # logger.debug(f"Lookup control by name {control_name}")
|
||||
# query = query.filter(cls.name.startswith(control_name))
|
||||
# limit = 1
|
||||
# case _:
|
||||
# pass
|
||||
# return cls.execute_query(query=query, limit=limit)
|
||||
@classmethod
|
||||
@setup_lookup
|
||||
def query(cls,
|
||||
submission_type: str | None = None,
|
||||
subtype: str | None = None,
|
||||
start_date: date | str | int | None = None,
|
||||
end_date: date | str | int | None = None,
|
||||
name: str | None = None,
|
||||
limit: int = 0, **kwargs
|
||||
) -> Control | List[Control]:
|
||||
"""
|
||||
Lookup control objects in the database based on a number of parameters.
|
||||
|
||||
Args:
|
||||
submission_type (str | None, optional): Control archetype. Defaults to None.
|
||||
start_date (date | str | int | None, optional): Beginning date to search by. Defaults to 2023-01-01 if end_date not None.
|
||||
end_date (date | str | int | None, optional): End date to search by. Defaults to today if start_date not None.
|
||||
name (str | None, optional): Name of control. Defaults to None.
|
||||
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
|
||||
|
||||
Returns:
|
||||
Control|List[Control]: Control object of interest.
|
||||
"""
|
||||
from backend.db import SubmissionType
|
||||
query: Query = cls.__database_session__.query(cls)
|
||||
match submission_type:
|
||||
case str():
|
||||
from backend import BasicSubmission, SubmissionType
|
||||
# logger.debug(f"Lookup controls by SubmissionType str: {submission_type}")
|
||||
query = query.join(BasicSubmission).join(SubmissionType).filter(SubmissionType.name == submission_type)
|
||||
case SubmissionType():
|
||||
from backend import BasicSubmission
|
||||
# logger.debug(f"Lookup controls by SubmissionType: {submission_type}")
|
||||
query = query.join(BasicSubmission).filter(BasicSubmission.submission_type_name == submission_type.name)
|
||||
case _:
|
||||
pass
|
||||
# NOTE: by control type
|
||||
match subtype:
|
||||
case str():
|
||||
if cls.__name__ == "Control":
|
||||
raise ValueError(f"Cannot query base class Control with subtype.")
|
||||
elif cls.__name__ == "IridaControl":
|
||||
query = query.filter(cls.subtype == subtype)
|
||||
else:
|
||||
try:
|
||||
query = query.filter(cls.subtype == subtype)
|
||||
except AttributeError as e:
|
||||
logger.error(e)
|
||||
case _:
|
||||
pass
|
||||
# NOTE: by date range
|
||||
if start_date is not None and end_date is None:
|
||||
logger.warning(f"Start date with no end date, using today.")
|
||||
end_date = date.today()
|
||||
if end_date is not None and start_date is None:
|
||||
logger.warning(f"End date with no start date, using 90 days ago.")
|
||||
# start_date = date(2023, 1, 1)
|
||||
start_date = date.today() - timedelta(days=90)
|
||||
if start_date is not None:
|
||||
match start_date:
|
||||
case date():
|
||||
# logger.debug(f"Lookup control by start date({start_date})")
|
||||
start_date = start_date.strftime("%Y-%m-%d")
|
||||
case int():
|
||||
# logger.debug(f"Lookup control by ordinal start date {start_date}")
|
||||
start_date = datetime.fromordinal(
|
||||
datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d")
|
||||
case _:
|
||||
# logger.debug(f"Lookup control with parsed start date {start_date}")
|
||||
start_date = parse(start_date).strftime("%Y-%m-%d")
|
||||
match end_date:
|
||||
case date():
|
||||
# logger.debug(f"Lookup control by end date({end_date})")
|
||||
end_date = end_date.strftime("%Y-%m-%d")
|
||||
case int():
|
||||
# logger.debug(f"Lookup control by ordinal end date {end_date}")
|
||||
end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date().strftime(
|
||||
"%Y-%m-%d")
|
||||
case _:
|
||||
# logger.debug(f"Lookup control with parsed end date {end_date}")
|
||||
end_date = parse(end_date).strftime("%Y-%m-%d")
|
||||
# logger.debug(f"Looking up BasicSubmissions from start date: {start_date} and end date: {end_date}")
|
||||
query = query.filter(cls.submitted_date.between(start_date, end_date))
|
||||
match name:
|
||||
case str():
|
||||
# logger.debug(f"Lookup control by name {control_name}")
|
||||
query = query.filter(cls.name.startswith(name))
|
||||
limit = 1
|
||||
case _:
|
||||
pass
|
||||
return cls.execute_query(query=query, limit=limit)
|
||||
|
||||
@classmethod
|
||||
def find_polymorphic_subclass(cls, polymorphic_identity: str | ControlType | None = None,
|
||||
@@ -323,82 +323,82 @@ class PCRControl(Control):
|
||||
return dict(name=self.name, ct=self.ct, subtype=self.subtype, target=self.target, reagent_lot=self.reagent_lot,
|
||||
submitted_date=self.submitted_date.date())
|
||||
|
||||
@classmethod
|
||||
@setup_lookup
|
||||
def query(cls,
|
||||
submission_type: str | None = None,
|
||||
start_date: date | str | int | None = None,
|
||||
end_date: date | str | int | None = None,
|
||||
control_name: str | None = None,
|
||||
limit: int = 0
|
||||
) -> Control | List[Control]:
|
||||
"""
|
||||
Lookup control objects in the database based on a number of parameters.
|
||||
|
||||
Args:
|
||||
submission_type (str | None, optional): Control archetype. Defaults to None.
|
||||
start_date (date | str | int | None, optional): Beginning date to search by. Defaults to 2023-01-01 if end_date not None.
|
||||
end_date (date | str | int | None, optional): End date to search by. Defaults to today if start_date not None.
|
||||
control_name (str | None, optional): Name of control. Defaults to None.
|
||||
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
|
||||
|
||||
Returns:
|
||||
PCRControl|List[PCRControl]: Control object of interest.
|
||||
"""
|
||||
from backend.db import SubmissionType
|
||||
query: Query = cls.__database_session__.query(cls)
|
||||
# NOTE: by date range
|
||||
if start_date is not None and end_date is None:
|
||||
logger.warning(f"Start date with no end date, using today.")
|
||||
end_date = date.today()
|
||||
if end_date is not None and start_date is None:
|
||||
logger.warning(f"End date with no start date, using 90 days ago.")
|
||||
# start_date = date(2023, 1, 1)
|
||||
start_date = date.today() - timedelta(days=90)
|
||||
if start_date is not None:
|
||||
match start_date:
|
||||
case date():
|
||||
# logger.debug(f"Lookup control by start date({start_date})")
|
||||
start_date = start_date.strftime("%Y-%m-%d")
|
||||
case int():
|
||||
# logger.debug(f"Lookup control by ordinal start date {start_date}")
|
||||
start_date = datetime.fromordinal(
|
||||
datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d")
|
||||
case _:
|
||||
# logger.debug(f"Lookup control with parsed start date {start_date}")
|
||||
start_date = parse(start_date).strftime("%Y-%m-%d")
|
||||
match end_date:
|
||||
case date():
|
||||
# logger.debug(f"Lookup control by end date({end_date})")
|
||||
end_date = end_date.strftime("%Y-%m-%d")
|
||||
case int():
|
||||
# logger.debug(f"Lookup control by ordinal end date {end_date}")
|
||||
end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date().strftime(
|
||||
"%Y-%m-%d")
|
||||
case _:
|
||||
# logger.debug(f"Lookup control with parsed end date {end_date}")
|
||||
end_date = parse(end_date).strftime("%Y-%m-%d")
|
||||
# logger.debug(f"Looking up BasicSubmissions from start date: {start_date} and end date: {end_date}")
|
||||
query = query.filter(cls.submitted_date.between(start_date, end_date))
|
||||
match submission_type:
|
||||
case str():
|
||||
from backend import BasicSubmission, SubmissionType
|
||||
# logger.debug(f"Lookup controls by SubmissionType str: {submission_type}")
|
||||
query = query.join(BasicSubmission).join(SubmissionType).filter(SubmissionType.name == submission_type)
|
||||
case SubmissionType():
|
||||
from backend import BasicSubmission
|
||||
# logger.debug(f"Lookup controls by SubmissionType: {submission_type}")
|
||||
query = query.join(BasicSubmission).filter(BasicSubmission.submission_type_name==submission_type.name)
|
||||
case _:
|
||||
pass
|
||||
match control_name:
|
||||
case str():
|
||||
# logger.debug(f"Lookup control by name {control_name}")
|
||||
query = query.filter(cls.name.startswith(control_name))
|
||||
limit = 1
|
||||
case _:
|
||||
pass
|
||||
return cls.execute_query(query=query, limit=limit)
|
||||
# @classmethod
|
||||
# @setup_lookup
|
||||
# def query(cls,
|
||||
# submission_type: str | None = None,
|
||||
# start_date: date | str | int | None = None,
|
||||
# end_date: date | str | int | None = None,
|
||||
# name: str | None = None,
|
||||
# limit: int = 0
|
||||
# ) -> Control | List[Control]:
|
||||
# """
|
||||
# Lookup control objects in the database based on a number of parameters.
|
||||
#
|
||||
# Args:
|
||||
# submission_type (str | None, optional): Control archetype. Defaults to None.
|
||||
# start_date (date | str | int | None, optional): Beginning date to search by. Defaults to 2023-01-01 if end_date not None.
|
||||
# end_date (date | str | int | None, optional): End date to search by. Defaults to today if start_date not None.
|
||||
# control_name (str | None, optional): Name of control. Defaults to None.
|
||||
# limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
|
||||
#
|
||||
# Returns:
|
||||
# PCRControl|List[PCRControl]: Control object of interest.
|
||||
# """
|
||||
# from backend.db import SubmissionType
|
||||
# query: Query = cls.__database_session__.query(cls)
|
||||
# # NOTE: by date range
|
||||
# if start_date is not None and end_date is None:
|
||||
# logger.warning(f"Start date with no end date, using today.")
|
||||
# end_date = date.today()
|
||||
# if end_date is not None and start_date is None:
|
||||
# logger.warning(f"End date with no start date, using 90 days ago.")
|
||||
# # start_date = date(2023, 1, 1)
|
||||
# start_date = date.today() - timedelta(days=90)
|
||||
# if start_date is not None:
|
||||
# match start_date:
|
||||
# case date():
|
||||
# # logger.debug(f"Lookup control by start date({start_date})")
|
||||
# start_date = start_date.strftime("%Y-%m-%d")
|
||||
# case int():
|
||||
# # logger.debug(f"Lookup control by ordinal start date {start_date}")
|
||||
# start_date = datetime.fromordinal(
|
||||
# datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d")
|
||||
# case _:
|
||||
# # logger.debug(f"Lookup control with parsed start date {start_date}")
|
||||
# start_date = parse(start_date).strftime("%Y-%m-%d")
|
||||
# match end_date:
|
||||
# case date():
|
||||
# # logger.debug(f"Lookup control by end date({end_date})")
|
||||
# end_date = end_date.strftime("%Y-%m-%d")
|
||||
# case int():
|
||||
# # logger.debug(f"Lookup control by ordinal end date {end_date}")
|
||||
# end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date().strftime(
|
||||
# "%Y-%m-%d")
|
||||
# case _:
|
||||
# # logger.debug(f"Lookup control with parsed end date {end_date}")
|
||||
# end_date = parse(end_date).strftime("%Y-%m-%d")
|
||||
# # logger.debug(f"Looking up BasicSubmissions from start date: {start_date} and end date: {end_date}")
|
||||
# query = query.filter(cls.submitted_date.between(start_date, end_date))
|
||||
# match submission_type:
|
||||
# case str():
|
||||
# from backend import BasicSubmission, SubmissionType
|
||||
# # logger.debug(f"Lookup controls by SubmissionType str: {submission_type}")
|
||||
# query = query.join(BasicSubmission).join(SubmissionType).filter(SubmissionType.name == submission_type)
|
||||
# case SubmissionType():
|
||||
# from backend import BasicSubmission
|
||||
# # logger.debug(f"Lookup controls by SubmissionType: {submission_type}")
|
||||
# query = query.join(BasicSubmission).filter(BasicSubmission.submission_type_name==submission_type.name)
|
||||
# case _:
|
||||
# pass
|
||||
# match control_name:
|
||||
# case str():
|
||||
# # logger.debug(f"Lookup control by name {control_name}")
|
||||
# query = query.filter(cls.name.startswith(control_name))
|
||||
# limit = 1
|
||||
# case _:
|
||||
# pass
|
||||
# return cls.execute_query(query=query, limit=limit)
|
||||
|
||||
@classmethod
|
||||
@report_result
|
||||
@@ -432,7 +432,7 @@ class PCRControl(Control):
|
||||
|
||||
def to_pydantic(self):
|
||||
from backend.validators import PydPCRControl
|
||||
return PydPCRControl(**self.to_sub_dict())
|
||||
return PydPCRControl(**self.to_sub_dict(), controltype_name=self.controltype_name, submission_id=self.submission_id)
|
||||
|
||||
|
||||
class IridaControl(Control):
|
||||
@@ -569,76 +569,76 @@ class IridaControl(Control):
|
||||
cols = []
|
||||
return cols
|
||||
|
||||
@classmethod
|
||||
@setup_lookup
|
||||
def query(cls,
|
||||
sub_type: str | None = None,
|
||||
start_date: date | str | int | None = None,
|
||||
end_date: date | str | int | None = None,
|
||||
control_name: str | None = None,
|
||||
limit: int = 0
|
||||
) -> Control | List[Control]:
|
||||
"""
|
||||
Lookup control objects in the database based on a number of parameters.
|
||||
|
||||
Args:
|
||||
sub_type (models.ControlType | str | None, optional): Control archetype. Defaults to None.
|
||||
start_date (date | str | int | None, optional): Beginning date to search by. Defaults to 2023-01-01 if end_date not None.
|
||||
end_date (date | str | int | None, optional): End date to search by. Defaults to today if start_date not None.
|
||||
control_name (str | None, optional): Name of control. Defaults to None.
|
||||
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
|
||||
|
||||
Returns:
|
||||
models.Control|List[models.Control]: Control object of interest.
|
||||
"""
|
||||
query: Query = cls.__database_session__.query(cls)
|
||||
# NOTE: by control type
|
||||
match sub_type:
|
||||
case str():
|
||||
query = query.filter(cls.subtype == sub_type)
|
||||
case _:
|
||||
pass
|
||||
# NOTE: If one date exists, we need the other one to exist as well.
|
||||
if start_date is not None and end_date is None:
|
||||
logger.warning(f"Start date with no end date, using today.")
|
||||
end_date = date.today()
|
||||
if end_date is not None and start_date is None:
|
||||
logger.warning(f"End date with no start date, using 90 days ago.")
|
||||
# start_date = date(2023, 1, 1)
|
||||
start_date = date.today() - timedelta(days=90)
|
||||
if start_date is not None:
|
||||
match start_date:
|
||||
case date():
|
||||
# logger.debug(f"Lookup control by start date({start_date})")
|
||||
start_date = start_date.strftime("%Y-%m-%d")
|
||||
case int():
|
||||
# logger.debug(f"Lookup control by ordinal start date {start_date}")
|
||||
start_date = datetime.fromordinal(
|
||||
datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d")
|
||||
case _:
|
||||
# logger.debug(f"Lookup control with parsed start date {start_date}")
|
||||
start_date = parse(start_date).strftime("%Y-%m-%d")
|
||||
match end_date:
|
||||
case date():
|
||||
# logger.debug(f"Lookup control by end date({end_date})")
|
||||
end_date = end_date.strftime("%Y-%m-%d")
|
||||
case int():
|
||||
# logger.debug(f"Lookup control by ordinal end date {end_date}")
|
||||
end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date().strftime(
|
||||
"%Y-%m-%d")
|
||||
case _:
|
||||
# logger.debug(f"Lookup control with parsed end date {end_date}")
|
||||
end_date = parse(end_date).strftime("%Y-%m-%d")
|
||||
# logger.debug(f"Looking up BasicSubmissions from start date: {start_date} and end date: {end_date}")
|
||||
query = query.filter(cls.submitted_date.between(start_date, end_date))
|
||||
match control_name:
|
||||
case str():
|
||||
# logger.debug(f"Lookup control by name {control_name}")
|
||||
query = query.filter(cls.name.startswith(control_name))
|
||||
limit = 1
|
||||
case _:
|
||||
pass
|
||||
return cls.execute_query(query=query, limit=limit)
|
||||
# @classmethod
|
||||
# @setup_lookup
|
||||
# def query(cls,
|
||||
# sub_type: str | None = None,
|
||||
# start_date: date | str | int | None = None,
|
||||
# end_date: date | str | int | None = None,
|
||||
# control_name: str | None = None,
|
||||
# limit: int = 0
|
||||
# ) -> Control | List[Control]:
|
||||
# """
|
||||
# Lookup control objects in the database based on a number of parameters.
|
||||
#
|
||||
# Args:
|
||||
# sub_type (models.ControlType | str | None, optional): Control archetype. Defaults to None.
|
||||
# start_date (date | str | int | None, optional): Beginning date to search by. Defaults to 2023-01-01 if end_date not None.
|
||||
# end_date (date | str | int | None, optional): End date to search by. Defaults to today if start_date not None.
|
||||
# control_name (str | None, optional): Name of control. Defaults to None.
|
||||
# limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
|
||||
#
|
||||
# Returns:
|
||||
# models.Control|List[models.Control]: Control object of interest.
|
||||
# """
|
||||
# query: Query = cls.__database_session__.query(cls)
|
||||
# # NOTE: by control type
|
||||
# match sub_type:
|
||||
# case str():
|
||||
# query = query.filter(cls.subtype == sub_type)
|
||||
# case _:
|
||||
# pass
|
||||
# # NOTE: If one date exists, we need the other one to exist as well.
|
||||
# if start_date is not None and end_date is None:
|
||||
# logger.warning(f"Start date with no end date, using today.")
|
||||
# end_date = date.today()
|
||||
# if end_date is not None and start_date is None:
|
||||
# logger.warning(f"End date with no start date, using 90 days ago.")
|
||||
# # start_date = date(2023, 1, 1)
|
||||
# start_date = date.today() - timedelta(days=90)
|
||||
# if start_date is not None:
|
||||
# match start_date:
|
||||
# case date():
|
||||
# # logger.debug(f"Lookup control by start date({start_date})")
|
||||
# start_date = start_date.strftime("%Y-%m-%d")
|
||||
# case int():
|
||||
# # logger.debug(f"Lookup control by ordinal start date {start_date}")
|
||||
# start_date = datetime.fromordinal(
|
||||
# datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d")
|
||||
# case _:
|
||||
# # logger.debug(f"Lookup control with parsed start date {start_date}")
|
||||
# start_date = parse(start_date).strftime("%Y-%m-%d")
|
||||
# match end_date:
|
||||
# case date():
|
||||
# # logger.debug(f"Lookup control by end date({end_date})")
|
||||
# end_date = end_date.strftime("%Y-%m-%d")
|
||||
# case int():
|
||||
# # logger.debug(f"Lookup control by ordinal end date {end_date}")
|
||||
# end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date().strftime(
|
||||
# "%Y-%m-%d")
|
||||
# case _:
|
||||
# # logger.debug(f"Lookup control with parsed end date {end_date}")
|
||||
# end_date = parse(end_date).strftime("%Y-%m-%d")
|
||||
# # logger.debug(f"Looking up BasicSubmissions from start date: {start_date} and end date: {end_date}")
|
||||
# query = query.filter(cls.submitted_date.between(start_date, end_date))
|
||||
# match control_name:
|
||||
# case str():
|
||||
# # logger.debug(f"Lookup control by name {control_name}")
|
||||
# query = query.filter(cls.name.startswith(control_name))
|
||||
# limit = 1
|
||||
# case _:
|
||||
# pass
|
||||
# return cls.execute_query(query=query, limit=limit)
|
||||
|
||||
@classmethod
|
||||
def make_parent_buttons(cls, parent: QWidget) -> None:
|
||||
@@ -828,7 +828,7 @@ class IridaControl(Control):
|
||||
return df, previous_dates
|
||||
# NOTE: if date was changed, rerun with new date
|
||||
else:
|
||||
logger.warning(f"Date check failed, running recursion")
|
||||
# logger.warning(f"Date check failed, running recursion")
|
||||
df, previous_dates = cls.check_date(df, item, previous_dates)
|
||||
return df, previous_dates
|
||||
|
||||
|
||||
@@ -485,6 +485,8 @@ class Reagent(BaseClass):
|
||||
output['editable'] = ['lot', 'expiry']
|
||||
return output
|
||||
|
||||
|
||||
|
||||
def update_last_used(self, kit: KitType) -> Report:
|
||||
"""
|
||||
Updates last used reagent lot for ReagentType/KitType
|
||||
@@ -1282,7 +1284,8 @@ class SubmissionReagentAssociation(BaseClass):
|
||||
try:
|
||||
return f"<{self.submission.rsl_plate_num} & {self.reagent.lot}>"
|
||||
except AttributeError:
|
||||
return f"<Unknown Submission & {self.reagent.lot}"
|
||||
logger.error(f"Reagent {self.reagent.lot} submission association {self.reagent_id} has no submissions!")
|
||||
return f"<Unknown Submission & {self.reagent.lot}>"
|
||||
|
||||
def __init__(self, reagent=None, submission=None):
|
||||
if isinstance(reagent, list):
|
||||
@@ -1347,6 +1350,9 @@ class SubmissionReagentAssociation(BaseClass):
|
||||
output['comments'] = self.comments
|
||||
return output
|
||||
|
||||
def to_pydantic(self, extraction_kit: KitType):
|
||||
from backend.validators import PydReagent
|
||||
return PydReagent(**self.to_sub_dict(extraction_kit=extraction_kit))
|
||||
|
||||
class Equipment(BaseClass):
|
||||
"""
|
||||
@@ -1394,6 +1400,8 @@ class Equipment(BaseClass):
|
||||
else:
|
||||
return {k: v for k, v in self.__dict__.items()}
|
||||
|
||||
|
||||
|
||||
def get_processes(self, submission_type: SubmissionType, extraction_kit: str | KitType | None = None) -> List[str]:
|
||||
"""
|
||||
Get all processes associated with this Equipment for a given SubmissionType
|
||||
@@ -1682,6 +1690,7 @@ class SubmissionEquipmentAssociation(BaseClass):
|
||||
|
||||
equipment = relationship(Equipment, back_populates="equipment_submission_associations") #: associated equipment
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<SubmissionEquipmentAssociation({self.submission.rsl_plate_num} & {self.equipment.name})>"
|
||||
|
||||
@@ -1706,6 +1715,10 @@ class SubmissionEquipmentAssociation(BaseClass):
|
||||
processes=[process], role=self.role, nickname=self.equipment.nickname)
|
||||
return output
|
||||
|
||||
def to_pydantic(self):
|
||||
from backend.validators import PydEquipment
|
||||
return PydEquipment(**self.to_sub_dict())
|
||||
|
||||
@classmethod
|
||||
@setup_lookup
|
||||
def query(cls, equipment_id: int, submission_id: int, role: str | None = None, limit: int = 0, **kwargs) -> Any | \
|
||||
@@ -1999,3 +2012,7 @@ class SubmissionTipsAssociation(BaseClass):
|
||||
query = query.filter(cls.submission_id == submission_id)
|
||||
query = query.filter(cls.role_name == role)
|
||||
return cls.execute_query(query=query, limit=limit, **kwargs)
|
||||
|
||||
def to_pydantic(self):
|
||||
from backend.validators import PydTips
|
||||
return PydTips(name=self.tips.name, lot=self.tips.lot, role=self.role_name)
|
||||
|
||||
@@ -2,30 +2,30 @@
|
||||
Models for the main submission and sample types.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
import types
|
||||
import zipfile
|
||||
# import sys
|
||||
# import types
|
||||
# import zipfile
|
||||
from copy import deepcopy
|
||||
from getpass import getuser
|
||||
import logging, uuid, tempfile, re, base64
|
||||
import logging, uuid, tempfile, re, base64, numpy as np, pandas as pd, types, sys
|
||||
from zipfile import ZipFile
|
||||
from tempfile import TemporaryDirectory, TemporaryFile
|
||||
from operator import itemgetter
|
||||
from pprint import pformat
|
||||
from . import BaseClass, Reagent, SubmissionType, KitType, Organization, Contact, LogMixin
|
||||
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, event, inspect
|
||||
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, event, inspect, func
|
||||
from sqlalchemy.orm import relationship, validates, Query
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError, StatementError, \
|
||||
ArgumentError
|
||||
from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
|
||||
import pandas as pd
|
||||
# import pandas as pd
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.drawing.image import Image as OpenpyxlImage
|
||||
from tools import row_map, setup_lookup, jinja_template_loading, rreplace, row_keys, check_key_or_attr, Result, Report, \
|
||||
report_result
|
||||
from datetime import datetime, date
|
||||
report_result, create_holidays_for_year
|
||||
from datetime import datetime, date, timedelta
|
||||
from typing import List, Any, Tuple, Literal, Generator
|
||||
from dateutil.parser import parse
|
||||
from pathlib import Path
|
||||
@@ -73,6 +73,7 @@ class BasicSubmission(BaseClass, LogMixin):
|
||||
custom = Column(JSON)
|
||||
controls = relationship("Control", back_populates="submission",
|
||||
uselist=True) #: A control sample added to submission
|
||||
completed_date = Column(TIMESTAMP)
|
||||
|
||||
submission_sample_associations = relationship(
|
||||
"SubmissionSampleAssociation",
|
||||
@@ -345,6 +346,8 @@ class BasicSubmission(BaseClass, LogMixin):
|
||||
tips = self.generate_associations(name="submission_tips_associations")
|
||||
cost_centre = self.cost_centre
|
||||
custom = self.custom
|
||||
controls = [item.to_sub_dict() for item in self.controls]
|
||||
|
||||
else:
|
||||
reagents = None
|
||||
samples = None
|
||||
@@ -352,6 +355,7 @@ class BasicSubmission(BaseClass, LogMixin):
|
||||
tips = None
|
||||
cost_centre = None
|
||||
custom = None
|
||||
controls = None
|
||||
# logger.debug("Getting comments")
|
||||
try:
|
||||
comments = self.comment
|
||||
@@ -381,6 +385,8 @@ class BasicSubmission(BaseClass, LogMixin):
|
||||
output["contact"] = contact
|
||||
output["contact_phone"] = contact_phone
|
||||
output["custom"] = custom
|
||||
output["controls"] = controls
|
||||
output["completed_date"] = self.completed_date
|
||||
return output
|
||||
|
||||
def calculate_column_count(self) -> int:
|
||||
@@ -619,7 +625,7 @@ class BasicSubmission(BaseClass, LogMixin):
|
||||
Returns:
|
||||
PydSubmission: converted object.
|
||||
"""
|
||||
from backend.validators import PydSubmission, PydSample, PydReagent, PydEquipment
|
||||
from backend.validators import PydSubmission
|
||||
dicto = self.to_dict(full_data=True, backup=backup)
|
||||
# logger.debug("To dict complete")
|
||||
new_dict = {}
|
||||
@@ -628,24 +634,43 @@ class BasicSubmission(BaseClass, LogMixin):
|
||||
missing = value in ['', 'None', None]
|
||||
match key:
|
||||
case "reagents":
|
||||
new_dict[key] = [PydReagent(**reagent) for reagent in value]
|
||||
# new_dict[key] = [PydReagent(**reagent) for reagent in value]
|
||||
field_value = [item.to_pydantic(extraction_kit=self.extraction_kit) for item in self.submission_reagent_associations]
|
||||
case "samples":
|
||||
new_dict[key] = [PydSample(**{k.lower().replace(" ", "_"): v for k, v in sample.items()}) for sample
|
||||
in dicto['samples']]
|
||||
field_value = [item.to_pydantic() for item in self.submission_sample_associations]
|
||||
case "equipment":
|
||||
field_value = [item.to_pydantic() for item in self.submission_equipment_associations]
|
||||
case "controls":
|
||||
try:
|
||||
new_dict[key] = [PydEquipment(**equipment) for equipment in dicto['equipment']]
|
||||
field_value = [item.to_pydantic() for item in self.__getattribute__(key)]
|
||||
except TypeError as e:
|
||||
logger.error(f"Possible no equipment error: {e}")
|
||||
logger.error(f"Error converting {key} to pydantic :{e}")
|
||||
continue
|
||||
case "tips":
|
||||
field_value = [item.to_pydantic() for item in self.submission_tips_associations]
|
||||
case "submission_type" | "contact":
|
||||
field_value = dict(value=self.__getattribute__(key).name, missing=missing)
|
||||
case "plate_number":
|
||||
new_dict['rsl_plate_num'] = dict(value=value, missing=missing)
|
||||
key = 'rsl_plate_num'
|
||||
field_value = dict(value=self.rsl_plate_num, missing=missing)
|
||||
# continue
|
||||
case "submitter_plate_number":
|
||||
new_dict['submitter_plate_num'] = dict(value=value, missing=missing)
|
||||
# new_dict['submitter_plate_num'] = dict(value=self.submitter_plate_num, missing=missing)
|
||||
# continue
|
||||
key = "submitter_plate_num"
|
||||
field_value = dict(value=self.submitter_plate_num, missing=missing)
|
||||
case "id":
|
||||
pass
|
||||
continue
|
||||
case _:
|
||||
logger.debug(f"Setting dict {key} to {value}")
|
||||
new_dict[key.lower().replace(" ", "_")] = dict(value=value, missing=missing)
|
||||
try:
|
||||
key = key.lower().replace(" ", "_")
|
||||
field_value = dict(value=self.__getattribute__(key), missing=missing)
|
||||
# new_dict[key.lower().replace(" ", "_")] = dict(value=self.__getattribute__(key), missing=missing)
|
||||
except AttributeError:
|
||||
logger.error(f"{key} is not available in {self}")
|
||||
continue
|
||||
logger.debug(f"Setting dict {key}")
|
||||
new_dict[key] = field_value
|
||||
# logger.debug(f"{key} complete after {time()-start}")
|
||||
new_dict['filepath'] = Path(tempfile.TemporaryFile().name)
|
||||
# logger.debug("Done converting fields.")
|
||||
@@ -1021,6 +1046,7 @@ class BasicSubmission(BaseClass, LogMixin):
|
||||
Tuple(dict, Template): (Updated dictionary, Template to be rendered)
|
||||
"""
|
||||
base_dict['excluded'] = cls.get_default_info('details_ignore')
|
||||
base_dict['excluded'] += ['controls']
|
||||
env = jinja_template_loading()
|
||||
temp_name = f"{cls.__name__.lower()}_details.html"
|
||||
# logger.debug(f"Returning template: {temp_name}")
|
||||
@@ -1081,11 +1107,11 @@ class BasicSubmission(BaseClass, LogMixin):
|
||||
end_date = date.today()
|
||||
if end_date is not None and start_date is None:
|
||||
logger.warning(f"End date with no start date, using Jan 1, 2023")
|
||||
start_date = date(2023, 1, 1)
|
||||
start_date = cls.__database_session__.query(cls, func.min(cls.submitted_date)).first()[1]
|
||||
if start_date is not None:
|
||||
# logger.debug(f"Querying with start date: {start_date} and end date: {end_date}")
|
||||
match start_date:
|
||||
case date():
|
||||
case date() | datetime():
|
||||
# logger.debug(f"Lookup BasicSubmission by start_date({start_date})")
|
||||
start_date = start_date.strftime("%Y-%m-%d")
|
||||
case int():
|
||||
@@ -1098,14 +1124,18 @@ class BasicSubmission(BaseClass, LogMixin):
|
||||
match end_date:
|
||||
case date() | datetime():
|
||||
# logger.debug(f"Lookup BasicSubmission by end_date({end_date})")
|
||||
end_date = end_date + timedelta(days=1)
|
||||
end_date = end_date.strftime("%Y-%m-%d")
|
||||
case int():
|
||||
# logger.debug(f"Lookup BasicSubmission by ordinal end_date {end_date}")
|
||||
end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date().strftime(
|
||||
end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date() + timedelta(
|
||||
days=1)
|
||||
end_date = end_date.strftime(
|
||||
"%Y-%m-%d")
|
||||
case _:
|
||||
# logger.debug(f"Lookup BasicSubmission by parsed str end_date {end_date}")
|
||||
end_date = parse(end_date).strftime("%Y-%m-%d")
|
||||
end_date = parse(end_date) + timedelta(days=1)
|
||||
end_date = end_date.strftime("%Y-%m-%d")
|
||||
# logger.debug(f"Compensating for same date by using time")
|
||||
if start_date == end_date:
|
||||
start_date = datetime.strptime(start_date, "%Y-%m-%d").strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||
@@ -1339,10 +1369,22 @@ class BasicSubmission(BaseClass, LogMixin):
|
||||
writer = pyd.to_writer()
|
||||
writer.xl.save(filename=fname.with_suffix(".xlsx"))
|
||||
|
||||
def get_turnaround_time(self):
|
||||
completed = self.completed_date or datetime.now()
|
||||
return self.calculate_turnaround(start_date=self.submitted_date.date(), end_date=completed.date())
|
||||
|
||||
@classmethod
|
||||
def calculate_turnaround(cls, start_date:date|None=None, end_date:date|None=None) -> int|None:
|
||||
try:
|
||||
delta = np.busday_count(start_date, end_date, holidays=create_holidays_for_year(start_date.year))
|
||||
except ValueError:
|
||||
return None
|
||||
return delta + 1
|
||||
|
||||
|
||||
# Below are the custom submission types
|
||||
|
||||
class BacterialCulture(BasicSubmission, LogMixin):
|
||||
class BacterialCulture(BasicSubmission):
|
||||
"""
|
||||
derivative submission type from BasicSubmission
|
||||
"""
|
||||
@@ -1429,7 +1471,7 @@ class BacterialCulture(BasicSubmission, LogMixin):
|
||||
return input_dict
|
||||
|
||||
|
||||
class Wastewater(BasicSubmission, LogMixin):
|
||||
class Wastewater(BasicSubmission):
|
||||
"""
|
||||
derivative submission type from BasicSubmission
|
||||
"""
|
||||
@@ -1868,7 +1910,7 @@ class WastewaterArtic(BasicSubmission):
|
||||
pass
|
||||
try:
|
||||
input_dict['source_plate_number'] = int(input_dict['source_plate_number'])
|
||||
except ValueError:
|
||||
except (ValueError, KeyError):
|
||||
input_dict['source_plate_number'] = 0
|
||||
# NOTE: Because generate_sample_object needs the submitter_id and the artic has the "({origin well})"
|
||||
# at the end, this has to be done here. No moving to sqlalchemy object :(
|
||||
@@ -2276,6 +2318,10 @@ class BasicSample(BaseClass):
|
||||
# logger.debug(f"Done converting {self} after {time()-start}")
|
||||
return sample
|
||||
|
||||
def to_pydantic(self):
|
||||
from backend.validators import PydSample
|
||||
return PydSample(**self.to_sub_dict())
|
||||
|
||||
def set_attribute(self, name: str, value):
|
||||
"""
|
||||
Custom attribute setter (depreciated over built-in __setattr__)
|
||||
@@ -2733,6 +2779,10 @@ class SubmissionSampleAssociation(BaseClass):
|
||||
sample['submission_rank'] = self.submission_rank
|
||||
return sample
|
||||
|
||||
def to_pydantic(self):
|
||||
from backend.validators import PydSample
|
||||
return PydSample(**self.to_sub_dict())
|
||||
|
||||
def to_hitpick(self) -> dict | None:
|
||||
"""
|
||||
Outputs a dictionary usable for html plate maps.
|
||||
|
||||
Reference in New Issue
Block a user