--- a
+++ b/qiita_db/study.py
@@ -0,0 +1,1597 @@
+r"""
+Study and StudyPerson objects (:mod:`qiita_db.study`)
+=====================================================
+
+.. currentmodule:: qiita_db.study
+
+This module provides the implementation of the Study and StudyPerson classes.
+The study class allows access to all basic information including name and
+pmids associated with the study, as well as returning ids for the data,
+sample template, owner, and shared users. It is the central hub for creating,
+deleting, and accessing a study in the database.
+
+Contacts are taken care of by the StudyPerson class. This holds the contact's
+name, email, address, and phone of the various persons in a study, e.g. The PI
+or lab contact.
+
+Classes
+-------
+
+.. autosummary::
+   :toctree: generated/
+
+   Study
+   StudyPerson
+"""
+
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014--, The Qiita Development Team.
+#
+# Distributed under the terms of the BSD 3-clause License.
+#
+# The full license is in the file LICENSE, distributed with this software.
+# -----------------------------------------------------------------------------
+from collections import defaultdict
+from copy import deepcopy
+from itertools import chain
+import warnings
+
+from qiita_core.exceptions import IncompetentQiitaDeveloperError
+from qiita_core.qiita_settings import qiita_config
+import qiita_db as qdb
+
+
+class Study(qdb.base.QiitaObject):
+    r"""Study object to access to the Qiita Study information
+
+    Attributes
+    ----------
+    data_types
+    info
+    investigation
+    name
+    pmids
+    shared_with
+    sample_template
+    status
+    title
+    owner
+    autoloaded
+
+    Methods
+    -------
+    raw_data
+    preprocessed_data
+    processed_data
+    add_pmid
+    exists
+    has_access
+    share
+    unshare
+
+    Notes
+    -----
+    All setters raise QiitaDBStatusError if trying to change a public study.
+    You should not be doing that.
+    """
+    _table = "study"
+    _portal_table = "study_portal"
+    # The following columns are considered not part of the study info
+    _non_info = frozenset(["email", "study_title", "ebi_study_accession",
+                           "autoloaded"])
+
+    def _lock_non_sandbox(self):
+        """Raises QiitaDBStatusError if study is non-sandboxed"""
+        if self.status != 'sandbox':
+            raise qdb.exceptions.QiitaDBStatusError(
+                "Illegal operation on non-sandbox study!")
+
+    @classmethod
+    def from_title(cls, title):
+        """Instantiate Study from title
+
+        Parameters
+        ----------
+        title : str
+            Tht title to search for
+
+        Returns
+        -------
+        Study
+            The study with the given title
+
+        Raises
+        ------
+        QiitaDBUnknownIDError
+            If the title doesn't exist
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT study_id
+                     FROM qiita.{}
+                     WHERE study_title = %s""".format(cls._table)
+
+            qdb.sql_connection.TRN.add(sql, [title])
+            sid = qdb.sql_connection.TRN.execute_fetchflatten()
+
+        if not sid:
+            raise qdb.exceptions.QiitaDBUnknownIDError(
+                cls._table, f'"{title}" does not exist')
+
+        return qdb.study.Study(sid[0])
+
+    @classmethod
+    def iter(cls):
+        """Iterate over all studies in the database
+
+        Returns
+        -------
+        generator
+            Yields a `Study` object for each study in the database,
+            in order of ascending study_id
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT study_id FROM qiita.{}
+                     ORDER BY study_id""".format(cls._table)
+            qdb.sql_connection.TRN.add(sql)
+
+            ids = qdb.sql_connection.TRN.execute_fetchflatten()
+
+        for id_ in ids:
+            yield Study(id_)
+
+    @property
+    def status(self):
+        r"""The status is inferred by the status of its artifacts"""
+        with qdb.sql_connection.TRN:
+            # Get the status of all its artifacts
+            sql = """SELECT DISTINCT visibility
+                     FROM qiita.visibility
+                        JOIN qiita.artifact USING (visibility_id)
+                        JOIN qiita.study_artifact USING (artifact_id)
+                     WHERE study_id = %s and visibility_id NOT IN %s"""
+            qdb.sql_connection.TRN.add(
+                sql, [self._id, qdb.util.artifact_visibilities_to_skip()])
+            return qdb.util.infer_status(
+                qdb.sql_connection.TRN.execute_fetchindex())
+
+    @staticmethod
+    def all_data_types():
+        """Returns list of all the data types available in the system
+
+        Returns
+        -------
+        list of str
+            All the data types available in the system
+        """
+        with qdb.sql_connection.TRN:
+            sql = "SELECT DISTINCT data_type FROM qiita.data_type"
+            qdb.sql_connection.TRN.add(sql)
+            return qdb.sql_connection.TRN.execute_fetchflatten()
+
+    @classmethod
+    def get_ids_by_status(cls, status):
+        """Returns study id for all Studies with given status
+
+        Parameters
+        ----------
+        status : str
+            Status setting to search for
+
+        Returns
+        -------
+        set of qiita_db.study.Study
+            All studies in the database that match the given status
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT DISTINCT study_id
+                     FROM qiita.study_artifact
+                        JOIN qiita.artifact USING (artifact_id)
+                        JOIN qiita.visibility USING (visibility_id)
+                        JOIN qiita.study_portal USING (study_id)
+                        JOIN qiita.portal_type USING (portal_type_id)
+                      WHERE visibility = %s AND portal = %s"""
+            qdb.sql_connection.TRN.add(sql, [status, qiita_config.portal])
+            sids = set(qdb.sql_connection.TRN.execute_fetchflatten())
+            # If status is sandbox, all the studies that are not present in the
+            # study_artifact table are also sandbox
+            if status == 'sandbox':
+                sql = """SELECT study_id
+                         FROM qiita.study
+                            JOIN qiita.study_portal USING (study_id)
+                            JOIN qiita.portal_type USING (portal_type_id)
+                         WHERE portal = %s AND study_id NOT IN (
+                                SELECT study_id
+                                FROM qiita.study_artifact)"""
+                qdb.sql_connection.TRN.add(sql, [qiita_config.portal])
+                sids = sids.union(
+                    qdb.sql_connection.TRN.execute_fetchflatten())
+
+            return sids
+
+    @classmethod
+    def get_by_status(cls, status):
+        """Returns study id for all Studies with given status
+
+        Parameters
+        ----------
+        status : str
+            Status setting to search for
+
+        Returns
+        -------
+        set of qiita_db.study.Study
+            All studies in the database that match the given status
+        """
+        return set(cls(sid) for sid in cls.get_ids_by_status(status))
+
+    @classmethod
+    def get_info(cls, study_ids=None, info_cols=None):
+        """Returns study data for a set of study_ids
+
+        Parameters
+        ----------
+        study_ids : list of ints, optional
+            Studies to get information for. Defauls to all studies
+        info_cols: list of str, optional
+            Information columns to retrieve. Defaults to all study data
+
+        Returns
+        -------
+        list of DictCursor
+            Table-like structure of metadata, one study per row. Can be
+            accessed as a list of dictionaries, keyed on column name.
+        """
+        # The following tables are considered part of info
+        _info_cols = frozenset(chain(
+            qdb.util.get_table_cols('study'),
+            qdb.util.get_table_cols('study_status'),
+            qdb.util.get_table_cols('timeseries_type'),
+            # placeholder for table study_publication
+            ['publications']))
+
+        if info_cols is None:
+            info_cols = _info_cols
+        elif not _info_cols.issuperset(info_cols):
+            warnings.warn("Non-info columns passed: %s" % ", ".join(
+                set(info_cols) - _info_cols))
+
+        search_cols = ",".join(sorted(_info_cols.intersection(info_cols)))
+
+        with qdb.sql_connection.TRN:
+            sql = """SELECT {0}
+                     FROM qiita.study
+                     LEFT JOIN (
+                            SELECT study_id,
+                            array_agg(row_to_json((publication, is_doi), true))
+                                AS publications
+                            FROM qiita.study_publication
+                            GROUP BY study_id)
+                                AS full_publications
+                        USING (study_id)
+                     JOIN qiita.timeseries_type  USING (timeseries_type_id)
+                     JOIN qiita.study_portal USING (study_id)
+                     JOIN qiita.portal_type USING (portal_type_id)
+                    WHERE portal = %s""".format(search_cols)
+            args = [qiita_config.portal]
+            if study_ids is not None:
+                sql = "{0} AND study_id IN %s".format(sql)
+                args.append(tuple(study_ids))
+
+            qdb.sql_connection.TRN.add(sql, args)
+            rows = qdb.sql_connection.TRN.execute_fetchindex()
+            if study_ids is not None and len(rows) != len(study_ids):
+                raise qdb.exceptions.QiitaDBError(
+                    'Non-portal-accessible studies asked for!')
+
+            res = []
+            for r in rows:
+                r = dict(r)
+                if 'ebi_study_accession' in info_cols:
+                    r['ebi_submission_status'] = cls(
+                        r['study_id']).ebi_submission_status
+                res.append(r)
+
+            return res
+
+    @classmethod
+    def exists(cls, study_title):
+        """Check if a study exists based on study_title, which is unique
+
+        Parameters
+        ----------
+        study_title : str
+            The title of the study to search for in the database
+
+        Returns
+        -------
+        bool
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT EXISTS(
+                        SELECT study_id
+                        FROM qiita.{}
+                        WHERE study_title = %s)""".format(cls._table)
+            qdb.sql_connection.TRN.add(sql, [study_title])
+            return qdb.sql_connection.TRN.execute_fetchlast()
+
+    @classmethod
+    def create(cls, owner, title, info, investigation=None):
+        """Creates a new study on the database
+
+        Parameters
+        ----------
+        owner : User object
+            the study's owner
+        title : str
+            Title of the study
+        info : dict
+            the information attached to the study. All "*_id" keys must pass
+            the objects associated with them.
+        investigation : Investigation object, optional
+            If passed, the investigation to associate with. Defaults to None.
+
+        Raises
+        ------
+        QiitaDBColumnError
+            Non-db columns in info dictionary
+            All required keys not passed
+        IncompetentQiitaDeveloperError
+            email, study_id, study_status_id, or study_title passed as a key
+        QiitaDBDuplicateError
+            If a study with the given title already exists
+
+        Notes
+        -----
+        All keys in info, must be equal to columns in qiita.study table in the
+        database.
+        """
+        # make sure not passing non-info columns in the info dict
+        if cls._non_info.intersection(info):
+            raise qdb.exceptions.QiitaDBColumnError(
+                "non info keys passed: %s" % cls._non_info.intersection(info))
+
+        # cleaning up title, this is also done in JS for the GUI but rather
+        # be safe than sorry
+        title = ' '.join(title.split()).strip()
+
+        with qdb.sql_connection.TRN:
+            if cls.exists(title):
+                raise qdb.exceptions.QiitaDBDuplicateError(
+                    "Study", "title: %s" % title)
+
+            # add default values to info
+            insertdict = deepcopy(info)
+            insertdict['email'] = owner.id
+            insertdict['study_title'] = title
+            if "reprocess" not in insertdict:
+                insertdict['reprocess'] = False
+
+            # No nuns allowed
+            insertdict = {k: v for k, v in insertdict.items()
+                          if v is not None}
+
+            # make sure dictionary only has keys for available columns in db
+            qdb.util.check_table_cols(insertdict, cls._table)
+            # make sure reqired columns in dictionary
+            qdb.util.check_required_columns(insertdict, cls._table)
+
+            # Insert study into database
+            sql = """INSERT INTO qiita.{0} ({1})
+                     VALUES ({2}) RETURNING study_id""".format(
+                cls._table, ','.join(insertdict),
+                ','.join(['%s'] * len(insertdict)))
+
+            # make sure data in same order as sql column names,
+            # and ids are used
+            data = []
+            for col in insertdict:
+                if isinstance(insertdict[col], qdb.base.QiitaObject):
+                    data.append(insertdict[col].id)
+                else:
+                    data.append(insertdict[col])
+
+            qdb.sql_connection.TRN.add(sql, data)
+            study_id = qdb.sql_connection.TRN.execute_fetchlast()
+
+            # Add to both QIITA and given portal (if not QIITA)
+            portal_id = qdb.util.convert_to_id(
+                qiita_config.portal, 'portal_type', 'portal')
+            sql = """INSERT INTO qiita.study_portal (study_id, portal_type_id)
+                     VALUES (%s, %s)"""
+            args = [[study_id, portal_id]]
+            if qiita_config.portal != 'QIITA':
+                qp_id = qdb.util.convert_to_id(
+                    'QIITA', 'portal_type', 'portal')
+                args.append([study_id, qp_id])
+            qdb.sql_connection.TRN.add(sql, args, many=True)
+            qdb.sql_connection.TRN.execute()
+
+            # add study to investigation if necessary
+            if investigation:
+                sql = """INSERT INTO qiita.investigation_study
+                            (investigation_id, study_id)
+                         VALUES (%s, %s)"""
+                qdb.sql_connection.TRN.add(sql, [investigation.id, study_id])
+
+            qdb.sql_connection.TRN.execute()
+
+            return cls(study_id)
+
+    @classmethod
+    def delete(cls, id_):
+        r"""Deletes the study from the database
+
+        Parameters
+        ----------
+        id_ : integer
+            The object identifier
+
+        Raises
+        ------
+        QiitaDBError
+            If the sample_(id_) table exists means a sample template exists
+        """
+        with qdb.sql_connection.TRN:
+            # checking that the id_ exists
+            cls(id_)
+
+            if qdb.util.exists_table('sample_%d' % id_):
+                raise qdb.exceptions.QiitaDBError(
+                    'Study "%s" cannot be erased because it has a '
+                    'sample template' % cls(id_).title)
+
+            args = [id_]
+
+            sql = "DELETE FROM qiita.study_portal WHERE study_id = %s"
+            qdb.sql_connection.TRN.add(sql, args)
+
+            sql = "DELETE FROM qiita.study_publication WHERE study_id = %s"
+            qdb.sql_connection.TRN.add(sql, args)
+
+            sql = """DELETE FROM qiita.study_environmental_package
+                     WHERE study_id = %s"""
+            qdb.sql_connection.TRN.add(sql, args)
+
+            sql = "DELETE FROM qiita.study_users WHERE study_id = %s"
+            qdb.sql_connection.TRN.add(sql, args)
+
+            sql = "DELETE FROM qiita.investigation_study WHERE study_id = %s"
+            qdb.sql_connection.TRN.add(sql, args)
+
+            sql = "DELETE FROM qiita.per_study_tags WHERE study_id = %s"
+            qdb.sql_connection.TRN.add(sql, args)
+
+            sql = "DELETE FROM qiita.study WHERE study_id = %s"
+            qdb.sql_connection.TRN.add(sql, args)
+
+            qdb.sql_connection.TRN.execute()
+
+    @classmethod
+    def get_tags(cls):
+        """Returns the available study tags
+
+        Returns
+        -------
+        list of DictCursor
+            Table-like structure of metadata, one tag per row. Can be
+            accessed as a list of dictionaries, keyed on column name.
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT qiita.user_level.name AS user_level,
+                        array_agg(study_tag ORDER BY study_tag)
+                    FROM qiita.study_tags
+                    LEFT JOIN qiita.qiita_user USING (email)
+                    LEFT JOIN qiita.user_level USING (user_level_id)
+                    GROUP BY qiita.user_level.name"""
+
+            qdb.sql_connection.TRN.add(sql)
+            results = dict(qdb.sql_connection.TRN.execute_fetchindex())
+            # when the system is empty,
+            # it's possible to get an empty dict, fixing
+            if 'admin' not in results:
+                results['admin'] = []
+            if 'user' not in results:
+                results['user'] = []
+
+            return results
+
+    @classmethod
+    def insert_tags(cls, user, tags):
+        """Insert available study tags
+
+        Parameters
+        ----------
+        user : qiita_db.user.User
+            The user adding the tags
+        tags : list of str
+            The list of tags to add
+        """
+        with qdb.sql_connection.TRN:
+            email = user.email
+            sql = """INSERT INTO qiita.study_tags (email, study_tag)
+                     SELECT %s, %s WHERE NOT EXISTS (
+                        SELECT 1 FROM qiita.study_tags WHERE study_tag = %s)"""
+            sql_args = [[email, tag, tag] for tag in tags]
+            qdb.sql_connection.TRN.add(sql, sql_args, many=True)
+            qdb.sql_connection.TRN.execute()
+
+# --- Attributes ---
+    @property
+    def autoloaded(self):
+        """Returns if the study was autoloaded
+
+        Returns
+        -------
+        bool
+            If the study was autoloaded or not
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT autoloaded FROM qiita.{0}
+                     WHERE study_id = %s""".format(self._table)
+            qdb.sql_connection.TRN.add(sql, [self._id])
+            return qdb.sql_connection.TRN.execute_fetchlast()
+
+    @autoloaded.setter
+    def autoloaded(self, value):
+        """Sets the autoloaded status of the study
+
+        Parameters
+        ----------
+        value : bool
+            Whether the study was autoloaded
+        """
+        sql = """UPDATE qiita.{0} SET autoloaded = %s
+                 WHERE study_id = %s""".format(self._table)
+        qdb.sql_connection.perform_as_transaction(sql, [value, self._id])
+
+    @property
+    def title(self):
+        """Returns the title of the study
+
+        Returns
+        -------
+        str
+            Title of study
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT study_title FROM qiita.{0}
+                     WHERE study_id = %s""".format(self._table)
+            qdb.sql_connection.TRN.add(sql, [self._id])
+            return qdb.sql_connection.TRN.execute_fetchlast()
+
+    @title.setter
+    def title(self, title):
+        """Sets the title of the study
+
+        Parameters
+        ----------
+        title : str
+            The study title
+        """
+        sql = """UPDATE qiita.{0} SET study_title = %s
+                 WHERE study_id = %s""".format(self._table)
+        qdb.sql_connection.perform_as_transaction(sql, [title, self._id])
+
+    @property
+    def notes(self):
+        """Returns the notes of the study
+
+        Returns
+        -------
+        str
+            Study notes
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT notes FROM qiita.{0}
+                     WHERE study_id = %s""".format(self._table)
+            qdb.sql_connection.TRN.add(sql, [self._id])
+            return qdb.sql_connection.TRN.execute_fetchlast()
+
+    @notes.setter
+    def notes(self, notes):
+        """Sets the notes of the study
+
+        Parameters
+        ----------
+        notes : str
+            The study notes
+        """
+        sql = """UPDATE qiita.{0} SET notes = %s
+                 WHERE study_id = %s""".format(self._table)
+        qdb.sql_connection.perform_as_transaction(sql, [notes, self._id])
+
+    @property
+    def public_raw_download(self):
+        """Returns if the study's raw data is available for download
+
+        Returns
+        -------
+        str
+            public_raw_download of study
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT public_raw_download FROM qiita.{0}
+                     WHERE study_id = %s""".format(self._table)
+            qdb.sql_connection.TRN.add(sql, [self._id])
+            return qdb.sql_connection.TRN.execute_fetchlast()
+
+    @public_raw_download.setter
+    def public_raw_download(self, public_raw_download):
+        """Sets if the study's raw data is available for download
+
+        Parameters
+        ----------
+        public_raw_download : bool
+            The study public_raw_download
+        """
+        sql = """UPDATE qiita.{0} SET public_raw_download = %s
+                 WHERE study_id = %s""".format(self._table)
+        qdb.sql_connection.perform_as_transaction(
+            sql, [public_raw_download, self._id])
+
+    @property
+    def info(self):
+        """Dict with all information attached to the study
+
+        Returns
+        -------
+        dict
+            info of study keyed to column names
+        """
+        with qdb.sql_connection.TRN:
+            sql = "SELECT * FROM qiita.{0} WHERE study_id = %s".format(
+                self._table)
+            qdb.sql_connection.TRN.add(sql, [self._id])
+            info = dict(qdb.sql_connection.TRN.execute_fetchindex()[0])
+            # remove non-info items from info
+            for item in self._non_info:
+                info.pop(item)
+            # removed because redundant to the id already stored in the object
+            info.pop('study_id')
+
+            if info['principal_investigator_id']:
+                info['principal_investigator'] = qdb.study.StudyPerson(
+                    info["principal_investigator_id"])
+            else:
+                info['principal_investigator'] = None
+            del info['principal_investigator_id']
+
+            if info['lab_person_id']:
+                info['lab_person'] = qdb.study.StudyPerson(
+                    info["lab_person_id"])
+            else:
+                info['lab_person'] = None
+            del info['lab_person_id']
+
+            return info
+
+    @info.setter
+    def info(self, info):
+        """Updates the information attached to the study
+
+        Parameters
+        ----------
+        info : dict
+            information to change/update for the study, keyed to column name
+
+        Raises
+        ------
+        IncompetentQiitaDeveloperError
+            Empty dict passed
+        QiitaDBColumnError
+            Unknown column names passed
+        """
+        if not info:
+            raise IncompetentQiitaDeveloperError("Need entries in info dict!")
+
+        if 'study_id' in info:
+            raise qdb.exceptions.QiitaDBColumnError("Cannot set study_id!")
+
+        if self._non_info.intersection(info):
+            raise qdb.exceptions.QiitaDBColumnError(
+                "non info keys passed: %s" % self._non_info.intersection(info))
+
+        with qdb.sql_connection.TRN:
+            if 'timeseries_type_id' in info:
+                # We only lock if the timeseries type changes
+                self._lock_non_sandbox()
+
+            # make sure dictionary only has keys for available columns in db
+            qdb.util.check_table_cols(info, self._table)
+
+            sql_vals = []
+            data = []
+            # build query with data values in correct order for SQL statement
+            for key, val in info.items():
+                sql_vals.append("{0} = %s".format(key))
+                if isinstance(val, qdb.base.QiitaObject):
+                    data.append(val.id)
+                else:
+                    data.append(val)
+            data.append(self._id)
+
+            sql = "UPDATE qiita.{0} SET {1} WHERE study_id = %s".format(
+                self._table, ','.join(sql_vals))
+            qdb.sql_connection.TRN.add(sql, data)
+            qdb.sql_connection.TRN.execute()
+
+    @property
+    def shared_with(self):
+        """list of users the study is shared with
+
+        Returns
+        -------
+        list of qiita_db.user.User
+            Users the study is shared with
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT email FROM qiita.{0}_users
+                     WHERE study_id = %s""".format(self._table)
+            qdb.sql_connection.TRN.add(sql, [self._id])
+            return [qdb.user.User(uid)
+                    for uid in qdb.sql_connection.TRN.execute_fetchflatten()]
+
+    @property
+    def publications(self):
+        """ Returns list of publications from this study
+
+        Returns
+        -------
+        list of (str, str)
+            list of all the DOI and pubmed ids
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT publication, is_doi
+                     FROM qiita.study_publication
+                     WHERE study_id = %s"""
+            qdb.sql_connection.TRN.add(sql, [self._id])
+            return qdb.sql_connection.TRN.execute_fetchindex()
+
+    @publications.setter
+    def publications(self, values):
+        """Sets the pmids for the study
+
+        Parameters
+        ----------
+        values : list of (str, str)
+            The list of (DOI, pubmed id) to associate with the study
+
+        Raises
+        ------
+        TypeError
+            If values is not a list
+        """
+        # Check that a list is actually passed
+        if not isinstance(values, list):
+            raise TypeError('publications should be a list')
+
+        with qdb.sql_connection.TRN:
+            # Delete the previous pmids associated with the study
+            sql = "DELETE FROM qiita.study_publication WHERE study_id = %s"
+            qdb.sql_connection.TRN.add(sql, [self._id])
+
+            # Set the new ones
+            sql = """INSERT INTO qiita.study_publication
+                            (study_id, publication, is_doi)
+                     VALUES (%s, %s, %s)"""
+            sql_args = [[self._id, pub, is_doi] for pub, is_doi in values]
+            qdb.sql_connection.TRN.add(sql, sql_args, many=True)
+            qdb.sql_connection.TRN.execute()
+
+    @property
+    def investigation(self):
+        """ Returns Investigation this study is part of
+
+        If the study doesn't have an investigation associated with it, it will
+        return None
+
+        Returns
+        -------
+        qiita_db.investigation.Investigation or None
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT investigation_id FROM qiita.investigation_study
+                     WHERE study_id = %s"""
+            qdb.sql_connection.TRN.add(sql, [self._id])
+            inv = qdb.sql_connection.TRN.execute_fetchindex()
+            # If this study belongs to an investigation it will be in
+            # the first value of the first row [0][0]
+            return qdb.investigation.Investigation(inv[0][0]) if inv else None
+
+    @property
+    def sample_template(self):
+        """Returns sample_template information
+
+        If the study doesn't have a sample template associated with it, it will
+        return None
+
+        Returns
+        -------
+        qiita_db.metadata_template.sample_template.SampleTemplate or None
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT EXISTS(SELECT *
+                                   FROM qiita.study_sample
+                                   WHERE study_id = %s)"""
+            qdb.sql_connection.TRN.add(sql, [self.id])
+            exists = qdb.sql_connection.TRN.execute_fetchlast()
+        return (qdb.metadata_template.sample_template.SampleTemplate(self._id)
+                if exists else None)
+
+    @property
+    def data_types(self):
+        """Returns list of the data types for this study
+
+        Returns
+        -------
+        list of str
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT DISTINCT data_type
+                     FROM qiita.study_prep_template
+                        JOIN qiita.prep_template USING (prep_template_id)
+                        JOIN qiita.data_type USING (data_type_id)
+                     WHERE study_id = %s"""
+            qdb.sql_connection.TRN.add(sql, [self._id])
+            return qdb.sql_connection.TRN.execute_fetchflatten()
+
+    @property
+    def owner(self):
+        """Gets the owner of the study
+
+        Returns
+        -------
+        qiita_db.user.User
+            The user that owns this study
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT email FROM qiita.{} WHERE study_id = %s""".format(
+                self._table)
+            qdb.sql_connection.TRN.add(sql, [self._id])
+            return qdb.user.User(qdb.sql_connection.TRN.execute_fetchlast())
+
+    @property
+    def environmental_packages(self):
+        """Gets the environmental packages associated with the study
+
+        Returns
+        -------
+        list of str
+            The environmental package names associated with the study
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT environmental_package_name
+                     FROM qiita.study_environmental_package
+                     WHERE study_id = %s"""
+            qdb.sql_connection.TRN.add(sql, [self._id])
+            return qdb.sql_connection.TRN.execute_fetchflatten()
+
+    @environmental_packages.setter
+    def environmental_packages(self, values):
+        """Sets the environmental packages for the study
+
+        Parameters
+        ----------
+        values : list of str
+            The list of environmental package names to associate with the study
+
+        Raises
+        ------
+        TypeError
+            If values is not a list
+        ValueError
+            If any environmental packages listed on values is not recognized
+        """
+        with qdb.sql_connection.TRN:
+            # The environmental packages can be changed only if the study is
+            # sandboxed
+            self._lock_non_sandbox()
+
+            # Check that a list is actually passed
+            if not isinstance(values, list):
+                raise TypeError('Environmental packages should be a list')
+
+            # Get all the environmental packages
+            env_pkgs = [pkg[0]
+                        for pkg in qdb.util.get_environmental_packages()]
+
+            # Check that all the passed values are valid environmental packages
+            missing = set(values).difference(env_pkgs)
+            if missing:
+                raise ValueError('Environmetal package(s) not recognized: %s'
+                                 % ', '.join(missing))
+
+            # Delete the previous environmental packages associated with
+            # the study
+            sql = """DELETE FROM qiita.study_environmental_package
+                     WHERE study_id=%s"""
+            qdb.sql_connection.TRN.add(sql, [self._id])
+
+            # Set the new ones
+            sql = """INSERT INTO qiita.study_environmental_package
+                        (study_id, environmental_package_name)
+                     VALUES (%s, %s)"""
+            sql_args = [[self._id, val] for val in values]
+            qdb.sql_connection.TRN.add(sql, sql_args, many=True)
+
+            qdb.sql_connection.TRN.execute()
+
+    @property
+    def _portals(self):
+        """Portals this study is associated with
+
+        Returns
+        -------
+        list of str
+            Portal names study is associated with
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT portal
+                     FROM qiita.portal_type
+                        JOIN qiita.study_portal USING (portal_type_id)
+                     WHERE study_id = %s"""
+            qdb.sql_connection.TRN.add(sql, [self._id])
+            return qdb.sql_connection.TRN.execute_fetchflatten()
+
+    @property
+    def ebi_study_accession(self):
+        """The EBI study accession for this study
+
+        Returns
+        -------
+        str
+            The study EBI accession
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT ebi_study_accession
+                     FROM qiita.{0}
+                     WHERE study_id = %s""".format(self._table)
+            qdb.sql_connection.TRN.add(sql, [self._id])
+            return qdb.sql_connection.TRN.execute_fetchlast()
+
+    @ebi_study_accession.setter
+    def ebi_study_accession(self, value):
+        """Sets the study's EBI study accession
+
+        Parameters
+        ----------
+        value : str
+            The new EBI study accession
+
+        Raises
+        ------
+        QiitDBError
+            If the study already has an EBI study accession
+        """
+        if self.ebi_study_accession is not None:
+            raise qdb.exceptions.QiitaDBError(
+                "Study %s already has an EBI study accession"
+                % self.id)
+        sql = """UPDATE qiita.{}
+                 SET ebi_study_accession = %s
+                 WHERE study_id = %s""".format(self._table)
+        qdb.sql_connection.perform_as_transaction(sql, [value, self.id])
+
+    def _ebi_submission_jobs(self):
+        """Helper code to avoid duplication"""
+        plugin = qdb.software.Software.from_name_and_version(
+            'Qiita', 'alpha')
+        cmd = plugin.get_command('submit_to_EBI')
+
+        sql = """SELECT processing_job_id,
+                    pj.command_parameters->>'artifact' as aid,
+                    processing_job_status, can_be_submitted_to_ebi,
+                    array_agg(ebi_run_accession)
+                 FROM qiita.processing_job pj
+                 LEFT JOIN qiita.processing_job_status
+                    USING (processing_job_status_id)
+                 LEFT JOIN qiita.artifact ON (
+                    artifact_id = (
+                        pj.command_parameters->>'artifact')::INT)
+                 LEFT JOIN qiita.ebi_run_accession era USING (artifact_id)
+                 LEFT JOIN qiita.artifact_type USING (artifact_type_id)
+                 WHERE pj.command_parameters->>'artifact' IN (
+                    SELECT artifact_id::text
+                    FROM qiita.study_artifact WHERE study_id = {0})
+                    AND pj.command_id = {1}
+                 GROUP BY processing_job_id, aid, processing_job_status,
+                    can_be_submitted_to_ebi""".format(self._id, cmd.id)
+        qdb.sql_connection.TRN.add(sql)
+
+        return qdb.sql_connection.TRN.execute_fetchindex()
+
+    @property
+    def ebi_submission_status(self):
+        """The EBI submission status of this study
+
+        Returns
+        -------
+        str
+            The study EBI submission status
+
+        Notes
+        -----
+        There are 4 possible states: 'not submitted', 'submitting',
+        'submitted' & 'failed'. We are going to assume 'not submitted' if the
+        study doesn't have an accession, 'submitted' if it has an accession,
+        'submitting' if there are submit_to_EBI jobs running using the study
+        artifacts, & 'failed' if there are artifacts with failed jobs without
+        successful ones.
+        """
+        status = 'not submitted'
+        with qdb.sql_connection.TRN:
+            if self.ebi_study_accession:
+                status = 'submitted'
+
+            jobs = defaultdict(dict)
+            for info in self._ebi_submission_jobs():
+                jid, aid, js, cbste, era = info
+                if not cbste or era != [None]:
+                    continue
+                jobs[js][aid] = jid
+
+            if 'queued' in jobs or 'running' in jobs:
+                status = 'submitting'
+            elif 'error' in jobs:
+                aids_error = []
+                aids_other = []
+                for s, aids in jobs.items():
+                    for aid in aids.keys():
+                        if s == 'error':
+                            aids_error.append(aid)
+                        else:
+                            aids_other.append(aid)
+                difference = set(aids_error) - set(aids_other)
+                if difference:
+                    status = ('Some artifact submissions failed: %s' %
+                              ', '.join(map(str, list(difference))))
+
+        return status
+
+    @property
+    def tags(self):
+        """Returns the tags of the study
+
+        Returns
+        -------
+        list of str
+            The study tags
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT study_tag
+                        FROM qiita.study_tags
+                        LEFT JOIN qiita.per_study_tags USING (study_tag)
+                        WHERE study_id = {0}
+                        ORDER BY study_tag""".format(self._id)
+            qdb.sql_connection.TRN.add(sql)
+            return [t[0] for t in qdb.sql_connection.TRN.execute_fetchindex()]
+
+# --- methods ---
+    def artifacts(self, dtype=None, artifact_type=None):
+        """Returns the list of artifacts associated with the study
+
+        Parameters
+        ----------
+        dtype : str, optional
+            If given, retrieve only artifacts for given data type. Default,
+            return all artifacts associated with the study.
+        artifact_type : str, optional
+            If given, retrieve only artifacts of given data type. Default,
+            return all artifacts associated with the study
+
+        Returns
+        -------
+        list of qiita_db.artifact.Artifact
+        """
+        with qdb.sql_connection.TRN:
+            sql_args = [self._id]
+            sql_where = ""
+            if dtype:
+                sql_args.append(dtype)
+                sql_where = " AND data_type = %s"
+
+            if artifact_type:
+                sql_args.append(artifact_type)
+                sql_where += " AND artifact_type = %s"
+
+            sql = """SELECT artifact_id
+                     FROM qiita.artifact
+                        JOIN qiita.data_type USING (data_type_id)
+                        JOIN qiita.study_artifact USING (artifact_id)
+                        JOIN qiita.artifact_type USING (artifact_type_id)
+                     WHERE study_id = %s{0} AND visibility_id NOT IN %s
+                     ORDER BY artifact_id""".format(sql_where)
+            sql_args.append(qdb.util.artifact_visibilities_to_skip())
+
+            qdb.sql_connection.TRN.add(sql, sql_args)
+            return [qdb.artifact.Artifact(aid)
+                    for aid in qdb.sql_connection.TRN.execute_fetchflatten()]
+
+    def prep_templates(self, data_type=None):
+        """Return list of prep template ids
+
+        Parameters
+        ----------
+        data_type : str, optional
+            If given, retrieve only prep templates for given datatype.
+            Default None.
+
+        Returns
+        -------
+        list of qiita_db.metadata_template.prep_template.PrepTemplate
+        """
+        with qdb.sql_connection.TRN:
+            spec_data = ""
+            args = [self._id]
+            if data_type:
+                spec_data = " AND data_type_id = %s"
+                args.append(qdb.util.convert_to_id(data_type, "data_type"))
+
+            sql = """SELECT prep_template_id
+                     FROM qiita.study_prep_template
+                        JOIN qiita.prep_template USING (prep_template_id)
+                     WHERE study_id = %s{0}
+                     ORDER BY prep_template_id""".format(spec_data)
+            qdb.sql_connection.TRN.add(sql, args)
+            return [qdb.metadata_template.prep_template.PrepTemplate(ptid)
+                    for ptid in qdb.sql_connection.TRN.execute_fetchflatten()]
+
+    def analyses(self):
+        """Get all analyses where samples from this study have been used
+
+        Returns
+        -------
+        list of qiita_db.analysis.Analysis
+        """
+        with qdb.sql_connection.TRN:
+            if self.sample_template is not None:
+                sids = self.sample_template.keys()
+                if sids:
+                    sql = """SELECT DISTINCT analysis_id
+                             FROM qiita.analysis_sample
+                             WHERE sample_id IN %s
+                             ORDER BY analysis_id"""
+                    qdb.sql_connection.TRN.add(
+                        sql, [tuple(self.sample_template.keys())])
+
+                    return [qdb.analysis.Analysis(_id) for _id in
+                            qdb.sql_connection.TRN.execute_fetchflatten()]
+            return []
+
+    def has_access(self, user, no_public=False):
+        """Returns whether the given user has access to the study
+
+        Parameters
+        ----------
+        user : User object
+            User we are checking access for
+        no_public: bool
+            If we should ignore those studies shared with the user. Defaults
+            to False
+
+        Returns
+        -------
+        bool
+            Whether user has access to study or not
+        """
+        with qdb.sql_connection.TRN:
+            # return True if the user is one of the admins
+            if user.level in {'superuser', 'admin'}:
+                return True
+
+            # if no_public is False then just check if the study is public
+            # and return True
+            if not no_public and self.status == 'public':
+                return True
+
+            # let's check if the study belongs to this user or has been
+            # shared with them
+            sql = """SELECT EXISTS (
+                        SELECT study_id
+                        FROM qiita.study
+                        JOIN qiita.study_portal USING (study_id)
+                        JOIN qiita.portal_type USING (portal_type_id)
+                        WHERE email = %s AND portal = %s AND study_id = %s
+                        UNION
+                        SELECT study_id
+                        FROM qiita.study_users
+                        JOIN qiita.study_portal USING (study_id)
+                        JOIN qiita.portal_type USING (portal_type_id)
+                        WHERE email = %s AND portal = %s AND study_id = %s
+                    )
+                  """
+            qdb.sql_connection.TRN.add(
+                sql, [user.email, qiita_config.portal, self.id,
+                      user.email, qiita_config.portal, self.id])
+            result = qdb.sql_connection.TRN.execute_fetchlast()
+
+            return result
+
+    def can_edit(self, user):
+        """Returns whether the given user can edit the study
+
+        Parameters
+        ----------
+        user : User object
+            User we are checking edit permissions for
+
+        Returns
+        -------
+        bool
+            Whether user can edit the study or not
+        """
+        # The study is editable only if the user is the owner, is in the shared
+        # list or the user is an admin
+        return (user.level in {'superuser', 'admin'} or self.owner == user or
+                user in self.shared_with)
+
+    def share(self, user):
+        """Share the study with another user
+
+        Parameters
+        ----------
+        user: User object
+            The user to share the study with
+        """
+        # Make sure the study is not already shared with the given user
+        if user in self.shared_with:
+            return
+        # Do not allow the study to be shared with the owner
+        if user == self.owner:
+            return
+
+        sql = """INSERT INTO qiita.study_users (study_id, email)
+                 VALUES (%s, %s)"""
+        qdb.sql_connection.perform_as_transaction(sql, [self._id, user.id])
+
+    def unshare(self, user):
+        """Unshare the study with another user
+
+        Parameters
+        ----------
+        user: User object
+            The user to unshare the study with
+        """
+        sql = """DELETE FROM qiita.study_users
+                 WHERE study_id = %s AND email = %s"""
+        qdb.sql_connection.perform_as_transaction(sql, [self._id, user.id])
+
+    def update_tags(self, user, tags):
+        """Sets the tags of the study
+
+        Parameters
+        ----------
+        user: User object
+            The user reqesting the study tags update
+        tags : list of str
+            The tags to update within the study
+
+        Returns
+        -------
+        str
+            Warnings during insertion
+        """
+        message = ''
+        # converting to set just to facilitate operations
+        system_tags_admin = set(self.get_tags()['admin'])
+        user_level = user.level
+        current_tags = set(self.tags)
+        to_delete = current_tags - set(tags)
+        to_add = set(tags) - current_tags
+
+        if to_delete or to_add:
+            with qdb.sql_connection.TRN:
+                if to_delete:
+                    if user_level != 'admin':
+                        admin_tags = to_delete & system_tags_admin
+                        if admin_tags:
+                            message += 'You cannot remove: %s' % ', '.join(
+                                admin_tags)
+                        to_delete = to_delete - admin_tags
+
+                    if to_delete:
+                        sql = """DELETE FROM qiita.per_study_tags
+                                     WHERE study_id = %s AND study_tag IN %s"""
+                        qdb.sql_connection.TRN.add(
+                            sql, [self._id, tuple(to_delete)])
+
+                if to_add:
+                    if user_level != 'admin':
+                        admin_tags = to_add & system_tags_admin
+                        if admin_tags:
+                            message += ('Only admins can assign: '
+                                        '%s' % ', '.join(admin_tags))
+                        to_add = to_add - admin_tags
+
+                    if to_add:
+                        self.insert_tags(user, to_add)
+
+                        sql = """INSERT INTO qiita.per_study_tags
+                                    (study_tag, study_id)
+                                 SELECT %s, %s
+                                    WHERE
+                                        NOT EXISTS (
+                                            SELECT study_tag, study_id
+                                            FROM qiita.per_study_tags
+                                            WHERE study_tag = %s
+                                                AND study_id = %s
+                                        )"""
+                        sql_args = [[t, self._id, t, self._id] for t in to_add]
+                        qdb.sql_connection.TRN.add(sql, sql_args, many=True)
+
+                qdb.sql_connection.TRN.execute()
+        else:
+            message = 'No changes in the tags.'
+
+        return message
+
+
+class StudyPerson(qdb.base.QiitaObject):
+    r"""Object handling information pertaining to people involved in a study
+
+    Attributes
+    ----------
+    name : str
+        name of the person
+    email : str
+        email of the person
+    affiliation : str
+        institution with which the person is affiliated
+    address : str or None
+        address of the person
+    phone : str or None
+        phone number of the person
+    """
+    _table = "study_person"
+
+    @classmethod
+    def iter(cls):
+        """Iterate over all study people in the database
+
+        Returns
+        -------
+        generator
+            Yields a `StudyPerson` object for each person in the database,
+            in order of ascending study_person_id
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT study_person_id FROM qiita.{}
+                     ORDER BY study_person_id""".format(cls._table)
+            qdb.sql_connection.TRN.add(sql)
+
+            for id_ in qdb.sql_connection.TRN.execute_fetchflatten():
+                yield StudyPerson(id_)
+
+    @classmethod
+    def exists(cls, name, affiliation):
+        """Checks if a person exists
+
+        Parameters
+        ----------
+        name: str
+            Name of the person
+        affiliation : str
+            institution with which the person is affiliated
+
+        Returns
+        -------
+        bool
+            True if person exists else false
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT EXISTS(
+                        SELECT * FROM qiita.{0}
+                        WHERE name = %s
+                            AND affiliation = %s)""".format(cls._table)
+            qdb.sql_connection.TRN.add(sql, [name, affiliation])
+            return qdb.sql_connection.TRN.execute_fetchlast()
+
+    @classmethod
+    def from_name_and_affiliation(cls, name, affiliation):
+        """Gets a StudyPerson object based on the name and affiliation
+
+        Parameters
+        ----------
+        name: str
+            Name of the person
+        affiliation : str
+            institution with which the person is affiliated
+
+        Returns
+        -------
+        StudyPerson
+            The StudyPerson for the name and affiliation
+        """
+        with qdb.sql_connection.TRN:
+            if not cls.exists(name, affiliation):
+                raise qdb.exceptions.QiitaDBLookupError(
+                        'Study person does not exist')
+
+            sql = """SELECT study_person_id FROM qiita.{0}
+                        WHERE name = %s
+                     AND affiliation = %s""".format(cls._table)
+            qdb.sql_connection.TRN.add(sql, [name, affiliation])
+            return cls(qdb.sql_connection.TRN.execute_fetchlast())
+
+    @classmethod
+    def create(cls, name, email, affiliation, address=None, phone=None):
+        """Create a StudyPerson object, checking if person already exists.
+
+        Parameters
+        ----------
+        name : str
+            name of person
+        email : str
+            email of person
+        affiliation : str
+            institution with which the person is affiliated
+        address : str, optional
+            address of person
+        phone : str, optional
+            phone number of person
+
+        Returns
+        -------
+        New StudyPerson object
+
+        """
+        with qdb.sql_connection.TRN:
+            if cls.exists(name, affiliation):
+                sql = """SELECT study_person_id
+                         FROM qiita.{0}
+                         WHERE name = %s
+                            AND affiliation = %s""".format(cls._table)
+                args = [name, affiliation]
+            else:
+                sql = """INSERT INTO qiita.{0} (name, email, affiliation,
+                                                address, phone)
+                         VALUES (%s, %s, %s, %s, %s)
+                         RETURNING study_person_id""".format(cls._table)
+                args = [name, email, affiliation, address, phone]
+
+            qdb.sql_connection.TRN.add(sql, args)
+            return cls(qdb.sql_connection.TRN.execute_fetchlast())
+
+    @classmethod
+    def delete(cls, id_):
+        r"""Deletes the StudyPerson from the database
+
+        Parameters
+        ----------
+        id_ : integer
+            The object identifier
+
+        Raises
+        ------
+        QiitaDBError
+            If the StudyPerson with the given id is attached to any study
+        """
+        with qdb.sql_connection.TRN:
+            # checking that the id_ exists
+            cls(id_)
+
+            # Check if the person is attached to any study
+            sql = """SELECT EXISTS(
+                        SELECT *
+                        FROM qiita.study
+                        WHERE lab_person_id = %s OR
+                            principal_investigator_id = %s)"""
+            qdb.sql_connection.TRN.add(sql, [id_, id_])
+            if qdb.sql_connection.TRN.execute_fetchlast():
+                sql = """SELECT study_id
+                         FROM qiita.study
+                         WHERE {} = %s"""
+                cols = ['lab_person_id', 'principal_investigator_id']
+                rel = {}
+                for c in cols:
+                    qdb.sql_connection.TRN.add(sql.format(c), [id_])
+                    rel[c] = qdb.sql_connection.TRN.execute_fetchindex()
+                raise qdb.exceptions.QiitaDBError(
+                    'StudyPerson "%s" cannot be deleted because there are '
+                    'studies referencing it: %s' % (id_, str(rel)))
+
+            sql = "DELETE FROM qiita.study_person WHERE study_person_id = %s"
+            qdb.sql_connection.TRN.add(sql, [id_])
+            qdb.sql_connection.TRN.execute()
+
+    # Properties
+    @property
+    def name(self):
+        """Returns the name of the person
+
+        Returns
+        -------
+        str
+            Name of person
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT name FROM qiita.{0}
+                     WHERE study_person_id = %s""".format(self._table)
+            qdb.sql_connection.TRN.add(sql, [self._id])
+            return qdb.sql_connection.TRN.execute_fetchlast()
+
+    @property
+    def email(self):
+        """Returns the email of the person
+
+        Returns
+        -------
+        str
+            Email of person
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT email FROM qiita.{0}
+                     WHERE study_person_id = %s""".format(self._table)
+            qdb.sql_connection.TRN.add(sql, [self._id])
+            return qdb.sql_connection.TRN.execute_fetchlast()
+
+    @property
+    def affiliation(self):
+        """Returns the affiliation of the person
+
+        Returns
+        -------
+        str
+            Affiliation of person
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT affiliation FROM qiita.{0}
+                     WHERE study_person_id = %s""".format(self._table)
+            qdb.sql_connection.TRN.add(sql, [self._id])
+            return qdb.sql_connection.TRN.execute_fetchlast()
+
+    @property
+    def address(self):
+        """Returns the address of the person
+
+        Returns
+        -------
+        str or None
+            address or None if no address in database
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT address FROM qiita.{0}
+                     WHERE study_person_id = %s""".format(self._table)
+            qdb.sql_connection.TRN.add(sql, [self._id])
+            return qdb.sql_connection.TRN.execute_fetchlast()
+
+    @address.setter
+    def address(self, value):
+        """Set/update the address of the person
+
+        Parameters
+        ----------
+        value : str
+            New address for person
+        """
+        sql = """UPDATE qiita.{0} SET address = %s
+                 WHERE study_person_id = %s""".format(self._table)
+        qdb.sql_connection.perform_as_transaction(sql, [value, self._id])
+
+    @property
+    def phone(self):
+        """Returns the phone number of the person
+
+        Returns
+        -------
+         str or None
+            phone or None if no address in database
+        """
+        with qdb.sql_connection.TRN:
+            sql = """SELECT phone FROM qiita.{0}
+                     WHERE study_person_id = %s""".format(self._table)
+            qdb.sql_connection.TRN.add(sql, [self._id])
+            return qdb.sql_connection.TRN.execute_fetchlast()
+
+    @phone.setter
+    def phone(self, value):
+        """Set/update the phone number of the person
+
+        Parameters
+        ----------
+        value : str
+            New phone number for person
+        """
+        sql = """UPDATE qiita.{0} SET phone = %s
+                 WHERE study_person_id = %s""".format(self._table)
+        qdb.sql_connection.perform_as_transaction(sql, [value, self._id])