Diff of /qiita_db/study.py [000000] .. [879b32]

Switch to unified view

a b/qiita_db/study.py
1
r"""
2
Study and StudyPerson objects (:mod:`qiita_db.study`)
3
=====================================================
4
5
.. currentmodule:: qiita_db.study
6
7
This module provides the implementation of the Study and StudyPerson classes.
8
The study class allows access to all basic information including name and
9
pmids associated with the study, as well as returning ids for the data,
10
sample template, owner, and shared users. It is the central hub for creating,
11
deleting, and accessing a study in the database.
12
13
Contacts are taken care of by the StudyPerson class. This holds the contact's
14
name, email, address, and phone of the various persons in a study, e.g. The PI
15
or lab contact.
16
17
Classes
18
-------
19
20
.. autosummary::
21
   :toctree: generated/
22
23
   Study
24
   StudyPerson
25
"""
26
27
# -----------------------------------------------------------------------------
28
# Copyright (c) 2014--, The Qiita Development Team.
29
#
30
# Distributed under the terms of the BSD 3-clause License.
31
#
32
# The full license is in the file LICENSE, distributed with this software.
33
# -----------------------------------------------------------------------------
34
from collections import defaultdict
35
from copy import deepcopy
36
from itertools import chain
37
import warnings
38
39
from qiita_core.exceptions import IncompetentQiitaDeveloperError
40
from qiita_core.qiita_settings import qiita_config
41
import qiita_db as qdb
42
43
44
class Study(qdb.base.QiitaObject):
45
    r"""Study object to access to the Qiita Study information
46
47
    Attributes
48
    ----------
49
    data_types
50
    info
51
    investigation
52
    name
53
    pmids
54
    shared_with
55
    sample_template
56
    status
57
    title
58
    owner
59
    autoloaded
60
61
    Methods
62
    -------
63
    raw_data
64
    preprocessed_data
65
    processed_data
66
    add_pmid
67
    exists
68
    has_access
69
    share
70
    unshare
71
72
    Notes
73
    -----
74
    All setters raise QiitaDBStatusError if trying to change a public study.
75
    You should not be doing that.
76
    """
77
    _table = "study"
78
    _portal_table = "study_portal"
79
    # The following columns are considered not part of the study info
80
    _non_info = frozenset(["email", "study_title", "ebi_study_accession",
81
                           "autoloaded"])
82
83
    def _lock_non_sandbox(self):
84
        """Raises QiitaDBStatusError if study is non-sandboxed"""
85
        if self.status != 'sandbox':
86
            raise qdb.exceptions.QiitaDBStatusError(
87
                "Illegal operation on non-sandbox study!")
88
89
    @classmethod
90
    def from_title(cls, title):
91
        """Instantiate Study from title
92
93
        Parameters
94
        ----------
95
        title : str
96
            Tht title to search for
97
98
        Returns
99
        -------
100
        Study
101
            The study with the given title
102
103
        Raises
104
        ------
105
        QiitaDBUnknownIDError
106
            If the title doesn't exist
107
        """
108
        with qdb.sql_connection.TRN:
109
            sql = """SELECT study_id
110
                     FROM qiita.{}
111
                     WHERE study_title = %s""".format(cls._table)
112
113
            qdb.sql_connection.TRN.add(sql, [title])
114
            sid = qdb.sql_connection.TRN.execute_fetchflatten()
115
116
        if not sid:
117
            raise qdb.exceptions.QiitaDBUnknownIDError(
118
                cls._table, f'"{title}" does not exist')
119
120
        return qdb.study.Study(sid[0])
121
122
    @classmethod
123
    def iter(cls):
124
        """Iterate over all studies in the database
125
126
        Returns
127
        -------
128
        generator
129
            Yields a `Study` object for each study in the database,
130
            in order of ascending study_id
131
        """
132
        with qdb.sql_connection.TRN:
133
            sql = """SELECT study_id FROM qiita.{}
134
                     ORDER BY study_id""".format(cls._table)
135
            qdb.sql_connection.TRN.add(sql)
136
137
            ids = qdb.sql_connection.TRN.execute_fetchflatten()
138
139
        for id_ in ids:
140
            yield Study(id_)
141
142
    @property
143
    def status(self):
144
        r"""The status is inferred by the status of its artifacts"""
145
        with qdb.sql_connection.TRN:
146
            # Get the status of all its artifacts
147
            sql = """SELECT DISTINCT visibility
148
                     FROM qiita.visibility
149
                        JOIN qiita.artifact USING (visibility_id)
150
                        JOIN qiita.study_artifact USING (artifact_id)
151
                     WHERE study_id = %s and visibility_id NOT IN %s"""
152
            qdb.sql_connection.TRN.add(
153
                sql, [self._id, qdb.util.artifact_visibilities_to_skip()])
154
            return qdb.util.infer_status(
155
                qdb.sql_connection.TRN.execute_fetchindex())
156
157
    @staticmethod
158
    def all_data_types():
159
        """Returns list of all the data types available in the system
160
161
        Returns
162
        -------
163
        list of str
164
            All the data types available in the system
165
        """
166
        with qdb.sql_connection.TRN:
167
            sql = "SELECT DISTINCT data_type FROM qiita.data_type"
168
            qdb.sql_connection.TRN.add(sql)
169
            return qdb.sql_connection.TRN.execute_fetchflatten()
170
171
    @classmethod
172
    def get_ids_by_status(cls, status):
173
        """Returns study id for all Studies with given status
174
175
        Parameters
176
        ----------
177
        status : str
178
            Status setting to search for
179
180
        Returns
181
        -------
182
        set of qiita_db.study.Study
183
            All studies in the database that match the given status
184
        """
185
        with qdb.sql_connection.TRN:
186
            sql = """SELECT DISTINCT study_id
187
                     FROM qiita.study_artifact
188
                        JOIN qiita.artifact USING (artifact_id)
189
                        JOIN qiita.visibility USING (visibility_id)
190
                        JOIN qiita.study_portal USING (study_id)
191
                        JOIN qiita.portal_type USING (portal_type_id)
192
                      WHERE visibility = %s AND portal = %s"""
193
            qdb.sql_connection.TRN.add(sql, [status, qiita_config.portal])
194
            sids = set(qdb.sql_connection.TRN.execute_fetchflatten())
195
            # If status is sandbox, all the studies that are not present in the
196
            # study_artifact table are also sandbox
197
            if status == 'sandbox':
198
                sql = """SELECT study_id
199
                         FROM qiita.study
200
                            JOIN qiita.study_portal USING (study_id)
201
                            JOIN qiita.portal_type USING (portal_type_id)
202
                         WHERE portal = %s AND study_id NOT IN (
203
                                SELECT study_id
204
                                FROM qiita.study_artifact)"""
205
                qdb.sql_connection.TRN.add(sql, [qiita_config.portal])
206
                sids = sids.union(
207
                    qdb.sql_connection.TRN.execute_fetchflatten())
208
209
            return sids
210
211
    @classmethod
212
    def get_by_status(cls, status):
213
        """Returns study id for all Studies with given status
214
215
        Parameters
216
        ----------
217
        status : str
218
            Status setting to search for
219
220
        Returns
221
        -------
222
        set of qiita_db.study.Study
223
            All studies in the database that match the given status
224
        """
225
        return set(cls(sid) for sid in cls.get_ids_by_status(status))
226
227
    @classmethod
228
    def get_info(cls, study_ids=None, info_cols=None):
229
        """Returns study data for a set of study_ids
230
231
        Parameters
232
        ----------
233
        study_ids : list of ints, optional
234
            Studies to get information for. Defauls to all studies
235
        info_cols: list of str, optional
236
            Information columns to retrieve. Defaults to all study data
237
238
        Returns
239
        -------
240
        list of DictCursor
241
            Table-like structure of metadata, one study per row. Can be
242
            accessed as a list of dictionaries, keyed on column name.
243
        """
244
        # The following tables are considered part of info
245
        _info_cols = frozenset(chain(
246
            qdb.util.get_table_cols('study'),
247
            qdb.util.get_table_cols('study_status'),
248
            qdb.util.get_table_cols('timeseries_type'),
249
            # placeholder for table study_publication
250
            ['publications']))
251
252
        if info_cols is None:
253
            info_cols = _info_cols
254
        elif not _info_cols.issuperset(info_cols):
255
            warnings.warn("Non-info columns passed: %s" % ", ".join(
256
                set(info_cols) - _info_cols))
257
258
        search_cols = ",".join(sorted(_info_cols.intersection(info_cols)))
259
260
        with qdb.sql_connection.TRN:
261
            sql = """SELECT {0}
262
                     FROM qiita.study
263
                     LEFT JOIN (
264
                            SELECT study_id,
265
                            array_agg(row_to_json((publication, is_doi), true))
266
                                AS publications
267
                            FROM qiita.study_publication
268
                            GROUP BY study_id)
269
                                AS full_publications
270
                        USING (study_id)
271
                     JOIN qiita.timeseries_type  USING (timeseries_type_id)
272
                     JOIN qiita.study_portal USING (study_id)
273
                     JOIN qiita.portal_type USING (portal_type_id)
274
                    WHERE portal = %s""".format(search_cols)
275
            args = [qiita_config.portal]
276
            if study_ids is not None:
277
                sql = "{0} AND study_id IN %s".format(sql)
278
                args.append(tuple(study_ids))
279
280
            qdb.sql_connection.TRN.add(sql, args)
281
            rows = qdb.sql_connection.TRN.execute_fetchindex()
282
            if study_ids is not None and len(rows) != len(study_ids):
283
                raise qdb.exceptions.QiitaDBError(
284
                    'Non-portal-accessible studies asked for!')
285
286
            res = []
287
            for r in rows:
288
                r = dict(r)
289
                if 'ebi_study_accession' in info_cols:
290
                    r['ebi_submission_status'] = cls(
291
                        r['study_id']).ebi_submission_status
292
                res.append(r)
293
294
            return res
295
296
    @classmethod
297
    def exists(cls, study_title):
298
        """Check if a study exists based on study_title, which is unique
299
300
        Parameters
301
        ----------
302
        study_title : str
303
            The title of the study to search for in the database
304
305
        Returns
306
        -------
307
        bool
308
        """
309
        with qdb.sql_connection.TRN:
310
            sql = """SELECT EXISTS(
311
                        SELECT study_id
312
                        FROM qiita.{}
313
                        WHERE study_title = %s)""".format(cls._table)
314
            qdb.sql_connection.TRN.add(sql, [study_title])
315
            return qdb.sql_connection.TRN.execute_fetchlast()
316
317
    @classmethod
318
    def create(cls, owner, title, info, investigation=None):
319
        """Creates a new study on the database
320
321
        Parameters
322
        ----------
323
        owner : User object
324
            the study's owner
325
        title : str
326
            Title of the study
327
        info : dict
328
            the information attached to the study. All "*_id" keys must pass
329
            the objects associated with them.
330
        investigation : Investigation object, optional
331
            If passed, the investigation to associate with. Defaults to None.
332
333
        Raises
334
        ------
335
        QiitaDBColumnError
336
            Non-db columns in info dictionary
337
            All required keys not passed
338
        IncompetentQiitaDeveloperError
339
            email, study_id, study_status_id, or study_title passed as a key
340
        QiitaDBDuplicateError
341
            If a study with the given title already exists
342
343
        Notes
344
        -----
345
        All keys in info, must be equal to columns in qiita.study table in the
346
        database.
347
        """
348
        # make sure not passing non-info columns in the info dict
349
        if cls._non_info.intersection(info):
350
            raise qdb.exceptions.QiitaDBColumnError(
351
                "non info keys passed: %s" % cls._non_info.intersection(info))
352
353
        # cleaning up title, this is also done in JS for the GUI but rather
354
        # be safe than sorry
355
        title = ' '.join(title.split()).strip()
356
357
        with qdb.sql_connection.TRN:
358
            if cls.exists(title):
359
                raise qdb.exceptions.QiitaDBDuplicateError(
360
                    "Study", "title: %s" % title)
361
362
            # add default values to info
363
            insertdict = deepcopy(info)
364
            insertdict['email'] = owner.id
365
            insertdict['study_title'] = title
366
            if "reprocess" not in insertdict:
367
                insertdict['reprocess'] = False
368
369
            # No nuns allowed
370
            insertdict = {k: v for k, v in insertdict.items()
371
                          if v is not None}
372
373
            # make sure dictionary only has keys for available columns in db
374
            qdb.util.check_table_cols(insertdict, cls._table)
375
            # make sure reqired columns in dictionary
376
            qdb.util.check_required_columns(insertdict, cls._table)
377
378
            # Insert study into database
379
            sql = """INSERT INTO qiita.{0} ({1})
380
                     VALUES ({2}) RETURNING study_id""".format(
381
                cls._table, ','.join(insertdict),
382
                ','.join(['%s'] * len(insertdict)))
383
384
            # make sure data in same order as sql column names,
385
            # and ids are used
386
            data = []
387
            for col in insertdict:
388
                if isinstance(insertdict[col], qdb.base.QiitaObject):
389
                    data.append(insertdict[col].id)
390
                else:
391
                    data.append(insertdict[col])
392
393
            qdb.sql_connection.TRN.add(sql, data)
394
            study_id = qdb.sql_connection.TRN.execute_fetchlast()
395
396
            # Add to both QIITA and given portal (if not QIITA)
397
            portal_id = qdb.util.convert_to_id(
398
                qiita_config.portal, 'portal_type', 'portal')
399
            sql = """INSERT INTO qiita.study_portal (study_id, portal_type_id)
400
                     VALUES (%s, %s)"""
401
            args = [[study_id, portal_id]]
402
            if qiita_config.portal != 'QIITA':
403
                qp_id = qdb.util.convert_to_id(
404
                    'QIITA', 'portal_type', 'portal')
405
                args.append([study_id, qp_id])
406
            qdb.sql_connection.TRN.add(sql, args, many=True)
407
            qdb.sql_connection.TRN.execute()
408
409
            # add study to investigation if necessary
410
            if investigation:
411
                sql = """INSERT INTO qiita.investigation_study
412
                            (investigation_id, study_id)
413
                         VALUES (%s, %s)"""
414
                qdb.sql_connection.TRN.add(sql, [investigation.id, study_id])
415
416
            qdb.sql_connection.TRN.execute()
417
418
            return cls(study_id)
419
420
    @classmethod
421
    def delete(cls, id_):
422
        r"""Deletes the study from the database
423
424
        Parameters
425
        ----------
426
        id_ : integer
427
            The object identifier
428
429
        Raises
430
        ------
431
        QiitaDBError
432
            If the sample_(id_) table exists means a sample template exists
433
        """
434
        with qdb.sql_connection.TRN:
435
            # checking that the id_ exists
436
            cls(id_)
437
438
            if qdb.util.exists_table('sample_%d' % id_):
439
                raise qdb.exceptions.QiitaDBError(
440
                    'Study "%s" cannot be erased because it has a '
441
                    'sample template' % cls(id_).title)
442
443
            args = [id_]
444
445
            sql = "DELETE FROM qiita.study_portal WHERE study_id = %s"
446
            qdb.sql_connection.TRN.add(sql, args)
447
448
            sql = "DELETE FROM qiita.study_publication WHERE study_id = %s"
449
            qdb.sql_connection.TRN.add(sql, args)
450
451
            sql = """DELETE FROM qiita.study_environmental_package
452
                     WHERE study_id = %s"""
453
            qdb.sql_connection.TRN.add(sql, args)
454
455
            sql = "DELETE FROM qiita.study_users WHERE study_id = %s"
456
            qdb.sql_connection.TRN.add(sql, args)
457
458
            sql = "DELETE FROM qiita.investigation_study WHERE study_id = %s"
459
            qdb.sql_connection.TRN.add(sql, args)
460
461
            sql = "DELETE FROM qiita.per_study_tags WHERE study_id = %s"
462
            qdb.sql_connection.TRN.add(sql, args)
463
464
            sql = "DELETE FROM qiita.study WHERE study_id = %s"
465
            qdb.sql_connection.TRN.add(sql, args)
466
467
            qdb.sql_connection.TRN.execute()
468
469
    @classmethod
470
    def get_tags(cls):
471
        """Returns the available study tags
472
473
        Returns
474
        -------
475
        list of DictCursor
476
            Table-like structure of metadata, one tag per row. Can be
477
            accessed as a list of dictionaries, keyed on column name.
478
        """
479
        with qdb.sql_connection.TRN:
480
            sql = """SELECT qiita.user_level.name AS user_level,
481
                        array_agg(study_tag ORDER BY study_tag)
482
                    FROM qiita.study_tags
483
                    LEFT JOIN qiita.qiita_user USING (email)
484
                    LEFT JOIN qiita.user_level USING (user_level_id)
485
                    GROUP BY qiita.user_level.name"""
486
487
            qdb.sql_connection.TRN.add(sql)
488
            results = dict(qdb.sql_connection.TRN.execute_fetchindex())
489
            # when the system is empty,
490
            # it's possible to get an empty dict, fixing
491
            if 'admin' not in results:
492
                results['admin'] = []
493
            if 'user' not in results:
494
                results['user'] = []
495
496
            return results
497
498
    @classmethod
499
    def insert_tags(cls, user, tags):
500
        """Insert available study tags
501
502
        Parameters
503
        ----------
504
        user : qiita_db.user.User
505
            The user adding the tags
506
        tags : list of str
507
            The list of tags to add
508
        """
509
        with qdb.sql_connection.TRN:
510
            email = user.email
511
            sql = """INSERT INTO qiita.study_tags (email, study_tag)
512
                     SELECT %s, %s WHERE NOT EXISTS (
513
                        SELECT 1 FROM qiita.study_tags WHERE study_tag = %s)"""
514
            sql_args = [[email, tag, tag] for tag in tags]
515
            qdb.sql_connection.TRN.add(sql, sql_args, many=True)
516
            qdb.sql_connection.TRN.execute()
517
518
# --- Attributes ---
519
    @property
520
    def autoloaded(self):
521
        """Returns if the study was autoloaded
522
523
        Returns
524
        -------
525
        bool
526
            If the study was autoloaded or not
527
        """
528
        with qdb.sql_connection.TRN:
529
            sql = """SELECT autoloaded FROM qiita.{0}
530
                     WHERE study_id = %s""".format(self._table)
531
            qdb.sql_connection.TRN.add(sql, [self._id])
532
            return qdb.sql_connection.TRN.execute_fetchlast()
533
534
    @autoloaded.setter
535
    def autoloaded(self, value):
536
        """Sets the autoloaded status of the study
537
538
        Parameters
539
        ----------
540
        value : bool
541
            Whether the study was autoloaded
542
        """
543
        sql = """UPDATE qiita.{0} SET autoloaded = %s
544
                 WHERE study_id = %s""".format(self._table)
545
        qdb.sql_connection.perform_as_transaction(sql, [value, self._id])
546
547
    @property
548
    def title(self):
549
        """Returns the title of the study
550
551
        Returns
552
        -------
553
        str
554
            Title of study
555
        """
556
        with qdb.sql_connection.TRN:
557
            sql = """SELECT study_title FROM qiita.{0}
558
                     WHERE study_id = %s""".format(self._table)
559
            qdb.sql_connection.TRN.add(sql, [self._id])
560
            return qdb.sql_connection.TRN.execute_fetchlast()
561
562
    @title.setter
563
    def title(self, title):
564
        """Sets the title of the study
565
566
        Parameters
567
        ----------
568
        title : str
569
            The study title
570
        """
571
        sql = """UPDATE qiita.{0} SET study_title = %s
572
                 WHERE study_id = %s""".format(self._table)
573
        qdb.sql_connection.perform_as_transaction(sql, [title, self._id])
574
575
    @property
576
    def notes(self):
577
        """Returns the notes of the study
578
579
        Returns
580
        -------
581
        str
582
            Study notes
583
        """
584
        with qdb.sql_connection.TRN:
585
            sql = """SELECT notes FROM qiita.{0}
586
                     WHERE study_id = %s""".format(self._table)
587
            qdb.sql_connection.TRN.add(sql, [self._id])
588
            return qdb.sql_connection.TRN.execute_fetchlast()
589
590
    @notes.setter
591
    def notes(self, notes):
592
        """Sets the notes of the study
593
594
        Parameters
595
        ----------
596
        notes : str
597
            The study notes
598
        """
599
        sql = """UPDATE qiita.{0} SET notes = %s
600
                 WHERE study_id = %s""".format(self._table)
601
        qdb.sql_connection.perform_as_transaction(sql, [notes, self._id])
602
603
    @property
604
    def public_raw_download(self):
605
        """Returns if the study's raw data is available for download
606
607
        Returns
608
        -------
609
        str
610
            public_raw_download of study
611
        """
612
        with qdb.sql_connection.TRN:
613
            sql = """SELECT public_raw_download FROM qiita.{0}
614
                     WHERE study_id = %s""".format(self._table)
615
            qdb.sql_connection.TRN.add(sql, [self._id])
616
            return qdb.sql_connection.TRN.execute_fetchlast()
617
618
    @public_raw_download.setter
619
    def public_raw_download(self, public_raw_download):
620
        """Sets if the study's raw data is available for download
621
622
        Parameters
623
        ----------
624
        public_raw_download : bool
625
            The study public_raw_download
626
        """
627
        sql = """UPDATE qiita.{0} SET public_raw_download = %s
628
                 WHERE study_id = %s""".format(self._table)
629
        qdb.sql_connection.perform_as_transaction(
630
            sql, [public_raw_download, self._id])
631
632
    @property
633
    def info(self):
634
        """Dict with all information attached to the study
635
636
        Returns
637
        -------
638
        dict
639
            info of study keyed to column names
640
        """
641
        with qdb.sql_connection.TRN:
642
            sql = "SELECT * FROM qiita.{0} WHERE study_id = %s".format(
643
                self._table)
644
            qdb.sql_connection.TRN.add(sql, [self._id])
645
            info = dict(qdb.sql_connection.TRN.execute_fetchindex()[0])
646
            # remove non-info items from info
647
            for item in self._non_info:
648
                info.pop(item)
649
            # removed because redundant to the id already stored in the object
650
            info.pop('study_id')
651
652
            if info['principal_investigator_id']:
653
                info['principal_investigator'] = qdb.study.StudyPerson(
654
                    info["principal_investigator_id"])
655
            else:
656
                info['principal_investigator'] = None
657
            del info['principal_investigator_id']
658
659
            if info['lab_person_id']:
660
                info['lab_person'] = qdb.study.StudyPerson(
661
                    info["lab_person_id"])
662
            else:
663
                info['lab_person'] = None
664
            del info['lab_person_id']
665
666
            return info
667
668
    @info.setter
669
    def info(self, info):
670
        """Updates the information attached to the study
671
672
        Parameters
673
        ----------
674
        info : dict
675
            information to change/update for the study, keyed to column name
676
677
        Raises
678
        ------
679
        IncompetentQiitaDeveloperError
680
            Empty dict passed
681
        QiitaDBColumnError
682
            Unknown column names passed
683
        """
684
        if not info:
685
            raise IncompetentQiitaDeveloperError("Need entries in info dict!")
686
687
        if 'study_id' in info:
688
            raise qdb.exceptions.QiitaDBColumnError("Cannot set study_id!")
689
690
        if self._non_info.intersection(info):
691
            raise qdb.exceptions.QiitaDBColumnError(
692
                "non info keys passed: %s" % self._non_info.intersection(info))
693
694
        with qdb.sql_connection.TRN:
695
            if 'timeseries_type_id' in info:
696
                # We only lock if the timeseries type changes
697
                self._lock_non_sandbox()
698
699
            # make sure dictionary only has keys for available columns in db
700
            qdb.util.check_table_cols(info, self._table)
701
702
            sql_vals = []
703
            data = []
704
            # build query with data values in correct order for SQL statement
705
            for key, val in info.items():
706
                sql_vals.append("{0} = %s".format(key))
707
                if isinstance(val, qdb.base.QiitaObject):
708
                    data.append(val.id)
709
                else:
710
                    data.append(val)
711
            data.append(self._id)
712
713
            sql = "UPDATE qiita.{0} SET {1} WHERE study_id = %s".format(
714
                self._table, ','.join(sql_vals))
715
            qdb.sql_connection.TRN.add(sql, data)
716
            qdb.sql_connection.TRN.execute()
717
718
    @property
719
    def shared_with(self):
720
        """list of users the study is shared with
721
722
        Returns
723
        -------
724
        list of qiita_db.user.User
725
            Users the study is shared with
726
        """
727
        with qdb.sql_connection.TRN:
728
            sql = """SELECT email FROM qiita.{0}_users
729
                     WHERE study_id = %s""".format(self._table)
730
            qdb.sql_connection.TRN.add(sql, [self._id])
731
            return [qdb.user.User(uid)
732
                    for uid in qdb.sql_connection.TRN.execute_fetchflatten()]
733
734
    @property
735
    def publications(self):
736
        """ Returns list of publications from this study
737
738
        Returns
739
        -------
740
        list of (str, str)
741
            list of all the DOI and pubmed ids
742
        """
743
        with qdb.sql_connection.TRN:
744
            sql = """SELECT publication, is_doi
745
                     FROM qiita.study_publication
746
                     WHERE study_id = %s"""
747
            qdb.sql_connection.TRN.add(sql, [self._id])
748
            return qdb.sql_connection.TRN.execute_fetchindex()
749
750
    @publications.setter
751
    def publications(self, values):
752
        """Sets the pmids for the study
753
754
        Parameters
755
        ----------
756
        values : list of (str, str)
757
            The list of (DOI, pubmed id) to associate with the study
758
759
        Raises
760
        ------
761
        TypeError
762
            If values is not a list
763
        """
764
        # Check that a list is actually passed
765
        if not isinstance(values, list):
766
            raise TypeError('publications should be a list')
767
768
        with qdb.sql_connection.TRN:
769
            # Delete the previous pmids associated with the study
770
            sql = "DELETE FROM qiita.study_publication WHERE study_id = %s"
771
            qdb.sql_connection.TRN.add(sql, [self._id])
772
773
            # Set the new ones
774
            sql = """INSERT INTO qiita.study_publication
775
                            (study_id, publication, is_doi)
776
                     VALUES (%s, %s, %s)"""
777
            sql_args = [[self._id, pub, is_doi] for pub, is_doi in values]
778
            qdb.sql_connection.TRN.add(sql, sql_args, many=True)
779
            qdb.sql_connection.TRN.execute()
780
781
    @property
782
    def investigation(self):
783
        """ Returns Investigation this study is part of
784
785
        If the study doesn't have an investigation associated with it, it will
786
        return None
787
788
        Returns
789
        -------
790
        qiita_db.investigation.Investigation or None
791
        """
792
        with qdb.sql_connection.TRN:
793
            sql = """SELECT investigation_id FROM qiita.investigation_study
794
                     WHERE study_id = %s"""
795
            qdb.sql_connection.TRN.add(sql, [self._id])
796
            inv = qdb.sql_connection.TRN.execute_fetchindex()
797
            # If this study belongs to an investigation it will be in
798
            # the first value of the first row [0][0]
799
            return qdb.investigation.Investigation(inv[0][0]) if inv else None
800
801
    @property
802
    def sample_template(self):
803
        """Returns sample_template information
804
805
        If the study doesn't have a sample template associated with it, it will
806
        return None
807
808
        Returns
809
        -------
810
        qiita_db.metadata_template.sample_template.SampleTemplate or None
811
        """
812
        with qdb.sql_connection.TRN:
813
            sql = """SELECT EXISTS(SELECT *
814
                                   FROM qiita.study_sample
815
                                   WHERE study_id = %s)"""
816
            qdb.sql_connection.TRN.add(sql, [self.id])
817
            exists = qdb.sql_connection.TRN.execute_fetchlast()
818
        return (qdb.metadata_template.sample_template.SampleTemplate(self._id)
819
                if exists else None)
820
821
    @property
822
    def data_types(self):
823
        """Returns list of the data types for this study
824
825
        Returns
826
        -------
827
        list of str
828
        """
829
        with qdb.sql_connection.TRN:
830
            sql = """SELECT DISTINCT data_type
831
                     FROM qiita.study_prep_template
832
                        JOIN qiita.prep_template USING (prep_template_id)
833
                        JOIN qiita.data_type USING (data_type_id)
834
                     WHERE study_id = %s"""
835
            qdb.sql_connection.TRN.add(sql, [self._id])
836
            return qdb.sql_connection.TRN.execute_fetchflatten()
837
838
    @property
839
    def owner(self):
840
        """Gets the owner of the study
841
842
        Returns
843
        -------
844
        qiita_db.user.User
845
            The user that owns this study
846
        """
847
        with qdb.sql_connection.TRN:
848
            sql = """SELECT email FROM qiita.{} WHERE study_id = %s""".format(
849
                self._table)
850
            qdb.sql_connection.TRN.add(sql, [self._id])
851
            return qdb.user.User(qdb.sql_connection.TRN.execute_fetchlast())
852
853
    @property
854
    def environmental_packages(self):
855
        """Gets the environmental packages associated with the study
856
857
        Returns
858
        -------
859
        list of str
860
            The environmental package names associated with the study
861
        """
862
        with qdb.sql_connection.TRN:
863
            sql = """SELECT environmental_package_name
864
                     FROM qiita.study_environmental_package
865
                     WHERE study_id = %s"""
866
            qdb.sql_connection.TRN.add(sql, [self._id])
867
            return qdb.sql_connection.TRN.execute_fetchflatten()
868
869
    @environmental_packages.setter
870
    def environmental_packages(self, values):
871
        """Sets the environmental packages for the study
872
873
        Parameters
874
        ----------
875
        values : list of str
876
            The list of environmental package names to associate with the study
877
878
        Raises
879
        ------
880
        TypeError
881
            If values is not a list
882
        ValueError
883
            If any environmental packages listed on values is not recognized
884
        """
885
        with qdb.sql_connection.TRN:
886
            # The environmental packages can be changed only if the study is
887
            # sandboxed
888
            self._lock_non_sandbox()
889
890
            # Check that a list is actually passed
891
            if not isinstance(values, list):
892
                raise TypeError('Environmental packages should be a list')
893
894
            # Get all the environmental packages
895
            env_pkgs = [pkg[0]
896
                        for pkg in qdb.util.get_environmental_packages()]
897
898
            # Check that all the passed values are valid environmental packages
899
            missing = set(values).difference(env_pkgs)
900
            if missing:
901
                raise ValueError('Environmetal package(s) not recognized: %s'
902
                                 % ', '.join(missing))
903
904
            # Delete the previous environmental packages associated with
905
            # the study
906
            sql = """DELETE FROM qiita.study_environmental_package
907
                     WHERE study_id=%s"""
908
            qdb.sql_connection.TRN.add(sql, [self._id])
909
910
            # Set the new ones
911
            sql = """INSERT INTO qiita.study_environmental_package
912
                        (study_id, environmental_package_name)
913
                     VALUES (%s, %s)"""
914
            sql_args = [[self._id, val] for val in values]
915
            qdb.sql_connection.TRN.add(sql, sql_args, many=True)
916
917
            qdb.sql_connection.TRN.execute()
918
919
    @property
920
    def _portals(self):
921
        """Portals this study is associated with
922
923
        Returns
924
        -------
925
        list of str
926
            Portal names study is associated with
927
        """
928
        with qdb.sql_connection.TRN:
929
            sql = """SELECT portal
930
                     FROM qiita.portal_type
931
                        JOIN qiita.study_portal USING (portal_type_id)
932
                     WHERE study_id = %s"""
933
            qdb.sql_connection.TRN.add(sql, [self._id])
934
            return qdb.sql_connection.TRN.execute_fetchflatten()
935
936
    @property
937
    def ebi_study_accession(self):
938
        """The EBI study accession for this study
939
940
        Returns
941
        -------
942
        str
943
            The study EBI accession
944
        """
945
        with qdb.sql_connection.TRN:
946
            sql = """SELECT ebi_study_accession
947
                     FROM qiita.{0}
948
                     WHERE study_id = %s""".format(self._table)
949
            qdb.sql_connection.TRN.add(sql, [self._id])
950
            return qdb.sql_connection.TRN.execute_fetchlast()
951
952
    @ebi_study_accession.setter
953
    def ebi_study_accession(self, value):
954
        """Sets the study's EBI study accession
955
956
        Parameters
957
        ----------
958
        value : str
959
            The new EBI study accession
960
961
        Raises
962
        ------
963
        QiitDBError
964
            If the study already has an EBI study accession
965
        """
966
        if self.ebi_study_accession is not None:
967
            raise qdb.exceptions.QiitaDBError(
968
                "Study %s already has an EBI study accession"
969
                % self.id)
970
        sql = """UPDATE qiita.{}
971
                 SET ebi_study_accession = %s
972
                 WHERE study_id = %s""".format(self._table)
973
        qdb.sql_connection.perform_as_transaction(sql, [value, self.id])
974
975
    def _ebi_submission_jobs(self):
976
        """Helper code to avoid duplication"""
977
        plugin = qdb.software.Software.from_name_and_version(
978
            'Qiita', 'alpha')
979
        cmd = plugin.get_command('submit_to_EBI')
980
981
        sql = """SELECT processing_job_id,
982
                    pj.command_parameters->>'artifact' as aid,
983
                    processing_job_status, can_be_submitted_to_ebi,
984
                    array_agg(ebi_run_accession)
985
                 FROM qiita.processing_job pj
986
                 LEFT JOIN qiita.processing_job_status
987
                    USING (processing_job_status_id)
988
                 LEFT JOIN qiita.artifact ON (
989
                    artifact_id = (
990
                        pj.command_parameters->>'artifact')::INT)
991
                 LEFT JOIN qiita.ebi_run_accession era USING (artifact_id)
992
                 LEFT JOIN qiita.artifact_type USING (artifact_type_id)
993
                 WHERE pj.command_parameters->>'artifact' IN (
994
                    SELECT artifact_id::text
995
                    FROM qiita.study_artifact WHERE study_id = {0})
996
                    AND pj.command_id = {1}
997
                 GROUP BY processing_job_id, aid, processing_job_status,
998
                    can_be_submitted_to_ebi""".format(self._id, cmd.id)
999
        qdb.sql_connection.TRN.add(sql)
1000
1001
        return qdb.sql_connection.TRN.execute_fetchindex()
1002
1003
    @property
1004
    def ebi_submission_status(self):
1005
        """The EBI submission status of this study
1006
1007
        Returns
1008
        -------
1009
        str
1010
            The study EBI submission status
1011
1012
        Notes
1013
        -----
1014
        There are 4 possible states: 'not submitted', 'submitting',
1015
        'submitted' & 'failed'. We are going to assume 'not submitted' if the
1016
        study doesn't have an accession, 'submitted' if it has an accession,
1017
        'submitting' if there are submit_to_EBI jobs running using the study
1018
        artifacts, & 'failed' if there are artifacts with failed jobs without
1019
        successful ones.
1020
        """
1021
        status = 'not submitted'
1022
        with qdb.sql_connection.TRN:
1023
            if self.ebi_study_accession:
1024
                status = 'submitted'
1025
1026
            jobs = defaultdict(dict)
1027
            for info in self._ebi_submission_jobs():
1028
                jid, aid, js, cbste, era = info
1029
                if not cbste or era != [None]:
1030
                    continue
1031
                jobs[js][aid] = jid
1032
1033
            if 'queued' in jobs or 'running' in jobs:
1034
                status = 'submitting'
1035
            elif 'error' in jobs:
1036
                aids_error = []
1037
                aids_other = []
1038
                for s, aids in jobs.items():
1039
                    for aid in aids.keys():
1040
                        if s == 'error':
1041
                            aids_error.append(aid)
1042
                        else:
1043
                            aids_other.append(aid)
1044
                difference = set(aids_error) - set(aids_other)
1045
                if difference:
1046
                    status = ('Some artifact submissions failed: %s' %
1047
                              ', '.join(map(str, list(difference))))
1048
1049
        return status
1050
1051
    @property
1052
    def tags(self):
1053
        """Returns the tags of the study
1054
1055
        Returns
1056
        -------
1057
        list of str
1058
            The study tags
1059
        """
1060
        with qdb.sql_connection.TRN:
1061
            sql = """SELECT study_tag
1062
                        FROM qiita.study_tags
1063
                        LEFT JOIN qiita.per_study_tags USING (study_tag)
1064
                        WHERE study_id = {0}
1065
                        ORDER BY study_tag""".format(self._id)
1066
            qdb.sql_connection.TRN.add(sql)
1067
            return [t[0] for t in qdb.sql_connection.TRN.execute_fetchindex()]
1068
1069
# --- methods ---
1070
    def artifacts(self, dtype=None, artifact_type=None):
1071
        """Returns the list of artifacts associated with the study
1072
1073
        Parameters
1074
        ----------
1075
        dtype : str, optional
1076
            If given, retrieve only artifacts for given data type. Default,
1077
            return all artifacts associated with the study.
1078
        artifact_type : str, optional
1079
            If given, retrieve only artifacts of given data type. Default,
1080
            return all artifacts associated with the study
1081
1082
        Returns
1083
        -------
1084
        list of qiita_db.artifact.Artifact
1085
        """
1086
        with qdb.sql_connection.TRN:
1087
            sql_args = [self._id]
1088
            sql_where = ""
1089
            if dtype:
1090
                sql_args.append(dtype)
1091
                sql_where = " AND data_type = %s"
1092
1093
            if artifact_type:
1094
                sql_args.append(artifact_type)
1095
                sql_where += " AND artifact_type = %s"
1096
1097
            sql = """SELECT artifact_id
1098
                     FROM qiita.artifact
1099
                        JOIN qiita.data_type USING (data_type_id)
1100
                        JOIN qiita.study_artifact USING (artifact_id)
1101
                        JOIN qiita.artifact_type USING (artifact_type_id)
1102
                     WHERE study_id = %s{0} AND visibility_id NOT IN %s
1103
                     ORDER BY artifact_id""".format(sql_where)
1104
            sql_args.append(qdb.util.artifact_visibilities_to_skip())
1105
1106
            qdb.sql_connection.TRN.add(sql, sql_args)
1107
            return [qdb.artifact.Artifact(aid)
1108
                    for aid in qdb.sql_connection.TRN.execute_fetchflatten()]
1109
1110
    def prep_templates(self, data_type=None):
1111
        """Return list of prep template ids
1112
1113
        Parameters
1114
        ----------
1115
        data_type : str, optional
1116
            If given, retrieve only prep templates for given datatype.
1117
            Default None.
1118
1119
        Returns
1120
        -------
1121
        list of qiita_db.metadata_template.prep_template.PrepTemplate
1122
        """
1123
        with qdb.sql_connection.TRN:
1124
            spec_data = ""
1125
            args = [self._id]
1126
            if data_type:
1127
                spec_data = " AND data_type_id = %s"
1128
                args.append(qdb.util.convert_to_id(data_type, "data_type"))
1129
1130
            sql = """SELECT prep_template_id
1131
                     FROM qiita.study_prep_template
1132
                        JOIN qiita.prep_template USING (prep_template_id)
1133
                     WHERE study_id = %s{0}
1134
                     ORDER BY prep_template_id""".format(spec_data)
1135
            qdb.sql_connection.TRN.add(sql, args)
1136
            return [qdb.metadata_template.prep_template.PrepTemplate(ptid)
1137
                    for ptid in qdb.sql_connection.TRN.execute_fetchflatten()]
1138
1139
    def analyses(self):
1140
        """Get all analyses where samples from this study have been used
1141
1142
        Returns
1143
        -------
1144
        list of qiita_db.analysis.Analysis
1145
        """
1146
        with qdb.sql_connection.TRN:
1147
            if self.sample_template is not None:
1148
                sids = self.sample_template.keys()
1149
                if sids:
1150
                    sql = """SELECT DISTINCT analysis_id
1151
                             FROM qiita.analysis_sample
1152
                             WHERE sample_id IN %s
1153
                             ORDER BY analysis_id"""
1154
                    qdb.sql_connection.TRN.add(
1155
                        sql, [tuple(self.sample_template.keys())])
1156
1157
                    return [qdb.analysis.Analysis(_id) for _id in
1158
                            qdb.sql_connection.TRN.execute_fetchflatten()]
1159
            return []
1160
1161
    def has_access(self, user, no_public=False):
1162
        """Returns whether the given user has access to the study
1163
1164
        Parameters
1165
        ----------
1166
        user : User object
1167
            User we are checking access for
1168
        no_public: bool
1169
            If we should ignore those studies shared with the user. Defaults
1170
            to False
1171
1172
        Returns
1173
        -------
1174
        bool
1175
            Whether user has access to study or not
1176
        """
1177
        with qdb.sql_connection.TRN:
1178
            # return True if the user is one of the admins
1179
            if user.level in {'superuser', 'admin'}:
1180
                return True
1181
1182
            # if no_public is False then just check if the study is public
1183
            # and return True
1184
            if not no_public and self.status == 'public':
1185
                return True
1186
1187
            # let's check if the study belongs to this user or has been
1188
            # shared with them
1189
            sql = """SELECT EXISTS (
1190
                        SELECT study_id
1191
                        FROM qiita.study
1192
                        JOIN qiita.study_portal USING (study_id)
1193
                        JOIN qiita.portal_type USING (portal_type_id)
1194
                        WHERE email = %s AND portal = %s AND study_id = %s
1195
                        UNION
1196
                        SELECT study_id
1197
                        FROM qiita.study_users
1198
                        JOIN qiita.study_portal USING (study_id)
1199
                        JOIN qiita.portal_type USING (portal_type_id)
1200
                        WHERE email = %s AND portal = %s AND study_id = %s
1201
                    )
1202
                  """
1203
            qdb.sql_connection.TRN.add(
1204
                sql, [user.email, qiita_config.portal, self.id,
1205
                      user.email, qiita_config.portal, self.id])
1206
            result = qdb.sql_connection.TRN.execute_fetchlast()
1207
1208
            return result
1209
1210
    def can_edit(self, user):
1211
        """Returns whether the given user can edit the study
1212
1213
        Parameters
1214
        ----------
1215
        user : User object
1216
            User we are checking edit permissions for
1217
1218
        Returns
1219
        -------
1220
        bool
1221
            Whether user can edit the study or not
1222
        """
1223
        # The study is editable only if the user is the owner, is in the shared
1224
        # list or the user is an admin
1225
        return (user.level in {'superuser', 'admin'} or self.owner == user or
1226
                user in self.shared_with)
1227
1228
    def share(self, user):
1229
        """Share the study with another user
1230
1231
        Parameters
1232
        ----------
1233
        user: User object
1234
            The user to share the study with
1235
        """
1236
        # Make sure the study is not already shared with the given user
1237
        if user in self.shared_with:
1238
            return
1239
        # Do not allow the study to be shared with the owner
1240
        if user == self.owner:
1241
            return
1242
1243
        sql = """INSERT INTO qiita.study_users (study_id, email)
1244
                 VALUES (%s, %s)"""
1245
        qdb.sql_connection.perform_as_transaction(sql, [self._id, user.id])
1246
1247
    def unshare(self, user):
1248
        """Unshare the study with another user
1249
1250
        Parameters
1251
        ----------
1252
        user: User object
1253
            The user to unshare the study with
1254
        """
1255
        sql = """DELETE FROM qiita.study_users
1256
                 WHERE study_id = %s AND email = %s"""
1257
        qdb.sql_connection.perform_as_transaction(sql, [self._id, user.id])
1258
1259
    def update_tags(self, user, tags):
1260
        """Sets the tags of the study
1261
1262
        Parameters
1263
        ----------
1264
        user: User object
1265
            The user reqesting the study tags update
1266
        tags : list of str
1267
            The tags to update within the study
1268
1269
        Returns
1270
        -------
1271
        str
1272
            Warnings during insertion
1273
        """
1274
        message = ''
1275
        # converting to set just to facilitate operations
1276
        system_tags_admin = set(self.get_tags()['admin'])
1277
        user_level = user.level
1278
        current_tags = set(self.tags)
1279
        to_delete = current_tags - set(tags)
1280
        to_add = set(tags) - current_tags
1281
1282
        if to_delete or to_add:
1283
            with qdb.sql_connection.TRN:
1284
                if to_delete:
1285
                    if user_level != 'admin':
1286
                        admin_tags = to_delete & system_tags_admin
1287
                        if admin_tags:
1288
                            message += 'You cannot remove: %s' % ', '.join(
1289
                                admin_tags)
1290
                        to_delete = to_delete - admin_tags
1291
1292
                    if to_delete:
1293
                        sql = """DELETE FROM qiita.per_study_tags
1294
                                     WHERE study_id = %s AND study_tag IN %s"""
1295
                        qdb.sql_connection.TRN.add(
1296
                            sql, [self._id, tuple(to_delete)])
1297
1298
                if to_add:
1299
                    if user_level != 'admin':
1300
                        admin_tags = to_add & system_tags_admin
1301
                        if admin_tags:
1302
                            message += ('Only admins can assign: '
1303
                                        '%s' % ', '.join(admin_tags))
1304
                        to_add = to_add - admin_tags
1305
1306
                    if to_add:
1307
                        self.insert_tags(user, to_add)
1308
1309
                        sql = """INSERT INTO qiita.per_study_tags
1310
                                    (study_tag, study_id)
1311
                                 SELECT %s, %s
1312
                                    WHERE
1313
                                        NOT EXISTS (
1314
                                            SELECT study_tag, study_id
1315
                                            FROM qiita.per_study_tags
1316
                                            WHERE study_tag = %s
1317
                                                AND study_id = %s
1318
                                        )"""
1319
                        sql_args = [[t, self._id, t, self._id] for t in to_add]
1320
                        qdb.sql_connection.TRN.add(sql, sql_args, many=True)
1321
1322
                qdb.sql_connection.TRN.execute()
1323
        else:
1324
            message = 'No changes in the tags.'
1325
1326
        return message
1327
1328
1329
class StudyPerson(qdb.base.QiitaObject):
1330
    r"""Object handling information pertaining to people involved in a study
1331
1332
    Attributes
1333
    ----------
1334
    name : str
1335
        name of the person
1336
    email : str
1337
        email of the person
1338
    affiliation : str
1339
        institution with which the person is affiliated
1340
    address : str or None
1341
        address of the person
1342
    phone : str or None
1343
        phone number of the person
1344
    """
1345
    _table = "study_person"
1346
1347
    @classmethod
1348
    def iter(cls):
1349
        """Iterate over all study people in the database
1350
1351
        Returns
1352
        -------
1353
        generator
1354
            Yields a `StudyPerson` object for each person in the database,
1355
            in order of ascending study_person_id
1356
        """
1357
        with qdb.sql_connection.TRN:
1358
            sql = """SELECT study_person_id FROM qiita.{}
1359
                     ORDER BY study_person_id""".format(cls._table)
1360
            qdb.sql_connection.TRN.add(sql)
1361
1362
            for id_ in qdb.sql_connection.TRN.execute_fetchflatten():
1363
                yield StudyPerson(id_)
1364
1365
    @classmethod
1366
    def exists(cls, name, affiliation):
1367
        """Checks if a person exists
1368
1369
        Parameters
1370
        ----------
1371
        name: str
1372
            Name of the person
1373
        affiliation : str
1374
            institution with which the person is affiliated
1375
1376
        Returns
1377
        -------
1378
        bool
1379
            True if person exists else false
1380
        """
1381
        with qdb.sql_connection.TRN:
1382
            sql = """SELECT EXISTS(
1383
                        SELECT * FROM qiita.{0}
1384
                        WHERE name = %s
1385
                            AND affiliation = %s)""".format(cls._table)
1386
            qdb.sql_connection.TRN.add(sql, [name, affiliation])
1387
            return qdb.sql_connection.TRN.execute_fetchlast()
1388
1389
    @classmethod
1390
    def from_name_and_affiliation(cls, name, affiliation):
1391
        """Gets a StudyPerson object based on the name and affiliation
1392
1393
        Parameters
1394
        ----------
1395
        name: str
1396
            Name of the person
1397
        affiliation : str
1398
            institution with which the person is affiliated
1399
1400
        Returns
1401
        -------
1402
        StudyPerson
1403
            The StudyPerson for the name and affiliation
1404
        """
1405
        with qdb.sql_connection.TRN:
1406
            if not cls.exists(name, affiliation):
1407
                raise qdb.exceptions.QiitaDBLookupError(
1408
                        'Study person does not exist')
1409
1410
            sql = """SELECT study_person_id FROM qiita.{0}
1411
                        WHERE name = %s
1412
                     AND affiliation = %s""".format(cls._table)
1413
            qdb.sql_connection.TRN.add(sql, [name, affiliation])
1414
            return cls(qdb.sql_connection.TRN.execute_fetchlast())
1415
1416
    @classmethod
1417
    def create(cls, name, email, affiliation, address=None, phone=None):
1418
        """Create a StudyPerson object, checking if person already exists.
1419
1420
        Parameters
1421
        ----------
1422
        name : str
1423
            name of person
1424
        email : str
1425
            email of person
1426
        affiliation : str
1427
            institution with which the person is affiliated
1428
        address : str, optional
1429
            address of person
1430
        phone : str, optional
1431
            phone number of person
1432
1433
        Returns
1434
        -------
1435
        New StudyPerson object
1436
1437
        """
1438
        with qdb.sql_connection.TRN:
1439
            if cls.exists(name, affiliation):
1440
                sql = """SELECT study_person_id
1441
                         FROM qiita.{0}
1442
                         WHERE name = %s
1443
                            AND affiliation = %s""".format(cls._table)
1444
                args = [name, affiliation]
1445
            else:
1446
                sql = """INSERT INTO qiita.{0} (name, email, affiliation,
1447
                                                address, phone)
1448
                         VALUES (%s, %s, %s, %s, %s)
1449
                         RETURNING study_person_id""".format(cls._table)
1450
                args = [name, email, affiliation, address, phone]
1451
1452
            qdb.sql_connection.TRN.add(sql, args)
1453
            return cls(qdb.sql_connection.TRN.execute_fetchlast())
1454
1455
    @classmethod
1456
    def delete(cls, id_):
1457
        r"""Deletes the StudyPerson from the database
1458
1459
        Parameters
1460
        ----------
1461
        id_ : integer
1462
            The object identifier
1463
1464
        Raises
1465
        ------
1466
        QiitaDBError
1467
            If the StudyPerson with the given id is attached to any study
1468
        """
1469
        with qdb.sql_connection.TRN:
1470
            # checking that the id_ exists
1471
            cls(id_)
1472
1473
            # Check if the person is attached to any study
1474
            sql = """SELECT EXISTS(
1475
                        SELECT *
1476
                        FROM qiita.study
1477
                        WHERE lab_person_id = %s OR
1478
                            principal_investigator_id = %s)"""
1479
            qdb.sql_connection.TRN.add(sql, [id_, id_])
1480
            if qdb.sql_connection.TRN.execute_fetchlast():
1481
                sql = """SELECT study_id
1482
                         FROM qiita.study
1483
                         WHERE {} = %s"""
1484
                cols = ['lab_person_id', 'principal_investigator_id']
1485
                rel = {}
1486
                for c in cols:
1487
                    qdb.sql_connection.TRN.add(sql.format(c), [id_])
1488
                    rel[c] = qdb.sql_connection.TRN.execute_fetchindex()
1489
                raise qdb.exceptions.QiitaDBError(
1490
                    'StudyPerson "%s" cannot be deleted because there are '
1491
                    'studies referencing it: %s' % (id_, str(rel)))
1492
1493
            sql = "DELETE FROM qiita.study_person WHERE study_person_id = %s"
1494
            qdb.sql_connection.TRN.add(sql, [id_])
1495
            qdb.sql_connection.TRN.execute()
1496
1497
    # Properties
1498
    @property
1499
    def name(self):
1500
        """Returns the name of the person
1501
1502
        Returns
1503
        -------
1504
        str
1505
            Name of person
1506
        """
1507
        with qdb.sql_connection.TRN:
1508
            sql = """SELECT name FROM qiita.{0}
1509
                     WHERE study_person_id = %s""".format(self._table)
1510
            qdb.sql_connection.TRN.add(sql, [self._id])
1511
            return qdb.sql_connection.TRN.execute_fetchlast()
1512
1513
    @property
1514
    def email(self):
1515
        """Returns the email of the person
1516
1517
        Returns
1518
        -------
1519
        str
1520
            Email of person
1521
        """
1522
        with qdb.sql_connection.TRN:
1523
            sql = """SELECT email FROM qiita.{0}
1524
                     WHERE study_person_id = %s""".format(self._table)
1525
            qdb.sql_connection.TRN.add(sql, [self._id])
1526
            return qdb.sql_connection.TRN.execute_fetchlast()
1527
1528
    @property
1529
    def affiliation(self):
1530
        """Returns the affiliation of the person
1531
1532
        Returns
1533
        -------
1534
        str
1535
            Affiliation of person
1536
        """
1537
        with qdb.sql_connection.TRN:
1538
            sql = """SELECT affiliation FROM qiita.{0}
1539
                     WHERE study_person_id = %s""".format(self._table)
1540
            qdb.sql_connection.TRN.add(sql, [self._id])
1541
            return qdb.sql_connection.TRN.execute_fetchlast()
1542
1543
    @property
1544
    def address(self):
1545
        """Returns the address of the person
1546
1547
        Returns
1548
        -------
1549
        str or None
1550
            address or None if no address in database
1551
        """
1552
        with qdb.sql_connection.TRN:
1553
            sql = """SELECT address FROM qiita.{0}
1554
                     WHERE study_person_id = %s""".format(self._table)
1555
            qdb.sql_connection.TRN.add(sql, [self._id])
1556
            return qdb.sql_connection.TRN.execute_fetchlast()
1557
1558
    @address.setter
1559
    def address(self, value):
1560
        """Set/update the address of the person
1561
1562
        Parameters
1563
        ----------
1564
        value : str
1565
            New address for person
1566
        """
1567
        sql = """UPDATE qiita.{0} SET address = %s
1568
                 WHERE study_person_id = %s""".format(self._table)
1569
        qdb.sql_connection.perform_as_transaction(sql, [value, self._id])
1570
1571
    @property
1572
    def phone(self):
1573
        """Returns the phone number of the person
1574
1575
        Returns
1576
        -------
1577
         str or None
1578
            phone or None if no address in database
1579
        """
1580
        with qdb.sql_connection.TRN:
1581
            sql = """SELECT phone FROM qiita.{0}
1582
                     WHERE study_person_id = %s""".format(self._table)
1583
            qdb.sql_connection.TRN.add(sql, [self._id])
1584
            return qdb.sql_connection.TRN.execute_fetchlast()
1585
1586
    @phone.setter
1587
    def phone(self, value):
1588
        """Set/update the phone number of the person
1589
1590
        Parameters
1591
        ----------
1592
        value : str
1593
            New phone number for person
1594
        """
1595
        sql = """UPDATE qiita.{0} SET phone = %s
1596
                 WHERE study_person_id = %s""".format(self._table)
1597
        qdb.sql_connection.perform_as_transaction(sql, [value, self._id])