Skip to content

Commit a3b8f53

Browse files
Merge pull request #412 from wasade/issue_246
Issue 246
2 parents 6ec8805 + 37ff916 commit a3b8f53

File tree

11 files changed

+204
-40
lines changed

11 files changed

+204
-40
lines changed

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ install:
3131
- travis_retry conda create -q --yes -n qiita python=3.6 pip libgfortran numpy nginx
3232
- source activate qiita
3333
- pip install sphinx sphinx-bootstrap-theme coveralls
34-
- pip install https://github.com/antgonza/qiita/archive/conditional-labman.zip --no-binary redbiom
34+
- pip install https://github.com/biocore/qiita/archive/dev.zip --no-binary redbiom
3535
# as we don't need redbiom we are going to use the default redis port
3636
- sed 's/PORT = 7777/PORT = 6379/g' $HOME/miniconda3/envs/qiita/lib/python3.6/site-packages/qiita_core/support_files/config_test.cfg > config_test.cfg
3737
- export QIITA_CONFIG_FP=${PWD}/config_test.cfg
@@ -71,6 +71,7 @@ install:
7171
# Verify db population
7272
# TODO: extend to labman schema
7373
- psql -d qiita_test -c 'select * from qiita.study;'
74+
- labman patch
7475
script:
7576
- nosetests --with-doctest --with-coverage -v --cover-package=labman
7677
- flake8 labman setup.py scripts/*

labman/db/environment.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# ----------------------------------------------------------------------------
2+
# Copyright (c) 2017-, labman development team.
3+
#
4+
# Distributed under the terms of the Modified BSD License.
5+
#
6+
# The full license is in the file LICENSE, distributed with this software.
7+
# ----------------------------------------------------------------------------
8+
from os.path import join, dirname, abspath, basename, splitext, exists
9+
from glob import glob
10+
from functools import partial
11+
12+
from natsort import natsorted
13+
14+
from labman.db import sql_connection
15+
16+
17+
def patch_database(verbose):
18+
"""Apply patches, if necessary, to the database"""
19+
get_support_file = partial(join, join(dirname(abspath(__file__)),
20+
'support_files'))
21+
patches_dir = get_support_file('patches')
22+
23+
with sql_connection.TRN:
24+
sql_connection.TRN.add("SELECT current_patch FROM labman.settings")
25+
try:
26+
current_patch = sql_connection.TRN.execute_fetchlast()
27+
except ValueError:
28+
# the system doesn't have the settings table so is unpatched
29+
current_patch = 'unpatched'
30+
31+
current_sql_patch_fp = join(patches_dir, current_patch)
32+
corresponding_py_patch = partial(join, patches_dir, 'python_patches')
33+
34+
sql_glob = join(patches_dir, '*.sql')
35+
sql_patch_files = natsorted(glob(sql_glob))
36+
37+
if current_patch == 'unpatched':
38+
next_patch_index = 0
39+
sql_connection.TRN.add("""CREATE TABLE labman.settings
40+
(current_patch varchar not null)""")
41+
sql_connection.TRN.add("""INSERT INTO labman.settings
42+
(current_patch) VALUES ('unpatched')""")
43+
sql_connection.TRN.execute()
44+
elif current_sql_patch_fp not in sql_patch_files:
45+
raise RuntimeError("Cannot find patch file %s" % current_patch)
46+
else:
47+
next_patch_index = sql_patch_files.index(current_sql_patch_fp) + 1
48+
49+
patch_update_sql = "UPDATE labman.settings SET current_patch = %s"
50+
51+
for sql_patch_fp in sql_patch_files[next_patch_index:]:
52+
sql_patch_filename = basename(sql_patch_fp)
53+
54+
py_patch_fp = corresponding_py_patch(
55+
splitext(basename(sql_patch_fp))[0] + '.py')
56+
py_patch_filename = basename(py_patch_fp)
57+
58+
with sql_connection.TRN:
59+
with open(sql_patch_fp, newline=None) as patch_file:
60+
if verbose:
61+
print('\tApplying patch %s...' % sql_patch_filename)
62+
sql_connection.TRN.add(patch_file.read())
63+
sql_connection.TRN.add(
64+
patch_update_sql, [sql_patch_filename])
65+
66+
sql_connection.TRN.execute()
67+
68+
if exists(py_patch_fp):
69+
if verbose:
70+
print('\t\tApplying python patch %s...'
71+
% py_patch_filename)
72+
with open(py_patch_fp) as py_patch:
73+
exec(py_patch.read(), globals())

labman/db/plate.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,8 @@ def list_plates(plate_types=None, only_quantified=False,
198198
-------
199199
list of dicts
200200
The list of plate information with the structure:
201-
[{'plate_id': int, 'external_id': string}]
201+
[{'plate_id': int, 'external_id': string,
202+
'creation_timestamp': datetime}]
202203
"""
203204
with sql_connection.TRN as TRN:
204205
sql_where, sql_discard, sql_plate_types = '', '', ''
@@ -232,8 +233,9 @@ def list_plates(plate_types=None, only_quantified=False,
232233
sql_studies = (', labman.get_plate_studies(p.plate_id) '
233234
'AS studies')
234235

235-
sql = """SELECT p.plate_id, p.external_id {}
236-
FROM (SELECT DISTINCT plate_id, external_id
236+
sql = """SELECT p.plate_id, p.external_id, p.creation_timestamp {}
237+
FROM (SELECT DISTINCT plate_id, external_id,
238+
creation_timestamp
237239
FROM labman.plate
238240
JOIN labman.well USING (plate_id)
239241
JOIN labman.composition USING (container_id)
@@ -244,7 +246,13 @@ def list_plates(plate_types=None, only_quantified=False,
244246
ORDER BY plate_id""".format(sql_studies, sql_join,
245247
sql_where)
246248
TRN.add(sql, sql_args)
247-
return [dict(r) for r in TRN.execute_fetchindex()]
249+
250+
results = []
251+
for r in TRN.execute_fetchindex():
252+
r = dict(r)
253+
r['creation_timestamp'] = str(r['creation_timestamp'])
254+
results.append(r)
255+
return results
248256

249257
@staticmethod
250258
def external_id_exists(external_id):
@@ -290,6 +298,10 @@ def create(cls, external_id, plate_configuration):
290298
TRN.add(sql, [external_id, plate_configuration.id])
291299
return cls(TRN.execute_fetchlast())
292300

301+
@property
302+
def creation_timestamp(self):
303+
return self._get_attr('creation_timestamp')
304+
293305
@property
294306
def external_id(self):
295307
"""The plate external identifier"""
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- April 2, 2019
2+
-- Implicitly track the date and time of plate creation
3+
ALTER TABLE labman.plate ADD COLUMN creation_timestamp timestamp NOT NULL default now();

labman/db/testing.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from qiita_client import QiitaClient
1414

1515
import labman
16+
from labman.db.environment import patch_database
1617

1718

1819
def reset_test_db():
@@ -52,8 +53,18 @@ def reset_test_db():
5253
TRN.add(f.read())
5354
TRN.execute()
5455

56+
patch_database(verbose=False)
57+
5558

5659
class LabmanTestCase(TestCase):
60+
_perform_reset = True
61+
62+
def do_not_reset_at_teardown(self):
63+
self.__class__._perform_reset = False
64+
5765
@classmethod
5866
def tearDownClass(cls):
59-
reset_test_db()
67+
if cls._perform_reset:
68+
reset_test_db()
69+
else:
70+
cls._perform_reset = True

labman/db/tests/test_plate.py

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from unittest import main
1010
from types import GeneratorType
11+
import datetime
1112

1213
from labman.db.testing import LabmanTestCase
1314
from labman.db.plate import PlateConfiguration, Plate
@@ -92,9 +93,20 @@ def test_search(self):
9293
Plate.search(plate_notes='interesting', well_notes='write',
9394
query_type='UNION'), [plate22, plate23])
9495

96+
def strip_out_creation_timestamp(self, plates):
97+
"""Kludge to remove creation_timestamp from plate list results"""
98+
obs_ = []
99+
for o in plates:
100+
o = o.copy()
101+
self.assertIn('creation_timestamp', o)
102+
o.pop('creation_timestamp')
103+
obs_.append(o)
104+
return obs_
105+
95106
def test_list_plates(self):
96107
# Test returning all plates
97-
obs = Plate.list_plates()
108+
obs = self.strip_out_creation_timestamp(Plate.list_plates())
109+
98110
# We are creating plates below, but at least we know there are 35
99111
# plates in the test database
100112
self.assertGreaterEqual(len(obs), 35)
@@ -107,11 +119,12 @@ def test_list_plates(self):
107119
'external_id': 'Test plate 1'})
108120

109121
# Test returning sample plates
110-
obs = Plate.list_plates(['sample'])
122+
obs = self.strip_out_creation_timestamp(Plate.list_plates(['sample']))
111123
self.assertGreaterEqual(len(obs), 4)
112124
self.assertEqual(obs[0], {'plate_id': 21,
113125
'external_id': 'Test plate 1'})
114-
obs = Plate.list_plates(['sample'], include_study_titles=True)
126+
obs = self.strip_out_creation_timestamp(Plate.list_plates(['sample'],
127+
include_study_titles=True))
115128
self.assertGreaterEqual(len(obs), 4)
116129
self.assertEqual(
117130
obs[0], {'plate_id': 21,
@@ -120,21 +133,22 @@ def test_list_plates(self):
120133
'for Cannabis Soils']})
121134

122135
# Test returning gDNA plates
123-
obs = Plate.list_plates(['gDNA'])
136+
obs = self.strip_out_creation_timestamp(Plate.list_plates(['gDNA']))
124137
self.assertEqual(
125138
obs, [{'plate_id': 22,
126139
'external_id': 'Test gDNA plate 1'},
127140
{'external_id': 'Test gDNA plate 2', 'plate_id': 28},
128141
{'external_id': 'Test gDNA plate 3', 'plate_id': 31},
129142
{'external_id': 'Test gDNA plate 4', 'plate_id': 34}])
130143

131-
obs = Plate.list_plates(['compressed gDNA'])
144+
obs = self.strip_out_creation_timestamp(
145+
Plate.list_plates(['compressed gDNA']))
132146
self.assertEqual(
133147
obs, [{'plate_id': 24,
134148
'external_id': 'Test compressed gDNA plates 1-4'}])
135149

136150
# Test returning primer plates
137-
obs = Plate.list_plates(['primer'])
151+
obs = self.strip_out_creation_timestamp(Plate.list_plates(['primer']))
138152
exp = [
139153
{'plate_id': 11,
140154
'external_id': 'EMP 16S V4 primer plate 1 10/23/2017'},
@@ -157,7 +171,8 @@ def test_list_plates(self):
157171
self.assertEqual(obs, exp)
158172

159173
# Test returning gDNA and compressed gDNA plates
160-
obs = Plate.list_plates(['gDNA', 'compressed gDNA'])
174+
obs = self.strip_out_creation_timestamp(
175+
Plate.list_plates(['gDNA', 'compressed gDNA']))
161176
self.assertEqual(
162177
obs, [{'plate_id': 22,
163178
'external_id': 'Test gDNA plate 1'},
@@ -168,25 +183,38 @@ def test_list_plates(self):
168183
{'external_id': 'Test gDNA plate 4', 'plate_id': 34}
169184
])
170185

171-
obs = Plate.list_plates(['compressed gDNA', 'normalized gDNA'])
186+
obs = self.strip_out_creation_timestamp(
187+
Plate.list_plates(['compressed gDNA', 'normalized gDNA']))
172188
self.assertEqual(
173189
obs, [{'plate_id': 24,
174190
'external_id': 'Test compressed gDNA plates 1-4'},
175191
{'plate_id': 25,
176192
'external_id': 'Test normalized gDNA plates 1-4'}])
177193

178-
obs = Plate.list_plates(['compressed gDNA', 'normalized gDNA'],
179-
only_quantified=True,
180-
include_study_titles=True)
194+
obs = self.strip_out_creation_timestamp(
195+
Plate.list_plates(['compressed gDNA', 'normalized gDNA'],
196+
only_quantified=True,
197+
include_study_titles=True))
181198
self.assertEqual(
182199
obs, [{'plate_id': 24,
183200
'external_id': 'Test compressed gDNA plates 1-4',
184201
'studies': ['Identification of the Microbiomes '
185202
'for Cannabis Soils']}])
186203

204+
def test_plate_list_include_timestamp(self):
205+
# ...limit pathological failures by testing within an hour of creation
206+
exp = datetime.datetime.now()
207+
exp = str(datetime.datetime(exp.year,
208+
exp.month,
209+
exp.day)).split(None, 1)[0]
210+
211+
for i in Plate.list_plates():
212+
obs = i['creation_timestamp'].split(None, 1)[0]
213+
self.assertEqual(obs, exp)
214+
187215
def test_plate_list_discarded_functionality(self):
188216
# test case based on the test_list_plates
189-
obs = Plate.list_plates()
217+
obs = self.strip_out_creation_timestamp(Plate.list_plates())
190218
Plate(21).discard = True
191219
self.assertGreaterEqual(len(obs), 25)
192220
self.assertEqual(obs[0], {'plate_id': 1,
@@ -202,7 +230,8 @@ def test_plate_list_discarded_functionality(self):
202230
Plate(11).discarded = True
203231
Plate(12).discarded = True
204232
Plate(13).discarded = True
205-
obs = Plate.list_plates(['primer'], include_discarded=False)
233+
obs = self.strip_out_creation_timestamp(Plate.list_plates(['primer'],
234+
include_discarded=False))
206235

207236
exp = [
208237
{'plate_id': 14,
@@ -220,7 +249,8 @@ def test_plate_list_discarded_functionality(self):
220249
self.assertEqual(obs, exp)
221250

222251
# Test returning discarded primer plates
223-
obs = Plate.list_plates(['primer'], include_discarded=True)
252+
obs = self.strip_out_creation_timestamp(Plate.list_plates(['primer'],
253+
include_discarded=True))
224254
exp = [
225255
{'plate_id': 11,
226256
'external_id': 'EMP 16S V4 primer plate 1 10/23/2017'},
@@ -268,6 +298,14 @@ def test_create(self):
268298
def test_properties(self):
269299
# Plate 21 - Defined in the test DB
270300
tester = Plate(21)
301+
302+
obs = tester.creation_timestamp
303+
obs = str(datetime.datetime(obs.year,
304+
obs.month,
305+
obs.day))
306+
exp = datetime.datetime.now()
307+
exp = str(datetime.datetime(exp.year, exp.month, exp.day))
308+
self.assertEqual(obs, exp)
271309
self.assertEqual(tester.external_id, 'Test plate 1')
272310
self.assertEqual(tester.plate_configuration, PlateConfiguration(1))
273311
self.assertFalse(tester.discarded)

labman/gui/handlers/plate.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ def get(self):
8181
if plate_type is not None else None)
8282
only_quantified = True if only_quantified == 'true' else False
8383

84-
rows_list = [[p['plate_id'], p['external_id'],
84+
rows_list = [[p['plate_id'],
85+
p['external_id'],
86+
p['creation_timestamp'],
8587
p['studies'] if p['studies'] is not None else []]
8688
for p in Plate.list_plates(
8789
plate_type, only_quantified=only_quantified,

labman/gui/templates/plate_list.html

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,10 @@
105105
var deleteButton = '<button onclick="discardPlate(' + row[0] + ', this)" class="btn btn-danger">Discard Plate ' +
106106
'<span class="glyphicon glyphicon-remove" data-toggle="tooltip" title="Discard plate"></span>' +
107107
'</button> ';
108-
// row[0] = plate id, row[1] = external id, row[2] = list of names of
109-
// studies associated with any sample on plate (may be empty list)
110-
newData.push([chBox, row[0], row[1], row[2].join('<br />'), deleteButton]);
108+
// row[0] = plate id, row[1] = external id, row[2] = creation
109+
// timestamprow[3] = list of names of studies associated with any
110+
// sample on plate (may be empty list)
111+
newData.push([chBox, row[0], row[1], row[2], row[3].join('<br />'), deleteButton]);
111112
}
112113
datatable.clear();
113114
datatable.rows.add(newData);
@@ -159,6 +160,7 @@
159160
<th></th>
160161
<th>Plate id</th>
161162
<th>Plate name</th>
163+
<th>Creation timestamp</th>
162164
<th>Studies</th>
163165
<th></th>
164166
</tr>

0 commit comments

Comments
 (0)