Skip to content

Commit d3555ac

Browse files
authored
Merge pull request #2088 from antgonza/adding-tags
Adding tags
2 parents 208bac3 + d6f5223 commit d3555ac

File tree

16 files changed

+618
-72
lines changed

16 files changed

+618
-72
lines changed

qiita_db/study.py

Lines changed: 91 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -475,11 +475,23 @@ def get_tags(cls):
475475
accessed as a list of dictionaries, keyed on column name.
476476
"""
477477
with qdb.sql_connection.TRN:
478-
sql = """SELECT study_tag_id, study_tag
479-
FROM qiita.study_tags"""
478+
sql = """SELECT qiita.user_level.name AS user_level,
479+
array_agg(study_tag)
480+
FROM qiita.study_tags
481+
LEFT JOIN qiita.qiita_user USING (email)
482+
LEFT JOIN qiita.user_level USING (user_level_id)
483+
GROUP BY qiita.user_level.name"""
480484

481485
qdb.sql_connection.TRN.add(sql)
482-
return qdb.sql_connection.TRN.execute_fetchindex()
486+
results = dict(qdb.sql_connection.TRN.execute_fetchindex())
487+
# when the system is empty,
488+
# it's possible to get an empty dict, fixing
489+
if 'admin' not in results:
490+
results['admin'] = []
491+
if 'user' not in results:
492+
results['user'] = []
493+
494+
return results
483495

484496
@classmethod
485497
def insert_tags(cls, user, tags):
@@ -493,14 +505,15 @@ def insert_tags(cls, user, tags):
493505
The list of tags to add
494506
"""
495507
with qdb.sql_connection.TRN:
508+
email = user.email
496509
sql = """INSERT INTO qiita.study_tags (email, study_tag)
497-
VALUES (%s, %s)"""
498-
sql_args = [[user.email, tag] for tag in tags]
510+
SELECT %s, %s WHERE NOT EXISTS (
511+
SELECT 1 FROM qiita.study_tags WHERE study_tag = %s)"""
512+
sql_args = [[email, tag, tag] for tag in tags]
499513

500514
qdb.sql_connection.TRN.add(sql, sql_args, many=True)
501515
qdb.sql_connection.TRN.execute()
502516

503-
504517
# --- Attributes ---
505518
@property
506519
def title(self):
@@ -967,40 +980,12 @@ def tags(self):
967980
The study tags
968981
"""
969982
with qdb.sql_connection.TRN:
970-
sql = """SELECT study_tag_id, study_tag
983+
sql = """SELECT study_tag
971984
FROM qiita.study_tags
972-
LEFT JOIN qiita.per_study_tags USING (study_tag_id)
985+
LEFT JOIN qiita.per_study_tags USING (study_tag)
973986
WHERE study_id = {0}""".format(self._id)
974987
qdb.sql_connection.TRN.add(sql)
975-
return qdb.sql_connection.TRN.execute_fetchindex()
976-
977-
@tags.setter
978-
def tags(self, tag_ids):
979-
"""Sets the tags of the study
980-
981-
Parameters
982-
----------
983-
tag_ids : list of int
984-
The tag ids of the study
985-
"""
986-
with qdb.sql_connection.TRN:
987-
sql = """DELETE FROM qiita.per_study_tags WHERE study_id = %s"""
988-
qdb.sql_connection.TRN.add(sql, [self._id])
989-
990-
if tag_ids:
991-
sql = """INSERT INTO qiita.per_study_tags
992-
(study_tag_id, study_id)
993-
SELECT %s, %s
994-
WHERE
995-
NOT EXISTS (
996-
SELECT study_tag_id, study_id
997-
FROM qiita.per_study_tags
998-
WHERE study_tag_id = %s AND study_id = %s
999-
)"""
1000-
sql_args = [[tid, self._id, tid, self._id] for tid in tag_ids]
1001-
qdb.sql_connection.TRN.add(sql, sql_args, many=True)
1002-
1003-
qdb.sql_connection.TRN.execute()
988+
return [t[0] for t in qdb.sql_connection.TRN.execute_fetchindex()]
1004989

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

1140+
def update_tags(self, user, tags):
1141+
"""Sets the tags of the study
1142+
1143+
Parameters
1144+
----------
1145+
user: User object
1146+
The user reqesting the study tags update
1147+
tags : list of str
1148+
The tags to update within the study
1149+
1150+
Returns
1151+
-------
1152+
str
1153+
Warnings during insertion
1154+
"""
1155+
message = ''
1156+
# converting to set just to facilitate operations
1157+
system_tags_admin = set(self.get_tags()['admin'])
1158+
user_level = user.level
1159+
current_tags = set(self.tags)
1160+
to_delete = current_tags - set(tags)
1161+
to_add = set(tags) - current_tags
1162+
1163+
if to_delete or to_add:
1164+
with qdb.sql_connection.TRN:
1165+
if to_delete:
1166+
if user_level != 'admin':
1167+
admin_tags = to_delete & system_tags_admin
1168+
if admin_tags:
1169+
message += 'You cannot remove: %s' % ', '.join(
1170+
admin_tags)
1171+
to_delete = to_delete - admin_tags
1172+
1173+
if to_delete:
1174+
sql = """DELETE FROM qiita.per_study_tags
1175+
WHERE study_id = %s AND study_tag IN %s"""
1176+
qdb.sql_connection.TRN.add(
1177+
sql, [self._id, tuple(to_delete)])
1178+
1179+
if to_add:
1180+
if user_level != 'admin':
1181+
admin_tags = to_add & system_tags_admin
1182+
if admin_tags:
1183+
message += ('Only admins can assign: '
1184+
'%s' % ', '.join(admin_tags))
1185+
to_add = to_add - admin_tags
1186+
1187+
if to_add:
1188+
self.insert_tags(user, to_add)
1189+
1190+
sql = """INSERT INTO qiita.per_study_tags
1191+
(study_tag, study_id)
1192+
SELECT %s, %s
1193+
WHERE
1194+
NOT EXISTS (
1195+
SELECT study_tag, study_id
1196+
FROM qiita.per_study_tags
1197+
WHERE study_tag = %s
1198+
AND study_id = %s
1199+
)"""
1200+
sql_args = [[t, self._id, t, self._id] for t in to_add]
1201+
qdb.sql_connection.TRN.add(sql, sql_args, many=True)
1202+
1203+
qdb.sql_connection.TRN.execute()
1204+
else:
1205+
message = 'No changes in the tags.'
1206+
1207+
return message
1208+
11551209

11561210
class StudyPerson(qdb.base.QiitaObject):
11571211
r"""Object handling information pertaining to people involved in a study

qiita_db/support_files/patches/52.sql

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
-- Mar 16, 2017
2+
-- Changing tagging system structure, now study_tag will be the index
3+
4+
-- dropping all not required constrints, indexes and columns
5+
ALTER TABLE qiita.study_tags DROP CONSTRAINT fk_study_tags;
6+
DROP INDEX qiita.idx_study_tag_id;
7+
ALTER TABLE qiita.study_tags DROP CONSTRAINT pk_study_tag;
8+
ALTER TABLE qiita.study_tags DROP CONSTRAINT pk_study_tag_id;
9+
ALTER TABLE qiita.study_tags DROP COLUMN study_tag_id;
10+
ALTER TABLE qiita.per_study_tags ADD COLUMN study_tag varchar NOT NULL;
11+
ALTER TABLE qiita.per_study_tags DROP CONSTRAINT pk_per_study_tags;
12+
ALTER TABLE qiita.per_study_tags DROP COLUMN study_tag_id;
13+
14+
-- adding new restrictions
15+
ALTER TABLE qiita.study_tags ADD CONSTRAINT pk_study_tags PRIMARY KEY ( study_tag );
16+
ALTER TABLE qiita.study_tags ADD CONSTRAINT fk_email FOREIGN KEY ( email ) REFERENCES qiita.qiita_user( email );
17+
ALTER TABLE qiita.per_study_tags ADD CONSTRAINT fk_study_tags FOREIGN KEY ( study_tag ) REFERENCES qiita.study_tags( study_tag );
18+
ALTER TABLE qiita.per_study_tags ADD CONSTRAINT fk_study_id FOREIGN KEY ( study_id ) REFERENCES qiita.study( study_id );
19+
ALTER TABLE qiita.per_study_tags ADD CONSTRAINT pk_per_study_tags PRIMARY KEY ( study_tag, study_id);
20+
21+
-- New structure:
22+
-- CREATE TABLE qiita.study_tags (
23+
-- email varchar NOT NULL,
24+
-- study_tag varchar NOT NULL,
25+
-- ) ;
26+
--
27+
-- CREATE TABLE qiita.per_study_tags (
28+
-- study_tag varchar NOT NULL,
29+
-- study_id bigint NOT NULL,
30+
-- ) ;

qiita_db/test/test_study.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -833,31 +833,43 @@ def test_environmental_packages_sandboxed(self):
833833
self.study.environmental_packages = ['air']
834834

835835
def test_study_tags(self):
836+
# testing empty tags
837+
obs = qdb.study.Study.get_tags()
838+
self.assertEqual(obs, {'admin': [], 'user': []})
839+
836840
# inserting new tags
837841
user = qdb.user.User('test@foo.bar')
838-
tags = ['this is my tag', 'I want GOLD!!']
842+
tags = ['this is my tag', 'I want GOLD!!', 'this is my tag']
839843
qdb.study.Study.insert_tags(user, tags)
844+
# now as admin
845+
admin = qdb.user.User('admin@foo.bar')
846+
admin_tags = ['actual GOLD!', 'this is my tag']
847+
qdb.study.Study.insert_tags(admin, admin_tags)
840848

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

846-
# assigning the tags to study
855+
# assigning the tags to study as user
847856
study = qdb.study.Study(1)
848-
self.assertEqual(study.tags, [])
849-
study.tags = [tig for tig, tag in obs]
850-
# and checking that everything went fine
851-
self.assertEqual(obs, study.tags)
852-
853-
# making sure that everything is overwritten
854-
obs.pop()
855-
study.tags = [tig for tig, tag in obs]
856-
self.assertEqual(obs, study.tags)
857+
tags = ['this is my tag', 'actual GOLD!']
858+
message = study.update_tags(user, tags)
859+
self.assertItemsEqual(study.tags, tags[:1])
860+
self.assertEqual(message, 'Only admins can assign: actual GOLD!')
861+
# now like admin
862+
message = study.update_tags(admin, tags)
863+
self.assertItemsEqual(study.tags, tags)
864+
self.assertEqual(message, '')
857865

858866
# cleaning tags
859-
study.tags = []
867+
message = study.update_tags(user, [])
868+
self.assertEqual(study.tags, ['actual GOLD!'])
869+
self.assertEqual(message, 'You cannot remove: actual GOLD!')
870+
message = study.update_tags(admin, [])
860871
self.assertEqual(study.tags, [])
872+
self.assertEqual(message, '')
861873

862874

863875
if __name__ == "__main__":

qiita_pet/handlers/api_proxy/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
prep_template_patch_req)
2727
from .studies import (
2828
data_types_get_req, study_get_req, study_prep_get_req, study_delete_req,
29-
study_files_get_req)
29+
study_files_get_req, study_tags_patch_request, study_get_tags_request,
30+
study_tags_request)
3031
from .artifact import (artifact_graph_get_req, artifact_types_get_req,
3132
artifact_post_req, artifact_get_req,
3233
artifact_status_put_req, artifact_delete_req,
@@ -58,6 +59,8 @@
5859
'sample_template_samples_get_req', 'prep_template_samples_get_req',
5960
'sample_template_category_get_req', 'new_prep_template_get_req',
6061
'study_files_get_req', 'prep_template_ajax_get_req',
62+
'study_tags_request', 'study_tags_patch_request',
63+
'study_get_tags_request',
6164
'prep_template_patch_req', 'ontology_patch_handler',
6265
'artifact_summary_get_request', 'artifact_summary_post_request',
6366
'list_commands_handler_get_req', 'process_artifact_handler_get_req',

qiita_pet/handlers/api_proxy/studies.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,3 +296,103 @@ def study_files_get_req(user_id, study_id, prep_template_id, artifact_type):
296296
'file_types': file_types,
297297
'num_prefixes': num_prefixes,
298298
'artifacts': artifact_options}
299+
300+
301+
def study_tags_request():
302+
"""Retrieve available study tags
303+
304+
Returns
305+
-------
306+
dict of {str, str}
307+
A dictionary with the following keys:
308+
- status: str, whether if the request is successful or not
309+
- message: str, if the request is unsuccessful, a human readable error
310+
- tags: {level: value, ..., ...}
311+
"""
312+
return {'status': 'success',
313+
'message': '',
314+
'tags': Study.get_tags()}
315+
316+
317+
def study_get_tags_request(user_id, study_id):
318+
"""Retrieve available study tags for study_id
319+
320+
Parameters
321+
----------
322+
user_id : int
323+
The id of the user performing the operation
324+
study_id : int
325+
The id of the study on which we will be performing the operation
326+
327+
Returns
328+
-------
329+
dict of {str, str}
330+
A dictionary with the following keys:
331+
- status: str, whether if the request is successful or not
332+
- message: str, if the request is unsuccessful, a human readable error
333+
- tags: [value, ..., ...]
334+
"""
335+
336+
access_error = check_access(study_id, user_id)
337+
if access_error:
338+
return access_error
339+
study = Study(study_id)
340+
341+
return {'status': 'success',
342+
'message': '',
343+
'tags': study.tags}
344+
345+
346+
def study_tags_patch_request(user_id, study_id,
347+
req_op, req_path, req_value=None, req_from=None):
348+
"""Modifies an attribute of the artifact
349+
350+
Parameters
351+
----------
352+
user_id : int
353+
The id of the user performing the patch operation
354+
study_id : int
355+
The id of the study on which we will be performing the patch operation
356+
req_op : str
357+
The operation to perform on the study
358+
req_path : str
359+
The attribute to patch
360+
req_value : str, optional
361+
The value that needs to be modified
362+
req_from : str, optional
363+
The original path of the element
364+
365+
Returns
366+
-------
367+
dict of {str, str}
368+
A dictionary with the following keys:
369+
- status: str, whether if the request is successful or not
370+
- message: str, if the request is unsuccessful, a human readable error
371+
"""
372+
if req_op == 'replace':
373+
req_path = [v for v in req_path.split('/') if v]
374+
if len(req_path) != 1:
375+
return {'status': 'error',
376+
'message': 'Incorrect path parameter'}
377+
378+
attribute = req_path[0]
379+
380+
# Check if the user actually has access to the study
381+
access_error = check_access(study_id, user_id)
382+
if access_error:
383+
return access_error
384+
study = Study(study_id)
385+
386+
if attribute == 'tags':
387+
message = study.update_tags(User(user_id), req_value)
388+
return {'status': 'success',
389+
'message': message}
390+
else:
391+
# We don't understand the attribute so return an error
392+
return {'status': 'error',
393+
'message': 'Attribute "%s" not found. '
394+
'Please, check the path parameter' % attribute}
395+
else:
396+
return {'status': 'error',
397+
'message': 'Operation "%s" not supported. '
398+
'Current supported operations: replace' % req_op}

0 commit comments

Comments
 (0)