Skip to content

Commit 3e39c70

Browse files
committed
I04_1-165: store and record handle holder barcode separately from pin barcodes. Added unit tests for record and store
1 parent a50ae5d commit 3e39c70

File tree

5 files changed

+480
-50
lines changed

5 files changed

+480
-50
lines changed

dls_barcode/data_store/record.py

Lines changed: 29 additions & 28 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,29 +119,28 @@ 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

122126
def any_barcode_matches(self, barcodes):
123127
""" Returns true if the record contains any barcode which is also
124128
contained in the specified list
125129
"""
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:
130+
valid_barcodes = [bc for bc in barcodes if bc not in Record.BAD_SYMBOLS]
131+
for bc in valid_barcodes:
132+
if bc in self._all_barcodes():
129133
return True
130134

131135
return False
132136

133-
#TODO: modify when two images merged
134-
# only the image of the top for the time being
137+
def _all_barcodes(self):
138+
return [self.holder_barcode] + self.barcodes
139+
135140
def image(self):
136141
image = Image.from_file(self.image_path)
137142
return image
138143

139-
#TODO: modify when two images merged
140-
# only the image of the top for the time being
141144
def marked_image(self, options):
142145
geo = self.geometry
143146
image = self.image()
@@ -155,9 +158,7 @@ def marked_image(self, options):
155158

156159
# marking the top image
157160
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):
161+
for i, bc in enumerate(self.barcodes):
161162
if bc == NOT_FOUND_SLOT_SYMBOL:
162163
color = options.col_bad()
163164
elif bc == EMPTY_SLOT_SYMBOL:

dls_barcode/data_store/store.py

Lines changed: 12 additions & 13 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,42 +52,41 @@ 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()
6767
self._truncate_record_list()
6868

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
69+
def merge_record(self, holder_barcode, plate, holder_img, pins_img):
70+
""" Create new record or replace existing record if it has the same holder barcode as the most
7171
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()):
72+
if self.records and self.records[0].holder_barcode == holder_barcode:
7473
self.delete_records([self.records[0]])
7574

76-
self.add_record(plate, second_plate, holder_img, pins_img)
75+
self._add_record(holder_barcode, plate, holder_img, pins_img)
7776

78-
def delete_records(self, records):
77+
def delete_records(self, records_to_delete):
7978
""" Remove all of the records in the supplied list from the store and
8079
save changes to the backing file.
8180
"""
82-
for record in records:
81+
for record in records_to_delete:
8382
self.records.remove(record)
8483
if self._file_manager.is_file(record.image_path):
8584
self._file_manager.remove(record.image_path)
8685

8786
self._process_change()
8887

8988
def _truncate_record_list(self):
90-
number = self._options.store_capacity.value()
89+
number = self._store_capacity.value()
9190
number = max(number, 2)
9291

9392
if len(self.records) > number:
@@ -102,7 +101,7 @@ def _process_change(self):
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

dls_barcode/gui/record_table.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ def _init_ui(self, to_run_on_table_clicked):
6767

6868
self.setLayout(vbox)
6969

70-
def add_record_frame(self, plate, second_plate, holder_img, pins_img):
70+
def add_record_frame(self, holder_barcode, plate, holder_img, pins_img):
7171
""" Add a new scan frame - creates a new record if its a new puck, else merges with previous record"""
72-
self._store.merge_record(plate, second_plate, holder_img, pins_img)
72+
self._store.merge_record(holder_barcode, plate, holder_img, pins_img)
7373
self._load_store_records()
7474
if self._options.scan_clipboard.value():
7575
self._barcodeTable.copy_selected_to_clipboard()

tests/unit_tests/test_dls_barcode/test_data_store/test_record.py

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import unittest
2+
from mock import MagicMock
23
from dls_barcode.data_store.record import Record
34
from dls_barcode.geometry.blank import BlankGeometry
5+
from dls_barcode.plate import NOT_FOUND_SLOT_SYMBOL, EMPTY_SLOT_SYMBOL
46

57
class TestRecord(unittest.TestCase):
68

79
def test_from_string_creates_new_record_with_all_given_parameters(self):
8-
str = "f59c92c1;1494238920.0;test.png;None;DLSL-010,DLSL-011,DLSL-012;1569:1106:70-2307:1073:68-1944:1071:68"
10+
# Arrange
11+
str = "f59c92c1;1494238920.0;test.png;None;DLSL-009,DLSL-010,DLSL-011,DLSL-012;1569:1106:70-2307:1073:68-1944:1071:68"
12+
13+
# ACt
914
r = Record.from_string(str)
1015

16+
# Assert
17+
self.assertEqual(r.holder_barcode, 'DLSL-009')
1118
barcodes = r.barcodes
1219
self.assertEqual(len(barcodes), 3)
1320
self.assertTrue('DLSL-010' in barcodes)
@@ -17,11 +24,15 @@ def test_from_string_creates_new_record_with_all_given_parameters(self):
1724
self.assertEquals(r.plate_type, 'None')
1825
self.assertTrue(isinstance(r.geometry, BlankGeometry))
1926

20-
2127
def test_from_string_creates_new_record_time_stamp_missing(self):
22-
str = "f59c92c1; ;test.png;None;DLSL-010,DLSL-011,DLSL-012;1569:1106:70-2307:1073:68-1944:1071:68"
28+
# Arrange
29+
str = "f59c92c1; ;test.png;None;DLSL-009,DLSL-010,DLSL-011,DLSL-012;1569:1106:70-2307:1073:68-1944:1071:68"
30+
31+
# Act
2332
r = Record.from_string(str)
2433

34+
# Assert
35+
self.assertEqual(r.holder_barcode, 'DLSL-009')
2536
barcodes = r.barcodes
2637
self.assertEqual(len(barcodes), 3)
2738
self.assertTrue('DLSL-010' in barcodes)
@@ -32,37 +43,47 @@ def test_from_string_creates_new_record_time_stamp_missing(self):
3243
self.assertTrue(isinstance(r.geometry, BlankGeometry))
3344

3445
def test_to_string_recreates_given_values_excluding_serial_number(self):
46+
# Arrange
3547
str = "f59c92c1;1494238920.0;test.png;None;DLSL-010,DLSL-011,DLSL-012;1569:1106:70-2307:1073:68-1944:1071:68"
3648
r = Record.from_string(str)
49+
50+
# Act
3751
new_str = r.to_string()
52+
53+
# Assert
3854
list_str = str.split(Record.ITEM_SEPARATOR)
3955
list_new_str = new_str.split(Record.ITEM_SEPARATOR)
40-
i = 0
41-
while (i < len(list_str)-1):
56+
for i in range(len(list_str) - 1):
4257
self.assertTrue(list_str[i] == list_new_str[i])
43-
i = i+1
4458

4559
def test_any_barcode_matches_returns_true_if_only_one_barcode_matches(self):
60+
# Arrange
4661
str = "f59c92c1;1494238920.0;test.png;None;DLSL-010,DLSL-011,DLSL-012;1569:1106:70-2307:1073:68-1944:1071:68"
4762
r = Record.from_string(str)
4863

64+
# Act-Assert
4965
self.assertTrue(r.any_barcode_matches(['DLSL-010','DLSL-011111']))
5066

51-
def test_any_barcode_matches_returns_false_if_no_barcode_match(self):
67+
def test_any_barcode_matches_returns_false_if_no_barcode_matches(self):
68+
# Arrange
5269
str = "f59c92c1;1494238920.0;test.png;None;DLSL-010,DLSL-011,DLSL-012;1569:1106:70-2307:1073:68-1944:1071:68"
5370
r = Record.from_string(str)
5471

72+
# Act-Assert
5573
self.assertFalse(r.any_barcode_matches(['DLSL', 'DLSL3']))
5674

5775
def test_any_barcode_matches_returns_false_if_no_barcodes_passed(self):
76+
# Arrange
5877
str = "f59c92c1;1494238920.0;test.png;None;DLSL-010,DLSL-011,DLSL-012;1569:1106:70-2307:1073:68-1944:1071:68"
5978
r = Record.from_string(str)
6079

80+
# Act=Assert
6181
self.assertFalse(r.any_barcode_matches([]))
6282

6383
def test_csv_string_contains_time_in_human_readable_format(self):
6484
# Arrange
6585
timestamp = 1505913516.3836024
86+
6687
# Full human readable timestamp is "2017-09-20 14:18:36" but Travis CI server runs on a different time zone so can't compare the hours
6788
human_readable_timestamp_day = "2017-09-20 "
6889
human_readable_timestamp_minutes = ":18:36"
@@ -77,6 +98,48 @@ def test_csv_string_contains_time_in_human_readable_format(self):
7798
self.assertIn(human_readable_timestamp_minutes, csv_string)
7899
self.assertNotIn(str(timestamp), csv_string)
79100

101+
def test_record_can_be_constructed_from_plate_info(self):
102+
# Arrange
103+
plate_type = "plate type"
104+
holder_barcode = "ABCD"
105+
barcodes = ["barcode1", "barcode2"]
106+
image_path = "a_path"
107+
mock_geometry = MagicMock()
108+
mock_plate = self._create_mock_plate(plate_type, barcodes, mock_geometry)
109+
110+
# Act
111+
r = Record.from_plate(holder_barcode, mock_plate, image_path)
112+
113+
# Assert
114+
self.assertIsNotNone(r.timestamp)
115+
self.assertEquals(r.image_path, image_path)
116+
self.assertEquals(r.plate_type, plate_type)
117+
self.assertEquals(r.holder_barcode, holder_barcode)
118+
self.assertListEqual(r.barcodes, barcodes)
119+
self.assertEquals(r.geometry, mock_geometry)
120+
self.assertIsNotNone(r.id)
121+
122+
def test_the_number_of_slots_are_counted_correctly_from_the_pin_barcodes(self):
123+
# Arrange
124+
barcodes = ["barcode1", "barcode2", EMPTY_SLOT_SYMBOL, "barcode3", NOT_FOUND_SLOT_SYMBOL, EMPTY_SLOT_SYMBOL]
125+
mock_plate = self._create_mock_plate("plate type", barcodes, MagicMock())
126+
127+
# Act
128+
r = Record.from_plate(holder_barcode="ABCD", plate=mock_plate, image_path="a_path")
129+
130+
# Assert
131+
self.assertEquals(r.num_slots, len(barcodes))
132+
self.assertEquals(r.num_empty_slots, 2)
133+
self.assertEquals(r.num_unread_slots, 1)
134+
self.assertEquals(r.num_valid_barcodes, 3)
135+
136+
def _create_mock_plate(self, plate_type, barcodes, geometry):
137+
mock_plate = MagicMock()
138+
mock_plate.type = plate_type
139+
mock_plate.barcodes.return_value = barcodes
140+
mock_plate.geometry.return_value = geometry
141+
return mock_plate
142+
80143

81144

82145

0 commit comments

Comments
 (0)