diff --git a/qiita_db/base.py b/qiita_db/base.py index b27c8fef4..b365538c5 100644 --- a/qiita_db/base.py +++ b/qiita_db/base.py @@ -199,7 +199,7 @@ def status(self): "{0}_id = %s)".format(self._table), (self._id, ))[0] - def _status_setter_checks(self): + def _status_setter_checks(self, conn_handler): r"""Perform any extra checks that needed to be done before setting the object status on the database. Should be overwritten by the subclasses """ @@ -218,10 +218,10 @@ def status(self, status): self._check_subclass() # Perform any extra checks needed before we update the status in the DB - self._status_setter_checks() + conn_handler = SQLConnectionHandler() + self._status_setter_checks(conn_handler) # Update the status of the object - conn_handler = SQLConnectionHandler() conn_handler.execute( "UPDATE qiita.{0} SET {0}_status_id = " "(SELECT {0}_status_id FROM qiita.{0}_status WHERE status = %s) " diff --git a/qiita_db/exceptions.py b/qiita_db/exceptions.py index 1e4c4a5b8..87325c5bb 100644 --- a/qiita_db/exceptions.py +++ b/qiita_db/exceptions.py @@ -30,13 +30,18 @@ class QiitaDBConnectionError(QiitaDBError): pass +class QiitaDBColumnError(QiitaDBError): + """Exception when missing table information or excess information passed""" + pass + + class QiitaDBDuplicateError(QiitaDBError): """Exception when duplicating something in the database""" pass -class QiitaDBColumnError(QiitaDBError): - """Exception when database column info missing or incorrect""" +class QiitaDBStatusError(QiitaDBError): + """Exception when editing is done with an unallowed status""" pass @@ -45,4 +50,4 @@ class QiitaDBUnknownIDError(QiitaDBError): def __init__(self, missing_id, table): super(QiitaDBUnknownIDError, self).__init__() self.args = ("The object with ID '%s' does not exists in table '%s" - % (missing_id, table)) + % (missing_id, table),) diff --git a/qiita_db/metadata_template.py b/qiita_db/metadata_template.py index 195519c68..1cd371fbf 100644 --- a/qiita_db/metadata_template.py +++ b/qiita_db/metadata_template.py @@ -66,6 +66,18 @@ class MetadataTemplate(QiitaStatusObject): # instantiate this base class _table_prefix = None _column_table = None + _id_column = None + + def _check_id(self, id_, conn_handler=None): + # PLACEHOLDER SO TESTS PASS. Jose will rewrite for metadata pr + r"""""" + self._check_subclass() + conn_handler = (conn_handler if conn_handler is not None + else SQLConnectionHandler()) + return conn_handler.execute_fetchone( + "SELECT EXISTS(SELECT * FROM qiita.{0} WHERE " + "{1}=%s)".format(self._table, self._id_column), + (id_, ))[0] @classmethod def _get_table_name(cls, study_id): @@ -262,8 +274,10 @@ def has_single_category_values(self, category): class SampleTemplate(MetadataTemplate): """""" + _table = "required_sample_info" _table_prefix = "sample_" _column_table = "study_sample_columns" + _id_column = "study_id" class PrepTemplate(MetadataTemplate): diff --git a/qiita_db/study.py b/qiita_db/study.py index faf511c7e..d2e51cb05 100644 --- a/qiita_db/study.py +++ b/qiita_db/study.py @@ -1,16 +1,92 @@ -#!/usr/bin/env python -from __future__ import division +r""" +Study and StudyPerson objects (:mod:`qiita_db.study`) +===================================================== -""" -Objects for dealing with Qiita studies +.. currentmodule:: qiita_db.study -This module provides the implementation of the Study class. +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 ------- -- `QittaStudy` -- A Qiita study class + +.. autosummary:: + :toctree: generated/ + + Study + StudyPerson + +Examples +-------- +Studies contain contact people (PIs, Lab members, and EBI contacts). These +people have names, emails, addresses, and phone numbers. The email and name are +the minimum required information. + +>>> from qiita_db.study import StudyPerson # doctest: +SKIP +>>> person = StudyPerson.create('Some Dude', 'somedude@foo.bar', +... address='111 fake street', +... phone='111-121-1313') # doctest: +SKIP +>>> person.name # doctest: +SKIP +Some dude +>>> person.email # doctest: +SKIP +somedude@foobar +>>> person.address # doctest: +SKIP +111 fake street +>>> person.phone # doctest: +SKIP +111-121-1313 + +A study requres a minimum of information to be created. Note that the people +must be passed as StudyPerson objects and the owner as a User object. + +>>> from qiita_db.study import Study # doctest: +SKIP +>>> from qiita_db.user import User # doctest: +SKIP +>>> info = { +... "timeseries_type_id": 1, +... "metadata_complete": True, +... "mixs_compliant": True, +... "number_samples_collected": 25, +... "number_samples_promised": 28, +... "portal_type_id": 3, +... "study_alias": "TST", +... "study_description": "Some description of the study goes here", +... "study_abstract": "Some abstract goes here", +... "emp_person_id": StudyPerson(2), +... "principal_investigator_id": StudyPerson(3), +... "lab_person_id": StudyPerson(1)} # doctest: +SKIP +>>> owner = User('owner@foo.bar') # doctest: +SKIP +>>> Study(owner, "New Study Title", 1, info) # doctest: +SKIP + +You can also add a study to an investigation by passing the investigation +object while creating the study. + +>>> from qiita_db.study import Study # doctest: +SKIP +>>> from qiita_db.user import User # doctest: +SKIP +>>> from qiita_db.study import Investigation # doctest: +SKIP +>>> info = { +... "timeseries_type_id": 1, +... "metadata_complete": True, +... "mixs_compliant": True, +... "number_samples_collected": 25, +... "number_samples_promised": 28, +... "portal_type_id": 3, +... "study_alias": "TST", +... "study_description": "Some description of the study goes here", +... "study_abstract": "Some abstract goes here", +... "emp_person_id": StudyPerson(2), +... "principal_investigator_id": StudyPerson(3), +... "lab_person_id": StudyPerson(1)} # doctest: +SKIP +>>> owner = User('owner@foo.bar') # doctest: +SKIP +>>> investigation = Investigation(1) # doctest: +SKIP +>>> Study(owner, "New Study Title", 1, info, investigation) # doctest: +SKIP """ + # ----------------------------------------------------------------------------- # Copyright (c) 2014--, The Qiita Development Team. # @@ -19,105 +95,530 @@ # The full license is in the file LICENSE, distributed with this software. # ----------------------------------------------------------------------------- -from .base import QiitaStatusObject -from .exceptions import QiitaDBNotImplementedError +from __future__ import division +from future.builtins import zip +from future.utils import viewitems +from datetime import date +from copy import deepcopy + +from qiita_core.exceptions import IncompetentQiitaDeveloperError +from .base import QiitaStatusObject, QiitaObject +from .exceptions import (QiitaDBDuplicateError, QiitaDBStatusError, + QiitaDBColumnError) +from .util import check_required_columns, check_table_cols +from .sql_connection import SQLConnectionHandler class Study(QiitaStatusObject): - """ - Study object to access to the Qiita Study information + r"""Study object to access to the Qiita Study information Attributes ---------- name - sample_ids info + status + efo + shared_with + pmids + investigation + sample_template + raw_data + preprocessed_data + processed_data Methods ------- - add_samples(samples) - Adds the samples listed in `samples` to the study + add_pmid - remove_samples(samples) - Removes the samples listed in `samples` from the study + Notes + ----- + All setters raise QiitaDBStatusError if trying to change a public study. + You should not be doing that. """ _table = "study" + # The following columns are considered not part of the study info + _non_info = {"email", "study_id", "study_status_id", "study_title"} - @staticmethod - def create(owner): + def _lock_public(self, conn_handler): + """Raises QiitaDBStatusError if study is public""" + if self.check_status(("public", )): + raise QiitaDBStatusError("Can't change status of public study!") + + def _status_setter_checks(self, conn_handler): + r"""Perform a check to make sure not setting status away from public + """ + self._lock_public(conn_handler) + + @classmethod + def create(cls, owner, title, efo, info, investigation=None): """Creates a new study on the database Parameters ---------- - owner : str - the user id of the study' owner + owner : User object + the study's owner + title : str + Title of the study + efo : list + Experimental Factor Ontology id(s) for 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 + empty efo list passed + + Notes + ----- + All keys in info, except the efo, 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 QiitaDBColumnError("non info keys passed: %s" % + cls._non_info.intersection(info)) + + # make sure efo info passed + if not efo: + raise IncompetentQiitaDeveloperError("Need EFO information!") + + # add default values to info + insertdict = deepcopy(info) + if "first_contact" not in insertdict: + insertdict['first_contact'] = date.today().isoformat() + insertdict['email'] = owner.id + insertdict['study_title'] = title + if "reprocess" not in insertdict: + insertdict['reprocess'] = False + # default to waiting_approval status + insertdict['study_status_id'] = 1 + + conn_handler = SQLConnectionHandler() + # make sure dictionary only has keys for available columns in db + check_table_cols(conn_handler, insertdict, cls._table) + # make sure reqired columns in dictionary + check_required_columns(conn_handler, 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], QiitaObject): + data.append(insertdict[col].id) + else: + data.append(insertdict[col]) + study_id = conn_handler.execute_fetchone(sql, data)[0] + + # insert efo information into database + sql = ("INSERT INTO qiita.{0}_experimental_factor (study_id, " + "efo_id) VALUES (%s, %s)".format(cls._table)) + conn_handler.executemany(sql, [(study_id, e) for e in efo]) + + # add study to investigation if necessary + if investigation: + sql = ("INSERT INTO qiita.investigation_study (investigation_id, " + "study_id) VALUES (%s, %s)") + conn_handler.execute(sql, (investigation.id, study_id)) + + return cls(study_id) + +# --- Attributes --- + @property + def title(self): + """Returns the title of the study + + Returns + ------- + str + Title of study """ - raise QiitaDBNotImplementedError() + conn_handler = SQLConnectionHandler() + sql = ("SELECT study_title FROM qiita.{0} WHERE " + "study_id = %s".format(self._table)) + return conn_handler.execute_fetchone(sql, (self._id, ))[0] - @staticmethod - def delete(id_): - """Deletes the study `id_` from the database + @title.setter + def title(self, title): + """Sets the title of the study Parameters ---------- - id_ : - The object identifier + title : str + The new study title """ - raise QiitaDBNotImplementedError() + conn_handler = SQLConnectionHandler() + self._lock_public(conn_handler) + sql = ("UPDATE qiita.{0} SET study_title = %s WHERE " + "study_id = %s".format(self._table)) + return conn_handler.execute(sql, (title, self._id)) @property - def name(self): - """Returns the name of the study""" - raise QiitaDBNotImplementedError() + def info(self): + """Dict with all information attached to the study + + Returns + ------- + dict + info of study keyed to column names + """ + conn_handler = SQLConnectionHandler() + sql = "SELECT * FROM qiita.{0} WHERE study_id = %s".format(self._table) + info = dict(conn_handler.execute_fetchone(sql, (self._id, ))) + # remove non-info items from info + for item in self._non_info: + info.pop(item) + return info - @name.setter - def name(self, name): - """Sets the name of the study + @info.setter + def info(self, info): + """Updates the information attached to the study Parameters ---------- - name : str - The new study name + 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 self._non_info.intersection(info): + raise QiitaDBColumnError("non info keys passed: %s" % + self._non_info.intersection(info)) + + conn_handler = SQLConnectionHandler() + self._lock_public(conn_handler) + + # make sure dictionary only has keys for available columns in db + check_table_cols(conn_handler, info, self._table) + + sql_vals = [] + data = [] + # build query with data values in correct order for SQL statement + for key, val in viewitems(info): + sql_vals.append("{0} = %s".format(key)) + if isinstance(val, 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))) + conn_handler.execute(sql, data) + + @property + def efo(self): + conn_handler = SQLConnectionHandler() + sql = ("SELECT efo_id FROM qiita.{0}_experimental_factor WHERE " + "study_id = %s".format(self._table)) + return [x[0] for x in conn_handler.execute_fetchall(sql, (self._id, ))] + + @efo.setter + def efo(self, efo_vals): + """Sets the efo for the study + + Parameters + ---------- + efo_vals : list + Id(s) for the new efo values + + Raises + ------ + IncompetentQiitaDeveloperError + Empty efo list passed """ - raise QiitaDBNotImplementedError() + if not efo_vals: + raise IncompetentQiitaDeveloperError("Need EFO information!") + conn_handler = SQLConnectionHandler() + self._lock_public(conn_handler) + # wipe out any EFOs currently attached to study + sql = ("DELETE FROM qiita.{0}_experimental_factor WHERE " + "study_id = %s".format(self._table)) + conn_handler.execute(sql, (self._id, )) + # insert new EFO information into database + sql = ("INSERT INTO qiita.{0}_experimental_factor (study_id, " + "efo_id) VALUES (%s, %s)".format(self._table)) + conn_handler.executemany(sql, [(self._id, efo) for efo in efo_vals]) @property - def sample_ids(self): - """Returns the IDs of all samples in study + def shared_with(self): + """list of users the study is shared with - The sample IDs are returned as a list of strings in alphabetical order. + Returns + ------- + list of User ids + Users the study is shared with """ - raise QiitaDBNotImplementedError() + conn_handler = SQLConnectionHandler() + sql = ("SELECT email FROM qiita.{0}_users WHERE " + "study_id = %s".format(self._table)) + return [x[0] for x in conn_handler.execute_fetchall(sql, (self._id,))] @property - def info(self): - """Dict with any other information attached to the study""" - raise QiitaDBNotImplementedError() + def pmids(self): + """ Returns list of paper PMIDs from this study - @info.setter - def info(self, info): - """Updates the information attached to the study + Returns + ------- + list of str + list of all the PMIDs + """ + conn_handler = SQLConnectionHandler() + sql = ("SELECT pmid FROM qiita.{0}_pmid WHERE " + "study_id = %s".format(self._table)) + return [x[0] for x in conn_handler.execute_fetchall(sql, (self._id, ))] + + @property + def investigation(self): + """ Returns Investigation this study is part of + + Returns + ------- + Investigation id + """ + conn_handler = SQLConnectionHandler() + sql = ("SELECT investigation_id FROM qiita.investigation_study WHERE " + "study_id = %s") + inv = conn_handler.execute_fetchone(sql, (self._id, )) + return inv[0] if inv is not None else inv + + @property + def sample_template(self): + """ Returns sample_template information id + + Returns + ------- + SampleTemplate id + """ + return self._id + + @property + def raw_data(self): + """ Returns list of data ids for raw data info + + Returns + ------- + list of RawData ids + """ + conn_handler = SQLConnectionHandler() + sql = ("SELECT raw_data_id FROM qiita.study_raw_data WHERE " + "study_id = %s") + return [x[0] for x in conn_handler.execute_fetchall(sql, (self._id,))] + + @property + def preprocessed_data(self): + """ Returns list of data ids for preprocessed data info + + Returns + ------- + list of PreprocessedData ids + """ + conn_handler = SQLConnectionHandler() + sql = ("SELECT preprocessed_data_id FROM qiita.study_preprocessed_data" + " WHERE study_id = %s") + return [x[0] for x in conn_handler.execute_fetchall(sql, (self._id,))] + + @property + def processed_data(self): + """ Returns list of data ids for processed data info + + Returns + ------- + list of ProcessedData ids + """ + conn_handler = SQLConnectionHandler() + sql = ("SELECT processed_data_id FROM qiita.processed_data WHERE " + "preprocessed_data_id IN (SELECT preprocessed_data_id FROM " + "qiita.study_preprocessed_data where study_id = %s)") + return [x[0] for x in conn_handler.execute_fetchall(sql, (self._id,))] + +# --- methods --- + def add_pmid(self, pmid): + """Adds PMID to study Parameters ---------- - info : dict + pmid : str + pmid to associate with study + """ + conn_handler = SQLConnectionHandler() + sql = ("INSERT INTO qiita.{0}_pmid (study_id, pmid) " + "VALUES (%s, %s)".format(self._table)) + conn_handler.execute(sql, (self._id, pmid)) + + +class StudyPerson(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 + address : str or None + address of the person + phone : str or None + phone number of the person + """ + _table = "study_person" + + @classmethod + def exists(cls, name, email): + """Checks if a person exists + + Parameters + ---------- + name: str + Name of the person + email: str + Email of the person + + Returns + ------- + bool + True if person exists else false + """ + conn_handler = SQLConnectionHandler() + sql = ("SELECT exists(SELECT * FROM qiita.{0} WHERE " + "name = %s AND email = %s)".format(cls._table)) + return conn_handler.execute_fetchone(sql, (name, email))[0] + + @classmethod + def create(cls, name, email, address=None, phone=None): + """Create a StudyPerson object, checking if person already exists. + + Parameters + ---------- + name : str + name of person + email : str + email of person + address : str, optional + address of person + phone : str, optional + phone number of person + + Returns + ------- + New StudyPerson object + + Raises + ------ + QiitaDBDuplicateError + Person already exists + """ + if cls.exists(name, email): + raise QiitaDBDuplicateError("StudyPerson already exists!") + + # Doesn't exist so insert new person + sql = ("INSERT INTO qiita.{0} (name, email, address, phone) VALUES" + " (%s, %s, %s, %s) RETURNING " + "study_person_id".format(cls._table)) + conn_handler = SQLConnectionHandler() + spid = conn_handler.execute_fetchone(sql, (name, email, address, + phone)) + return cls(spid[0]) + + # Properties + @property + def name(self): + """Returns the name of the person + + Returns + ------- + str + Name of person + """ + conn_handler = SQLConnectionHandler() + sql = ("SELECT name FROM qiita.{0} WHERE " + "study_person_id = %s".format(self._table)) + return conn_handler.execute_fetchone(sql, (self._id, ))[0] + + @property + def email(self): + """Returns the email of the person + + Returns + ------- + str + Email of person """ - raise QiitaDBNotImplementedError() + conn_handler = SQLConnectionHandler() + sql = ("SELECT email FROM qiita.{0} WHERE " + "study_person_id = %s".format(self._table)) + return conn_handler.execute_fetchone(sql, (self._id, ))[0] + + @property + def address(self): + """Returns the address of the person + + Returns + ------- + str or None + address or None if no address in database + """ + conn_handler = SQLConnectionHandler() + sql = ("SELECT address FROM qiita.{0} WHERE study_person_id =" + " %s".format(self._table)) + return conn_handler.execute_fetchone(sql, (self._id, ))[0] + + @address.setter + def address(self, value): + """Set/update the address of the person - def add_samples(self, samples): - """Adds the samples listed in `samples` to the study Parameters ---------- - samples : list of strings - The sample Ids to be added to the study + value : str + New address for person """ - raise QiitaDBNotImplementedError() + conn_handler = SQLConnectionHandler() + sql = ("UPDATE qiita.{0} SET address = %s WHERE " + "study_person_id = %s".format(self._table)) + conn_handler.execute(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 + """ + conn_handler = SQLConnectionHandler() + sql = ("SELECT phone FROM qiita.{0} WHERE " + "study_person_id = %s".format(self._table)) + return conn_handler.execute_fetchone(sql, (self._id, ))[0] + + @phone.setter + def phone(self, value): + """Set/update the phone number of the person - def remove_samples(self, samples): - """Removes the samples listed in `samples` from the study Parameters ---------- - samples : list of strings - The sample Ids to be removed from the study + value : str + New phone number for person """ - raise QiitaDBNotImplementedError() + conn_handler = SQLConnectionHandler() + sql = ("UPDATE qiita.{0} SET phone = %s WHERE " + "study_person_id = %s".format(self._table)) + conn_handler.execute(sql, (value, self._id)) diff --git a/qiita_db/support_files/populate_test_db.sql b/qiita_db/support_files/populate_test_db.sql index 3ae115cb8..a34357948 100644 --- a/qiita_db/support_files/populate_test_db.sql +++ b/qiita_db/support_files/populate_test_db.sql @@ -16,8 +16,8 @@ INSERT INTO qiita.qiita_user (email, user_level_id, password, name, -- Insert some study persons INSERT INTO qiita.study_person (name, email, address, phone) VALUES - ('LabDude', 'lab_dude@foo.bar', '123 lab street', NULL), - ('empDude', 'emp_dude@foo.bar', '123 emp street', NULL), + ('LabDude', 'lab_dude@foo.bar', '123 lab street', '121-222-3333'), + ('empDude', 'emp_dude@foo.bar', NULL, '444-222-3333'), ('PIDude', 'PI_dude@foo.bar', '123 PI street', NULL); -- Insert a study: EMP 1001 @@ -36,6 +36,9 @@ INSERT INTO qiita.study (email, study_status_id, emp_person_id, first_contact, -- Insert study_users (share study 1 with shared user) INSERT INTO qiita.study_users (study_id, email) VALUES (1, 'shared@foo.bar'); +-- Insert PMIDs for study +INSERT INTO qiita.study_pmid (study_id, pmid) VALUES (1, '123456'), (1, '7891011'); + -- Insert an investigation INSERT INTO qiita.investigation (name, description, contact_person_id) VALUES ('TestInvestigation', 'An investigation for testing purposes', 3); diff --git a/qiita_db/test/test_study.py b/qiita_db/test/test_study.py new file mode 100644 index 000000000..9657e7b37 --- /dev/null +++ b/qiita_db/test/test_study.py @@ -0,0 +1,410 @@ +from unittest import TestCase, main +from datetime import date + +from future.utils import viewitems + +from qiita_core.exceptions import IncompetentQiitaDeveloperError +from qiita_core.util import qiita_test_checker +from qiita_db.base import QiitaObject +from qiita_db.study import Study, StudyPerson +from qiita_db.investigation import Investigation +from qiita_db.user import User +from qiita_db.exceptions import (QiitaDBDuplicateError, QiitaDBColumnError, + QiitaDBStatusError) +from qiita_db.sql_connection import SQLConnectionHandler + +# ----------------------------------------------------------------------------- +# 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. +# ----------------------------------------------------------------------------- + + +@qiita_test_checker() +class TestStudyPerson(TestCase): + def setUp(self): + self.studyperson = StudyPerson(1) + + def test_create_studyperson(self): + new = StudyPerson.create('SomeDude', 'somedude@foo.bar', + '111 fake street', '111-121-1313') + self.assertEqual(new.id, 4) + conn = SQLConnectionHandler() + obs = conn.execute_fetchall("SELECT * FROM qiita.study_person WHERE " + "study_person_id = 4") + self.assertEqual(obs, [[4, 'SomeDude', 'somedude@foo.bar', + '111 fake street', '111-121-1313']]) + + def test_create_studyperson_already_exists(self): + with self.assertRaises(QiitaDBDuplicateError): + StudyPerson.create('LabDude', 'lab_dude@foo.bar') + + def test_retrieve_name(self): + self.assertEqual(self.studyperson.name, 'LabDude') + + def test_set_name_fail(self): + with self.assertRaises(AttributeError): + self.studyperson.name = 'Fail Dude' + + def test_retrieve_email(self): + self.assertEqual(self.studyperson.email, 'lab_dude@foo.bar') + + def test_set_email_fail(self): + with self.assertRaises(AttributeError): + self.studyperson.email = 'faildude@foo.bar' + + def test_retrieve_address(self): + self.assertEqual(self.studyperson.address, '123 lab street') + + def test_retrieve_address_null(self): + person = StudyPerson(2) + self.assertEqual(person.address, None) + + def test_set_address(self): + self.studyperson.address = '123 nonsense road' + self.assertEqual(self.studyperson.address, '123 nonsense road') + + def test_retrieve_phone(self): + self.assertEqual(self.studyperson.phone, '121-222-3333') + + def test_retrieve_phone_null(self): + person = StudyPerson(3) + self.assertEqual(person.phone, None) + + def test_set_phone(self): + self.studyperson.phone = '111111111111111111121' + self.assertEqual(self.studyperson.phone, '111111111111111111121') + + +@qiita_test_checker() +class TestStudy(TestCase): + def setUp(self): + self.study = Study(1) + + self.info = { + "timeseries_type_id": 1, + "metadata_complete": True, + "mixs_compliant": True, + "number_samples_collected": 25, + "number_samples_promised": 28, + "portal_type_id": 3, + "study_alias": "FCM", + "study_description": "Microbiome of people who eat nothing but " + "fried chicken", + "study_abstract": "Exploring how a high fat diet changes the " + "gut microbiome", + "emp_person_id": StudyPerson(2), + "principal_investigator_id": StudyPerson(3), + "lab_person_id": StudyPerson(1) + } + + self.infoexp = { + "timeseries_type_id": 1, + "metadata_complete": True, + "mixs_compliant": True, + "number_samples_collected": 25, + "number_samples_promised": 28, + "portal_type_id": 3, + "study_alias": "FCM", + "study_description": "Microbiome of people who eat nothing but " + "fried chicken", + "study_abstract": "Exploring how a high fat diet changes the " + "gut microbiome", + "emp_person_id": 2, + "principal_investigator_id": 3, + "lab_person_id": 1 + } + + self.existingexp = { + 'mixs_compliant': True, + 'metadata_complete': True, + 'reprocess': False, + 'number_samples_promised': 27, + 'emp_person_id': StudyPerson(2), + 'funding': None, + 'vamps_id': None, + 'first_contact': '2014-05-19 16:10', + 'principal_investigator_id': StudyPerson(3), + 'timeseries_type_id': 1, + 'study_abstract': + "This is a preliminary study to examine the " + "microbiota associated with the Cannabis plant. Soils samples " + "from the bulk soil, soil associated with the roots, and the " + "rhizosphere were extracted and the DNA sequenced. Roots " + "from three independent plants of different strains were " + "examined. These roots were obtained November 11, 2011 from " + "plants that had been harvested in the summer. Future " + "studies will attempt to analyze the soils and rhizospheres " + "from the same location at different time points in the plant " + "lifecycle.", + 'spatial_series': False, + 'study_description': 'Analysis of the Cannabis Plant Microbiome', + 'portal_type_id': 2, + 'study_alias': 'Cannabis Soils', + 'most_recent_contact': '2014-05-19 16:11', + 'lab_person_id': StudyPerson(1), + 'number_samples_collected': 27} + + def test_create_study_min_data(self): + """Insert a study into the database""" + obs = Study.create(User('test@foo.bar'), "Fried chicken microbiome", + [1], self.info) + self.assertEqual(obs.id, 2) + exp = {'mixs_compliant': True, 'metadata_complete': True, + 'reprocess': False, 'study_status_id': 1, + 'number_samples_promised': 28, 'emp_person_id': 2, + 'funding': None, 'vamps_id': None, + 'first_contact': date.today().isoformat(), + 'principal_investigator_id': 3, + 'timeseries_type_id': 1, + 'study_abstract': 'Exploring how a high fat diet changes the ' + 'gut microbiome', + 'email': 'test@foo.bar', 'spatial_series': None, + 'study_description': 'Microbiome of people who eat nothing but' + ' fried chicken', + 'portal_type_id': 3, 'study_alias': 'FCM', 'study_id': 2, + 'most_recent_contact': None, 'lab_person_id': 1, + 'study_title': 'Fried chicken microbiome', + 'number_samples_collected': 25} + + conn = SQLConnectionHandler() + obsins = conn.execute_fetchall("SELECT * FROM qiita.study WHERE " + "study_id = 2") + self.assertEqual(len(obsins), 1) + obsins = dict(obsins[0]) + self.assertEqual(obsins, exp) + + # make sure EFO went in to table correctly + efo = conn.execute_fetchall("SELECT efo_id FROM " + "qiita.study_experimental_factor WHERE " + "study_id = 2") + self.assertEqual(efo, [[1]]) + + def test_create_study_with_investigation(self): + """Insert a study into the database with an investigation""" + obs = Study.create(User('test@foo.bar'), "Fried chicken microbiome", + [1], self.info, Investigation(1)) + self.assertEqual(obs.id, 2) + # check the investigation was assigned + conn = SQLConnectionHandler() + obs = conn.execute_fetchall("SELECT * from qiita.investigation_study " + "WHERE study_id = 2") + self.assertEqual(obs, [[1, 2]]) + + def test_create_study_all_data(self): + """Insert a study into the database with every info field""" + self.info.update({ + 'vamps_id': 'MBE_1111111', + 'funding': 'FundAgency', + 'spatial_series': True, + 'metadata_complete': False, + 'reprocess': True, + 'first_contact': "Today" + }) + obs = Study.create(User('test@foo.bar'), "Fried chicken microbiome", + [1], self.info) + self.assertEqual(obs.id, 2) + exp = {'mixs_compliant': True, 'metadata_complete': False, + 'reprocess': True, 'study_status_id': 1, + 'number_samples_promised': 28, 'emp_person_id': 2, + 'funding': 'FundAgency', 'vamps_id': 'MBE_1111111', + 'first_contact': "Today", + 'principal_investigator_id': 3, 'timeseries_type_id': 1, + 'study_abstract': 'Exploring how a high fat diet changes the ' + 'gut microbiome', + 'email': 'test@foo.bar', 'spatial_series': True, + 'study_description': 'Microbiome of people who eat nothing ' + 'but fried chicken', + 'portal_type_id': 3, 'study_alias': 'FCM', 'study_id': 2, + 'most_recent_contact': None, 'lab_person_id': 1, + 'study_title': 'Fried chicken microbiome', + 'number_samples_collected': 25} + conn = SQLConnectionHandler() + obsins = conn.execute_fetchall("SELECT * FROM qiita.study WHERE " + "study_id = 2") + self.assertEqual(len(obsins), 1) + obsins = dict(obsins[0]) + self.assertEqual(obsins, exp) + + # make sure EFO went in to table correctly + obsefo = conn.execute_fetchall("SELECT efo_id FROM " + "qiita.study_experimental_factor WHERE " + "study_id = 2") + self.assertEqual(obsefo, [[1]]) + + def test_create_missing_required(self): + """ Insert a study that is missing a required info key""" + self.info.pop("study_alias") + with self.assertRaises(QiitaDBColumnError): + Study.create(User('test@foo.bar'), "Fried Chicken Microbiome", + [1], self.info) + + def test_create_empty_efo(self): + """ Insert a study that is missing a required info key""" + with self.assertRaises(IncompetentQiitaDeveloperError): + Study.create(User('test@foo.bar'), "Fried Chicken Microbiome", + [], self.info) + + def test_create_study_with_not_allowed_key(self): + """Insert a study with key from _non_info present""" + self.info.update({"study_id": 1}) + with self.assertRaises(QiitaDBColumnError): + Study.create(User('test@foo.bar'), "Fried Chicken Microbiome", + [1], self.info) + + def test_create_unknown_db_col(self): + """ Insert a study with an info key not in the database""" + self.info["SHOULDNOTBEHERE"] = "BWAHAHAHAHAHA" + with self.assertRaises(QiitaDBColumnError): + Study.create(User('test@foo.bar'), "Fried Chicken Microbiome", + [1], self.info) + + def test_retrieve_title(self): + self.assertEqual(self.study.title, 'Identification of the Microbiomes' + ' for Cannabis Soils') + + def test_set_title(self): + new = Study.create(User('test@foo.bar'), 'Identification of the ' + 'Microbiomes for Cannabis Soils', [1], self.info) + new.title = "Cannabis soils" + self.assertEqual(new.title, "Cannabis soils") + + def test_set_title_public(self): + """Tests for fail if editing title of a public study""" + with self.assertRaises(QiitaDBStatusError): + self.study.title = "FAILBOAT" + + def test_get_efo(self): + self.assertEqual(self.study.efo, [1]) + + def test_set_efo(self): + """Set efo with list efo_id""" + new = Study.create(User('test@foo.bar'), 'Identification of the ' + 'Microbiomes for Cannabis Soils', [1], self.info) + new.efo = [3, 4] + self.assertEqual(new.efo, [3, 4]) + + def test_set_efo_empty(self): + """Set efo with list efo_id""" + new = Study.create(User('test@foo.bar'), 'Identification of the ' + 'Microbiomes for Cannabis Soils', [1], self.info) + with self.assertRaises(IncompetentQiitaDeveloperError): + new.efo = [] + + def test_set_efo_public(self): + """Set efo on a public study""" + with self.assertRaises(QiitaDBStatusError): + self.study.efo = 6 + + def test_retrieve_info(self): + for key, val in viewitems(self.existingexp): + if isinstance(val, QiitaObject): + self.existingexp[key] = val.id + self.assertEqual(self.study.info, self.existingexp) + + def test_set_info(self): + """Set info in a study""" + newinfo = { + "timeseries_type_id": 2, + "metadata_complete": False, + "number_samples_collected": 28, + "lab_person_id": StudyPerson(2), + "vamps_id": 'MBE_111222', + "first_contact": "June 11, 2014" + } + new = Study.create(User('test@foo.bar'), 'Identification of the ' + 'Microbiomes for Cannabis Soils', [1], self.info) + self.infoexp.update(newinfo) + new.info = newinfo + # add missing table cols + self.infoexp["funding"] = None + self.infoexp["spatial_series"] = None + self.infoexp["most_recent_contact"] = None + self.infoexp["reprocess"] = False + self.infoexp["lab_person_id"] = 2 + self.assertEqual(new.info, self.infoexp) + + def test_set_info_public(self): + """Tests for fail if editing info of a public study""" + with self.assertRaises(QiitaDBStatusError): + self.study.info = {"vamps_id": "12321312"} + + def test_set_info_disallowed_keys(self): + """Tests for fail if sending non-info keys in info dict""" + new = Study.create(User('test@foo.bar'), 'Identification of the ' + 'Microbiomes for Cannabis Soils', [1], self.info) + with self.assertRaises(QiitaDBColumnError): + new.info = {"email": "fail@fail.com"} + + def test_info_empty(self): + new = Study.create(User('test@foo.bar'), 'Identification of the ' + 'Microbiomes for Cannabis Soils', [1], self.info) + with self.assertRaises(IncompetentQiitaDeveloperError): + new.info = {} + + def test_retrieve_status(self): + self.assertEqual(self.study.status, "public") + + def test_set_status(self): + new = Study.create(User('test@foo.bar'), 'Identification of the ' + 'Microbiomes for Cannabis Soils', [1], self.info) + new.status = "private" + self.assertEqual(new.status, "private") + + def test_retrieve_shared_with(self): + self.assertEqual(self.study.shared_with, ['shared@foo.bar']) + + def test_retrieve_pmids(self): + exp = ['123456', '7891011'] + self.assertEqual(self.study.pmids, exp) + + def test_retrieve_pmids_empty(self): + new = Study.create(User('test@foo.bar'), 'Identification of the ' + 'Microbiomes for Cannabis Soils', [1], self.info) + self.assertEqual(new.pmids, []) + + def test_retrieve_investigation(self): + self.assertEqual(self.study.investigation, 1) + + def test_retrieve_investigation_empty(self): + new = Study.create(User('test@foo.bar'), 'Identification of the ' + 'Microbiomes for Cannabis Soils', [1], self.info) + self.assertEqual(new.investigation, None) + + def test_retrieve_sample_template(self): + self.assertEqual(self.study.sample_template, 1) + + def test_retrieve_raw_data(self): + self.assertEqual(self.study.raw_data, [1, 2]) + + def test_retrieve_raw_data_none(self): + new = Study.create(User('test@foo.bar'), 'Identification of the ' + 'Microbiomes for Cannabis Soils', [1], self.info) + self.assertEqual(new.raw_data, []) + + def test_retrieve_preprocessed_data(self): + self.assertEqual(self.study.preprocessed_data, [1, 2]) + + def test_retrieve_preprocessed_data_none(self): + new = Study.create(User('test@foo.bar'), 'Identification of the ' + 'Microbiomes for Cannabis Soils', [1], self.info) + self.assertEqual(new.preprocessed_data, []) + + def test_retrieve_processed_data(self): + self.assertEqual(self.study.processed_data, [1]) + + def test_retrieve_processed_data_none(self): + new = Study.create(User('test@foo.bar'), 'Identification of the ' + 'Microbiomes for Cannabis Soils', [1], self.info) + self.assertEqual(new.processed_data, []) + + def test_add_pmid(self): + self.study.add_pmid('4544444') + exp = ['123456', '7891011', '4544444'] + self.assertEqual(self.study.pmids, exp) + + +if __name__ == "__main__": + main() diff --git a/qiita_db/test/test_user.py b/qiita_db/test/test_user.py index 323bb8329..a30612e09 100644 --- a/qiita_db/test/test_user.py +++ b/qiita_db/test/test_user.py @@ -8,12 +8,12 @@ from unittest import TestCase, main -from qiita_core.exceptions import (IncompetentQiitaDeveloperError, - IncorrectEmailError, IncorrectPasswordError) +from qiita_core.exceptions import IncorrectEmailError, IncorrectPasswordError from qiita_core.util import qiita_test_checker from qiita_db.user import User from qiita_db.sql_connection import SQLConnectionHandler -from qiita_db.exceptions import QiitaDBDuplicateError, QiitaDBColumnError +from qiita_db.exceptions import (QiitaDBDuplicateError, QiitaDBColumnError, + QiitaDBUnknownIDError) @qiita_test_checker() @@ -31,6 +31,13 @@ def setUp(self): 'phone': '111-222-3344' } + def test_instantiate_user(self): + User('admin@foo.bar') + + def test_instantiate_unknown_user(self): + with self.assertRaises(QiitaDBUnknownIDError): + User('FAIL@OMG.bar') + def _check_correct_info(self, obs, exp): self.assertEqual(set(exp.keys()), set(obs.keys())) for key in exp: diff --git a/qiita_db/user.py b/qiita_db/user.py index b4f64dac3..1383168b3 100644 --- a/qiita_db/user.py +++ b/qiita_db/user.py @@ -31,6 +31,8 @@ from re import match from .base import QiitaObject +from .exceptions import QiitaDBNotImplementedError +from .sql_connection import SQLConnectionHandler from qiita_core.exceptions import IncorrectEmailError, IncorrectPasswordError from .exceptions import QiitaDBDuplicateError, QiitaDBColumnError @@ -70,6 +72,29 @@ class User(QiitaObject): _non_info = {"email", "user_level_id", "password", "user_verify_code", "pass_reset_code", "pass_reset_timestamp"} + def _check_id(self, id_, conn_handler=None): + r"""Check that the provided ID actually exists in the database + + Parameters + ---------- + id_ : object + The ID to test + conn_handler : SQLConnectionHandler + The connection handler object connected to the DB + + Notes + ----- + This function overwrites the base function, as sql layout doesn't + follow the same conventions done in the other classes. + """ + self._check_subclass() + + conn_handler = (conn_handler if conn_handler is not None + else SQLConnectionHandler()) + return conn_handler.execute_fetchone( + "SELECT EXISTS(SELECT * FROM qiita.qiita_user WHERE " + "email = %s)", (id_, ))[0] + @classmethod def login(cls, email, password): """Logs a user into the system @@ -191,27 +216,6 @@ def create(cls, email, password, info=None): conn_handler.execute(sql, values) return cls(email) - def _check_id(self, id_, conn_handler=None): - r"""Check that the provided ID actually exists on the database - - Parameters - ---------- - id_ : str - The ID to test - conn_handler - The connection handler object connected to the DB - - Notes - ----- - This function overwrites the base function, as sql layout doesn't - follow the same conventions done in the other tables. - """ - conn_handler = (conn_handler if conn_handler is not None - else SQLConnectionHandler()) - return conn_handler.execute_fetchone( - "SELECT EXISTS(SELECT * FROM qiita.qiita_user WHERE " - "email = %s)", (id_, ))[0] - # ---properties--- @property diff --git a/qiita_db/util.py b/qiita_db/util.py index b20f5ac2b..da67c8b42 100644 --- a/qiita_db/util.py +++ b/qiita_db/util.py @@ -20,7 +20,8 @@ exists_dynamic_table get_db_files_base_dir compute_checksum - + check_table_cols + check_required_columns """ # ----------------------------------------------------------------------------- # Copyright (c) 2014--, The Qiita Development Team.