Skip to content

Commit b36dd3e

Browse files
authored
Merge pull request #51 from DiamondLightSource/I04_1-165_holder_barcode_in_records_table
I04 1 165 holder barcode in records table
2 parents 9755de7 + 5d9b82d commit b36dd3e

File tree

7 files changed

+602
-136
lines changed

7 files changed

+602
-136
lines changed

dls_barcode/data_store/record.py

Lines changed: 27 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ class Record:
2929

3030
BAD_SYMBOLS = [EMPTY_SLOT_SYMBOL, NOT_FOUND_SLOT_SYMBOL]
3131

32-
def __init__(self, plate_type, barcodes, image_path, geometry, timestamp=0.0, id=0):
32+
def __init__(self, plate_type, holder_barcode, barcodes, image_path, geometry, timestamp=0.0, id=0):
3333
"""
3434
:param plate_type: the type of the sample holder plate (string)
35+
:param holder_barcode: the barcode of the holder plate
3536
:param barcodes: ordered array of strings giving the barcodes in each slot
3637
of the plate in order. Empty slots should be denoted by empty strings.
3738
:param image_path: the absolute path of the image.
@@ -43,8 +44,10 @@ def __init__(self, plate_type, barcodes, image_path, geometry, timestamp=0.0, id
4344
self.timestamp = float(timestamp)
4445
except ValueError:
4546
self.timestamp = 0.0
47+
4648
self.image_path = image_path
4749
self.plate_type = plate_type
50+
self.holder_barcode = holder_barcode
4851
self.barcodes = barcodes
4952
self.geometry = geometry
5053
self.id = str(id)
@@ -57,27 +60,25 @@ def __init__(self, plate_type, barcodes, image_path, geometry, timestamp=0.0, id
5760
# Generate timestamp and uid if none are supplied
5861
if timestamp == 0:
5962
self.timestamp = time.time()
63+
6064
if id == 0:
6165
self.id = str(uuid.uuid4())
6266

63-
# Separate Data and Time
67+
# Separate Date and Time
6468
dt = self._formatted_date().split(" ")
6569
self.date = dt[0]
6670
self.time = dt[1]
6771

6872
# Counts of numbers slots and barcodes
69-
self.num_slots = len(barcodes)
70-
self.num_empty_slots = len([b for b in barcodes if b == EMPTY_SLOT_SYMBOL])
71-
self.num_unread_slots = len([b for b in barcodes if b == NOT_FOUND_SLOT_SYMBOL])
73+
self.num_slots = len(self.barcodes)
74+
self.num_empty_slots = len([b for b in self.barcodes if b == EMPTY_SLOT_SYMBOL])
75+
self.num_unread_slots = len([b for b in self.barcodes if b == NOT_FOUND_SLOT_SYMBOL])
7276
self.num_valid_barcodes = self.num_slots - self.num_unread_slots - self.num_empty_slots
7377

7478
@staticmethod
75-
def from_plate(plate, second_plate, image_path):
76-
plate_type = second_plate.type
77-
geometry = second_plate.geometry()
78-
barcodes = plate.barcodes() + second_plate.barcodes()
79-
80-
return Record(plate_type=plate_type, barcodes=barcodes, image_path=image_path, geometry=geometry)
79+
def from_plate(holder_barcode, plate, image_path):
80+
return Record(plate_type=plate.type, holder_barcode=holder_barcode, barcodes=plate.barcodes(),
81+
image_path=image_path, geometry=plate.geometry())
8182

8283
@staticmethod
8384
def from_string(string):
@@ -89,21 +90,24 @@ def from_string(string):
8990
timestamp = items[Record.IND_TIMESTAMP] #used to convert into float twice
9091
image = items[Record.IND_IMAGE]
9192
plate_type = items[Record.IND_PLATE]
92-
barcodes = items[Record.IND_BARCODES].split(Record.BC_SEPARATOR)
93+
all_barcodes = items[Record.IND_BARCODES].split(Record.BC_SEPARATOR)
94+
holder_barcode = all_barcodes[0]
95+
pin_barcodes = all_barcodes[1:]
9396

9497
geo_class = Geometry.get_class(plate_type)
9598
geometry = geo_class.deserialize(items[Record.IND_GEOMETRY])
9699

97-
return Record(plate_type=plate_type, barcodes=barcodes, timestamp=timestamp,
100+
return Record(plate_type=plate_type, holder_barcode=holder_barcode, barcodes=pin_barcodes, timestamp=timestamp,
98101
image_path=image, id=id, geometry=geometry)
99102

100103
def to_csv_string(self):
101104
""" Converts a scan record object into a string that can be stored in a csv file.
102105
"""
103-
items = [0] * 3
104-
items[0] = str(self.id)
105-
items[1] = str(self._formatted_date())
106-
items[2] = Record.BC_SEPARATOR.join(self.barcodes)
106+
items = list()
107+
items.append(str(self.id))
108+
items.append(str(self._formatted_date()))
109+
items.append(self.holder_barcode)
110+
items.append(Record.BC_SEPARATOR.join(self.barcodes))
107111
return Record.BC_SEPARATOR.join(items)
108112

109113
def to_string(self):
@@ -115,32 +119,20 @@ def to_string(self):
115119
items[Record.IND_TIMESTAMP] = str(self.timestamp)
116120
items[Record.IND_IMAGE] = self.image_path
117121
items[Record.IND_PLATE] = self.plate_type
118-
items[Record.IND_BARCODES] = Record.BC_SEPARATOR.join(self.barcodes)
122+
items[Record.IND_BARCODES] = Record.BC_SEPARATOR.join(self._all_barcodes())
119123
items[Record.IND_GEOMETRY] = self.geometry.serialize()
120124
return Record.ITEM_SEPARATOR.join(items)
121125

122-
def any_barcode_matches(self, barcodes):
123-
""" Returns true if the record contains any barcode which is also
124-
contained in the specified list
125-
"""
126-
barcodes = [bc for bc in barcodes if bc not in Record.BAD_SYMBOLS]
127-
for bc in barcodes:
128-
if bc in self.barcodes:
129-
return True
130-
131-
return False
126+
def _all_barcodes(self):
127+
return [self.holder_barcode] + self.barcodes
132128

133-
#TODO: modify when two images merged
134-
# only the image of the top for the time being
135-
def image(self):
129+
def _image(self):
136130
image = Image.from_file(self.image_path)
137131
return image
138132

139-
#TODO: modify when two images merged
140-
# only the image of the top for the time being
141133
def marked_image(self, options):
142134
geo = self.geometry
143-
image = self.image()
135+
image = self._image()
144136

145137
if options.image_puck.value():
146138
geo.draw_plate(image, Color.Blue())
@@ -155,9 +147,7 @@ def marked_image(self, options):
155147

156148
# marking the top image
157149
def _draw_pins(self, image, geometry, options):
158-
top_barcodes = list(self.barcodes) #copy of the list
159-
top_barcodes.pop(0)# remove the first element (side barcode)
160-
for i, bc in enumerate(top_barcodes):
150+
for i, bc in enumerate(self.barcodes):
161151
if bc == NOT_FOUND_SLOT_SYMBOL:
162152
color = options.col_bad()
163153
elif bc == EMPTY_SLOT_SYMBOL:

dls_barcode/data_store/store.py

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ class Store:
88
""" Maintains a list of records of previous barcodes scans. Any changes (additions
99
or deletions) are automatically written to the backing file.
1010
"""
11-
def __init__(self, directory, options, file_manager):
11+
def __init__(self, directory, store_capacity, file_manager):
1212
""" Initializes a new instance of Store.
1313
"""
14-
self._options = options
14+
self._store_capacity = store_capacity
1515
self._directory = directory
1616
self._file_manager = file_manager
1717
self._file = os.path.join(directory, "store.txt")
@@ -52,57 +52,56 @@ def get_record(self, index):
5252
"""
5353
return self.records[index] if self.records else None
5454

55-
def add_record(self, plate, second_plate, holder_img, pins_img):
55+
def _add_record(self, holder_barcode, plate, holder_img, pins_img):
5656
""" Add a new record to the store and save to the backing file.
5757
"""
5858
merged_img = self._merge_holder_image_into_pins_image(holder_img, pins_img)
5959
guid = str(uuid.uuid4())
6060
filename = os.path.abspath(os.path.join(self._img_dir, guid + '.png'))
6161
merged_img.save_as(filename)
6262

63-
record = Record.from_plate(plate, second_plate, filename)
63+
record = Record.from_plate(holder_barcode, plate, filename)
6464

6565
self.records.append(record)
6666
self._process_change()
67-
self._truncate_record_list()
6867

69-
def merge_record(self, plate, second_plate, holder_img, pins_img):
70-
""" Create new record or replace existing record if it has the same barcodes as the most
68+
def merge_record(self, holder_barcode, plate, holder_img, pins_img):
69+
""" Create new record or replace existing record if it has the same holder barcode as the most
7170
recent record. Save to backing store. """
72-
#Checking only holder barcodes is sufficient
73-
if len(self.records) > 0 and self.records[0].any_barcode_matches(plate.barcodes()):
71+
if self.records and self.records[0].holder_barcode == holder_barcode:
7472
self.delete_records([self.records[0]])
7573

76-
self.add_record(plate, second_plate, holder_img, pins_img)
74+
self._add_record(holder_barcode, plate, holder_img, pins_img)
7775

78-
def delete_records(self, records):
76+
def delete_records(self, records_to_delete):
7977
""" Remove all of the records in the supplied list from the store and
8078
save changes to the backing file.
8179
"""
82-
for record in records:
80+
for record in records_to_delete:
8381
self.records.remove(record)
8482
if self._file_manager.is_file(record.image_path):
8583
self._file_manager.remove(record.image_path)
8684

8785
self._process_change()
8886

8987
def _truncate_record_list(self):
90-
number = self._options.store_capacity.value()
91-
number = max(number, 2)
88+
min_store_capacity = 2
89+
actual_store_capacity = max(self._store_capacity.value(), min_store_capacity)
9290

93-
if len(self.records) > number:
94-
to_delete = self.records[number:]
91+
if len(self.records) > actual_store_capacity:
92+
to_delete = self.records[actual_store_capacity:]
9593
self.delete_records(to_delete)
9694

9795
def _process_change(self):
9896
""" Sort the records and save to file.
9997
"""
10098
self._sort_records()
99+
self._truncate_record_list()
101100
self._to_file()
102101
self._to_csv_file()
103102

104103
def _sort_records(self):
105-
""" Sort the records in descending date order (must recent first).
104+
""" Sort the records in descending date order (most recent first).
106105
"""
107106
self.records.sort(reverse=True, key=lambda record: record.timestamp)
108107

@@ -126,5 +125,6 @@ def _merge_holder_image_into_pins_image(self, holder_img, pins_img):
126125
merged_img.paste(small_holder_img, 0, 0)
127126
return merged_img
128127

129-
130-
128+
def is_latest_holder_barcode(self, holder_barcode):
129+
latest_record = self.get_record(0)
130+
return latest_record is not None and holder_barcode == latest_record.holder_barcode

dls_barcode/gui/barcode_table.py

Lines changed: 48 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from PyQt4 import QtGui
66
from PyQt4.QtCore import Qt
7-
from PyQt4.QtGui import QGroupBox, QVBoxLayout, QHBoxLayout, QTableWidget
7+
from PyQt4.QtGui import QGroupBox, QVBoxLayout, QHBoxLayout
88

99
from dls_barcode.plate import NOT_FOUND_SLOT_SYMBOL, EMPTY_SLOT_SYMBOL
1010

@@ -15,16 +15,15 @@ class BarcodeTable(QGroupBox):
1515
def __init__(self, options):
1616
super(BarcodeTable, self).__init__()
1717

18-
self._barcodes = []
19-
2018
self._options = options
2119

2220
self.setTitle("Plate Barcodes")
2321
self._init_ui()
22+
self.clear()
2423

2524
def _init_ui(self):
26-
# Create record table - lists all the records in the store
27-
self._table = QTableWidget()
25+
# Plate being displayed
26+
self._plate_lbl = QtGui.QLabel()
2827

2928
# Create barcode table - lists all the barcodes in a record
3029
self._table = QtGui.QTableWidget()
@@ -35,70 +34,85 @@ def _init_ui(self):
3534
self._table.setHorizontalHeaderLabels(['Barcode'])
3635
self._table.setColumnWidth(0, 100)
3736

38-
3937
# Clipboard button - copy the selected barcodes to the clipboard
4038
self._btn_clipboard = QtGui.QPushButton('Copy To Clipboard')
4139
self._btn_clipboard.setToolTip('Copy barcodes for the selected record to the clipboard')
4240
self._btn_clipboard.resize(self._btn_clipboard.sizeHint())
43-
self._btn_clipboard.clicked.connect(self.copy_selected_to_clipboard)
44-
self._btn_clipboard.setEnabled(False)
41+
self._btn_clipboard.clicked.connect(self.copy_to_clipboard)
4542

4643
hbox = QHBoxLayout()
4744
hbox.setSpacing(10)
4845
hbox.addWidget(self._btn_clipboard)
4946
hbox.addStretch(1)
5047

5148
vbox = QVBoxLayout()
49+
vbox.addWidget(self._plate_lbl)
5250
vbox.addWidget(self._table)
5351
vbox.addLayout(hbox)
5452

5553
self.setLayout(vbox)
5654

57-
def populate(self, barcodes):
58-
""" Called when a new row is selected on the record table. Displays all of the
59-
barcodes from the selected record in the barcode table. By default, valid barcodes are
55+
def populate(self, holder_barcode, barcodes):
56+
""" Called when a new row is selected on the record table.
57+
"""
58+
self._holder_barcode = holder_barcode
59+
self._barcodes = barcodes[:]
60+
self._update_state()
61+
62+
def clear(self):
63+
self._holder_barcode = None
64+
self._barcodes = []
65+
self._update_state()
66+
67+
def _update_state(self):
68+
self._populate_table()
69+
self._update_button_state()
70+
self._update_plate_label()
71+
72+
def _populate_table(self):
73+
"""Displays all of the barcodes from the selected record in the barcode table. By default, valid barcodes are
6074
highlighted green, invalid barcodes are highlighted red, and empty slots are grey.
6175
"""
62-
num_slots = len(barcodes)
6376
self._table.clearContents()
64-
self._table.setRowCount(num_slots)
77+
self._table.setRowCount(len(self._barcodes))
6578

66-
for index, barcode in enumerate(barcodes):
67-
# Select appropriate background color
79+
for index, barcode in enumerate(self._barcodes):
6880
if barcode == NOT_FOUND_SLOT_SYMBOL:
69-
color = self._options.col_bad()
81+
cell_color = self._options.col_bad()
7082
elif barcode == EMPTY_SLOT_SYMBOL:
71-
color = self._options.col_empty()
83+
cell_color = self._options.col_empty()
7284
else:
73-
color = self._options.col_ok()
85+
cell_color = self._options.col_ok()
7486

75-
color.a = 192
87+
cell_color.a = 192
7688

7789
# Set table item
7890
barcode = QtGui.QTableWidgetItem(barcode)
79-
barcode.setBackgroundColor(color.to_qt())
91+
barcode.setBackgroundColor(cell_color.to_qt())
8092
barcode.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
8193
self._table.setItem(index, 0, barcode)
8294

83-
self._barcodes = barcodes[:]
84-
for i, barcode in enumerate(self._barcodes):
85-
if barcode in [NOT_FOUND_SLOT_SYMBOL, EMPTY_SLOT_SYMBOL]:
86-
self._barcodes[i] = ""
87-
88-
self._update_button_state()
89-
9095
def _update_button_state(self):
91-
if self._barcodes is None or len(self._barcodes) == 0:
92-
self._btn_clipboard.setEnabled(False)
93-
else:
94-
self._btn_clipboard.setEnabled(True)
96+
self._btn_clipboard.setEnabled(self._has_barcodes())
9597

96-
def copy_selected_to_clipboard(self):
98+
def copy_to_clipboard(self):
9799
""" Called when the copy to clipboard button is pressed. Copies the list/s of
98100
barcodes for the currently selected records to the clipboard so that the user
99101
can paste it elsewhere.
100102
"""
103+
clipboard_barcodes = [self._holder_barcode] + self._barcodes[:]
104+
for i, barcode in enumerate(clipboard_barcodes):
105+
if barcode in [NOT_FOUND_SLOT_SYMBOL, EMPTY_SLOT_SYMBOL]:
106+
clipboard_barcodes[i] = ""
107+
101108
sep = os.linesep
102-
if self._barcodes:
109+
if clipboard_barcodes:
103110
import pyperclip
104-
pyperclip.copy(sep.join(self._barcodes))
111+
pyperclip.copy(sep.join(clipboard_barcodes))
112+
113+
def _update_plate_label(self):
114+
text = "Plate : " + str(self._holder_barcode) if self._has_barcodes() else "Plate:"
115+
self._plate_lbl.setText(text)
116+
117+
def _has_barcodes(self):
118+
return self._barcodes is not None and len(self._barcodes) > 0

0 commit comments

Comments
 (0)