Can now calculate turnaround time including holidays.

This commit is contained in:
lwark
2024-11-25 13:34:02 -06:00
parent 7d1e6dc606
commit 7f0b7feb5d
13 changed files with 533 additions and 300 deletions

View File

@@ -1,6 +1,12 @@
## 202411.05
- Can now calculate turnaround time including holidays.
## 202411.04 ## 202411.04
- Add reagent from scrape now limits roles to those found in kit to prevent confusion. - Add reagent from scrape now limits roles to those found in kit to prevent confusion.
- Added audit logs to track changes.
- Added completed_date column to _basicsubmission to track turnaround time.
## 202411.01 ## 202411.01

View File

@@ -41,9 +41,9 @@ from .models import *
def update_log(mapper, connection, target): def update_log(mapper, connection, target):
logger.debug("\n\nBefore update\n\n") logger.debug("\n\nBefore update\n\n")
state = inspect(target) state = inspect(target)
logger.debug(state) # logger.debug(state)
update = dict(user=getuser(), time=datetime.now(), object=str(state.object), changes=[]) update = dict(user=getuser(), time=datetime.now(), object=str(state.object), changes=[])
logger.debug(update) # logger.debug(update)
for attr in state.attrs: for attr in state.attrs:
hist = attr.load_history() hist = attr.load_history()
if not hist.has_changes(): if not hist.has_changes():
@@ -51,24 +51,24 @@ def update_log(mapper, connection, target):
added = [str(item) for item in hist.added] added = [str(item) for item in hist.added]
deleted = [str(item) for item in hist.deleted] deleted = [str(item) for item in hist.deleted]
change = dict(field=attr.key, added=added, deleted=deleted) change = dict(field=attr.key, added=added, deleted=deleted)
logger.debug(f"Adding: {pformat(change)}") # logger.debug(f"Adding: {pformat(change)}")
if added != deleted:
try: try:
update['changes'].append(change) update['changes'].append(change)
except Exception as e: except Exception as e:
logger.error(f"Something went horribly wrong adding attr: {attr.key}: {e}") logger.error(f"Something went wrong adding attr: {attr.key}: {e}")
continue continue
# logger.debug(f"Adding to audit logs: {pformat(update)}")
logger.debug(f"Adding to audit logs: {pformat(update)}")
if update['changes']: if update['changes']:
# Note: must use execute as the session will be busy at this point. # 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 # https://medium.com/@singh.surbhicse/creating-audit-table-to-log-insert-update-and-delete-changes-in-flask-sqlalchemy-f2ca53f7b02f
table = AuditLog.__table__ table = AuditLog.__table__
logger.debug(f"Adding to {table}") # logger.debug(f"Adding to {table}")
connection.execute(table.insert().values(**update)) connection.execute(table.insert().values(**update))
# logger.debug("Here is where I would insert values, if I was able.") # logger.debug("Here is where I would insert values, if I was able.")
else: else:
logger.info(f"No changes detected, not updating logs.") logger.info(f"No changes detected, not updating logs.")
# event.listen(LogMixin, 'after_update', update_log, propagate=True) event.listen(LogMixin, 'after_update', update_log, propagate=True)
# event.listen(LogMixin, 'after_insert', update_log, propagate=True) event.listen(LogMixin, 'after_insert', update_log, propagate=True)

View 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()

View File

@@ -142,97 +142,97 @@ class Control(BaseClass):
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<{self.controltype_name}({self.name})>" return f"<{self.controltype_name}({self.name})>"
# @classmethod @classmethod
# @setup_lookup @setup_lookup
# def query(cls, def query(cls,
# submission_type: str | None = None, submission_type: str | None = None,
# subtype: str | None = None, subtype: str | None = None,
# start_date: date | str | int | None = None, start_date: date | str | int | None = None,
# end_date: date | str | int | None = None, end_date: date | str | int | None = None,
# control_name: str | None = None, name: str | None = None,
# limit: int = 0, **kwargs limit: int = 0, **kwargs
# ) -> Control | List[Control]: ) -> Control | List[Control]:
# """ """
# Lookup control objects in the database based on a number of parameters. Lookup control objects in the database based on a number of parameters.
#
# Args: Args:
# submission_type (str | None, optional): Control archetype. Defaults to None. 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. 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. 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. name (str | None, optional): Name of control. Defaults to None.
# limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0. limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
#
# Returns: Returns:
# PCRControl|List[PCRControl]: Control object of interest. Control|List[Control]: Control object of interest.
# """ """
# from backend.db import SubmissionType from backend.db import SubmissionType
# query: Query = cls.__database_session__.query(cls) query: Query = cls.__database_session__.query(cls)
# match submission_type: match submission_type:
# case str(): case str():
# from backend import BasicSubmission, SubmissionType from backend import BasicSubmission, SubmissionType
# # logger.debug(f"Lookup controls by SubmissionType str: {submission_type}") # logger.debug(f"Lookup controls by SubmissionType str: {submission_type}")
# query = query.join(BasicSubmission).join(SubmissionType).filter(SubmissionType.name == submission_type) query = query.join(BasicSubmission).join(SubmissionType).filter(SubmissionType.name == submission_type)
# case SubmissionType(): case SubmissionType():
# from backend import BasicSubmission from backend import BasicSubmission
# # logger.debug(f"Lookup controls by SubmissionType: {submission_type}") # logger.debug(f"Lookup controls by SubmissionType: {submission_type}")
# query = query.join(BasicSubmission).filter(BasicSubmission.submission_type_name == submission_type.name) query = query.join(BasicSubmission).filter(BasicSubmission.submission_type_name == submission_type.name)
# case _: case _:
# pass pass
# # NOTE: by control type # NOTE: by control type
# match subtype: match subtype:
# case str(): case str():
# if cls.__name__ == "Control": if cls.__name__ == "Control":
# raise ValueError(f"Cannot query base class Control with subtype.") raise ValueError(f"Cannot query base class Control with subtype.")
# elif cls.__name__ == "IridaControl": elif cls.__name__ == "IridaControl":
# query = query.filter(cls.subtype == subtype) query = query.filter(cls.subtype == subtype)
# else: else:
# try: try:
# query = query.filter(cls.subtype == subtype) query = query.filter(cls.subtype == subtype)
# except AttributeError as e: except AttributeError as e:
# logger.error(e) logger.error(e)
# case _: case _:
# pass pass
# # NOTE: by date range # NOTE: by date range
# if start_date is not None and end_date is None: if start_date is not None and end_date is None:
# logger.warning(f"Start date with no end date, using today.") logger.warning(f"Start date with no end date, using today.")
# end_date = date.today() end_date = date.today()
# if end_date is not None and start_date is None: if end_date is not None and start_date is None:
# logger.warning(f"End date with no start date, using 90 days ago.") logger.warning(f"End date with no start date, using 90 days ago.")
# # start_date = date(2023, 1, 1) # start_date = date(2023, 1, 1)
# start_date = date.today() - timedelta(days=90) start_date = date.today() - timedelta(days=90)
# if start_date is not None: if start_date is not None:
# match start_date: match start_date:
# case date(): case date():
# # logger.debug(f"Lookup control by start date({start_date})") # logger.debug(f"Lookup control by start date({start_date})")
# start_date = start_date.strftime("%Y-%m-%d") start_date = start_date.strftime("%Y-%m-%d")
# case int(): case int():
# # logger.debug(f"Lookup control by ordinal start date {start_date}") # logger.debug(f"Lookup control by ordinal start date {start_date}")
# start_date = datetime.fromordinal( start_date = datetime.fromordinal(
# datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d") datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d")
# case _: case _:
# # logger.debug(f"Lookup control with parsed start date {start_date}") # logger.debug(f"Lookup control with parsed start date {start_date}")
# start_date = parse(start_date).strftime("%Y-%m-%d") start_date = parse(start_date).strftime("%Y-%m-%d")
# match end_date: match end_date:
# case date(): case date():
# # logger.debug(f"Lookup control by end date({end_date})") # logger.debug(f"Lookup control by end date({end_date})")
# end_date = end_date.strftime("%Y-%m-%d") end_date = end_date.strftime("%Y-%m-%d")
# case int(): case int():
# # logger.debug(f"Lookup control by ordinal end date {end_date}") # 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( end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date().strftime(
# "%Y-%m-%d") "%Y-%m-%d")
# case _: case _:
# # logger.debug(f"Lookup control with parsed end date {end_date}") # logger.debug(f"Lookup control with parsed end date {end_date}")
# end_date = parse(end_date).strftime("%Y-%m-%d") 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}") # 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)) query = query.filter(cls.submitted_date.between(start_date, end_date))
# match control_name: match name:
# case str(): case str():
# # logger.debug(f"Lookup control by name {control_name}") # logger.debug(f"Lookup control by name {control_name}")
# query = query.filter(cls.name.startswith(control_name)) query = query.filter(cls.name.startswith(name))
# limit = 1 limit = 1
# case _: case _:
# pass pass
# return cls.execute_query(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
@classmethod @classmethod
def find_polymorphic_subclass(cls, polymorphic_identity: str | ControlType | None = None, 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, 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()) submitted_date=self.submitted_date.date())
@classmethod # @classmethod
@setup_lookup # @setup_lookup
def query(cls, # def query(cls,
submission_type: str | None = None, # submission_type: str | None = None,
start_date: date | str | int | None = None, # start_date: date | str | int | None = None,
end_date: date | str | int | None = None, # end_date: date | str | int | None = None,
control_name: str | None = None, # name: str | None = None,
limit: int = 0 # limit: int = 0
) -> Control | List[Control]: # ) -> Control | List[Control]:
""" # """
Lookup control objects in the database based on a number of parameters. # Lookup control objects in the database based on a number of parameters.
#
Args: # Args:
submission_type (str | None, optional): Control archetype. Defaults to None. # 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. # 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. # 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. # 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. # limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
#
Returns: # Returns:
PCRControl|List[PCRControl]: Control object of interest. # PCRControl|List[PCRControl]: Control object of interest.
""" # """
from backend.db import SubmissionType # from backend.db import SubmissionType
query: Query = cls.__database_session__.query(cls) # query: Query = cls.__database_session__.query(cls)
# NOTE: by date range # # NOTE: by date range
if start_date is not None and end_date is None: # if start_date is not None and end_date is None:
logger.warning(f"Start date with no end date, using today.") # logger.warning(f"Start date with no end date, using today.")
end_date = date.today() # end_date = date.today()
if end_date is not None and start_date is None: # if end_date is not None and start_date is None:
logger.warning(f"End date with no start date, using 90 days ago.") # logger.warning(f"End date with no start date, using 90 days ago.")
# start_date = date(2023, 1, 1) # # start_date = date(2023, 1, 1)
start_date = date.today() - timedelta(days=90) # start_date = date.today() - timedelta(days=90)
if start_date is not None: # if start_date is not None:
match start_date: # match start_date:
case date(): # case date():
# logger.debug(f"Lookup control by start date({start_date})") # # logger.debug(f"Lookup control by start date({start_date})")
start_date = start_date.strftime("%Y-%m-%d") # start_date = start_date.strftime("%Y-%m-%d")
case int(): # case int():
# logger.debug(f"Lookup control by ordinal start date {start_date}") # # logger.debug(f"Lookup control by ordinal start date {start_date}")
start_date = datetime.fromordinal( # start_date = datetime.fromordinal(
datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d") # datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d")
case _: # case _:
# logger.debug(f"Lookup control with parsed start date {start_date}") # # logger.debug(f"Lookup control with parsed start date {start_date}")
start_date = parse(start_date).strftime("%Y-%m-%d") # start_date = parse(start_date).strftime("%Y-%m-%d")
match end_date: # match end_date:
case date(): # case date():
# logger.debug(f"Lookup control by end date({end_date})") # # logger.debug(f"Lookup control by end date({end_date})")
end_date = end_date.strftime("%Y-%m-%d") # end_date = end_date.strftime("%Y-%m-%d")
case int(): # case int():
# logger.debug(f"Lookup control by ordinal end date {end_date}") # # 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( # end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date().strftime(
"%Y-%m-%d") # "%Y-%m-%d")
case _: # case _:
# logger.debug(f"Lookup control with parsed end date {end_date}") # # logger.debug(f"Lookup control with parsed end date {end_date}")
end_date = parse(end_date).strftime("%Y-%m-%d") # 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}") # # 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)) # query = query.filter(cls.submitted_date.between(start_date, end_date))
match submission_type: # match submission_type:
case str(): # case str():
from backend import BasicSubmission, SubmissionType # from backend import BasicSubmission, SubmissionType
# logger.debug(f"Lookup controls by SubmissionType str: {submission_type}") # # logger.debug(f"Lookup controls by SubmissionType str: {submission_type}")
query = query.join(BasicSubmission).join(SubmissionType).filter(SubmissionType.name == submission_type) # query = query.join(BasicSubmission).join(SubmissionType).filter(SubmissionType.name == submission_type)
case SubmissionType(): # case SubmissionType():
from backend import BasicSubmission # from backend import BasicSubmission
# logger.debug(f"Lookup controls by SubmissionType: {submission_type}") # # logger.debug(f"Lookup controls by SubmissionType: {submission_type}")
query = query.join(BasicSubmission).filter(BasicSubmission.submission_type_name==submission_type.name) # query = query.join(BasicSubmission).filter(BasicSubmission.submission_type_name==submission_type.name)
case _: # case _:
pass # pass
match control_name: # match control_name:
case str(): # case str():
# logger.debug(f"Lookup control by name {control_name}") # # logger.debug(f"Lookup control by name {control_name}")
query = query.filter(cls.name.startswith(control_name)) # query = query.filter(cls.name.startswith(control_name))
limit = 1 # limit = 1
case _: # case _:
pass # pass
return cls.execute_query(query=query, limit=limit) # return cls.execute_query(query=query, limit=limit)
@classmethod @classmethod
@report_result @report_result
@@ -432,7 +432,7 @@ class PCRControl(Control):
def to_pydantic(self): def to_pydantic(self):
from backend.validators import PydPCRControl 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): class IridaControl(Control):
@@ -569,76 +569,76 @@ class IridaControl(Control):
cols = [] cols = []
return cols return cols
@classmethod # @classmethod
@setup_lookup # @setup_lookup
def query(cls, # def query(cls,
sub_type: str | None = None, # sub_type: str | None = None,
start_date: date | str | int | None = None, # start_date: date | str | int | None = None,
end_date: date | str | int | None = None, # end_date: date | str | int | None = None,
control_name: str | None = None, # control_name: str | None = None,
limit: int = 0 # limit: int = 0
) -> Control | List[Control]: # ) -> Control | List[Control]:
""" # """
Lookup control objects in the database based on a number of parameters. # Lookup control objects in the database based on a number of parameters.
#
Args: # Args:
sub_type (models.ControlType | str | None, optional): Control archetype. Defaults to None. # 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. # 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. # 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. # 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. # limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
#
Returns: # Returns:
models.Control|List[models.Control]: Control object of interest. # models.Control|List[models.Control]: Control object of interest.
""" # """
query: Query = cls.__database_session__.query(cls) # query: Query = cls.__database_session__.query(cls)
# NOTE: by control type # # NOTE: by control type
match sub_type: # match sub_type:
case str(): # case str():
query = query.filter(cls.subtype == sub_type) # query = query.filter(cls.subtype == sub_type)
case _: # case _:
pass # pass
# NOTE: If one date exists, we need the other one to exist as well. # # 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: # if start_date is not None and end_date is None:
logger.warning(f"Start date with no end date, using today.") # logger.warning(f"Start date with no end date, using today.")
end_date = date.today() # end_date = date.today()
if end_date is not None and start_date is None: # if end_date is not None and start_date is None:
logger.warning(f"End date with no start date, using 90 days ago.") # logger.warning(f"End date with no start date, using 90 days ago.")
# start_date = date(2023, 1, 1) # # start_date = date(2023, 1, 1)
start_date = date.today() - timedelta(days=90) # start_date = date.today() - timedelta(days=90)
if start_date is not None: # if start_date is not None:
match start_date: # match start_date:
case date(): # case date():
# logger.debug(f"Lookup control by start date({start_date})") # # logger.debug(f"Lookup control by start date({start_date})")
start_date = start_date.strftime("%Y-%m-%d") # start_date = start_date.strftime("%Y-%m-%d")
case int(): # case int():
# logger.debug(f"Lookup control by ordinal start date {start_date}") # # logger.debug(f"Lookup control by ordinal start date {start_date}")
start_date = datetime.fromordinal( # start_date = datetime.fromordinal(
datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d") # datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d")
case _: # case _:
# logger.debug(f"Lookup control with parsed start date {start_date}") # # logger.debug(f"Lookup control with parsed start date {start_date}")
start_date = parse(start_date).strftime("%Y-%m-%d") # start_date = parse(start_date).strftime("%Y-%m-%d")
match end_date: # match end_date:
case date(): # case date():
# logger.debug(f"Lookup control by end date({end_date})") # # logger.debug(f"Lookup control by end date({end_date})")
end_date = end_date.strftime("%Y-%m-%d") # end_date = end_date.strftime("%Y-%m-%d")
case int(): # case int():
# logger.debug(f"Lookup control by ordinal end date {end_date}") # # 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( # end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date().strftime(
"%Y-%m-%d") # "%Y-%m-%d")
case _: # case _:
# logger.debug(f"Lookup control with parsed end date {end_date}") # # logger.debug(f"Lookup control with parsed end date {end_date}")
end_date = parse(end_date).strftime("%Y-%m-%d") # 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}") # # 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)) # query = query.filter(cls.submitted_date.between(start_date, end_date))
match control_name: # match control_name:
case str(): # case str():
# logger.debug(f"Lookup control by name {control_name}") # # logger.debug(f"Lookup control by name {control_name}")
query = query.filter(cls.name.startswith(control_name)) # query = query.filter(cls.name.startswith(control_name))
limit = 1 # limit = 1
case _: # case _:
pass # pass
return cls.execute_query(query=query, limit=limit) # return cls.execute_query(query=query, limit=limit)
@classmethod @classmethod
def make_parent_buttons(cls, parent: QWidget) -> None: def make_parent_buttons(cls, parent: QWidget) -> None:
@@ -828,7 +828,7 @@ class IridaControl(Control):
return df, previous_dates return df, previous_dates
# NOTE: if date was changed, rerun with new date # NOTE: if date was changed, rerun with new date
else: 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) df, previous_dates = cls.check_date(df, item, previous_dates)
return df, previous_dates return df, previous_dates

View File

@@ -485,6 +485,8 @@ class Reagent(BaseClass):
output['editable'] = ['lot', 'expiry'] output['editable'] = ['lot', 'expiry']
return output return output
def update_last_used(self, kit: KitType) -> Report: def update_last_used(self, kit: KitType) -> Report:
""" """
Updates last used reagent lot for ReagentType/KitType Updates last used reagent lot for ReagentType/KitType
@@ -1282,7 +1284,8 @@ class SubmissionReagentAssociation(BaseClass):
try: try:
return f"<{self.submission.rsl_plate_num} & {self.reagent.lot}>" return f"<{self.submission.rsl_plate_num} & {self.reagent.lot}>"
except AttributeError: 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): def __init__(self, reagent=None, submission=None):
if isinstance(reagent, list): if isinstance(reagent, list):
@@ -1347,6 +1350,9 @@ class SubmissionReagentAssociation(BaseClass):
output['comments'] = self.comments output['comments'] = self.comments
return output 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): class Equipment(BaseClass):
""" """
@@ -1394,6 +1400,8 @@ class Equipment(BaseClass):
else: else:
return {k: v for k, v in self.__dict__.items()} 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]: 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 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 equipment = relationship(Equipment, back_populates="equipment_submission_associations") #: associated equipment
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<SubmissionEquipmentAssociation({self.submission.rsl_plate_num} & {self.equipment.name})>" 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) processes=[process], role=self.role, nickname=self.equipment.nickname)
return output return output
def to_pydantic(self):
from backend.validators import PydEquipment
return PydEquipment(**self.to_sub_dict())
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, equipment_id: int, submission_id: int, role: str | None = None, limit: int = 0, **kwargs) -> Any | \ 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.submission_id == submission_id)
query = query.filter(cls.role_name == role) query = query.filter(cls.role_name == role)
return cls.execute_query(query=query, limit=limit, **kwargs) 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)

View File

@@ -2,30 +2,30 @@
Models for the main submission and sample types. Models for the main submission and sample types.
""" """
from __future__ import annotations from __future__ import annotations
import sys # import sys
import types # import types
import zipfile # import zipfile
from copy import deepcopy from copy import deepcopy
from getpass import getuser 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 zipfile import ZipFile
from tempfile import TemporaryDirectory, TemporaryFile from tempfile import TemporaryDirectory, TemporaryFile
from operator import itemgetter from operator import itemgetter
from pprint import pformat from pprint import pformat
from . import BaseClass, Reagent, SubmissionType, KitType, Organization, Contact, LogMixin 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 import relationship, validates, Query
from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError, StatementError, \ from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError, StatementError, \
ArgumentError ArgumentError
from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
import pandas as pd # import pandas as pd
from openpyxl import Workbook from openpyxl import Workbook
from openpyxl.drawing.image import Image as OpenpyxlImage 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, \ from tools import row_map, setup_lookup, jinja_template_loading, rreplace, row_keys, check_key_or_attr, Result, Report, \
report_result report_result, create_holidays_for_year
from datetime import datetime, date from datetime import datetime, date, timedelta
from typing import List, Any, Tuple, Literal, Generator from typing import List, Any, Tuple, Literal, Generator
from dateutil.parser import parse from dateutil.parser import parse
from pathlib import Path from pathlib import Path
@@ -73,6 +73,7 @@ class BasicSubmission(BaseClass, LogMixin):
custom = Column(JSON) custom = Column(JSON)
controls = relationship("Control", back_populates="submission", controls = relationship("Control", back_populates="submission",
uselist=True) #: A control sample added to submission uselist=True) #: A control sample added to submission
completed_date = Column(TIMESTAMP)
submission_sample_associations = relationship( submission_sample_associations = relationship(
"SubmissionSampleAssociation", "SubmissionSampleAssociation",
@@ -345,6 +346,8 @@ class BasicSubmission(BaseClass, LogMixin):
tips = self.generate_associations(name="submission_tips_associations") tips = self.generate_associations(name="submission_tips_associations")
cost_centre = self.cost_centre cost_centre = self.cost_centre
custom = self.custom custom = self.custom
controls = [item.to_sub_dict() for item in self.controls]
else: else:
reagents = None reagents = None
samples = None samples = None
@@ -352,6 +355,7 @@ class BasicSubmission(BaseClass, LogMixin):
tips = None tips = None
cost_centre = None cost_centre = None
custom = None custom = None
controls = None
# logger.debug("Getting comments") # logger.debug("Getting comments")
try: try:
comments = self.comment comments = self.comment
@@ -381,6 +385,8 @@ class BasicSubmission(BaseClass, LogMixin):
output["contact"] = contact output["contact"] = contact
output["contact_phone"] = contact_phone output["contact_phone"] = contact_phone
output["custom"] = custom output["custom"] = custom
output["controls"] = controls
output["completed_date"] = self.completed_date
return output return output
def calculate_column_count(self) -> int: def calculate_column_count(self) -> int:
@@ -619,7 +625,7 @@ class BasicSubmission(BaseClass, LogMixin):
Returns: Returns:
PydSubmission: converted object. 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) dicto = self.to_dict(full_data=True, backup=backup)
# logger.debug("To dict complete") # logger.debug("To dict complete")
new_dict = {} new_dict = {}
@@ -628,24 +634,43 @@ class BasicSubmission(BaseClass, LogMixin):
missing = value in ['', 'None', None] missing = value in ['', 'None', None]
match key: match key:
case "reagents": 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": case "samples":
new_dict[key] = [PydSample(**{k.lower().replace(" ", "_"): v for k, v in sample.items()}) for sample field_value = [item.to_pydantic() for item in self.submission_sample_associations]
in dicto['samples']]
case "equipment": case "equipment":
field_value = [item.to_pydantic() for item in self.submission_equipment_associations]
case "controls":
try: 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: 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": 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": 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": case "id":
pass continue
case _: case _:
logger.debug(f"Setting dict {key} to {value}") try:
new_dict[key.lower().replace(" ", "_")] = dict(value=value, missing=missing) 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}") # logger.debug(f"{key} complete after {time()-start}")
new_dict['filepath'] = Path(tempfile.TemporaryFile().name) new_dict['filepath'] = Path(tempfile.TemporaryFile().name)
# logger.debug("Done converting fields.") # logger.debug("Done converting fields.")
@@ -1021,6 +1046,7 @@ class BasicSubmission(BaseClass, LogMixin):
Tuple(dict, Template): (Updated dictionary, Template to be rendered) Tuple(dict, Template): (Updated dictionary, Template to be rendered)
""" """
base_dict['excluded'] = cls.get_default_info('details_ignore') base_dict['excluded'] = cls.get_default_info('details_ignore')
base_dict['excluded'] += ['controls']
env = jinja_template_loading() env = jinja_template_loading()
temp_name = f"{cls.__name__.lower()}_details.html" temp_name = f"{cls.__name__.lower()}_details.html"
# logger.debug(f"Returning template: {temp_name}") # logger.debug(f"Returning template: {temp_name}")
@@ -1081,11 +1107,11 @@ class BasicSubmission(BaseClass, LogMixin):
end_date = date.today() end_date = date.today()
if end_date is not None and start_date is None: if end_date is not None and start_date is None:
logger.warning(f"End date with no start date, using Jan 1, 2023") 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: if start_date is not None:
# logger.debug(f"Querying with start date: {start_date} and end date: {end_date}") # logger.debug(f"Querying with start date: {start_date} and end date: {end_date}")
match start_date: match start_date:
case date(): case date() | datetime():
# logger.debug(f"Lookup BasicSubmission by start_date({start_date})") # logger.debug(f"Lookup BasicSubmission by start_date({start_date})")
start_date = start_date.strftime("%Y-%m-%d") start_date = start_date.strftime("%Y-%m-%d")
case int(): case int():
@@ -1098,14 +1124,18 @@ class BasicSubmission(BaseClass, LogMixin):
match end_date: match end_date:
case date() | datetime(): case date() | datetime():
# logger.debug(f"Lookup BasicSubmission by end_date({end_date})") # 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") end_date = end_date.strftime("%Y-%m-%d")
case int(): case int():
# logger.debug(f"Lookup BasicSubmission by ordinal end_date {end_date}") # 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") "%Y-%m-%d")
case _: case _:
# logger.debug(f"Lookup BasicSubmission by parsed str end_date {end_date}") # 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") # logger.debug(f"Compensating for same date by using time")
if start_date == end_date: if start_date == end_date:
start_date = datetime.strptime(start_date, "%Y-%m-%d").strftime("%Y-%m-%d %H:%M:%S.%f") 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 = pyd.to_writer()
writer.xl.save(filename=fname.with_suffix(".xlsx")) 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 # Below are the custom submission types
class BacterialCulture(BasicSubmission, LogMixin): class BacterialCulture(BasicSubmission):
""" """
derivative submission type from BasicSubmission derivative submission type from BasicSubmission
""" """
@@ -1429,7 +1471,7 @@ class BacterialCulture(BasicSubmission, LogMixin):
return input_dict return input_dict
class Wastewater(BasicSubmission, LogMixin): class Wastewater(BasicSubmission):
""" """
derivative submission type from BasicSubmission derivative submission type from BasicSubmission
""" """
@@ -1868,7 +1910,7 @@ class WastewaterArtic(BasicSubmission):
pass pass
try: try:
input_dict['source_plate_number'] = int(input_dict['source_plate_number']) input_dict['source_plate_number'] = int(input_dict['source_plate_number'])
except ValueError: except (ValueError, KeyError):
input_dict['source_plate_number'] = 0 input_dict['source_plate_number'] = 0
# NOTE: Because generate_sample_object needs the submitter_id and the artic has the "({origin well})" # 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 :( # 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}") # logger.debug(f"Done converting {self} after {time()-start}")
return sample return sample
def to_pydantic(self):
from backend.validators import PydSample
return PydSample(**self.to_sub_dict())
def set_attribute(self, name: str, value): def set_attribute(self, name: str, value):
""" """
Custom attribute setter (depreciated over built-in __setattr__) Custom attribute setter (depreciated over built-in __setattr__)
@@ -2733,6 +2779,10 @@ class SubmissionSampleAssociation(BaseClass):
sample['submission_rank'] = self.submission_rank sample['submission_rank'] = self.submission_rank
return sample return sample
def to_pydantic(self):
from backend.validators import PydSample
return PydSample(**self.to_sub_dict())
def to_hitpick(self) -> dict | None: def to_hitpick(self) -> dict | None:
""" """
Outputs a dictionary usable for html plate maps. Outputs a dictionary usable for html plate maps.

View File

@@ -227,6 +227,9 @@ class InfoParser(object):
case "submission_type": case "submission_type":
value, missing = is_missing(value) value, missing = is_missing(value)
value = value.title() value = value.title()
case "submitted_date":
value, missing = is_missing(value)
logger.debug(f"Parsed submitted date: {value}")
# NOTE: is field a JSON? # NOTE: is field a JSON?
case thing if thing in self.sub_object.jsons(): case thing if thing in self.sub_object.jsons():
value, missing = is_missing(value) value, missing = is_missing(value)

View File

@@ -807,22 +807,28 @@ class PydSubmission(BaseModel, extra='allow'):
# logger.debug(f"Setting {key} to {value}") # logger.debug(f"Setting {key} to {value}")
match key: match key:
case "reagents": case "reagents":
if report.results[0].code == 1: # if report.results[0].code == 1:
instance.submission_reagent_associations = [] # instance.submission_reagent_associations = []
# logger.debug(f"Looking through {self.reagents}") # logger.debug(f"Looking through {self.reagents}")
for reagent in self.reagents: for reagent in self.reagents:
reagent, assoc, _ = reagent.toSQL(submission=instance) reagent, assoc, _ = reagent.toSQL(submission=instance)
# logger.debug(f"Association: {assoc}") # logger.debug(f"Association: {assoc}")
if assoc is not None: # and assoc not in instance.submission_reagent_associations: if assoc is not None: # and assoc not in instance.submission_reagent_associations:
if assoc not in instance.submission_reagent_associations:
instance.submission_reagent_associations.append(assoc) instance.submission_reagent_associations.append(assoc)
else:
logger.warning(f"Reagent association {assoc} is already present in {instance}")
case "samples": case "samples":
for sample in self.samples: for sample in self.samples:
sample, associations, _ = sample.toSQL(submission=instance) sample, associations, _ = sample.toSQL(submission=instance)
# logger.debug(f"Sample SQL object to be added to submission: {sample.__dict__}") # logger.debug(f"Sample SQL object to be added to submission: {sample.__dict__}")
logger.debug(associations) # logger.debug(associations)
for assoc in associations: for assoc in associations:
if assoc is not None and assoc not in instance.submission_sample_associations: if assoc is not None:
if assoc not in instance.submission_sample_associations:
instance.submission_sample_associations.append(assoc) instance.submission_sample_associations.append(assoc)
else:
logger.warning(f"Sample association {assoc} is already present in {instance}")
case "equipment": case "equipment":
# logger.debug(f"Equipment: {pformat(self.equipment)}") # logger.debug(f"Equipment: {pformat(self.equipment)}")
for equip in self.equipment: for equip in self.equipment:
@@ -841,8 +847,11 @@ class PydSubmission(BaseModel, extra='allow'):
association = tips.to_sql(submission=instance) association = tips.to_sql(submission=instance)
except AttributeError: except AttributeError:
continue continue
if association is not None and association not in instance.submission_tips_associations: if association is not None:
if association not in instance.submission_tips_associations:
instance.submission_tips_associations.append(association) instance.submission_tips_associations.append(association)
else:
logger.warning(f"Tips association {association} is already present in {instance}")
case item if item in instance.timestamps(): case item if item in instance.timestamps():
logger.warning(f"Incoming timestamp key: {item}, with value: {value}") logger.warning(f"Incoming timestamp key: {item}, with value: {value}")
# value = value.replace(tzinfo=timezone) # value = value.replace(tzinfo=timezone)
@@ -870,6 +879,11 @@ class PydSubmission(BaseModel, extra='allow'):
value[k] = v value[k] = v
instance.set_attribute(key=key, value=value) instance.set_attribute(key=key, value=value)
case _: case _:
try:
check = instance.__getattribute__(key) != value
except AttributeError:
continue
if check:
try: try:
instance.set_attribute(key=key, value=value) instance.set_attribute(key=key, value=value)
# instance.update({key:value}) # instance.update({key:value})
@@ -878,7 +892,8 @@ class PydSubmission(BaseModel, extra='allow'):
continue continue
except KeyError: except KeyError:
continue continue
print(f"\n\n{instance}\n\n") else:
logger.warning(f"{key} already == {value} so no updating.")
try: try:
# logger.debug(f"Calculating costs for procedure...") # logger.debug(f"Calculating costs for procedure...")
instance.calculate_base_cost() instance.calculate_base_cost()
@@ -1119,6 +1134,16 @@ class PydPCRControl(BaseModel):
submission_id: int submission_id: int
controltype_name: str controltype_name: str
def to_sql(self):
instance = PCRControl.query(name=self.name)
if not instance:
instance = PCRControl()
for key in self.model_fields:
field_value = self.__getattribute__(key)
if instance.__getattribute__(key) != field_value:
instance.__setattr__(key, field_value)
return instance
class PydIridaControl(BaseModel, extra='ignore'): class PydIridaControl(BaseModel, extra='ignore'):
name: str name: str
@@ -1133,3 +1158,13 @@ class PydIridaControl(BaseModel, extra='ignore'):
submitted_date: datetime #: Date submitted to Robotics submitted_date: datetime #: Date submitted to Robotics
submission_id: int submission_id: int
controltype_name: str controltype_name: str
def to_sql(self):
instance = IridaControl.query(name=self.name)
if not instance:
instance = IridaControl()
for key in self.model_fields:
field_value = self.__getattribute__(key)
if instance.__getattribute__(key) != field_value:
instance.__setattr__(key, field_value)
return instance

View File

@@ -110,7 +110,7 @@ class AddReagentForm(QDialog):
""" """
# logger.debug(self.type_input.currentText()) # logger.debug(self.type_input.currentText())
self.name_input.clear() self.name_input.clear()
lookup = Reagent.query(reagent_role=self.type_input.currentText()) lookup = Reagent.query(role=self.type_input.currentText())
self.name_input.addItems(list(set([item.name for item in lookup]))) self.name_input.addItems(list(set([item.name for item in lookup])))

View File

@@ -176,6 +176,7 @@ class SubmissionDetails(QDialog):
if isinstance(submission, str): if isinstance(submission, str):
submission = BasicSubmission.query(rsl_plate_num=submission) submission = BasicSubmission.query(rsl_plate_num=submission)
submission.signed_by = getuser() submission.signed_by = getuser()
submission.completed = datetime.now().date()
submission.save() submission.save()
self.submission_details(submission=self.rsl_plate_num) self.submission_details(submission=self.rsl_plate_num)

View File

@@ -542,10 +542,25 @@ class SubmissionFormWidget(QWidget):
# NOTE: lookup organizations suitable for submitting_lab (ctx: self.InfoItem.SubmissionFormWidget.SubmissionFormContainer.AddSubForm ) # NOTE: lookup organizations suitable for submitting_lab (ctx: self.InfoItem.SubmissionFormWidget.SubmissionFormContainer.AddSubForm )
labs = [item.name for item in Organization.query()] labs = [item.name for item in Organization.query()]
# NOTE: try to set closest match to top of list # NOTE: try to set closest match to top of list
# try:
# labs = difflib.get_close_matches(value, labs, len(labs), 0)
# except (TypeError, ValueError):
# pass
if isinstance(value, dict):
value = value['value']
if isinstance(value, Organization):
value = value.name
try: try:
labs = difflib.get_close_matches(value, labs, len(labs), 0) looked_up_lab = Organization.query(name=value, limit=1)
except (TypeError, ValueError): except AttributeError:
pass looked_up_lab = None
logger.debug(f"\n\nLooked up lab: {looked_up_lab}")
if looked_up_lab:
try:
labs.remove(str(looked_up_lab.name))
except ValueError as e:
logger.error(f"Error reordering labs: {e}")
labs.insert(0, str(looked_up_lab.name))
# NOTE: set combobox values to lookedup values # NOTE: set combobox values to lookedup values
add_widget.addItems(labs) add_widget.addItems(labs)
add_widget.setToolTip("Select submitting lab.") add_widget.setToolTip("Select submitting lab.")
@@ -760,7 +775,6 @@ class SubmissionFormWidget(QWidget):
if looked_up_reg: if looked_up_reg:
try: try:
relevant_reagents.remove(str(looked_up_reg.lot)) relevant_reagents.remove(str(looked_up_reg.lot))
except ValueError as e: except ValueError as e:
logger.error(f"Error reordering relevant reagents: {e}") logger.error(f"Error reordering relevant reagents: {e}")
relevant_reagents.insert(0, str(looked_up_reg.lot)) relevant_reagents.insert(0, str(looked_up_reg.lot))

View File

@@ -71,7 +71,7 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block signing_button %} {% block signing_button %}
{% if permission %} {% if permission and not sub['signed_by'] %}
<button type="button" id="sign_btn">Sign Off</button> <button type="button" id="sign_btn">Sign Off</button>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -5,10 +5,12 @@ from __future__ import annotations
import json import json
import pprint import pprint
from datetime import date, datetime, timedelta
from json import JSONDecodeError from json import JSONDecodeError
import numpy as np import numpy as np
import logging, re, yaml, sys, os, stat, platform, getpass, inspect import logging, re, yaml, sys, os, stat, platform, getpass, inspect
import pandas as pd import pandas as pd
from dateutil.easter import easter
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from logging import handlers from logging import handlers
from pathlib import Path from pathlib import Path
@@ -990,3 +992,41 @@ def report_result(func):
return wrapper return wrapper
def create_holidays_for_year(year: int|None=None) -> List[date]:
def find_nth_monday(year, month, occurence: int | None=None, day: int|None=None):
if not occurence:
occurence = 1
if not day:
day = occurence * 7
max_days = (date(2012, month+1, 1) - date(2012, month, 1)).days
if day > max_days:
day = max_days
try:
d = datetime(year, int(month), day=day)
except ValueError:
return
offset = -d.weekday() # weekday == 0 means Monday
output = d + timedelta(offset)
return output.date()
if not year:
year = date.today().year
# Includes New Year's day for next year.
holidays = [date(year, 1, 1), date(year, 7,1), date(year, 9, 30),
date(year, 11, 11), date(year, 12, 25), date(year, 12, 26),
date(year+1, 1, 1)]
# August Civic
# holidays.append(find_nth_monday(year, 8))
# Labour Day
holidays.append(find_nth_monday(year, 9))
# Thanksgiving
holidays.append(find_nth_monday(year, 10, occurence=2))
# Victoria Day
holidays.append(find_nth_monday(year, 5, day=25))
# Easter, etc
holidays.append(easter(year) - timedelta(days=2))
holidays.append(easter(year) + timedelta(days=1))
return sorted(holidays)