From b174eb1221f50895a23d8deff6178982c33a93ce Mon Sep 17 00:00:00 2001
From: lwark
Date: Wed, 11 Dec 2024 15:04:26 -0600
Subject: [PATCH] Added ability to not import reagents on first import.
---
CHANGELOG.md | 8 +++
src/submissions/__main__.py | 37 ++++++++++-
src/submissions/backend/db/__init__.py | 11 ++--
src/submissions/backend/db/models/__init__.py | 10 +++
src/submissions/backend/db/models/controls.py | 2 +-
src/submissions/backend/db/models/kits.py | 9 +--
.../backend/db/models/submissions.py | 3 +-
src/submissions/backend/excel/writer.py | 16 +++++
src/submissions/backend/scripts/__init__.py | 7 ++
src/submissions/backend/scripts/irida.py | 56 ++++++++++++++++
src/submissions/frontend/widgets/app.py | 2 +-
.../frontend/widgets/submission_details.py | 3 -
.../frontend/widgets/submission_widget.py | 64 +++++++++++++++++--
.../templates/basicsubmission_details.html | 4 +-
src/submissions/tools/__init__.py | 4 +-
15 files changed, 209 insertions(+), 27 deletions(-)
create mode 100644 src/submissions/backend/scripts/__init__.py
create mode 100644 src/submissions/backend/scripts/irida.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3c945e6..6ae2769 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+## 202412.03
+
+- Automated truncating of object names longer than 64 chars going into _auditlog
+- Writer will now blank out the lookup table before writing to ensure removal of extraneous help info.
+- Added support for running startup and teardown scripts.
+- Created startup script to pull irida controls from secondary database.
+- Added ability to not import reagents on first import.
+
## 202412.02
- Addition of turnaround time tracking
diff --git a/src/submissions/__main__.py b/src/submissions/__main__.py
index 47c02c9..39af6f8 100644
--- a/src/submissions/__main__.py
+++ b/src/submissions/__main__.py
@@ -1,17 +1,50 @@
import sys, os
from tools import ctx, setup_logger, check_if_app
+from backend import scripts
# environment variable must be set to enable qtwebengine in network path
if check_if_app():
os.environ['QTWEBENGINE_DISABLE_SANDBOX'] = "1"
+
# setup custom logger
logger = setup_logger(verbosity=3)
-# create settings object
from PyQt6.QtWidgets import QApplication
from frontend.widgets.app import App
+
+def run_startup():
+ try:
+ startup_scripts = ctx.startup_scripts
+ except AttributeError as e:
+ logger.error(f"Couldn't get startup scripts due to {e}")
+ return
+ for script in startup_scripts:
+ try:
+ func = getattr(scripts, script)
+ except AttributeError as e:
+ logger.error(f"Couldn't run startup script {script} due to {e}")
+ continue
+ func(ctx)
+
+
+def run_teardown():
+ try:
+ teardown_scripts = ctx.teardown_scripts
+ except AttributeError as e:
+ logger.error(f"Couldn't get teardown scripts due to {e}")
+ return
+ for script in teardown_scripts:
+ try:
+ func = getattr(scripts, script)
+ except AttributeError as e:
+ logger.error(f"Couldn't run teardown script {script} due to {e}")
+ continue
+ func(ctx)
+
if __name__ == '__main__':
+ run_startup()
app = QApplication(['', '--no-sandbox'])
ex = App(ctx=ctx)
- sys.exit(app.exec())
+ app.exec()
+ sys.exit(run_teardown())
diff --git a/src/submissions/backend/db/__init__.py b/src/submissions/backend/db/__init__.py
index 502da09..93eb7a6 100644
--- a/src/submissions/backend/db/__init__.py
+++ b/src/submissions/backend/db/__init__.py
@@ -37,10 +37,11 @@ from .models import *
def update_log(mapper, connection, target):
- logger.debug("\n\nBefore update\n\n")
+ # logger.debug("\n\nBefore update\n\n")
state = inspect(target)
# logger.debug(state)
- update = dict(user=getuser(), time=datetime.now(), object=str(state.object), changes=[])
+ object_name = state.object.truncated_name()
+ update = dict(user=getuser(), time=datetime.now(), object=object_name, changes=[])
# logger.debug(update)
for attr in state.attrs:
hist = attr.load_history()
@@ -49,8 +50,10 @@ def update_log(mapper, connection, target):
if attr.key == "custom":
continue
added = [str(item) for item in hist.added]
- if attr.key in ['submission_sample_associations', 'submission_reagent_associations']:
- added = ['Numbers truncated for space purposes.']
+ if attr.key in ['artic_technician', 'submission_sample_associations', 'submission_reagent_associations',
+ 'submission_equipment_associations', 'submission_tips_associations', 'contact_id', 'gel_info',
+ 'gel_controls', 'source_plates']:
+ continue
deleted = [str(item) for item in hist.deleted]
change = dict(field=attr.key, added=added, deleted=deleted)
# logger.debug(f"Adding: {pformat(change)}")
diff --git a/src/submissions/backend/db/models/__init__.py b/src/submissions/backend/db/models/__init__.py
index 8e4d07f..46f9250 100644
--- a/src/submissions/backend/db/models/__init__.py
+++ b/src/submissions/backend/db/models/__init__.py
@@ -25,6 +25,16 @@ logger = logging.getLogger(f"submissions.{__name__}")
class LogMixin(Base):
__abstract__ = True
+ def truncated_name(self):
+ name = str(self)
+ if len(name) > 64:
+ name = name.replace("<", "").replace(">", "")
+ if len(name) > 64:
+ name = name.replace("agent", "")
+ if len(name) > 64:
+ name = f"...{name[-61:]}"
+ return name
+
class BaseClass(Base):
"""
diff --git a/src/submissions/backend/db/models/controls.py b/src/submissions/backend/db/models/controls.py
index ac47238..d603580 100644
--- a/src/submissions/backend/db/models/controls.py
+++ b/src/submissions/backend/db/models/controls.py
@@ -539,7 +539,7 @@ class IridaControl(Control):
except AttributeError:
consolidate = False
report = Report()
- logger.debug(f"settings: {pformat(chart_settings)}")
+ # logger.debug(f"settings: {pformat(chart_settings)}")
controls = cls.query(subtype=chart_settings['sub_type'], start_date=chart_settings['start_date'],
end_date=chart_settings['end_date'])
# logger.debug(f"Controls found: {controls}")
diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py
index 2f18dd3..0c45d4f 100644
--- a/src/submissions/backend/db/models/kits.py
+++ b/src/submissions/backend/db/models/kits.py
@@ -427,9 +427,10 @@ class Reagent(BaseClass, LogMixin):
def __repr__(self):
if self.name:
- return f""
+ name = f""
else:
- return f""
+ name = f""
+ return name
def to_sub_dict(self, extraction_kit: KitType = None, full_data: bool = False, **kwargs) -> dict:
"""
@@ -1347,7 +1348,7 @@ class SubmissionReagentAssociation(BaseClass):
return PydReagent(**self.to_sub_dict(extraction_kit=extraction_kit))
-class Equipment(BaseClass):
+class Equipment(BaseClass, LogMixin):
"""
A concrete instance of equipment
"""
@@ -1851,7 +1852,7 @@ class TipRole(BaseClass):
super().save()
-class Tips(BaseClass):
+class Tips(BaseClass, LogMixin):
"""
A concrete instance of tips.
"""
diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py
index 6bbbe31..19e74ce 100644
--- a/src/submissions/backend/db/models/submissions.py
+++ b/src/submissions/backend/db/models/submissions.py
@@ -174,7 +174,8 @@ class BasicSubmission(BaseClass, LogMixin):
'platemap', 'export_map', 'equipment', 'tips', 'custom'],
# NOTE: Fields not placed in ui form
form_ignore=['reagents', 'ctx', 'id', 'cost', 'extraction_info', 'signed_by', 'comment', 'namer',
- 'submission_object', "tips", 'contact_phone', 'custom', 'cost_centre'] + recover,
+ 'submission_object', "tips", 'contact_phone', 'custom', 'cost_centre', 'completed_date',
+ 'controls'] + recover,
# NOTE: Fields not placed in ui form to be moved to pydantic
form_recover=recover
))
diff --git a/src/submissions/backend/excel/writer.py b/src/submissions/backend/excel/writer.py
index c13bf55..d41bcff 100644
--- a/src/submissions/backend/excel/writer.py
+++ b/src/submissions/backend/excel/writer.py
@@ -3,6 +3,7 @@ contains writer objects for pushing values to submission sheet templates.
"""
import logging
from copy import copy
+from datetime import date
from operator import itemgetter
from pprint import pformat
from typing import List, Generator, Tuple
@@ -214,6 +215,10 @@ class ReagentWriter(object):
Returns:
List[dict]: merged dictionary
"""
+ filled_roles = [item['role'] for item in reagent_list]
+ for map_obj in reagent_map.keys():
+ if map_obj not in filled_roles:
+ reagent_list.append(dict(name="Not Applicable", role=map_obj, lot="Not Applicable", expiry="Not Applicable"))
for reagent in reagent_list:
try:
mp_info = reagent_map[reagent['role']]
@@ -268,6 +273,7 @@ class SampleWriter(object):
# NOTE: exclude any samples without a submission rank.
samples = [item for item in self.reconcile_map(sample_list) if item['submission_rank'] > 0]
self.samples = sorted(samples, key=itemgetter('submission_rank'))
+ self.blank_lookup_table()
def reconcile_map(self, sample_list: list) -> Generator[dict, None, None]:
"""
@@ -291,6 +297,16 @@ class SampleWriter(object):
new[k] = v
yield new
+ def blank_lookup_table(self):
+ """
+ Blanks out columns in the lookup table to ensure help values are removed before writing.
+ """
+ sheet = self.xl[self.sample_map['sheet']]
+ for row in range(self.sample_map['start_row'], self.sample_map['end_row'] + 1):
+ for column in self.sample_map['sample_columns'].values():
+ if sheet.cell(row, column).data_type != 'f':
+ sheet.cell(row=row, column=column, value="")
+
def write_samples(self) -> Workbook:
"""
Performs writing operations.
diff --git a/src/submissions/backend/scripts/__init__.py b/src/submissions/backend/scripts/__init__.py
new file mode 100644
index 0000000..89b4971
--- /dev/null
+++ b/src/submissions/backend/scripts/__init__.py
@@ -0,0 +1,7 @@
+from .irida import import_irida
+
+def hello(ctx):
+ print("\n\nHello!\n\n")
+
+def goodbye(ctx):
+ print("\n\nGoodbye\n\n")
diff --git a/src/submissions/backend/scripts/irida.py b/src/submissions/backend/scripts/irida.py
new file mode 100644
index 0000000..f555dac
--- /dev/null
+++ b/src/submissions/backend/scripts/irida.py
@@ -0,0 +1,56 @@
+import logging, sqlite3, json
+from pprint import pformat, pprint
+from datetime import datetime
+from tools import Settings
+from backend import BasicSample
+from backend.db import IridaControl, ControlType
+
+logger = logging.getLogger(f"submissions.{__name__}")
+
+def import_irida(ctx:Settings):
+ """
+ Grabs Irida controls from secondary database.
+
+ Args:
+ ctx (Settings): Settings inherited from app.
+
+ """
+ ct = ControlType.query(name="Irida Control")
+ existing_controls = [item.name for item in IridaControl.query()]
+ prm_list = ", ".join([f"'{thing}'" for thing in existing_controls])
+ ctrl_db_path = ctx.directory_path.joinpath("submissions_parser_output", "submissions.db")
+ # print(f"Incoming settings: {pformat(ctx)}")
+ try:
+ conn = sqlite3.connect(ctrl_db_path)
+ except AttributeError as e:
+ print(f"Error, could not import from irida due to {e}")
+ return
+ sql = f"SELECT name, submitted_date, submission_id, contains, matches, kraken, subtype, refseq_version, " \
+ f"kraken2_version, kraken2_db_version, sample_id FROM _iridacontrol INNER JOIN _control on _control.id " \
+ f"= _iridacontrol.id WHERE _control.name NOT IN ({prm_list})"
+ cursor = conn.execute(sql)
+ records = [dict(name=row[0], submitted_date=row[1], submission_id=row[2], contains=row[3], matches=row[4], kraken=row[5],
+ subtype=row[6], refseq_version=row[7], kraken2_version=row[8], kraken2_db_version=row[9],
+ sample_id=row[10]) for row in cursor]
+ # incoming_controls = set(item['name'] for item in records)
+ # relevant = list(incoming_controls - existing_controls)
+ for record in records:
+ instance = IridaControl.query(name=record['name'])
+ if instance:
+ logger.warning(f"Irida Control {instance.name} already exists, skipping.")
+ continue
+ record['contains'] = json.loads(record['contains'])
+ assert isinstance(record['contains'], dict)
+ record['matches'] = json.loads(record['matches'])
+ assert isinstance(record['matches'], dict)
+ record['kraken'] = json.loads(record['kraken'])
+ assert isinstance(record['kraken'], dict)
+ record['submitted_date'] = datetime.strptime(record['submitted_date'], "%Y-%m-%d %H:%M:%S.%f")
+ assert isinstance(record['submitted_date'], datetime)
+ instance = IridaControl(controltype=ct, **record)
+ sample = BasicSample.query(submitter_id=instance.name)
+ if sample:
+ instance.sample = sample
+ instance.submission = sample.submissions[0]
+ # pprint(instance.__dict__)
+ instance.save()
\ No newline at end of file
diff --git a/src/submissions/frontend/widgets/app.py b/src/submissions/frontend/widgets/app.py
index 5167855..39cb739 100644
--- a/src/submissions/frontend/widgets/app.py
+++ b/src/submissions/frontend/widgets/app.py
@@ -27,7 +27,7 @@ from .turnaround import TurnaroundTime
from .omni_search import SearchBox
logger = logging.getLogger(f'submissions.{__name__}')
-logger.info("Hello, I am a logger")
+# logger.info("Hello, I am a logger")
class App(QMainWindow):
diff --git a/src/submissions/frontend/widgets/submission_details.py b/src/submissions/frontend/widgets/submission_details.py
index 60bda33..24474ac 100644
--- a/src/submissions/frontend/widgets/submission_details.py
+++ b/src/submissions/frontend/widgets/submission_details.py
@@ -64,9 +64,6 @@ class SubmissionDetails(QDialog):
self.reagent_details(reagent=sub)
self.webview.page().setWebChannel(self.channel)
- # def back_function(self):
- # self.webview.back()
-
def activate_export(self):
title = self.webview.title()
self.setWindowTitle(title)
diff --git a/src/submissions/frontend/widgets/submission_widget.py b/src/submissions/frontend/widgets/submission_widget.py
index a3f2564..0e4ec5b 100644
--- a/src/submissions/frontend/widgets/submission_widget.py
+++ b/src/submissions/frontend/widgets/submission_widget.py
@@ -3,9 +3,9 @@ Contains all submission related frontend functions
'''
from PyQt6.QtWidgets import (
QWidget, QPushButton, QVBoxLayout,
- QComboBox, QDateEdit, QLineEdit, QLabel
+ QComboBox, QDateEdit, QLineEdit, QLabel, QCheckBox, QBoxLayout, QHBoxLayout, QGridLayout
)
-from PyQt6.QtCore import pyqtSignal, Qt
+from PyQt6.QtCore import pyqtSignal, Qt, QSignalBlocker
from . import select_open_file, select_save_file
import logging
from pathlib import Path
@@ -228,9 +228,26 @@ class SubmissionFormWidget(QWidget):
# if k == "extraction_kit":
if k in self.__class__.update_reagent_fields:
add_widget.input.currentTextChanged.connect(self.scrape_reagents)
+ self.disabler = self.DisableReagents(self)
+ self.disabler.checkbox.setChecked(True)
+ self.layout.addWidget(self.disabler)
+ self.disabler.checkbox.checkStateChanged.connect(self.disable_reagents)
self.setStyleSheet(main_form_style)
self.scrape_reagents(self.extraction_kit)
+ def disable_reagents(self):
+ for reagent in self.findChildren(self.ReagentFormWidget):
+ # if self.disabler.checkbox.isChecked():
+ # # reagent.setVisible(True)
+ # # with QSignalBlocker(self.disabler.checkbox) as b:
+ # reagent.flip_check()
+ # else:
+ # # reagent.setVisible(False)
+ # # with QSignalBlocker(self.disabler.checkbox) as b:
+ # reagent.check.setChecked(False)
+ reagent.flip_check(self.disabler.checkbox.isChecked())
+
+
def create_widget(self, key: str, value: dict | PydReagent, submission_type: str | SubmissionType | None = None,
extraction_kit: str | None = None, sub_obj: BasicSubmission | None = None,
disable: bool = False) -> "self.InfoItem":
@@ -350,8 +367,9 @@ class SubmissionFormWidget(QWidget):
report.add_result(result)
# logger.debug(f"Submission: {pformat(self.pyd)}")
# logger.debug("Checking kit integrity...")
- _, result = self.pyd.check_kit_integrity()
- report.add_result(result)
+ if self.disabler.checkbox.isChecked():
+ _, result = self.pyd.check_kit_integrity()
+ report.add_result(result)
if len(result.results) > 0:
return
# logger.debug(f"PYD before transformation into SQL:\n\n{self.pyd}\n\n")
@@ -665,11 +683,15 @@ class SubmissionFormWidget(QWidget):
self.app = self.parent().parent().parent().parent().parent().parent().parent().parent()
self.reagent = reagent
self.extraction_kit = extraction_kit
- layout = QVBoxLayout()
+ layout = QGridLayout()
+ self.check = QCheckBox()
+ self.check.setChecked(True)
+ self.check.checkStateChanged.connect(self.disable)
+ layout.addWidget(self.check, 0, 0, 1, 1)
self.label = self.ReagentParsedLabel(reagent=reagent)
- layout.addWidget(self.label)
+ layout.addWidget(self.label, 0, 1, 1, 9)
self.lot = self.ReagentLot(scrollWidget=parent, reagent=reagent, extraction_kit=extraction_kit)
- layout.addWidget(self.lot)
+ layout.addWidget(self.lot, 1, 0, 1, 10)
# NOTE: Remove spacing between reagents
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
@@ -678,6 +700,20 @@ class SubmissionFormWidget(QWidget):
# NOTE: If changed set self.missing to True and update self.label
self.lot.currentTextChanged.connect(self.updated)
+ def flip_check(self, checked:bool):
+ with QSignalBlocker(self.check) as b:
+ self.check.setChecked(checked)
+ self.lot.setEnabled(checked)
+ self.label.setEnabled(checked)
+
+ def disable(self):
+ self.lot.setEnabled(self.check.isChecked())
+ self.label.setEnabled(self.check.isChecked())
+ if not any([item.lot.isEnabled() for item in self.parent().findChildren(self.__class__)]):
+ self.parent().disabler.checkbox.setChecked(False)
+ else:
+ self.parent().disabler.checkbox.setChecked(True)
+
def parse_form(self) -> Tuple[PydReagent | None, Report]:
"""
Pulls form info into PydReagent
@@ -686,6 +722,8 @@ class SubmissionFormWidget(QWidget):
Tuple[PydReagent, dict]: PydReagent and Report(?)
"""
report = Report()
+ if not self.lot.isEnabled():
+ return None, report
lot = self.lot.currentText()
# logger.debug(f"Using this lot for the reagent {self.reagent}: {lot}")
wanted_reagent = Reagent.query(lot=lot, role=self.reagent.role)
@@ -786,3 +824,15 @@ class SubmissionFormWidget(QWidget):
self.setObjectName(f"lot_{reagent.role}")
self.addItems(relevant_reagents)
self.setToolTip(f"Enter lot number for the reagent used for {reagent.role}")
+
+ class DisableReagents(QWidget):
+
+ def __init__(self, parent: QWidget):
+ super().__init__(parent)
+ self.app = self.parent().parent().parent().parent().parent().parent().parent().parent()
+ layout = QHBoxLayout()
+ self.label = QLabel("Import Reagents")
+ self.checkbox = QCheckBox()
+ layout.addWidget(self.label)
+ layout.addWidget(self.checkbox)
+ self.setLayout(layout)
diff --git a/src/submissions/templates/basicsubmission_details.html b/src/submissions/templates/basicsubmission_details.html
index 6d642ef..6ffb13a 100644
--- a/src/submissions/templates/basicsubmission_details.html
+++ b/src/submissions/templates/basicsubmission_details.html
@@ -17,12 +17,12 @@
{% if sub['custom'] %}{% for key, value in sub['custom'].items() %}
{{ key | replace("_", " ") | title }}: {{ value }}
{% endfor %}{% endif %}
-
+ {% if sub['reagents'] %}
Reagents:
{% for item in sub['reagents'] %}
{{ item['role'] }}: {{ item['lot'] }} (EXP: {{ item['expiry'] }})
{% endfor %}
-
+ {% endif %}
{% if sub['equipment'] %}
Equipment:
{% for item in sub['equipment'] %}
diff --git a/src/submissions/tools/__init__.py b/src/submissions/tools/__init__.py
index eff21b3..a11f595 100644
--- a/src/submissions/tools/__init__.py
+++ b/src/submissions/tools/__init__.py
@@ -418,13 +418,13 @@ class Settings(BaseSettings, extra="allow"):
super().__init__(*args, **kwargs)
self.set_from_db()
- pprint(f"User settings:\n{self.__dict__}")
+ # pprint(f"User settings:\n{self.__dict__}")
def set_from_db(self):
if 'pytest' in sys.modules:
output = dict(power_users=['lwark', 'styson', 'ruwang'])
else:
- print(f"Hello from database settings getter.")
+ # print(f"Hello from database settings getter.")
# print(self.__dict__)
session = self.database_session
metadata = MetaData()