Improved previous sub finding.

This commit is contained in:
Landon Wark
2024-03-06 15:04:43 -06:00
parent 8b4b39f33e
commit b466cb61d2
14 changed files with 343 additions and 117 deletions

View File

@@ -1,3 +1,7 @@
## 202402.04
- Addition of comments to gel box.
## 202402.01
- Addition of gel box for Artic quality control.

View File

@@ -55,9 +55,9 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
# are written from script.py.mako
# output_encoding = utf-8
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\python\submissions\tests\test_assets\submissions-test.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-demo.db
sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\tests\test_assets\submissions-test.db
[post_write_hooks]

View File

@@ -0,0 +1,48 @@
"""adding source plates to Artic submission
Revision ID: fabf697c721d
Revises: 70426df72f80
Create Date: 2024-03-06 11:01:34.794411
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'fabf697c721d'
down_revision = '70426df72f80'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
# batch_op.create_unique_constraint("ssa_unique", ['id'])
with op.batch_alter_table('_wastewaterartic', schema=None) as batch_op:
batch_op.add_column(sa.Column('source_plates', sa.JSON(), nullable=True))
with op.batch_alter_table('_wastewaterassociation', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.INTEGER(),
nullable=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_wastewaterassociation', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.INTEGER(),
nullable=True)
with op.batch_alter_table('_wastewaterartic', schema=None) as batch_op:
batch_op.drop_column('source_plates')
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
# batch_op.drop_constraint("ssa_unique", type_='unique')
# ### end Alembic commands ###

View File

@@ -4,7 +4,7 @@ from pathlib import Path
# Version of the realpython-reader package
__project__ = "submissions"
__version__ = "202402.4b"
__version__ = "202403.1b"
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
__copyright__ = "2022-2024, Government of Canada"

View File

@@ -276,7 +276,7 @@ class BasicSubmission(BaseClass):
sample_list = self.hitpick_plate()
# logger.debug("Setting background colours")
for sample in sample_list:
if sample['positive']:
if sample['Positive']:
sample['background_color'] = "#f10f07"
else:
if "colour" in sample.keys():
@@ -288,7 +288,7 @@ class BasicSubmission(BaseClass):
for column in range(1, plate_columns+1):
for row in range(1, plate_rows+1):
try:
well = [item for item in sample_list if item['row'] == row and item['column']==column][0]
well = [item for item in sample_list if item['Row'] == row and item['Column']==column][0]
except IndexError:
well = dict(name="", row=row, column=column, background_color="#ffffff")
output_samples.append(well)
@@ -429,7 +429,8 @@ class BasicSubmission(BaseClass):
case "reagents":
new_dict[key] = [PydReagent(**reagent) for reagent in value]
case "samples":
new_dict[key] = [PydSample(**sample) for sample in dicto['samples']]
# samples = {k.lower().replace(" ", "_"):v for k,v in dicto['samples'].items()}
new_dict[key] = [PydSample(**{k.lower().replace(" ", "_"):v for k,v in sample.items()}) for sample in dicto['samples']]
case "equipment":
try:
new_dict[key] = [PydEquipment(**equipment) for equipment in dicto['equipment']]
@@ -1293,6 +1294,7 @@ class WastewaterArtic(BasicSubmission):
pcr_info = Column(JSON) #: unstructured output from pcr table logger or user(Artic)
gel_image = Column(String(64)) #: file name of gel image in zip file
gel_info = Column(JSON) #: unstructured data from gel.
source_plates = Column(JSON) #: wastewater plates that samples come from
__mapper_args__ = dict(polymorphic_identity="Wastewater Artic",
polymorphic_load="inline",
@@ -1328,12 +1330,33 @@ class WastewaterArtic(BasicSubmission):
output['gel_info'] = self.gel_info
output['gel_image'] = self.gel_image
output['dna_core_submission_number'] = self.dna_core_submission_number
output['source_plates'] = self.source_plates
return output
@classmethod
def get_abbreviation(cls) -> str:
return "AR"
@classmethod
def parse_info(cls, input_dict:dict, xl:pd.ExcelFile|None=None) -> dict:
"""
Update submission dictionary with type specific information
Args:
input_dict (dict): Input sample dictionary
xl (pd.ExcelFile): original xl workbook, used for child classes mostly
Returns:
dict: Updated sample dictionary
"""
input_dict = super().parse_info(input_dict)
df = xl.parse("First Strand List", header=None)
plates = []
for row in [8,9,10]:
plates.append(dict(plate=df.iat[row-1, 2], start_sample=df.iat[row-1, 3]))
input_dict['source_plates'] = plates
return input_dict
@classmethod
def parse_samples(cls, input_dict: dict) -> dict:
"""
@@ -1364,8 +1387,11 @@ class WastewaterArtic(BasicSubmission):
Returns:
str: output name
"""
"""
# Remove letters.
processed = re.sub(r"[A-Z]", "", input_str)
# Remove trailing '-' if any
processed = processed.strip("-")
try:
en_num = re.search(r"\-\d{1}$", processed).group()
processed = rreplace(processed, en_num, "")
@@ -1507,18 +1533,22 @@ class WastewaterArtic(BasicSubmission):
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)
try:
source_plates = [item['plate'] for item in info['source_plates']]
first_samples = [item['start_sample'] for item in info['source_plates']]
except:
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 += ['None'] * (3 - len(source_plates))
first_samples += [''] * (3 - len(first_samples))
@@ -1573,7 +1603,7 @@ class WastewaterArtic(BasicSubmission):
Tuple[dict, Template]: (Updated dictionary, Template to be rendered)
"""
base_dict, template = super().get_details_template(base_dict=base_dict)
base_dict['excluded'] += ['gel_info', 'gel_image', 'headers', "dna_core_submission_number"]
base_dict['excluded'] += ['gel_info', 'gel_image', 'headers', "dna_core_submission_number", "source_plates"]
base_dict['DNA Core ID'] = base_dict['dna_core_submission_number']
check = 'gel_info' in base_dict.keys() and base_dict['gel_info'] != None
if check:
@@ -1598,20 +1628,20 @@ class WastewaterArtic(BasicSubmission):
List[dict]: Updated dictionaries
"""
logger.debug(f"Hello from {self.__class__.__name__} dictionary sample adjuster.")
if backup:
output = []
for assoc in self.submission_sample_associations:
dicto = assoc.to_sub_dict()
old_sub = assoc.sample.get_previous_ww_submission(current_artic_submission=self)
try:
dicto['plate_name'] = old_sub.rsl_plate_num
except AttributeError:
dicto['plate_name'] = ""
old_assoc = WastewaterAssociation.query(submission=old_sub, sample=assoc.sample, limit=1)
dicto['well'] = f"{row_map[old_assoc.row]}{old_assoc.column}"
output.append(dicto)
else:
output = super().adjust_to_dict_samples(backup=False)
# if backup:
output = []
for assoc in self.submission_sample_associations:
dicto = assoc.to_sub_dict()
old_sub = assoc.sample.get_previous_ww_submission(current_artic_submission=self)
try:
dicto['plate_name'] = old_sub.rsl_plate_num
except AttributeError:
dicto['plate_name'] = ""
old_assoc = WastewaterAssociation.query(submission=old_sub, sample=assoc.sample, limit=1)
dicto['well'] = f"{row_map[old_assoc.row]}{old_assoc.column}"
output.append(dicto)
# else:
# output = super().adjust_to_dict_samples(backup=False)
return output
def custom_context_events(self) -> dict:
@@ -1637,9 +1667,15 @@ class WastewaterArtic(BasicSubmission):
fname = select_open_file(obj=obj, file_extension="jpg")
dlg = GelBox(parent=obj, img_path=fname)
if dlg.exec():
self.dna_core_submission_number, img_path, output = dlg.parse_form()
self.dna_core_submission_number, img_path, output, comment = dlg.parse_form()
self.gel_image = img_path.name
self.gel_info = output
dt = datetime.strftime(datetime.now(), "%Y-%m-%d %H:%M:%S")
com = dict(text=comment, name=getuser(), time=dt)
if self.comment is not None:
self.comment.append(com)
else:
self.comment = [com]
logger.debug(pformat(self.gel_info))
with ZipFile(self.__directory_path__.joinpath("submission_imgs.zip"), 'a') as zipf:
# Add a file located at the source_path to the destination within the zip
@@ -1703,7 +1739,7 @@ class BasicSample(BaseClass):
except AttributeError:
return f"<Sample({self.submitter_id})"
def to_sub_dict(self) -> dict:
def to_sub_dict(self, full_data:bool=False) -> dict:
"""
gui friendly dictionary, extends parent method.
@@ -1712,8 +1748,10 @@ class BasicSample(BaseClass):
"""
# logger.debug(f"Converting {self} to dict.")
sample = {}
sample['submitter_id'] = self.submitter_id
sample['sample_type'] = self.sample_type
sample['Submitter ID'] = self.submitter_id
sample['Sample Type'] = self.sample_type
if full_data:
sample['submissions'] = [item.to_sub_dict() for item in self.sample_submission_associations]
return sample
def set_attribute(self, name:str, value):
@@ -1797,6 +1835,41 @@ class BasicSample(BaseClass):
"""
return input_dict
@classmethod
def get_details_template(cls, base_dict:dict) -> Tuple[dict, Template]:
"""
Get the details jinja template for the correct class
Args:
base_dict (dict): incoming dictionary of Submission fields
Returns:
Tuple(dict, Template): (Updated dictionary, Template to be rendered)
"""
base_dict['excluded'] = ['submissions', 'excluded']
env = jinja_template_loading()
temp_name = f"{cls.__name__.lower()}_details.html"
logger.debug(f"Returning template: {temp_name}")
try:
template = env.get_template(temp_name)
except TemplateNotFound as e:
logger.error(f"Couldn't find template {e}")
template = env.get_template("basicsample_details.html")
return base_dict, template
def show_details(self, obj):
"""
Creates Widget for showing sample details.
Args:
obj (_type_): parent widget
"""
logger.debug("Hello from details")
from frontend.widgets.sample_details import SampleDetails
dlg = SampleDetails(parent=obj, samp=self)
if dlg.exec():
pass
@classmethod
@setup_lookup
def query(cls,
@@ -1896,18 +1969,18 @@ class WastewaterSample(BasicSample):
polymorphic_load="inline",
inherit_condition=(id == BasicSample.id))
def to_sub_dict(self) -> dict:
def to_sub_dict(self, full_data:bool=False) -> dict:
"""
gui friendly dictionary, extends parent method.
Returns:
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
"""
sample = super().to_sub_dict()
sample['ww_processing_num'] = self.ww_processing_num
sample['sample_location'] = self.sample_location
sample['received_date'] = self.received_date
sample['collection_date'] = self.collection_date
sample = super().to_sub_dict(full_data=full_data)
sample['WW Processing Number'] = self.ww_processing_num
sample['Sample Location'] = self.sample_location
sample['Received Date'] = self.received_date
sample['Collection Date'] = self.collection_date
return sample
@classmethod
@@ -1944,9 +2017,14 @@ class WastewaterSample(BasicSample):
def get_previous_ww_submission(self, current_artic_submission:WastewaterArtic):
# assocs = [assoc for assoc in self.sample_submission_associations if assoc.submission.submission_type_name=="Wastewater"]
subs = self.submissions[:self.submissions.index(current_artic_submission)]
subs = [sub for sub in subs if sub.submission_type_name=="Wastewater"]
logger.debug(f"Submissions up to current artic submission: {subs}")
# subs = self.submissions[:self.submissions.index(current_artic_submission)]
try:
plates = [item['plate'] for item in current_artic_submission.source_plates]
except TypeError as e:
logger.error(f"source_plates must not be present")
plates = [item.rsl_plate_num for item in self.submissions[:self.submissions.index(current_artic_submission)]]
subs = [sub for sub in self.submissions if sub.rsl_plate_num in plates]
logger.debug(f"Submissions: {subs}")
try:
return subs[-1]
except IndexError:
@@ -1964,17 +2042,17 @@ class BacterialCultureSample(BasicSample):
polymorphic_load="inline",
inherit_condition=(id == BasicSample.id))
def to_sub_dict(self) -> dict:
def to_sub_dict(self, full_data:bool=False) -> dict:
"""
gui friendly dictionary, extends parent method.
Returns:
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
"""
sample = super().to_sub_dict()
sample['name'] = self.submitter_id
sample['organism'] = self.organism
sample['concentration'] = self.concentration
sample = super().to_sub_dict(full_data=full_data)
sample['Name'] = self.submitter_id
sample['Organism'] = self.organism
sample['Concentration'] = self.concentration
if self.control != None:
sample['colour'] = [0,128,0]
sample['tooltip'] = f"Control: {self.control.controltype.name} - {self.control.controltype.targets}"
@@ -2038,16 +2116,16 @@ class SubmissionSampleAssociation(BaseClass):
# Get sample info
# logger.debug(f"Running {self.__repr__()}")
sample = self.sample.to_sub_dict()
sample['name'] = self.sample.submitter_id
sample['row'] = self.row
sample['column'] = self.column
sample['Name'] = self.sample.submitter_id
sample['Row'] = self.row
sample['Column'] = self.column
try:
sample['well'] = f"{row_map[self.row]}{self.column}"
sample['Well'] = f"{row_map[self.row]}{self.column}"
except KeyError as e:
logger.error(f"Unable to find row {self.row} in row_map.")
sample['well'] = None
sample['plate_name'] = self.submission.rsl_plate_num
sample['positive'] = False
sample['Well'] = None
sample['Plate Name'] = self.submission.rsl_plate_num
sample['Positive'] = False
return sample
def to_hitpick(self) -> dict|None:

View File

@@ -225,6 +225,7 @@ class PydEquipment(BaseModel, extra='ignore'):
@classmethod
def make_empty_list(cls, value):
# logger.debug(f"Pydantic value: {value}")
value = convert_nans_to_nones(value)
if value == None:
value = ['']
if len(value)==0:

View File

@@ -2,7 +2,8 @@
Gel box for artic quality control
"""
from PyQt6.QtWidgets import (QWidget, QDialog, QGridLayout,
QLabel, QLineEdit, QDialogButtonBox
QLabel, QLineEdit, QDialogButtonBox,
QTextEdit
)
import numpy as np
import pyqtgraph as pg
@@ -44,7 +45,8 @@ class GelBox(QDialog):
# creating image view object
self.imv = pg.ImageView()
img = np.array(Image.open(self.img_path).rotate(-90).transpose(Image.FLIP_LEFT_RIGHT))
self.imv.setImage(img)#, xvals=np.linspace(1., 3., data.shape[0]))
self.imv.setImage(img, scale=None)#, xvals=np.linspace(1., 3., data.shape[0]))
layout = QGridLayout()
layout.addWidget(QLabel("DNA Core Submission Number"),0,1)
self.core_number = QLineEdit()
@@ -59,7 +61,7 @@ class GelBox(QDialog):
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
layout.addWidget(self.buttonBox, 22, 5, 1, 1)#, alignment=Qt.AlignmentFlag.AlignTop)
layout.addWidget(self.buttonBox, 23, 1, 1, 1)#, alignment=Qt.AlignmentFlag.AlignTop)
self.setLayout(layout)
def parse_form(self) -> Tuple[str, str|Path, list]:
@@ -70,14 +72,14 @@ class GelBox(QDialog):
Tuple[str, str|Path, list]: output values
"""
dna_core_submission_number = self.core_number.text()
return dna_core_submission_number, self.img_path, self.form.parse_form()
values, comment = self.form.parse_form()
return dna_core_submission_number, self.img_path, values, comment
class ControlsForm(QWidget):
def __init__(self, parent) -> None:
super().__init__(parent)
self.layout = QGridLayout()
columns = []
rows = []
for iii, item in enumerate(["Negative Control Key", "Description", "Results - 65 C", "Results - 63 C", "Results - Spike"]):
@@ -98,6 +100,11 @@ class ControlsForm(QWidget):
widge.setText("Neg")
widge.setObjectName(f"{rows[iii]} : {columns[jjj]}")
self.layout.addWidget(widge, iii+1, jjj+2, 1, 1)
self.layout.addWidget(QLabel("Comments:"), 0,5,1,1)
self.comment_field = QTextEdit(self)
self.comment_field.setFixedHeight(50)
self.layout.addWidget(self.comment_field, 1,5,4,1)
self.setLayout(self.layout)
def parse_form(self) -> List[dict]:
@@ -118,4 +125,4 @@ class ControlsForm(QWidget):
if label[0] not in [item['name'] for item in output]:
output.append(dicto)
logger.debug(pformat(output))
return output
return output, self.comment_field.toPlainText()

View File

@@ -1,8 +1,10 @@
from PyQt6.QtWidgets import (QDialog, QScrollArea, QPushButton, QVBoxLayout, QMessageBox,
QDialogButtonBox, QTextEdit)
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtCore import Qt
from backend.db.models import BasicSubmission
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtCore import Qt, pyqtSlot
from backend.db.models import BasicSubmission, BasicSample
from tools import check_if_app
from .functions import select_save_file
from io import BytesIO
@@ -31,7 +33,7 @@ class SubmissionDetails(QDialog):
self.app = parent.parent().parent().parent().parent().parent().parent()
except AttributeError:
self.app = None
self.setWindowTitle("Submission Details")
self.setWindowTitle(f"Submission Details - {sub.rsl_plate_num}")
# create scrollable interior
interior = QScrollArea()
interior.setParent(self)
@@ -46,19 +48,34 @@ class SubmissionDetails(QDialog):
self.base_dict['platemap'] = sub.make_plate_map()
self.base_dict, self.template = sub.get_details_template(base_dict=self.base_dict)
self.html = self.template.render(sub=self.base_dict)
webview = QWebEngineView()
webview.setMinimumSize(900, 500)
webview.setMaximumSize(900, 500)
webview.setHtml(self.html)
self.webview = QWebEngineView(parent=self)
self.webview.setMinimumSize(900, 500)
self.webview.setMaximumSize(900, 500)
self.webview.setHtml(self.html)
self.layout = QVBoxLayout()
interior.resize(900, 500)
interior.setWidget(webview)
interior.setWidget(self.webview)
self.setFixedSize(900, 500)
# button to export a pdf version
btn = QPushButton("Export PDF")
btn.setParent(self)
btn.setFixedWidth(900)
btn.clicked.connect(self.export)
# setup channel
self.channel = QWebChannel()
self.channel.registerObject('backend', self)
self.webview.page().setWebChannel(self.channel)
@pyqtSlot(str)
def sample_details(self, sample):
# print(f"{string} is in row {row}, column {column}")
# self.webview.setHtml(f"<html><body><br><br>{sample}</body></html>")
sample = BasicSample.query(submitter_id=sample)
base_dict = sample.to_sub_dict(full_data=True)
base_dict, template = sample.get_details_template(base_dict=base_dict)
html = template.render(sample=base_dict)
self.webview.setHtml(html)
# sample.show_details(obj=self)
def export(self):
"""
@@ -130,4 +147,4 @@ class SubmissionComment(QDialog):
full_comment = [{"name":commenter, "time": dt, "text": comment}]
logger.debug(f"Full comment: {full_comment}")
return full_comment

View File

@@ -103,6 +103,7 @@ class SubmissionsSheet(QTableView):
Args:
event (_type_): the item of interest
"""
# logger.debug(event().__dict__)
id = self.selectionModel().currentIndex()
id = id.sibling(id.row(),0).data()
submission = BasicSubmission.query(id=id)

View File

@@ -0,0 +1,54 @@
<!doctype html>
<html>
<head>
{% block head %}
<style>
/* Tooltip container */
.tooltip {
position: relative;
display: inline-block;
border-bottom: 1px dotted black; /* If you want dots under the hoverable text */
}
/* Tooltip text */
.tooltip .tooltiptext {
visibility: hidden;
width: 120px;
background-color: black;
color: #fff;
text-align: center;
padding: 5px 0;
border-radius: 6px;
/* Position the tooltip text - see examples below! */
position: absolute;
z-index: 1;
bottom: 100%;
left: 50%;
margin-left: -60px;
}
/* Show the tooltip text when you mouse over the tooltip container */
.tooltip:hover .tooltiptext {
visibility: visible;
font-size: large;
}
</style>
<title>Sample Details for {{ sample['Submitter ID'] }}</title>
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
{% endblock %}
</head>
<body>
{% block body %}
<h2><u>Sample Details for {{ sample['Submitter ID'] }}</u></h2>
<p>{% for key, value in sample.items() if key not in sample['excluded'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key }}: </b>{{ value }}<br>
{% endfor %}</p>
{% if sample['submissions'] %}<h2>Submissions:</h2>
{% for submission in sample['submissions'] %}
<p>{{ submission['Plate Name'] }}: {{ submission['Well'] }}</p>
{% endfor %}
{% endif %}
{% endblock %}
</body>
</html>

View File

@@ -35,6 +35,7 @@
}
</style>
<title>Submission Details for {{ sub['Plate Number'] }}</title>
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
{% endblock %}
</head>
<body>
@@ -57,7 +58,7 @@
{% if sub['samples'] %}
<h3><u>Samples:</u></h3>
<p>{% for item in sub['samples'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['well'] }}:</b> {% if item['organism'] %} {{ item['name'] }} - ({{ item['organism']|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}){% else %} {{ item['name']|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}{% endif %}<br>
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['Well'] }}:</b> {% if item['Organism'] %} {{ item['Name'] }} - ({{ item['Organism']|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}){% else %} {{ item['Name']|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}{% endif %}<br>
{% endfor %}</p>
{% endif %}
{% if sub['controls'] %}
@@ -116,4 +117,15 @@
{% endif %}
{% endblock %}
</body>
<script>
var backend;
new QWebChannel(qt.webChannelTransport, function (channel) {
backend = channel.objects.backend;
});
{% for sample in sub['samples'] %}
document.getElementById("{{sample['Submitter ID']}}").addEventListener("dblclick", function(){
backend.sample_details("{{ sample['Submitter ID'] }}");
});
{% endfor %}
</script>
</html>

View File

@@ -1,15 +1,15 @@
<div class="gallery" style="display: grid;grid-template-columns: repeat({{ PLATE_COLUMNS }}, 7.5vw);grid-template-rows: repeat({{ PLATE_ROWS }}, 7.5vw);grid-gap: 2px;">
{% for sample in samples %}
<div class="well" style="background-color: {{sample['background_color']}};
<div class="well" id="{{sample['Submitter ID']}}" style="background-color: {{sample['background_color']}};
border: 1px solid #000;
padding: 20px;
grid-column-start: {{sample['column']}};
grid-column-end: {{sample['column']}};
grid-row-start: {{sample['row']}};
grid-row-end: {{sample['row']}};
grid-column-start: {{sample['Column']}};
grid-column-end: {{sample['Column']}};
grid-row-start: {{sample['Row']}};
grid-row-end: {{sample['Row']}};
display: flex;
">
<div class="tooltip" style="font-size: 0.5em; text-align: center; word-wrap: break-word;">{{ sample['name'] }}
<div class="tooltip" style="font-size: 0.5em; text-align: center; word-wrap: break-word;">{{ sample['Name'] }}
<span class="tooltiptext">{{ sample['tooltip'] }}</span>
</div>
</div>

View File

@@ -1,4 +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['well'] }}<!--{{ fields['column'] }}-->
Sample name: {{ fields['Submitter ID'] }}<br>
{% if fields['Organism'] %}Organism: {{ fields['Organism'] }}<br>{% endif %}
{% if fields['Concentration'] %}Concentration: {{ fields['Concentration'] }}<br>{% endif %}
Well: {{ fields['Well'] }}<!--{{ fields['column'] }}-->

View File

@@ -116,37 +116,7 @@ def check_regex_match(pattern:str, check:str) -> bool:
except TypeError:
return False
class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler):
def doRollover(self):
"""
Override base class method to make the new log file group writable.
"""
# Rotate the file first.
handlers.RotatingFileHandler.doRollover(self)
# Add group write to the current permissions.
currMode = os.stat(self.baseFilename).st_mode
os.chmod(self.baseFilename, currMode | stat.S_IWGRP)
def _open(self):
prevumask=os.umask(0o002)
rtv=handlers.RotatingFileHandler._open(self)
os.umask(prevumask)
return rtv
class StreamToLogger(object):
"""
Fake file-like stream object that redirects writes to a logger instance.
"""
def __init__(self, logger, log_level=logging.INFO):
self.logger = logger
self.log_level = log_level
self.linebuf = ''
def write(self, buf):
for line in buf.rstrip().splitlines():
self.logger.log(self.log_level, line.rstrip())
# Settings
class Settings(BaseSettings):
"""
@@ -303,6 +273,26 @@ def get_config(settings_path: Path|str|None=None) -> Settings:
settings = yaml.load(stream, Loader=yaml.Loader)
return Settings(**settings)
# Logging formatters
class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler):
def doRollover(self):
"""
Override base class method to make the new log file group writable.
"""
# Rotate the file first.
handlers.RotatingFileHandler.doRollover(self)
# Add group write to the current permissions.
currMode = os.stat(self.baseFilename).st_mode
os.chmod(self.baseFilename, currMode | stat.S_IWGRP)
def _open(self):
prevumask=os.umask(0o002)
rtv=handlers.RotatingFileHandler._open(self)
os.umask(prevumask)
return rtv
class CustomFormatter(logging.Formatter):
grey = "\x1b[38;20m"
@@ -326,6 +316,20 @@ class CustomFormatter(logging.Formatter):
formatter = logging.Formatter(log_fmt)
return formatter.format(record)
class StreamToLogger(object):
"""
Fake file-like stream object that redirects writes to a logger instance.
"""
def __init__(self, logger, log_level=logging.INFO):
self.logger = logger
self.log_level = log_level
self.linebuf = ''
def write(self, buf):
for line in buf.rstrip().splitlines():
self.logger.log(self.log_level, line.rstrip())
def setup_logger(verbosity:int=3):
"""
Set logger levels using settings.