-
Notifications
You must be signed in to change notification settings - Fork 19
/
source_repo.py
260 lines (221 loc) · 9.55 KB
/
source_repo.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
import psycopg2
import datetime
from microsetta_private_api.exceptions import RepoException
from microsetta_private_api.repo.account_repo import AccountRepo
from microsetta_private_api.repo.base_repo import BaseRepo
from microsetta_private_api.model.source import Source, HumanInfo, NonHumanInfo
from werkzeug.exceptions import NotFound
from hashlib import sha512
def _source_to_row(s):
d = s.source_data
row = (s.id,
s.account_id,
s.source_type,
s.name,
getattr(d, 'email', None),
getattr(d, 'is_juvenile', None),
getattr(d, 'parent1_name', None),
getattr(d, 'parent2_name', None),
getattr(d, 'deceased_parent', None),
getattr(d, 'consent_date', None),
getattr(d, 'date_revoked', None),
getattr(d, 'assent_obtainer', None),
getattr(d, 'age_range', None),
getattr(d, 'description', None))
return row
def _row_to_human_info(r):
return HumanInfo(
r['participant_email'],
r['is_juvenile'],
r['parent_1_name'],
r['parent_2_name'],
r['deceased_parent'],
r['date_signed'],
r['date_revoked'],
r['assent_obtainer'],
r['age_range'])
def _row_to_nonhuman_info(r):
return NonHumanInfo(r["description"])
row_to_obj_funcs_by_type = {
Source.SOURCE_TYPE_HUMAN: _row_to_human_info,
Source.SOURCE_TYPE_ANIMAL: _row_to_nonhuman_info,
Source.SOURCE_TYPE_ENVIRONMENT: _row_to_nonhuman_info
}
def _row_to_source(r):
row_to_obj = row_to_obj_funcs_by_type[
r['source_type']]
return Source(r[0], r[1], r[2], r[3], row_to_obj(r))
# Note: By convention, this references sources by both account_id AND source_id
# This should make it more difficult to accidentally muck up sources when the
# user doesn't have the right permissions
class SourceRepo(BaseRepo):
def __init__(self, transaction):
super().__init__(transaction)
read_cols = "id, account_id, " \
"source_type, source_name, participant_email, " \
"is_juvenile, parent_1_name, parent_2_name, " \
"deceased_parent, date_signed, date_revoked, " \
"assent_obtainer, age_range, description, " \
"creation_time, update_time"
write_cols = "id, account_id, source_type, " \
"source_name, participant_email, " \
"is_juvenile, parent_1_name, parent_2_name, " \
"deceased_parent, date_signed, date_revoked, " \
"assent_obtainer, age_range, description"
def get_sources_in_account(self, account_id, source_type=None,
allow_revoked=False):
if not allow_revoked:
no_revoked = " AND source.date_revoked IS NULL"
else:
no_revoked = ""
with self._transaction.dict_cursor() as cur:
if source_type is None:
cur.execute("SELECT " + SourceRepo.read_cols + " FROM "
"source "
"WHERE "
"source.account_id = %s" + no_revoked,
(account_id,))
else:
cur.execute("SELECT " + SourceRepo.read_cols + " FROM "
"source "
"WHERE "
"source.account_id = %s AND "
"source.source_type = %s" + no_revoked,
(account_id, source_type))
rows = cur.fetchall()
return [_row_to_source(x) for x in rows]
def get_source(self, account_id, source_id, allow_revoked=False):
if not allow_revoked:
no_revoked = " AND source.date_revoked IS NULL"
else:
no_revoked = ""
with self._transaction.dict_cursor() as cur:
cur.execute("SELECT " + SourceRepo.read_cols + " FROM "
"source "
"WHERE "
"source.id = %s AND "
"source.account_id = %s" + no_revoked,
(source_id, account_id))
r = cur.fetchone()
if r is None:
return None
return _row_to_source(r)
def update_source_data_api_fields(self, source):
# Business Policy: For now I will let them edit only name and
# description. Anything else they have to recreate the source
# Everything else they send up, we currently ignore.
# TODO: Change yaml to remove extraneous fields?
# Raise exc in this layer?
with self._transaction.cursor() as cur:
cur.execute("UPDATE source "
"SET "
"source_name = %s, "
"description = %s "
"WHERE "
"source.id = %s AND "
"source.account_id = %s",
(
getattr(source, 'name', None),
getattr(source.source_data, 'description', None),
source.id,
source.account_id
)
)
return cur.rowcount == 1
def create_source(self, source):
with self._transaction.cursor() as cur:
acct_repo = AccountRepo(self._transaction)
if acct_repo.get_account(source.account_id) is None:
raise NotFound("No such account_id")
cur.execute("INSERT INTO source (" + SourceRepo.write_cols + ") "
"VALUES("
"%s, %s, %s, "
"%s, %s, "
"%s, %s, %s, "
"%s, %s, %s, "
"%s, %s, %s)",
_source_to_row(source))
host_subject_id = self.construct_host_subject_id(source.account_id,
source.name)
cur.execute("""INSERT INTO source_host_subject_id (source_id,
host_subject_id)
VALUES (%s, %s)""",
(source.id, host_subject_id))
return cur.rowcount == 1
@staticmethod
def construct_host_subject_id(account_id, source_name):
prehash = account_id + source_name.lower()
return sha512(prehash.encode()).hexdigest()
def get_host_subject_id(self, source):
with self._transaction.cursor() as cur:
cur.execute("""SELECT host_subject_id
FROM source_host_subject_id
WHERE source_id = %s""",
(source.id, ))
r = cur.fetchone()
if r is None:
return None
return r
def delete_source(self, account_id, source_id):
try:
with self._transaction.cursor() as cur:
cur.execute("DELETE FROM source_host_subject_id "
"WHERE source_id = %s",
(source_id, ))
cur.execute("DELETE FROM source WHERE source.id = %s AND "
"source.account_id = %s",
(source_id, account_id))
return cur.rowcount == 1
except psycopg2.errors.ForeignKeyViolation as e:
if e.diag.constraint_name == "fk_ag_kit_barcodes_sources":
raise RepoException("A source cannot be deleted while samples "
"are associated with it") from e
raise RepoException("Error deleting source") from e
def scrub(self, account_id, source_id):
"""Replace identifying information with scrubbed text
Parameters
----------
account_id : str, uuid
The associated account ID to scrub
source_id : str, uuid
The associated source ID to scrub
Raises
------
RepoException
If the source was not found
If the source is not human
If the update failed
Returns
-------
True if the record was updated, raises otherwise
"""
source = self.get_source(account_id, source_id)
if source is None:
raise RepoException("Source not found")
if source.source_type != Source.SOURCE_TYPE_HUMAN:
raise RepoException("Source is not human")
name = "scrubbed"
description = "scrubbed"
email = "scrubbed@microsetta.ucsd.edu"
parent1_name = "scrubbed"
parent2_name = "scrubbed"
assent_obtainer = "scrubbed"
date_revoked = datetime.datetime.now()
with self._transaction.cursor() as cur:
cur.execute("""UPDATE source
SET source_name = %s,
participant_email = %s,
description = %s,
parent_1_name = %s,
parent_2_name = %s,
date_revoked = %s,
assent_obtainer = %s,
update_time = %s
WHERE id = %s""",
(name, email, description, parent1_name, parent2_name,
date_revoked, assent_obtainer, date_revoked,
source_id))
if cur.rowcount != 1:
raise RepoException("Invalid source relation")
else:
return True