Improved form regeneration for artic.
This commit is contained in:
@@ -1,3 +1,7 @@
|
|||||||
|
## 202401.01
|
||||||
|
|
||||||
|
- Improved tooltips and form regeneration.
|
||||||
|
|
||||||
## 202312.03
|
## 202312.03
|
||||||
|
|
||||||
- Enabled creation of new submission types in gui.
|
- Enabled creation of new submission types in gui.
|
||||||
|
|||||||
3
TODO.md
3
TODO.md
@@ -1,5 +1,6 @@
|
|||||||
|
- [x] Finish Equipment Parser (add in regex to id asset_number)
|
||||||
- [ ] Complete info_map in the SubmissionTypeCreator widget.
|
- [ ] Complete info_map in the SubmissionTypeCreator widget.
|
||||||
- [ ] Update Artic and add in equipment listings... *sigh*.
|
- [x] Update Artic and add in equipment listings... *sigh*.
|
||||||
- [x] Fix WastewaterAssociations not in Session error.
|
- [x] Fix WastewaterAssociations not in Session error.
|
||||||
- Done... I think?
|
- Done... I think?
|
||||||
- [x] Fix submitted date always being today.
|
- [x] Fix submitted date always being today.
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
|
|||||||
# are written from script.py.mako
|
# are written from script.py.mako
|
||||||
# output_encoding = utf-8
|
# output_encoding = utf-8
|
||||||
|
|
||||||
sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db
|
; sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db
|
||||||
; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\Submissions_app_backups\DB_backups\submissions-new.db
|
sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\Submissions_app_backups\DB_backups\submissions-new.db
|
||||||
; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\tests\test_assets\submissions-test.db
|
; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\tests\test_assets\submissions-test.db
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
40
alembic/versions/30aab47d6f12_adding_role_tag_to_.py
Normal file
40
alembic/versions/30aab47d6f12_adding_role_tag_to_.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
"""Adding role tag to SubmissionEquipmentAssociation
|
||||||
|
|
||||||
|
Revision ID: 30aab47d6f12
|
||||||
|
Revises: bc7a74476609
|
||||||
|
Create Date: 2023-12-27 09:00:26.262904
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '30aab47d6f12'
|
||||||
|
down_revision = 'bc7a74476609'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
# op.drop_table('_alembic_tmp__equipment')
|
||||||
|
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('role', 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('role')
|
||||||
|
|
||||||
|
# op.create_table('_alembic_tmp__equipment',
|
||||||
|
# sa.Column('id', sa.INTEGER(), nullable=False),
|
||||||
|
# sa.Column('name', sa.VARCHAR(length=64), nullable=True),
|
||||||
|
# sa.Column('nickname', sa.VARCHAR(length=64), nullable=True),
|
||||||
|
# sa.Column('asset_number', sa.VARCHAR(length=16), nullable=True),
|
||||||
|
# sa.PrimaryKeyConstraint('id')
|
||||||
|
# )
|
||||||
|
# ### end Alembic commands ###
|
||||||
36
alembic/versions/94289d4e63e6_updating_primary_key_for_.py
Normal file
36
alembic/versions/94289d4e63e6_updating_primary_key_for_.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
"""Updating primary key for SubmissionEquipmentAssociation
|
||||||
|
|
||||||
|
Revision ID: 94289d4e63e6
|
||||||
|
Revises: 30aab47d6f12
|
||||||
|
Create Date: 2024-01-03 15:14:21.156127
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '94289d4e63e6'
|
||||||
|
down_revision = '30aab47d6f12'
|
||||||
|
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.alter_column('role',
|
||||||
|
existing_type=sa.VARCHAR(length=64),
|
||||||
|
nullable=False)
|
||||||
|
|
||||||
|
# ### 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.alter_column('role',
|
||||||
|
existing_type=sa.VARCHAR(length=64),
|
||||||
|
nullable=True)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
59
alembic/versions/bc7a74476609_adding_equipment_roles.py
Normal file
59
alembic/versions/bc7a74476609_adding_equipment_roles.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
"""Adding equipment roles
|
||||||
|
|
||||||
|
Revision ID: bc7a74476609
|
||||||
|
Revises: 761baf9d7842
|
||||||
|
Create Date: 2023-12-21 10:44:23.520392
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import sqlite
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'bc7a74476609'
|
||||||
|
down_revision = '761baf9d7842'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('_equipment_roles',
|
||||||
|
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=32), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('_equipmentroles_equipment',
|
||||||
|
sa.Column('equipment_id', sa.INTEGER(), nullable=True),
|
||||||
|
sa.Column('equipmentroles_id', sa.INTEGER(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['equipment_id'], ['_equipment.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['equipmentroles_id'], ['_equipment_roles.id'], )
|
||||||
|
)
|
||||||
|
op.create_table('_submissiontype_equipmentrole',
|
||||||
|
sa.Column('equipmentrole_id', sa.INTEGER(), nullable=False),
|
||||||
|
sa.Column('submissiontype_id', sa.INTEGER(), nullable=False),
|
||||||
|
sa.Column('uses', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('static', sa.INTEGER(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['equipmentrole_id'], ['_equipment_roles.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['submissiontype_id'], ['_submission_types.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('equipmentrole_id', 'submissiontype_id')
|
||||||
|
)
|
||||||
|
op.drop_table('_submissiontype_equipment')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('_submissiontype_equipment',
|
||||||
|
sa.Column('equipment_id', sa.INTEGER(), nullable=False),
|
||||||
|
sa.Column('submissiontype_id', sa.INTEGER(), nullable=False),
|
||||||
|
sa.Column('uses', sqlite.JSON(), nullable=True),
|
||||||
|
sa.Column('static', sa.INTEGER(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['equipment_id'], ['_equipment.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['submissiontype_id'], ['_submission_types.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('equipment_id', 'submissiontype_id')
|
||||||
|
)
|
||||||
|
op.drop_table('_submissiontype_equipmentrole')
|
||||||
|
op.drop_table('_equipmentroles_equipment')
|
||||||
|
op.drop_table('_equipment_roles')
|
||||||
|
# ### 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.3b"
|
__version__ = "202312.4b"
|
||||||
__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"
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Int
|
|||||||
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 datetime import date
|
from datetime import date
|
||||||
import logging
|
import logging, re
|
||||||
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
|
||||||
@@ -24,6 +24,14 @@ reagenttypes_reagents = Table(
|
|||||||
extend_existing = True
|
extend_existing = True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
equipmentroles_equipment = Table(
|
||||||
|
"_equipmentroles_equipment",
|
||||||
|
Base.metadata,
|
||||||
|
Column("equipment_id", INTEGER, ForeignKey("_equipment.id")),
|
||||||
|
Column("equipmentroles_id", INTEGER, ForeignKey("_equipment_roles.id")),
|
||||||
|
extend_existing=True
|
||||||
|
)
|
||||||
|
|
||||||
class KitType(BaseClass):
|
class KitType(BaseClass):
|
||||||
"""
|
"""
|
||||||
Base of kits used in submission processing
|
Base of kits used in submission processing
|
||||||
@@ -589,13 +597,13 @@ 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(
|
submissiontype_equipmentrole_associations = relationship(
|
||||||
"SubmissionTypeEquipmentAssociation",
|
"SubmissionTypeEquipmentRoleAssociation",
|
||||||
back_populates="submission_type",
|
back_populates="submission_type",
|
||||||
cascade="all, delete-orphan"
|
cascade="all, delete-orphan"
|
||||||
)
|
)
|
||||||
|
|
||||||
equipment = association_proxy("submissiontype_equipment_associations", "equipment")
|
equipment = association_proxy("submissiontype_equipmentrole_associations", "equipment_role")
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<SubmissionType({self.name})>"
|
return f"<SubmissionType({self.name})>"
|
||||||
@@ -609,34 +617,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):
|
def set_template_file(self, ctx:Settings, filepath:Path|str):
|
||||||
if isinstance(filepath, str):
|
if isinstance(filepath, str):
|
||||||
filepath = Path(filepath)
|
filepath = Path(filepath)
|
||||||
with open (filepath, "rb") as f:
|
with open (filepath, "rb") as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
self.template_file = data
|
self.template_file = data
|
||||||
self.save()
|
self.save(ctx=ctx)
|
||||||
|
|
||||||
def get_equipment(self) -> list:
|
def construct_equipment_map(self):
|
||||||
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 = []
|
output = []
|
||||||
pools = list(set([item.pool_name for item in preliminary1 if item.pool_name != None]))
|
for item in self.submissiontype_equipmentrole_associations:
|
||||||
for pool in pools:
|
map = item.uses
|
||||||
c_ = []
|
map['role'] = item.equipment_role.name
|
||||||
for item in preliminary1:
|
output.append(map)
|
||||||
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
|
return output
|
||||||
|
# return [item.uses for item in self.submissiontype_equipmentrole_associations]
|
||||||
|
|
||||||
|
def get_equipment(self) -> List['PydEquipmentRole']:
|
||||||
|
return [item.to_pydantic(submission_type=self) for item in self.equipment]
|
||||||
|
|
||||||
|
def get_processes_for_role(self, equipment_role:str|EquipmentRole):
|
||||||
|
match equipment_role:
|
||||||
|
case str():
|
||||||
|
relevant = [item.get_all_processes() for item in self.submissiontype_equipmentrole_associations if item.equipment_role.name==equipment_role]
|
||||||
|
case EquipmentRole():
|
||||||
|
relevant = [item.get_all_processes() for item in self.submissiontype_equipmentrole_associations if item.equipment_role==equipment_role]
|
||||||
|
case _:
|
||||||
|
raise TypeError(f"Type {type(equipment_role)} is not allowed")
|
||||||
|
return list(set([item for items in relevant for item in items if item != None ]))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@setup_lookup
|
@setup_lookup
|
||||||
@@ -832,7 +841,7 @@ class Equipment(BaseClass):
|
|||||||
name = Column(String(64))
|
name = Column(String(64))
|
||||||
nickname = Column(String(64))
|
nickname = Column(String(64))
|
||||||
asset_number = Column(String(16))
|
asset_number = Column(String(16))
|
||||||
pool_name = Column(String(16))
|
roles = relationship("EquipmentRole", back_populates="instances", secondary=equipmentroles_equipment)
|
||||||
|
|
||||||
equipment_submission_associations = relationship(
|
equipment_submission_associations = relationship(
|
||||||
"SubmissionEquipmentAssociation",
|
"SubmissionEquipmentAssociation",
|
||||||
@@ -842,17 +851,12 @@ class Equipment(BaseClass):
|
|||||||
|
|
||||||
submissions = association_proxy("equipment_submission_associations", "submission")
|
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):
|
def __repr__(self):
|
||||||
return f"<Equipment({self.name})>"
|
return f"<Equipment({self.name})>"
|
||||||
|
|
||||||
|
def get_processes(self, submission_type:SubmissionType):
|
||||||
|
return [assoc.process for assoc in self.equipment_submission_associations if assoc.submission.submission_type_name==submission_type.name]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@setup_lookup
|
@setup_lookup
|
||||||
def query(cls,
|
def query(cls,
|
||||||
@@ -882,14 +886,66 @@ class Equipment(BaseClass):
|
|||||||
pass
|
pass
|
||||||
return query_return(query=query, limit=limit)
|
return query_return(query=query, limit=limit)
|
||||||
|
|
||||||
def to_pydantic(self, static):
|
def to_pydantic(self, submission_type:SubmissionType):
|
||||||
from backend.validators.pydant import PydEquipment
|
from backend.validators.pydant import PydEquipment
|
||||||
return PydEquipment(static=static, **self.__dict__)
|
return PydEquipment(processes=self.get_processes(submission_type=submission_type), role=None, **self.__dict__)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
self.__database_session__.add(self)
|
self.__database_session__.add(self)
|
||||||
self.__database_session__.commit()
|
self.__database_session__.commit()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_regex(cls) -> re.Pattern:
|
||||||
|
return re.compile(r"""
|
||||||
|
(?P<PHAC>50\d{5}$)|
|
||||||
|
(?P<HC>HC-\d{6}$)|
|
||||||
|
(?P<Beckman>[^\d][A-Z0-9]{6}$)|
|
||||||
|
(?P<Axygen>[A-Z]{3}-\d{2}-[A-Z]-[A-Z]$)|
|
||||||
|
(?P<Labcon>\d{4}-\d{3}-\d{3}-\d$)""",
|
||||||
|
re.VERBOSE)
|
||||||
|
|
||||||
|
class EquipmentRole(BaseClass):
|
||||||
|
|
||||||
|
__tablename__ = "_equipment_roles"
|
||||||
|
|
||||||
|
id = Column(INTEGER, primary_key=True)
|
||||||
|
name = Column(String(32))
|
||||||
|
instances = relationship("Equipment", back_populates="roles", secondary=equipmentroles_equipment)
|
||||||
|
|
||||||
|
equipmentrole_submissiontype_associations = relationship(
|
||||||
|
"SubmissionTypeEquipmentRoleAssociation",
|
||||||
|
back_populates="equipment_role",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
)
|
||||||
|
|
||||||
|
submission_types = association_proxy("equipmentrole_submission_associations", "submission_type")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<EquipmentRole({self.name})>"
|
||||||
|
|
||||||
|
def to_pydantic(self, submission_type:SubmissionType):
|
||||||
|
from backend.validators.pydant import PydEquipmentRole
|
||||||
|
equipment = [item.to_pydantic(submission_type=submission_type) for item in self.instances]
|
||||||
|
return PydEquipmentRole(equipment=equipment, **self.__dict__)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@setup_lookup
|
||||||
|
def query(cls, name:str|None=None, id:int|None=None, limit:int=0) -> EquipmentRole|List[EquipmentRole]:
|
||||||
|
query = cls.__database_session__.query(cls)
|
||||||
|
match id:
|
||||||
|
case int():
|
||||||
|
query = query.filter(cls.id==id)
|
||||||
|
limit = 1
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
|
match name:
|
||||||
|
case str():
|
||||||
|
query = query.filter(cls.name==name)
|
||||||
|
limit = 1
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
|
return query_return(query=query, limit=limit)
|
||||||
|
|
||||||
class SubmissionEquipmentAssociation(BaseClass):
|
class SubmissionEquipmentAssociation(BaseClass):
|
||||||
|
|
||||||
# Currently abstract until ready to implement
|
# Currently abstract until ready to implement
|
||||||
@@ -899,6 +955,7 @@ class SubmissionEquipmentAssociation(BaseClass):
|
|||||||
|
|
||||||
equipment_id = Column(INTEGER, ForeignKey("_equipment.id"), primary_key=True) #: id of associated equipment
|
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
|
submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True) #: id of associated submission
|
||||||
|
role = Column(String(64), primary_key=True) #: name of the role the equipment fills
|
||||||
process = Column(String(64)) #: name of the process run on this equipment
|
process = Column(String(64)) #: name of the process run on this equipment
|
||||||
start_time = Column(TIMESTAMP)
|
start_time = Column(TIMESTAMP)
|
||||||
end_time = Column(TIMESTAMP)
|
end_time = Column(TIMESTAMP)
|
||||||
@@ -913,27 +970,27 @@ class SubmissionEquipmentAssociation(BaseClass):
|
|||||||
self.equipment = equipment
|
self.equipment = equipment
|
||||||
|
|
||||||
def to_sub_dict(self) -> dict:
|
def to_sub_dict(self) -> dict:
|
||||||
output = dict(name=self.equipment.name, asset_number=self.equipment.asset_number, comment=self.comments)
|
output = dict(name=self.equipment.name, asset_number=self.equipment.asset_number, comment=self.comments, process=[self.process], role=self.role, nickname=self.equipment.nickname)
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
self.__database_session__.add(self)
|
self.__database_session__.add(self)
|
||||||
self.__database_session__.commit()
|
self.__database_session__.commit()
|
||||||
|
|
||||||
class SubmissionTypeEquipmentAssociation(BaseClass):
|
class SubmissionTypeEquipmentRoleAssociation(BaseClass):
|
||||||
|
|
||||||
# __abstract__ = True
|
# __abstract__ = True
|
||||||
|
|
||||||
__tablename__ = "_submissiontype_equipment"
|
__tablename__ = "_submissiontype_equipmentrole"
|
||||||
|
|
||||||
equipment_id = Column(INTEGER, ForeignKey("_equipment.id"), primary_key=True) #: id of associated equipment
|
equipmentrole_id = Column(INTEGER, ForeignKey("_equipment_roles.id"), primary_key=True) #: id of associated equipment
|
||||||
submissiontype_id = Column(INTEGER, ForeignKey("_submission_types.id"), primary_key=True) #: id of associated submission
|
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.
|
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?
|
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
|
submission_type = relationship(SubmissionType, back_populates="submissiontype_equipmentrole_associations") #: associated submission
|
||||||
|
|
||||||
equipment = relationship(Equipment, back_populates="equipment_submissiontype_associations") #: associated equipment
|
equipment_role = relationship(EquipmentRole, back_populates="equipmentrole_submissiontype_associations") #: associated equipment
|
||||||
|
|
||||||
@validates('static')
|
@validates('static')
|
||||||
def validate_age(self, key, value):
|
def validate_age(self, key, value):
|
||||||
@@ -954,6 +1011,11 @@ class SubmissionTypeEquipmentAssociation(BaseClass):
|
|||||||
raise ValueError(f'Invalid required value {value}. Must be 0 or 1.')
|
raise ValueError(f'Invalid required value {value}. Must be 0 or 1.')
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def get_all_processes(self):
|
||||||
|
processes = [equipment.get_processes(self.submission_type) for equipment in self.equipment_role.instances]
|
||||||
|
processes = [item for items in processes for item in items if item != None ]
|
||||||
|
return processes
|
||||||
|
|
||||||
@check_authorization
|
@check_authorization
|
||||||
def save(self, ctx:Settings):
|
def save(self, ctx:Settings):
|
||||||
self.__database_session__.add(self)
|
self.__database_session__.add(self)
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ Models for the main submission types.
|
|||||||
from __future__ import annotations
|
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 operator import attrgetter
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from . import Reagent, SubmissionType, KitType, Organization, Equipment, SubmissionEquipmentAssociation
|
from . import Reagent, SubmissionType, KitType, Organization
|
||||||
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
|
||||||
@@ -13,7 +14,7 @@ from sqlalchemy.ext.associationproxy import association_proxy
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
from openpyxl import Workbook
|
from openpyxl import Workbook
|
||||||
from . import BaseClass
|
from . import BaseClass
|
||||||
from tools import check_not_nan, row_map, query_return, setup_lookup
|
from tools import check_not_nan, row_map, query_return, setup_lookup, jinja_template_loading
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
from typing import List
|
from typing import List
|
||||||
from dateutil.parser import parse
|
from dateutil.parser import parse
|
||||||
@@ -137,14 +138,6 @@ class BasicSubmission(BaseClass):
|
|||||||
reagents = None
|
reagents = None
|
||||||
# samples = [item.sample.to_sub_dict(submission_rsl=self.rsl_plate_num) for item in self.submission_sample_associations]
|
# samples = [item.sample.to_sub_dict(submission_rsl=self.rsl_plate_num) for item in self.submission_sample_associations]
|
||||||
samples = [item.to_sub_dict() for item in self.submission_sample_associations]
|
samples = [item.to_sub_dict() for item in self.submission_sample_associations]
|
||||||
else:
|
|
||||||
reagents = None
|
|
||||||
samples = None
|
|
||||||
try:
|
|
||||||
comments = self.comment
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error setting comment: {self.comment}")
|
|
||||||
comments = None
|
|
||||||
try:
|
try:
|
||||||
equipment = [item.to_sub_dict() for item in self.submission_equipment_associations]
|
equipment = [item.to_sub_dict() for item in self.submission_equipment_associations]
|
||||||
if len(equipment) == 0:
|
if len(equipment) == 0:
|
||||||
@@ -152,6 +145,16 @@ class BasicSubmission(BaseClass):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error setting equipment: {self.equipment}")
|
logger.error(f"Error setting equipment: {self.equipment}")
|
||||||
equipment = None
|
equipment = None
|
||||||
|
else:
|
||||||
|
reagents = None
|
||||||
|
samples = None
|
||||||
|
equipment = None
|
||||||
|
try:
|
||||||
|
comments = self.comment
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error setting comment: {self.comment}")
|
||||||
|
comments = None
|
||||||
|
|
||||||
output = {
|
output = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"Plate Number": self.rsl_plate_num,
|
"Plate Number": self.rsl_plate_num,
|
||||||
@@ -508,7 +511,7 @@ class BasicSubmission(BaseClass):
|
|||||||
field_value = len(self.samples)
|
field_value = len(self.samples)
|
||||||
else:
|
else:
|
||||||
field_value = value
|
field_value = value
|
||||||
case "ctx" | "csv" | "filepath":
|
case "ctx" | "csv" | "filepath" | "equipment":
|
||||||
return
|
return
|
||||||
case "comment":
|
case "comment":
|
||||||
if value == "" or value == None or value == 'null':
|
if value == "" or value == None or value == 'null':
|
||||||
@@ -552,8 +555,9 @@ class BasicSubmission(BaseClass):
|
|||||||
Returns:
|
Returns:
|
||||||
PydSubmission: converted object.
|
PydSubmission: converted object.
|
||||||
"""
|
"""
|
||||||
from backend.validators import PydSubmission, PydSample, PydReagent
|
from backend.validators import PydSubmission, PydSample, PydReagent, PydEquipment
|
||||||
dicto = self.to_dict(full_data=True)
|
dicto = self.to_dict(full_data=True)
|
||||||
|
logger.debug(f"Backup dictionary: {pformat(dicto)}")
|
||||||
# dicto['filepath'] = Path(tempfile.TemporaryFile().name)
|
# dicto['filepath'] = Path(tempfile.TemporaryFile().name)
|
||||||
new_dict = {}
|
new_dict = {}
|
||||||
for key, value in dicto.items():
|
for key, value in dicto.items():
|
||||||
@@ -562,6 +566,8 @@ class BasicSubmission(BaseClass):
|
|||||||
new_dict[key] = [PydReagent(**reagent) for reagent in value]
|
new_dict[key] = [PydReagent(**reagent) for reagent in value]
|
||||||
case "samples":
|
case "samples":
|
||||||
new_dict[key] = [PydSample(**sample) for sample in dicto['samples']]
|
new_dict[key] = [PydSample(**sample) for sample in dicto['samples']]
|
||||||
|
case "equipment":
|
||||||
|
new_dict[key] = [PydEquipment(**equipment) for equipment in dicto['equipment']]
|
||||||
case "Plate Number":
|
case "Plate Number":
|
||||||
new_dict['rsl_plate_num'] = dict(value=value, missing=True)
|
new_dict['rsl_plate_num'] = dict(value=value, missing=True)
|
||||||
case "Submitter Plate Number":
|
case "Submitter Plate Number":
|
||||||
@@ -576,13 +582,14 @@ class BasicSubmission(BaseClass):
|
|||||||
# sys.exit()
|
# sys.exit()
|
||||||
return PydSubmission(**new_dict)
|
return PydSubmission(**new_dict)
|
||||||
|
|
||||||
def backup(self, fname:Path):
|
def backup(self, fname:Path, full_backup:bool=True):
|
||||||
"""
|
"""
|
||||||
Exports xlsx and yml info files for this instance.
|
Exports xlsx and yml info files for this instance.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
fname (Path): Filename of xlsx file.
|
fname (Path): Filename of xlsx file.
|
||||||
"""
|
"""
|
||||||
|
if full_backup:
|
||||||
backup = self.to_dict(full_data=True)
|
backup = self.to_dict(full_data=True)
|
||||||
try:
|
try:
|
||||||
with open(self.__backup_path__.joinpath(fname.with_suffix(".yml")), "w") as f:
|
with open(self.__backup_path__.joinpath(fname.with_suffix(".yml")), "w") as f:
|
||||||
@@ -766,6 +773,8 @@ 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
|
||||||
|
|
||||||
|
def get_used_equipment(self) -> List[str]:
|
||||||
|
return [item.role for item in self.submission_equipment_associations]
|
||||||
|
|
||||||
# Below are the custom submission types
|
# Below are the custom submission types
|
||||||
|
|
||||||
@@ -882,7 +891,8 @@ class BacterialCulture(BasicSubmission):
|
|||||||
Returns:
|
Returns:
|
||||||
str: string for regex construction
|
str: string for regex construction
|
||||||
"""
|
"""
|
||||||
return "(?P<Bacterial_Culture>RSL-?\\d{2}-?\\d{4})"
|
# return "(?P<Bacterial_Culture>RSL-?\\d{2}-?\\d{4})"
|
||||||
|
return "(?P<Bacterial_Culture>RSL(?:-|_)?BC(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(_|-)?\d?([^_0123456789\s]|$)?R?\d?)?)"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def filename_template(cls):
|
def filename_template(cls):
|
||||||
@@ -1176,6 +1186,35 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
input_dict['csv'] = df
|
input_dict['csv'] = df
|
||||||
return input_dict
|
return input_dict
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def custom_autofill(cls, input_excel: Workbook, info: dict | None = None, backup: bool = False) -> Workbook:
|
||||||
|
input_excel = super().custom_autofill(input_excel, info, backup)
|
||||||
|
worksheet = input_excel["First Strand List"]
|
||||||
|
samples = cls.query(rsl_number=info['rsl_plate_num']['value']).submission_sample_associations
|
||||||
|
samples = sorted(samples, key=attrgetter('column', 'row'))
|
||||||
|
source_plates = []
|
||||||
|
first_samples = []
|
||||||
|
for sample in samples:
|
||||||
|
sample = sample.sample
|
||||||
|
try:
|
||||||
|
assoc = [item.submission.rsl_plate_num for item in sample.sample_submission_associations if item.submission.submission_type_name=="Wastewater"][-1]
|
||||||
|
except IndexError:
|
||||||
|
logger.error(f"Association not found for {sample}")
|
||||||
|
continue
|
||||||
|
if assoc not in source_plates:
|
||||||
|
source_plates.append(assoc)
|
||||||
|
first_samples.append(sample.ww_processing_num)
|
||||||
|
# Pad list to length of 3
|
||||||
|
# source_plates = list(set(source_plates))
|
||||||
|
source_plates += ['None'] * (3 - len(source_plates))
|
||||||
|
first_samples += [''] * (3 - len(first_samples))
|
||||||
|
source_plates = zip(source_plates, first_samples, strict=False)
|
||||||
|
for iii, plate in enumerate(source_plates, start=8):
|
||||||
|
logger.debug(f"Plate: {plate}")
|
||||||
|
for jjj, value in enumerate(plate, start=3):
|
||||||
|
worksheet.cell(row=iii, column=jjj, value=value)
|
||||||
|
return input_excel
|
||||||
|
|
||||||
# Sample Classes
|
# Sample Classes
|
||||||
|
|
||||||
class BasicSample(BaseClass):
|
class BasicSample(BaseClass):
|
||||||
@@ -1286,11 +1325,15 @@ class BasicSample(BaseClass):
|
|||||||
dict: dictionary of sample id, row and column in elution plate
|
dict: dictionary of sample id, row and column in elution plate
|
||||||
"""
|
"""
|
||||||
# Since there is no PCR, negliable result is necessary.
|
# Since there is no PCR, negliable result is necessary.
|
||||||
assoc = [item for item in self.sample_submission_associations if item.submission.rsl_plate_num==submission_rsl][0]
|
# assoc = [item for item in self.sample_submission_associations if item.submission.rsl_plate_num==submission_rsl][0]
|
||||||
tooltip_text = f"""
|
fields = self.to_sub_dict(submission_rsl=submission_rsl)
|
||||||
Sample name: {self.submitter_id}<br>
|
env = jinja_template_loading()
|
||||||
Well: {row_map[assoc.row]}{assoc.column}
|
template = env.get_template("tooltip.html")
|
||||||
"""
|
tooltip_text = template.render(fields=fields)
|
||||||
|
# tooltip_text = f"""
|
||||||
|
# Sample name: {self.submitter_id}<br>
|
||||||
|
# Well: {row_map[assoc.row]}{assoc.column}
|
||||||
|
# """
|
||||||
return dict(name=self.submitter_id[:10], positive=False, tooltip=tooltip_text)
|
return dict(name=self.submitter_id[:10], positive=False, tooltip=tooltip_text)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1436,6 +1479,7 @@ class BasicSample(BaseClass):
|
|||||||
used_class = cls.find_subclasses(attrs=kwargs, sample_type=sample_type)
|
used_class = cls.find_subclasses(attrs=kwargs, sample_type=sample_type)
|
||||||
instance = used_class(**kwargs)
|
instance = used_class(**kwargs)
|
||||||
instance.sample_type = sample_type
|
instance.sample_type = sample_type
|
||||||
|
logger.debug(f"Creating instance: {instance}")
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
@@ -1523,6 +1567,25 @@ class WastewaterSample(BasicSample):
|
|||||||
del output_dict['collection_date']
|
del output_dict['collection_date']
|
||||||
return output_dict
|
return output_dict
|
||||||
|
|
||||||
|
def to_sub_dict(self, submission_rsl: str | BasicSubmission) -> dict:
|
||||||
|
sample = super().to_sub_dict(submission_rsl)
|
||||||
|
if self.ww_processing_num != None:
|
||||||
|
sample['ww_processing_num'] = self.ww_processing_num
|
||||||
|
else:
|
||||||
|
sample['ww_processing_num'] = self.submitter_id
|
||||||
|
try:
|
||||||
|
assoc = [item for item in self.sample_submission_associations if item.submission.submission_type_name=="Wastewater"][-1]
|
||||||
|
except:
|
||||||
|
assoc = None
|
||||||
|
if assoc != None:
|
||||||
|
try:
|
||||||
|
sample['ct'] = f"{assoc.ct_n1:.2f}, {assoc.ct_n2:.2f}"
|
||||||
|
except TypeError:
|
||||||
|
sample['ct'] = "None, None"
|
||||||
|
sample['source_plate'] = assoc.submission.rsl_plate_num
|
||||||
|
sample['source_well'] = f"{row_map[assoc.row]}{assoc.column}"
|
||||||
|
return sample
|
||||||
|
|
||||||
class BacterialCultureSample(BasicSample):
|
class BacterialCultureSample(BasicSample):
|
||||||
"""
|
"""
|
||||||
base of bacterial culture sample
|
base of bacterial culture sample
|
||||||
@@ -1541,7 +1604,9 @@ class BacterialCultureSample(BasicSample):
|
|||||||
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
|
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
|
||||||
"""
|
"""
|
||||||
sample = super().to_sub_dict(submission_rsl=submission_rsl)
|
sample = super().to_sub_dict(submission_rsl=submission_rsl)
|
||||||
sample['name'] = f"{self.submitter_id} - ({self.organism})"
|
sample['name'] = self.submitter_id
|
||||||
|
sample['organism'] = self.organism
|
||||||
|
sample['concentration'] = self.concentration
|
||||||
return sample
|
return sample
|
||||||
|
|
||||||
def to_hitpick(self, submission_rsl: str | None = None) -> dict | None:
|
def to_hitpick(self, submission_rsl: str | None = None) -> dict | None:
|
||||||
@@ -1622,13 +1687,15 @@ class SubmissionSampleAssociation(BaseClass):
|
|||||||
if isinstance(polymorphic_identity, dict):
|
if isinstance(polymorphic_identity, dict):
|
||||||
polymorphic_identity = polymorphic_identity['value']
|
polymorphic_identity = polymorphic_identity['value']
|
||||||
if polymorphic_identity == None:
|
if polymorphic_identity == None:
|
||||||
return cls
|
output = cls
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
return [item for item in cls.__subclasses__() if item.__mapper_args__['polymorphic_identity']==polymorphic_identity][0]
|
output = [item for item in cls.__subclasses__() if item.__mapper_args__['polymorphic_identity']==polymorphic_identity][0]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Could not get polymorph {polymorphic_identity} of {cls} due to {e}")
|
logger.error(f"Could not get polymorph {polymorphic_identity} of {cls} due to {e}")
|
||||||
return cls
|
output = cls
|
||||||
|
logger.debug(f"Using SubmissionSampleAssociation subclass: {output}")
|
||||||
|
return output
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@setup_lookup
|
@setup_lookup
|
||||||
@@ -1707,6 +1774,7 @@ class SubmissionSampleAssociation(BaseClass):
|
|||||||
Returns:
|
Returns:
|
||||||
SubmissionSampleAssociation: Queried or new association.
|
SubmissionSampleAssociation: Queried or new association.
|
||||||
"""
|
"""
|
||||||
|
logger.debug(f"Attempting create or query with {kwargs}")
|
||||||
match submission:
|
match submission:
|
||||||
case BasicSubmission():
|
case BasicSubmission():
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import pandas as pd
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from backend.db.models import *
|
from backend.db.models import *
|
||||||
from backend.validators import PydSubmission, PydReagent, RSLNamer, PydSample
|
from backend.validators import PydSubmission, PydReagent, RSLNamer, PydSample, PydEquipment
|
||||||
import logging, re
|
import logging, re
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from datetime import date
|
from datetime import date
|
||||||
@@ -53,6 +53,7 @@ class SheetParser(object):
|
|||||||
self.parse_reagents()
|
self.parse_reagents()
|
||||||
self.import_reagent_validation_check()
|
self.import_reagent_validation_check()
|
||||||
self.parse_samples()
|
self.parse_samples()
|
||||||
|
self.parse_equipment()
|
||||||
self.finalize_parse()
|
self.finalize_parse()
|
||||||
logger.debug(f"Parser.sub after info scrape: {pformat(self.sub)}")
|
logger.debug(f"Parser.sub after info scrape: {pformat(self.sub)}")
|
||||||
|
|
||||||
@@ -90,6 +91,10 @@ class SheetParser(object):
|
|||||||
self.sample_result, self.sub['samples'] = parser.parse_samples()
|
self.sample_result, self.sub['samples'] = parser.parse_samples()
|
||||||
self.plate_map = parser.plate_map
|
self.plate_map = parser.plate_map
|
||||||
|
|
||||||
|
def parse_equipment(self):
|
||||||
|
parser = EquipmentParser(xl=self.xl, submission_type=self.sub['submission_type']['value'])
|
||||||
|
self.sub['equipment'] = parser.parse_equipment()
|
||||||
|
|
||||||
def import_kit_validation_check(self):
|
def import_kit_validation_check(self):
|
||||||
"""
|
"""
|
||||||
Enforce that the parser has an extraction kit
|
Enforce that the parser has an extraction kit
|
||||||
@@ -129,6 +134,9 @@ class SheetParser(object):
|
|||||||
PydSubmission: output pydantic model
|
PydSubmission: output pydantic model
|
||||||
"""
|
"""
|
||||||
# logger.debug(f"Submission dictionary coming into 'to_pydantic':\n{pformat(self.sub)}")
|
# logger.debug(f"Submission dictionary coming into 'to_pydantic':\n{pformat(self.sub)}")
|
||||||
|
logger.debug(f"Equipment: {self.sub['equipment']}")
|
||||||
|
if len(self.sub['equipment']) == 0:
|
||||||
|
self.sub['equipment'] = None
|
||||||
psm = PydSubmission(filepath=self.filepath, **self.sub)
|
psm = PydSubmission(filepath=self.filepath, **self.sub)
|
||||||
return psm
|
return psm
|
||||||
|
|
||||||
@@ -480,6 +488,45 @@ class SampleParser(object):
|
|||||||
plates.append(output)
|
plates.append(output)
|
||||||
return plates
|
return plates
|
||||||
|
|
||||||
|
class EquipmentParser(object):
|
||||||
|
|
||||||
|
def __init__(self, xl:pd.ExcelFile, submission_type:str) -> None:
|
||||||
|
self.submission_type = submission_type
|
||||||
|
self.xl = xl
|
||||||
|
self.map = self.fetch_equipment_map()
|
||||||
|
# self.equipment = self.parse_equipment()
|
||||||
|
|
||||||
|
def fetch_equipment_map(self) -> List[dict]:
|
||||||
|
submission_type = SubmissionType.query(name=self.submission_type)
|
||||||
|
return submission_type.construct_equipment_map()
|
||||||
|
|
||||||
|
def get_asset_number(self, input:str) -> str:
|
||||||
|
regex = Equipment.get_regex()
|
||||||
|
return regex.search(input).group().strip("-")
|
||||||
|
|
||||||
|
def parse_equipment(self):
|
||||||
|
logger.debug(f"Equipment parser going into parsing: {pformat(self.__dict__)}")
|
||||||
|
output = []
|
||||||
|
# sheets = list(set([item['sheet'] for item in self.map]))
|
||||||
|
# logger.debug(f"Sheets: {sheets}")
|
||||||
|
for sheet in self.xl.sheet_names:
|
||||||
|
df = self.xl.parse(sheet, header=None, dtype=object)
|
||||||
|
relevant = [item for item in self.map if item['sheet']==sheet]
|
||||||
|
# logger.debug(f"Relevant equipment: {pformat(relevant)}")
|
||||||
|
previous_asset = ""
|
||||||
|
for equipment in relevant:
|
||||||
|
asset = df.iat[equipment['name']['row']-1, equipment['name']['column']-1]
|
||||||
|
if not check_not_nan(asset):
|
||||||
|
asset = previous_asset
|
||||||
|
else:
|
||||||
|
previous_asset = asset
|
||||||
|
asset = self.get_asset_number(input=asset)
|
||||||
|
eq = Equipment.query(asset_number=asset)
|
||||||
|
process = df.iat[equipment['process']['row']-1, equipment['process']['column']-1]
|
||||||
|
output.append(PydEquipment(name=eq.name, process=[process], role=equipment['role'], asset_number=asset, nickname=eq.nickname))
|
||||||
|
# logger.debug(f"Here is the output so far: {pformat(output)}")
|
||||||
|
return output
|
||||||
|
|
||||||
class PCRParser(object):
|
class PCRParser(object):
|
||||||
"""
|
"""
|
||||||
Object to pull data from Design and Analysis PCR export file.
|
Object to pull data from Design and Analysis PCR export file.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'''
|
'''
|
||||||
Contains pydantic models and accompanying validators
|
Contains pydantic models and accompanying validators
|
||||||
'''
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
import uuid, re, logging
|
import uuid, re, logging
|
||||||
from pydantic import BaseModel, field_validator, Field
|
from pydantic import BaseModel, field_validator, Field
|
||||||
@@ -189,6 +190,7 @@ class PydSample(BaseModel, extra='allow'):
|
|||||||
continue
|
continue
|
||||||
case _:
|
case _:
|
||||||
instance.set_attribute(name=key, value=value)
|
instance.set_attribute(name=key, value=value)
|
||||||
|
out_associations = []
|
||||||
if submission != None:
|
if submission != None:
|
||||||
assoc_type = self.sample_type.replace("Sample", "").strip()
|
assoc_type = self.sample_type.replace("Sample", "").strip()
|
||||||
for row, column in zip(self.row, self.column):
|
for row, column in zip(self.row, self.column):
|
||||||
@@ -198,12 +200,14 @@ class PydSample(BaseModel, extra='allow'):
|
|||||||
submission=submission,
|
submission=submission,
|
||||||
sample=instance,
|
sample=instance,
|
||||||
row=row, column=column)
|
row=row, column=column)
|
||||||
|
logger.debug(f"Using submission_sample_association: {association}")
|
||||||
try:
|
try:
|
||||||
instance.sample_submission_associations.append(association)
|
instance.sample_submission_associations.append(association)
|
||||||
|
out_associations.append(association)
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
logger.error(f"Could not attach submission sample association due to: {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, out_associations, report
|
||||||
|
|
||||||
class PydSubmission(BaseModel, extra='allow'):
|
class PydSubmission(BaseModel, extra='allow'):
|
||||||
filepath: Path
|
filepath: Path
|
||||||
@@ -220,7 +224,16 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
submission_category: dict|None = Field(default=dict(value=None, missing=True), validate_default=True)
|
submission_category: dict|None = Field(default=dict(value=None, missing=True), validate_default=True)
|
||||||
comment: dict|None = Field(default=dict(value="", missing=True), validate_default=True)
|
comment: dict|None = Field(default=dict(value="", missing=True), validate_default=True)
|
||||||
reagents: List[dict]|List[PydReagent] = []
|
reagents: List[dict]|List[PydReagent] = []
|
||||||
samples: List[Any]
|
samples: List[PydSample]
|
||||||
|
equipment: List[PydEquipment]|None
|
||||||
|
|
||||||
|
@field_validator('equipment', mode='before')
|
||||||
|
@classmethod
|
||||||
|
def convert_equipment_dict(cls, value):
|
||||||
|
logger.debug(f"Equipment: {value}")
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return value['value']
|
||||||
|
return value
|
||||||
|
|
||||||
@field_validator('comment', mode='before')
|
@field_validator('comment', mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -425,7 +438,17 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
match key:
|
match key:
|
||||||
case "samples":
|
case "samples":
|
||||||
for sample in self.samples:
|
for sample in self.samples:
|
||||||
sample, _ = sample.toSQL(submission=instance)
|
sample, associations, _ = sample.toSQL(submission=instance)
|
||||||
|
logger.debug(f"Sample SQL object to be added to submission: {sample.__dict__}")
|
||||||
|
for assoc in associations:
|
||||||
|
instance.submission_sample_associations.append(assoc)
|
||||||
|
case "equipment":
|
||||||
|
logger.debug(f"Equipment: {pformat(self.equipment)}")
|
||||||
|
for equip in self.equipment:
|
||||||
|
equip, association = equip.toSQL(submission=instance)
|
||||||
|
if association != None:
|
||||||
|
logger.debug(f"Equipment association SQL object to be added to submission: {association.__dict__}")
|
||||||
|
instance.submission_equipment_associations.append(association)
|
||||||
case _:
|
case _:
|
||||||
try:
|
try:
|
||||||
instance.set_attribute(key=key, value=value)
|
instance.set_attribute(key=key, value=value)
|
||||||
@@ -559,6 +582,7 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Could not write name {reagent['name']['value']} due to {e}")
|
logger.error(f"Could not write name {reagent['name']['value']} due to {e}")
|
||||||
# Get relevant info for that sheet
|
# Get relevant info for that sheet
|
||||||
|
new_info = [item for item in new_info if isinstance(item['location'], dict)]
|
||||||
sheet_info = [item for item in new_info if sheet in item['location']['sheets']]
|
sheet_info = [item for item in new_info if sheet in item['location']['sheets']]
|
||||||
for item in sheet_info:
|
for item in sheet_info:
|
||||||
logger.debug(f"Attempting: {item['type']} in row {item['location']['row']}, column {item['location']['column']}")
|
logger.debug(f"Attempting: {item['type']} in row {item['location']['row']}, column {item['location']['column']}")
|
||||||
@@ -579,9 +603,11 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
Workbook: Updated excel workbook
|
Workbook: Updated excel workbook
|
||||||
"""
|
"""
|
||||||
sample_info = SubmissionType.query(name=self.submission_type['value']).info_map['samples']
|
sample_info = SubmissionType.query(name=self.submission_type['value']).info_map['samples']
|
||||||
|
logger.debug(f"Sample info: {pformat(sample_info)}")
|
||||||
|
logger.debug(f"Workbook sheets: {workbook.sheetnames}")
|
||||||
worksheet = workbook[sample_info["lookup_table"]['sheet']]
|
worksheet = workbook[sample_info["lookup_table"]['sheet']]
|
||||||
samples = sorted(self.samples, key=attrgetter('column', 'row'))
|
samples = sorted(self.samples, key=attrgetter('column', 'row'))
|
||||||
logger.debug(f"Samples: {samples}")
|
logger.debug(f"Samples: {pformat(samples)}")
|
||||||
# Fail safe against multiple instances of the same sample
|
# Fail safe against multiple instances of the same sample
|
||||||
for iii, sample in enumerate(samples, start=1):
|
for iii, sample in enumerate(samples, start=1):
|
||||||
row = sample_info['lookup_table']['start_row'] + iii
|
row = sample_info['lookup_table']['start_row'] + iii
|
||||||
@@ -744,33 +770,46 @@ class PydKit(BaseModel):
|
|||||||
|
|
||||||
class PydEquipment(BaseModel, extra='ignore'):
|
class PydEquipment(BaseModel, extra='ignore'):
|
||||||
|
|
||||||
|
asset_number: str
|
||||||
name: str
|
name: str
|
||||||
nickname: str|None
|
nickname: str|None
|
||||||
asset_number: str
|
process: List[str]|None
|
||||||
pool_name: str|None
|
role: str|None
|
||||||
static: bool|int
|
|
||||||
|
|
||||||
@field_validator("static")
|
@field_validator('process')
|
||||||
@classmethod
|
@classmethod
|
||||||
def to_boolean(cls, value):
|
def remove_dupes(cls, value):
|
||||||
match value:
|
if isinstance(value, list):
|
||||||
case int():
|
return list(set(value))
|
||||||
if value == 0:
|
|
||||||
return False
|
|
||||||
else:
|
else:
|
||||||
return True
|
|
||||||
case _:
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def toForm(self, parent):
|
def toForm(self, parent):
|
||||||
from frontend.widgets.equipment_usage import EquipmentCheckBox
|
from frontend.widgets.equipment_usage import EquipmentCheckBox
|
||||||
return EquipmentCheckBox(parent=parent, equipment=self)
|
return EquipmentCheckBox(parent=parent, equipment=self)
|
||||||
|
|
||||||
class PydEquipmentPool(BaseModel):
|
def toSQL(self, submission:BasicSubmission|str=None):
|
||||||
|
if isinstance(submission, str):
|
||||||
|
submission = BasicSubmission.query(rsl_number=submission)
|
||||||
|
equipment = Equipment.query(asset_number=self.asset_number)
|
||||||
|
if equipment == None:
|
||||||
|
return
|
||||||
|
if submission != None:
|
||||||
|
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=equipment)
|
||||||
|
assoc.process = self.process[0]
|
||||||
|
assoc.role = self.role
|
||||||
|
# equipment.equipment_submission_associations.append(assoc)
|
||||||
|
equipment.equipment_submission_associations.append(assoc)
|
||||||
|
else:
|
||||||
|
assoc = None
|
||||||
|
return equipment, assoc
|
||||||
|
|
||||||
|
class PydEquipmentRole(BaseModel):
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
equipment: List[PydEquipment]
|
equipment: List[PydEquipment]
|
||||||
|
|
||||||
def toForm(self, parent):
|
def toForm(self, parent, submission_type, used):
|
||||||
from frontend.widgets.equipment_usage import PoolComboBox
|
from frontend.widgets.equipment_usage import RoleComboBox
|
||||||
return PoolComboBox(parent=parent, pool=self)
|
return RoleComboBox(parent=parent, role=self, submission_type=submission_type, used=used)
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,26 @@ from PyQt6.QtCore import Qt
|
|||||||
from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox,
|
from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox,
|
||||||
QLabel, QWidget, QHBoxLayout,
|
QLabel, QWidget, QHBoxLayout,
|
||||||
QVBoxLayout, QDialogButtonBox)
|
QVBoxLayout, QDialogButtonBox)
|
||||||
from backend.db.models import SubmissionType
|
from backend.db.models import SubmissionType, Equipment, BasicSubmission
|
||||||
from backend.validators.pydant import PydEquipment, PydEquipmentPool
|
from backend.validators.pydant import PydEquipment, PydEquipmentRole
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|
||||||
class EquipmentUsage(QDialog):
|
class EquipmentUsage(QDialog):
|
||||||
|
|
||||||
def __init__(self, parent, submission_type:SubmissionType|str) -> QDialog:
|
def __init__(self, parent, submission_type:SubmissionType|str, submission:BasicSubmission) -> QDialog:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle("Equipment Checklist")
|
self.setWindowTitle("Equipment Checklist")
|
||||||
|
self.used_equipment = submission.get_used_equipment()
|
||||||
|
logger.debug(f"Existing equipment: {self.used_equipment}")
|
||||||
if isinstance(submission_type, str):
|
if isinstance(submission_type, str):
|
||||||
submission_type = SubmissionType.query(name=submission_type)
|
self.submission_type = SubmissionType.query(name=submission_type)
|
||||||
|
else:
|
||||||
|
self.submission_type = submission_type
|
||||||
# self.static_equipment = submission_type.get_equipment()
|
# self.static_equipment = submission_type.get_equipment()
|
||||||
self.opt_equipment = submission_type.get_equipment()
|
self.opt_equipment = self.submission_type.get_equipment()
|
||||||
|
logger.debug(f"EquipmentRoles: {self.opt_equipment}")
|
||||||
self.layout = QVBoxLayout()
|
self.layout = QVBoxLayout()
|
||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
self.populate_form()
|
self.populate_form()
|
||||||
@@ -24,14 +32,14 @@ class EquipmentUsage(QDialog):
|
|||||||
self.buttonBox.accepted.connect(self.accept)
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
self.buttonBox.rejected.connect(self.reject)
|
self.buttonBox.rejected.connect(self.reject)
|
||||||
for eq in self.opt_equipment:
|
for eq in self.opt_equipment:
|
||||||
self.layout.addWidget(eq.toForm(parent=self))
|
self.layout.addWidget(eq.toForm(parent=self, submission_type=self.submission_type, used=self.used_equipment))
|
||||||
self.layout.addWidget(self.buttonBox)
|
self.layout.addWidget(self.buttonBox)
|
||||||
|
|
||||||
def parse_form(self):
|
def parse_form(self):
|
||||||
output = []
|
output = []
|
||||||
for widget in self.findChildren(QWidget):
|
for widget in self.findChildren(QWidget):
|
||||||
match widget:
|
match widget:
|
||||||
case (EquipmentCheckBox()|PoolComboBox()) :
|
case (EquipmentCheckBox()|RoleComboBox()) :
|
||||||
output.append(widget.parse_form())
|
output.append(widget.parse_form())
|
||||||
case _:
|
case _:
|
||||||
pass
|
pass
|
||||||
@@ -65,25 +73,41 @@ class EquipmentCheckBox(QWidget):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
class PoolComboBox(QWidget):
|
class RoleComboBox(QWidget):
|
||||||
|
|
||||||
def __init__(self, parent, pool:PydEquipmentPool) -> None:
|
def __init__(self, parent, role:PydEquipmentRole, submission_type:SubmissionType, used:list) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.layout = QHBoxLayout()
|
self.layout = QHBoxLayout()
|
||||||
# label = QLabel()
|
# label = QLabel()
|
||||||
# label.setText(pool.name)
|
# label.setText(pool.name)
|
||||||
|
self.role = role
|
||||||
|
self.check = QCheckBox()
|
||||||
|
if role.name in used:
|
||||||
|
self.check.setChecked(False)
|
||||||
|
else:
|
||||||
|
self.check.setChecked(True)
|
||||||
self.box = QComboBox()
|
self.box = QComboBox()
|
||||||
self.box.setMaximumWidth(125)
|
self.box.setMaximumWidth(125)
|
||||||
self.box.setMinimumWidth(125)
|
self.box.setMinimumWidth(125)
|
||||||
self.box.addItems([item.name for item in pool.equipment])
|
self.box.addItems([item.name for item in role.equipment])
|
||||||
self.check = QCheckBox()
|
# self.check = QCheckBox()
|
||||||
# self.layout.addWidget(label)
|
# self.layout.addWidget(label)
|
||||||
self.layout.addWidget(self.box)
|
self.process = QComboBox()
|
||||||
|
self.process.setMaximumWidth(125)
|
||||||
|
self.process.setMinimumWidth(125)
|
||||||
|
self.process.setEditable(True)
|
||||||
|
self.process.addItems(submission_type.get_processes_for_role(equipment_role=role.name))
|
||||||
self.layout.addWidget(self.check)
|
self.layout.addWidget(self.check)
|
||||||
|
self.layout.addWidget(QLabel(f"{role.name}:"))
|
||||||
|
self.layout.addWidget(self.box)
|
||||||
|
self.layout.addWidget(self.process)
|
||||||
|
# self.layout.addWidget(self.check)
|
||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
def parse_form(self) -> str:
|
def parse_form(self) -> str|None:
|
||||||
if self.check.isChecked():
|
eq = Equipment.query(name=self.box.currentText())
|
||||||
return self.box.currentText()
|
if self.check:
|
||||||
|
return PydEquipment(name=eq.name, processes=[self.process.currentText()], role=self.role.name, asset_number=eq.asset_number, nickname=eq.nickname)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -187,16 +187,19 @@ class SubmissionsSheet(QTableView):
|
|||||||
def add_equipment_function(self, rsl_plate_id):
|
def add_equipment_function(self, rsl_plate_id):
|
||||||
submission = BasicSubmission.query(id=rsl_plate_id)
|
submission = BasicSubmission.query(id=rsl_plate_id)
|
||||||
submission_type = submission.submission_type_name
|
submission_type = submission.submission_type_name
|
||||||
dlg = EquipmentUsage(parent=self, submission_type=submission_type)
|
dlg = EquipmentUsage(parent=self, submission_type=submission_type, submission=submission)
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
equipment = dlg.parse_form()
|
equipment = dlg.parse_form()
|
||||||
|
logger.debug(f"We've got equipment: {equipment}")
|
||||||
for equip in equipment:
|
for equip in equipment:
|
||||||
e = Equipment.query(name=equip)
|
e = Equipment.query(name=equip.name)
|
||||||
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=e)
|
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=e)
|
||||||
|
assoc.process = equip.processes[0]
|
||||||
|
assoc.role = equip.role
|
||||||
# submission.submission_equipment_associations.append(assoc)
|
# submission.submission_equipment_associations.append(assoc)
|
||||||
logger.debug(f"Appending SubmissionEquipmentAssociation: {assoc}")
|
logger.debug(f"Appending SubmissionEquipmentAssociation: {assoc}")
|
||||||
# submission.save()
|
# submission.save()
|
||||||
assoc.save()
|
# assoc.save()
|
||||||
|
|
||||||
def delete_item(self, event):
|
def delete_item(self, event):
|
||||||
"""
|
"""
|
||||||
@@ -429,7 +432,7 @@ class SubmissionsSheet(QTableView):
|
|||||||
# delete_submission(id=value)
|
# delete_submission(id=value)
|
||||||
sub = BasicSubmission.query(id=value)
|
sub = BasicSubmission.query(id=value)
|
||||||
fname = select_save_file(self, default_name=sub.to_pydantic().construct_filename(), extension="xlsx")
|
fname = select_save_file(self, default_name=sub.to_pydantic().construct_filename(), extension="xlsx")
|
||||||
sub.backup(fname=fname)
|
sub.backup(fname=fname, full_backup=False)
|
||||||
|
|
||||||
class SubmissionDetails(QDialog):
|
class SubmissionDetails(QDialog):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from PyQt6.QtWidgets import (
|
|||||||
)
|
)
|
||||||
from sqlalchemy import FLOAT, INTEGER
|
from sqlalchemy import FLOAT, INTEGER
|
||||||
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
||||||
from backend.db import SubmissionType, Equipment, SubmissionTypeEquipmentAssociation, BasicSubmission
|
from backend.db import SubmissionType, Equipment, SubmissionTypeEquipmentRoleAssociation, BasicSubmission
|
||||||
from backend.validators import PydReagentType, PydKit
|
from backend.validators import PydReagentType, PydKit
|
||||||
import logging
|
import logging
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
|
|||||||
@@ -315,6 +315,8 @@ class SubmissionFormContainer(QWidget):
|
|||||||
logger.debug(f"Here is the final submission: {pformat(base_submission.__dict__)}")
|
logger.debug(f"Here is the final submission: {pformat(base_submission.__dict__)}")
|
||||||
logger.debug(f"Parsed reagents: {pformat(base_submission.reagents)}")
|
logger.debug(f"Parsed reagents: {pformat(base_submission.reagents)}")
|
||||||
logger.debug(f"Sending submission: {base_submission.rsl_plate_num} to database.")
|
logger.debug(f"Sending submission: {base_submission.rsl_plate_num} to database.")
|
||||||
|
logger.debug(f"Samples from pyd: {pformat(self.pyd.samples)}")
|
||||||
|
logger.debug(f"Samples SQL: {pformat([item.__dict__ for item in base_submission.samples])}")
|
||||||
base_submission.save()
|
base_submission.save()
|
||||||
# update summary sheet
|
# update summary sheet
|
||||||
self.app.table_widget.sub_wid.setData()
|
self.app.table_widget.sub_wid.setData()
|
||||||
@@ -428,8 +430,8 @@ class SubmissionFormWidget(QWidget):
|
|||||||
# self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
|
# self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
|
||||||
# "qt_scrollarea_vcontainer", "submit_btn"
|
# "qt_scrollarea_vcontainer", "submit_btn"
|
||||||
# ]
|
# ]
|
||||||
self.ignore = ['filepath', 'samples', 'reagents', 'csv', 'ctx', 'comment']
|
self.ignore = ['filepath', 'samples', 'reagents', 'csv', 'ctx', 'comment', 'equipment']
|
||||||
self.recover = ['filepath', 'samples', 'csv', 'comment']
|
self.recover = ['filepath', 'samples', 'csv', 'comment', 'equipment']
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
for k, v in kwargs.items():
|
for k, v in kwargs.items():
|
||||||
if k not in self.ignore:
|
if k not in self.ignore:
|
||||||
@@ -475,8 +477,11 @@ class SubmissionFormWidget(QWidget):
|
|||||||
logger.debug(f"Reagents: {pformat(reagents)}")
|
logger.debug(f"Reagents: {pformat(reagents)}")
|
||||||
# logger.debug(f"Attrs not in info: {[k for k, v in self.__dict__.items() if k not in info.keys()]}")
|
# logger.debug(f"Attrs not in info: {[k for k, v in self.__dict__.items() if k not in info.keys()]}")
|
||||||
for item in self.recover:
|
for item in self.recover:
|
||||||
|
logger.debug(f"Attempting to recover: {item}")
|
||||||
if hasattr(self, item):
|
if hasattr(self, item):
|
||||||
info[item] = getattr(self, item)
|
value = getattr(self, item)
|
||||||
|
logger.debug(f"Setting {item}")
|
||||||
|
info[item] = value
|
||||||
# app = self.parent().parent().parent().parent().parent().parent().parent().parent
|
# app = self.parent().parent().parent().parent().parent().parent().parent().parent
|
||||||
# submission = PydSubmission(filepath=self.filepath, reagents=reagents, samples=self.samples, **info)
|
# submission = PydSubmission(filepath=self.filepath, reagents=reagents, samples=self.samples, **info)
|
||||||
submission = PydSubmission(reagents=reagents, **info)
|
submission = PydSubmission(reagents=reagents, **info)
|
||||||
@@ -728,6 +733,8 @@ class ReagentFormWidget(QWidget):
|
|||||||
looked_up_reg = Reagent.query(lot_number=looked_up_rt.last_used)
|
looked_up_reg = Reagent.query(lot_number=looked_up_rt.last_used)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
looked_up_reg = None
|
looked_up_reg = None
|
||||||
|
if isinstance(looked_up_reg, list):
|
||||||
|
looked_up_reg = None
|
||||||
logger.debug(f"Because there was no reagent listed for {reagent.lot}, we will insert the last lot used: {looked_up_reg}")
|
logger.debug(f"Because there was no reagent listed for {reagent.lot}, we will insert the last lot used: {looked_up_reg}")
|
||||||
if looked_up_reg != None:
|
if looked_up_reg != None:
|
||||||
relevant_reagents.remove(str(looked_up_reg.lot))
|
relevant_reagents.remove(str(looked_up_reg.lot))
|
||||||
|
|||||||
@@ -48,13 +48,13 @@
|
|||||||
{% if sub['equipment'] %}
|
{% if sub['equipment'] %}
|
||||||
<h3><u>Equipment:</u></h3>
|
<h3><u>Equipment:</u></h3>
|
||||||
<p>{% for item in sub['equipment'] %}
|
<p>{% for item in sub['equipment'] %}
|
||||||
<b>{{ item['name'] }}:</b> {{ item['asset_number']|replace('\n\t', '<br> ') }}<br>
|
<b>{{ item['role'] }}:</b> {{ item['name'] }}({{ item['asset_number'] }}): {{ item['process']|replace('\n\t', '<br> ') }}<br>
|
||||||
{% endfor %}</p>
|
{% endfor %}</p>
|
||||||
{% endif %}
|
{% 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'] %}
|
||||||
<b>{{ item['well'] }}:</b> {{ item['name']|replace('\n\t', '<br> ') }}<br>
|
<b>{{ item['well'] }}:</b> {% if item['organism'] %} {{ item['name'] }} - ({{ item['organism']|replace('\n\t', '<br> ') }}){% else %} {{ item['name']|replace('\n\t', '<br> ') }}{% endif %}<br>
|
||||||
{% endfor %}</p>
|
{% endfor %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if sub['controls'] %}
|
{% if sub['controls'] %}
|
||||||
|
|||||||
4
src/submissions/templates/tooltip.html
Normal file
4
src/submissions/templates/tooltip.html
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Sample name: {{ fields['submitter_id'] }}<br>
|
||||||
|
{% if fields['organism'] %}Organism: {{ fields['organism'] }}<br>{% endif %}
|
||||||
|
{% if fields['concentration'] %}Concentration: {{ fields['concentration'] }}<br>{% endif %}
|
||||||
|
Well: {{ fields['row'] }}{{ fields['column'] }}
|
||||||
@@ -45,8 +45,10 @@ def check_not_nan(cell_contents) -> bool:
|
|||||||
bool: True if cell has value, else, false.
|
bool: True if cell has value, else, false.
|
||||||
"""
|
"""
|
||||||
# check for nan as a string first
|
# check for nan as a string first
|
||||||
|
exclude = ['unnamed:', 'blank', 'void']
|
||||||
try:
|
try:
|
||||||
if "Unnamed:" in cell_contents or "blank" in cell_contents.lower():
|
# if "Unnamed:" in cell_contents or "blank" in cell_contents.lower():
|
||||||
|
if cell_contents.lower() in exclude:
|
||||||
cell_contents = np.nan
|
cell_contents = np.nan
|
||||||
cell_contents = cell_contents.lower()
|
cell_contents = cell_contents.lower()
|
||||||
except (TypeError, AttributeError):
|
except (TypeError, AttributeError):
|
||||||
|
|||||||
Reference in New Issue
Block a user