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