|
a |
|
b/qiita_db/user.py |
|
|
1 |
r""" |
|
|
2 |
User object (:mod:`qiita_db.user`) |
|
|
3 |
================================== |
|
|
4 |
|
|
|
5 |
.. currentmodule:: qiita_db.user |
|
|
6 |
|
|
|
7 |
This modules provides the implementation of the User class. This is used for |
|
|
8 |
handling creation, deletion, and login of users, as well as retrieval of all |
|
|
9 |
studies and analyses that are owned by or shared with the user. |
|
|
10 |
|
|
|
11 |
Classes |
|
|
12 |
------- |
|
|
13 |
|
|
|
14 |
.. autosummary:: |
|
|
15 |
:toctree: generated/ |
|
|
16 |
|
|
|
17 |
User |
|
|
18 |
""" |
|
|
19 |
# ----------------------------------------------------------------------------- |
|
|
20 |
# Copyright (c) 2014--, The Qiita Development Team. |
|
|
21 |
# |
|
|
22 |
# Distributed under the terms of the BSD 3-clause License. |
|
|
23 |
# |
|
|
24 |
# The full license is in the file LICENSE, distributed with this software. |
|
|
25 |
# ----------------------------------------------------------------------------- |
|
|
26 |
from re import sub |
|
|
27 |
from datetime import datetime |
|
|
28 |
|
|
|
29 |
from qiita_core.exceptions import (IncorrectEmailError, IncorrectPasswordError, |
|
|
30 |
IncompetentQiitaDeveloperError) |
|
|
31 |
from qiita_core.qiita_settings import qiita_config |
|
|
32 |
|
|
|
33 |
import qiita_db as qdb |
|
|
34 |
|
|
|
35 |
|
|
|
36 |
class User(qdb.base.QiitaObject): |
|
|
37 |
""" |
|
|
38 |
User object to access to the Qiita user information |
|
|
39 |
|
|
|
40 |
Attributes |
|
|
41 |
---------- |
|
|
42 |
email |
|
|
43 |
level |
|
|
44 |
info |
|
|
45 |
user_studies |
|
|
46 |
shared_studies |
|
|
47 |
default_analysis |
|
|
48 |
private_analyses |
|
|
49 |
shared_analyses |
|
|
50 |
unread_messages |
|
|
51 |
jobs |
|
|
52 |
|
|
|
53 |
Methods |
|
|
54 |
------- |
|
|
55 |
change_password |
|
|
56 |
generate_reset_code |
|
|
57 |
change_forgot_password |
|
|
58 |
iter |
|
|
59 |
messages |
|
|
60 |
mark_messages |
|
|
61 |
delete_messages |
|
|
62 |
""" |
|
|
63 |
|
|
|
64 |
_table = "qiita_user" |
|
|
65 |
# The following columns are considered not part of the user info |
|
|
66 |
_non_info = {"email", "user_level_id", "password"} |
|
|
67 |
|
|
|
68 |
def _check_id(self, id_): |
|
|
69 |
r"""Check that the provided ID actually exists in the database |
|
|
70 |
|
|
|
71 |
Parameters |
|
|
72 |
---------- |
|
|
73 |
id_ : object |
|
|
74 |
The ID to test |
|
|
75 |
|
|
|
76 |
Notes |
|
|
77 |
----- |
|
|
78 |
This function overwrites the base function, as sql layout doesn't |
|
|
79 |
follow the same conventions done in the other classes. |
|
|
80 |
""" |
|
|
81 |
with qdb.sql_connection.TRN: |
|
|
82 |
sql = """SELECT EXISTS( |
|
|
83 |
SELECT * FROM qiita.qiita_user WHERE email = %s)""" |
|
|
84 |
qdb.sql_connection.TRN.add(sql, [id_]) |
|
|
85 |
return qdb.sql_connection.TRN.execute_fetchlast() |
|
|
86 |
|
|
|
87 |
@classmethod |
|
|
88 |
def iter(cls): |
|
|
89 |
"""Iterates over all users, sorted by their email addresses |
|
|
90 |
|
|
|
91 |
Returns |
|
|
92 |
------- |
|
|
93 |
generator |
|
|
94 |
Yields a user ID (email) and name for each user in the database, |
|
|
95 |
in order of ascending ID |
|
|
96 |
""" |
|
|
97 |
with qdb.sql_connection.TRN: |
|
|
98 |
sql = """select email, name from qiita.{}""".format(cls._table) |
|
|
99 |
qdb.sql_connection.TRN.add(sql) |
|
|
100 |
# Using [-1] to get the results of the last SQL query |
|
|
101 |
for result in qdb.sql_connection.TRN.execute_fetchindex(): |
|
|
102 |
yield result |
|
|
103 |
|
|
|
104 |
@classmethod |
|
|
105 |
def login(cls, email, password): |
|
|
106 |
"""Logs a user into the system |
|
|
107 |
|
|
|
108 |
Parameters |
|
|
109 |
---------- |
|
|
110 |
email : str |
|
|
111 |
The email of the user |
|
|
112 |
password: str |
|
|
113 |
The plaintext password of the user |
|
|
114 |
|
|
|
115 |
Returns |
|
|
116 |
------- |
|
|
117 |
User object |
|
|
118 |
Returns the User object corresponding to the login information |
|
|
119 |
if correct login information |
|
|
120 |
|
|
|
121 |
Raises |
|
|
122 |
------ |
|
|
123 |
IncorrectEmailError |
|
|
124 |
Email passed is not a valid email |
|
|
125 |
IncorrectPasswordError |
|
|
126 |
Password passed is not correct for user |
|
|
127 |
""" |
|
|
128 |
with qdb.sql_connection.TRN: |
|
|
129 |
# see if user exists |
|
|
130 |
if not cls.exists(email): |
|
|
131 |
raise IncorrectEmailError("Email not valid: %s" % email) |
|
|
132 |
|
|
|
133 |
if not validate_password(password): |
|
|
134 |
raise IncorrectPasswordError("Password not valid!") |
|
|
135 |
|
|
|
136 |
# pull password out of database |
|
|
137 |
sql = ("SELECT password, user_level_id FROM qiita.{0} WHERE " |
|
|
138 |
"email = %s".format(cls._table)) |
|
|
139 |
qdb.sql_connection.TRN.add(sql, [email]) |
|
|
140 |
# Using [0] because there is only one row |
|
|
141 |
info = qdb.sql_connection.TRN.execute_fetchindex()[0] |
|
|
142 |
|
|
|
143 |
# verify user email verification |
|
|
144 |
# MAGIC NUMBER 5 = unverified email |
|
|
145 |
if int(info[1]) == 5: |
|
|
146 |
return False |
|
|
147 |
|
|
|
148 |
# verify password |
|
|
149 |
dbpass = info[0] |
|
|
150 |
hashed = qdb.util.hash_password(password, dbpass) |
|
|
151 |
if hashed == dbpass: |
|
|
152 |
return cls(email) |
|
|
153 |
else: |
|
|
154 |
raise IncorrectPasswordError("Password not valid!") |
|
|
155 |
|
|
|
156 |
@classmethod |
|
|
157 |
def exists(cls, email): |
|
|
158 |
"""Checks if a user exists on the database |
|
|
159 |
|
|
|
160 |
Parameters |
|
|
161 |
---------- |
|
|
162 |
email : str |
|
|
163 |
the email of the user |
|
|
164 |
""" |
|
|
165 |
with qdb.sql_connection.TRN: |
|
|
166 |
if not validate_email(email): |
|
|
167 |
raise IncorrectEmailError("Email string not valid: %s" % email) |
|
|
168 |
|
|
|
169 |
sql = """SELECT EXISTS( |
|
|
170 |
SELECT * FROM qiita.{0} |
|
|
171 |
WHERE email = %s)""".format(cls._table) |
|
|
172 |
qdb.sql_connection.TRN.add(sql, [email]) |
|
|
173 |
return qdb.sql_connection.TRN.execute_fetchlast() |
|
|
174 |
|
|
|
175 |
@classmethod |
|
|
176 |
def create(cls, email, password, info=None): |
|
|
177 |
"""Creates a new user on the database |
|
|
178 |
|
|
|
179 |
Parameters |
|
|
180 |
---------- |
|
|
181 |
email : str |
|
|
182 |
The email of the user - used for log in |
|
|
183 |
password : |
|
|
184 |
The plaintext password of the user |
|
|
185 |
info: dict |
|
|
186 |
Other information for the user keyed to table column name |
|
|
187 |
|
|
|
188 |
Raises |
|
|
189 |
------ |
|
|
190 |
IncorrectPasswordError |
|
|
191 |
Password string given is not proper format |
|
|
192 |
IncorrectEmailError |
|
|
193 |
Email string given is not a valid email |
|
|
194 |
QiitaDBDuplicateError |
|
|
195 |
User already exists |
|
|
196 |
""" |
|
|
197 |
with qdb.sql_connection.TRN: |
|
|
198 |
# validate email and password for new user |
|
|
199 |
if not validate_email(email): |
|
|
200 |
raise IncorrectEmailError("Bad email given: %s" % email) |
|
|
201 |
if not validate_password(password): |
|
|
202 |
raise IncorrectPasswordError("Bad password given!") |
|
|
203 |
|
|
|
204 |
# make sure user does not already exist |
|
|
205 |
if cls.exists(email): |
|
|
206 |
raise qdb.exceptions.QiitaDBDuplicateError( |
|
|
207 |
"User", "email: %s" % email) |
|
|
208 |
|
|
|
209 |
# make sure non-info columns aren't passed in info dict |
|
|
210 |
if info: |
|
|
211 |
if cls._non_info.intersection(info): |
|
|
212 |
raise qdb.exceptions.QiitaDBColumnError( |
|
|
213 |
"non info keys passed: %s" % |
|
|
214 |
cls._non_info.intersection(info)) |
|
|
215 |
else: |
|
|
216 |
info = {} |
|
|
217 |
|
|
|
218 |
# create email verification code and hashed password to insert |
|
|
219 |
# add values to info |
|
|
220 |
info["email"] = email |
|
|
221 |
info["password"] = qdb.util.hash_password(password) |
|
|
222 |
info["user_verify_code"] = qdb.util.create_rand_string( |
|
|
223 |
20, punct=False) |
|
|
224 |
|
|
|
225 |
# make sure keys in info correspond to columns in table |
|
|
226 |
qdb.util.check_table_cols(info, cls._table) |
|
|
227 |
|
|
|
228 |
# build info to insert making sure columns and data are in |
|
|
229 |
# same order for sql insertion |
|
|
230 |
columns = info.keys() |
|
|
231 |
values = [info[col] for col in columns] |
|
|
232 |
# crete user |
|
|
233 |
sql = "INSERT INTO qiita.{0} ({1}) VALUES ({2})".format( |
|
|
234 |
cls._table, ','.join(columns), ','.join(['%s'] * len(values))) |
|
|
235 |
qdb.sql_connection.TRN.add(sql, values) |
|
|
236 |
|
|
|
237 |
# Add system messages to user |
|
|
238 |
sql = """INSERT INTO qiita.message_user (email, message_id) |
|
|
239 |
SELECT %s, message_id FROM qiita.message |
|
|
240 |
WHERE expiration > %s""" |
|
|
241 |
qdb.sql_connection.TRN.add(sql, [email, datetime.now()]) |
|
|
242 |
|
|
|
243 |
qdb.sql_connection.TRN.execute() |
|
|
244 |
|
|
|
245 |
return cls(email) |
|
|
246 |
|
|
|
247 |
@classmethod |
|
|
248 |
def verify_code(cls, email, code, code_type): |
|
|
249 |
"""Verify that a code and email match |
|
|
250 |
|
|
|
251 |
Parameters |
|
|
252 |
---------- |
|
|
253 |
email : str |
|
|
254 |
email address of the user |
|
|
255 |
code : str |
|
|
256 |
code to verify |
|
|
257 |
code_type : {'create', 'reset'} |
|
|
258 |
type of code being verified, whether creating user or reset pass. |
|
|
259 |
|
|
|
260 |
Returns |
|
|
261 |
------- |
|
|
262 |
bool |
|
|
263 |
|
|
|
264 |
Raises |
|
|
265 |
------ |
|
|
266 |
IncompentQiitaDeveloper |
|
|
267 |
code_type is not create or reset |
|
|
268 |
QiitaDBError |
|
|
269 |
User has no code of the given type |
|
|
270 |
""" |
|
|
271 |
with qdb.sql_connection.TRN: |
|
|
272 |
if code_type == 'create': |
|
|
273 |
column = 'user_verify_code' |
|
|
274 |
elif code_type == 'reset': |
|
|
275 |
column = 'pass_reset_code' |
|
|
276 |
else: |
|
|
277 |
raise IncompetentQiitaDeveloperError( |
|
|
278 |
"code_type must be 'create' or 'reset' Uknown type %s" |
|
|
279 |
% code_type) |
|
|
280 |
sql = "SELECT {0} FROM qiita.{1} WHERE email = %s".format( |
|
|
281 |
column, cls._table) |
|
|
282 |
qdb.sql_connection.TRN.add(sql, [email]) |
|
|
283 |
db_code = qdb.sql_connection.TRN.execute_fetchindex() |
|
|
284 |
|
|
|
285 |
if not db_code: |
|
|
286 |
return False |
|
|
287 |
|
|
|
288 |
db_code = db_code[0][0] |
|
|
289 |
if db_code is None: |
|
|
290 |
raise qdb.exceptions.QiitaDBError( |
|
|
291 |
"No %s code for user %s" % (column, email)) |
|
|
292 |
|
|
|
293 |
correct_code = db_code == code |
|
|
294 |
|
|
|
295 |
if correct_code: |
|
|
296 |
sql = """UPDATE qiita.{0} SET {1} = NULL |
|
|
297 |
WHERE email = %s""".format(cls._table, column) |
|
|
298 |
qdb.sql_connection.TRN.add(sql, [email]) |
|
|
299 |
|
|
|
300 |
if code_type == "create": |
|
|
301 |
# verify the user |
|
|
302 |
level = qdb.util.convert_to_id( |
|
|
303 |
'user', 'user_level', 'name') |
|
|
304 |
sql = """UPDATE qiita.{} SET user_level_id = %s |
|
|
305 |
WHERE email = %s""".format(cls._table) |
|
|
306 |
qdb.sql_connection.TRN.add(sql, [level, email]) |
|
|
307 |
|
|
|
308 |
# create user default sample holders once verified |
|
|
309 |
# create one per portal |
|
|
310 |
sql = "SELECT portal_type_id FROM qiita.portal_type" |
|
|
311 |
qdb.sql_connection.TRN.add(sql) |
|
|
312 |
|
|
|
313 |
an_sql = """INSERT INTO qiita.analysis |
|
|
314 |
(email, name, description, dflt) |
|
|
315 |
VALUES (%s, %s, %s, %s) |
|
|
316 |
RETURNING analysis_id""" |
|
|
317 |
ap_sql = """INSERT INTO qiita.analysis_portal |
|
|
318 |
(analysis_id, portal_type_id) |
|
|
319 |
VALUES (%s, %s)""" |
|
|
320 |
|
|
|
321 |
portal_ids = qdb.sql_connection.TRN.execute_fetchflatten() |
|
|
322 |
for portal_id in portal_ids: |
|
|
323 |
args = [email, '%s-dflt-%d' % (email, portal_id), |
|
|
324 |
'dflt', True] |
|
|
325 |
qdb.sql_connection.TRN.add(an_sql, args) |
|
|
326 |
an_id = qdb.sql_connection.TRN.execute_fetchlast() |
|
|
327 |
qdb.sql_connection.TRN.add(ap_sql, [an_id, portal_id]) |
|
|
328 |
|
|
|
329 |
qdb.sql_connection.TRN.execute() |
|
|
330 |
|
|
|
331 |
return correct_code |
|
|
332 |
|
|
|
333 |
@classmethod |
|
|
334 |
def delete(cls, email, force=False): |
|
|
335 |
if not cls.exists(email): |
|
|
336 |
raise IncorrectEmailError(f'This email does not exist: {email}') |
|
|
337 |
|
|
|
338 |
tables = ['qiita.study_users', 'qiita.study_tags', |
|
|
339 |
'qiita.processing_job_workflow', 'qiita.processing_job', |
|
|
340 |
'qiita.message_user', 'qiita.analysis_users', |
|
|
341 |
'qiita.analysis'] |
|
|
342 |
|
|
|
343 |
not_empty = [] |
|
|
344 |
for t in tables: |
|
|
345 |
with qdb.sql_connection.TRN: |
|
|
346 |
sql = f"SELECT COUNT(email) FROM {t} WHERE email = %s" |
|
|
347 |
qdb.sql_connection.TRN.add(sql, [email]) |
|
|
348 |
count = qdb.sql_connection.TRN.execute_fetchflatten()[0] |
|
|
349 |
if count != 0: |
|
|
350 |
not_empty.append(t) |
|
|
351 |
|
|
|
352 |
if not_empty and not force: |
|
|
353 |
raise ValueError(f'These tables are not empty: "{not_empty}", ' |
|
|
354 |
'delete them first or use `force=True`') |
|
|
355 |
|
|
|
356 |
sql = """ |
|
|
357 |
DELETE FROM qiita.study_users WHERE email = %(email)s; |
|
|
358 |
DELETE FROM qiita.study_tags WHERE email = %(email)s; |
|
|
359 |
DELETE FROM qiita.processing_job_workflow_root |
|
|
360 |
WHERE processing_job_workflow_id IN ( |
|
|
361 |
SELECT processing_job_workflow_id |
|
|
362 |
FROM qiita.processing_job_workflow |
|
|
363 |
WHERE email = %(email)s); |
|
|
364 |
DELETE FROM qiita.processing_job_workflow WHERE email = %(email)s; |
|
|
365 |
DELETE FROM qiita.processing_job_validator |
|
|
366 |
WHERE processing_job_id IN ( |
|
|
367 |
SELECT processing_job_id |
|
|
368 |
FROM qiita.processing_job |
|
|
369 |
WHERE email = %(email)s); |
|
|
370 |
DELETE FROM qiita.analysis_processing_job |
|
|
371 |
WHERE processing_job_id IN ( |
|
|
372 |
SELECT processing_job_id |
|
|
373 |
FROM qiita.processing_job |
|
|
374 |
WHERE email = %(email)s); |
|
|
375 |
DELETE FROM qiita.artifact_output_processing_job |
|
|
376 |
WHERE processing_job_id IN ( |
|
|
377 |
SELECT processing_job_id |
|
|
378 |
FROM qiita.processing_job |
|
|
379 |
WHERE email = %(email)s); |
|
|
380 |
DELETE FROM qiita.artifact_processing_job |
|
|
381 |
WHERE processing_job_id IN ( |
|
|
382 |
SELECT processing_job_id |
|
|
383 |
FROM qiita.processing_job |
|
|
384 |
WHERE email = %(email)s); |
|
|
385 |
DELETE FROM qiita.parent_processing_job WHERE parent_id IN ( |
|
|
386 |
SELECT processing_job_id |
|
|
387 |
FROM qiita.processing_job |
|
|
388 |
WHERE email = %(email)s); |
|
|
389 |
DELETE FROM qiita.processing_job WHERE email = %(email)s; |
|
|
390 |
DELETE FROM qiita.message_user WHERE email = %(email)s; |
|
|
391 |
DELETE FROM qiita.analysis_users WHERE email = %(email)s; |
|
|
392 |
DELETE FROM qiita.analysis_portal WHERE analysis_id IN ( |
|
|
393 |
SELECT analysis_id |
|
|
394 |
FROM qiita.analysis |
|
|
395 |
WHERE email = %(email)s); |
|
|
396 |
DELETE FROM qiita.analysis_artifact WHERE analysis_id IN ( |
|
|
397 |
SELECT analysis_id |
|
|
398 |
FROM qiita.analysis |
|
|
399 |
WHERE email = %(email)s); |
|
|
400 |
DELETE FROM qiita.analysis_filepath WHERE analysis_id IN ( |
|
|
401 |
SELECT analysis_id |
|
|
402 |
FROM qiita.analysis |
|
|
403 |
WHERE email = %(email)s); |
|
|
404 |
DELETE FROM qiita.analysis_sample WHERE analysis_id IN ( |
|
|
405 |
SELECT analysis_id |
|
|
406 |
FROM qiita.analysis |
|
|
407 |
WHERE email = %(email)s); |
|
|
408 |
DELETE FROM qiita.analysis WHERE email = %(email)s; |
|
|
409 |
DELETE FROM qiita.qiita_user WHERE email = %(email)s;""" |
|
|
410 |
|
|
|
411 |
with qdb.sql_connection.TRN: |
|
|
412 |
qdb.sql_connection.TRN.add(sql, {'email': email}) |
|
|
413 |
qdb.sql_connection.TRN.execute() |
|
|
414 |
|
|
|
415 |
# ---properties--- |
|
|
416 |
@property |
|
|
417 |
def email(self): |
|
|
418 |
"""The email of the user""" |
|
|
419 |
return self._id |
|
|
420 |
|
|
|
421 |
@property |
|
|
422 |
def password(self): |
|
|
423 |
"""The password of the user""" |
|
|
424 |
with qdb.sql_connection.TRN: |
|
|
425 |
# pull password out of database |
|
|
426 |
sql = "SELECT password FROM qiita.{0} WHERE email = %s".format( |
|
|
427 |
self._table) |
|
|
428 |
qdb.sql_connection.TRN.add(sql, [self.email]) |
|
|
429 |
|
|
|
430 |
return qdb.sql_connection.TRN.execute_fetchlast() |
|
|
431 |
|
|
|
432 |
@property |
|
|
433 |
def level(self): |
|
|
434 |
"""The level of privileges of the user""" |
|
|
435 |
with qdb.sql_connection.TRN: |
|
|
436 |
sql = """SELECT ul.name |
|
|
437 |
FROM qiita.user_level ul |
|
|
438 |
JOIN qiita.{0} u |
|
|
439 |
ON ul.user_level_id = u.user_level_id |
|
|
440 |
WHERE u.email = %s""".format(self._table) |
|
|
441 |
qdb.sql_connection.TRN.add(sql, [self._id]) |
|
|
442 |
return qdb.sql_connection.TRN.execute_fetchlast() |
|
|
443 |
|
|
|
444 |
@property |
|
|
445 |
def info(self): |
|
|
446 |
"""Dict with any other information attached to the user""" |
|
|
447 |
with qdb.sql_connection.TRN: |
|
|
448 |
sql = "SELECT * from qiita.{0} WHERE email = %s".format( |
|
|
449 |
self._table) |
|
|
450 |
# Need direct typecast from psycopg2 dict to standard dict |
|
|
451 |
qdb.sql_connection.TRN.add(sql, [self._id]) |
|
|
452 |
# [0] retrieves the first row (the only one present) |
|
|
453 |
info = dict(qdb.sql_connection.TRN.execute_fetchindex()[0]) |
|
|
454 |
# Remove non-info columns |
|
|
455 |
for col in self._non_info: |
|
|
456 |
info.pop(col) |
|
|
457 |
return info |
|
|
458 |
|
|
|
459 |
@info.setter |
|
|
460 |
def info(self, info): |
|
|
461 |
"""Updates the information attached to the user |
|
|
462 |
|
|
|
463 |
Parameters |
|
|
464 |
---------- |
|
|
465 |
info : dict |
|
|
466 |
""" |
|
|
467 |
with qdb.sql_connection.TRN: |
|
|
468 |
# make sure non-info columns aren't passed in info dict |
|
|
469 |
if self._non_info.intersection(info): |
|
|
470 |
raise qdb.exceptions.QiitaDBColumnError( |
|
|
471 |
"non info keys passed!") |
|
|
472 |
|
|
|
473 |
# make sure keys in info correspond to columns in table |
|
|
474 |
qdb.util.check_table_cols(info, self._table) |
|
|
475 |
|
|
|
476 |
# build sql command and data to update |
|
|
477 |
sql_insert = [] |
|
|
478 |
data = [] |
|
|
479 |
# items used for py3 compatability |
|
|
480 |
for key, val in info.items(): |
|
|
481 |
sql_insert.append("{0} = %s".format(key)) |
|
|
482 |
data.append(val) |
|
|
483 |
data.append(self._id) |
|
|
484 |
|
|
|
485 |
sql = ("UPDATE qiita.{0} SET {1} WHERE " |
|
|
486 |
"email = %s".format(self._table, ','.join(sql_insert))) |
|
|
487 |
qdb.sql_connection.TRN.add(sql, data) |
|
|
488 |
qdb.sql_connection.TRN.execute() |
|
|
489 |
|
|
|
490 |
@property |
|
|
491 |
def default_analysis(self): |
|
|
492 |
with qdb.sql_connection.TRN: |
|
|
493 |
sql = """SELECT analysis_id |
|
|
494 |
FROM qiita.analysis |
|
|
495 |
JOIN qiita.analysis_portal USING (analysis_id) |
|
|
496 |
JOIN qiita.portal_type USING (portal_type_id) |
|
|
497 |
WHERE email = %s AND dflt = true AND portal = %s""" |
|
|
498 |
qdb.sql_connection.TRN.add(sql, [self._id, qiita_config.portal]) |
|
|
499 |
return qdb.analysis.Analysis( |
|
|
500 |
qdb.sql_connection.TRN.execute_fetchlast()) |
|
|
501 |
|
|
|
502 |
@property |
|
|
503 |
def user_studies(self): |
|
|
504 |
"""Returns a list of study ids owned by the user""" |
|
|
505 |
with qdb.sql_connection.TRN: |
|
|
506 |
sql = """SELECT study_id |
|
|
507 |
FROM qiita.study |
|
|
508 |
JOIN qiita.study_portal USING (study_id) |
|
|
509 |
JOIN qiita.portal_type USING (portal_type_id) |
|
|
510 |
WHERE email = %s AND portal = %s""" |
|
|
511 |
qdb.sql_connection.TRN.add(sql, [self._id, qiita_config.portal]) |
|
|
512 |
return set( |
|
|
513 |
qdb.study.Study(sid) |
|
|
514 |
for sid in qdb.sql_connection.TRN.execute_fetchflatten()) |
|
|
515 |
|
|
|
516 |
@property |
|
|
517 |
def shared_studies(self): |
|
|
518 |
"""Returns a list of study ids shared with the user""" |
|
|
519 |
with qdb.sql_connection.TRN: |
|
|
520 |
sql = """SELECT study_id |
|
|
521 |
FROM qiita.study_users |
|
|
522 |
JOIN qiita.study_portal USING (study_id) |
|
|
523 |
JOIN qiita.portal_type USING (portal_type_id) |
|
|
524 |
WHERE email = %s and portal = %s""" |
|
|
525 |
qdb.sql_connection.TRN.add(sql, [self._id, qiita_config.portal]) |
|
|
526 |
return set( |
|
|
527 |
qdb.study.Study(sid) |
|
|
528 |
for sid in qdb.sql_connection.TRN.execute_fetchflatten()) |
|
|
529 |
|
|
|
530 |
@property |
|
|
531 |
def private_analyses(self): |
|
|
532 |
"""Returns a list of private analysis ids owned by the user""" |
|
|
533 |
with qdb.sql_connection.TRN: |
|
|
534 |
sql = """SELECT analysis_id FROM qiita.analysis |
|
|
535 |
JOIN qiita.analysis_portal USING (analysis_id) |
|
|
536 |
JOIN qiita.portal_type USING (portal_type_id) |
|
|
537 |
WHERE email = %s AND dflt = false AND portal = %s""" |
|
|
538 |
qdb.sql_connection.TRN.add(sql, [self._id, qiita_config.portal]) |
|
|
539 |
return set( |
|
|
540 |
qdb.analysis.Analysis(aid) |
|
|
541 |
for aid in qdb.sql_connection.TRN.execute_fetchflatten()) |
|
|
542 |
|
|
|
543 |
@property |
|
|
544 |
def shared_analyses(self): |
|
|
545 |
"""Returns a list of analysis ids shared with the user""" |
|
|
546 |
with qdb.sql_connection.TRN: |
|
|
547 |
sql = """SELECT analysis_id FROM qiita.analysis_users |
|
|
548 |
JOIN qiita.analysis_portal USING (analysis_id) |
|
|
549 |
JOIN qiita.portal_type USING (portal_type_id) |
|
|
550 |
WHERE email = %s AND portal = %s""" |
|
|
551 |
qdb.sql_connection.TRN.add(sql, [self._id, qiita_config.portal]) |
|
|
552 |
return set( |
|
|
553 |
qdb.analysis.Analysis(aid) |
|
|
554 |
for aid in qdb.sql_connection.TRN.execute_fetchflatten()) |
|
|
555 |
|
|
|
556 |
@property |
|
|
557 |
def unread_messages(self): |
|
|
558 |
"""Returns all unread messages for a user""" |
|
|
559 |
with qdb.sql_connection.TRN: |
|
|
560 |
sql = """SELECT message_id, message, message_time, read |
|
|
561 |
FROM qiita.message_user |
|
|
562 |
JOIN qiita.message USING (message_id) |
|
|
563 |
WHERE email = %s AND read = FALSE |
|
|
564 |
ORDER BY message_time DESC""" |
|
|
565 |
qdb.sql_connection.TRN.add(sql, [self._id]) |
|
|
566 |
return qdb.sql_connection.TRN.execute_fetchindex() |
|
|
567 |
|
|
|
568 |
@property |
|
|
569 |
def slurm_parameters(self): |
|
|
570 |
"Returns the slumn parameters for this user given by its user level" |
|
|
571 |
with qdb.sql_connection.TRN: |
|
|
572 |
sql = """SELECT slurm_parameters |
|
|
573 |
FROM qiita.user_level |
|
|
574 |
JOIN qiita.qiita_user USING (user_level_id) |
|
|
575 |
WHERE email = %s""" |
|
|
576 |
qdb.sql_connection.TRN.add(sql, [self._id]) |
|
|
577 |
return qdb.sql_connection.TRN.execute_fetchflatten()[0] |
|
|
578 |
|
|
|
579 |
# ------- methods --------- |
|
|
580 |
def user_artifacts(self, artifact_type=None): |
|
|
581 |
"""Returns the artifacts owned by the user, grouped by study |
|
|
582 |
|
|
|
583 |
Parameters |
|
|
584 |
---------- |
|
|
585 |
artifact_type : str, optional |
|
|
586 |
The artifact type to retrieve. Default: retrieve all artfact types |
|
|
587 |
|
|
|
588 |
Returns |
|
|
589 |
------- |
|
|
590 |
dict of {qiita_db.study.Study: list of qiita_db.artifact.Artifact} |
|
|
591 |
The artifacts owned by the user |
|
|
592 |
""" |
|
|
593 |
with qdb.sql_connection.TRN: |
|
|
594 |
sql_args = [self.id, qiita_config.portal] |
|
|
595 |
sql_a_type = "" |
|
|
596 |
if artifact_type: |
|
|
597 |
sql_a_type = " AND artifact_type = %s" |
|
|
598 |
sql_args.append(artifact_type) |
|
|
599 |
|
|
|
600 |
sql = """SELECT study_id, array_agg( |
|
|
601 |
artifact_id ORDER BY artifact_id) |
|
|
602 |
FROM qiita.study |
|
|
603 |
JOIN qiita.study_portal USING (study_id) |
|
|
604 |
JOIN qiita.portal_type USING (portal_type_id) |
|
|
605 |
JOIN qiita.study_artifact USING (study_id) |
|
|
606 |
JOIN qiita.artifact USING (artifact_id) |
|
|
607 |
JOIN qiita.artifact_type USING (artifact_type_id) |
|
|
608 |
WHERE email = %s AND portal = %s{0} |
|
|
609 |
GROUP BY study_id |
|
|
610 |
ORDER BY study_id""".format(sql_a_type) |
|
|
611 |
qdb.sql_connection.TRN.add(sql, sql_args) |
|
|
612 |
db_res = dict(qdb.sql_connection.TRN.execute_fetchindex()) |
|
|
613 |
res = {} |
|
|
614 |
for s_id, artifact_ids in db_res.items(): |
|
|
615 |
res[qdb.study.Study(s_id)] = [ |
|
|
616 |
qdb.artifact.Artifact(a_id) for a_id in artifact_ids] |
|
|
617 |
|
|
|
618 |
return res |
|
|
619 |
|
|
|
620 |
def change_password(self, oldpass, newpass): |
|
|
621 |
"""Changes the password from oldpass to newpass |
|
|
622 |
|
|
|
623 |
Parameters |
|
|
624 |
---------- |
|
|
625 |
oldpass : str |
|
|
626 |
User's old password |
|
|
627 |
newpass : str |
|
|
628 |
User's new password |
|
|
629 |
|
|
|
630 |
Returns |
|
|
631 |
------- |
|
|
632 |
bool |
|
|
633 |
password changed or not |
|
|
634 |
""" |
|
|
635 |
with qdb.sql_connection.TRN: |
|
|
636 |
sql = "SELECT password FROM qiita.{0} WHERE email = %s".format( |
|
|
637 |
self._table) |
|
|
638 |
qdb.sql_connection.TRN.add(sql, [self._id]) |
|
|
639 |
dbpass = qdb.sql_connection.TRN.execute_fetchlast() |
|
|
640 |
if dbpass == qdb.util.hash_password(oldpass, dbpass): |
|
|
641 |
self._change_pass(newpass) |
|
|
642 |
return True |
|
|
643 |
return False |
|
|
644 |
|
|
|
645 |
def generate_reset_code(self): |
|
|
646 |
"""Generates a password reset code for user""" |
|
|
647 |
reset_code = qdb.util.create_rand_string(20, punct=False) |
|
|
648 |
sql = """UPDATE qiita.{0} |
|
|
649 |
SET pass_reset_code = %s, pass_reset_timestamp = NOW() |
|
|
650 |
WHERE email = %s""".format(self._table) |
|
|
651 |
qdb.sql_connection.perform_as_transaction(sql, [reset_code, self._id]) |
|
|
652 |
|
|
|
653 |
def change_forgot_password(self, code, newpass): |
|
|
654 |
"""Changes the password if the code is valid |
|
|
655 |
|
|
|
656 |
Parameters |
|
|
657 |
---------- |
|
|
658 |
code : str |
|
|
659 |
User's forgotten password ID code |
|
|
660 |
newpass : str |
|
|
661 |
User's new password |
|
|
662 |
|
|
|
663 |
Returns |
|
|
664 |
------- |
|
|
665 |
bool |
|
|
666 |
password changed or not |
|
|
667 |
""" |
|
|
668 |
with qdb.sql_connection.TRN: |
|
|
669 |
if self.verify_code(self._id, code, "reset"): |
|
|
670 |
self._change_pass(newpass) |
|
|
671 |
return True |
|
|
672 |
return False |
|
|
673 |
|
|
|
674 |
def _change_pass(self, newpass): |
|
|
675 |
if not validate_password(newpass): |
|
|
676 |
raise IncorrectPasswordError("Bad password given!") |
|
|
677 |
|
|
|
678 |
sql = """UPDATE qiita.{0} |
|
|
679 |
SET password=%s, pass_reset_code = NULL |
|
|
680 |
WHERE email = %s""".format(self._table) |
|
|
681 |
qdb.sql_connection.perform_as_transaction( |
|
|
682 |
sql, [qdb.util.hash_password(newpass), self._id]) |
|
|
683 |
|
|
|
684 |
def messages(self, count=None): |
|
|
685 |
"""Return messages in user's queue |
|
|
686 |
|
|
|
687 |
Parameters |
|
|
688 |
---------- |
|
|
689 |
count : int, optional |
|
|
690 |
Number of messages to return, starting with newest. Default all |
|
|
691 |
|
|
|
692 |
Returns |
|
|
693 |
------- |
|
|
694 |
list of tuples |
|
|
695 |
Messages in the queue, in the form |
|
|
696 |
[(msg_id, msg, timestamp, read, system_message), ...] |
|
|
697 |
|
|
|
698 |
Notes |
|
|
699 |
----- |
|
|
700 |
system_message is a bool. When True, this is a systemwide message. |
|
|
701 |
""" |
|
|
702 |
with qdb.sql_connection.TRN: |
|
|
703 |
sql_info = [self._id] |
|
|
704 |
sql = """SELECT message_id, message, message_time, read, |
|
|
705 |
(expiration IS NOT NULL) AS system_message |
|
|
706 |
FROM qiita.message_user |
|
|
707 |
JOIN qiita.message USING (message_id) |
|
|
708 |
WHERE email = %s ORDER BY message_time DESC""" |
|
|
709 |
if count is not None: |
|
|
710 |
sql += " LIMIT %s" |
|
|
711 |
sql_info.append(count) |
|
|
712 |
qdb.sql_connection.TRN.add(sql, sql_info) |
|
|
713 |
return qdb.sql_connection.TRN.execute_fetchindex() |
|
|
714 |
|
|
|
715 |
def mark_messages(self, messages, read=True): |
|
|
716 |
"""Mark given messages as read/unread |
|
|
717 |
|
|
|
718 |
Parameters |
|
|
719 |
---------- |
|
|
720 |
messages : list of ints |
|
|
721 |
Message IDs to mark as read/unread |
|
|
722 |
read : bool, optional |
|
|
723 |
Marks as read if True, unread if False. Default True |
|
|
724 |
""" |
|
|
725 |
with qdb.sql_connection.TRN: |
|
|
726 |
sql = """UPDATE qiita.message_user |
|
|
727 |
SET read = %s |
|
|
728 |
WHERE message_id IN %s AND email = %s""" |
|
|
729 |
qdb.sql_connection.TRN.add(sql, [read, tuple(messages), self._id]) |
|
|
730 |
return qdb.sql_connection.TRN.execute_fetchindex() |
|
|
731 |
|
|
|
732 |
def delete_messages(self, messages): |
|
|
733 |
"""Delete given messages for the user |
|
|
734 |
|
|
|
735 |
Parameters |
|
|
736 |
---------- |
|
|
737 |
messages : list of ints |
|
|
738 |
Message IDs to delete |
|
|
739 |
""" |
|
|
740 |
with qdb.sql_connection.TRN: |
|
|
741 |
# remove message from user |
|
|
742 |
sql = """DELETE FROM qiita.message_user |
|
|
743 |
WHERE message_id IN %s AND email = %s""" |
|
|
744 |
qdb.sql_connection.TRN.add(sql, [tuple(messages), self._id]) |
|
|
745 |
# Remove any messages that no longer are attached to a user |
|
|
746 |
# and are not system messages |
|
|
747 |
sql = """DELETE FROM qiita.message |
|
|
748 |
WHERE message_id NOT IN |
|
|
749 |
(SELECT DISTINCT message_id FROM qiita.message_user |
|
|
750 |
UNION |
|
|
751 |
SELECT message_id FROM qiita.message |
|
|
752 |
WHERE expiration IS NOT NULL)""" |
|
|
753 |
qdb.sql_connection.TRN.add(sql) |
|
|
754 |
qdb.sql_connection.TRN.execute() |
|
|
755 |
|
|
|
756 |
def jobs(self, limit=30, ignore_status=['success'], show_hidden=False): |
|
|
757 |
"""Return jobs created by the user |
|
|
758 |
|
|
|
759 |
Parameters |
|
|
760 |
---------- |
|
|
761 |
limit : int, optional |
|
|
762 |
max number of rows to return |
|
|
763 |
ignore_status: list of str, optional |
|
|
764 |
don't retieve jobs that have one of these status |
|
|
765 |
show_hidden: bool, optional |
|
|
766 |
If true, return all jobs, including the hidden ones |
|
|
767 |
|
|
|
768 |
Returns |
|
|
769 |
------- |
|
|
770 |
list of ProcessingJob |
|
|
771 |
|
|
|
772 |
""" |
|
|
773 |
with qdb.sql_connection.TRN: |
|
|
774 |
sql = """SELECT processing_job_id |
|
|
775 |
FROM qiita.processing_job |
|
|
776 |
LEFT JOIN qiita.processing_job_status |
|
|
777 |
USING (processing_job_status_id) |
|
|
778 |
WHERE email = %s |
|
|
779 |
""" |
|
|
780 |
|
|
|
781 |
if ignore_status: |
|
|
782 |
sql_info = [self._id, tuple(ignore_status), limit] |
|
|
783 |
sql += " AND processing_job_status NOT IN %s" |
|
|
784 |
else: |
|
|
785 |
sql_info = [self._id, limit] |
|
|
786 |
|
|
|
787 |
if not show_hidden: |
|
|
788 |
sql += ' AND hidden = false' |
|
|
789 |
|
|
|
790 |
sql += """ |
|
|
791 |
ORDER BY CASE processing_job_status |
|
|
792 |
WHEN 'in_construction' THEN 1 |
|
|
793 |
WHEN 'running' THEN 2 |
|
|
794 |
WHEN 'queued' THEN 3 |
|
|
795 |
WHEN 'waiting' THEN 4 |
|
|
796 |
WHEN 'error' THEN 5 |
|
|
797 |
WHEN 'success' THEN 6 |
|
|
798 |
END, heartbeat DESC LIMIT %s""" |
|
|
799 |
|
|
|
800 |
qdb.sql_connection.TRN.add(sql, sql_info) |
|
|
801 |
return [qdb.processing_job.ProcessingJob(p[0]) |
|
|
802 |
for p in qdb.sql_connection.TRN.execute_fetchindex()] |
|
|
803 |
|
|
|
804 |
def update_email(self, email): |
|
|
805 |
if not validate_email(email): |
|
|
806 |
raise IncorrectEmailError(f'Bad email given: {email}') |
|
|
807 |
|
|
|
808 |
if self.exists(email): |
|
|
809 |
raise IncorrectEmailError(f'This email already exists: {email}') |
|
|
810 |
|
|
|
811 |
with qdb.sql_connection.TRN: |
|
|
812 |
sql = 'UPDATE qiita.qiita_user SET email = %s where email = %s' |
|
|
813 |
qdb.sql_connection.TRN.add(sql, [email, self.email]) |
|
|
814 |
|
|
|
815 |
|
|
|
816 |
def validate_email(email): |
|
|
817 |
"""Validates an email |
|
|
818 |
|
|
|
819 |
Notes |
|
|
820 |
----- |
|
|
821 |
An email address is of the form local-part@domain_part |
|
|
822 |
For our purposes: |
|
|
823 |
|
|
|
824 |
- No quoted strings are allowed |
|
|
825 |
- No unicode strings are allowed |
|
|
826 |
- There must be exactly one @ symbol |
|
|
827 |
- Neither local-part nor domain-part can be blank |
|
|
828 |
- The local-part cannot start or end with a dot |
|
|
829 |
- The local-part must be composed of the following characters: |
|
|
830 |
a-zA-Z0-9#_~!$&'()*+,;=:.- |
|
|
831 |
- The domain-part must be a valid hostname, composed of: |
|
|
832 |
a-zA-Z0-9. |
|
|
833 |
|
|
|
834 |
Parameters |
|
|
835 |
---------- |
|
|
836 |
email: str |
|
|
837 |
email to validate |
|
|
838 |
|
|
|
839 |
Returns |
|
|
840 |
------- |
|
|
841 |
bool |
|
|
842 |
Whether or not the email is valid |
|
|
843 |
""" |
|
|
844 |
# Do not accept email addresses that have unicode characters |
|
|
845 |
try: |
|
|
846 |
email.encode('ascii') |
|
|
847 |
except UnicodeError: |
|
|
848 |
return False |
|
|
849 |
|
|
|
850 |
# we are not allowing quoted strings in the email address |
|
|
851 |
if '"' in email: |
|
|
852 |
return False |
|
|
853 |
|
|
|
854 |
# Must have exactly 1 @ symbol |
|
|
855 |
if email.count('@') != 1: |
|
|
856 |
return False |
|
|
857 |
|
|
|
858 |
local_part, domain_part = email.split('@') |
|
|
859 |
|
|
|
860 |
# Neither part can be blank |
|
|
861 |
if not (local_part and domain_part): |
|
|
862 |
return False |
|
|
863 |
|
|
|
864 |
# The local part cannot begin or end with a dot |
|
|
865 |
if local_part.startswith('.') or local_part.endswith('.'): |
|
|
866 |
return False |
|
|
867 |
|
|
|
868 |
# The domain part cannot begin or end with a hyphen |
|
|
869 |
if domain_part.startswith('-') or domain_part.endswith('-'): |
|
|
870 |
return False |
|
|
871 |
|
|
|
872 |
# This is the full set of allowable characters for the local part. |
|
|
873 |
local_valid_chars = "[a-zA-Z0-9#_~!$&'()*+,;=:.-]" |
|
|
874 |
if len(sub(local_valid_chars, '', local_part)): |
|
|
875 |
return False |
|
|
876 |
|
|
|
877 |
domain_valid_chars = "[a-zA-Z0-9.-]" |
|
|
878 |
if len(sub(domain_valid_chars, '', domain_part)): |
|
|
879 |
return False |
|
|
880 |
|
|
|
881 |
return True |
|
|
882 |
|
|
|
883 |
|
|
|
884 |
def validate_password(password): |
|
|
885 |
"""Validates a password |
|
|
886 |
|
|
|
887 |
Notes |
|
|
888 |
----- |
|
|
889 |
The valid characters for a password are: |
|
|
890 |
|
|
|
891 |
* lowercase letters |
|
|
892 |
* uppercase letters |
|
|
893 |
* numbers |
|
|
894 |
* special characters (e.g., !@#$%^&*()-_=+`~[]{}|;:'",<.>/?) with the |
|
|
895 |
exception of a backslash |
|
|
896 |
* must be ASCII |
|
|
897 |
* no spaces |
|
|
898 |
* must be at least 8 characters |
|
|
899 |
|
|
|
900 |
Parameters |
|
|
901 |
---------- |
|
|
902 |
password: str |
|
|
903 |
Password to validate |
|
|
904 |
|
|
|
905 |
Returns |
|
|
906 |
------- |
|
|
907 |
bool |
|
|
908 |
Whether or not the password is valid |
|
|
909 |
|
|
|
910 |
References |
|
|
911 |
----- |
|
|
912 |
http://stackoverflow.com/q/196345 |
|
|
913 |
""" |
|
|
914 |
if len(password) < 8: |
|
|
915 |
return False |
|
|
916 |
|
|
|
917 |
if "\\" in password or " " in password: |
|
|
918 |
return False |
|
|
919 |
|
|
|
920 |
try: |
|
|
921 |
password.encode('ascii') |
|
|
922 |
except UnicodeError: |
|
|
923 |
return False |
|
|
924 |
|
|
|
925 |
return True |