Mid-progress adding controls to pydantic creation.
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
"""
|
||||
All database related operations.
|
||||
"""
|
||||
from sqlalchemy import event
|
||||
import sqlalchemy.orm
|
||||
from sqlalchemy import event, inspect
|
||||
from sqlalchemy.engine import Engine
|
||||
|
||||
from tools import ctx
|
||||
|
||||
|
||||
@@ -16,7 +18,7 @@ def set_sqlite_pragma(dbapi_connection, connection_record):
|
||||
Args:
|
||||
dbapi_connection (_type_): _description_
|
||||
connection_record (_type_): _description_
|
||||
"""
|
||||
"""
|
||||
cursor = dbapi_connection.cursor()
|
||||
# print(ctx.database_schema)
|
||||
if ctx.database_schema == "sqlite":
|
||||
@@ -34,3 +36,39 @@ def set_sqlite_pragma(dbapi_connection, connection_record):
|
||||
|
||||
|
||||
from .models import *
|
||||
|
||||
|
||||
def update_log(mapper, connection, target):
|
||||
logger.debug("\n\nBefore update\n\n")
|
||||
state = inspect(target)
|
||||
logger.debug(state)
|
||||
update = dict(user=getuser(), time=datetime.now(), object=str(state.object), changes=[])
|
||||
logger.debug(update)
|
||||
for attr in state.attrs:
|
||||
hist = attr.load_history()
|
||||
if not hist.has_changes():
|
||||
continue
|
||||
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)}")
|
||||
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}")
|
||||
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)
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import sys, logging
|
||||
|
||||
import pandas as pd
|
||||
from sqlalchemy import Column, INTEGER, String, JSON
|
||||
from sqlalchemy import Column, INTEGER, String, JSON, event, inspect
|
||||
from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session
|
||||
from sqlalchemy.ext.declarative import declared_attr
|
||||
from sqlalchemy.exc import ArgumentError
|
||||
@@ -22,6 +22,12 @@ Base: DeclarativeMeta = declarative_base()
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
|
||||
class LogMixin(Base):
|
||||
|
||||
__abstract__ = True
|
||||
|
||||
|
||||
|
||||
class BaseClass(Base):
|
||||
"""
|
||||
Abstract class to pass ctx values to all SQLAlchemy objects.
|
||||
@@ -99,6 +105,15 @@ class BaseClass(Base):
|
||||
singles = list(set(cls.singles + BaseClass.singles))
|
||||
return dict(singles=singles)
|
||||
|
||||
@classmethod
|
||||
def find_regular_subclass(cls, name: str | None = None):
|
||||
if not name:
|
||||
return cls
|
||||
if " " in name:
|
||||
search = name.title().replace(" ", "")
|
||||
logger.debug(f"Searching for subclass: {search}")
|
||||
return next((item for item in cls.__subclasses__() if item.__name__ == search), cls)
|
||||
|
||||
@classmethod
|
||||
def fuzzy_search(cls, **kwargs):
|
||||
query: Query = cls.__database_session__.query(cls)
|
||||
@@ -175,9 +190,11 @@ class BaseClass(Base):
|
||||
"""
|
||||
# logger.debug(f"Saving object: {pformat(self.__dict__)}")
|
||||
report = Report()
|
||||
state = inspect(self)
|
||||
try:
|
||||
self.__database_session__.add(self)
|
||||
self.__database_session__.commit()
|
||||
return state
|
||||
except Exception as e:
|
||||
logger.critical(f"Problem saving object: {e}")
|
||||
logger.error(f"Error message: {type(e)}")
|
||||
@@ -186,6 +203,8 @@ class BaseClass(Base):
|
||||
return report
|
||||
|
||||
|
||||
|
||||
|
||||
class ConfigItem(BaseClass):
|
||||
"""
|
||||
Key:JSON objects to store config settings in database.
|
||||
@@ -222,6 +241,7 @@ from .controls import *
|
||||
from .organizations import *
|
||||
from .kits import *
|
||||
from .submissions import *
|
||||
from .audit import AuditLog
|
||||
|
||||
# NOTE: Add a creator to the submission for reagent association. Assigned here due to circular import constraints.
|
||||
# https://docs.sqlalchemy.org/en/20/orm/extensions/associationproxy.html#sqlalchemy.ext.associationproxy.association_proxy.params.creator
|
||||
|
||||
@@ -16,6 +16,7 @@ from typing import List, Literal, Tuple, Generator
|
||||
from dateutil.parser import parse
|
||||
from re import Pattern
|
||||
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
|
||||
@@ -429,6 +430,10 @@ class PCRControl(Control):
|
||||
fig = PCRFigure(df=df, modes=[])
|
||||
return report, fig
|
||||
|
||||
def to_pydantic(self):
|
||||
from backend.validators import PydPCRControl
|
||||
return PydPCRControl(**self.to_sub_dict())
|
||||
|
||||
|
||||
class IridaControl(Control):
|
||||
|
||||
@@ -878,3 +883,7 @@ class IridaControl(Control):
|
||||
exclude = [re.sub(rerun_regex, "", sample) for sample in sample_names if rerun_regex.search(sample)]
|
||||
df = df[df.name not in exclude]
|
||||
return df
|
||||
|
||||
def to_pydantic(self):
|
||||
from backend.validators import PydIridaControl
|
||||
return PydIridaControl(**self.__dict__)
|
||||
|
||||
@@ -567,7 +567,7 @@ class Reagent(BaseClass):
|
||||
return cls.execute_query(query=query, limit=limit)
|
||||
|
||||
@check_authorization
|
||||
def edit_from_search(self, **kwargs):
|
||||
def edit_from_search(self, obj, **kwargs):
|
||||
from frontend.widgets.misc import AddReagentForm
|
||||
role = ReagentRole.query(kwargs['role'])
|
||||
if role:
|
||||
@@ -1279,7 +1279,10 @@ class SubmissionReagentAssociation(BaseClass):
|
||||
Returns:
|
||||
str: Representation of this SubmissionReagentAssociation
|
||||
"""
|
||||
return f"<{self.submission.rsl_plate_num} & {self.reagent.lot}>"
|
||||
try:
|
||||
return f"<{self.submission.rsl_plate_num} & {self.reagent.lot}>"
|
||||
except AttributeError:
|
||||
return f"<Unknown Submission & {self.reagent.lot}"
|
||||
|
||||
def __init__(self, reagent=None, submission=None):
|
||||
if isinstance(reagent, list):
|
||||
|
||||
@@ -12,8 +12,8 @@ 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
|
||||
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case
|
||||
from . import BaseClass, Reagent, SubmissionType, KitType, Organization, Contact, LogMixin
|
||||
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, event, inspect
|
||||
from sqlalchemy.orm import relationship, validates, Query
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
@@ -36,7 +36,7 @@ from PIL import Image
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
|
||||
class BasicSubmission(BaseClass):
|
||||
class BasicSubmission(BaseClass, LogMixin):
|
||||
"""
|
||||
Concrete of basic submission which polymorphs into BacterialCulture and Wastewater
|
||||
"""
|
||||
@@ -544,7 +544,7 @@ class BasicSubmission(BaseClass):
|
||||
field_value = len(self.samples)
|
||||
else:
|
||||
field_value = value
|
||||
case "ctx" | "csv" | "filepath" | "equipment":
|
||||
case "ctx" | "csv" | "filepath" | "equipment" | "controls":
|
||||
return
|
||||
case item if item in self.jsons():
|
||||
match key:
|
||||
@@ -577,10 +577,13 @@ class BasicSubmission(BaseClass):
|
||||
except AttributeError:
|
||||
field_value = value
|
||||
# NOTE: insert into field
|
||||
try:
|
||||
self.__setattr__(key, field_value)
|
||||
except AttributeError as e:
|
||||
logger.error(f"Could not set {self} attribute {key} to {value} due to \n{e}")
|
||||
current = self.__getattribute__(key)
|
||||
if field_value and current != field_value:
|
||||
logger.debug(f"Updated value: {key}: {current} to {field_value}")
|
||||
try:
|
||||
self.__setattr__(key, field_value)
|
||||
except AttributeError as e:
|
||||
logger.error(f"Could not set {self} attribute {key} to {value} due to \n{e}")
|
||||
|
||||
def update_subsampassoc(self, sample: BasicSample, input_dict: dict):
|
||||
"""
|
||||
@@ -1339,7 +1342,7 @@ class BasicSubmission(BaseClass):
|
||||
|
||||
# Below are the custom submission types
|
||||
|
||||
class BacterialCulture(BasicSubmission):
|
||||
class BacterialCulture(BasicSubmission, LogMixin):
|
||||
"""
|
||||
derivative submission type from BasicSubmission
|
||||
"""
|
||||
@@ -1426,7 +1429,7 @@ class BacterialCulture(BasicSubmission):
|
||||
return input_dict
|
||||
|
||||
|
||||
class Wastewater(BasicSubmission):
|
||||
class Wastewater(BasicSubmission, LogMixin):
|
||||
"""
|
||||
derivative submission type from BasicSubmission
|
||||
"""
|
||||
@@ -2189,6 +2192,8 @@ class BasicSample(BaseClass):
|
||||
Base of basic sample which polymorphs into BCSample and WWSample
|
||||
"""
|
||||
|
||||
searchables = ['submitter_id']
|
||||
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
submitter_id = Column(String(64), nullable=False, unique=True) #: identification from submitter
|
||||
sample_type = Column(String(32)) #: mode_sub_type of sample
|
||||
@@ -2509,6 +2514,9 @@ class BasicSample(BaseClass):
|
||||
if dlg.exec():
|
||||
pass
|
||||
|
||||
def edit_from_search(self, obj, **kwargs):
|
||||
self.show_details(obj)
|
||||
|
||||
|
||||
# Below are the custom sample types
|
||||
|
||||
@@ -2516,6 +2524,9 @@ class WastewaterSample(BasicSample):
|
||||
"""
|
||||
Derivative wastewater sample
|
||||
"""
|
||||
|
||||
searchables = BasicSample.searchables + ['ww_processing_num', 'ww_full_sample_id', 'rsl_number']
|
||||
|
||||
id = Column(INTEGER, ForeignKey('_basicsample.id'), primary_key=True)
|
||||
ww_processing_num = Column(String(64)) #: wastewater processing number
|
||||
ww_full_sample_id = Column(String(64)) #: full id given by entrics
|
||||
|
||||
Reference in New Issue
Block a user