Prior to updating queries to use query alias.
This commit is contained in:
@@ -6,13 +6,12 @@ import sys, logging
|
|||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import Column, INTEGER, String, JSON
|
from sqlalchemy import Column, INTEGER, String, JSON
|
||||||
from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session
|
from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session, InstrumentedAttribute
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
from sqlalchemy.exc import ArgumentError
|
from sqlalchemy.exc import ArgumentError
|
||||||
from typing import Any, List
|
from typing import Any, List
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tools import report_result
|
from tools import report_result, list_sort_dict
|
||||||
|
|
||||||
|
|
||||||
# NOTE: Load testing environment
|
# NOTE: Load testing environment
|
||||||
if 'pytest' in sys.modules:
|
if 'pytest' in sys.modules:
|
||||||
@@ -49,6 +48,30 @@ class BaseClass(Base):
|
|||||||
__table_args__ = {'extend_existing': True} #: Will only add new columns
|
__table_args__ = {'extend_existing': True} #: Will only add new columns
|
||||||
|
|
||||||
singles = ['id']
|
singles = ['id']
|
||||||
|
omni_removes = ['submissions']
|
||||||
|
omni_sort = ["name"]
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def skip_on_edit(cls):
|
||||||
|
if "association" in cls.__name__.lower() or cls.__name__.lower() == "discount":
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def aliases(cls):
|
||||||
|
return [cls.__name__.lower()]
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def level(cls):
|
||||||
|
if "association" in cls.__name__.lower() or cls.__name__.lower() == "discount":
|
||||||
|
return 2
|
||||||
|
else:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def query_alias(cls):
|
||||||
|
return cls.__name__.lower()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@declared_attr
|
@declared_attr
|
||||||
@@ -175,7 +198,7 @@ class BaseClass(Base):
|
|||||||
try:
|
try:
|
||||||
records = [obj.to_sub_dict(**kwargs) for obj in objects]
|
records = [obj.to_sub_dict(**kwargs) for obj in objects]
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
records = [obj.omnigui_dict for obj in objects]
|
records = [{k:v['instance_attr'] for k, v in obj.to_omnigui_dict(**kwargs).items()} for obj in objects]
|
||||||
return DataFrame.from_records(records)
|
return DataFrame.from_records(records)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -249,11 +272,25 @@ class BaseClass(Base):
|
|||||||
Returns:
|
Returns:
|
||||||
dict: Dictionary of object minus _sa_instance_state with id at the front.
|
dict: Dictionary of object minus _sa_instance_state with id at the front.
|
||||||
"""
|
"""
|
||||||
dicto = {k: v for k, v in self.__dict__.items() if k not in ["_sa_instance_state"]}
|
# dicto = {k: v for k, v in self.__dict__.items() if k not in ["_sa_instance_state"]}
|
||||||
|
dicto = {key: dict(class_attr=getattr(self.__class__, key), instance_attr=getattr(self, key))
|
||||||
|
for key in dir(self.__class__) if
|
||||||
|
isinstance(getattr(self.__class__, key), InstrumentedAttribute) and key not in self.omni_removes
|
||||||
|
}
|
||||||
|
for k, v in dicto.items():
|
||||||
|
try:
|
||||||
|
v['instance_attr'] = v['instance_attr'].name
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
dicto = list_sort_dict(input_dict=dicto, sort_list=self.__class__.omni_sort)
|
||||||
|
except TypeError as e:
|
||||||
|
logger.error(f"Could not sort {self.__class__.__name__} by list due to :{e}")
|
||||||
try:
|
try:
|
||||||
dicto = {'id': dicto.pop('id'), **dicto}
|
dicto = {'id': dicto.pop('id'), **dicto}
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
# logger.debug(f"{self.__class__.__name__} omnigui dict:\n\n{pformat(dicto)}")
|
||||||
return dicto
|
return dicto
|
||||||
|
|
||||||
@classproperty
|
@classproperty
|
||||||
@@ -268,6 +305,7 @@ class BaseClass(Base):
|
|||||||
try:
|
try:
|
||||||
model = getattr(pydant, f"Pyd{cls.__name__}")
|
model = getattr(pydant, f"Pyd{cls.__name__}")
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
logger.warning(f"Couldn't get {cls.__name__} pydantic model.")
|
||||||
return None
|
return None
|
||||||
return model
|
return model
|
||||||
|
|
||||||
@@ -281,6 +319,13 @@ class BaseClass(Base):
|
|||||||
"""
|
"""
|
||||||
return dict()
|
return dict()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def relevant_relationships(cls, relationship_instance):
|
||||||
|
query_kwargs = {relationship_instance.query_alias:relationship_instance}
|
||||||
|
return cls.query(**query_kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigItem(BaseClass):
|
class ConfigItem(BaseClass):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from pprint import pformat
|
|||||||
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB
|
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB
|
||||||
from sqlalchemy.orm import relationship, validates, Query
|
from sqlalchemy.orm import relationship, validates, Query
|
||||||
from sqlalchemy.ext.associationproxy import association_proxy
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, yaml_regex_creator, timezone
|
from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, yaml_regex_creator, timezone
|
||||||
from typing import List, Literal, Generator, Any, Tuple
|
from typing import List, Literal, Generator, Any, Tuple
|
||||||
@@ -95,6 +96,8 @@ class KitType(BaseClass):
|
|||||||
Base of kits used in submission processing
|
Base of kits used in submission processing
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
query_alias = "kit_type"
|
||||||
|
|
||||||
id = Column(INTEGER, primary_key=True) #: primary key
|
id = Column(INTEGER, primary_key=True) #: primary key
|
||||||
name = Column(String(64), unique=True) #: name of kit
|
name = Column(String(64), unique=True) #: name of kit
|
||||||
submissions = relationship("BasicSubmission", back_populates="extraction_kit") #: submissions this kit was used for
|
submissions = relationship("BasicSubmission", back_populates="extraction_kit") #: submissions this kit was used for
|
||||||
@@ -129,6 +132,10 @@ class KitType(BaseClass):
|
|||||||
"""
|
"""
|
||||||
return f"<KitType({self.name})>"
|
return f"<KitType({self.name})>"
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def aliases(cls):
|
||||||
|
return super().aliases + [cls.query_alias, "kit_types"]
|
||||||
|
|
||||||
def get_reagents(self,
|
def get_reagents(self,
|
||||||
required: bool = False,
|
required: bool = False,
|
||||||
submission_type: str | SubmissionType | None = None
|
submission_type: str | SubmissionType | None = None
|
||||||
@@ -157,7 +164,7 @@ class KitType(BaseClass):
|
|||||||
else:
|
else:
|
||||||
return (item.reagent_role for item in relevant_associations)
|
return (item.reagent_role for item in relevant_associations)
|
||||||
|
|
||||||
def construct_xl_map_for_use(self, submission_type: str | SubmissionType) -> Tuple[dict|None, KitType]:
|
def construct_xl_map_for_use(self, submission_type: str | SubmissionType) -> Tuple[dict | None, KitType]:
|
||||||
"""
|
"""
|
||||||
Creates map of locations in Excel workbook for a SubmissionType
|
Creates map of locations in Excel workbook for a SubmissionType
|
||||||
|
|
||||||
@@ -402,12 +409,14 @@ class ReagentRole(BaseClass):
|
|||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
kit_type: KitType | str | None = None,
|
kit_type: KitType | str | None = None,
|
||||||
reagent: Reagent | str | None = None,
|
reagent: Reagent | str | None = None,
|
||||||
|
id: int | None = None,
|
||||||
limit: int = 0,
|
limit: int = 0,
|
||||||
) -> ReagentRole | List[ReagentRole]:
|
) -> ReagentRole | List[ReagentRole]:
|
||||||
"""
|
"""
|
||||||
Lookup reagent types in the database.
|
Lookup reagent types in the database.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
id (id | None, optional): Id of the object. Defaults to None.
|
||||||
name (str | None, optional): Reagent type name. Defaults to None.
|
name (str | None, optional): Reagent type name. Defaults to None.
|
||||||
kit_type (KitType | str | None, optional): Kit the type of interest belongs to. Defaults to None.
|
kit_type (KitType | str | None, optional): Kit the type of interest belongs to. Defaults to None.
|
||||||
reagent (Reagent | str | None, optional): Concrete instance of the type of interest. Defaults to None.
|
reagent (Reagent | str | None, optional): Concrete instance of the type of interest. Defaults to None.
|
||||||
@@ -445,6 +454,12 @@ class ReagentRole(BaseClass):
|
|||||||
limit = 1
|
limit = 1
|
||||||
case _:
|
case _:
|
||||||
pass
|
pass
|
||||||
|
match id:
|
||||||
|
case int():
|
||||||
|
query = query.filter(cls.id == id)
|
||||||
|
limit = 1
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
return cls.execute_query(query=query, limit=limit)
|
return cls.execute_query(query=query, limit=limit)
|
||||||
|
|
||||||
def to_pydantic(self) -> "PydReagent":
|
def to_pydantic(self) -> "PydReagent":
|
||||||
@@ -476,7 +491,7 @@ class Reagent(BaseClass, LogMixin):
|
|||||||
Concrete reagent instance
|
Concrete reagent instance
|
||||||
"""
|
"""
|
||||||
|
|
||||||
searchables = [dict(label="Lot", field="lot")]
|
|
||||||
|
|
||||||
id = Column(INTEGER, primary_key=True) #: primary key
|
id = Column(INTEGER, primary_key=True) #: primary key
|
||||||
role = relationship("ReagentRole", back_populates="instances",
|
role = relationship("ReagentRole", back_populates="instances",
|
||||||
@@ -504,6 +519,10 @@ class Reagent(BaseClass, LogMixin):
|
|||||||
name = f"<Reagent({self.role.name}-{self.lot})>"
|
name = f"<Reagent({self.role.name}-{self.lot})>"
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def searchables(cls):
|
||||||
|
return [dict(label="Lot", field="lot")]
|
||||||
|
|
||||||
def to_sub_dict(self, extraction_kit: KitType = None, full_data: bool = False, **kwargs) -> dict:
|
def to_sub_dict(self, extraction_kit: KitType = None, full_data: bool = False, **kwargs) -> dict:
|
||||||
"""
|
"""
|
||||||
dictionary containing values necessary for gui
|
dictionary containing values necessary for gui
|
||||||
@@ -581,7 +600,7 @@ class Reagent(BaseClass, LogMixin):
|
|||||||
from backend.validators.pydant import PydReagent
|
from backend.validators.pydant import PydReagent
|
||||||
new = False
|
new = False
|
||||||
disallowed = ['expiry']
|
disallowed = ['expiry']
|
||||||
sanitized_kwargs = {k:v for k,v in kwargs.items() if k not in disallowed}
|
sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed}
|
||||||
instance = cls.query(**sanitized_kwargs)
|
instance = cls.query(**sanitized_kwargs)
|
||||||
if not instance or isinstance(instance, list):
|
if not instance or isinstance(instance, list):
|
||||||
if "role" not in kwargs:
|
if "role" not in kwargs:
|
||||||
@@ -700,6 +719,8 @@ class Discount(BaseClass):
|
|||||||
Relationship table for client labs for certain kits.
|
Relationship table for client labs for certain kits.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
skip_on_edit = True
|
||||||
|
|
||||||
id = Column(INTEGER, primary_key=True) #: primary key
|
id = Column(INTEGER, primary_key=True) #: primary key
|
||||||
kit = relationship("KitType") #: joined parent reagent type
|
kit = relationship("KitType") #: joined parent reagent type
|
||||||
kit_id = Column(INTEGER, ForeignKey("_kittype.id", ondelete='SET NULL', name="fk_kit_type_id")) #: id of joined kit
|
kit_id = Column(INTEGER, ForeignKey("_kittype.id", ondelete='SET NULL', name="fk_kit_type_id")) #: id of joined kit
|
||||||
@@ -812,6 +833,14 @@ class SubmissionType(BaseClass):
|
|||||||
"""
|
"""
|
||||||
return f"<SubmissionType({self.name})>"
|
return f"<SubmissionType({self.name})>"
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def aliases(cls):
|
||||||
|
return super().aliases + ["submission_types", "submission_type"]
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def omni_removes(cls):
|
||||||
|
return super().omni_removes + ["template_file", "defaults", "instances"]
|
||||||
|
|
||||||
@classproperty
|
@classproperty
|
||||||
def basic_template(cls) -> bytes:
|
def basic_template(cls) -> bytes:
|
||||||
"""
|
"""
|
||||||
@@ -1063,6 +1092,10 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
|
|||||||
Abstract of relationship between kits and their submission type.
|
Abstract of relationship between kits and their submission type.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
omni_removes = BaseClass.omni_removes + ["submission_types_id", "kits_id"]
|
||||||
|
omni_sort = ["submission_type", "kit_type"]
|
||||||
|
level = 2
|
||||||
|
|
||||||
submission_types_id = Column(INTEGER, ForeignKey("_submissiontype.id"),
|
submission_types_id = Column(INTEGER, ForeignKey("_submissiontype.id"),
|
||||||
primary_key=True) #: id of joined submission type
|
primary_key=True) #: id of joined submission type
|
||||||
kits_id = Column(INTEGER, ForeignKey("_kittype.id"), primary_key=True) #: id of joined kit
|
kits_id = Column(INTEGER, ForeignKey("_kittype.id"), primary_key=True) #: id of joined kit
|
||||||
@@ -1093,12 +1126,17 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
|
|||||||
"""
|
"""
|
||||||
return f"<SubmissionTypeKitTypeAssociation({self.submission_type.name}&{self.kit_type.name})>"
|
return f"<SubmissionTypeKitTypeAssociation({self.submission_type.name}&{self.kit_type.name})>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return f"{self.submission_type.name} -> {self.kit_type.name}"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@setup_lookup
|
@setup_lookup
|
||||||
def query(cls,
|
def query(cls,
|
||||||
submission_type: SubmissionType | str | int | None = None,
|
submission_type: SubmissionType | str | int | None = None,
|
||||||
kit_type: KitType | str | int | None = None,
|
kit_type: KitType | str | int | None = None,
|
||||||
limit: int = 0
|
limit: int = 0,
|
||||||
|
**kwargs
|
||||||
) -> SubmissionTypeKitTypeAssociation | List[SubmissionTypeKitTypeAssociation]:
|
) -> SubmissionTypeKitTypeAssociation | List[SubmissionTypeKitTypeAssociation]:
|
||||||
"""
|
"""
|
||||||
Lookup SubmissionTypeKitTypeAssociations of interest.
|
Lookup SubmissionTypeKitTypeAssociations of interest.
|
||||||
@@ -1126,7 +1164,9 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
|
|||||||
query = query.join(KitType).filter(KitType.name == kit_type)
|
query = query.join(KitType).filter(KitType.name == kit_type)
|
||||||
case int():
|
case int():
|
||||||
query = query.join(KitType).filter(KitType.id == kit_type)
|
query = query.join(KitType).filter(KitType.id == kit_type)
|
||||||
limit = query.count()
|
if kit_type is not None and submission_type is not None:
|
||||||
|
limit = 1
|
||||||
|
# limit = query.count()
|
||||||
return cls.execute_query(query=query, limit=limit)
|
return cls.execute_query(query=query, limit=limit)
|
||||||
|
|
||||||
def to_export_dict(self):
|
def to_export_dict(self):
|
||||||
@@ -1148,6 +1188,9 @@ class KitTypeReagentRoleAssociation(BaseClass):
|
|||||||
DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
|
DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
omni_removes = BaseClass.omni_removes + ["submission_type_id", "kits_id", "reagent_roles_id", "last_used"]
|
||||||
|
omni_sort = ["submission_type", "kit_type", "reagent_role", "required", "uses"]
|
||||||
|
|
||||||
reagent_roles_id = Column(INTEGER, ForeignKey("_reagentrole.id"),
|
reagent_roles_id = Column(INTEGER, ForeignKey("_reagentrole.id"),
|
||||||
primary_key=True) #: id of associated reagent type
|
primary_key=True) #: id of associated reagent type
|
||||||
kits_id = Column(INTEGER, ForeignKey("_kittype.id"), primary_key=True) #: id of associated reagent type
|
kits_id = Column(INTEGER, ForeignKey("_kittype.id"), primary_key=True) #: id of associated reagent type
|
||||||
@@ -1176,6 +1219,10 @@ class KitTypeReagentRoleAssociation(BaseClass):
|
|||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<KitTypeReagentRoleAssociation({self.kit_type} & {self.reagent_role})>"
|
return f"<KitTypeReagentRoleAssociation({self.kit_type} & {self.reagent_role})>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return f"{self.kit_type.name} -> {self.reagent_role.name}"
|
||||||
|
|
||||||
@validates('required')
|
@validates('required')
|
||||||
def validate_required(self, key, value):
|
def validate_required(self, key, value):
|
||||||
"""
|
"""
|
||||||
@@ -1219,7 +1266,8 @@ class KitTypeReagentRoleAssociation(BaseClass):
|
|||||||
def query(cls,
|
def query(cls,
|
||||||
kit_type: KitType | str | None = None,
|
kit_type: KitType | str | None = None,
|
||||||
reagent_role: ReagentRole | str | None = None,
|
reagent_role: ReagentRole | str | None = None,
|
||||||
limit: int = 0
|
limit: int = 0,
|
||||||
|
**kwargs
|
||||||
) -> KitTypeReagentRoleAssociation | List[KitTypeReagentRoleAssociation]:
|
) -> KitTypeReagentRoleAssociation | List[KitTypeReagentRoleAssociation]:
|
||||||
"""
|
"""
|
||||||
Lookup junction of ReagentType and KitType
|
Lookup junction of ReagentType and KitType
|
||||||
@@ -1280,6 +1328,12 @@ class KitTypeReagentRoleAssociation(BaseClass):
|
|||||||
for rel_reagent in relevant_reagents:
|
for rel_reagent in relevant_reagents:
|
||||||
yield rel_reagent
|
yield rel_reagent
|
||||||
|
|
||||||
|
@property
|
||||||
|
def omnigui_dict(self) -> dict:
|
||||||
|
dicto = super().omnigui_dict
|
||||||
|
dicto['required']['instance_attr'] = bool(dicto['required']['instance_attr'])
|
||||||
|
return dicto
|
||||||
|
|
||||||
|
|
||||||
class SubmissionReagentAssociation(BaseClass):
|
class SubmissionReagentAssociation(BaseClass):
|
||||||
"""
|
"""
|
||||||
@@ -1420,7 +1474,7 @@ class Equipment(BaseClass, LogMixin):
|
|||||||
|
|
||||||
def get_processes(self, submission_type: str | SubmissionType | None = None,
|
def get_processes(self, submission_type: str | SubmissionType | None = None,
|
||||||
extraction_kit: str | KitType | None = None,
|
extraction_kit: str | KitType | None = None,
|
||||||
equipment_role: str | EquipmentRole | None=None) -> List[str]:
|
equipment_role: str | EquipmentRole | 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
|
||||||
|
|
||||||
@@ -1498,7 +1552,8 @@ class Equipment(BaseClass, LogMixin):
|
|||||||
PydEquipment: pydantic equipment object
|
PydEquipment: pydantic equipment object
|
||||||
"""
|
"""
|
||||||
from backend.validators.pydant import PydEquipment
|
from backend.validators.pydant import PydEquipment
|
||||||
processes = self.get_processes(submission_type=submission_type, extraction_kit=extraction_kit, equipment_role=role)
|
processes = self.get_processes(submission_type=submission_type, extraction_kit=extraction_kit,
|
||||||
|
equipment_role=role)
|
||||||
return PydEquipment(processes=processes, role=role,
|
return PydEquipment(processes=processes, role=role,
|
||||||
**self.to_dict(processes=False))
|
**self.to_dict(processes=False))
|
||||||
|
|
||||||
@@ -1718,7 +1773,8 @@ class SubmissionEquipmentAssociation(BaseClass):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@setup_lookup
|
@setup_lookup
|
||||||
def query(cls, equipment_id: int|None=None, submission_id: int|None=None, role: str | None = None, limit: int = 0, **kwargs) \
|
def query(cls, equipment_id: int | None = None, submission_id: int | None = None, role: str | None = None,
|
||||||
|
limit: int = 0, **kwargs) \
|
||||||
-> Any | List[Any]:
|
-> Any | List[Any]:
|
||||||
query: Query = cls.__database_session__.query(cls)
|
query: Query = cls.__database_session__.query(cls)
|
||||||
query = query.filter(cls.equipment_id == equipment_id)
|
query = query.filter(cls.equipment_id == equipment_id)
|
||||||
@@ -1819,9 +1875,10 @@ class Process(BaseClass):
|
|||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
id: int | None = None,
|
id: int | None = None,
|
||||||
submission_type: str | SubmissionType | None = None,
|
submission_type: str | SubmissionType | None = None,
|
||||||
extraction_kit : str | KitType | None = None,
|
extraction_kit: str | KitType | None = None,
|
||||||
equipment_role: str | KitType | None = None,
|
equipment_role: str | KitType | None = None,
|
||||||
limit: int = 0) -> Process | List[Process]:
|
limit: int = 0,
|
||||||
|
**kwargs) -> Process | List[Process]:
|
||||||
"""
|
"""
|
||||||
Lookup Processes
|
Lookup Processes
|
||||||
|
|
||||||
@@ -1876,6 +1933,8 @@ class Process(BaseClass):
|
|||||||
def save(self):
|
def save(self):
|
||||||
super().save()
|
super().save()
|
||||||
|
|
||||||
|
# @classmethod
|
||||||
|
|
||||||
|
|
||||||
class TipRole(BaseClass):
|
class TipRole(BaseClass):
|
||||||
"""
|
"""
|
||||||
@@ -2019,7 +2078,6 @@ class SubmissionTipsAssociation(BaseClass):
|
|||||||
instance = SubmissionTipsAssociation(submission=submission, tips=tips, role_name=role)
|
instance = SubmissionTipsAssociation(submission=submission, tips=tips, role_name=role)
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
def to_pydantic(self):
|
def to_pydantic(self):
|
||||||
from backend.validators import PydTips
|
from backend.validators import PydTips
|
||||||
return PydTips(name=self.tips.name, lot=self.tips.lot, role=self.role_name)
|
return PydTips(name=self.tips.name, lot=self.tips.lot, role=self.role_name)
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ class Organization(BaseClass):
|
|||||||
Base of organization
|
Base of organization
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
id = Column(INTEGER, primary_key=True) #: primary key
|
id = Column(INTEGER, primary_key=True) #: primary key
|
||||||
name = Column(String(64)) #: organization name
|
name = Column(String(64)) #: organization name
|
||||||
submissions = relationship("BasicSubmission",
|
submissions = relationship("BasicSubmission",
|
||||||
@@ -124,8 +126,6 @@ class Contact(BaseClass):
|
|||||||
Base of Contact
|
Base of Contact
|
||||||
"""
|
"""
|
||||||
|
|
||||||
searchables = []
|
|
||||||
|
|
||||||
id = Column(INTEGER, primary_key=True) #: primary key
|
id = Column(INTEGER, primary_key=True) #: primary key
|
||||||
name = Column(String(64)) #: contact name
|
name = Column(String(64)) #: contact name
|
||||||
email = Column(String(64)) #: contact email
|
email = Column(String(64)) #: contact email
|
||||||
@@ -137,6 +137,10 @@ class Contact(BaseClass):
|
|||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<Contact({self.name})>"
|
return f"<Contact({self.name})>"
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def searchables(cls):
|
||||||
|
return []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@setup_lookup
|
@setup_lookup
|
||||||
def query(cls,
|
def query(cls,
|
||||||
|
|||||||
@@ -1960,6 +1960,7 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
input_dict['rsl_number'] = cls.en_adapter(input_str=input_dict['submitter_id'])
|
input_dict['rsl_number'] = cls.en_adapter(input_str=input_dict['submitter_id'])
|
||||||
# NOTE: Check for extraction negative control (Robotics)
|
# NOTE: Check for extraction negative control (Robotics)
|
||||||
if re.search(rf"^{year}-(RSL)", input_dict['submitter_id']):
|
if re.search(rf"^{year}-(RSL)", input_dict['submitter_id']):
|
||||||
|
logger.debug(f"Found {year}-(RSL), so we are going to run PBS adapter:")
|
||||||
input_dict['rsl_number'] = cls.pbs_adapter(input_str=input_dict['submitter_id'])
|
input_dict['rsl_number'] = cls.pbs_adapter(input_str=input_dict['submitter_id'])
|
||||||
return input_dict
|
return input_dict
|
||||||
|
|
||||||
@@ -2019,7 +2020,9 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
"""
|
"""
|
||||||
# NOTE: Remove letters.
|
# NOTE: Remove letters.
|
||||||
processed = input_str.replace("RSL", "")
|
processed = input_str.replace("RSL", "")
|
||||||
|
# NOTE: Remove brackets at end
|
||||||
processed = re.sub(r"\(.*\)$", "", processed).strip()
|
processed = re.sub(r"\(.*\)$", "", processed).strip()
|
||||||
|
# NOTE: Remove any non-R letters at end.
|
||||||
processed = re.sub(r"[A-QS-Z]+\d*", "", processed)
|
processed = re.sub(r"[A-QS-Z]+\d*", "", processed)
|
||||||
# NOTE: Remove trailing '-' if any
|
# NOTE: Remove trailing '-' if any
|
||||||
processed = processed.strip("-")
|
processed = processed.strip("-")
|
||||||
@@ -2037,6 +2040,8 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
if repeat_num is None and "R" in plate_num:
|
if repeat_num is None and "R" in plate_num:
|
||||||
repeat_num = "1"
|
repeat_num = "1"
|
||||||
plate_num = re.sub(r"R", rf"R{repeat_num}", plate_num)
|
plate_num = re.sub(r"R", rf"R{repeat_num}", plate_num)
|
||||||
|
# NOTE: Remove any redundant -digits
|
||||||
|
processed = re.sub(r"-\d$", "", processed)
|
||||||
day = re.search(r"\d{2}$", processed).group()
|
day = re.search(r"\d{2}$", processed).group()
|
||||||
processed = rreplace(processed, day, "")
|
processed = rreplace(processed, day, "")
|
||||||
month = re.search(r"\d{2}$", processed).group()
|
month = re.search(r"\d{2}$", processed).group()
|
||||||
@@ -2237,8 +2242,6 @@ class BasicSample(BaseClass, LogMixin):
|
|||||||
Base of basic sample which polymorphs into BCSample and WWSample
|
Base of basic sample which polymorphs into BCSample and WWSample
|
||||||
"""
|
"""
|
||||||
|
|
||||||
searchables = [dict(label="Submitter ID", field="submitter_id")]
|
|
||||||
|
|
||||||
id = Column(INTEGER, primary_key=True) #: primary key
|
id = Column(INTEGER, primary_key=True) #: primary key
|
||||||
submitter_id = Column(String(64), nullable=False, unique=True) #: identification from submitter
|
submitter_id = Column(String(64), nullable=False, unique=True) #: identification from submitter
|
||||||
sample_type = Column(String(32)) #: mode_sub_type of sample
|
sample_type = Column(String(32)) #: mode_sub_type of sample
|
||||||
@@ -2287,6 +2290,10 @@ class BasicSample(BaseClass, LogMixin):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
return f"<Sample({self.submitter_id})"
|
return f"<Sample({self.submitter_id})"
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def searchables(cls):
|
||||||
|
return [dict(label="Submitter ID", field="submitter_id")]
|
||||||
|
|
||||||
@classproperty
|
@classproperty
|
||||||
def timestamps(cls) -> List[str]:
|
def timestamps(cls) -> List[str]:
|
||||||
"""
|
"""
|
||||||
@@ -2657,7 +2664,7 @@ class WastewaterSample(BasicSample):
|
|||||||
Returns:
|
Returns:
|
||||||
List[str]: List of fields.
|
List[str]: List of fields.
|
||||||
"""
|
"""
|
||||||
searchables = super().searchables
|
searchables = deepcopy(super().searchables)
|
||||||
for item in ["ww_processing_num", "ww_full_sample_id", "rsl_number"]:
|
for item in ["ww_processing_num", "ww_full_sample_id", "rsl_number"]:
|
||||||
label = item.strip("ww_").replace("_", " ").replace("rsl", "RSL").title()
|
label = item.strip("ww_").replace("_", " ").replace("rsl", "RSL").title()
|
||||||
searchables.append(dict(label=label, field=item))
|
searchables.append(dict(label=label, field=item))
|
||||||
|
|||||||
@@ -1205,3 +1205,33 @@ class PydIridaControl(BaseModel, extra='ignore'):
|
|||||||
if instance.__getattribute__(key) != field_value:
|
if instance.__getattribute__(key) != field_value:
|
||||||
instance.__setattr__(key, field_value)
|
instance.__setattr__(key, field_value)
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class PydProcess(BaseModel, extra="allow"):
|
||||||
|
name: str
|
||||||
|
submission_types: List[str]
|
||||||
|
equipment: List[str]
|
||||||
|
equipment_roles: List[str]
|
||||||
|
kit_types: List[str]
|
||||||
|
tip_roles: List[str]
|
||||||
|
|
||||||
|
@field_validator("submission_types", "equipment", "equipment_roles", "kit_types", "tip_roles", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def enforce_list(cls, value):
|
||||||
|
if not isinstance(value, list):
|
||||||
|
return [value]
|
||||||
|
return value
|
||||||
|
|
||||||
|
def to_sql(self):
|
||||||
|
instance = Process.query(name=self.name)
|
||||||
|
if not instance:
|
||||||
|
instance = Process()
|
||||||
|
dicto = instance.omnigui_dict
|
||||||
|
for key in self.model_fields:
|
||||||
|
# field_value = self.__getattribute__(key)
|
||||||
|
# if instance.__getattribute__(key) != field_value:
|
||||||
|
# instance.__setattr__(key, field_value)
|
||||||
|
test = dicto[key]
|
||||||
|
print(f"Attribute: {test['class_attr'].property}")
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|||||||
@@ -6,31 +6,40 @@ from pprint import pformat
|
|||||||
from typing import Any, Tuple
|
from typing import Any, Tuple
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QLabel, QDialog, QWidget, QLineEdit, QGridLayout, QComboBox, QDialogButtonBox, QDateEdit, QSpinBox, QDoubleSpinBox
|
QLabel, QDialog, QWidget, QLineEdit, QGridLayout, QComboBox, QDialogButtonBox, QDateEdit, QSpinBox, QDoubleSpinBox,
|
||||||
|
QCheckBox
|
||||||
)
|
)
|
||||||
from sqlalchemy import String, TIMESTAMP, INTEGER, FLOAT
|
from sqlalchemy import String, TIMESTAMP, INTEGER, FLOAT, JSON, BLOB
|
||||||
from sqlalchemy.orm import InstrumentedAttribute, ColumnProperty
|
from sqlalchemy.orm import InstrumentedAttribute, ColumnProperty
|
||||||
import logging
|
import logging
|
||||||
from sqlalchemy.orm.relationships import _RelationshipDeclared
|
from sqlalchemy.orm.relationships import _RelationshipDeclared
|
||||||
from tools import Report, report_result
|
from tools import Report, report_result
|
||||||
|
from backend import db
|
||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|
||||||
|
|
||||||
class AddEdit(QDialog):
|
class AddEdit(QDialog):
|
||||||
|
|
||||||
def __init__(self, parent, instance: Any | None = None, manager: str = ""):
|
def __init__(self, parent, instance: Any | None = None, managers: set = set()):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
logger.debug(f"Managers: {managers}")
|
||||||
self.instance = instance
|
self.instance = instance
|
||||||
self.object_type = instance.__class__
|
self.object_type = instance.__class__
|
||||||
|
self.managers = managers
|
||||||
|
if instance.level < 2:
|
||||||
|
try:
|
||||||
|
self.managers.add(self.parent().instance)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
logger.debug(f"Managers: {managers}")
|
||||||
self.layout = QGridLayout(self)
|
self.layout = QGridLayout(self)
|
||||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||||
self.buttonBox = QDialogButtonBox(QBtn)
|
self.buttonBox = QDialogButtonBox(QBtn)
|
||||||
self.buttonBox.accepted.connect(self.accept)
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
self.buttonBox.rejected.connect(self.reject)
|
self.buttonBox.rejected.connect(self.reject)
|
||||||
fields = {key: dict(class_attr=getattr(self.object_type, key), instance_attr=getattr(self.instance, key))
|
logger.debug(f"Fields: {pformat(self.instance.omnigui_dict)}")
|
||||||
for key in dir(self.object_type) if isinstance(getattr(self.object_type, key), InstrumentedAttribute)
|
fields = {k: v for k, v in self.instance.omnigui_dict.items() if "id" not in k}
|
||||||
and "id" not in key and key != manager}
|
|
||||||
# NOTE: Move 'name' to the front
|
# NOTE: Move 'name' to the front
|
||||||
try:
|
try:
|
||||||
fields = {'name': fields.pop('name'), **fields}
|
fields = {'name': fields.pop('name'), **fields}
|
||||||
@@ -52,15 +61,17 @@ class AddEdit(QDialog):
|
|||||||
self.layout.addWidget(widget, self.layout.rowCount(), 0)
|
self.layout.addWidget(widget, self.layout.rowCount(), 0)
|
||||||
height_counter += 1
|
height_counter += 1
|
||||||
self.layout.addWidget(self.buttonBox)
|
self.layout.addWidget(self.buttonBox)
|
||||||
self.setWindowTitle(f"Add/Edit {self.object_type.__name__}")
|
self.setWindowTitle(f"Add/Edit {self.object_type.__name__} - Manager: {self.managers}")
|
||||||
self.setMinimumSize(600, 50 * height_counter)
|
self.setMinimumSize(600, 50 * height_counter)
|
||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
@report_result
|
@report_result
|
||||||
def parse_form(self) -> Tuple[BaseModel, Report]:
|
def parse_form(self) -> Tuple[BaseModel, Report]:
|
||||||
|
from backend.validators import pydant
|
||||||
report = Report()
|
report = Report()
|
||||||
parsed = {result[0].strip(":"): result[1] for result in [item.parse_form() for item in self.findChildren(EditProperty)] if result[0]}
|
parsed = {result[0].strip(":"): result[1] for result in
|
||||||
# logger.debug(parsed)
|
[item.parse_form() for item in self.findChildren(EditProperty)] if result[0]}
|
||||||
|
logger.debug(f"Parsed form: {parsed}")
|
||||||
model = self.object_type.pydantic_model
|
model = self.object_type.pydantic_model
|
||||||
# NOTE: Hand-off to pydantic model for validation.
|
# NOTE: Hand-off to pydantic model for validation.
|
||||||
# NOTE: Also, why am I not just using the toSQL method here. I could write one for contacts.
|
# NOTE: Also, why am I not just using the toSQL method here. I could write one for contacts.
|
||||||
@@ -75,23 +86,42 @@ class EditProperty(QWidget):
|
|||||||
self.name = key
|
self.name = key
|
||||||
self.label = QLabel(key.title().replace("_", " "))
|
self.label = QLabel(key.title().replace("_", " "))
|
||||||
self.layout = QGridLayout()
|
self.layout = QGridLayout()
|
||||||
self.layout.addWidget(self.label, 0, 0, 1, 1)
|
|
||||||
self.setObjectName(key)
|
self.setObjectName(key)
|
||||||
|
try:
|
||||||
|
self.property_class = column_type['class_attr'].property.entity.class_
|
||||||
|
except AttributeError:
|
||||||
|
self.property_class = None
|
||||||
|
try:
|
||||||
|
self.is_list = column_type['class_attr'].property.uselist
|
||||||
|
except AttributeError:
|
||||||
|
self.is_list = False
|
||||||
match column_type['class_attr'].property:
|
match column_type['class_attr'].property:
|
||||||
case ColumnProperty():
|
case ColumnProperty():
|
||||||
self.column_property_set(column_type, value=value)
|
self.column_property_set(column_type, value=value)
|
||||||
case _RelationshipDeclared():
|
case _RelationshipDeclared():
|
||||||
self.relationship_property_set(column_type, value=value)
|
if not self.property_class.skip_on_edit:
|
||||||
|
self.relationship_property_set(column_type, value=value)
|
||||||
|
else:
|
||||||
|
return
|
||||||
case _:
|
case _:
|
||||||
logger.error(f"{column_type} not a supported type.")
|
logger.error(f"{column_type} not a supported type.")
|
||||||
return
|
return
|
||||||
|
# if not self.is_list:
|
||||||
|
self.layout.addWidget(self.label, 0, 0, 1, 1)
|
||||||
self.layout.addWidget(self.widget, 0, 1, 1, 3)
|
self.layout.addWidget(self.widget, 0, 1, 1, 3)
|
||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
def relationship_property_set(self, relationship_property, value=None):
|
def relationship_property_set(self, relationship, value=None):
|
||||||
self.property_class = relationship_property['class_attr'].property.entity.class_
|
self.widget = QComboBox()
|
||||||
self.is_list = relationship_property['class_attr'].property.uselist
|
logger.debug(self.parent().managers)
|
||||||
choices = [""] + [item.name for item in self.property_class.query()]
|
for manager in self.parent().managers:
|
||||||
|
if self.name in manager.aliases:
|
||||||
|
logger.debug(f"Name: {self.name} is in aliases: {manager.aliases}")
|
||||||
|
choices = [manager.name]
|
||||||
|
self.widget.setEnabled(False)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
choices = [""] + [item.name for item in self.property_class.query()]
|
||||||
try:
|
try:
|
||||||
instance_value = getattr(self.parent().instance, self.objectName())
|
instance_value = getattr(self.parent().instance, self.objectName())
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -102,21 +132,25 @@ class EditProperty(QWidget):
|
|||||||
instance_value = next((item.name for item in instance_value), None)
|
instance_value = next((item.name for item in instance_value), None)
|
||||||
if instance_value:
|
if instance_value:
|
||||||
choices.insert(0, choices.pop(choices.index(instance_value)))
|
choices.insert(0, choices.pop(choices.index(instance_value)))
|
||||||
self.widget = QComboBox()
|
|
||||||
self.widget.addItems(choices)
|
self.widget.addItems(choices)
|
||||||
|
|
||||||
def column_property_set(self, column_property, value=None):
|
def column_property_set(self, column_property, value=None):
|
||||||
|
logger.debug(f"Column Property: {column_property['class_attr'].expression} {column_property}, Value: {value}")
|
||||||
match column_property['class_attr'].expression.type:
|
match column_property['class_attr'].expression.type:
|
||||||
case String():
|
case String():
|
||||||
if not value:
|
if value is None:
|
||||||
value = ""
|
value = ""
|
||||||
self.widget = QLineEdit(self)
|
self.widget = QLineEdit(self)
|
||||||
self.widget.setText(value)
|
self.widget.setText(value)
|
||||||
case INTEGER():
|
case INTEGER():
|
||||||
if not value:
|
if isinstance(column_property['instance_attr'], bool):
|
||||||
value = 1
|
self.widget = QCheckBox()
|
||||||
self.widget = QSpinBox()
|
self.widget.setChecked(value)
|
||||||
self.widget.setValue(value)
|
else:
|
||||||
|
if value is None:
|
||||||
|
value = 1
|
||||||
|
self.widget = QSpinBox()
|
||||||
|
self.widget.setValue(value)
|
||||||
case FLOAT():
|
case FLOAT():
|
||||||
if not value:
|
if not value:
|
||||||
value = 1.0
|
value = 1.0
|
||||||
@@ -127,6 +161,10 @@ class EditProperty(QWidget):
|
|||||||
if not value:
|
if not value:
|
||||||
value = date.today()
|
value = date.today()
|
||||||
self.widget.setDate(value)
|
self.widget.setDate(value)
|
||||||
|
case JSON():
|
||||||
|
self.widget = QLabel("JSON Under construction")
|
||||||
|
case BLOB():
|
||||||
|
self.widget = QLabel("BLOB Under construction")
|
||||||
case _:
|
case _:
|
||||||
logger.error(f"{column_property} not a supported property.")
|
logger.error(f"{column_property} not a supported property.")
|
||||||
self.widget = None
|
self.widget = None
|
||||||
@@ -151,10 +189,10 @@ class EditProperty(QWidget):
|
|||||||
value = self.widget.currentText()
|
value = self.widget.currentText()
|
||||||
case QSpinBox() | QDoubleSpinBox():
|
case QSpinBox() | QDoubleSpinBox():
|
||||||
value = self.widget.value()
|
value = self.widget.value()
|
||||||
# if self.is_list:
|
case QCheckBox():
|
||||||
# value = [self.property_class.query(name=prelim)]
|
value = self.widget.isChecked()
|
||||||
# else:
|
|
||||||
# value = self.property_class.query(name=prelim)
|
|
||||||
case _:
|
case _:
|
||||||
value = None
|
value = None
|
||||||
return self.objectName(), value
|
return self.objectName(), value
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ from PyQt6.QtCore import QSortFilterProxyModel, Qt
|
|||||||
from PyQt6.QtGui import QAction, QCursor
|
from PyQt6.QtGui import QAction, QCursor
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QLabel, QDialog,
|
QLabel, QDialog,
|
||||||
QTableView, QWidget, QLineEdit, QGridLayout, QComboBox, QPushButton, QDialogButtonBox, QDateEdit, QMenu
|
QTableView, QWidget, QLineEdit, QGridLayout, QComboBox, QPushButton, QDialogButtonBox, QDateEdit, QMenu,
|
||||||
|
QDoubleSpinBox, QSpinBox, QCheckBox
|
||||||
)
|
)
|
||||||
from sqlalchemy import String, TIMESTAMP
|
from sqlalchemy import String, TIMESTAMP, FLOAT, INTEGER, JSON, BLOB
|
||||||
from sqlalchemy.orm import InstrumentedAttribute
|
from sqlalchemy.orm import InstrumentedAttribute
|
||||||
from sqlalchemy.orm.collections import InstrumentedList
|
from sqlalchemy.orm.collections import InstrumentedList
|
||||||
from sqlalchemy.orm.properties import ColumnProperty
|
from sqlalchemy.orm.properties import ColumnProperty
|
||||||
@@ -28,10 +29,16 @@ class ManagerWindow(QDialog):
|
|||||||
Initially this is a window to manage Organization Contacts, but hope to abstract it more later.
|
Initially this is a window to manage Organization Contacts, but hope to abstract it more later.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, parent, object_type: Any, extras: List[str], **kwargs):
|
def __init__(self, parent, object_type: Any, extras: List[str], managers: set = set(), **kwargs):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.object_type = self.original_type = object_type
|
self.object_type = self.original_type = object_type
|
||||||
self.instance = None
|
self.instance = None
|
||||||
|
self.managers = managers
|
||||||
|
try:
|
||||||
|
self.managers.add(self.parent().instance)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
logger.debug(f"Managers: {managers}")
|
||||||
self.extras = extras
|
self.extras = extras
|
||||||
self.context = kwargs
|
self.context = kwargs
|
||||||
self.layout = QGridLayout(self)
|
self.layout = QGridLayout(self)
|
||||||
@@ -55,7 +62,7 @@ class ManagerWindow(QDialog):
|
|||||||
self.options.setObjectName("options")
|
self.options.setObjectName("options")
|
||||||
self.update_options()
|
self.update_options()
|
||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
self.setWindowTitle(f"Manage {self.object_type.__name__}")
|
self.setWindowTitle(f"Manage {self.object_type.__name__} - Managers: {self.managers}")
|
||||||
|
|
||||||
def update_options(self) -> None:
|
def update_options(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -63,8 +70,15 @@ class ManagerWindow(QDialog):
|
|||||||
"""
|
"""
|
||||||
if self.sub_class:
|
if self.sub_class:
|
||||||
self.object_type = getattr(db, self.sub_class.currentText())
|
self.object_type = getattr(db, self.sub_class.currentText())
|
||||||
options = [item.name for item in self.object_type.query()]
|
logger.debug(f"From update options, managers: {self.managers}")
|
||||||
logger.debug(f"self.instance: {self.instance}")
|
try:
|
||||||
|
query_kwargs = {self.parent().instance.query_alias: self.parent().instance}
|
||||||
|
except AttributeError as e:
|
||||||
|
logger.debug(f"Couldn't set query kwargs due to: {e}")
|
||||||
|
query_kwargs = {}
|
||||||
|
logger.debug(f"Query kwargs: {query_kwargs}")
|
||||||
|
options = [item.name for item in self.object_type.query(**query_kwargs)]
|
||||||
|
logger.debug(f"self.object_type: {self.object_type}")
|
||||||
if self.instance:
|
if self.instance:
|
||||||
options.insert(0, options.pop(options.index(self.instance.name)))
|
options.insert(0, options.pop(options.index(self.instance.name)))
|
||||||
self.options.clear()
|
self.options.clear()
|
||||||
@@ -92,21 +106,24 @@ class ManagerWindow(QDialog):
|
|||||||
for item in deletes:
|
for item in deletes:
|
||||||
item.setParent(None)
|
item.setParent(None)
|
||||||
# NOTE: Find the instance this manager will update
|
# NOTE: Find the instance this manager will update
|
||||||
self.instance = self.object_type.query(name=self.options.currentText())
|
logger.debug(f"Querying with {self.options.currentText()}")
|
||||||
fields = {k: v for k, v in self.object_type.__dict__.items() if
|
self.instance = self.object_type.query(name=self.options.currentText(), limit=1)
|
||||||
isinstance(v, InstrumentedAttribute) and k != "id"}
|
logger.debug(f"Instance: {self.instance}")
|
||||||
|
fields = {k: v for k, v in self.instance.omnigui_dict.items() if
|
||||||
|
isinstance(v['class_attr'], InstrumentedAttribute) and k != "id"}
|
||||||
|
# logger.debug(f"Instance fields: {fields}")
|
||||||
for key, field in fields.items():
|
for key, field in fields.items():
|
||||||
match field.property:
|
match field['class_attr'].property:
|
||||||
# NOTE: ColumnProperties will be directly edited.
|
# NOTE: ColumnProperties will be directly edited.
|
||||||
case ColumnProperty():
|
case ColumnProperty():
|
||||||
# NOTE: field.property.expression.type gives db column type eg. STRING or TIMESTAMP
|
# NOTE: field.property.expression.type gives db column type eg. STRING or TIMESTAMP
|
||||||
widget = EditProperty(self, key=key, column_type=field.property.expression.type,
|
widget = EditProperty(self, key=key, column_type=field,
|
||||||
value=getattr(self.instance, key))
|
value=getattr(self.instance, key))
|
||||||
# NOTE: RelationshipDeclareds will be given a list of existing related objects.
|
# NOTE: RelationshipDeclareds will be given a list of existing related objects.
|
||||||
case _RelationshipDeclared():
|
case _RelationshipDeclared():
|
||||||
if key != "submissions":
|
if key != "submissions":
|
||||||
# NOTE: field.comparator.entity.class_ gives the relationship class
|
# NOTE: field.comparator.entity.class_ gives the relationship class
|
||||||
widget = EditRelationship(self, key=key, entity=field.comparator.entity.class_,
|
widget = EditRelationship(self, key=key, entity=field['class_attr'].comparator.entity.class_,
|
||||||
value=getattr(self.instance, key))
|
value=getattr(self.instance, key))
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
@@ -132,7 +149,7 @@ class ManagerWindow(QDialog):
|
|||||||
return self.instance
|
return self.instance
|
||||||
|
|
||||||
def add_new(self):
|
def add_new(self):
|
||||||
dlg = AddEdit(parent=self, instance=self.object_type(), manager=self.object_type.__name__.lower())
|
dlg = AddEdit(parent=self, instance=self.object_type(), managers=self.managers)
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
new_pyd = dlg.parse_form()
|
new_pyd = dlg.parse_form()
|
||||||
new_instance = new_pyd.to_sql()
|
new_instance = new_pyd.to_sql()
|
||||||
@@ -148,13 +165,33 @@ class EditProperty(QWidget):
|
|||||||
self.label = QLabel(key.title().replace("_", " "))
|
self.label = QLabel(key.title().replace("_", " "))
|
||||||
self.layout = QGridLayout()
|
self.layout = QGridLayout()
|
||||||
self.layout.addWidget(self.label, 0, 0, 1, 1)
|
self.layout.addWidget(self.label, 0, 0, 1, 1)
|
||||||
match column_type:
|
logger.debug(f"Column type: {column_type}")
|
||||||
|
match column_type['class_attr'].property.expression.type:
|
||||||
case String():
|
case String():
|
||||||
self.widget = QLineEdit(self)
|
self.widget = QLineEdit(self)
|
||||||
self.widget.setText(value)
|
self.widget.setText(value)
|
||||||
|
case INTEGER():
|
||||||
|
if isinstance(column_type['instance_attr'], bool):
|
||||||
|
self.widget = QCheckBox()
|
||||||
|
self.widget.setChecked(value)
|
||||||
|
else:
|
||||||
|
if value is None:
|
||||||
|
value = 1
|
||||||
|
self.widget = QSpinBox()
|
||||||
|
self.widget.setValue(value)
|
||||||
|
case FLOAT():
|
||||||
|
if not value:
|
||||||
|
value = 1.0
|
||||||
|
self.widget = QDoubleSpinBox()
|
||||||
|
self.widget.setMaximum(999.99)
|
||||||
|
self.widget.setValue(value)
|
||||||
case TIMESTAMP():
|
case TIMESTAMP():
|
||||||
self.widget = QDateEdit(self)
|
self.widget = QDateEdit(self)
|
||||||
self.widget.setDate(value)
|
self.widget.setDate(value)
|
||||||
|
case JSON():
|
||||||
|
self.widget = QLabel("JSON Under construction")
|
||||||
|
case BLOB():
|
||||||
|
self.widget = QLabel("BLOB Under construction")
|
||||||
case _:
|
case _:
|
||||||
self.widget = None
|
self.widget = None
|
||||||
self.layout.addWidget(self.widget, 0, 1, 1, 3)
|
self.layout.addWidget(self.widget, 0, 1, 1, 3)
|
||||||
@@ -175,7 +212,7 @@ class EditRelationship(QWidget):
|
|||||||
|
|
||||||
def __init__(self, parent, key: str, entity: Any, value):
|
def __init__(self, parent, key: str, entity: Any, value):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.entity = entity
|
self.entity = entity #: The class of interest
|
||||||
self.data = value
|
self.data = value
|
||||||
self.label = QLabel(key.title().replace("_", " "))
|
self.label = QLabel(key.title().replace("_", " "))
|
||||||
self.setObjectName(key)
|
self.setObjectName(key)
|
||||||
@@ -184,10 +221,11 @@ class EditRelationship(QWidget):
|
|||||||
self.add_button.clicked.connect(self.add_new)
|
self.add_button.clicked.connect(self.add_new)
|
||||||
self.existing_button = QPushButton("Add Existing")
|
self.existing_button = QPushButton("Add Existing")
|
||||||
self.existing_button.clicked.connect(self.add_existing)
|
self.existing_button.clicked.connect(self.add_existing)
|
||||||
|
self.existing_button.setEnabled(self.entity.level == 1)
|
||||||
self.layout = QGridLayout()
|
self.layout = QGridLayout()
|
||||||
self.layout.addWidget(self.label, 0, 0, 1, 5)
|
self.layout.addWidget(self.label, 0, 0, 1, 5)
|
||||||
self.layout.addWidget(self.table, 1, 0, 1, 8)
|
self.layout.addWidget(self.table, 1, 0, 1, 8)
|
||||||
self.layout.addWidget(self.add_button, 0, 6, 1, 1, alignment=Qt.AlignmentFlag.AlignRight)
|
self.layout.addWidget(self.add_button, 0, 6, 1, 1, alignment=Qt.AlignmentFlag.AlignRight)
|
||||||
self.layout.addWidget(self.existing_button, 0, 7, 1, 1, alignment=Qt.AlignmentFlag.AlignRight)
|
self.layout.addWidget(self.existing_button, 0, 7, 1, 1, alignment=Qt.AlignmentFlag.AlignRight)
|
||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
self.set_data()
|
self.set_data()
|
||||||
@@ -205,17 +243,30 @@ class EditRelationship(QWidget):
|
|||||||
self.add_edit(instance=object)
|
self.add_edit(instance=object)
|
||||||
|
|
||||||
def add_new(self, instance: Any = None):
|
def add_new(self, instance: Any = None):
|
||||||
|
# NOTE: if an existing instance is not being edited, create a new instance
|
||||||
if not instance:
|
if not instance:
|
||||||
instance = self.entity()
|
instance = self.entity()
|
||||||
dlg = AddEdit(self, instance=instance, manager=self.parent().object_type.__name__.lower())
|
# if self.parent().object_type.level == 2:
|
||||||
|
managers = self.parent().managers
|
||||||
|
# else:
|
||||||
|
# managers = self.parent().managers + [self.parent().instance]
|
||||||
|
match instance.level:
|
||||||
|
case 1:
|
||||||
|
dlg = AddEdit(self.parent(), instance=instance, managers=managers)
|
||||||
|
case 2:
|
||||||
|
dlg = ManagerWindow(self.parent(), object_type=instance.__class__, extras=[], managers=managers)
|
||||||
|
case _:
|
||||||
|
return
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
new_instance = dlg.parse_form()
|
new_instance = dlg.parse_form()
|
||||||
new_instance, result = new_instance.to_sql()
|
new_instance = new_instance.to_sql()
|
||||||
logger.debug(f"New instance: {new_instance}")
|
logger.debug(f"New instance: {new_instance}")
|
||||||
addition = getattr(self.parent().instance, self.objectName())
|
addition = getattr(self.parent().instance, self.objectName())
|
||||||
if isinstance(addition, InstrumentedList):
|
logger.debug(f"Addition: {addition}")
|
||||||
addition.append(new_instance)
|
# NOTE: Saving currently disabled
|
||||||
self.parent().instance.save()
|
# if isinstance(addition, InstrumentedList):
|
||||||
|
# addition.append(new_instance)
|
||||||
|
# self.parent().instance.save()
|
||||||
self.parent().update_data()
|
self.parent().update_data()
|
||||||
|
|
||||||
def add_existing(self):
|
def add_existing(self):
|
||||||
@@ -223,11 +274,15 @@ class EditRelationship(QWidget):
|
|||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
rows = dlg.return_selected_rows()
|
rows = dlg.return_selected_rows()
|
||||||
for row in rows:
|
for row in rows:
|
||||||
|
logger.debug(f"Querying with {row}")
|
||||||
instance = self.entity.query(**row)
|
instance = self.entity.query(**row)
|
||||||
|
logger.debug(f"Queried instance: {instance}")
|
||||||
addition = getattr(self.parent().instance, self.objectName())
|
addition = getattr(self.parent().instance, self.objectName())
|
||||||
if isinstance(addition, InstrumentedList):
|
logger.debug(f"Addition: {addition}")
|
||||||
addition.append(instance)
|
# NOTE: Saving currently disabled
|
||||||
self.parent().instance.save()
|
# if isinstance(addition, InstrumentedList):
|
||||||
|
# addition.append(instance)
|
||||||
|
# self.parent().instance.save()
|
||||||
self.parent().update_data()
|
self.parent().update_data()
|
||||||
|
|
||||||
def set_data(self) -> None:
|
def set_data(self) -> None:
|
||||||
@@ -235,7 +290,11 @@ class EditRelationship(QWidget):
|
|||||||
sets data in model
|
sets data in model
|
||||||
"""
|
"""
|
||||||
# logger.debug(self.data)
|
# logger.debug(self.data)
|
||||||
self.data = DataFrame.from_records([item.omnigui_dict for item in self.data])
|
if not isinstance(self.data, list):
|
||||||
|
self.data = [self.data]
|
||||||
|
records = [{k: v['instance_attr'] for k, v in item.omnigui_dict.items()} for item in self.data]
|
||||||
|
# logger.debug(f"Records: {records}")
|
||||||
|
self.data = DataFrame.from_records(records)
|
||||||
try:
|
try:
|
||||||
self.columns_of_interest = [dict(name=item, column=self.data.columns.get_loc(item)) for item in self.extras]
|
self.columns_of_interest = [dict(name=item, column=self.data.columns.get_loc(item)) for item in self.extras]
|
||||||
except (KeyError, AttributeError):
|
except (KeyError, AttributeError):
|
||||||
@@ -261,8 +320,13 @@ class EditRelationship(QWidget):
|
|||||||
event (_type_): the item of interest
|
event (_type_): the item of interest
|
||||||
"""
|
"""
|
||||||
id = self.table.selectionModel().currentIndex()
|
id = self.table.selectionModel().currentIndex()
|
||||||
id = int(id.sibling(id.row(), 0).data())
|
# NOTE: the overly complicated {column_name: row_value} dictionary construction
|
||||||
object = self.entity.query(id=id)
|
row_data = {self.data.columns[column]: self.table.model().index(id.row(), column).data() for column in
|
||||||
|
range(self.table.model().columnCount())}
|
||||||
|
object = self.entity.query(**row_data)
|
||||||
|
if isinstance(object, list):
|
||||||
|
object = object[0]
|
||||||
|
logger.debug(object)
|
||||||
self.menu = QMenu(self)
|
self.menu = QMenu(self)
|
||||||
action = QAction(f"Remove {object.name}", self)
|
action = QAction(f"Remove {object.name}", self)
|
||||||
action.triggered.connect(lambda: self.remove_item(object=object))
|
action.triggered.connect(lambda: self.remove_item(object=object))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Search box that performs fuzzy search for various object types
|
Search box that performs fuzzy search for various object types
|
||||||
"""
|
"""
|
||||||
|
from copy import deepcopy
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from typing import Tuple, Any, List, Generator
|
from typing import Tuple, Any, List, Generator
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
@@ -22,7 +23,8 @@ class SearchBox(QDialog):
|
|||||||
|
|
||||||
def __init__(self, parent, object_type: Any, extras: List[dict], returnable: bool = False, **kwargs):
|
def __init__(self, parent, object_type: Any, extras: List[dict], returnable: bool = False, **kwargs):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.object_type = self.original_type = object_type
|
self.object_type = object_type
|
||||||
|
self.original_type = object_type
|
||||||
self.extras = extras
|
self.extras = extras
|
||||||
self.context = kwargs
|
self.context = kwargs
|
||||||
self.layout = QGridLayout(self)
|
self.layout = QGridLayout(self)
|
||||||
@@ -43,7 +45,7 @@ class SearchBox(QDialog):
|
|||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
self.setWindowTitle(f"Search {self.object_type.__name__}")
|
self.setWindowTitle(f"Search {self.object_type.__name__}")
|
||||||
self.update_widgets()
|
self.update_widgets()
|
||||||
self.update_data()
|
# self.update_data()
|
||||||
if returnable:
|
if returnable:
|
||||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||||
self.buttonBox = QDialogButtonBox(QBtn)
|
self.buttonBox = QDialogButtonBox(QBtn)
|
||||||
@@ -57,21 +59,30 @@ class SearchBox(QDialog):
|
|||||||
"""
|
"""
|
||||||
Changes form inputs based on sample type
|
Changes form inputs based on sample type
|
||||||
"""
|
"""
|
||||||
|
search_fields = []
|
||||||
|
logger.debug(f"Search fields: {search_fields}")
|
||||||
deletes = [item for item in self.findChildren(FieldSearch)]
|
deletes = [item for item in self.findChildren(FieldSearch)]
|
||||||
for item in deletes:
|
for item in deletes:
|
||||||
item.setParent(None)
|
item.setParent(None)
|
||||||
# NOTE: Handle any subclasses
|
# NOTE: Handle any subclasses
|
||||||
if not self.sub_class:
|
if not self.sub_class:
|
||||||
|
logger.warning(f"No subclass selected.")
|
||||||
self.update_data()
|
self.update_data()
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
if self.sub_class.currentText() == "Any":
|
if self.sub_class.currentText() == "Any":
|
||||||
self.object_type = self.original_type
|
self.object_type = self.original_type
|
||||||
else:
|
else:
|
||||||
self.object_type = self.original_type.find_regular_subclass(self.sub_class.currentText())
|
self.object_type = self.original_type.find_regular_subclass(self.sub_class.currentText())
|
||||||
try:
|
# logger.debug(f"Object type: {self.object_type} - {self.object_type.searchables}")
|
||||||
search_fields = self.object_type.searchables
|
# logger.debug(f"Original type: {self.original_type} - {self.original_type.searchables}")
|
||||||
except AttributeError:
|
for item in self.object_type.searchables:
|
||||||
search_fields = []
|
if item['field'] in [item['field'] for item in search_fields]:
|
||||||
|
logger.debug(f"Already have {item['field']}")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
search_fields.append(item)
|
||||||
|
logger.debug(f"Search fields: {search_fields}")
|
||||||
for iii, searchable in enumerate(search_fields):
|
for iii, searchable in enumerate(search_fields):
|
||||||
widget = FieldSearch(parent=self, label=searchable['label'], field_name=searchable['field'])
|
widget = FieldSearch(parent=self, label=searchable['label'], field_name=searchable['field'])
|
||||||
widget.setObjectName(searchable['field'])
|
widget.setObjectName(searchable['field'])
|
||||||
@@ -120,6 +131,7 @@ class FieldSearch(QWidget):
|
|||||||
|
|
||||||
def __init__(self, parent, label, field_name):
|
def __init__(self, parent, label, field_name):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self.setParent(parent)
|
||||||
self.layout = QVBoxLayout(self)
|
self.layout = QVBoxLayout(self)
|
||||||
label_widget = QLabel(label)
|
label_widget = QLabel(label)
|
||||||
self.layout.addWidget(label_widget)
|
self.layout.addWidget(label_widget)
|
||||||
@@ -158,9 +170,8 @@ class SearchResults(QTableView):
|
|||||||
self.context = kwargs
|
self.context = kwargs
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.object_type = object_type
|
self.object_type = object_type
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.extras = extras + self.object_type.searchables
|
self.extras = extras + [item for item in deepcopy(self.object_type.searchables)]
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
self.extras = extras
|
self.extras = extras
|
||||||
logger.debug(f"Extras: {self.extras}")
|
logger.debug(f"Extras: {self.extras}")
|
||||||
|
|||||||
@@ -684,7 +684,7 @@ class SubmissionFormWidget(QWidget):
|
|||||||
message=f"Couldn't find reagent type {self.reagent.role}: {lot} in the database.\n\nWould you like to add it?")
|
message=f"Couldn't find reagent type {self.reagent.role}: {lot} in the database.\n\nWould you like to add it?")
|
||||||
|
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
wanted_reagent = self.parent().parent().new_add_reagent(instance=wanted_reagent)
|
wanted_reagent = self.parent.parent().add_reagent(instance=wanted_reagent)
|
||||||
return wanted_reagent, report
|
return wanted_reagent, report
|
||||||
else:
|
else:
|
||||||
# NOTE: In this case we will have an empty reagent and the submission will fail kit integrity check
|
# NOTE: In this case we will have an empty reagent and the submission will fail kit integrity check
|
||||||
|
|||||||
@@ -931,6 +931,17 @@ def rreplace(s: str, old: str, new: str) -> str:
|
|||||||
return (s[::-1].replace(old[::-1], new[::-1], 1))[::-1]
|
return (s[::-1].replace(old[::-1], new[::-1], 1))[::-1]
|
||||||
|
|
||||||
|
|
||||||
|
def list_sort_dict(input_dict: dict, sort_list: list) -> dict:
|
||||||
|
# sort_list.reverse()
|
||||||
|
sort_list = reversed(sort_list)
|
||||||
|
for item in sort_list:
|
||||||
|
try:
|
||||||
|
input_dict = {item: input_dict.pop(item), **input_dict}
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
return input_dict
|
||||||
|
|
||||||
|
|
||||||
def remove_key_from_list_of_dicts(input_list: list, key: str) -> list:
|
def remove_key_from_list_of_dicts(input_list: list, key: str) -> list:
|
||||||
"""
|
"""
|
||||||
Removes a key from all dictionaries in a list of dictionaries
|
Removes a key from all dictionaries in a list of dictionaries
|
||||||
@@ -1067,7 +1078,6 @@ def report_result(func):
|
|||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
logger.info(f"Report result being called by {func.__name__}")
|
logger.info(f"Report result being called by {func.__name__}")
|
||||||
output = func(*args, **kwargs)
|
output = func(*args, **kwargs)
|
||||||
print(f"Function output: {output}")
|
|
||||||
match output:
|
match output:
|
||||||
case Report():
|
case Report():
|
||||||
report = output
|
report = output
|
||||||
@@ -1091,14 +1101,11 @@ def report_result(func):
|
|||||||
logger.error(f"Problem reporting due to {e}")
|
logger.error(f"Problem reporting due to {e}")
|
||||||
logger.error(result.msg)
|
logger.error(result.msg)
|
||||||
if output:
|
if output:
|
||||||
print(f"Output going into checking: {output}")
|
|
||||||
if is_list_etc(output):
|
if is_list_etc(output):
|
||||||
print(f"Output of type {type(output)} is iterable")
|
|
||||||
true_output = tuple(item for item in output if not isinstance(item, Report))
|
true_output = tuple(item for item in output if not isinstance(item, Report))
|
||||||
if len(true_output) == 1:
|
if len(true_output) == 1:
|
||||||
true_output = true_output[0]
|
true_output = true_output[0]
|
||||||
else:
|
else:
|
||||||
print(f"Output is of type {type(output)}")
|
|
||||||
if isinstance(output, Report):
|
if isinstance(output, Report):
|
||||||
true_output = None
|
true_output = None
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user