Addition of Equipment and SubmissionType creation.
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
|
## 202312.03
|
||||||
|
|
||||||
|
- Enabled creation of new submission types in gui.
|
||||||
|
- Enabled Equipment addition.
|
||||||
|
|
||||||
## 202312.02
|
## 202312.02
|
||||||
|
|
||||||
|
- Bug fixes for switching kits
|
||||||
|
|
||||||
## 202312.01
|
## 202312.01
|
||||||
|
|
||||||
- Control samples info now available in plate map.
|
- Control samples info now available in plate map.
|
||||||
|
|||||||
7
TODO.md
7
TODO.md
@@ -1,9 +1,14 @@
|
|||||||
|
- [ ] Update Artic and add in equipment listings... *sigh*.
|
||||||
|
- [x] Fix WastewaterAssociations not in Session error.
|
||||||
|
- Done... I think?
|
||||||
|
- [x] Fix submitted date always being today.
|
||||||
|
- this is an issue with the way client is filling in form with =TODAY()
|
||||||
- [x] SubmissionReagentAssociation.query
|
- [x] SubmissionReagentAssociation.query
|
||||||
- [x] Move as much from db.functions to objects as possible.
|
- [x] Move as much from db.functions to objects as possible.
|
||||||
- [x] Clean up DB objects after failed test fix.
|
- [x] Clean up DB objects after failed test fix.
|
||||||
- [x] Fix tests.
|
- [x] Fix tests.
|
||||||
- [x] Fix pydant.PydSample.handle_duplicate_samples?
|
- [x] Fix pydant.PydSample.handle_duplicate_samples?
|
||||||
- [ ] See if the number of queries in BasicSubmission functions (and others) can be trimmed down.
|
- [x] See if the number of queries in BasicSubmission functions (and others) can be trimmed down.
|
||||||
- [x] Document code
|
- [x] Document code
|
||||||
- [x] Create a result object to facilitate returning function results.
|
- [x] Create a result object to facilitate returning function results.
|
||||||
- [x] Refactor main_window_functions into as many objects (forms, etc.) as possible to clean it up.
|
- [x] Refactor main_window_functions into as many objects (forms, etc.) as possible to clean it up.
|
||||||
|
|||||||
52
alembic/versions/36a47d8837ca_adding_in_equipment.py
Normal file
52
alembic/versions/36a47d8837ca_adding_in_equipment.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
"""Adding in Equipment
|
||||||
|
|
||||||
|
Revision ID: 36a47d8837ca
|
||||||
|
Revises: 238c3c3e5863
|
||||||
|
Create Date: 2023-12-12 09:16:09.559753
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '36a47d8837ca'
|
||||||
|
down_revision = '238c3c3e5863'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('_equipment',
|
||||||
|
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=64), nullable=True),
|
||||||
|
sa.Column('nickname', sa.String(length=64), nullable=True),
|
||||||
|
sa.Column('asset_number', sa.String(length=16), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('_submissiontype_equipment',
|
||||||
|
sa.Column('equipment_id', sa.INTEGER(), nullable=False),
|
||||||
|
sa.Column('submission_id', sa.INTEGER(), nullable=False),
|
||||||
|
sa.Column('uses', sa.JSON(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['equipment_id'], ['_equipment.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['submission_id'], ['_submission_types.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('equipment_id', 'submission_id')
|
||||||
|
)
|
||||||
|
op.create_table('_equipment_submissions',
|
||||||
|
sa.Column('equipment_id', sa.INTEGER(), nullable=False),
|
||||||
|
sa.Column('submission_id', sa.INTEGER(), nullable=False),
|
||||||
|
sa.Column('comments', sa.String(length=1024), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['equipment_id'], ['_equipment.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['submission_id'], ['_submissions.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('equipment_id', 'submission_id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('_equipment_submissions')
|
||||||
|
op.drop_table('_submissiontype_equipment')
|
||||||
|
op.drop_table('_equipment')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""Adding times to equipSubAssoc
|
||||||
|
|
||||||
|
Revision ID: 3e94fecbbe91
|
||||||
|
Revises: cd5c225b5d2a
|
||||||
|
Create Date: 2023-12-15 09:38:33.931976
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '3e94fecbbe91'
|
||||||
|
down_revision = 'cd5c225b5d2a'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('start_time', sa.TIMESTAMP(), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('end_time', sa.TIMESTAMP(), nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('end_time')
|
||||||
|
batch_op.drop_column('start_time')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
32
alembic/versions/761baf9d7842_adding_equipment_clustering.py
Normal file
32
alembic/versions/761baf9d7842_adding_equipment_clustering.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"""Adding equipment clustering
|
||||||
|
|
||||||
|
Revision ID: 761baf9d7842
|
||||||
|
Revises: 3e94fecbbe91
|
||||||
|
Create Date: 2023-12-18 14:31:21.533319
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '761baf9d7842'
|
||||||
|
down_revision = '3e94fecbbe91'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('_equipment', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('cluster_name', sa.String(length=16), nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('_equipment', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('cluster_name')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""Adding static option to equipSTASsoc
|
||||||
|
|
||||||
|
Revision ID: cd11db3794ed
|
||||||
|
Revises: 36a47d8837ca
|
||||||
|
Create Date: 2023-12-12 14:47:20.924443
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'cd11db3794ed'
|
||||||
|
down_revision = '36a47d8837ca'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('_submissiontype_equipment', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('static', sa.INTEGER(), nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('_submissiontype_equipment', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('static')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""Adding process to equipSubAssoc
|
||||||
|
|
||||||
|
Revision ID: cd5c225b5d2a
|
||||||
|
Revises: cd11db3794ed
|
||||||
|
Create Date: 2023-12-15 09:13:23.492512
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'cd5c225b5d2a'
|
||||||
|
down_revision = 'cd11db3794ed'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('process', sa.String(length=64), nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('process')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -4,7 +4,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
# Version of the realpython-reader package
|
# Version of the realpython-reader package
|
||||||
__project__ = "submissions"
|
__project__ = "submissions"
|
||||||
__version__ = "202312.2b"
|
__version__ = "202312.3b"
|
||||||
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
|
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
|
||||||
__copyright__ = "2022-2023, Government of Canada"
|
__copyright__ = "2022-2023, Government of Canada"
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import logging
|
|||||||
from tools import check_authorization, setup_lookup, query_return, Report, Result, Settings
|
from tools import check_authorization, setup_lookup, query_return, Report, Result, Settings
|
||||||
from typing import List
|
from typing import List
|
||||||
from pandas import ExcelFile
|
from pandas import ExcelFile
|
||||||
|
from pathlib import Path
|
||||||
from . import Base, BaseClass, Organization
|
from . import Base, BaseClass, Organization
|
||||||
|
|
||||||
logger = logging.getLogger(f'submissions.{__name__}')
|
logger = logging.getLogger(f'submissions.{__name__}')
|
||||||
@@ -55,7 +56,7 @@ class KitType(BaseClass):
|
|||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<KitType({self.name})>"
|
return f"<KitType({self.name})>"
|
||||||
|
|
||||||
def get_reagents(self, required:bool=False, submission_type:str|SubmissionType|None=None) -> list:
|
def get_reagents(self, required:bool=False, submission_type:str|SubmissionType|None=None) -> List[ReagentType]:
|
||||||
"""
|
"""
|
||||||
Return ReagentTypes linked to kit through KitTypeReagentTypeAssociation.
|
Return ReagentTypes linked to kit through KitTypeReagentTypeAssociation.
|
||||||
|
|
||||||
@@ -243,6 +244,10 @@ class ReagentType(BaseClass):
|
|||||||
pass
|
pass
|
||||||
return query_return(query=query, limit=limit)
|
return query_return(query=query, limit=limit)
|
||||||
|
|
||||||
|
def to_pydantic(self):
|
||||||
|
from backend.validators.pydant import PydReagent
|
||||||
|
return PydReagent(lot=None, type=self.name, name=self.name, expiry=date.today())
|
||||||
|
|
||||||
class KitTypeReagentTypeAssociation(BaseClass):
|
class KitTypeReagentTypeAssociation(BaseClass):
|
||||||
"""
|
"""
|
||||||
table containing reagenttype/kittype associations
|
table containing reagenttype/kittype associations
|
||||||
@@ -583,6 +588,14 @@ class SubmissionType(BaseClass):
|
|||||||
|
|
||||||
kit_types = association_proxy("submissiontype_kit_associations", "kit_type") #: Proxy of kittype association
|
kit_types = association_proxy("submissiontype_kit_associations", "kit_type") #: Proxy of kittype association
|
||||||
|
|
||||||
|
submissiontype_equipment_associations = relationship(
|
||||||
|
"SubmissionTypeEquipmentAssociation",
|
||||||
|
back_populates="submission_type",
|
||||||
|
cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
|
||||||
|
equipment = association_proxy("submissiontype_equipment_associations", "equipment")
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<SubmissionType({self.name})>"
|
return f"<SubmissionType({self.name})>"
|
||||||
|
|
||||||
@@ -595,6 +608,35 @@ class SubmissionType(BaseClass):
|
|||||||
"""
|
"""
|
||||||
return ExcelFile(self.template_file).sheet_names
|
return ExcelFile(self.template_file).sheet_names
|
||||||
|
|
||||||
|
def set_template_file(self, filepath:Path|str):
|
||||||
|
if isinstance(filepath, str):
|
||||||
|
filepath = Path(filepath)
|
||||||
|
with open (filepath, "rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
self.template_file = data
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def get_equipment(self) -> list:
|
||||||
|
from backend.validators.pydant import PydEquipmentPool
|
||||||
|
# if static:
|
||||||
|
# return [item.equipment.to_pydantic() for item in self.submissiontype_equipment_associations if item.static==1]
|
||||||
|
# else:
|
||||||
|
preliminary1 = [item.equipment.to_pydantic(static=item.static) for item in self.submissiontype_equipment_associations]# if item.static==0]
|
||||||
|
preliminary2 = [item.equipment.to_pydantic(static=item.static) for item in self.submissiontype_equipment_associations]# if item.static==0]
|
||||||
|
output = []
|
||||||
|
pools = list(set([item.pool_name for item in preliminary1 if item.pool_name != None]))
|
||||||
|
for pool in pools:
|
||||||
|
c_ = []
|
||||||
|
for item in preliminary1:
|
||||||
|
if item.pool_name == pool:
|
||||||
|
c_.append(item)
|
||||||
|
preliminary2.remove(item)
|
||||||
|
if len(c_) > 0:
|
||||||
|
output.append(PydEquipmentPool(name=pool, equipment=c_))
|
||||||
|
for item in preliminary2:
|
||||||
|
output.append(item)
|
||||||
|
return output
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@setup_lookup
|
@setup_lookup
|
||||||
def query(cls,
|
def query(cls,
|
||||||
@@ -772,4 +814,145 @@ class SubmissionReagentAssociation(BaseClass):
|
|||||||
# limit = query.count()
|
# limit = query.count()
|
||||||
return query_return(query=query, limit=limit)
|
return query_return(query=query, limit=limit)
|
||||||
|
|
||||||
|
def to_sub_dict(self, extraction_kit):
|
||||||
|
output = self.reagent.to_sub_dict(extraction_kit)
|
||||||
|
output['comments'] = self.comments
|
||||||
|
return output
|
||||||
|
|
||||||
|
class Equipment(BaseClass):
|
||||||
|
|
||||||
|
# Currently abstract until ready to implement
|
||||||
|
# __abstract__ = True
|
||||||
|
|
||||||
|
__tablename__ = "_equipment"
|
||||||
|
|
||||||
|
id = Column(INTEGER, primary_key=True)
|
||||||
|
name = Column(String(64))
|
||||||
|
nickname = Column(String(64))
|
||||||
|
asset_number = Column(String(16))
|
||||||
|
pool_name = Column(String(16))
|
||||||
|
|
||||||
|
equipment_submission_associations = relationship(
|
||||||
|
"SubmissionEquipmentAssociation",
|
||||||
|
back_populates="equipment",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
)
|
||||||
|
|
||||||
|
submissions = association_proxy("equipment_submission_associations", "submission")
|
||||||
|
|
||||||
|
equipment_submissiontype_associations = relationship(
|
||||||
|
"SubmissionTypeEquipmentAssociation",
|
||||||
|
back_populates="equipment",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
)
|
||||||
|
|
||||||
|
submission_types = association_proxy("equipment_submission_associations", "submission_type")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Equipment({self.name})>"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@setup_lookup
|
||||||
|
def query(cls,
|
||||||
|
name:str|None=None,
|
||||||
|
nickname:str|None=None,
|
||||||
|
asset_number:str|None=None,
|
||||||
|
limit:int=0
|
||||||
|
) -> Equipment|List[Equipment]:
|
||||||
|
query = cls.__database_session__.query(cls)
|
||||||
|
match name:
|
||||||
|
case str():
|
||||||
|
query = query.filter(cls.name==name)
|
||||||
|
limit = 1
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
|
match nickname:
|
||||||
|
case str():
|
||||||
|
query = query.filter(cls.nickname==nickname)
|
||||||
|
limit = 1
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
|
match asset_number:
|
||||||
|
case str():
|
||||||
|
query = query.filter(cls.asset_number==asset_number)
|
||||||
|
limit = 1
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
|
return query_return(query=query, limit=limit)
|
||||||
|
|
||||||
|
def to_pydantic(self, static):
|
||||||
|
from backend.validators.pydant import PydEquipment
|
||||||
|
return PydEquipment(static=static, **self.__dict__)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
self.__database_session__.add(self)
|
||||||
|
self.__database_session__.commit()
|
||||||
|
|
||||||
|
class SubmissionEquipmentAssociation(BaseClass):
|
||||||
|
|
||||||
|
# Currently abstract until ready to implement
|
||||||
|
# __abstract__ = True
|
||||||
|
|
||||||
|
__tablename__ = "_equipment_submissions"
|
||||||
|
|
||||||
|
equipment_id = Column(INTEGER, ForeignKey("_equipment.id"), primary_key=True) #: id of associated equipment
|
||||||
|
submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True) #: id of associated submission
|
||||||
|
process = Column(String(64)) #: name of the process run on this equipment
|
||||||
|
start_time = Column(TIMESTAMP)
|
||||||
|
end_time = Column(TIMESTAMP)
|
||||||
|
comments = Column(String(1024))
|
||||||
|
|
||||||
|
submission = relationship("BasicSubmission", back_populates="submission_equipment_associations") #: associated submission
|
||||||
|
|
||||||
|
equipment = relationship(Equipment, back_populates="equipment_submission_associations") #: associated submission
|
||||||
|
|
||||||
|
def __init__(self, submission, equipment):
|
||||||
|
self.submission = submission
|
||||||
|
self.equipment = equipment
|
||||||
|
|
||||||
|
def to_sub_dict(self) -> dict:
|
||||||
|
output = dict(name=self.equipment.name, asset_number=self.equipment.asset_number, comment=self.comments)
|
||||||
|
return output
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
self.__database_session__.add(self)
|
||||||
|
self.__database_session__.commit()
|
||||||
|
|
||||||
|
class SubmissionTypeEquipmentAssociation(BaseClass):
|
||||||
|
|
||||||
|
# __abstract__ = True
|
||||||
|
|
||||||
|
__tablename__ = "_submissiontype_equipment"
|
||||||
|
|
||||||
|
equipment_id = Column(INTEGER, ForeignKey("_equipment.id"), primary_key=True) #: id of associated equipment
|
||||||
|
submissiontype_id = Column(INTEGER, ForeignKey("_submission_types.id"), primary_key=True) #: id of associated submission
|
||||||
|
uses = Column(JSON) #: locations of equipment on the submission type excel sheet.
|
||||||
|
static = Column(INTEGER, default=1) #: if 1 this piece of equipment will always be used, otherwise it will need to be selected from list?
|
||||||
|
|
||||||
|
submission_type = relationship(SubmissionType, back_populates="submissiontype_equipment_associations") #: associated submission
|
||||||
|
|
||||||
|
equipment = relationship(Equipment, back_populates="equipment_submissiontype_associations") #: associated equipment
|
||||||
|
|
||||||
|
@validates('static')
|
||||||
|
def validate_age(self, key, value):
|
||||||
|
"""
|
||||||
|
Ensures only 1 & 0 used in 'static'
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): name of attribute
|
||||||
|
value (_type_): value of attribute
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: Raised if bad value given
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
_type_: value
|
||||||
|
"""
|
||||||
|
if not 0 <= value < 2:
|
||||||
|
raise ValueError(f'Invalid required value {value}. Must be 0 or 1.')
|
||||||
|
return value
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
self.__database_session__.add(self)
|
||||||
|
self.__database_session__.commit()
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|||||||
from getpass import getuser
|
from getpass import getuser
|
||||||
import math, json, logging, uuid, tempfile, re, yaml
|
import math, json, logging, uuid, tempfile, re, yaml
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from . import Reagent, SubmissionType, KitType, Organization
|
from . import Reagent, SubmissionType, KitType, Organization, Equipment, SubmissionEquipmentAssociation
|
||||||
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case
|
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case
|
||||||
from sqlalchemy.orm import relationship, validates, Query
|
from sqlalchemy.orm import relationship, validates, Query
|
||||||
from json.decoder import JSONDecodeError
|
from json.decoder import JSONDecodeError
|
||||||
@@ -69,6 +69,13 @@ class BasicSubmission(BaseClass):
|
|||||||
# to "keyword" attribute
|
# to "keyword" attribute
|
||||||
reagents = association_proxy("submission_reagent_associations", "reagent") #: Association proxy to SubmissionSampleAssociation.samples
|
reagents = association_proxy("submission_reagent_associations", "reagent") #: Association proxy to SubmissionSampleAssociation.samples
|
||||||
|
|
||||||
|
submission_equipment_associations = relationship(
|
||||||
|
"SubmissionEquipmentAssociation",
|
||||||
|
back_populates="submission",
|
||||||
|
cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
equipment = association_proxy("submission_equipment_associations", "equipment")
|
||||||
|
|
||||||
# Allows for subclassing into ex. BacterialCulture, Wastewater, etc.
|
# Allows for subclassing into ex. BacterialCulture, Wastewater, etc.
|
||||||
__mapper_args__ = {
|
__mapper_args__ = {
|
||||||
"polymorphic_identity": "Basic Submission",
|
"polymorphic_identity": "Basic Submission",
|
||||||
@@ -124,7 +131,7 @@ class BasicSubmission(BaseClass):
|
|||||||
# Updated 2023-09 to use the extraction kit to pull reagents.
|
# Updated 2023-09 to use the extraction kit to pull reagents.
|
||||||
if full_data:
|
if full_data:
|
||||||
try:
|
try:
|
||||||
reagents = [item.to_sub_dict(extraction_kit=self.extraction_kit) for item in self.reagents]
|
reagents = [item.to_sub_dict(extraction_kit=self.extraction_kit) for item in self.submission_reagent_associations]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"We got an error retrieving reagents: {e}")
|
logger.error(f"We got an error retrieving reagents: {e}")
|
||||||
reagents = None
|
reagents = None
|
||||||
@@ -138,6 +145,13 @@ class BasicSubmission(BaseClass):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error setting comment: {self.comment}")
|
logger.error(f"Error setting comment: {self.comment}")
|
||||||
comments = None
|
comments = None
|
||||||
|
try:
|
||||||
|
equipment = [item.to_sub_dict() for item in self.submission_equipment_associations]
|
||||||
|
if len(equipment) == 0:
|
||||||
|
equipment = None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error setting equipment: {self.equipment}")
|
||||||
|
equipment = None
|
||||||
output = {
|
output = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"Plate Number": self.rsl_plate_num,
|
"Plate Number": self.rsl_plate_num,
|
||||||
@@ -153,7 +167,8 @@ class BasicSubmission(BaseClass):
|
|||||||
"reagents": reagents,
|
"reagents": reagents,
|
||||||
"samples": samples,
|
"samples": samples,
|
||||||
"extraction_info": ext_info,
|
"extraction_info": ext_info,
|
||||||
"comment": comments
|
"comment": comments,
|
||||||
|
"equipment": equipment
|
||||||
}
|
}
|
||||||
return output
|
return output
|
||||||
|
|
||||||
@@ -447,7 +462,7 @@ class BasicSubmission(BaseClass):
|
|||||||
logger.debug(f"Got {len(subs)} submissions.")
|
logger.debug(f"Got {len(subs)} submissions.")
|
||||||
df = pd.DataFrame.from_records(subs)
|
df = pd.DataFrame.from_records(subs)
|
||||||
# Exclude sub information
|
# Exclude sub information
|
||||||
for item in ['controls', 'extraction_info', 'pcr_info', 'comment', 'comments', 'samples', 'reagents']:
|
for item in ['controls', 'extraction_info', 'pcr_info', 'comment', 'comments', 'samples', 'reagents', 'equipment']:
|
||||||
try:
|
try:
|
||||||
df = df.drop(item, axis=1)
|
df = df.drop(item, axis=1)
|
||||||
except:
|
except:
|
||||||
@@ -520,7 +535,7 @@ class BasicSubmission(BaseClass):
|
|||||||
_type_: _description_
|
_type_: _description_
|
||||||
"""
|
"""
|
||||||
# assoc = SubmissionSampleAssociation.query(submission=self, sample=sample, limit=1)
|
# assoc = SubmissionSampleAssociation.query(submission=self, sample=sample, limit=1)
|
||||||
assoc = [item.sample for item in self.submission_sample_associations if item.sample==sample][0]
|
assoc = [item for item in self.submission_sample_associations if item.sample==sample][0]
|
||||||
for k,v in input_dict.items():
|
for k,v in input_dict.items():
|
||||||
try:
|
try:
|
||||||
setattr(assoc, k, v)
|
setattr(assoc, k, v)
|
||||||
@@ -751,6 +766,7 @@ class BasicSubmission(BaseClass):
|
|||||||
msg = "This submission already exists.\nWould you like to overwrite?"
|
msg = "This submission already exists.\nWould you like to overwrite?"
|
||||||
return instance, code, msg
|
return instance, code, msg
|
||||||
|
|
||||||
|
|
||||||
# Below are the custom submission types
|
# Below are the custom submission types
|
||||||
|
|
||||||
class BacterialCulture(BasicSubmission):
|
class BacterialCulture(BasicSubmission):
|
||||||
@@ -877,6 +893,12 @@ class BacterialCulture(BasicSubmission):
|
|||||||
template += "_{{ submitting_lab }}_{{ submitter_plate_num }}"
|
template += "_{{ submitting_lab }}_{{ submitter_plate_num }}"
|
||||||
return template
|
return template
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_info(cls, input_dict: dict, xl: pd.ExcelFile | None = None) -> dict:
|
||||||
|
input_dict = super().parse_info(input_dict, xl)
|
||||||
|
input_dict['submitted_date']['missing'] = True
|
||||||
|
return input_dict
|
||||||
|
|
||||||
class Wastewater(BasicSubmission):
|
class Wastewater(BasicSubmission):
|
||||||
"""
|
"""
|
||||||
derivative submission type from BasicSubmission
|
derivative submission type from BasicSubmission
|
||||||
@@ -1009,7 +1031,8 @@ class Wastewater(BasicSubmission):
|
|||||||
Returns:
|
Returns:
|
||||||
str: String for regex construction
|
str: String for regex construction
|
||||||
"""
|
"""
|
||||||
return "(?P<Wastewater>RSL(?:-|_)?WW(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(_|-)?\d?([^_0123456789]|$)R?\d?)?)"
|
# return "(?P<Wastewater>RSL(?:-|_)?WW(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(_|-)?\d?([^_0123456789\s]|$)R?\d?)?)"
|
||||||
|
return "(?P<Wastewater>RSL(?:-|_)?WW(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(_|-)?\d?([^_0123456789\s]|$)?R?\d?)?)"
|
||||||
|
|
||||||
class WastewaterArtic(BasicSubmission):
|
class WastewaterArtic(BasicSubmission):
|
||||||
"""
|
"""
|
||||||
@@ -1416,7 +1439,9 @@ class BasicSample(BaseClass):
|
|||||||
return instance
|
return instance
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
raise AttributeError(f"Save not implemented for {self.__class__}")
|
# raise AttributeError(f"Save not implemented for {self.__class__}")
|
||||||
|
self.__database_session__.add(self)
|
||||||
|
self.__database_session__.commit()
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
raise AttributeError(f"Delete not implemented for {self.__class__}")
|
raise AttributeError(f"Delete not implemented for {self.__class__}")
|
||||||
@@ -1735,4 +1760,3 @@ class WastewaterAssociation(SubmissionSampleAssociation):
|
|||||||
pcr_results = Column(JSON) #: imported PCR status from QuantStudio
|
pcr_results = Column(JSON) #: imported PCR status from QuantStudio
|
||||||
|
|
||||||
__mapper_args__ = {"polymorphic_identity": "Wastewater Association", "polymorphic_load": "inline"}
|
__mapper_args__ = {"polymorphic_identity": "Wastewater Association", "polymorphic_load": "inline"}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import logging, re
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from dateutil.parser import parse, ParserError
|
from dateutil.parser import parse, ParserError
|
||||||
from tools import check_not_nan, convert_nans_to_nones, Settings
|
from tools import check_not_nan, convert_nans_to_nones, Settings, is_missing
|
||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|
||||||
@@ -186,23 +186,15 @@ class InfoParser(object):
|
|||||||
value = df.iat[relevant[item]['row']-1, relevant[item]['column']-1]
|
value = df.iat[relevant[item]['row']-1, relevant[item]['column']-1]
|
||||||
match item:
|
match item:
|
||||||
case "submission_type":
|
case "submission_type":
|
||||||
|
value, missing = is_missing(value)
|
||||||
value = value.title()
|
value = value.title()
|
||||||
case _:
|
case _:
|
||||||
pass
|
value, missing = is_missing(value)
|
||||||
logger.debug(f"Setting {item} on {sheet} to {value}")
|
logger.debug(f"Setting {item} on {sheet} to {value}")
|
||||||
if check_not_nan(value):
|
try:
|
||||||
if value != "None":
|
dicto[item] = dict(value=value, missing=missing)
|
||||||
try:
|
except (KeyError, IndexError):
|
||||||
dicto[item] = dict(value=value, missing=False)
|
continue
|
||||||
except (KeyError, IndexError):
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
dicto[item] = dict(value=value, missing=True)
|
|
||||||
except (KeyError, IndexError):
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
dicto[item] = dict(value=convert_nans_to_nones(value), missing=True)
|
|
||||||
return self.custom_parser(input_dict=dicto, xl=self.xl)
|
return self.custom_parser(input_dict=dicto, xl=self.xl)
|
||||||
|
|
||||||
class ReagentParser(object):
|
class ReagentParser(object):
|
||||||
@@ -293,7 +285,9 @@ class SampleParser(object):
|
|||||||
self.xl = xl
|
self.xl = xl
|
||||||
self.submission_type = submission_type
|
self.submission_type = submission_type
|
||||||
sample_info_map = self.fetch_sample_info_map(submission_type=submission_type)
|
sample_info_map = self.fetch_sample_info_map(submission_type=submission_type)
|
||||||
|
logger.debug(f"sample_info_map: {sample_info_map}")
|
||||||
self.plate_map = self.construct_plate_map(plate_map_location=sample_info_map['plate_map'])
|
self.plate_map = self.construct_plate_map(plate_map_location=sample_info_map['plate_map'])
|
||||||
|
logger.debug(f"plate_map: {self.plate_map}")
|
||||||
self.lookup_table = self.construct_lookup_table(lookup_table_location=sample_info_map['lookup_table'])
|
self.lookup_table = self.construct_lookup_table(lookup_table_location=sample_info_map['lookup_table'])
|
||||||
if "plates" in sample_info_map:
|
if "plates" in sample_info_map:
|
||||||
self.plates = sample_info_map['plates']
|
self.plates = sample_info_map['plates']
|
||||||
@@ -332,10 +326,12 @@ class SampleParser(object):
|
|||||||
Returns:
|
Returns:
|
||||||
pd.DataFrame: Plate map grid
|
pd.DataFrame: Plate map grid
|
||||||
"""
|
"""
|
||||||
|
logger.debug(f"Plate map location: {plate_map_location}")
|
||||||
df = self.xl.parse(plate_map_location['sheet'], header=None, dtype=object)
|
df = self.xl.parse(plate_map_location['sheet'], header=None, dtype=object)
|
||||||
df = df.iloc[plate_map_location['start_row']-1:plate_map_location['end_row'], plate_map_location['start_column']-1:plate_map_location['end_column']]
|
df = df.iloc[plate_map_location['start_row']-1:plate_map_location['end_row'], plate_map_location['start_column']-1:plate_map_location['end_column']]
|
||||||
df = pd.DataFrame(df.values[1:], columns=df.iloc[0])
|
df = pd.DataFrame(df.values[1:], columns=df.iloc[0])
|
||||||
df = df.set_index(df.columns[0])
|
df = df.set_index(df.columns[0])
|
||||||
|
logger.debug(f"Vanilla platemap: {df}")
|
||||||
# custom_mapper = get_polymorphic_subclass(models.BasicSubmission, self.submission_type)
|
# custom_mapper = get_polymorphic_subclass(models.BasicSubmission, self.submission_type)
|
||||||
custom_mapper = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
|
custom_mapper = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
|
||||||
df = custom_mapper.custom_platemap(self.xl, df)
|
df = custom_mapper.custom_platemap(self.xl, df)
|
||||||
@@ -440,6 +436,7 @@ class SampleParser(object):
|
|||||||
"""
|
"""
|
||||||
result = None
|
result = None
|
||||||
new_samples = []
|
new_samples = []
|
||||||
|
logger.debug(f"Starting samples: {pformat(self.samples)}")
|
||||||
for ii, sample in enumerate(self.samples):
|
for ii, sample in enumerate(self.samples):
|
||||||
# try:
|
# try:
|
||||||
# if sample['submitter_id'] in [check_sample['sample'].submitter_id for check_sample in new_samples]:
|
# if sample['submitter_id'] in [check_sample['sample'].submitter_id for check_sample in new_samples]:
|
||||||
|
|||||||
@@ -127,9 +127,10 @@ class PydReagent(BaseModel):
|
|||||||
reagent.name = value
|
reagent.name = value
|
||||||
case "comment":
|
case "comment":
|
||||||
continue
|
continue
|
||||||
assoc = SubmissionReagentAssociation(reagent=reagent, submission=submission)
|
if submission != None:
|
||||||
assoc.comments = self.comment
|
assoc = SubmissionReagentAssociation(reagent=reagent, submission=submission)
|
||||||
reagent.reagent_submission_associations.append(assoc)
|
assoc.comments = self.comment
|
||||||
|
reagent.reagent_submission_associations.append(assoc)
|
||||||
# add end-of-life extension from reagent type to expiry date
|
# add end-of-life extension from reagent type to expiry date
|
||||||
# NOTE: this will now be done only in the reporting phase to account for potential changes in end-of-life extensions
|
# NOTE: this will now be done only in the reporting phase to account for potential changes in end-of-life extensions
|
||||||
return reagent, report
|
return reagent, report
|
||||||
@@ -199,7 +200,8 @@ class PydSample(BaseModel, extra='allow'):
|
|||||||
row=row, column=column)
|
row=row, column=column)
|
||||||
try:
|
try:
|
||||||
instance.sample_submission_associations.append(association)
|
instance.sample_submission_associations.append(association)
|
||||||
except IntegrityError:
|
except IntegrityError as e:
|
||||||
|
logger.error(f"Could not attach submission sample association due to: {e}")
|
||||||
instance.metadata.session.rollback()
|
instance.metadata.session.rollback()
|
||||||
return instance, report
|
return instance, report
|
||||||
|
|
||||||
@@ -420,13 +422,18 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
value = value['value']
|
value = value['value']
|
||||||
logger.debug(f"Setting {key} to {value}")
|
logger.debug(f"Setting {key} to {value}")
|
||||||
try:
|
match key:
|
||||||
instance.set_attribute(key=key, value=value)
|
case "samples":
|
||||||
except AttributeError as e:
|
for sample in self.samples:
|
||||||
logger.debug(f"Could not set attribute: {key} to {value} due to: \n\n {e}")
|
sample, _ = sample.toSQL(submission=instance)
|
||||||
continue
|
case _:
|
||||||
except KeyError:
|
try:
|
||||||
continue
|
instance.set_attribute(key=key, value=value)
|
||||||
|
except AttributeError as e:
|
||||||
|
logger.debug(f"Could not set attribute: {key} to {value} due to: \n\n {e}")
|
||||||
|
continue
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
logger.debug(f"Calculating costs for procedure...")
|
logger.debug(f"Calculating costs for procedure...")
|
||||||
instance.calculate_base_cost()
|
instance.calculate_base_cost()
|
||||||
@@ -735,4 +742,35 @@ class PydKit(BaseModel):
|
|||||||
[item.toSQL(instance) for item in self.reagent_types]
|
[item.toSQL(instance) for item in self.reagent_types]
|
||||||
return instance, report
|
return instance, report
|
||||||
|
|
||||||
|
class PydEquipment(BaseModel, extra='ignore'):
|
||||||
|
|
||||||
|
name: str
|
||||||
|
nickname: str|None
|
||||||
|
asset_number: str
|
||||||
|
pool_name: str|None
|
||||||
|
static: bool|int
|
||||||
|
|
||||||
|
@field_validator("static")
|
||||||
|
@classmethod
|
||||||
|
def to_boolean(cls, value):
|
||||||
|
match value:
|
||||||
|
case int():
|
||||||
|
if value == 0:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
case _:
|
||||||
|
return value
|
||||||
|
|
||||||
|
def toForm(self, parent):
|
||||||
|
from frontend.widgets.equipment_usage import EquipmentCheckBox
|
||||||
|
return EquipmentCheckBox(parent=parent, equipment=self)
|
||||||
|
|
||||||
|
class PydEquipmentPool(BaseModel):
|
||||||
|
|
||||||
|
name: str
|
||||||
|
equipment: List[PydEquipment]
|
||||||
|
|
||||||
|
def toForm(self, parent):
|
||||||
|
from frontend.widgets.equipment_usage import PoolComboBox
|
||||||
|
return PoolComboBox(parent=parent, pool=self)
|
||||||
@@ -18,6 +18,8 @@ from .submission_table import SubmissionsSheet
|
|||||||
from .submission_widget import SubmissionFormContainer
|
from .submission_widget import SubmissionFormContainer
|
||||||
from .controls_chart import ControlsViewer
|
from .controls_chart import ControlsViewer
|
||||||
from .kit_creator import KitAdder
|
from .kit_creator import KitAdder
|
||||||
|
from .submission_type_creator import SubbmissionTypeAdder
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(f'submissions.{__name__}')
|
logger = logging.getLogger(f'submissions.{__name__}')
|
||||||
logger.info("Hello, I am a logger")
|
logger.info("Hello, I am a logger")
|
||||||
@@ -207,11 +209,13 @@ class AddSubForm(QWidget):
|
|||||||
self.tab1 = QWidget()
|
self.tab1 = QWidget()
|
||||||
self.tab2 = QWidget()
|
self.tab2 = QWidget()
|
||||||
self.tab3 = QWidget()
|
self.tab3 = QWidget()
|
||||||
|
self.tab4 = QWidget()
|
||||||
self.tabs.resize(300,200)
|
self.tabs.resize(300,200)
|
||||||
# Add tabs
|
# Add tabs
|
||||||
self.tabs.addTab(self.tab1,"Submissions")
|
self.tabs.addTab(self.tab1,"Submissions")
|
||||||
self.tabs.addTab(self.tab2,"Controls")
|
self.tabs.addTab(self.tab2,"Controls")
|
||||||
self.tabs.addTab(self.tab3, "Add Kit")
|
self.tabs.addTab(self.tab3, "Add SubmissionType")
|
||||||
|
self.tabs.addTab(self.tab4, "Add Kit")
|
||||||
# Create submission adder form
|
# Create submission adder form
|
||||||
self.formwidget = SubmissionFormContainer(self)
|
self.formwidget = SubmissionFormContainer(self)
|
||||||
self.formlayout = QVBoxLayout(self)
|
self.formlayout = QVBoxLayout(self)
|
||||||
@@ -238,10 +242,14 @@ class AddSubForm(QWidget):
|
|||||||
self.tab2.layout.addWidget(self.controls_viewer)
|
self.tab2.layout.addWidget(self.controls_viewer)
|
||||||
self.tab2.setLayout(self.tab2.layout)
|
self.tab2.setLayout(self.tab2.layout)
|
||||||
# create custom widget to add new tabs
|
# create custom widget to add new tabs
|
||||||
adder = KitAdder(self)
|
ST_adder = SubbmissionTypeAdder(self)
|
||||||
self.tab3.layout = QVBoxLayout(self)
|
self.tab3.layout = QVBoxLayout(self)
|
||||||
self.tab3.layout.addWidget(adder)
|
self.tab3.layout.addWidget(ST_adder)
|
||||||
self.tab3.setLayout(self.tab3.layout)
|
self.tab3.setLayout(self.tab3.layout)
|
||||||
|
kit_adder = KitAdder(self)
|
||||||
|
self.tab4.layout = QVBoxLayout(self)
|
||||||
|
self.tab4.layout.addWidget(kit_adder)
|
||||||
|
self.tab4.setLayout(self.tab4.layout)
|
||||||
# add tabs to main widget
|
# add tabs to main widget
|
||||||
self.layout.addWidget(self.tabs)
|
self.layout.addWidget(self.tabs)
|
||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
89
src/submissions/frontend/widgets/equipment_usage.py
Normal file
89
src/submissions/frontend/widgets/equipment_usage.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox,
|
||||||
|
QLabel, QWidget, QHBoxLayout,
|
||||||
|
QVBoxLayout, QDialogButtonBox)
|
||||||
|
from backend.db.models import SubmissionType
|
||||||
|
from backend.validators.pydant import PydEquipment, PydEquipmentPool
|
||||||
|
|
||||||
|
class EquipmentUsage(QDialog):
|
||||||
|
|
||||||
|
def __init__(self, parent, submission_type:SubmissionType|str) -> QDialog:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle("Equipment Checklist")
|
||||||
|
if isinstance(submission_type, str):
|
||||||
|
submission_type = SubmissionType.query(name=submission_type)
|
||||||
|
# self.static_equipment = submission_type.get_equipment()
|
||||||
|
self.opt_equipment = submission_type.get_equipment()
|
||||||
|
self.layout = QVBoxLayout()
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
self.populate_form()
|
||||||
|
|
||||||
|
def populate_form(self):
|
||||||
|
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||||
|
self.buttonBox = QDialogButtonBox(QBtn)
|
||||||
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
|
self.buttonBox.rejected.connect(self.reject)
|
||||||
|
for eq in self.opt_equipment:
|
||||||
|
self.layout.addWidget(eq.toForm(parent=self))
|
||||||
|
self.layout.addWidget(self.buttonBox)
|
||||||
|
|
||||||
|
def parse_form(self):
|
||||||
|
output = []
|
||||||
|
for widget in self.findChildren(QWidget):
|
||||||
|
match widget:
|
||||||
|
case (EquipmentCheckBox()|PoolComboBox()) :
|
||||||
|
output.append(widget.parse_form())
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
|
return [item for item in output if item != None]
|
||||||
|
|
||||||
|
class EquipmentCheckBox(QWidget):
|
||||||
|
|
||||||
|
def __init__(self, parent, equipment:PydEquipment) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.layout = QHBoxLayout()
|
||||||
|
self.label = QLabel()
|
||||||
|
self.label.setMaximumWidth(125)
|
||||||
|
self.label.setMinimumWidth(125)
|
||||||
|
self.check = QCheckBox()
|
||||||
|
if equipment.static:
|
||||||
|
self.check.setChecked(True)
|
||||||
|
# self.check.setEnabled(False)
|
||||||
|
if equipment.nickname != None:
|
||||||
|
text = f"{equipment.name} ({equipment.nickname})"
|
||||||
|
else:
|
||||||
|
text = equipment.name
|
||||||
|
self.setObjectName(equipment.name)
|
||||||
|
self.label.setText(text)
|
||||||
|
self.layout.addWidget(self.label)
|
||||||
|
self.layout.addWidget(self.check)
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
|
def parse_form(self) -> str|None:
|
||||||
|
if self.check.isChecked():
|
||||||
|
return self.objectName()
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
class PoolComboBox(QWidget):
|
||||||
|
|
||||||
|
def __init__(self, parent, pool:PydEquipmentPool) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.layout = QHBoxLayout()
|
||||||
|
# label = QLabel()
|
||||||
|
# label.setText(pool.name)
|
||||||
|
self.box = QComboBox()
|
||||||
|
self.box.setMaximumWidth(125)
|
||||||
|
self.box.setMinimumWidth(125)
|
||||||
|
self.box.addItems([item.name for item in pool.equipment])
|
||||||
|
self.check = QCheckBox()
|
||||||
|
# self.layout.addWidget(label)
|
||||||
|
self.layout.addWidget(self.box)
|
||||||
|
self.layout.addWidget(self.check)
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
|
def parse_form(self) -> str:
|
||||||
|
if self.check.isChecked():
|
||||||
|
return self.box.currentText()
|
||||||
|
else:
|
||||||
|
return None
|
||||||
@@ -82,7 +82,7 @@ class KitAdder(QWidget):
|
|||||||
print(self.app)
|
print(self.app)
|
||||||
# get bottommost row
|
# get bottommost row
|
||||||
maxrow = self.grid.rowCount()
|
maxrow = self.grid.rowCount()
|
||||||
reg_form = ReagentTypeForm()
|
reg_form = ReagentTypeForm(parent=self)
|
||||||
reg_form.setObjectName(f"ReagentForm_{maxrow}")
|
reg_form.setObjectName(f"ReagentForm_{maxrow}")
|
||||||
# self.grid.addWidget(reg_form, maxrow + 1,0,1,2)
|
# self.grid.addWidget(reg_form, maxrow + 1,0,1,2)
|
||||||
self.grid.addWidget(reg_form, maxrow,0,1,4)
|
self.grid.addWidget(reg_form, maxrow,0,1,4)
|
||||||
@@ -139,8 +139,8 @@ class ReagentTypeForm(QWidget):
|
|||||||
"""
|
"""
|
||||||
custom widget to add information about a new reagenttype
|
custom widget to add information about a new reagenttype
|
||||||
"""
|
"""
|
||||||
def __init__(self) -> None:
|
def __init__(self, parent) -> None:
|
||||||
super().__init__()
|
super().__init__(parent)
|
||||||
grid = QGridLayout()
|
grid = QGridLayout()
|
||||||
self.setLayout(grid)
|
self.setLayout(grid)
|
||||||
grid.addWidget(QLabel("Reagent Type Name"),0,0)
|
grid.addWidget(QLabel("Reagent Type Name"),0,0)
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class KitSelector(QDialog):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.setWindowTitle(title)
|
self.setWindowTitle(title)
|
||||||
self.widget = QComboBox()
|
self.widget = QComboBox()
|
||||||
kits = [item.__str__() for item in KitType.query()]
|
kits = [item.name for item in KitType.query()]
|
||||||
self.widget.addItems(kits)
|
self.widget.addItems(kits)
|
||||||
self.widget.setEditable(False)
|
self.widget.setEditable(False)
|
||||||
# set yes/no buttons
|
# set yes/no buttons
|
||||||
|
|||||||
@@ -15,11 +15,12 @@ from PyQt6.QtWidgets import (
|
|||||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||||
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
|
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
|
||||||
from PyQt6.QtGui import QAction, QCursor, QPixmap, QPainter
|
from PyQt6.QtGui import QAction, QCursor, QPixmap, QPainter
|
||||||
from backend.db.models import BasicSubmission
|
from backend.db.models import BasicSubmission, Equipment, SubmissionEquipmentAssociation
|
||||||
from backend.excel import make_report_html, make_report_xlsx
|
from backend.excel import make_report_html, make_report_xlsx
|
||||||
from tools import check_if_app, Report, Result, jinja_template_loading, get_first_blank_df_row, row_map
|
from tools import check_if_app, Report, Result, jinja_template_loading, get_first_blank_df_row, row_map
|
||||||
from xhtml2pdf import pisa
|
from xhtml2pdf import pisa
|
||||||
from .pop_ups import QuestionAsker
|
from .pop_ups import QuestionAsker
|
||||||
|
from .equipment_usage import EquipmentUsage
|
||||||
from ..visualizations import make_plate_barcode, make_plate_map, make_plate_map_html
|
from ..visualizations import make_plate_barcode, make_plate_map, make_plate_map_html
|
||||||
from .functions import select_save_file, select_open_file
|
from .functions import select_save_file, select_open_file
|
||||||
from .misc import ReportDatePicker
|
from .misc import ReportDatePicker
|
||||||
@@ -159,22 +160,44 @@ class SubmissionsSheet(QTableView):
|
|||||||
# barcodeAction = QAction("Print Barcode", self)
|
# barcodeAction = QAction("Print Barcode", self)
|
||||||
commentAction = QAction("Add Comment", self)
|
commentAction = QAction("Add Comment", self)
|
||||||
backupAction = QAction("Backup", self)
|
backupAction = QAction("Backup", self)
|
||||||
|
equipAction = QAction("Add Equipment", self)
|
||||||
# hitpickAction = QAction("Hitpicks", self)
|
# hitpickAction = QAction("Hitpicks", self)
|
||||||
renameAction.triggered.connect(lambda: self.delete_item(event))
|
renameAction.triggered.connect(lambda: self.delete_item(event))
|
||||||
detailsAction.triggered.connect(lambda: self.show_details())
|
detailsAction.triggered.connect(lambda: self.show_details())
|
||||||
# barcodeAction.triggered.connect(lambda: self.create_barcode())
|
# barcodeAction.triggered.connect(lambda: self.create_barcode())
|
||||||
commentAction.triggered.connect(lambda: self.add_comment())
|
commentAction.triggered.connect(lambda: self.add_comment())
|
||||||
backupAction.triggered.connect(lambda: self.regenerate_submission_form())
|
backupAction.triggered.connect(lambda: self.regenerate_submission_form())
|
||||||
|
equipAction.triggered.connect(lambda: self.add_equipment())
|
||||||
# hitpickAction.triggered.connect(lambda: self.hit_pick())
|
# hitpickAction.triggered.connect(lambda: self.hit_pick())
|
||||||
self.menu.addAction(detailsAction)
|
self.menu.addAction(detailsAction)
|
||||||
self.menu.addAction(renameAction)
|
self.menu.addAction(renameAction)
|
||||||
# self.menu.addAction(barcodeAction)
|
# self.menu.addAction(barcodeAction)
|
||||||
self.menu.addAction(commentAction)
|
self.menu.addAction(commentAction)
|
||||||
self.menu.addAction(backupAction)
|
self.menu.addAction(backupAction)
|
||||||
|
self.menu.addAction(equipAction)
|
||||||
# self.menu.addAction(hitpickAction)
|
# self.menu.addAction(hitpickAction)
|
||||||
# add other required actions
|
# add other required actions
|
||||||
self.menu.popup(QCursor.pos())
|
self.menu.popup(QCursor.pos())
|
||||||
|
|
||||||
|
def add_equipment(self):
|
||||||
|
index = (self.selectionModel().currentIndex())
|
||||||
|
value = index.sibling(index.row(),0).data()
|
||||||
|
self.add_equipment_function(rsl_plate_id=value)
|
||||||
|
|
||||||
|
def add_equipment_function(self, rsl_plate_id):
|
||||||
|
submission = BasicSubmission.query(id=rsl_plate_id)
|
||||||
|
submission_type = submission.submission_type_name
|
||||||
|
dlg = EquipmentUsage(parent=self, submission_type=submission_type)
|
||||||
|
if dlg.exec():
|
||||||
|
equipment = dlg.parse_form()
|
||||||
|
for equip in equipment:
|
||||||
|
e = Equipment.query(name=equip)
|
||||||
|
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=e)
|
||||||
|
# submission.submission_equipment_associations.append(assoc)
|
||||||
|
logger.debug(f"Appending SubmissionEquipmentAssociation: {assoc}")
|
||||||
|
# submission.save()
|
||||||
|
assoc.save()
|
||||||
|
|
||||||
def delete_item(self, event):
|
def delete_item(self, event):
|
||||||
"""
|
"""
|
||||||
Confirms user deletion and sends id to backend for deletion.
|
Confirms user deletion and sends id to backend for deletion.
|
||||||
@@ -193,65 +216,6 @@ class SubmissionsSheet(QTableView):
|
|||||||
return
|
return
|
||||||
self.setData()
|
self.setData()
|
||||||
|
|
||||||
# def hit_pick(self):
|
|
||||||
# """
|
|
||||||
# Extract positive samples from submissions with PCR results and export to csv.
|
|
||||||
# NOTE: For this to work for arbitrary samples, positive samples must have 'positive' in their name
|
|
||||||
# """
|
|
||||||
# # Get all selected rows
|
|
||||||
# indices = self.selectionModel().selectedIndexes()
|
|
||||||
# # convert to id numbers
|
|
||||||
# indices = [index.sibling(index.row(), 0).data() for index in indices]
|
|
||||||
# # biomek can handle 4 plates maximum
|
|
||||||
# if len(indices) > 4:
|
|
||||||
# logger.error(f"Error: Had to truncate number of plates to 4.")
|
|
||||||
# indices = indices[:4]
|
|
||||||
# # lookup ids in the database
|
|
||||||
# # subs = [lookup_submissions(ctx=self.ctx, id=id) for id in indices]
|
|
||||||
# subs = [BasicSubmission.query(id=id) for id in indices]
|
|
||||||
# # full list of samples
|
|
||||||
# dicto = []
|
|
||||||
# # list to contain plate images
|
|
||||||
# images = []
|
|
||||||
# for iii, sub in enumerate(subs):
|
|
||||||
# # second check to make sure there aren't too many plates
|
|
||||||
# if iii > 3:
|
|
||||||
# logger.error(f"Error: Had to truncate number of plates to 4.")
|
|
||||||
# continue
|
|
||||||
# plate_dicto = sub.hitpick_plate(plate_number=iii+1)
|
|
||||||
# if plate_dicto == None:
|
|
||||||
# continue
|
|
||||||
# image = make_plate_map(plate_dicto)
|
|
||||||
# images.append(image)
|
|
||||||
# for item in plate_dicto:
|
|
||||||
# if len(dicto) < 94:
|
|
||||||
# dicto.append(item)
|
|
||||||
# else:
|
|
||||||
# logger.error(f"We had to truncate the number of samples to 94.")
|
|
||||||
# logger.debug(f"We found {len(dicto)} to hitpick")
|
|
||||||
# # convert all samples to dataframe
|
|
||||||
# df = make_hitpicks(dicto)
|
|
||||||
# df = df[df.positive != False]
|
|
||||||
# logger.debug(f"Size of the dataframe: {df.shape[0]}")
|
|
||||||
# msg = AlertPop(message=f"We found {df.shape[0]} samples to hitpick", status="INFORMATION")
|
|
||||||
# msg.exec()
|
|
||||||
# if df.size == 0:
|
|
||||||
# return
|
|
||||||
# date = datetime.strftime(datetime.today(), "%Y-%m-%d")
|
|
||||||
# # ask for filename and save as csv.
|
|
||||||
# home_dir = Path(self.ctx.directory_path).joinpath(f"Hitpicks_{date}.csv").resolve().__str__()
|
|
||||||
# fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".csv")[0])
|
|
||||||
# if fname.__str__() == ".":
|
|
||||||
# logger.debug("Saving csv was cancelled.")
|
|
||||||
# return
|
|
||||||
# df.to_csv(fname.__str__(), index=False)
|
|
||||||
# # show plate maps
|
|
||||||
# for image in images:
|
|
||||||
# try:
|
|
||||||
# image.show()
|
|
||||||
# except Exception as e:
|
|
||||||
# logger.error(f"Could not show image: {e}.")
|
|
||||||
|
|
||||||
def link_extractions(self):
|
def link_extractions(self):
|
||||||
self.link_extractions_function()
|
self.link_extractions_function()
|
||||||
self.app.report.add_result(self.report)
|
self.app.report.add_result(self.report)
|
||||||
|
|||||||
118
src/submissions/frontend/widgets/submission_type_creator.py
Normal file
118
src/submissions/frontend/widgets/submission_type_creator.py
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QScrollArea,
|
||||||
|
QGridLayout, QPushButton, QLabel,
|
||||||
|
QLineEdit, QComboBox, QDoubleSpinBox,
|
||||||
|
QSpinBox, QDateEdit
|
||||||
|
)
|
||||||
|
from sqlalchemy import FLOAT, INTEGER
|
||||||
|
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
||||||
|
from backend.db import SubmissionType, Equipment, SubmissionTypeEquipmentAssociation, BasicSubmission
|
||||||
|
from backend.validators import PydReagentType, PydKit
|
||||||
|
import logging
|
||||||
|
from pprint import pformat
|
||||||
|
from tools import Report
|
||||||
|
from typing import Tuple
|
||||||
|
from .functions import select_open_file
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|
||||||
|
class SubbmissionTypeAdder(QWidget):
|
||||||
|
|
||||||
|
def __init__(self, parent) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.report = Report()
|
||||||
|
self.app = parent.parent()
|
||||||
|
self.template_path = ""
|
||||||
|
main_box = QVBoxLayout(self)
|
||||||
|
scroll = QScrollArea(self)
|
||||||
|
main_box.addWidget(scroll)
|
||||||
|
scroll.setWidgetResizable(True)
|
||||||
|
scrollContent = QWidget(scroll)
|
||||||
|
self.grid = QGridLayout()
|
||||||
|
scrollContent.setLayout(self.grid)
|
||||||
|
# insert submit button at top
|
||||||
|
self.submit_btn = QPushButton("Submit")
|
||||||
|
self.grid.addWidget(self.submit_btn,0,0,1,1)
|
||||||
|
self.grid.addWidget(QLabel("Submission Type Name:"),2,0)
|
||||||
|
# widget to get kit name
|
||||||
|
self.st_name = QLineEdit()
|
||||||
|
self.st_name.setObjectName("submission_type_name")
|
||||||
|
self.grid.addWidget(self.st_name,2,1,1,2)
|
||||||
|
self.grid.addWidget(QLabel("Template File"),3,0)
|
||||||
|
template_selector = QPushButton("Select")
|
||||||
|
self.grid.addWidget(template_selector,3,1)
|
||||||
|
self.template_label = QLabel("None")
|
||||||
|
self.grid.addWidget(self.template_label,3,2)
|
||||||
|
# self.grid.addWidget(QLabel("Used For Submission Type:"),3,0)
|
||||||
|
# widget to get uses of kit
|
||||||
|
exclude = ['id', 'submitting_lab_id', 'extraction_kit_id', 'reagents_id', 'extraction_info', 'pcr_info', 'run_cost']
|
||||||
|
self.columns = {key:value for key, value in BasicSubmission.__dict__.items() if isinstance(value, InstrumentedAttribute)}
|
||||||
|
self.columns = {key:value for key, value in self.columns.items() if hasattr(value, "type") and key not in exclude}
|
||||||
|
for iii, key in enumerate(self.columns):
|
||||||
|
idx = iii + 4
|
||||||
|
# convert field name to human readable.
|
||||||
|
# field_name = key
|
||||||
|
# self.grid.addWidget(QLabel(field_name),idx,0)
|
||||||
|
# print(self.columns[key].type)
|
||||||
|
# match self.columns[key].type:
|
||||||
|
# case FLOAT():
|
||||||
|
# add_widget = QDoubleSpinBox()
|
||||||
|
# add_widget.setMinimum(0)
|
||||||
|
# add_widget.setMaximum(9999)
|
||||||
|
# case INTEGER():
|
||||||
|
# add_widget = QSpinBox()
|
||||||
|
# add_widget.setMinimum(0)
|
||||||
|
# add_widget.setMaximum(9999)
|
||||||
|
# case _:
|
||||||
|
# add_widget = QLineEdit()
|
||||||
|
# add_widget.setObjectName(key)
|
||||||
|
self.grid.addWidget(InfoWidget(parent=self, key=key), idx,0,1,3)
|
||||||
|
scroll.setWidget(scrollContent)
|
||||||
|
self.submit_btn.clicked.connect(self.submit)
|
||||||
|
template_selector.clicked.connect(self.get_template_path)
|
||||||
|
|
||||||
|
def submit(self):
|
||||||
|
info = self.parse_form()
|
||||||
|
ST = SubmissionType(name=self.st_name.text(), info_map=info)
|
||||||
|
with open(self.template_path, "rb") as f:
|
||||||
|
ST.template_file = f.read()
|
||||||
|
logger.debug(ST.__dict__)
|
||||||
|
|
||||||
|
def parse_form(self):
|
||||||
|
widgets = [widget for widget in self.findChildren(QWidget) if isinstance(widget, InfoWidget)]
|
||||||
|
return [{widget.objectName():widget.parse_form()} for widget in widgets]
|
||||||
|
|
||||||
|
def get_template_path(self):
|
||||||
|
self.template_path = select_open_file(obj=self, file_extension="xlsx")
|
||||||
|
self.template_label.setText(self.template_path.__str__())
|
||||||
|
|
||||||
|
class InfoWidget(QWidget):
|
||||||
|
|
||||||
|
def __init__(self, parent: QWidget, key) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
grid = QGridLayout()
|
||||||
|
self.setLayout(grid)
|
||||||
|
grid.addWidget(QLabel(key.replace("_", " ").title()),0,0,1,4)
|
||||||
|
self.setObjectName(key)
|
||||||
|
grid.addWidget(QLabel("Sheet Names (comma seperated):"),1,0)
|
||||||
|
self.sheet = QLineEdit()
|
||||||
|
self.sheet.setObjectName("sheets")
|
||||||
|
grid.addWidget(self.sheet, 1,1,1,3)
|
||||||
|
grid.addWidget(QLabel("Row:"),2,0,alignment=Qt.AlignmentFlag.AlignRight)
|
||||||
|
self.row = QSpinBox()
|
||||||
|
self.row.setObjectName("row")
|
||||||
|
grid.addWidget(self.row,2,1)
|
||||||
|
grid.addWidget(QLabel("Column:"),2,2,alignment=Qt.AlignmentFlag.AlignRight)
|
||||||
|
self.column = QSpinBox()
|
||||||
|
self.column.setObjectName("column")
|
||||||
|
grid.addWidget(self.column,2,3)
|
||||||
|
|
||||||
|
def parse_form(self):
|
||||||
|
return dict(
|
||||||
|
sheets = self.sheet.text().split(","),
|
||||||
|
row = self.row.value(),
|
||||||
|
column = self.column.value()
|
||||||
|
)
|
||||||
@@ -62,12 +62,13 @@ class SubmissionFormContainer(QWidget):
|
|||||||
self.app.result_reporter()
|
self.app.result_reporter()
|
||||||
|
|
||||||
def scrape_reagents(self, *args, **kwargs):
|
def scrape_reagents(self, *args, **kwargs):
|
||||||
print(f"\n\n{inspect.stack()[1].function}\n\n")
|
caller = inspect.stack()[1].function.__repr__().replace("'", "")
|
||||||
self.scrape_reagents_function(args[0])
|
logger.debug(f"Args: {args}, kwargs: {kwargs}")
|
||||||
|
self.scrape_reagents_function(args[0], caller=caller)
|
||||||
self.kit_integrity_completion()
|
self.kit_integrity_completion()
|
||||||
self.app.report.add_result(self.report)
|
self.app.report.add_result(self.report)
|
||||||
self.report = Report()
|
self.report = Report()
|
||||||
match inspect.stack()[1].function:
|
match inspect.stack()[1].function.__repr__():
|
||||||
case "import_submission_function":
|
case "import_submission_function":
|
||||||
pass
|
pass
|
||||||
case _:
|
case _:
|
||||||
@@ -83,7 +84,7 @@ class SubmissionFormContainer(QWidget):
|
|||||||
self.kit_integrity_completion_function()
|
self.kit_integrity_completion_function()
|
||||||
self.app.report.add_result(self.report)
|
self.app.report.add_result(self.report)
|
||||||
self.report = Report()
|
self.report = Report()
|
||||||
match inspect.stack()[1].function:
|
match inspect.stack()[1].function.__repr__():
|
||||||
case "import_submission_function":
|
case "import_submission_function":
|
||||||
pass
|
pass
|
||||||
case _:
|
case _:
|
||||||
@@ -161,7 +162,7 @@ class SubmissionFormContainer(QWidget):
|
|||||||
logger.debug(f"Outgoing report: {self.report.results}")
|
logger.debug(f"Outgoing report: {self.report.results}")
|
||||||
logger.debug(f"All attributes of submission container:\n{pformat(self.__dict__)}")
|
logger.debug(f"All attributes of submission container:\n{pformat(self.__dict__)}")
|
||||||
|
|
||||||
def scrape_reagents_function(self, extraction_kit:str):
|
def scrape_reagents_function(self, extraction_kit:str, caller:str|None=None):
|
||||||
"""
|
"""
|
||||||
Extracted scrape reagents function that will run when
|
Extracted scrape reagents function that will run when
|
||||||
form 'extraction_kit' widget is updated.
|
form 'extraction_kit' widget is updated.
|
||||||
@@ -173,6 +174,9 @@ class SubmissionFormContainer(QWidget):
|
|||||||
Returns:
|
Returns:
|
||||||
Tuple[QMainWindow, dict]: Updated application and result
|
Tuple[QMainWindow, dict]: Updated application and result
|
||||||
"""
|
"""
|
||||||
|
self.form.reagents = []
|
||||||
|
logger.debug(f"\n\n{caller}\n\n")
|
||||||
|
# assert caller == "import_submission_function"
|
||||||
report = Report()
|
report = Report()
|
||||||
logger.debug(f"Extraction kit: {extraction_kit}")
|
logger.debug(f"Extraction kit: {extraction_kit}")
|
||||||
# obj.reagents = []
|
# obj.reagents = []
|
||||||
@@ -195,7 +199,15 @@ class SubmissionFormContainer(QWidget):
|
|||||||
# obj.reagents.append(reagent)
|
# obj.reagents.append(reagent)
|
||||||
# else:
|
# else:
|
||||||
# obj.missing_reagents.append(reagent)
|
# obj.missing_reagents.append(reagent)
|
||||||
self.form.reagents = self.prsr.sub['reagents']
|
match caller:
|
||||||
|
case "import_submission_function":
|
||||||
|
self.form.reagents = self.prsr.sub['reagents']
|
||||||
|
case _:
|
||||||
|
already_have = [reagent for reagent in self.prsr.sub['reagents'] if not reagent.missing]
|
||||||
|
names = list(set([item.type for item in already_have]))
|
||||||
|
logger.debug(f"reagents: {already_have}")
|
||||||
|
reagents = [item.to_pydantic() for item in KitType.query(name=extraction_kit).get_reagents(submission_type=self.pyd.submission_type) if item.name not in names]
|
||||||
|
self.form.reagents = already_have + reagents
|
||||||
# logger.debug(f"Imported reagents: {obj.reagents}")
|
# logger.debug(f"Imported reagents: {obj.reagents}")
|
||||||
# logger.debug(f"Missing reagents: {obj.missing_reagents}")
|
# logger.debug(f"Missing reagents: {obj.missing_reagents}")
|
||||||
self.report.add_result(report)
|
self.report.add_result(report)
|
||||||
@@ -221,6 +233,7 @@ class SubmissionFormContainer(QWidget):
|
|||||||
self.ext_kit = kit_widget.currentText()
|
self.ext_kit = kit_widget.currentText()
|
||||||
# for reagent in obj.pyd.reagents:
|
# for reagent in obj.pyd.reagents:
|
||||||
for reagent in self.form.reagents:
|
for reagent in self.form.reagents:
|
||||||
|
logger.debug(f"Creating widget for {reagent}")
|
||||||
add_widget = ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.ext_kit)
|
add_widget = ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.ext_kit)
|
||||||
# add_widget.setParent(sub_form_container.form)
|
# add_widget.setParent(sub_form_container.form)
|
||||||
self.form.layout().addWidget(add_widget)
|
self.form.layout().addWidget(add_widget)
|
||||||
|
|||||||
@@ -32,10 +32,10 @@
|
|||||||
visibility: visible;
|
visibility: visible;
|
||||||
font-size: large;
|
font-size: large;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<title>Submission Details for {{ sub['Plate Number'] }}</title>
|
<title>Submission Details for {{ sub['Plate Number'] }}</title>
|
||||||
</head>
|
</head>
|
||||||
{% set excluded = ['reagents', 'samples', 'controls', 'extraction_info', 'pcr_info', 'comment', 'barcode', 'platemap', 'export_map'] %}
|
{% set excluded = ['reagents', 'samples', 'controls', 'extraction_info', 'pcr_info', 'comment', 'barcode', 'platemap', 'export_map', 'equipment'] %}
|
||||||
<body>
|
<body>
|
||||||
<h2><u>Submission Details for {{ sub['Plate Number'] }}</u></h2> {% if sub['barcode'] %}<img align='right' height="30px" width="120px" src="data:image/jpeg;base64,{{ sub['barcode'] | safe }}">{% endif %}
|
<h2><u>Submission Details for {{ sub['Plate Number'] }}</u></h2> {% if sub['barcode'] %}<img align='right' height="30px" width="120px" src="data:image/jpeg;base64,{{ sub['barcode'] | safe }}">{% endif %}
|
||||||
<p>{% for key, value in sub.items() if key not in excluded %}
|
<p>{% for key, value in sub.items() if key not in excluded %}
|
||||||
@@ -45,6 +45,12 @@
|
|||||||
<p>{% for item in sub['reagents'] %}
|
<p>{% for item in sub['reagents'] %}
|
||||||
<b>{{ item['type'] }}</b>: {{ item['lot'] }} (EXP: {{ item['expiry'] }})<br>
|
<b>{{ item['type'] }}</b>: {{ item['lot'] }} (EXP: {{ item['expiry'] }})<br>
|
||||||
{% endfor %}</p>
|
{% endfor %}</p>
|
||||||
|
{% if sub['equipment'] %}
|
||||||
|
<h3><u>Equipment:</u></h3>
|
||||||
|
<p>{% for item in sub['equipment'] %}
|
||||||
|
<b>{{ item['name'] }}:</b> {{ item['asset_number']|replace('\n\t', '<br> ') }}<br>
|
||||||
|
{% endfor %}</p>
|
||||||
|
{% endif %}
|
||||||
{% if sub['samples'] %}
|
{% if sub['samples'] %}
|
||||||
<h3><u>Samples:</u></h3>
|
<h3><u>Samples:</u></h3>
|
||||||
<p>{% for item in sub['samples'] %}
|
<p>{% for item in sub['samples'] %}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ Contains miscellaenous functions used by both frontend and backend.
|
|||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
|
||||||
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
|
||||||
@@ -99,13 +98,6 @@ def check_regex_match(pattern:str, check:str) -> bool:
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# def massage_common_reagents(reagent_name:str):
|
|
||||||
# logger.debug(f"Attempting to massage {reagent_name}")
|
|
||||||
# if reagent_name.endswith("water") or "H2O" in reagent_name.upper():
|
|
||||||
# reagent_name = "molecular_grade_water"
|
|
||||||
# reagent_name = reagent_name.replace("µ", "u")
|
|
||||||
# return reagent_name
|
|
||||||
|
|
||||||
class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler):
|
class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler):
|
||||||
|
|
||||||
def doRollover(self):
|
def doRollover(self):
|
||||||
@@ -143,7 +135,7 @@ class Settings(BaseSettings):
|
|||||||
Pydantic model to hold settings
|
Pydantic model to hold settings
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FileNotFoundError: _description_
|
FileNotFoundError: Error if database not found.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
directory_path: Path
|
directory_path: Path
|
||||||
@@ -516,4 +508,10 @@ def readInChunks(fileObj, chunkSize=2048):
|
|||||||
def get_first_blank_df_row(df:pd.DataFrame) -> int:
|
def get_first_blank_df_row(df:pd.DataFrame) -> int:
|
||||||
return len(df) + 1
|
return len(df) + 1
|
||||||
|
|
||||||
|
def is_missing(value:Any) -> Tuple[Any, bool]:
|
||||||
|
if check_not_nan(value):
|
||||||
|
return value, False
|
||||||
|
else:
|
||||||
|
return convert_nans_to_nones(value), True
|
||||||
|
|
||||||
ctx = get_config(None)
|
ctx = get_config(None)
|
||||||
|
|||||||
Reference in New Issue
Block a user