Skip to content

Adding tags #2088

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Mar 27, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 91 additions & 37 deletions qiita_db/study.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,11 +475,23 @@ def get_tags(cls):
accessed as a list of dictionaries, keyed on column name.
"""
with qdb.sql_connection.TRN:
sql = """SELECT study_tag_id, study_tag
FROM qiita.study_tags"""
sql = """SELECT qiita.user_level.name AS user_level,
array_agg(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)
return qdb.sql_connection.TRN.execute_fetchindex()
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):
Expand All @@ -493,14 +505,15 @@ def insert_tags(cls, user, tags):
The list of tags to add
"""
with qdb.sql_connection.TRN:
email = user.email
sql = """INSERT INTO qiita.study_tags (email, study_tag)
VALUES (%s, %s)"""
sql_args = [[user.email, tag] for tag in tags]
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 title(self):
Expand Down Expand Up @@ -967,40 +980,12 @@ def tags(self):
The study tags
"""
with qdb.sql_connection.TRN:
sql = """SELECT study_tag_id, study_tag
sql = """SELECT study_tag
FROM qiita.study_tags
LEFT JOIN qiita.per_study_tags USING (study_tag_id)
LEFT JOIN qiita.per_study_tags USING (study_tag)
WHERE study_id = {0}""".format(self._id)
qdb.sql_connection.TRN.add(sql)
return qdb.sql_connection.TRN.execute_fetchindex()

@tags.setter
def tags(self, tag_ids):
"""Sets the tags of the study

Parameters
----------
tag_ids : list of int
The tag ids of the study
"""
with qdb.sql_connection.TRN:
sql = """DELETE FROM qiita.per_study_tags WHERE study_id = %s"""
qdb.sql_connection.TRN.add(sql, [self._id])

if tag_ids:
sql = """INSERT INTO qiita.per_study_tags
(study_tag_id, study_id)
SELECT %s, %s
WHERE
NOT EXISTS (
SELECT study_tag_id, study_id
FROM qiita.per_study_tags
WHERE study_tag_id = %s AND study_id = %s
)"""
sql_args = [[tid, self._id, tid, self._id] for tid in tag_ids]
qdb.sql_connection.TRN.add(sql, sql_args, many=True)

qdb.sql_connection.TRN.execute()
return [t[0] for t in qdb.sql_connection.TRN.execute_fetchindex()]

# --- methods ---
def artifacts(self, dtype=None, artifact_type=None):
Expand Down Expand Up @@ -1152,6 +1137,75 @@ def unshare(self, user):
qdb.sql_connection.TRN.add(sql, [self._id, user.id])
qdb.sql_connection.TRN.execute()

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
Expand Down
30 changes: 30 additions & 0 deletions qiita_db/support_files/patches/52.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-- Mar 16, 2017
-- Changing tagging system structure, now study_tag will be the index

-- dropping all not required constrints, indexes and columns
ALTER TABLE qiita.study_tags DROP CONSTRAINT fk_study_tags;
DROP INDEX qiita.idx_study_tag_id;
ALTER TABLE qiita.study_tags DROP CONSTRAINT pk_study_tag;
ALTER TABLE qiita.study_tags DROP CONSTRAINT pk_study_tag_id;
ALTER TABLE qiita.study_tags DROP COLUMN study_tag_id;
ALTER TABLE qiita.per_study_tags ADD COLUMN study_tag varchar NOT NULL;
ALTER TABLE qiita.per_study_tags DROP CONSTRAINT pk_per_study_tags;
ALTER TABLE qiita.per_study_tags DROP COLUMN study_tag_id;

-- adding new restrictions
ALTER TABLE qiita.study_tags ADD CONSTRAINT pk_study_tags PRIMARY KEY ( study_tag );
ALTER TABLE qiita.study_tags ADD CONSTRAINT fk_email FOREIGN KEY ( email ) REFERENCES qiita.qiita_user( email );
ALTER TABLE qiita.per_study_tags ADD CONSTRAINT fk_study_tags FOREIGN KEY ( study_tag ) REFERENCES qiita.study_tags( study_tag );
ALTER TABLE qiita.per_study_tags ADD CONSTRAINT fk_study_id FOREIGN KEY ( study_id ) REFERENCES qiita.study( study_id );
ALTER TABLE qiita.per_study_tags ADD CONSTRAINT pk_per_study_tags PRIMARY KEY ( study_tag, study_id);

-- New structure:
-- CREATE TABLE qiita.study_tags (
-- email varchar NOT NULL,
-- study_tag varchar NOT NULL,
-- ) ;
--
-- CREATE TABLE qiita.per_study_tags (
-- study_tag varchar NOT NULL,
-- study_id bigint NOT NULL,
-- ) ;
38 changes: 25 additions & 13 deletions qiita_db/test/test_study.py
Original file line number Diff line number Diff line change
Expand Up @@ -833,31 +833,43 @@ def test_environmental_packages_sandboxed(self):
self.study.environmental_packages = ['air']

def test_study_tags(self):
# testing empty tags
obs = qdb.study.Study.get_tags()
self.assertEqual(obs, {'admin': [], 'user': []})

# inserting new tags
user = qdb.user.User('test@foo.bar')
tags = ['this is my tag', 'I want GOLD!!']
tags = ['this is my tag', 'I want GOLD!!', 'this is my tag']
qdb.study.Study.insert_tags(user, tags)
# now as admin
admin = qdb.user.User('admin@foo.bar')
admin_tags = ['actual GOLD!', 'this is my tag']
qdb.study.Study.insert_tags(admin, admin_tags)

# testing that insertion went fine
obs = qdb.study.Study.get_tags()
exp = [[i + 1, tag] for i, tag in enumerate(tags)]
exp = {'user': ['this is my tag', 'I want GOLD!!'],
'admin': ['actual GOLD!']}
self.assertEqual(obs, exp)

# assigning the tags to study
# assigning the tags to study as user
study = qdb.study.Study(1)
self.assertEqual(study.tags, [])
study.tags = [tig for tig, tag in obs]
# and checking that everything went fine
self.assertEqual(obs, study.tags)

# making sure that everything is overwritten
obs.pop()
study.tags = [tig for tig, tag in obs]
self.assertEqual(obs, study.tags)
tags = ['this is my tag', 'actual GOLD!']
message = study.update_tags(user, tags)
self.assertItemsEqual(study.tags, tags[:1])
self.assertEqual(message, 'Only admins can assign: actual GOLD!')
# now like admin
message = study.update_tags(admin, tags)
self.assertItemsEqual(study.tags, tags)
self.assertEqual(message, '')

# cleaning tags
study.tags = []
message = study.update_tags(user, [])
self.assertEqual(study.tags, ['actual GOLD!'])
self.assertEqual(message, 'You cannot remove: actual GOLD!')
message = study.update_tags(admin, [])
self.assertEqual(study.tags, [])
self.assertEqual(message, '')


if __name__ == "__main__":
Expand Down
5 changes: 4 additions & 1 deletion qiita_pet/handlers/api_proxy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
prep_template_patch_req)
from .studies import (
data_types_get_req, study_get_req, study_prep_get_req, study_delete_req,
study_files_get_req)
study_files_get_req, study_tags_patch_request, study_get_tags_request,
study_tags_request)
from .artifact import (artifact_graph_get_req, artifact_types_get_req,
artifact_post_req, artifact_get_req,
artifact_status_put_req, artifact_delete_req,
Expand Down Expand Up @@ -58,6 +59,8 @@
'sample_template_samples_get_req', 'prep_template_samples_get_req',
'sample_template_category_get_req', 'new_prep_template_get_req',
'study_files_get_req', 'prep_template_ajax_get_req',
'study_tags_request', 'study_tags_patch_request',
'study_get_tags_request',
'prep_template_patch_req', 'ontology_patch_handler',
'artifact_summary_get_request', 'artifact_summary_post_request',
'list_commands_handler_get_req', 'process_artifact_handler_get_req',
Expand Down
100 changes: 100 additions & 0 deletions qiita_pet/handlers/api_proxy/studies.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,3 +296,103 @@ def study_files_get_req(user_id, study_id, prep_template_id, artifact_type):
'file_types': file_types,
'num_prefixes': num_prefixes,
'artifacts': artifact_options}


def study_tags_request():
"""Retrieve available study tags

Returns
-------
dict of {str, str}
A dictionary with the following keys:
- status: str, whether if the request is successful or not
- message: str, if the request is unsuccessful, a human readable error
- tags: {level: value, ..., ...}
"""
return {'status': 'success',
'message': '',
'tags': Study.get_tags()}


def study_get_tags_request(user_id, study_id):
"""Retrieve available study tags for study_id

Parameters
----------
user_id : int
The id of the user performing the operation
study_id : int
The id of the study on which we will be performing the operation

Returns
-------
dict of {str, str}
A dictionary with the following keys:
- status: str, whether if the request is successful or not
- message: str, if the request is unsuccessful, a human readable error
- tags: [value, ..., ...]
"""

access_error = check_access(study_id, user_id)
if access_error:
return access_error
study = Study(study_id)

return {'status': 'success',
'message': '',
'tags': study.tags}


def study_tags_patch_request(user_id, study_id,
req_op, req_path, req_value=None, req_from=None):
"""Modifies an attribute of the artifact

Parameters
----------
user_id : int
The id of the user performing the patch operation
study_id : int
The id of the study on which we will be performing the patch operation
req_op : str
The operation to perform on the study
req_path : str
The attribute to patch
req_value : str, optional
The value that needs to be modified
req_from : str, optional
The original path of the element

Returns
-------
dict of {str, str}
A dictionary with the following keys:
- status: str, whether if the request is successful or not
- message: str, if the request is unsuccessful, a human readable error
"""
if req_op == 'replace':
req_path = [v for v in req_path.split('/') if v]
if len(req_path) != 1:
return {'status': 'error',
'message': 'Incorrect path parameter'}

attribute = req_path[0]

# Check if the user actually has access to the study
access_error = check_access(study_id, user_id)
if access_error:
return access_error
study = Study(study_id)

if attribute == 'tags':
message = study.update_tags(User(user_id), req_value)
return {'status': 'success',
'message': message}
else:
# We don't understand the attribute so return an error
return {'status': 'error',
'message': 'Attribute "%s" not found. '
'Please, check the path parameter' % attribute}
else:
return {'status': 'error',
'message': 'Operation "%s" not supported. '
'Current supported operations: replace' % req_op}
Loading