diff --git a/qiita_core/tests/test_util.py b/qiita_core/tests/test_util.py index 2b3fbdc27..28c878be5 100644 --- a/qiita_core/tests/test_util.py +++ b/qiita_core/tests/test_util.py @@ -12,7 +12,8 @@ from qiita_core.util import ( send_email, qiita_test_checker, execute_as_transaction, get_qiita_version, - is_test_environment) + is_test_environment, get_release_info) +from qiita_db.meta_util import generate_biom_and_metadata_release import qiita_db as qdb @@ -64,6 +65,21 @@ def test_get_qiita_version(self): # testing just the version self.assertEqual(exp_version, qdb.__version__) + def test_get_release_info(self): + # making sure there is a release + generate_biom_and_metadata_release('private') + # just checking that is not empty cause the MD5 will change on every + # run + md5sum, filepath, timestamp = get_release_info('private') + self.assertNotEqual(md5sum, '') + self.assertNotEqual(filepath, '') + self.assertNotEqual(timestamp, '') + + md5sum, filepath, timestamp = get_release_info('public') + self.assertEqual(md5sum, '') + self.assertEqual(filepath, '') + self.assertEqual(timestamp, '') + if __name__ == '__main__': main() diff --git a/qiita_core/util.py b/qiita_core/util.py index 7d3face3c..114b85ae9 100644 --- a/qiita_core/util.py +++ b/qiita_core/util.py @@ -11,6 +11,7 @@ from os.path import dirname from git import Repo from git.exc import InvalidGitRepositoryError +from moi import r_client from qiita_core.qiita_settings import qiita_config from qiita_pet import __version__ as qiita_pet_lib_version @@ -141,3 +142,32 @@ def get_qiita_version(): sha = '' return (qiita_pet_lib_version, sha) + + +def get_release_info(study_status='public'): + """Returns the study status release MD5 + + Parameters + ---------- + study_status : str, optional + The study status to search for. Note that this should always be set + to 'public' but having this exposed helps with testing. The other + options are 'private' and 'sandbox' + + Returns + ------ + str, str, str + The release MD5, filepath and timestamp + """ + portal = qiita_config.portal + md5sum = r_client.get('%s:release:%s:md5sum' % (portal, study_status)) + filepath = r_client.get('%s:release:%s:filepath' % (portal, study_status)) + timestamp = r_client.get('%s:release:%s:time' % (portal, study_status)) + if md5sum is None: + md5sum = '' + if filepath is None: + filepath = '' + if timestamp is None: + timestamp = '' + + return md5sum, filepath, timestamp diff --git a/qiita_db/artifact.py b/qiita_db/artifact.py index 9bc553a53..717579d72 100644 --- a/qiita_db/artifact.py +++ b/qiita_db/artifact.py @@ -274,9 +274,11 @@ def create(cls, filepaths, artifact_type, name=None, prep_template=None, Notes ----- - The visibility of the artifact is set by default to `sandbox` - The timestamp of the artifact is set by default to `datetime.now()` - The value of `submitted_to_vamps` is set by default to `False` + The visibility of the artifact is set by default to `sandbox` if + prep_template is passed but if parents is passed we will inherit the + most closed visibility. + The timestamp of the artifact is set by default to `datetime.now()`. + The value of `submitted_to_vamps` is set by default to `False`. """ # We need at least one file if not filepaths: @@ -689,19 +691,21 @@ def visibility(self, value): only applies when the new visibility is more open than before. """ with qdb.sql_connection.TRN: + # In order to correctly propagate the visibility we need to find + # the root of this artifact and then propagate to all the artifacts + sql = "SELECT * FROM qiita.find_artifact_roots(%s)" + qdb.sql_connection.TRN.add(sql, [self.id]) + root_id = qdb.sql_connection.TRN.execute_fetchlast() + root = qdb.artifact.Artifact(root_id) + # these are the ids of all the children from the root + ids = [a.id for a in root.descendants.nodes()] + sql = """UPDATE qiita.artifact SET visibility_id = %s - WHERE artifact_id = %s""" - qdb.sql_connection.TRN.add( - sql, [qdb.util.convert_to_id(value, "visibility"), self.id]) + WHERE artifact_id IN %s""" + vis_id = qdb.util.convert_to_id(value, "visibility") + qdb.sql_connection.TRN.add(sql, [vis_id, tuple(ids)]) qdb.sql_connection.TRN.execute() - # In order to correctly propagate the visibility upstream, we need - # to go one step at a time. By setting up the visibility of our - # parents first, we accomplish that, since they will propagate - # the changes to its parents - for p in self.parents: - visibilites = [[d.visibility] for d in p.descendants.nodes()] - p.visibility = qdb.util.infer_status(visibilites) @property def artifact_type(self): diff --git a/qiita_db/meta_util.py b/qiita_db/meta_util.py index 84e7f70a8..1978ca5e4 100644 --- a/qiita_db/meta_util.py +++ b/qiita_db/meta_util.py @@ -25,7 +25,8 @@ from __future__ import division from moi import r_client -from os import stat +from os import stat, makedirs, rename +from os.path import join, relpath, exists from time import strftime, localtime import matplotlib.pyplot as plt import matplotlib as mpl @@ -34,8 +35,11 @@ from StringIO import StringIO from future.utils import viewitems from datetime import datetime +from tarfile import open as topen, TarInfo +from hashlib import md5 from qiita_core.qiita_settings import qiita_config +from qiita_core.configuration_manager import ConfigurationManager import qiita_db as qdb @@ -126,14 +130,21 @@ def validate_filepath_access_by_user(user, filepath_id): # the prep access is given by it's artifacts, if the user has # access to any artifact, it should have access to the prep # [0] cause we should only have 1 - a = qdb.metadata_template.prep_template.PrepTemplate( - pid[0]).artifact - if (a.visibility == 'public' or a.study.has_access(user)): - return True + pt = qdb.metadata_template.prep_template.PrepTemplate( + pid[0]) + a = pt.artifact + # however, the prep info file could not have any artifacts attached + # , in that case we will use the study access level + if a is None: + return qdb.study.Study(pt.study_id).has_access(user) else: - for c in a.descendants.nodes(): - if (c.visibility == 'public' or c.study.has_access(user)): - return True + if (a.visibility == 'public' or a.study.has_access(user)): + return True + else: + for c in a.descendants.nodes(): + if ((c.visibility == 'public' or + c.study.has_access(user))): + return True return False # analyses elif anid: @@ -305,7 +316,8 @@ def get_lat_longs(): WHERE table_name SIMILAR TO 'sample_[0-9]+' AND table_schema = 'qiita' AND column_name IN ('latitude', 'longitude') - AND SPLIT_PART(table_name, '_', 2)::int IN %s;""" + AND SPLIT_PART(table_name, '_', 2)::int IN %s + GROUP BY table_name HAVING COUNT(column_name) = 2;""" qdb.sql_connection.TRN.add(sql, [tuple(portal_table_ids)]) sql = [('SELECT CAST(latitude AS FLOAT), ' @@ -319,3 +331,99 @@ def get_lat_longs(): qdb.sql_connection.TRN.add(sql) return qdb.sql_connection.TRN.execute_fetchindex() + + +def generate_biom_and_metadata_release(study_status='public'): + """Generate a list of biom/meatadata filepaths and a tgz of those files + + Parameters + ---------- + study_status : str, optional + The study status to search for. Note that this should always be set + to 'public' but having this exposed helps with testing. The other + options are 'private' and 'sandbox' + """ + studies = qdb.study.Study.get_by_status(study_status) + qiita_config = ConfigurationManager() + working_dir = qiita_config.working_dir + portal = qiita_config.portal + bdir = qdb.util.get_db_files_base_dir() + time = datetime.now().strftime('%m-%d-%y %H:%M:%S') + + data = [] + for s in studies: + # [0] latest is first, [1] only getting the filepath + sample_fp = relpath(s.sample_template.get_filepaths()[0][1], bdir) + + for a in s.artifacts(artifact_type='BIOM'): + if a.processing_parameters is None: + continue + + cmd_name = a.processing_parameters.command.name + + # this loop is necessary as in theory an artifact can be + # generated from multiple prep info files + human_cmd = [] + for p in a.parents: + pp = p.processing_parameters + pp_cmd_name = pp.command.name + if pp_cmd_name == 'Trimming': + human_cmd.append('%s @ %s' % ( + cmd_name, str(pp.values['length']))) + else: + human_cmd.append('%s, %s' % (cmd_name, pp_cmd_name)) + human_cmd = ', '.join(human_cmd) + + for _, fp, fp_type in a.filepaths: + if fp_type != 'biom' or 'only-16s' in fp: + continue + fp = relpath(fp, bdir) + # format: (biom_fp, sample_fp, prep_fp, qiita_artifact_id, + # human readable name) + for pt in a.prep_templates: + for _, prep_fp in pt.get_filepaths(): + if 'qiime' not in prep_fp: + break + prep_fp = relpath(prep_fp, bdir) + data.append((fp, sample_fp, prep_fp, a.id, human_cmd)) + + # writing text and tgz file + ts = datetime.now().strftime('%m%d%y-%H%M%S') + tgz_dir = join(working_dir, 'releases') + if not exists(tgz_dir): + makedirs(tgz_dir) + tgz_name = join(tgz_dir, '%s-%s-building.tgz' % (portal, study_status)) + tgz_name_final = join(tgz_dir, '%s-%s.tgz' % (portal, study_status)) + txt_hd = StringIO() + with topen(tgz_name, "w|gz") as tgz: + # writing header for txt + txt_hd.write( + "biom_fp\tsample_fp\tprep_fp\tqiita_artifact_id\tcommand\n") + for biom_fp, sample_fp, prep_fp, artifact_id, human_cmd in data: + txt_hd.write("%s\t%s\t%s\t%s\t%s\n" % ( + biom_fp, sample_fp, prep_fp, artifact_id, human_cmd)) + tgz.add(join(bdir, biom_fp), arcname=biom_fp, recursive=False) + tgz.add(join(bdir, sample_fp), arcname=sample_fp, recursive=False) + tgz.add(join(bdir, prep_fp), arcname=prep_fp, recursive=False) + + txt_hd.seek(0) + info = TarInfo(name='%s-%s-%s.txt' % (portal, study_status, ts)) + info.size = len(txt_hd.buf) + tgz.addfile(tarinfo=info, fileobj=txt_hd) + + with open(tgz_name, "rb") as f: + md5sum = md5() + for c in iter(lambda: f.read(4096), b""): + md5sum.update(c) + + rename(tgz_name, tgz_name_final) + + vals = [ + ('filepath', tgz_name_final[len(working_dir):], r_client.set), + ('md5sum', md5sum.hexdigest(), r_client.set), + ('time', time, r_client.set)] + for k, v, f in vals: + redis_key = '%s:release:%s:%s' % (portal, study_status, k) + # important to "flush" variables to avoid errors + r_client.delete(redis_key) + f(redis_key, v) diff --git a/qiita_db/study.py b/qiita_db/study.py index ef21feddc..c26169712 100644 --- a/qiita_db/study.py +++ b/qiita_db/study.py @@ -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): @@ -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): @@ -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): @@ -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 @@ -1213,6 +1267,33 @@ def exists(cls, name, affiliation): 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. diff --git a/qiita_db/support_files/patches/52.sql b/qiita_db/support_files/patches/52.sql index 24c0904d8..2f32707c5 100644 --- a/qiita_db/support_files/patches/52.sql +++ b/qiita_db/support_files/patches/52.sql @@ -1,120 +1,30 @@ --- Jan 5, 2017 --- Move the analysis to the plugin system. This is a major rewrite of the --- database backend that supports the analysis pipeline. --- After exploring the data on the database, we realized that --- there are a lot of inconsistencies in the data. Unfortunately, this --- makes the process of transferring the data from the old structure --- to the new one a bit more challenging, as we will need to handle --- different special cases. Furthermore, all the information needed is not --- present in the database, since it requires checking BIOM files. Due to these --- reason, the vast majority of the data transfer is done in the python patch --- 51.py - --- In this file we are just creating the new data structures. The old --- datastructure will be dropped in the python patch once all data has been --- transferred. - --- Create the new data structures - --- Table that links the analysis with the initial set of artifacts -CREATE TABLE qiita.analysis_artifact ( - analysis_id bigint NOT NULL, - artifact_id bigint NOT NULL, - CONSTRAINT idx_analysis_artifact_0 PRIMARY KEY (analysis_id, artifact_id) -); -CREATE INDEX idx_analysis_artifact_analysis ON qiita.analysis_artifact (analysis_id); -CREATE INDEX idx_analysis_artifact_artifact ON qiita.analysis_artifact (artifact_id); -ALTER TABLE qiita.analysis_artifact ADD CONSTRAINT fk_analysis_artifact_analysis FOREIGN KEY ( analysis_id ) REFERENCES qiita.analysis( analysis_id ); -ALTER TABLE qiita.analysis_artifact ADD CONSTRAINT fk_analysis_artifact_artifact FOREIGN KEY ( artifact_id ) REFERENCES qiita.artifact( artifact_id ); - --- Droping the analysis status column cause now it depends on the artifacts --- status, like the study does. -ALTER TABLE qiita.analysis DROP COLUMN analysis_status_id; - --- Create a table to link the analysis with the jobs that create the initial --- artifacts -CREATE TABLE qiita.analysis_processing_job ( - analysis_id bigint NOT NULL, - processing_job_id uuid NOT NULL, - CONSTRAINT idx_analysis_processing_job PRIMARY KEY ( analysis_id, processing_job_id ) - ) ; - -CREATE INDEX idx_analysis_processing_job_analysis ON qiita.analysis_processing_job ( analysis_id ) ; -CREATE INDEX idx_analysis_processing_job_pj ON qiita.analysis_processing_job ( processing_job_id ) ; -ALTER TABLE qiita.analysis_processing_job ADD CONSTRAINT fk_analysis_processing_job FOREIGN KEY ( analysis_id ) REFERENCES qiita.analysis( analysis_id ) ; -ALTER TABLE qiita.analysis_processing_job ADD CONSTRAINT fk_analysis_processing_job_pj FOREIGN KEY ( processing_job_id ) REFERENCES qiita.processing_job( processing_job_id ) ; - --- Add a logging column in the analysis -ALTER TABLE qiita.analysis ADD logging_id bigint ; -CREATE INDEX idx_analysis_0 ON qiita.analysis ( logging_id ) ; -ALTER TABLE qiita.analysis ADD CONSTRAINT fk_analysis_logging FOREIGN KEY ( logging_id ) REFERENCES qiita.logging( logging_id ) ; - --- Alter the software command table to differentiate between commands that --- apply to the analysis pipeline or commands that apply on the study --- processing pipeline -ALTER TABLE qiita.software_command ADD is_analysis bool DEFAULT 'False' NOT NULL; - --- We can handle some of the special cases here, so we simplify the work in the --- python patch - --- Special case 1: there are jobs in the database that do not contain --- any information about the options used to process those parameters. --- However, these jobs do not have any results and all are marked either --- as queued or error, although no error log has been saved. Since these --- jobs are mainly useleess, we are going to remove them from the system -DELETE FROM qiita.analysis_job - WHERE job_id IN (SELECT job_id FROM qiita.job WHERE options = '{}'); -DELETE FROM qiita.job WHERE options = '{}'; - --- Special case 2: there are a fair amount of jobs (719 last time I --- checked) that are not attached to any analysis. Not sure how this --- can happen, but these orphan jobs can't be accessed from anywhere --- in the interface. Remove them from the system. Note that we are --- unlinking the files but we are not removing them from the filepath --- table. We will do that on the patch 47.py using the --- purge_filepaths function, as it will make sure that those files are --- not used anywhere else -DELETE FROM qiita.job_results_filepath WHERE job_id IN ( - SELECT job_id FROM qiita.job J WHERE NOT EXISTS ( - SELECT * FROM qiita.analysis_job AJ WHERE J.job_id = AJ.job_id)); -DELETE FROM qiita.job J WHERE NOT EXISTS ( - SELECT * FROM qiita.analysis_job AJ WHERE J.job_id = AJ.job_id); - --- In the analysis pipeline, an artifact can have mutliple datatypes --- (e.g. procrustes). Allow this by creating a new data_type being "multiomic" -INSERT INTO qiita.data_type (data_type) VALUES ('Multiomic'); - - --- The valdiate command from BIOM will have an extra parameter, analysis --- Magic number -> 4 BIOM command_id -> known for sure since it was added in --- patch 36.sql -INSERT INTO qiita.command_parameter (command_id, parameter_name, parameter_type, required) - VALUES (4, 'analysis', 'analysis', FALSE); --- The template comand now becomes optional, since it can be added either to --- an analysis or to a prep template. command_parameter_id known from patch --- 36.sql -UPDATE qiita.command_parameter SET required = FALSE WHERE command_parameter_id = 34; - --- We are going to add a new special software type, and a new software. --- This is going to be used internally by Qiita, so submit the private jobs. --- This is needed for the analysis. -INSERT INTO qiita.software_type (software_type, description) - VALUES ('private', 'Internal Qiita jobs'); - -DO $do$ -DECLARE - qiita_sw_id bigint; - baf_cmd_id bigint; -BEGIN - INSERT INTO qiita.software (name, version, description, environment_script, start_script, software_type_id, active) - VALUES ('Qiita', 'alpha', 'Internal Qiita jobs', 'source activate qiita', 'qiita-private-plugin', 3, True) - RETURNING software_id INTO qiita_sw_id; - - INSERT INTO qiita.software_command (software_id, name, description) - VALUES (qiita_sw_id, 'build_analysis_files', 'Builds the files needed for the analysis') - RETURNING command_id INTO baf_cmd_id; - - INSERT INTO qiita.command_parameter (command_id, parameter_name, parameter_type, required, default_value) - VALUES (baf_cmd_id, 'analysis', 'analysis', True, NULL), - (baf_cmd_id, 'merge_dup_sample_ids', 'bool', False, 'False'); -END $do$ +-- 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, +-- ) ; diff --git a/qiita_db/support_files/patches/53.sql b/qiita_db/support_files/patches/53.sql new file mode 100644 index 000000000..0ae05a48c --- /dev/null +++ b/qiita_db/support_files/patches/53.sql @@ -0,0 +1,5 @@ +-- Apr 1, 2017 +-- setting visibility of all artifacts to be the most open of the full +-- processing tree + +SELECT 42; diff --git a/qiita_db/support_files/patches/54.sql b/qiita_db/support_files/patches/54.sql new file mode 100644 index 000000000..24c0904d8 --- /dev/null +++ b/qiita_db/support_files/patches/54.sql @@ -0,0 +1,120 @@ +-- Jan 5, 2017 +-- Move the analysis to the plugin system. This is a major rewrite of the +-- database backend that supports the analysis pipeline. +-- After exploring the data on the database, we realized that +-- there are a lot of inconsistencies in the data. Unfortunately, this +-- makes the process of transferring the data from the old structure +-- to the new one a bit more challenging, as we will need to handle +-- different special cases. Furthermore, all the information needed is not +-- present in the database, since it requires checking BIOM files. Due to these +-- reason, the vast majority of the data transfer is done in the python patch +-- 51.py + +-- In this file we are just creating the new data structures. The old +-- datastructure will be dropped in the python patch once all data has been +-- transferred. + +-- Create the new data structures + +-- Table that links the analysis with the initial set of artifacts +CREATE TABLE qiita.analysis_artifact ( + analysis_id bigint NOT NULL, + artifact_id bigint NOT NULL, + CONSTRAINT idx_analysis_artifact_0 PRIMARY KEY (analysis_id, artifact_id) +); +CREATE INDEX idx_analysis_artifact_analysis ON qiita.analysis_artifact (analysis_id); +CREATE INDEX idx_analysis_artifact_artifact ON qiita.analysis_artifact (artifact_id); +ALTER TABLE qiita.analysis_artifact ADD CONSTRAINT fk_analysis_artifact_analysis FOREIGN KEY ( analysis_id ) REFERENCES qiita.analysis( analysis_id ); +ALTER TABLE qiita.analysis_artifact ADD CONSTRAINT fk_analysis_artifact_artifact FOREIGN KEY ( artifact_id ) REFERENCES qiita.artifact( artifact_id ); + +-- Droping the analysis status column cause now it depends on the artifacts +-- status, like the study does. +ALTER TABLE qiita.analysis DROP COLUMN analysis_status_id; + +-- Create a table to link the analysis with the jobs that create the initial +-- artifacts +CREATE TABLE qiita.analysis_processing_job ( + analysis_id bigint NOT NULL, + processing_job_id uuid NOT NULL, + CONSTRAINT idx_analysis_processing_job PRIMARY KEY ( analysis_id, processing_job_id ) + ) ; + +CREATE INDEX idx_analysis_processing_job_analysis ON qiita.analysis_processing_job ( analysis_id ) ; +CREATE INDEX idx_analysis_processing_job_pj ON qiita.analysis_processing_job ( processing_job_id ) ; +ALTER TABLE qiita.analysis_processing_job ADD CONSTRAINT fk_analysis_processing_job FOREIGN KEY ( analysis_id ) REFERENCES qiita.analysis( analysis_id ) ; +ALTER TABLE qiita.analysis_processing_job ADD CONSTRAINT fk_analysis_processing_job_pj FOREIGN KEY ( processing_job_id ) REFERENCES qiita.processing_job( processing_job_id ) ; + +-- Add a logging column in the analysis +ALTER TABLE qiita.analysis ADD logging_id bigint ; +CREATE INDEX idx_analysis_0 ON qiita.analysis ( logging_id ) ; +ALTER TABLE qiita.analysis ADD CONSTRAINT fk_analysis_logging FOREIGN KEY ( logging_id ) REFERENCES qiita.logging( logging_id ) ; + +-- Alter the software command table to differentiate between commands that +-- apply to the analysis pipeline or commands that apply on the study +-- processing pipeline +ALTER TABLE qiita.software_command ADD is_analysis bool DEFAULT 'False' NOT NULL; + +-- We can handle some of the special cases here, so we simplify the work in the +-- python patch + +-- Special case 1: there are jobs in the database that do not contain +-- any information about the options used to process those parameters. +-- However, these jobs do not have any results and all are marked either +-- as queued or error, although no error log has been saved. Since these +-- jobs are mainly useleess, we are going to remove them from the system +DELETE FROM qiita.analysis_job + WHERE job_id IN (SELECT job_id FROM qiita.job WHERE options = '{}'); +DELETE FROM qiita.job WHERE options = '{}'; + +-- Special case 2: there are a fair amount of jobs (719 last time I +-- checked) that are not attached to any analysis. Not sure how this +-- can happen, but these orphan jobs can't be accessed from anywhere +-- in the interface. Remove them from the system. Note that we are +-- unlinking the files but we are not removing them from the filepath +-- table. We will do that on the patch 47.py using the +-- purge_filepaths function, as it will make sure that those files are +-- not used anywhere else +DELETE FROM qiita.job_results_filepath WHERE job_id IN ( + SELECT job_id FROM qiita.job J WHERE NOT EXISTS ( + SELECT * FROM qiita.analysis_job AJ WHERE J.job_id = AJ.job_id)); +DELETE FROM qiita.job J WHERE NOT EXISTS ( + SELECT * FROM qiita.analysis_job AJ WHERE J.job_id = AJ.job_id); + +-- In the analysis pipeline, an artifact can have mutliple datatypes +-- (e.g. procrustes). Allow this by creating a new data_type being "multiomic" +INSERT INTO qiita.data_type (data_type) VALUES ('Multiomic'); + + +-- The valdiate command from BIOM will have an extra parameter, analysis +-- Magic number -> 4 BIOM command_id -> known for sure since it was added in +-- patch 36.sql +INSERT INTO qiita.command_parameter (command_id, parameter_name, parameter_type, required) + VALUES (4, 'analysis', 'analysis', FALSE); +-- The template comand now becomes optional, since it can be added either to +-- an analysis or to a prep template. command_parameter_id known from patch +-- 36.sql +UPDATE qiita.command_parameter SET required = FALSE WHERE command_parameter_id = 34; + +-- We are going to add a new special software type, and a new software. +-- This is going to be used internally by Qiita, so submit the private jobs. +-- This is needed for the analysis. +INSERT INTO qiita.software_type (software_type, description) + VALUES ('private', 'Internal Qiita jobs'); + +DO $do$ +DECLARE + qiita_sw_id bigint; + baf_cmd_id bigint; +BEGIN + INSERT INTO qiita.software (name, version, description, environment_script, start_script, software_type_id, active) + VALUES ('Qiita', 'alpha', 'Internal Qiita jobs', 'source activate qiita', 'qiita-private-plugin', 3, True) + RETURNING software_id INTO qiita_sw_id; + + INSERT INTO qiita.software_command (software_id, name, description) + VALUES (qiita_sw_id, 'build_analysis_files', 'Builds the files needed for the analysis') + RETURNING command_id INTO baf_cmd_id; + + INSERT INTO qiita.command_parameter (command_id, parameter_name, parameter_type, required, default_value) + VALUES (baf_cmd_id, 'analysis', 'analysis', True, NULL), + (baf_cmd_id, 'merge_dup_sample_ids', 'bool', False, 'False'); +END $do$ diff --git a/qiita_db/support_files/patches/python_patches/53.py b/qiita_db/support_files/patches/python_patches/53.py new file mode 100644 index 000000000..48961d4de --- /dev/null +++ b/qiita_db/support_files/patches/python_patches/53.py @@ -0,0 +1,20 @@ +from qiita_db.study import Study + +studies = Study.get_by_status('private').union( + Study.get_by_status('public')).union(Study.get_by_status('sandbox')) +raw_data = [pt.artifact for s in studies for pt in s.prep_templates() + if pt.artifact is not None] + +for rd in raw_data: + # getting the most open visibility of all the children in the pipeline + children = rd.descendants.nodes() + vis = [a.visibility for a in children] + vis.append(rd.visibility) + + new_vis = 'sandbox' + if 'public' in vis: + new_vis = 'public' + elif 'private' in vis: + new_vis = 'private' + + rd.visibility = new_vis diff --git a/qiita_db/support_files/patches/python_patches/52.py b/qiita_db/support_files/patches/python_patches/54.py similarity index 100% rename from qiita_db/support_files/patches/python_patches/52.py rename to qiita_db/support_files/patches/python_patches/54.py diff --git a/qiita_db/support_files/test_data/raw_data/1_s_G1_L001_sequences.fastq.gz b/qiita_db/support_files/test_data/raw_data/1_s_G1_L001_sequences.fastq.gz new file mode 100644 index 000000000..76cb17801 Binary files /dev/null and b/qiita_db/support_files/test_data/raw_data/1_s_G1_L001_sequences.fastq.gz differ diff --git a/qiita_db/support_files/test_data/raw_data/1_s_G1_L001_sequences_barcodes.fastq.gz b/qiita_db/support_files/test_data/raw_data/1_s_G1_L001_sequences_barcodes.fastq.gz new file mode 100644 index 000000000..76cb17801 Binary files /dev/null and b/qiita_db/support_files/test_data/raw_data/1_s_G1_L001_sequences_barcodes.fastq.gz differ diff --git a/qiita_db/test/test_artifact.py b/qiita_db/test/test_artifact.py index 89b13c661..0f51c498c 100644 --- a/qiita_db/test/test_artifact.py +++ b/qiita_db/test/test_artifact.py @@ -1047,8 +1047,8 @@ def test_visibility_setter(self): # /- 2 (private) -|- 5 (private) # 1 (private) -| \- 6 (private) # \- 3 (private) - # By changing the visibility of 4 to public, the visibility of 1 and - # 2 also changes to public + # By changing the visibility of 4 to public, the visibility of all + # should change a1 = qdb.artifact.Artifact(1) a2 = qdb.artifact.Artifact(2) a3 = qdb.artifact.Artifact(3) @@ -1060,17 +1060,16 @@ def test_visibility_setter(self): self.assertEqual(a1.visibility, "public") self.assertEqual(a2.visibility, "public") - self.assertEqual(a3.visibility, "private") + self.assertEqual(a3.visibility, "public") self.assertEqual(a4.visibility, "public") - self.assertEqual(a5.visibility, "private") - self.assertEqual(a6.visibility, "private") + self.assertEqual(a5.visibility, "public") + self.assertEqual(a6.visibility, "public") - # However, if we change it back to private, - # it should remain public + # Same if we go back a4.visibility = 'private' - self.assertEqual(a1.visibility, "public") - self.assertEqual(a2.visibility, "public") + self.assertEqual(a1.visibility, "private") + self.assertEqual(a2.visibility, "private") self.assertEqual(a3.visibility, "private") self.assertEqual(a4.visibility, "private") self.assertEqual(a5.visibility, "private") diff --git a/qiita_db/test/test_meta_util.py b/qiita_db/test/test_meta_util.py index d68260bcd..17f28f402 100644 --- a/qiita_db/test/test_meta_util.py +++ b/qiita_db/test/test_meta_util.py @@ -7,6 +7,10 @@ # ----------------------------------------------------------------------------- from unittest import TestCase, main +import numpy.testing as npt +from tarfile import open as topen +from os import remove +from os.path import exists, join import pandas as pd @@ -21,9 +25,13 @@ class MetaUtilTests(TestCase): def setUp(self): self.old_portal = qiita_config.portal + self.files_to_remove = [] def tearDown(self): qiita_config.portal = self.old_portal + for fp in self.files_to_remove: + if exists(fp): + remove(fp) def _set_artifact_private(self): self.conn_handler.execute( @@ -99,7 +107,32 @@ def test_validate_filepath_access_by_user(self): self.assertTrue(qdb.meta_util.validate_filepath_access_by_user( admin, i[0])) - # returning to origina sharing + # testing access to a prep info file without artifacts + # returning artifacts to private + self._set_artifact_private() + PT = qdb.metadata_template.prep_template.PrepTemplate + md_dict = { + 'SKB8.640193': {'center_name': 'ANL', + 'center_project_name': 'Test Project', + 'ebi_submission_accession': None, + 'linkerprimersequence': 'GTGCCAGCMGCCGCGGTAA', + 'barcodesequence': 'GTCCGCAAGTTA', + 'run_prefix': "s_G1_L001_sequences", + 'platform': 'ILLUMINA', + 'instrument_model': 'Illumina MiSeq', + 'library_construction_protocol': 'AAAA', + 'experiment_design_description': 'BBBB'} + } + md = pd.DataFrame.from_dict(md_dict, orient='index', dtype=str) + # creating prep info on Study(1), which is our default Study + pt = npt.assert_warns(qdb.exceptions.QiitaDBWarning, PT.create, md, + qdb.study.Study(1), "18S") + for idx, _ in pt.get_filepaths(): + self.assertFalse(qdb.meta_util.validate_filepath_access_by_user( + user, idx)) + + # returning to original sharing + PT.delete(pt.id) qdb.study.Study(1).share(user) qdb.analysis.Analysis(1).share(user) qdb.study.Study.delete(study.id) @@ -202,6 +235,164 @@ def test_update_redis_stats(self): redis_key = '%s:stats:%s' % (portal, k) self.assertEqual(f(redis_key), exp) + def test_generate_biom_and_metadata_release(self): + level = 'private' + qdb.meta_util.generate_biom_and_metadata_release(level) + portal = qiita_config.portal + working_dir = qiita_config.working_dir + + vals = [ + ('filepath', r_client.get), + ('md5sum', r_client.get), + ('time', r_client.get)] + # we are storing the [0] filepath, [1] md5sum and [2] time but we are + # only going to check the filepath contents so ignoring the others + tgz = vals[0][1]('%s:release:%s:%s' % (portal, level, vals[0][0])) + tgz = join(working_dir, tgz) + + self.files_to_remove.extend([tgz]) + + tmp = topen(tgz, "r:gz") + tgz_obs = [ti.name for ti in tmp] + tmp.close() + # files names might change due to updates and patches so just check + # that the prefix exists. + fn = 'processed_data/1_study_1001_closed_reference_otu_table.biom' + self.assertTrue(fn in tgz_obs) + tgz_obs.remove(fn) + # yes, this file is there twice + self.assertTrue(fn in tgz_obs) + tgz_obs.remove(fn) + # let's check the next biom + fn = ('processed_data/1_study_1001_closed_reference_otu_table_Silva.' + 'biom') + self.assertTrue(fn in tgz_obs) + tgz_obs.remove(fn) + # now let's check prep info files based on their suffix, just take + # the first one and check/rm the occurances of that file + fn_prep = [f for f in tgz_obs + if f.startswith('templates/1_prep_1_')][0] + # 3 times + self.assertTrue(fn_prep in tgz_obs) + tgz_obs.remove(fn_prep) + self.assertTrue(fn_prep in tgz_obs) + tgz_obs.remove(fn_prep) + self.assertTrue(fn_prep in tgz_obs) + tgz_obs.remove(fn_prep) + fn_sample = [f for f in tgz_obs if f.startswith('templates/1_')][0] + # 3 times + self.assertTrue(fn_sample in tgz_obs) + tgz_obs.remove(fn_sample) + self.assertTrue(fn_sample in tgz_obs) + tgz_obs.remove(fn_sample) + self.assertTrue(fn_sample in tgz_obs) + tgz_obs.remove(fn_sample) + # now we should only have the text file + txt = tgz_obs.pop() + # now it should be empty + self.assertEqual(tgz_obs, []) + + tmp = topen(tgz, "r:gz") + fhd = tmp.extractfile(txt) + txt_obs = fhd.readlines() + tmp.close() + txt_exp = [ + 'biom_fp\tsample_fp\tprep_fp\tqiita_artifact_id\tcommand\n', + 'processed_data/1_study_1001_closed_reference_otu_table.biom\t' + '%s\t%s\t4\tPick closed-reference OTUs, Split libraries FASTQ\n' + % (fn_sample, fn_prep), + 'processed_data/1_study_1001_closed_reference_otu_table.biom\t' + '%s\t%s\t5\tPick closed-reference OTUs, Split libraries FASTQ\n' + % (fn_sample, fn_prep), + 'processed_data/1_study_1001_closed_reference_otu_table_Silva.bio' + 'm\t%s\t%s\t6\tPick closed-reference OTUs, Split libraries FASTQ\n' + % (fn_sample, fn_prep)] + self.assertEqual(txt_obs, txt_exp) + + # whatever the configuration was, we will change to settings so we can + # test the other option when dealing with the end '/' + with qdb.sql_connection.TRN: + qdb.sql_connection.TRN.add( + "SELECT base_data_dir FROM settings") + obdr = qdb.sql_connection.TRN.execute_fetchlast() + if obdr[-1] == '/': + bdr = obdr[:-1] + else: + bdr = obdr + '/' + + qdb.sql_connection.TRN.add( + "UPDATE settings SET base_data_dir = '%s'" % bdr) + bdr = qdb.sql_connection.TRN.execute() + + qdb.meta_util.generate_biom_and_metadata_release(level) + # we are storing the [0] filepath, [1] md5sum and [2] time but we are + # only going to check the filepath contents so ignoring the others + tgz = vals[0][1]('%s:release:%s:%s' % (portal, level, vals[0][0])) + tgz = join(working_dir, tgz) + + tmp = topen(tgz, "r:gz") + tgz_obs = [ti.name for ti in tmp] + tmp.close() + # files names might change due to updates and patches so just check + # that the prefix exists. + fn = 'processed_data/1_study_1001_closed_reference_otu_table.biom' + self.assertTrue(fn in tgz_obs) + tgz_obs.remove(fn) + # yes, this file is there twice + self.assertTrue(fn in tgz_obs) + tgz_obs.remove(fn) + # let's check the next biom + fn = ('processed_data/1_study_1001_closed_reference_otu_table_Silva.' + 'biom') + self.assertTrue(fn in tgz_obs) + tgz_obs.remove(fn) + # now let's check prep info files based on their suffix, just take + # the first one and check/rm the occurances of that file + fn_prep = [f for f in tgz_obs + if f.startswith('templates/1_prep_1_')][0] + # 3 times + self.assertTrue(fn_prep in tgz_obs) + tgz_obs.remove(fn_prep) + self.assertTrue(fn_prep in tgz_obs) + tgz_obs.remove(fn_prep) + self.assertTrue(fn_prep in tgz_obs) + tgz_obs.remove(fn_prep) + fn_sample = [f for f in tgz_obs if f.startswith('templates/1_')][0] + # 3 times + self.assertTrue(fn_sample in tgz_obs) + tgz_obs.remove(fn_sample) + self.assertTrue(fn_sample in tgz_obs) + tgz_obs.remove(fn_sample) + self.assertTrue(fn_sample in tgz_obs) + tgz_obs.remove(fn_sample) + # now we should only have the text file + txt = tgz_obs.pop() + # now it should be empty + self.assertEqual(tgz_obs, []) + + tmp = topen(tgz, "r:gz") + fhd = tmp.extractfile(txt) + txt_obs = fhd.readlines() + tmp.close() + txt_exp = [ + 'biom_fp\tsample_fp\tprep_fp\tqiita_artifact_id\tcommand\n', + 'processed_data/1_study_1001_closed_reference_otu_table.biom\t' + '%s\t%s\t4\tPick closed-reference OTUs, Split libraries FASTQ\n' + % (fn_sample, fn_prep), + 'processed_data/1_study_1001_closed_reference_otu_table.biom\t' + '%s\t%s\t5\tPick closed-reference OTUs, Split libraries FASTQ\n' + % (fn_sample, fn_prep), + 'processed_data/1_study_1001_closed_reference_otu_table_Silva.bio' + 'm\t%s\t%s\t6\tPick closed-reference OTUs, Split libraries FASTQ\n' + % (fn_sample, fn_prep)] + self.assertEqual(txt_obs, txt_exp) + + # returning configuration + with qdb.sql_connection.TRN: + qdb.sql_connection.TRN.add( + "UPDATE settings SET base_data_dir = '%s'" % obdr) + bdr = qdb.sql_connection.TRN.execute() + EXP_LAT_LONG = ( '[[60.1102854322, 74.7123248382], [23.1218032799, 42.838497795],' diff --git a/qiita_db/test/test_study.py b/qiita_db/test/test_study.py index 2ccf23c67..153ff9e75 100644 --- a/qiita_db/test/test_study.py +++ b/qiita_db/test/test_study.py @@ -46,6 +46,19 @@ def test_delete(self): self.assertFalse( qdb.study.StudyPerson.exists('SomeDude', 'affil')) + def test_retrieve_non_existant_people(self): + with self.assertRaises(qdb.exceptions.QiitaDBLookupError): + qdb.study.StudyPerson.from_name_and_affiliation('Boaty McBoatFace', + 'UCSD') + + p = qdb.study.StudyPerson.from_name_and_affiliation('LabDude', + 'knight lab') + self.assertEqual(p.name, 'LabDude') + self.assertEqual(p.affiliation, 'knight lab') + self.assertEqual(p.address, '123 lab street') + self.assertEqual(p.phone, '121-222-3333') + self.assertEqual(p.email, 'lab_dude@foo.bar') + def test_iter(self): """Make sure that each and every StudyPerson is retrieved""" expected = [ @@ -833,31 +846,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__": diff --git a/qiita_db/test/test_user.py b/qiita_db/test/test_user.py index 0df6ccd11..1a3433ef8 100644 --- a/qiita_db/test/test_user.py +++ b/qiita_db/test/test_user.py @@ -454,14 +454,20 @@ def test_jobs_all(self): PJ = qdb.processing_job.ProcessingJob ignore_status = [] # generates expected jobs - jobs = qdb.user.User('shared@foo.bar').jobs(ignore_status) + jobs = qdb.user.User('shared@foo.bar').jobs( + ignore_status=ignore_status) self.assertEqual(jobs, [ PJ('d19f76ee-274e-4c1b-b3a2-a12d73507c55'), PJ('b72369f9-a886-4193-8d3d-f7b504168e75')]) + # just one job + self.assertEqual(qdb.user.User('shared@foo.bar').jobs( + limit=1, ignore_status=ignore_status), [ + PJ('d19f76ee-274e-4c1b-b3a2-a12d73507c55')]) + # no jobs self.assertEqual(qdb.user.User('admin@foo.bar').jobs( - ignore_status), []) + ignore_status=ignore_status), []) def test_jobs_defaults(self): PJ = qdb.processing_job.ProcessingJob diff --git a/qiita_db/test/test_util.py b/qiita_db/test/test_util.py index 2797e5098..84df14c5e 100644 --- a/qiita_db/test/test_util.py +++ b/qiita_db/test/test_util.py @@ -14,7 +14,6 @@ from datetime import datetime from functools import partial from string import punctuation -from tarfile import open as topen import pandas as pd @@ -741,142 +740,6 @@ def test_supported_filepath_types(self): exp = [["biom", True], ["directory", False], ["log", False]] self.assertItemsEqual(obs, exp) - def test_generate_biom_and_metadata_release(self): - tgz, txt = qdb.util.generate_biom_and_metadata_release('private') - self.files_to_remove.extend([tgz, txt]) - - tmp = topen(tgz, "r:gz") - tgz_obs = [ti.name for ti in tmp] - tmp.close() - # files names might change due to updates and patches so just check - # that the prefix exists. - fn = 'processed_data/1_study_1001_closed_reference_otu_table.biom' - self.assertTrue(fn in tgz_obs) - tgz_obs.remove(fn) - # yes, this file is there twice - self.assertTrue(fn in tgz_obs) - tgz_obs.remove(fn) - # let's check the next biom - fn = ('processed_data/1_study_1001_closed_reference_otu_table_Silva.' - 'biom') - self.assertTrue(fn in tgz_obs) - tgz_obs.remove(fn) - # now let's check prep info files based on their suffix, just take - # the first one and check/rm the occurances of that file - fn_prep = [f for f in tgz_obs - if f.startswith('templates/1_prep_1_')][0] - # 3 times - self.assertTrue(fn_prep in tgz_obs) - tgz_obs.remove(fn_prep) - self.assertTrue(fn_prep in tgz_obs) - tgz_obs.remove(fn_prep) - self.assertTrue(fn_prep in tgz_obs) - tgz_obs.remove(fn_prep) - fn_sample = [f for f in tgz_obs if f.startswith('templates/1_')][0] - # 3 times - self.assertTrue(fn_sample in tgz_obs) - tgz_obs.remove(fn_sample) - self.assertTrue(fn_sample in tgz_obs) - tgz_obs.remove(fn_sample) - self.assertTrue(fn_sample in tgz_obs) - tgz_obs.remove(fn_sample) - # now it should be empty - self.assertEqual(tgz_obs, []) - - tmp = open(txt) - txt_obs = tmp.readlines() - tmp.close() - txt_exp = [ - 'biom_fp\tsample_fp\tprep_fp\tqiita_artifact_id\tcommand\n', - 'processed_data/1_study_1001_closed_reference_otu_table.biom\t' - '%s\t%s\t4\tPick closed-reference OTUs, Split libraries FASTQ\n' - % (fn_sample, fn_prep), - 'processed_data/1_study_1001_closed_reference_otu_table.biom\t' - '%s\t%s\t5\tPick closed-reference OTUs, Split libraries FASTQ\n' - % (fn_sample, fn_prep), - 'processed_data/1_study_1001_closed_reference_otu_table_Silva.bio' - 'm\t%s\t%s\t6\tPick closed-reference OTUs, Split libraries FASTQ\n' - % (fn_sample, fn_prep)] - self.assertEqual(txt_obs, txt_exp) - - # whatever the configuration was, we will change to settings so we can - # test the other option when dealing with the end '/' - with qdb.sql_connection.TRN: - qdb.sql_connection.TRN.add( - "SELECT base_data_dir FROM settings") - obdr = qdb.sql_connection.TRN.execute_fetchlast() - if obdr[-1] == '/': - bdr = obdr[:-1] - else: - bdr = obdr + '/' - - qdb.sql_connection.TRN.add( - "UPDATE settings SET base_data_dir = '%s'" % bdr) - bdr = qdb.sql_connection.TRN.execute() - - tgz, txt = qdb.util.generate_biom_and_metadata_release('private') - self.files_to_remove.extend([tgz, txt]) - - tmp = topen(tgz, "r:gz") - tgz_obs = [ti.name for ti in tmp] - tmp.close() - # files names might change due to updates and patches so just check - # that the prefix exists. - fn = 'processed_data/1_study_1001_closed_reference_otu_table.biom' - self.assertTrue(fn in tgz_obs) - tgz_obs.remove(fn) - # yes, this file is there twice - self.assertTrue(fn in tgz_obs) - tgz_obs.remove(fn) - # let's check the next biom - fn = ('processed_data/1_study_1001_closed_reference_otu_table_Silva.' - 'biom') - self.assertTrue(fn in tgz_obs) - tgz_obs.remove(fn) - # now let's check prep info files based on their suffix, just take - # the first one and check/rm the occurances of that file - fn_prep = [f for f in tgz_obs - if f.startswith('templates/1_prep_1_')][0] - # 3 times - self.assertTrue(fn_prep in tgz_obs) - tgz_obs.remove(fn_prep) - self.assertTrue(fn_prep in tgz_obs) - tgz_obs.remove(fn_prep) - self.assertTrue(fn_prep in tgz_obs) - tgz_obs.remove(fn_prep) - fn_sample = [f for f in tgz_obs if f.startswith('templates/1_')][0] - # 3 times - self.assertTrue(fn_sample in tgz_obs) - tgz_obs.remove(fn_sample) - self.assertTrue(fn_sample in tgz_obs) - tgz_obs.remove(fn_sample) - self.assertTrue(fn_sample in tgz_obs) - tgz_obs.remove(fn_sample) - # now it should be empty - self.assertEqual(tgz_obs, []) - - tmp = open(txt) - txt_obs = tmp.readlines() - tmp.close() - txt_exp = [ - 'biom_fp\tsample_fp\tprep_fp\tqiita_artifact_id\tcommand\n', - 'processed_data/1_study_1001_closed_reference_otu_table.biom\t' - '%s\t%s\t4\tPick closed-reference OTUs, Split libraries FASTQ\n' - % (fn_sample, fn_prep), - 'processed_data/1_study_1001_closed_reference_otu_table.biom\t' - '%s\t%s\t5\tPick closed-reference OTUs, Split libraries FASTQ\n' - % (fn_sample, fn_prep), - 'processed_data/1_study_1001_closed_reference_otu_table_Silva.bio' - 'm\t%s\t%s\t6\tPick closed-reference OTUs, Split libraries FASTQ\n' - % (fn_sample, fn_prep)] - self.assertEqual(txt_obs, txt_exp) - - # returning configuration - with qdb.sql_connection.TRN: - qdb.sql_connection.TRN.add( - "UPDATE settings SET base_data_dir = '%s'" % obdr) - bdr = qdb.sql_connection.TRN.execute() - @qiita_test_checker() class UtilTests(TestCase): @@ -934,6 +797,20 @@ def test_get_pubmed_ids_from_dois(self): self.assertEqual(obs, exp) def test_generate_study_list(self): + # creating a new study to make sure that empty studies are also + # returned + info = {"timeseries_type_id": 1, "metadata_complete": True, + "mixs_compliant": True, "number_samples_collected": 25, + "number_samples_promised": 28, "study_alias": "TST", + "study_description": "Some description of the study goes here", + "study_abstract": "Some abstract goes here", + "emp_person_id": qdb.study.StudyPerson(1), + "principal_investigator_id": qdb.study.StudyPerson(1), + "lab_person_id": qdb.study.StudyPerson(1)} + new_study = qdb.study.Study.create( + qdb.user.User('shared@foo.bar'), 'test_study_1', efo=[1], + info=info) + exp_info = [{ 'metadata_complete': True, 'ebi_submission_status': 'submitted', @@ -958,97 +835,72 @@ def test_generate_study_list(self): 'ebi_study_accession': 'EBI123456-BB', 'study_title': ('Identification of the Microbiomes for Cannabis ' 'Soils'), - 'number_samples_collected': 27 - }] - obs_info = qdb.util.generate_study_list([1, 2, 3, 4], False) + 'number_samples_collected': 27, + 'study_tags': None + }, { + 'metadata_complete': True, + 'ebi_submission_status': 'not submitted', 'publication_pid': [], + 'study_abstract': 'Some abstract goes here', + 'pi': ('lab_dude@foo.bar', 'LabDude'), 'status': 'sandbox', + 'proc_data_info': [], 'study_tags': None, 'shared': [], + 'publication_doi': [], 'study_id': new_study.id, + 'ebi_study_accession': None, 'study_title': 'test_study_1', + 'number_samples_collected': 0}] + obs_info = qdb.util.generate_study_list([1, 2, 3, 4], True) self.assertEqual(obs_info, exp_info) qdb.artifact.Artifact(4).visibility = 'public' exp_info[0]['status'] = 'public' - exp_info[0]['proc_data_info'] = [{ - 'data_type': '18S', - 'algorithm': 'QIIME (Pick closed-reference OTUs)', - 'pid': 4, - 'processed_date': '2012-10-02 17:30:00', - 'params': { - 'similarity': 0.97, - 'reference_name': 'Greengenes', - 'sortmerna_e_value': 1, - 'sortmerna_max_pos': 10000, - 'threads': 1, - 'sortmerna_coverage': 0.97, - 'reference_version': '13_8'}, - 'samples': ['1.SKB1.640202', '1.SKB2.640194', '1.SKB3.640195', - '1.SKB4.640189', '1.SKB5.640181', '1.SKB6.640176', - '1.SKB7.640196', '1.SKB8.640193', '1.SKB9.640200', - '1.SKD1.640179', '1.SKD2.640178', '1.SKD3.640198', - '1.SKD4.640185', '1.SKD5.640186', '1.SKD6.640190', - '1.SKD7.640191', '1.SKD8.640184', '1.SKD9.640182', - '1.SKM1.640183', '1.SKM2.640199', '1.SKM3.640197', - '1.SKM4.640180', '1.SKM5.640177', '1.SKM6.640187', - '1.SKM7.640188', '1.SKM8.640201', '1.SKM9.640192']}] - obs_info = qdb.util.generate_study_list([1, 2, 3, 4], True, - public_only=True) + exp_info[0]['proc_data_info'] = [ + {'data_type': '18S', 'name': 'BIOM', + 'algorithm': 'QIIME v1.9.1 (Pick closed-reference OTUs)', + 'pid': 4, 'processed_date': '2012-10-02 17:30:00', + 'params': {'similarity': 0.97, 'reference_name': 'Greengenes', + 'sortmerna_e_value': 1, u'sortmerna_max_pos': 10000, + 'threads': 1, u'sortmerna_coverage': 0.97, + 'reference_version': '13_8'}}, + {'data_type': '18S', 'name': 'BIOM', + 'algorithm': 'QIIME v1.9.1 (Pick closed-reference OTUs)', + 'pid': 5, 'processed_date': '2012-10-02 17:30:00', + 'params': {'similarity': 0.97, 'reference_name': 'Greengenes', + 'sortmerna_e_value': 1, u'sortmerna_max_pos': 10000, + 'threads': 1, 'sortmerna_coverage': 0.97, + 'reference_version': '13_8'}}, + {'data_type': '16S', 'name': 'BIOM', + 'algorithm': 'QIIME v1.9.1 (Pick closed-reference OTUs)', + 'pid': 6, 'processed_date': '2012-10-02 17:30:00', + 'params': {'similarity': 0.97, 'reference_name': 'Silva', + 'sortmerna_e_value': 1, u'sortmerna_max_pos': 10000, + 'threads': 1, 'sortmerna_coverage': 0.97, + 'reference_version': 'test'}}] + obs_info = qdb.util.generate_study_list([1, 2, 3, 4], True) self.assertEqual(obs_info, exp_info) - exp_info[0]['proc_data_info'].extend( - [{ - 'data_type': '18S', - 'algorithm': 'QIIME (Pick closed-reference OTUs)', - 'pid': 5, - 'processed_date': '2012-10-02 17:30:00', - 'params': { - 'similarity': 0.97, - 'reference_name': 'Greengenes', - 'sortmerna_e_value': 1, - 'sortmerna_max_pos': 10000, - 'threads': 1, - 'sortmerna_coverage': 0.97, - 'reference_version': '13_8'}, - 'samples': ['1.SKB1.640202', '1.SKB2.640194', '1.SKB3.640195', - '1.SKB4.640189', '1.SKB5.640181', '1.SKB6.640176', - '1.SKB7.640196', '1.SKB8.640193', '1.SKB9.640200', - '1.SKD1.640179', '1.SKD2.640178', '1.SKD3.640198', - '1.SKD4.640185', '1.SKD5.640186', '1.SKD6.640190', - '1.SKD7.640191', '1.SKD8.640184', '1.SKD9.640182', - '1.SKM1.640183', '1.SKM2.640199', '1.SKM3.640197', - '1.SKM4.640180', '1.SKM5.640177', '1.SKM6.640187', - '1.SKM7.640188', '1.SKM8.640201', '1.SKM9.640192']}, { - 'data_type': '16S', - 'algorithm': 'QIIME (Pick closed-reference OTUs)', - 'pid': 6, - 'processed_date': '2012-10-02 17:30:00', - 'params': { - 'similarity': 0.97, - 'reference_name': 'Silva', - 'sortmerna_e_value': 1, - 'sortmerna_max_pos': 10000, - 'threads': 1, - 'sortmerna_coverage': 0.97, - 'reference_version': 'test'}, - 'samples': ['1.SKB1.640202', '1.SKB2.640194', '1.SKB3.640195', - '1.SKB4.640189', '1.SKB5.640181', '1.SKB6.640176', - '1.SKB7.640196', '1.SKB8.640193', '1.SKB9.640200', - '1.SKD1.640179', '1.SKD2.640178', '1.SKD3.640198', - '1.SKD4.640185', '1.SKD5.640186', '1.SKD6.640190', - '1.SKD7.640191', '1.SKD8.640184', '1.SKD9.640182', - '1.SKM1.640183', '1.SKM2.640199', '1.SKM3.640197', - '1.SKM4.640180', '1.SKM5.640177', '1.SKM6.640187', - '1.SKM7.640188', '1.SKM8.640201', '1.SKM9.640192']}, { - 'processed_date': '2012-10-02 17:30:00', - 'pid': 7, - 'samples': ['1.SKB1.640202', '1.SKB2.640194', '1.SKB3.640195', - '1.SKB4.640189', '1.SKB5.640181', '1.SKB6.640176', - '1.SKB7.640196', '1.SKB8.640193', '1.SKB9.640200', - '1.SKD1.640179', '1.SKD2.640178', '1.SKD3.640198', - '1.SKD4.640185', '1.SKD5.640186', '1.SKD6.640190', - '1.SKD7.640191', '1.SKD8.640184', '1.SKD9.640182', - '1.SKM1.640183', '1.SKM2.640199', '1.SKM3.640197', - '1.SKM4.640180', '1.SKM5.640177', '1.SKM6.640187', - '1.SKM7.640188', '1.SKM8.640201', '1.SKM9.640192'], - 'data_type': '16S'}]) - - obs_info = qdb.util.generate_study_list([1, 2, 3, 4], True) + exp_info[0]['proc_data_info'] = [ + {'data_type': '18S', 'name': 'BIOM', + 'algorithm': 'QIIME v1.9.1 (Pick closed-reference OTUs)', + 'pid': 4, 'processed_date': '2012-10-02 17:30:00', + 'params': {'similarity': 0.97, 'reference_name': 'Greengenes', + 'sortmerna_e_value': 1, u'sortmerna_max_pos': 10000, + 'threads': 1, 'sortmerna_coverage': 0.97, + 'reference_version': '13_8'}}, + {'data_type': '18S', 'name': 'BIOM', + 'algorithm': 'QIIME v1.9.1 (Pick closed-reference OTUs)', + 'pid': 5, 'processed_date': '2012-10-02 17:30:00', + 'params': {'similarity': 0.97, 'reference_name': 'Greengenes', + 'sortmerna_e_value': 1, u'sortmerna_max_pos': 10000, + 'threads': 1, 'sortmerna_coverage': 0.97, + 'reference_version': '13_8'}}, + {'data_type': '16S', 'name': 'BIOM', + 'algorithm': 'QIIME v1.9.1 (Pick closed-reference OTUs)', + 'pid': 6, 'processed_date': '2012-10-02 17:30:00', + 'params': {'similarity': 0.97, 'reference_name': 'Silva', + 'sortmerna_e_value': 1, u'sortmerna_max_pos': 10000, + 'threads': 1, 'sortmerna_coverage': 0.97, + 'reference_version': 'test'}}, + {'processed_date': '2012-10-02 17:30:00', 'pid': 7, 'name': 'BIOM', + 'data_type': '16S'}] + obs_info = qdb.util.generate_study_list([1, 2, 3, 4], False) self.assertEqual(obs_info, exp_info) diff --git a/qiita_db/user.py b/qiita_db/user.py index 46b29ece8..4564c0da8 100644 --- a/qiita_db/user.py +++ b/qiita_db/user.py @@ -660,12 +660,14 @@ def delete_messages(self, messages): qdb.sql_connection.TRN.add(sql) qdb.sql_connection.TRN.execute() - def jobs(self, ignore_status=['success']): + def jobs(self, limit=30, ignore_status=['success']): """Return jobs created by the user Parameters ---------- - ignore_status, list of str + limit : int, optional + max number of rows to return + ignore_status: list of str, optional don't retieve jobs that have one of these status Returns @@ -682,10 +684,10 @@ def jobs(self, ignore_status=['success']): """ if ignore_status: - sql_info = [self._id, tuple(ignore_status)] + sql_info = [self._id, tuple(ignore_status), limit] sql += " AND processing_job_status NOT IN %s" else: - sql_info = [self._id] + sql_info = [self._id, limit] sql += """ ORDER BY CASE processing_job_status @@ -695,7 +697,7 @@ def jobs(self, ignore_status=['success']): WHEN 'waiting' THEN 4 WHEN 'error' THEN 5 WHEN 'success' THEN 6 - END, heartbeat DESC""" + END, heartbeat DESC LIMIT %s""" qdb.sql_connection.TRN.add(sql, sql_info) return [qdb.processing_job.ProcessingJob(p[0]) diff --git a/qiita_db/util.py b/qiita_db/util.py index 59ee743b9..a6513aca6 100644 --- a/qiita_db/util.py +++ b/qiita_db/util.py @@ -54,10 +54,8 @@ from json import dumps from datetime import datetime from itertools import chain -from tarfile import open as topen from qiita_core.exceptions import IncompetentQiitaDeveloperError -from qiita_core.configuration_manager import ConfigurationManager import qiita_db as qdb @@ -896,12 +894,12 @@ def filepath_id_to_rel_path(filepath_id): LEFT JOIN qiita.artifact_filepath USING (filepath_id) WHERE filepath_id = %s""" qdb.sql_connection.TRN.add(sql, [filepath_id]) + # It should be only one row mp, fp, sd, a_id = qdb.sql_connection.TRN.execute_fetchindex()[0] if sd: result = join(mp, str(a_id), fp) else: result = join(mp, fp) - # It should be only one row return result @@ -1225,16 +1223,13 @@ def supported_filepath_types(artifact_type): return qdb.sql_connection.TRN.execute_fetchindex() -def generate_study_list(study_ids, build_samples, public_only=False): +def generate_study_list(study_ids, public_only=False): """Get general study information Parameters ---------- study_ids : list of ints The study ids to look for. Non-existing ids will be ignored - build_samples : bool - If true the sample information for each process artifact within each - study will be included public_only : bool, optional If true, return only public BIOM artifacts. Default: false. @@ -1293,6 +1288,13 @@ def generate_study_list(study_ids, build_samples, public_only=False): LEFT JOIN qiita.artifact_type USING (artifact_type_id) WHERE artifact_type='BIOM' AND study_id = qiita.study.study_id) AS artifact_biom_ts, + - all the BIOM names sorted by artifact_id that belong to the study + (SELECT array_agg(name ORDER BY artifact_id) + FROM qiita.study_artifact + LEFT JOIN qiita.artifact USING (artifact_id) + LEFT JOIN qiita.artifact_type USING (artifact_type_id) + WHERE artifact_type='BIOM' AND + study_id = qiita.study.study_id) AS artifact_biom_name, - all the BIOM visibility sorted by artifact_id that belong to the study (SELECT array_agg(visibility ORDER BY artifact_id) FROM qiita.study_artifact @@ -1321,6 +1323,9 @@ def generate_study_list(study_ids, build_samples, public_only=False): (SELECT array_agg(email ORDER BY email) FROM qiita.study_users LEFT JOIN qiita.qiita_user USING (email) WHERE study_id=qiita.study.study_id) AS shared_with_email + - all study tags + (SELECT array_agg(study_tag) FROM qiita.per_study_tags + WHERE study_id=qiita.study.study_id) AS study_tags """ with qdb.sql_connection.TRN: sql = """ @@ -1364,6 +1369,12 @@ def generate_study_list(study_ids, build_samples, public_only=False): LEFT JOIN qiita.artifact_type USING (artifact_type_id) WHERE artifact_type='BIOM' AND study_id = qiita.study.study_id) AS artifact_biom_ts, + (SELECT array_agg(name ORDER BY artifact_id) + FROM qiita.study_artifact + LEFT JOIN qiita.artifact USING (artifact_id) + LEFT JOIN qiita.artifact_type USING (artifact_type_id) + WHERE artifact_type='BIOM' AND + study_id = qiita.study.study_id) AS artifact_biom_name, (SELECT array_agg(visibility ORDER BY artifact_id) FROM qiita.study_artifact LEFT JOIN qiita.artifact USING (artifact_id) @@ -1386,7 +1397,9 @@ def generate_study_list(study_ids, build_samples, public_only=False): WHERE study_id=qiita.study.study_id) AS shared_with_name, (SELECT array_agg(email ORDER BY email) FROM qiita.study_users LEFT JOIN qiita.qiita_user USING (email) - WHERE study_id=qiita.study.study_id) AS shared_with_email + WHERE study_id=qiita.study.study_id) AS shared_with_email, + (SELECT array_agg(study_tag) FROM qiita.per_study_tags + WHERE study_id=qiita.study.study_id) AS study_tags FROM qiita.study LEFT JOIN qiita.study_person ON ( study_person_id=principal_investigator_id) @@ -1435,17 +1448,19 @@ def generate_study_list(study_ids, build_samples, public_only=False): del info["shared_with_email"] info['proc_data_info'] = [] - if build_samples and info['artifact_biom_ids']: + if info['artifact_biom_ids']: to_loop = zip( info['artifact_biom_ids'], info['artifact_biom_dts'], info['artifact_biom_ts'], info['artifact_biom_params'], - info['artifact_biom_cmd'], info['artifact_biom_vis']) - for artifact_id, dt, ts, params, cmd, vis in to_loop: + info['artifact_biom_cmd'], info['artifact_biom_vis'], + info['artifact_biom_name']) + for artifact_id, dt, ts, params, cmd, vis, name in to_loop: if public_only and vis != 'public': continue proc_info = {'processed_date': str(ts)} proc_info['pid'] = artifact_id proc_info['data_type'] = dt + proc_info['name'] = name # if cmd exists then we can get its parameters if cmd is not None: @@ -1457,6 +1472,7 @@ def generate_study_list(study_ids, build_samples, public_only=False): 'del_keys': [k for k, v in viewitems( c.parameters) if v[0] == 'artifact'], 'sfwn': c.software.name, + 'sfv': c.software.version, 'cmdn': c.name } for k in commands[cmd]['del_keys']: @@ -1480,110 +1496,27 @@ def generate_study_list(study_ids, build_samples, public_only=False): params['reference_version'] = refs[rid][ 'version'] - proc_info['algorithm'] = '%s (%s)' % ( - commands[cmd]['sfwn'], commands[cmd]['cmdn']) + proc_info['algorithm'] = '%s v%s (%s)' % ( + commands[cmd]['sfwn'], commands[cmd]['sfv'], + commands[cmd]['cmdn']) proc_info['params'] = params - # getting all samples - sql = """SELECT sample_id from qiita.prep_template_sample - WHERE prep_template_id = ( - SELECT prep_template_id - FROM qiita.prep_template - WHERE artifact_id IN ( - SELECT * - FROM qiita.find_artifact_roots(%s)))""" - qdb.sql_connection.TRN.add(sql, [proc_info['pid']]) - proc_info['samples'] = sorted( - qdb.sql_connection.TRN.execute_fetchflatten()) - info["proc_data_info"].append(proc_info) - del info["artifact_biom_ids"] - del info["artifact_biom_dts"] - del info["artifact_biom_ts"] - del info["artifact_biom_params"] - del info['artifact_biom_cmd'] - del info['artifact_biom_vis'] - - infolist.append(info) + infolist.append({ + 'metadata_complete': info['metadata_complete'], + 'publication_pid': info['publication_pid'], + 'ebi_submission_status': info['ebi_submission_status'], + 'shared': info['shared'], + 'study_abstract': info['study_abstract'], 'pi': info['pi'], + 'status': info['status'], + 'proc_data_info': info['proc_data_info'], + 'study_tags': info['study_tags'], + 'publication_doi': info['publication_doi'], + 'study_id': info['study_id'], + 'ebi_study_accession': info['ebi_study_accession'], + 'study_title': info['study_title'], + 'number_samples_collected': info['number_samples_collected'] + }) return infolist - - -def generate_biom_and_metadata_release(study_status='public'): - """Generate a list of biom/meatadata filepaths and a tgz of those files - - Parameters - ---------- - study_status : str, optional - The study status to search for. Note that this should always be set - to 'public' but having this exposed as helps with testing. The other - options are 'private' and 'sandbox' - - Returns - ------- - str, str - tgz_name: the filepath of the new generated tgz - txt_name: the filepath of the new generated txt - """ - studies = qdb.study.Study.get_by_status(study_status) - qiita_config = ConfigurationManager() - working_dir = qiita_config.working_dir - portal = qiita_config.portal - bdir = qdb.util.get_db_files_base_dir() - - data = [] - for s in studies: - # [0] latest is first, [1] only getting the filepath - sample_fp = relpath(s.sample_template.get_filepaths()[0][1], bdir) - - for a in s.artifacts(artifact_type='BIOM'): - if a.processing_parameters is None: - continue - - cmd_name = a.processing_parameters.command.name - - # this loop is necessary as in theory an artifact can be - # generated from multiple prep info files - human_cmd = [] - for p in a.parents: - pp = p.processing_parameters - pp_cmd_name = pp.command.name - if pp_cmd_name == 'Trimming': - human_cmd.append('%s @ %s' % ( - cmd_name, str(pp.values['length']))) - else: - human_cmd.append('%s, %s' % (cmd_name, pp_cmd_name)) - human_cmd = ', '.join(human_cmd) - - for _, fp, fp_type in a.filepaths: - if fp_type != 'biom' or 'only-16s' in fp: - continue - fp = relpath(fp, bdir) - # format: (biom_fp, sample_fp, prep_fp, qiita_artifact_id, - # human readable name) - for pt in a.prep_templates: - for _, prep_fp in pt.get_filepaths(): - if 'qiime' not in prep_fp: - break - prep_fp = relpath(prep_fp, bdir) - data.append((fp, sample_fp, prep_fp, a.id, human_cmd)) - - # writing text and tgz file - ts = datetime.now().strftime('%m%d%y-%H%M%S') - tgz_dir = join(working_dir, 'releases') - if not exists(tgz_dir): - makedirs(tgz_dir) - tgz_name = join(tgz_dir, '%s-%s-%s.tgz' % (portal, study_status, ts)) - txt_name = join(tgz_dir, '%s-%s-%s.txt' % (portal, study_status, ts)) - with open(txt_name, 'w') as txt, topen(tgz_name, "w|gz") as tgz: - # writing header for txt - txt.write("biom_fp\tsample_fp\tprep_fp\tqiita_artifact_id\tcommand\n") - for biom_fp, sample_fp, prep_fp, artifact_id, human_cmd in data: - txt.write("%s\t%s\t%s\t%s\t%s\n" % ( - biom_fp, sample_fp, prep_fp, artifact_id, human_cmd)) - tgz.add(join(bdir, biom_fp), arcname=biom_fp, recursive=False) - tgz.add(join(bdir, sample_fp), arcname=sample_fp, recursive=False) - tgz.add(join(bdir, prep_fp), arcname=prep_fp, recursive=False) - - return tgz_name, txt_name diff --git a/qiita_pet/handlers/api_proxy/__init__.py b/qiita_pet/handlers/api_proxy/__init__.py index c7288edc8..68a6956e1 100644 --- a/qiita_pet/handlers/api_proxy/__init__.py +++ b/qiita_pet/handlers/api_proxy/__init__.py @@ -26,11 +26,12 @@ 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, - artifact_summary_get_request, + artifact_summary_get_request, artifact_get_prep_req, artifact_summary_post_request, artifact_patch_request) from .ontology import ontology_patch_handler from .processing import ( @@ -47,7 +48,7 @@ 'study_get_req', 'sample_template_summary_get_req', 'sample_template_delete_req', 'sample_template_filepaths_get_req', 'prep_template_summary_get_req', 'prep_template_post_req', - 'prep_template_delete_req', + 'prep_template_delete_req', 'artifact_get_prep_req', 'prep_template_graph_get_req', 'prep_template_filepaths_get_req', 'artifact_get_req', 'artifact_status_put_req', 'artifact_delete_req', 'prep_template_get_req', 'study_delete_req', @@ -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', diff --git a/qiita_pet/handlers/api_proxy/artifact.py b/qiita_pet/handlers/api_proxy/artifact.py index 6034f41b3..d6e04e6b3 100644 --- a/qiita_pet/handlers/api_proxy/artifact.py +++ b/qiita_pet/handlers/api_proxy/artifact.py @@ -11,6 +11,7 @@ from future.utils import viewitems from moi import r_client +from skbio.util import flatten from qiita_core.util import execute_as_transaction from qiita_core.qiita_settings import qiita_config @@ -24,7 +25,6 @@ from qiita_db.software import Command, Parameters from qiita_db.processing_job import ProcessingJob - PREP_TEMPLATE_KEY_FORMAT = 'prep_template_%s' @@ -263,6 +263,39 @@ def artifact_get_req(user_id, artifact_id): 'is_submitted_vamps': is_submitted_vamps} +@execute_as_transaction +def artifact_get_prep_req(user_id, artifact_ids): + """Returns all prep info sample ids for the given artifact_ids + + Parameters + ---------- + user_id : str + user making the request + artifact_ids : list of int + list of artifact ids + + Returns + ------- + dict of objects + A dictionary containing the artifact information + {'status': status, + 'message': message, + 'data': {artifact_id: [prep info sample ids]} + """ + samples = {} + + for aid in artifact_ids: + artifact = Artifact(aid) + access_error = check_access(artifact.study.id, user_id) + if access_error: + return access_error + + samples[aid] = flatten( + [pt.keys() for pt in Artifact(aid).prep_templates]) + + return {'status': 'success', 'msg': '', 'data': samples} + + @execute_as_transaction def artifact_post_req(user_id, filepaths, artifact_type, name, prep_template_id, artifact_id=None): diff --git a/qiita_pet/handlers/api_proxy/studies.py b/qiita_pet/handlers/api_proxy/studies.py index 98b4bdebf..3dded0cf2 100644 --- a/qiita_pet/handlers/api_proxy/studies.py +++ b/qiita_pet/handlers/api_proxy/studies.py @@ -100,6 +100,15 @@ def study_get_req(study_id, user_id): samples = study.sample_template study_info['num_samples'] = 0 if samples is None else len(list(samples)) study_info['owner'] = study.owner.id + # Study.has_access no_public=True, will return True only if the user_id is + # the owner of the study or if the study is shared with the user_id + study_info['has_access_to_raw_data'] = study.has_access( + User(user_id), True) + + study_info['show_biom_download_button'] = 'BIOM' in [ + a.artifact_type for a in study.artifacts()] + study_info['show_raw_download_button'] = any([ + True for pt in study.prep_templates() if pt.artifact is not None]) return {'status': 'success', 'message': '', @@ -296,3 +305,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} diff --git a/qiita_pet/handlers/api_proxy/tests/test_artifact.py b/qiita_pet/handlers/api_proxy/tests/test_artifact.py index fa819c96b..77a9dd09e 100644 --- a/qiita_pet/handlers/api_proxy/tests/test_artifact.py +++ b/qiita_pet/handlers/api_proxy/tests/test_artifact.py @@ -29,7 +29,7 @@ artifact_get_req, artifact_status_put_req, artifact_graph_get_req, artifact_delete_req, artifact_types_get_req, artifact_post_req, artifact_summary_get_request, artifact_summary_post_request, - artifact_patch_request) + artifact_patch_request, artifact_get_prep_req) class TestArtifactAPIReadOnly(TestCase): @@ -302,7 +302,7 @@ def test_artifact_summary_get_request(self): 'files': exp_files, 'errored_jobs': [], 'editable': True, - 'visibility': 'private', + 'visibility': 'sandbox', 'job': None, 'message': '', 'name': 'Demultiplexed 1', @@ -316,17 +316,14 @@ def test_artifact_summary_get_request(self): 'min_per_read_length_fraction': 0.75, 'barcode_type': u'golay_12'}, 'summary': None, - 'buttons': (' Submit to VAMPS'), + 'buttons': ( + ' ' + ' Submit to ' + 'VAMPS'), 'study_id': 1, 'prep_id': 1} self.assertEqual(obs, exp) @@ -408,6 +405,25 @@ def test_artifact_delete_req_no_access(self): 'message': 'User does not have access to study'} self.assertEqual(obs, exp) + def test_artifact_get_prep_req(self): + obs = artifact_get_prep_req('test@foo.bar', [4]) + exp = {'status': 'success', 'msg': '', 'data': { + 4: ['1.SKB2.640194', '1.SKM4.640180', '1.SKB3.640195', + '1.SKB6.640176', '1.SKD6.640190', '1.SKM6.640187', + '1.SKD9.640182', '1.SKM8.640201', '1.SKM2.640199', + '1.SKD2.640178', '1.SKB7.640196', '1.SKD4.640185', + '1.SKB8.640193', '1.SKM3.640197', '1.SKD5.640186', + '1.SKB1.640202', '1.SKM1.640183', '1.SKD1.640179', + '1.SKD3.640198', '1.SKB5.640181', '1.SKB4.640189', + '1.SKB9.640200', '1.SKM9.640192', '1.SKD8.640184', + '1.SKM5.640177', '1.SKM7.640188', '1.SKD7.640191']}} + self.assertEqual(obs, exp) + + obs = artifact_get_prep_req('demo@microbio.me', [4]) + exp = {'status': 'error', + 'message': 'User does not have access to study'} + self.assertEqual(obs, exp) + def test_artifact_post_req(self): # Create new prep template to attach artifact to pt = npt.assert_warns( diff --git a/qiita_pet/handlers/api_proxy/tests/test_prep_template.py b/qiita_pet/handlers/api_proxy/tests/test_prep_template.py index fa43db045..48133cdea 100644 --- a/qiita_pet/handlers/api_proxy/tests/test_prep_template.py +++ b/qiita_pet/handlers/api_proxy/tests/test_prep_template.py @@ -332,7 +332,7 @@ def test_prep_template_graph_get_req(self): obs = prep_template_graph_get_req(1, 'demo@microbio.me') self.assertEqual(obs['message'], '') self.assertEqual(obs['status'], 'success') - self.assertEqual(8, len(obs['node_labels'])) + self.assertEqual(11, len(obs['node_labels'])) self.assertIn(('artifact', 1, 'Raw data 1 - FASTQ'), obs['node_labels']) self.assertIn(('artifact', 2, 'Demultiplexed 1 - Demultiplexed'), diff --git a/qiita_pet/handlers/api_proxy/tests/test_studies.py b/qiita_pet/handlers/api_proxy/tests/test_studies.py index e700d37f3..9bea17b96 100644 --- a/qiita_pet/handlers/api_proxy/tests/test_studies.py +++ b/qiita_pet/handlers/api_proxy/tests/test_studies.py @@ -20,6 +20,7 @@ import qiita_db as qdb from qiita_pet.handlers.api_proxy.studies import ( data_types_get_req, study_get_req, study_prep_get_req, study_delete_req, + study_tags_request, study_get_tags_request, study_tags_patch_request, study_files_get_req) @@ -93,9 +94,11 @@ def test_study_get_req(self): 'number_samples_collected': 27, 'owner': 'test@foo.bar', 'ebi_submission_status': 'submitted', + 'has_access_to_raw_data': True, + 'show_biom_download_button': True, + 'show_raw_download_button': True, 'ebi_study_accession': 'EBI123456-BB'}, 'editable': True} - self.assertEqual(obs, exp) # Test with no lab person @@ -527,6 +530,57 @@ def test_study_files_get_req_multiple(self): PREP.delete(pt.id) + def test_study_get_tags_request(self): + obs = study_get_tags_request('shared@foo.bar', 1) + exp = {'status': 'success', 'message': '', 'tags': []} + self.assertEqual(obs, exp) + + # check error + obs = study_get_tags_request('shared@foo.bar', 2) + exp = {'message': 'Study does not exist', 'status': 'error'} + self.assertEqual(obs, exp) + + def test_study_tags_patch_request(self): + # adding test for study_tags_request here as it makes sense to check + # that the tags were added + obs = study_tags_request() + exp = {'status': 'success', 'message': '', + 'tags': {'admin': [], 'user': []}} + self.assertEqual(obs, exp) + + obs = study_tags_patch_request( + 'shared@foo.bar', 1, 'replace', '/tags', ['testA', 'testB']) + exp = {'status': 'success', 'message': ''} + self.assertEqual(obs, exp) + + obs = study_tags_request() + exp = {'status': 'success', 'message': '', + 'tags': {'admin': [], 'user': ['testA', 'testB']}} + self.assertEqual(obs, exp) + + # check errors + obs = study_tags_patch_request( + 'shared@foo.bar', 1, 'no-exists', '/tags', ['testA', 'testB']) + exp = {'message': ('Operation "no-exists" not supported. Current ' + 'supported operations: replace'), 'status': 'error'} + self.assertEqual(obs, exp) + + obs = study_tags_patch_request( + 'shared@foo.bar', 1, 'replace', '/tags/na', ['testA', 'testB']) + exp = {'message': 'Incorrect path parameter', 'status': 'error'} + self.assertEqual(obs, exp) + + obs = study_tags_patch_request( + 'shared@foo.bar', 2, 'replace', '/tags', ['testA', 'testB']) + exp = {'message': 'Study does not exist', 'status': 'error'} + self.assertEqual(obs, exp) + + obs = study_tags_patch_request( + 'shared@foo.bar', 1, 'replace', '/na') + exp = {'message': ('Attribute "na" not found. Please, check the ' + 'path parameter'), 'status': 'error'} + self.assertEqual(obs, exp) + if __name__ == '__main__': main() diff --git a/qiita_pet/handlers/api_proxy/user.py b/qiita_pet/handlers/api_proxy/user.py index ab5a1fd80..aabf482d5 100644 --- a/qiita_pet/handlers/api_proxy/user.py +++ b/qiita_pet/handlers/api_proxy/user.py @@ -26,11 +26,11 @@ def user_jobs_get_req(user, limit=30): dict of objects {'status': status, 'message': message, - 'template': {{column: value, ...}, ...} + 'jobs': {{column: value, ...}, ...} """ response = [] - for i, j in enumerate(user.jobs()): + for i, j in enumerate(user.jobs(limit=limit)): name = j.command.name hb = j.heartbeat hb = "" if hb is None else hb.strftime("%Y-%m-%d %H:%M:%S") diff --git a/qiita_pet/handlers/download.py b/qiita_pet/handlers/download.py index bbf10699f..2b797bfdb 100644 --- a/qiita_pet/handlers/download.py +++ b/qiita_pet/handlers/download.py @@ -1,12 +1,15 @@ -from tornado.web import authenticated +from tornado.web import authenticated, HTTPError -from os.path import basename +from os.path import basename, getsize, join +from os import walk +from datetime import datetime from .base_handlers import BaseHandler -from qiita_pet.exceptions import QiitaPetAuthorizationError -from qiita_db.util import filepath_id_to_rel_path +from qiita_pet.handlers.api_proxy import study_get_req +from qiita_db.study import Study +from qiita_db.util import filepath_id_to_rel_path, get_db_files_base_dir from qiita_db.meta_util import validate_filepath_access_by_user -from qiita_core.util import execute_as_transaction +from qiita_core.util import execute_as_transaction, get_release_info class DownloadHandler(BaseHandler): @@ -16,8 +19,9 @@ def get(self, filepath_id): fid = int(filepath_id) if not validate_filepath_access_by_user(self.current_user, fid): - raise QiitaPetAuthorizationError( - self.current_user, 'filepath id %s' % str(fid)) + raise HTTPError( + 403, "%s doesn't have access to " + "filepath_id: %s" % (self.current_user.email, str(fid))) relpath = filepath_id_to_rel_path(fid) fname = basename(relpath) @@ -37,3 +41,176 @@ def get(self, filepath_id): 'attachment; filename=%s' % fname) self.finish() + + +class DownloadStudyBIOMSHandler(BaseHandler): + @authenticated + @execute_as_transaction + def get(self, study_id): + study_id = int(study_id) + # Check access to study + study_info = study_get_req(study_id, self.current_user.id) + + if study_info['status'] != 'success': + raise HTTPError(405, "%s: %s, %s" % (study_info['message'], + self.current_user.email, + str(study_id))) + + study = Study(study_id) + basedir = get_db_files_base_dir() + basedir_len = len(basedir) + 1 + # loop over artifacts and retrieve those that we have access to + to_download = [] + for a in study.artifacts(): + if a.artifact_type == 'BIOM': + for i, (fid, path, data_type) in enumerate(a.filepaths): + # ignore if tgz as they could create problems and the + # raw data is in the folder + if data_type == 'tgz': + continue + if data_type == 'directory': + # If we have a directory, we actually need to list + # all the files from the directory so NGINX can + # actually download all of them + for dp, _, fps in walk(path): + for fname in fps: + fullpath = join(dp, fname) + spath = fullpath + if fullpath.startswith(basedir): + spath = fullpath[basedir_len:] + to_download.append((fullpath, spath, spath)) + elif path.startswith(basedir): + spath = path[basedir_len:] + to_download.append((path, spath, spath)) + else: + # We are not aware of any case that can trigger this + # situation, but we wanted to be overly cautious + # There is no test for this line cause we don't know + # how to trigger it + to_download.append((path, path, path)) + + for pt in a.prep_templates: + qmf = pt.qiime_map_fp + if qmf is not None: + sqmf = qmf + if qmf.startswith(basedir): + sqmf = qmf[basedir_len:] + to_download.append( + (qmf, sqmf, 'mapping_files/%s_mapping_file.txt' + % a.id)) + + # If we don't have nginx, write a file that indicates this + all_files = '\n'.join(["- %s /protected/%s %s" % (getsize(fp), sfp, n) + for fp, sfp, n in to_download]) + self.write("%s\n" % all_files) + + zip_fn = 'study_%d_%s.zip' % ( + study_id, datetime.now().strftime('%m%d%y-%H%M%S')) + + self.set_header('Content-Description', 'File Transfer') + self.set_header('Expires', '0') + self.set_header('Cache-Control', 'no-cache') + self.set_header('X-Archive-Files', 'zip') + self.set_header('Content-Disposition', + 'attachment; filename=%s' % zip_fn) + self.finish() + + +class DownloadRelease(BaseHandler): + def get(self, extras): + _, relpath, _ = get_release_info() + + # If we don't have nginx, write a file that indicates this + # Note that this configuration will automatically create and download + # ("on the fly") the zip file via the contents in all_files + self.write("This installation of Qiita was not equipped with nginx, " + "so it is incapable of serving files. The file you " + "attempted to download is located at %s" % relpath) + + self.set_header('Content-Description', 'File Transfer') + self.set_header('Content-Type', 'application/octet-stream') + self.set_header('Content-Transfer-Encoding', 'binary') + self.set_header('Expires', '0') + self.set_header('Cache-Control', 'no-cache') + self.set_header('X-Accel-Redirect', + '/protected-working_dir/' + relpath) + self.set_header('Content-Disposition', + 'attachment; filename=%s' % basename(relpath)) + self.finish() + + +class DownloadRawData(BaseHandler): + @authenticated + @execute_as_transaction + def get(self, study_id): + study_id = int(study_id) + # Check general access to study + study_info = study_get_req(study_id, self.current_user.id) + if study_info['status'] != 'success': + raise HTTPError(405, "%s: %s, %s" % (study_info['message'], + self.current_user.email, + str(study_id))) + + study = Study(study_id) + user = self.current_user + # Check "owner" access to the study + if not study.has_access(user, True): + raise HTTPError(405, "%s: %s, %s" % ('No raw data access', + self.current_user.email, + str(study_id))) + + basedir = get_db_files_base_dir() + basedir_len = len(basedir) + 1 + # loop over artifacts and retrieve raw data (no parents) + to_download = [] + for a in study.artifacts(): + if not a.parents: + for i, (fid, path, data_type) in enumerate(a.filepaths): + if data_type == 'directory': + # If we have a directory, we actually need to list + # all the files from the directory so NGINX can + # actually download all of them + for dp, _, fps in walk(path): + for fname in fps: + fullpath = join(dp, fname) + spath = fullpath + if fullpath.startswith(basedir): + spath = fullpath[basedir_len:] + to_download.append((fullpath, spath, spath)) + elif path.startswith(basedir): + spath = path[basedir_len:] + to_download.append((path, spath, spath)) + else: + # We are not aware of any case that can trigger this + # situation, but we wanted to be overly cautious + # There is no test for this line cause we don't know + # how to trigger it + to_download.append((path, path, path)) + + for pt in a.prep_templates: + qmf = pt.qiime_map_fp + if qmf is not None: + sqmf = qmf + if qmf.startswith(basedir): + sqmf = qmf[basedir_len:] + to_download.append( + (qmf, sqmf, 'mapping_files/%s_mapping_file.txt' + % a.id)) + + # If we don't have nginx, write a file that indicates this + # Note that this configuration will automatically create and download + # ("on the fly") the zip file via the contents in all_files + all_files = '\n'.join(["- %s /protected/%s %s" % (getsize(fp), sfp, n) + for fp, sfp, n in to_download]) + self.write("%s\n" % all_files) + + zip_fn = 'study_raw_data_%d_%s.zip' % ( + study_id, datetime.now().strftime('%m%d%y-%H%M%S')) + + self.set_header('Content-Description', 'File Transfer') + self.set_header('Expires', '0') + self.set_header('Cache-Control', 'no-cache') + self.set_header('X-Archive-Files', 'zip') + self.set_header('Content-Disposition', + 'attachment; filename=%s' % zip_fn) + self.finish() diff --git a/qiita_pet/handlers/rest/__init__.py b/qiita_pet/handlers/rest/__init__.py new file mode 100644 index 000000000..cfd842dc7 --- /dev/null +++ b/qiita_pet/handlers/rest/__init__.py @@ -0,0 +1,35 @@ +# ----------------------------------------------------------------------------- +# 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 .study import StudyHandler, StudyCreatorHandler, StudyStatusHandler +from .study_samples import (StudySamplesHandler, StudySamplesInfoHandler, + StudySamplesCategoriesHandler) +from .study_person import StudyPersonHandler +from .study_preparation import (StudyPrepCreatorHandler, + StudyPrepArtifactCreatorHandler) + + +__all__ = ['StudyHandler', 'StudySamplesHandler', 'StudySamplesInfoHandler', + 'StudySamplesCategoriesHandler', 'StudyPersonHandler', + 'StudyCreatorHandler', 'StudyPrepCreatorHandler', + 'StudyPrepArtifactCreatorHandler', 'StudyStatusHandler'] + + +ENDPOINTS = ( + (r"/api/v1/study$", StudyCreatorHandler), + (r"/api/v1/study/([0-9]+)$", StudyHandler), + (r"/api/v1/study/([0-9]+)/samples/categories=([a-zA-Z\-0-9\.:,_]*)", + StudySamplesCategoriesHandler), + (r"/api/v1/study/([0-9]+)/samples", StudySamplesHandler), + (r"/api/v1/study/([0-9]+)/samples/info", StudySamplesInfoHandler), + (r"/api/v1/person(.*)", StudyPersonHandler), + (r"/api/v1/study/([0-9]+)/preparation/([0-9]+)/artifact", + StudyPrepArtifactCreatorHandler), + (r"/api/v1/study/([0-9]+)/preparation(.*)", StudyPrepCreatorHandler), + (r"/api/v1/study/([0-9]+)/status$", StudyStatusHandler) +) diff --git a/qiita_pet/handlers/rest/rest_handler.py b/qiita_pet/handlers/rest/rest_handler.py new file mode 100644 index 000000000..8aa281f41 --- /dev/null +++ b/qiita_pet/handlers/rest/rest_handler.py @@ -0,0 +1,31 @@ +# ----------------------------------------------------------------------------- +# 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 qiita_db.study import Study +from qiita_db.exceptions import QiitaDBUnknownIDError +from qiita_pet.handlers.util import to_int +from qiita_pet.handlers.base_handlers import BaseHandler + + +class RESTHandler(BaseHandler): + def fail(self, msg, status, **kwargs): + out = {'message': msg} + out.update(kwargs) + + self.write(out) + self.set_status(status) + self.finish() + + def safe_get_study(self, study_id): + study_id = to_int(study_id) + s = None + try: + s = Study(study_id) + except QiitaDBUnknownIDError: + self.fail('Study not found', 404) + finally: + return s diff --git a/qiita_pet/handlers/rest/study.py b/qiita_pet/handlers/rest/study.py new file mode 100644 index 000000000..25533ebe3 --- /dev/null +++ b/qiita_pet/handlers/rest/study.py @@ -0,0 +1,154 @@ +# ----------------------------------------------------------------------------- +# 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. +# ----------------------------------------------------------------------------- +import warnings + +from tornado.escape import json_decode + +from qiita_db.handlers.oauth2 import authenticate_oauth +from qiita_db.study import StudyPerson, Study +from qiita_db.user import User +from .rest_handler import RESTHandler +from qiita_db.metadata_template.constants import SAMPLE_TEMPLATE_COLUMNS + + +class StudyHandler(RESTHandler): + + @authenticate_oauth + def get(self, study_id): + study = self.safe_get_study(study_id) + if study is None: + return + + info = study.info + pi = info['principal_investigator'] + lp = info['lab_person'] + self.write({'title': study.title, + 'contacts': {'principal_investigator': [ + pi.name, + pi.affiliation, + pi.email], + 'lab_person': [ + lp.name, + lp.affiliation, + lp.email]}, + 'study_abstract': info['study_abstract'], + 'study_description': info['study_description'], + 'study_alias': info['study_alias']}) + self.finish() + + +class StudyCreatorHandler(RESTHandler): + + @authenticate_oauth + def post(self): + try: + payload = json_decode(self.request.body) + except ValueError: + self.fail('Could not parse body', 400) + return + + required = {'title', 'study_abstract', 'study_description', + 'study_alias', 'owner', 'contacts'} + + if not required.issubset(payload): + self.fail('Not all required arguments provided', 400) + return + + title = payload['title'] + study_abstract = payload['study_abstract'] + study_desc = payload['study_description'] + study_alias = payload['study_alias'] + + owner = payload['owner'] + if not User.exists(owner): + self.fail('Unknown user', 403) + return + else: + owner = User(owner) + + contacts = payload['contacts'] + + if Study.exists(title): + self.fail('Study title already exists', 409) + return + + pi_name = contacts['principal_investigator'][0] + pi_aff = contacts['principal_investigator'][1] + if not StudyPerson.exists(pi_name, pi_aff): + self.fail('Unknown principal investigator', 403) + return + else: + pi = StudyPerson.from_name_and_affiliation(pi_name, pi_aff) + + lp_name = contacts['lab_person'][0] + lp_aff = contacts['lab_person'][1] + if not StudyPerson.exists(lp_name, lp_aff): + self.fail('Unknown lab person', 403) + return + else: + lp = StudyPerson.from_name_and_affiliation(lp_name, lp_aff) + + info = {'lab_person_id': lp, + 'principal_investigator_id': pi, + 'study_abstract': study_abstract, + 'study_description': study_desc, + 'study_alias': study_alias, + + # TODO: we believe it is accurate that mixs is false and + # metadata completion is false as these cannot be known + # at study creation here no matter what. + # we do not know what should be done with the timeseries. + 'mixs_compliant': False, + 'metadata_complete': False, + 'timeseries_type_id': 1} + study = Study.create(owner, title, [1], info) + + self.set_status(201) + self.write({'id': study.id}) + self.finish() + + +class StudyStatusHandler(RESTHandler): + @authenticate_oauth + def get(self, study_id): + study = self.safe_get_study(study_id) + if study is None: + return + + public = study.status == 'public' + st = study.sample_template + sample_information = st is not None + if sample_information: + with warnings.catch_warnings(): + try: + st.validate(SAMPLE_TEMPLATE_COLUMNS) + except Warning: + sample_information_warnings = True + else: + sample_information_warnings = False + else: + sample_information_warnings = False + + preparations = [] + for prep in study.prep_templates(): + pid = prep.id + art = prep.artifact is not None + # TODO: unclear how to test for warnings on the preparations as + # it requires knowledge of the preparation type. It is possible + # to tease this out, but it replicates code present in + # PrepTemplate.create, see: + # https://github.com/biocore/qiita/issues/2096 + preparations.append({'id': pid, 'has_artifact': art}) + + self.write({'is_public': public, + 'has_sample_information': sample_information, + 'sample_information_has_warnings': + sample_information_warnings, + 'preparations': preparations}) + self.set_status(200) + self.finish() diff --git a/qiita_pet/handlers/rest/study_person.py b/qiita_pet/handlers/rest/study_person.py new file mode 100644 index 000000000..b8466f07d --- /dev/null +++ b/qiita_pet/handlers/rest/study_person.py @@ -0,0 +1,61 @@ +# ----------------------------------------------------------------------------- +# 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 tornado.escape import json_encode +from tornado.web import MissingArgumentError + +from qiita_db.handlers.oauth2 import authenticate_oauth +from qiita_db.study import StudyPerson +from qiita_db.exceptions import QiitaDBLookupError +from .rest_handler import RESTHandler + + +class StudyPersonHandler(RESTHandler): + @authenticate_oauth + def get(self, *args, **kwargs): + name = self.get_argument('name', None) + affiliation = self.get_argument('affiliation', None) + + if name is None and affiliation is None: + # Retrieve the list of all the StudyPerson + sp = [{'name': p.name, 'affiliation': p.affiliation} + for p in StudyPerson.iter()] + self.write(json_encode(sp)) + self.finish() + elif name is not None and affiliation is not None: + try: + p = StudyPerson.from_name_and_affiliation(name, affiliation) + except QiitaDBLookupError: + self.fail('Person not found', 404) + return + self.write({'address': p.address, 'phone': p.phone, + 'email': p.email, 'id': p.id}) + self.finish() + else: + arg_name = 'name' if name is None else 'affiliation' + raise MissingArgumentError(arg_name) + + @authenticate_oauth + def post(self, *args, **kwargs): + name = self.get_argument('name') + affiliation = self.get_argument('affiliation') + email = self.get_argument('email') + + phone = self.get_argument('phone', None) + address = self.get_argument('address', None) + + if StudyPerson.exists(name, affiliation): + self.fail('Person already exists', 409) + return + + p = StudyPerson.create(name=name, affiliation=affiliation, email=email, + phone=phone, address=address) + + self.set_status(201) + self.write({'id': p.id}) + self.finish() diff --git a/qiita_pet/handlers/rest/study_preparation.py b/qiita_pet/handlers/rest/study_preparation.py new file mode 100644 index 000000000..02d9c5f1b --- /dev/null +++ b/qiita_pet/handlers/rest/study_preparation.py @@ -0,0 +1,88 @@ +# ----------------------------------------------------------------------------- +# 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. +# ----------------------------------------------------------------------------- + +import os + +import pandas as pd +from tornado.escape import json_decode + +from qiita_db.util import get_mountpoint +from qiita_db.artifact import Artifact +from qiita_pet.handlers.util import to_int +from qiita_db.exceptions import QiitaDBUnknownIDError, QiitaError +from qiita_db.metadata_template.prep_template import PrepTemplate +from qiita_db.handlers.oauth2 import authenticate_oauth +from .rest_handler import RESTHandler + + +class StudyPrepCreatorHandler(RESTHandler): + # TODO: do something smart about warnings, perhaps this should go in its + # own endpoint i.e. /api/v1/study//preparation/validate + # See also: https://github.com/biocore/qiita/issues/2096 + + @authenticate_oauth + def post(self, study_id, *args, **kwargs): + data_type = self.get_argument('data_type') + investigation_type = self.get_argument('investigation_type', None) + + study_id = self.safe_get_study(study_id) + if study_id is None: + return + + data = pd.DataFrame.from_dict(json_decode(self.request.body), + orient='index') + + try: + p = PrepTemplate.create(data, study_id, data_type, + investigation_type) + except QiitaError as e: + self.fail(e.message, 406) + return + + self.write({'id': p.id}) + self.set_status(201) + self.finish() + + +class StudyPrepArtifactCreatorHandler(RESTHandler): + + @authenticate_oauth + def post(self, study_id, prep_id): + study = self.safe_get_study(study_id) + if study is None: + return + + prep_id = to_int(prep_id) + try: + p = PrepTemplate(prep_id) + except QiitaDBUnknownIDError: + self.fail('Preparation not found', 404) + return + + if p.study_id != study.id: + self.fail('Preparation ID not associated with the study', 409) + return + + artifact_deets = json_decode(self.request.body) + _, upload = get_mountpoint('uploads')[0] + base = os.path.join(upload, study_id) + filepaths = [(os.path.join(base, fp), fp_type) + for fp, fp_type in artifact_deets['filepaths']] + + try: + art = Artifact.create(filepaths, + artifact_deets['artifact_type'], + artifact_deets['artifact_name'], + p) + except QiitaError as e: + self.fail(e.message, 406) + return + + self.write({'id': art.id}) + self.set_status(201) + self.finish() diff --git a/qiita_pet/handlers/rest/study_samples.py b/qiita_pet/handlers/rest/study_samples.py new file mode 100644 index 000000000..a9f420dbe --- /dev/null +++ b/qiita_pet/handlers/rest/study_samples.py @@ -0,0 +1,136 @@ +# ----------------------------------------------------------------------------- +# 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 tornado.escape import json_encode, json_decode +import pandas as pd + +from qiita_db.handlers.oauth2 import authenticate_oauth +from .rest_handler import RESTHandler + + +class StudySamplesHandler(RESTHandler): + + @authenticate_oauth + def get(self, study_id): + study = self.safe_get_study(study_id) + if study is None: + return + + if study.sample_template is None: + samples = [] + else: + samples = list(study.sample_template.keys()) + + self.write(json_encode(samples)) + self.finish() + + @authenticate_oauth + def patch(self, study_id): + study = self.safe_get_study(study_id) + if study is None: + return + + if study.sample_template is None: + self.fail('No sample information found', 404) + return + else: + sample_info = study.sample_template.to_dataframe() + + data = pd.DataFrame.from_dict(json_decode(self.request.body), + orient='index') + + if len(data.index) == 0: + self.fail('No samples provided', 400) + return + + categories = set(study.sample_template.categories()) + + if set(data.columns) != categories: + if set(data.columns).issubset(categories): + self.fail('Not all sample information categories provided', + 400) + else: + unknown = set(data.columns) - categories + self.fail("Some categories do not exist in the sample " + "information", 400, + categories_not_found=sorted(unknown)) + return + + existing_samples = set(sample_info.index) + overlapping_ids = set(data.index).intersection(existing_samples) + new_ids = set(data.index) - existing_samples + status = 500 + + # warnings generated are not currently caught + # see https://github.com/biocore/qiita/issues/2096 + if overlapping_ids: + to_update = data.loc[overlapping_ids] + study.sample_template.update(to_update) + status = 200 + + if new_ids: + to_extend = data.loc[new_ids] + study.sample_template.extend(to_extend) + status = 201 + + self.set_status(status) + self.finish() + + +class StudySamplesCategoriesHandler(RESTHandler): + + @authenticate_oauth + def get(self, study_id, categories): + if not categories: + self.fail('No categories specified', 405) + return + + study = self.safe_get_study(study_id) + if study is None: + return + + categories = categories.split(',') + + if study.sample_template is None: + self.fail('Study does not have sample information', 404) + return + + available_categories = set(study.sample_template.categories()) + not_found = set(categories) - available_categories + if not_found: + self.fail('Category not found', 404, + categories_not_found=sorted(not_found)) + return + + blob = {'header': categories, + 'samples': {}} + df = study.sample_template.to_dataframe() + for idx, row in df[categories].iterrows(): + blob['samples'][idx] = list(row) + + self.write(json_encode(blob)) + self.finish() + + +class StudySamplesInfoHandler(RESTHandler): + + @authenticate_oauth + def get(self, study_id): + study = self.safe_get_study(study_id) + if study is None: + return + + st = study.sample_template + if st is None: + info = {'number-of-samples': 0, + 'categories': []} + else: + info = {'number-of-samples': len(st), + 'categories': st.categories()} + + self.write(json_encode(info)) + self.finish() diff --git a/qiita_pet/handlers/study_handlers/__init__.py b/qiita_pet/handlers/study_handlers/__init__.py index 55b682146..423d6a66f 100644 --- a/qiita_pet/handlers/study_handlers/__init__.py +++ b/qiita_pet/handlers/study_handlers/__init__.py @@ -13,7 +13,7 @@ from .ebi_handlers import EBISubmitHandler from .vamps_handlers import VAMPSHandler from .base import (StudyIndexHandler, StudyBaseInfoAJAX, StudyDeleteAjax, - DataTypesMenuAJAX, StudyFilesAJAX) + DataTypesMenuAJAX, StudyFilesAJAX, StudyGetTags, StudyTags) from .prep_template import ( PrepTemplateGraphAJAX, PrepTemplateAJAX, PrepFilesHandler, NewPrepTemplateAjax, PrepTemplateSummaryAJAX) @@ -21,7 +21,8 @@ ListOptionsHandler, WorkflowHandler, WorkflowRunHandler, JobAJAX) from .artifact import (ArtifactGraphAJAX, NewArtifactHandler, - ArtifactAdminAJAX, ArtifactAJAX, ArtifactSummaryAJAX) + ArtifactAdminAJAX, ArtifactAJAX, ArtifactSummaryAJAX, + ArtifactGetSamples) from .sample_template import SampleTemplateAJAX, SampleAJAX __all__ = ['ListStudiesHandler', 'StudyApprovalList', 'ShareStudyAJAX', @@ -34,4 +35,5 @@ 'StudyDeleteAjax', 'ArtifactAJAX', 'NewPrepTemplateAjax', 'DataTypesMenuAJAX', 'StudyFilesAJAX', 'PrepTemplateSummaryAJAX', 'ArtifactSummaryAJAX', 'WorkflowHandler', 'WorkflowRunHandler', - 'JobAJAX', 'AutocompleteHandler'] + 'JobAJAX', 'AutocompleteHandler', 'StudyGetTags', 'StudyTags', + 'ArtifactGetSamples'] diff --git a/qiita_pet/handlers/study_handlers/artifact.py b/qiita_pet/handlers/study_handlers/artifact.py index d1063953d..3f1e3ccfa 100644 --- a/qiita_pet/handlers/study_handlers/artifact.py +++ b/qiita_pet/handlers/study_handlers/artifact.py @@ -16,7 +16,7 @@ artifact_graph_get_req, artifact_types_get_req, artifact_post_req, artifact_status_put_req, artifact_get_req, artifact_delete_req, artifact_summary_get_request, artifact_summary_post_request, - artifact_patch_request) + artifact_patch_request, artifact_get_prep_req) from qiita_core.util import execute_as_transaction from qiita_core.qiita_settings import qiita_config @@ -107,6 +107,16 @@ def patch(self): self.write(response) +class ArtifactGetSamples(BaseHandler): + @authenticated + def get(self): + aids = map(int, self.request.arguments.get('ids[]', [])) + + response = artifact_get_prep_req(self.current_user.id, aids) + + self.write(response) + + class ArtifactAdminAJAX(BaseHandler): @authenticated def get(self): diff --git a/qiita_pet/handlers/study_handlers/base.py b/qiita_pet/handlers/study_handlers/base.py index 5d4dee400..d243ecf81 100644 --- a/qiita_pet/handlers/study_handlers/base.py +++ b/qiita_pet/handlers/study_handlers/base.py @@ -13,8 +13,8 @@ from qiita_pet.handlers.util import to_int, doi_linkifier, pubmed_linkifier from qiita_pet.handlers.base_handlers import BaseHandler from qiita_pet.handlers.api_proxy import ( - study_prep_get_req, study_get_req, study_delete_req, - study_files_get_req) + study_prep_get_req, study_get_req, study_delete_req, study_tags_request, + study_tags_patch_request, study_get_tags_request, study_files_get_req) class StudyIndexHandler(BaseHandler): @@ -62,6 +62,7 @@ def get(self): class StudyDeleteAjax(BaseHandler): + @authenticated def post(self): study_id = self.get_argument('study_id') self.write(study_delete_req(int(study_id), self.current_user.id)) @@ -93,3 +94,37 @@ def get(self): res = study_files_get_req(self.current_user.id, study_id, pt_id, atype) self.render('study_ajax/artifact_file_selector.html', **res) + + +class StudyGetTags(BaseHandler): + @authenticated + def get(self): + response = study_tags_request() + self.write(response) + + +class StudyTags(BaseHandler): + @authenticated + def get(self, study_id): + study_id = to_int(study_id) + + response = study_get_tags_request(self.current_user.id, study_id) + self.write(response) + + @authenticated + def patch(self, study_id): + """Patches a prep template in the system + + Follows the JSON PATCH specification: + https://tools.ietf.org/html/rfc6902 + """ + study_id = to_int(study_id) + req_op = self.get_argument('op') + req_path = self.get_argument('path') + req_value = self.request.arguments.get('value[]', None) + req_form = self.get_argument('form', None) + + response = study_tags_patch_request( + self.current_user.id, study_id, req_op, req_path, + req_value, req_form) + self.write(response) diff --git a/qiita_pet/handlers/study_handlers/edit_handlers.py b/qiita_pet/handlers/study_handlers/edit_handlers.py index 3266e9411..aa911e74b 100644 --- a/qiita_pet/handlers/study_handlers/edit_handlers.py +++ b/qiita_pet/handlers/study_handlers/edit_handlers.py @@ -166,7 +166,7 @@ def _check_study_exists_and_user_access(self, study_id): raise HTTPError(404, "Study %s does not exist" % study_id) # We need to check if the user has access to the study - check_access(self.current_user, study) + check_access(self.current_user, study, raise_error=True) return study def _get_study_person_id(self, index, new_people_info): diff --git a/qiita_pet/handlers/study_handlers/listing_handlers.py b/qiita_pet/handlers/study_handlers/listing_handlers.py index 2d10365c5..b28c8b6fd 100644 --- a/qiita_pet/handlers/study_handlers/listing_handlers.py +++ b/qiita_pet/handlers/study_handlers/listing_handlers.py @@ -59,7 +59,6 @@ def _build_study_info(user, search_type, study_proc=None, proc_samples=None): ----- Both study_proc and proc_samples must be passed, or neither passed. """ - build_samples = False # Logic check to make sure both needed parts passed if study_proc is not None and proc_samples is None: raise IncompetentQiitaDeveloperError( @@ -67,19 +66,18 @@ def _build_study_info(user, search_type, study_proc=None, proc_samples=None): elif proc_samples is not None and study_proc is None: raise IncompetentQiitaDeveloperError( 'Must pass study_proc when proc_samples given') - elif study_proc is None: - build_samples = True # get list of studies for table + user_study_set = user.user_studies.union(user.shared_studies) if search_type == 'user': - user_study_set = user.user_studies.union(user.shared_studies) if user.level == 'admin': user_study_set = (user_study_set | Study.get_by_status('sandbox') | - Study.get_by_status('private')) - study_set = user_study_set - Study.get_by_status('public') + Study.get_by_status('private') - + Study.get_by_status('public')) + study_set = user_study_set elif search_type == 'public': - study_set = Study.get_by_status('public') + study_set = Study.get_by_status('public') - user_study_set else: raise ValueError('Not a valid search type') if study_proc is not None: @@ -88,7 +86,7 @@ def _build_study_info(user, search_type, study_proc=None, proc_samples=None): # No studies left so no need to continue return [] - return generate_study_list([s.id for s in study_set], build_samples, + return generate_study_list([s.id for s in study_set], public_only=(search_type == 'public')) diff --git a/qiita_pet/handlers/study_handlers/tests/test_artifact.py b/qiita_pet/handlers/study_handlers/tests/test_artifact.py index 304dc2844..aaa9531d3 100644 --- a/qiita_pet/handlers/study_handlers/tests/test_artifact.py +++ b/qiita_pet/handlers/study_handlers/tests/test_artifact.py @@ -140,6 +140,34 @@ def test_delete_artifact(self): wait_for_prep_information_job(1) +class ArtifactGetSamplesTest(TestHandlerBase): + def test_get(self): + response = self.get('/artifact/samples/', {'ids[]': [4, 5]}) + self.assertEqual(response.code, 200) + exp = ( + {"status": "success", "msg": "", + "data": + {"4": ["1.SKB2.640194", "1.SKM4.640180", "1.SKB3.640195", + "1.SKB6.640176", "1.SKD6.640190", "1.SKM6.640187", + "1.SKD9.640182", "1.SKM8.640201", "1.SKM2.640199", + "1.SKD2.640178", "1.SKB7.640196", "1.SKD4.640185", + "1.SKB8.640193", "1.SKM3.640197", "1.SKD5.640186", + "1.SKB1.640202", "1.SKM1.640183", "1.SKD1.640179", + "1.SKD3.640198", "1.SKB5.640181", "1.SKB4.640189", + "1.SKB9.640200", "1.SKM9.640192", "1.SKD8.640184", + "1.SKM5.640177", "1.SKM7.640188", "1.SKD7.640191"], + "5": ["1.SKB2.640194", "1.SKM4.640180", "1.SKB3.640195", + "1.SKB6.640176", "1.SKD6.640190", "1.SKM6.640187", + "1.SKD9.640182", "1.SKM8.640201", "1.SKM2.640199", + "1.SKD2.640178", "1.SKB7.640196", "1.SKD4.640185", + "1.SKB8.640193", "1.SKM3.640197", "1.SKD5.640186", + "1.SKB1.640202", "1.SKM1.640183", "1.SKD1.640179", + "1.SKD3.640198", "1.SKB5.640181", "1.SKB4.640189", + "1.SKB9.640200", "1.SKM9.640192", "1.SKD8.640184", + "1.SKM5.640177", "1.SKM7.640188", "1.SKD7.640191"]}}) + self.assertEqual(loads(response.body), exp) + + class ArtifactAdminAJAXTestsReadOnly(TestHandlerBase): def test_get_admin(self): response = self.get('/admin/artifact/', diff --git a/qiita_pet/handlers/study_handlers/tests/test_base.py b/qiita_pet/handlers/study_handlers/tests/test_base.py index 33168bbf4..400660718 100644 --- a/qiita_pet/handlers/study_handlers/tests/test_base.py +++ b/qiita_pet/handlers/study_handlers/tests/test_base.py @@ -9,6 +9,7 @@ from json import loads from qiita_pet.test.tornado_test_base import TestHandlerBase +from qiita_db.handlers.tests.oauthbase import OauthTestingBase class StudyIndexHandlerTests(TestHandlerBase): @@ -27,7 +28,6 @@ class StudyBaseInfoAJAX(TestHandlerBase): class StudyDeleteAjaxTests(TestHandlerBase): - def test_delete_study(self): response = self.post('/study/delete/', {'study_id': 1}) self.assertEqual(response.code, 200) @@ -59,5 +59,45 @@ def test_get(self): self.assertNotEqual(response.body, "") +class TestStudyGetTags(TestHandlerBase): + def test_get(self): + response = self.get('/study/get_tags/') + exp = ('{"status": "success", "message": "", "tags": ' + '{"admin": [], "user": []}}') + self.assertEqual(response.code, 200) + self.assertEqual(response.body, exp) + + +class TestStudyTags(OauthTestingBase): + def test_get(self): + response = self.get('/study/tags/1') + exp = ('{"status": "success", "message": "", "tags": []}') + self.assertEqual(response.code, 200) + self.assertEqual(response.body, exp) + + # test error + response = self.get('/study/tags/bla') + self.assertEqual(response.code, 400) + + def test_patch(self): + arguments = {'op': 'replace', 'path': '/tags', + 'value[]': "['testA', 'testB']"} + obs = self.patch('/study/tags/1', headers=self.header, data=arguments) + + self.assertEqual(obs.code, 200) + self.assertEqual(obs.body, '{"status": "success", "message": ""}') + # checking the tags were added + response = self.get('/study/tags/1') + exp = ('{"status": "success", "message": "", "tags": ' + '["[\'testA\', \'testB\']"]}') + self.assertEqual(response.code, 200) + self.assertEqual(response.body, exp) + + arguments = {'op': 'replace', 'path': '/tags', + 'value[]': "['testA', 'testB']"} + obs = self.patch('/study/tags/b', headers=self.header, data=arguments) + self.assertEqual(obs.code, 400) + + if __name__ == "__main__": main() diff --git a/qiita_pet/handlers/study_handlers/tests/test_edit_handlers.py b/qiita_pet/handlers/study_handlers/tests/test_edit_handlers.py index 8e68b8bfc..7a65942fd 100644 --- a/qiita_pet/handlers/study_handlers/tests/test_edit_handlers.py +++ b/qiita_pet/handlers/study_handlers/tests/test_edit_handlers.py @@ -7,9 +7,12 @@ # The full license is in the file LICENSE, distributed with this software. # ----------------------------------------------------------------------------- from unittest import main +from mock import Mock +from qiita_pet.handlers.base_handlers import BaseHandler from qiita_pet.test.tornado_test_base import TestHandlerBase from qiita_db.study import StudyPerson, Study +from qiita_db.user import User from qiita_db.util import get_count, check_count @@ -126,13 +129,27 @@ def test_post_edit_blank_doi(self): 'principal_investigator': study_info['principal_investigator'].id, 'lab_person': study_info['lab_person'].id} - self.post('/study/edit/1', post_data) - + response = self.post('/study/edit/1', post_data) + self.assertEqual(response.code, 200) # Check that the study was updated self.assertTrue(check_count('qiita.study', study_count_before)) self.assertEqual(study.title, 'New title - test post edit') self.assertEqual(study.publications, []) + # check for failure + old_title = post_data['study_title'] + post_data['study_title'] = 'My new title!' + shared = User('shared@foo.bar') + study.unshare(shared) + BaseHandler.get_current_user = Mock(return_value=shared) + response = self.post('/study/edit/1', post_data) + self.assertEqual(response.code, 403) + # Check that the study wasn't updated + self.assertEqual(study.title, old_title) + + # returning sharing + study.share(shared) + class TestCreateStudyAJAX(TestHandlerBase): def test_get(self): diff --git a/qiita_pet/handlers/study_handlers/tests/test_listing_handlers.py b/qiita_pet/handlers/study_handlers/tests/test_listing_handlers.py index 33fa2cbdc..e4de44bb5 100644 --- a/qiita_pet/handlers/study_handlers/tests/test_listing_handlers.py +++ b/qiita_pet/handlers/study_handlers/tests/test_listing_handlers.py @@ -21,32 +21,28 @@ _build_study_info) from qiita_pet.handlers.base_handlers import BaseHandler - -SAMPLES = ['1.SKB1.640202', '1.SKB2.640194', '1.SKB3.640195', '1.SKB4.640189', - '1.SKB5.640181', '1.SKB6.640176', '1.SKB7.640196', '1.SKB8.640193', - '1.SKB9.640200', '1.SKD1.640179', '1.SKD2.640178', '1.SKD3.640198', - '1.SKD4.640185', '1.SKD5.640186', '1.SKD6.640190', '1.SKD7.640191', - '1.SKD8.640184', '1.SKD9.640182', '1.SKM1.640183', '1.SKM2.640199', - '1.SKM3.640197', '1.SKM4.640180', '1.SKM5.640177', '1.SKM6.640187', - '1.SKM7.640188', '1.SKM8.640201', '1.SKM9.640192'] GPARAMS = {'similarity': 0.97, 'reference_name': 'Greengenes', 'sortmerna_e_value': 1, 'sortmerna_max_pos': 10000, 'threads': 1, 'sortmerna_coverage': 0.97, 'reference_version': u'13_8'} PROC_DATA_INFO = [ - {'data_type': u'18S', 'algorithm': 'QIIME (Pick closed-reference OTUs)', + {'data_type': u'18S', + 'algorithm': 'QIIME v1.9.1 (Pick closed-reference OTUs)', 'pid': 4, 'processed_date': '2012-10-02 17:30:00', 'params': GPARAMS, - 'samples': SAMPLES}, - {'data_type': '18S', 'algorithm': 'QIIME (Pick closed-reference OTUs)', + 'name': 'BIOM'}, + {'data_type': '18S', + 'algorithm': 'QIIME v1.9.1 (Pick closed-reference OTUs)', 'pid': 5, 'processed_date': '2012-10-02 17:30:00', 'params': GPARAMS, - 'samples': SAMPLES}, - {'data_type': '16S', 'algorithm': 'QIIME (Pick closed-reference OTUs)', + 'name': 'BIOM'}, + {'data_type': '16S', + 'algorithm': 'QIIME v1.9.1 (Pick closed-reference OTUs)', 'pid': 6, 'processed_date': '2012-10-02 17:30:00', 'params': {'similarity': 0.97, 'reference_name': u'Silva', 'sortmerna_e_value': 1, 'sortmerna_max_pos': 10000, 'threads': 1, 'sortmerna_coverage': 0.97, - 'reference_version': 'test'}, 'samples': SAMPLES}, + 'reference_version': 'test'}, + 'name': 'BIOM'}, {'processed_date': '2012-10-02 17:30:00', 'pid': 7, 'data_type': '16S', - 'samples': SAMPLES}] + 'name': 'BIOM'}] class TestHelpers(TestHandlerBase): @@ -78,14 +74,42 @@ def setUp(self): 'publication_doi': ['10.100/123456', '10.100/7891011'], 'publication_pid': ['123456', '7891011'], 'pi': ('PI_dude@foo.bar', 'PIDude'), + 'study_tags': None, 'proc_data_info': PROC_DATA_INFO } self.exp = [self.single_exp] def test_build_study_info(self): + for a in Study(1).artifacts(): + a.visibility = 'private' + + obs = _build_study_info(User('test@foo.bar'), 'user') + self.assertEqual(obs, self.exp) + + obs = _build_study_info(User('test@foo.bar'), 'public') + self.assertEqual(obs, []) + + obs = _build_study_info(User('demo@microbio.me'), 'public') + self.assertEqual(obs, []) + + # make all the artifacts public - (1) the only study in the tests, + for a in Study(1).artifacts(): + a.visibility = 'public' + self.exp[0]['status'] = 'public' + obs = _build_study_info(User('test@foo.bar'), 'user') self.assertEqual(obs, self.exp) + obs = _build_study_info(User('test@foo.bar'), 'public') + self.assertEqual(obs, []) + + obs = _build_study_info(User('demo@microbio.me'), 'public') + self.assertEqual(obs, self.exp) + + # return to it's private status + for a in Study(1).artifacts(): + a.visibility = 'private' + def test_build_study_info_erros(self): with self.assertRaises(IncompetentQiitaDeveloperError): _build_study_info(User('test@foo.bar'), 'user', study_proc={}) @@ -124,6 +148,7 @@ def test_build_study_info_empty_study(self): 'study_id': 2, 'ebi_study_accession': None, 'study_title': 'My study', + 'study_tags': None, 'number_samples_collected': 0}) self.assertItemsEqual(obs, self.exp) @@ -280,7 +305,8 @@ def setUp(self): 'Future studies will attempt to analyze the soils and ' 'rhizospheres from the same location at different time ' 'points in the plant lifecycle.'), - 'number_samples_collected': 27}], + 'number_samples_collected': 27, + 'study_tags': None}], 'sEcho': 1021, 'iTotalDisplayRecords': 1} self.empty = {'aaData': [], diff --git a/qiita_pet/nginx_example.conf b/qiita_pet/nginx_example.conf index f0cca6f81..e6e9d3628 100644 --- a/qiita_pet/nginx_example.conf +++ b/qiita_pet/nginx_example.conf @@ -13,12 +13,22 @@ http { # download configuration, based on: # https://groups.google.com/forum/#!topic/python-tornado/sgadmx8Hd_s + # protected location for working diretory + location /protected-working_dir/ { + internal; + + # CHANGE ME: This should match the WORKING_DIR in your qiita + # config. E.g., + # alias /Users/username/qiita/qiita_db/support_files/test_data/working_dir/; + alias ; + } + # protected location location /protected/ { internal; # CHANGE ME: This should match the BASE_DATA_DIR in your qiita - # config. E.g., + # config. E.g., # alias /Users/username/qiita/qiita_db/support_files/test_data/; alias ; } @@ -30,6 +40,7 @@ http { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Accept-Encoding identity; } } } diff --git a/qiita_pet/static/js/qiita.js b/qiita_pet/static/js/qiita.js index 48e8ab5b5..10b0d1657 100644 --- a/qiita_pet/static/js/qiita.js +++ b/qiita_pet/static/js/qiita.js @@ -7,6 +7,9 @@ * @param timeout: OPTIONAL. When given, time (in ms) before alert fades out * */ + +var timeoutHandleForBoostrapAlert = null; + function bootstrapAlert(message, severity, timeout){ // Clear the previous alert - so they don't keep stacking on top of each other $('#bootstrap-alert').alert('close'); @@ -32,7 +35,13 @@ function bootstrapAlert(message, severity, timeout){ $('#qiita-main').prepend(alertDiv); if(timeout > 0) { - window.setTimeout(function() { $('#alert-message').alert('close'); }, timeout); + if (timeoutHandleForBoostrapAlert != null) { + window.clearTimeout(timeoutHandleForBoostrapAlert); + } + timeoutHandleForBoostrapAlert = window.setTimeout(function() { + $('#alert-message').alert('close'); + timeoutHandleForBoostrapAlert = null; + }, timeout); } } diff --git a/qiita_pet/static/vendor/css/jquery.dataTables.css b/qiita_pet/static/vendor/css/jquery.dataTables.css deleted file mode 100644 index 4e6fbe38b..000000000 --- a/qiita_pet/static/vendor/css/jquery.dataTables.css +++ /dev/null @@ -1,476 +0,0 @@ -/* - * Table styles - */ -table.dataTable { - width: 100%; - margin: 0 auto; - clear: both; - border-collapse: separate; - border-spacing: 0; - /* - * Header and footer styles - */ - /* - * Body styles - */ -} -table.dataTable thead th, -table.dataTable tfoot th { - font-weight: bold; -} -table.dataTable thead th, -table.dataTable thead td { - padding: 10px 18px; - border-bottom: 1px solid #111111; -} -table.dataTable thead th:active, -table.dataTable thead td:active { - outline: none; -} -table.dataTable tfoot th, -table.dataTable tfoot td { - padding: 10px 18px 6px 18px; - border-top: 1px solid #111111; -} -table.dataTable thead .sorting_asc, -table.dataTable thead .sorting_desc, -table.dataTable thead .sorting { - cursor: pointer; - *cursor: hand; -} -table.dataTable thead .sorting { - background: url("../images/sort_both.png") no-repeat center right; -} -table.dataTable thead .sorting_asc { - background: url("../images/sort_asc.png") no-repeat center right; -} -table.dataTable thead .sorting_desc { - background: url("../images/sort_desc.png") no-repeat center right; -} -table.dataTable thead .sorting_asc_disabled { - background: url("../images/sort_asc_disabled.png") no-repeat center right; -} -table.dataTable thead .sorting_desc_disabled { - background: url("../images/sort_desc_disabled.png") no-repeat center right; -} -table.dataTable tbody tr { - background-color: white; -} -table.dataTable tbody tr.selected { - background-color: #b0bed9; -} -table.dataTable tbody th, -table.dataTable tbody td { - padding: 8px 10px; -} -table.dataTable.row-border tbody th, table.dataTable.row-border tbody td, table.dataTable.display tbody th, table.dataTable.display tbody td { - border-top: 1px solid #dddddd; -} -table.dataTable.row-border tbody tr:first-child th, -table.dataTable.row-border tbody tr:first-child td, table.dataTable.display tbody tr:first-child th, -table.dataTable.display tbody tr:first-child td { - border-top: none; -} -table.dataTable.cell-border tbody th, table.dataTable.cell-border tbody td { - border-top: 1px solid #dddddd; - border-right: 1px solid #dddddd; -} -table.dataTable.cell-border tbody tr th:first-child, -table.dataTable.cell-border tbody tr td:first-child { - border-left: 1px solid #dddddd; -} -table.dataTable.cell-border tbody tr:first-child th, -table.dataTable.cell-border tbody tr:first-child td { - border-top: none; -} -table.dataTable.stripe tbody tr.odd, table.dataTable.display tbody tr.odd { - background-color: #f9f9f9; -} -table.dataTable.stripe tbody tr.odd.selected, table.dataTable.display tbody tr.odd.selected { - background-color: #abb9d3; -} -table.dataTable.hover tbody tr:hover, -table.dataTable.hover tbody tr.odd:hover, -table.dataTable.hover tbody tr.even:hover, table.dataTable.display tbody tr:hover, -table.dataTable.display tbody tr.odd:hover, -table.dataTable.display tbody tr.even:hover { - background-color: whitesmoke; -} -table.dataTable.hover tbody tr:hover.selected, -table.dataTable.hover tbody tr.odd:hover.selected, -table.dataTable.hover tbody tr.even:hover.selected, table.dataTable.display tbody tr:hover.selected, -table.dataTable.display tbody tr.odd:hover.selected, -table.dataTable.display tbody tr.even:hover.selected { - background-color: #a9b7d1; -} -table.dataTable.order-column tbody tr > .sorting_1, -table.dataTable.order-column tbody tr > .sorting_2, -table.dataTable.order-column tbody tr > .sorting_3, table.dataTable.display tbody tr > .sorting_1, -table.dataTable.display tbody tr > .sorting_2, -table.dataTable.display tbody tr > .sorting_3 { - background-color: #f9f9f9; -} -table.dataTable.order-column tbody tr.selected > .sorting_1, -table.dataTable.order-column tbody tr.selected > .sorting_2, -table.dataTable.order-column tbody tr.selected > .sorting_3, table.dataTable.display tbody tr.selected > .sorting_1, -table.dataTable.display tbody tr.selected > .sorting_2, -table.dataTable.display tbody tr.selected > .sorting_3 { - background-color: #acbad4; -} -table.dataTable.display tbody tr.odd > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd > .sorting_1 { - background-color: #f1f1f1; -} -table.dataTable.display tbody tr.odd > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd > .sorting_2 { - background-color: #f3f3f3; -} -table.dataTable.display tbody tr.odd > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd > .sorting_3 { - background-color: whitesmoke; -} -table.dataTable.display tbody tr.odd.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_1 { - background-color: #a6b3cd; -} -table.dataTable.display tbody tr.odd.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_2 { - background-color: #a7b5ce; -} -table.dataTable.display tbody tr.odd.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_3 { - background-color: #a9b6d0; -} -table.dataTable.display tbody tr.even > .sorting_1, table.dataTable.order-column.stripe tbody tr.even > .sorting_1 { - background-color: #f9f9f9; -} -table.dataTable.display tbody tr.even > .sorting_2, table.dataTable.order-column.stripe tbody tr.even > .sorting_2 { - background-color: #fbfbfb; -} -table.dataTable.display tbody tr.even > .sorting_3, table.dataTable.order-column.stripe tbody tr.even > .sorting_3 { - background-color: #fdfdfd; -} -table.dataTable.display tbody tr.even.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_1 { - background-color: #acbad4; -} -table.dataTable.display tbody tr.even.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_2 { - background-color: #adbbd6; -} -table.dataTable.display tbody tr.even.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_3 { - background-color: #afbdd8; -} -table.dataTable.display tbody tr:hover > .sorting_1, -table.dataTable.display tbody tr.odd:hover > .sorting_1, -table.dataTable.display tbody tr.even:hover > .sorting_1, table.dataTable.order-column.hover tbody tr:hover > .sorting_1, -table.dataTable.order-column.hover tbody tr.odd:hover > .sorting_1, -table.dataTable.order-column.hover tbody tr.even:hover > .sorting_1 { - background-color: #eaeaea; -} -table.dataTable.display tbody tr:hover > .sorting_2, -table.dataTable.display tbody tr.odd:hover > .sorting_2, -table.dataTable.display tbody tr.even:hover > .sorting_2, table.dataTable.order-column.hover tbody tr:hover > .sorting_2, -table.dataTable.order-column.hover tbody tr.odd:hover > .sorting_2, -table.dataTable.order-column.hover tbody tr.even:hover > .sorting_2 { - background-color: #ebebeb; -} -table.dataTable.display tbody tr:hover > .sorting_3, -table.dataTable.display tbody tr.odd:hover > .sorting_3, -table.dataTable.display tbody tr.even:hover > .sorting_3, table.dataTable.order-column.hover tbody tr:hover > .sorting_3, -table.dataTable.order-column.hover tbody tr.odd:hover > .sorting_3, -table.dataTable.order-column.hover tbody tr.even:hover > .sorting_3 { - background-color: #eeeeee; -} -table.dataTable.display tbody tr:hover.selected > .sorting_1, -table.dataTable.display tbody tr.odd:hover.selected > .sorting_1, -table.dataTable.display tbody tr.even:hover.selected > .sorting_1, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_1, -table.dataTable.order-column.hover tbody tr.odd:hover.selected > .sorting_1, -table.dataTable.order-column.hover tbody tr.even:hover.selected > .sorting_1 { - background-color: #a1aec7; -} -table.dataTable.display tbody tr:hover.selected > .sorting_2, -table.dataTable.display tbody tr.odd:hover.selected > .sorting_2, -table.dataTable.display tbody tr.even:hover.selected > .sorting_2, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_2, -table.dataTable.order-column.hover tbody tr.odd:hover.selected > .sorting_2, -table.dataTable.order-column.hover tbody tr.even:hover.selected > .sorting_2 { - background-color: #a2afc8; -} -table.dataTable.display tbody tr:hover.selected > .sorting_3, -table.dataTable.display tbody tr.odd:hover.selected > .sorting_3, -table.dataTable.display tbody tr.even:hover.selected > .sorting_3, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_3, -table.dataTable.order-column.hover tbody tr.odd:hover.selected > .sorting_3, -table.dataTable.order-column.hover tbody tr.even:hover.selected > .sorting_3 { - background-color: #a4b2cb; -} -table.dataTable.no-footer { - border-bottom: 1px solid #111111; -} -table.dataTable.nowrap th, table.dataTable.nowrap td { - white-space: nowrap; -} -table.dataTable.compact thead th, -table.dataTable.compact thead td { - padding: 5px 9px; -} -table.dataTable.compact tfoot th, -table.dataTable.compact tfoot td { - padding: 5px 9px 3px 9px; -} -table.dataTable.compact tbody th, -table.dataTable.compact tbody td { - padding: 4px 5px; -} -table.dataTable th.dt-left, -table.dataTable td.dt-left { - text-align: left; -} -table.dataTable th.dt-center, -table.dataTable td.dt-center, -table.dataTable td.dataTables_empty { - text-align: center; -} -table.dataTable th.dt-right, -table.dataTable td.dt-right { - text-align: right; -} -table.dataTable th.dt-justify, -table.dataTable td.dt-justify { - text-align: justify; -} -table.dataTable th.dt-nowrap, -table.dataTable td.dt-nowrap { - white-space: nowrap; -} -table.dataTable thead th.dt-head-left, -table.dataTable thead td.dt-head-left, -table.dataTable tfoot th.dt-head-left, -table.dataTable tfoot td.dt-head-left { - text-align: left; -} -table.dataTable thead th.dt-head-center, -table.dataTable thead td.dt-head-center, -table.dataTable tfoot th.dt-head-center, -table.dataTable tfoot td.dt-head-center { - text-align: center; -} -table.dataTable thead th.dt-head-right, -table.dataTable thead td.dt-head-right, -table.dataTable tfoot th.dt-head-right, -table.dataTable tfoot td.dt-head-right { - text-align: right; -} -table.dataTable thead th.dt-head-justify, -table.dataTable thead td.dt-head-justify, -table.dataTable tfoot th.dt-head-justify, -table.dataTable tfoot td.dt-head-justify { - text-align: justify; -} -table.dataTable thead th.dt-head-nowrap, -table.dataTable thead td.dt-head-nowrap, -table.dataTable tfoot th.dt-head-nowrap, -table.dataTable tfoot td.dt-head-nowrap { - white-space: nowrap; -} -table.dataTable tbody th.dt-body-left, -table.dataTable tbody td.dt-body-left { - text-align: left; -} -table.dataTable tbody th.dt-body-center, -table.dataTable tbody td.dt-body-center { - text-align: center; -} -table.dataTable tbody th.dt-body-right, -table.dataTable tbody td.dt-body-right { - text-align: right; -} -table.dataTable tbody th.dt-body-justify, -table.dataTable tbody td.dt-body-justify { - text-align: justify; -} -table.dataTable tbody th.dt-body-nowrap, -table.dataTable tbody td.dt-body-nowrap { - white-space: nowrap; -} - -table.dataTable, -table.dataTable th, -table.dataTable td { - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; -} - -/* - * Control feature layout - */ -.dataTables_wrapper { - position: relative; - clear: both; - *zoom: 1; - zoom: 1; -} -.dataTables_wrapper .dataTables_length { - float: left; -} -.dataTables_wrapper .dataTables_filter { - float: right; - text-align: right; -} -.dataTables_wrapper .dataTables_filter input { - margin-left: 0.5em; -} -.dataTables_wrapper .dataTables_info { - clear: both; - float: left; - padding-top: 0.755em; -} -.dataTables_wrapper .dataTables_paginate { - float: right; - text-align: right; - padding-top: 0.25em; -} -.dataTables_wrapper .dataTables_paginate .paginate_button { - box-sizing: border-box; - display: inline-block; - min-width: 1.5em; - padding: 0.5em 1em; - margin-left: 2px; - text-align: center; - text-decoration: none !important; - cursor: pointer; - *cursor: hand; - color: #333333 !important; - border: 1px solid transparent; -} -.dataTables_wrapper .dataTables_paginate .paginate_button.current, .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover { - color: #333333 !important; - border: 1px solid #cacaca; - background-color: white; - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, white), color-stop(100%, gainsboro)); - /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, white 0%, gainsboro 100%); - /* Chrome10+,Safari5.1+ */ - background: -moz-linear-gradient(top, white 0%, gainsboro 100%); - /* FF3.6+ */ - background: -ms-linear-gradient(top, white 0%, gainsboro 100%); - /* IE10+ */ - background: -o-linear-gradient(top, white 0%, gainsboro 100%); - /* Opera 11.10+ */ - background: linear-gradient(to bottom, white 0%, gainsboro 100%); - /* W3C */ -} -.dataTables_wrapper .dataTables_paginate .paginate_button.disabled, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active { - cursor: default; - color: #666 !important; - border: 1px solid transparent; - background: transparent; - box-shadow: none; -} -.dataTables_wrapper .dataTables_paginate .paginate_button:hover { - color: white !important; - border: 1px solid #111111; - background-color: #585858; - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111111)); - /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, #585858 0%, #111111 100%); - /* Chrome10+,Safari5.1+ */ - background: -moz-linear-gradient(top, #585858 0%, #111111 100%); - /* FF3.6+ */ - background: -ms-linear-gradient(top, #585858 0%, #111111 100%); - /* IE10+ */ - background: -o-linear-gradient(top, #585858 0%, #111111 100%); - /* Opera 11.10+ */ - background: linear-gradient(to bottom, #585858 0%, #111111 100%); - /* W3C */ -} -.dataTables_wrapper .dataTables_paginate .paginate_button:active { - outline: none; - background-color: #2b2b2b; - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c)); - /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%); - /* Chrome10+,Safari5.1+ */ - background: -moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%); - /* FF3.6+ */ - background: -ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%); - /* IE10+ */ - background: -o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%); - /* Opera 11.10+ */ - background: linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%); - /* W3C */ - box-shadow: inset 0 0 3px #111; -} -.dataTables_wrapper .dataTables_processing { - position: absolute; - top: 50%; - left: 50%; - width: 100%; - height: 40px; - margin-left: -50%; - margin-top: -25px; - padding-top: 20px; - text-align: center; - font-size: 1.2em; - background-color: white; - background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(25%, rgba(255, 255, 255, 0.9)), color-stop(75%, rgba(255, 255, 255, 0.9)), color-stop(100%, rgba(255, 255, 255, 0))); - /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); - /* Chrome10+,Safari5.1+ */ - background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); - /* FF3.6+ */ - background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); - /* IE10+ */ - background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); - /* Opera 11.10+ */ - background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); - /* W3C */ -} -.dataTables_wrapper .dataTables_length, -.dataTables_wrapper .dataTables_filter, -.dataTables_wrapper .dataTables_info, -.dataTables_wrapper .dataTables_processing, -.dataTables_wrapper .dataTables_paginate { - color: #333333; -} -.dataTables_wrapper .dataTables_scroll { - clear: both; -} -.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody { - *margin-top: -1px; - -webkit-overflow-scrolling: touch; -} -.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody th > div.dataTables_sizing, -.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody td > div.dataTables_sizing { - height: 0; - overflow: hidden; - margin: 0 !important; - padding: 0 !important; -} -.dataTables_wrapper.no-footer .dataTables_scrollBody { - border-bottom: 1px solid #111111; -} -.dataTables_wrapper.no-footer div.dataTables_scrollHead table, -.dataTables_wrapper.no-footer div.dataTables_scrollBody table { - border-bottom: none; -} -.dataTables_wrapper:after { - visibility: hidden; - display: block; - content: ""; - clear: both; - height: 0; -} - -@media screen and (max-width: 767px) { - .dataTables_wrapper .dataTables_info, - .dataTables_wrapper .dataTables_paginate { - float: none; - text-align: center; - } - .dataTables_wrapper .dataTables_paginate { - margin-top: 0.5em; - } -} -@media screen and (max-width: 640px) { - .dataTables_wrapper .dataTables_length, - .dataTables_wrapper .dataTables_filter { - float: none; - text-align: center; - } - .dataTables_wrapper .dataTables_filter { - margin-top: 0.5em; - } -} diff --git a/qiita_pet/static/vendor/css/jquery.dataTables.min.css b/qiita_pet/static/vendor/css/jquery.dataTables.min.css new file mode 100644 index 000000000..781de6bfc --- /dev/null +++ b/qiita_pet/static/vendor/css/jquery.dataTables.min.css @@ -0,0 +1 @@ +table.dataTable{width:100%;margin:0 auto;clear:both;border-collapse:separate;border-spacing:0}table.dataTable thead th,table.dataTable tfoot th{font-weight:bold}table.dataTable thead th,table.dataTable thead td{padding:10px 18px;border-bottom:1px solid #111}table.dataTable thead th:active,table.dataTable thead td:active{outline:none}table.dataTable tfoot th,table.dataTable tfoot td{padding:10px 18px 6px 18px;border-top:1px solid #111}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc{cursor:pointer;*cursor:hand}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{background-repeat:no-repeat;background-position:center right}table.dataTable thead .sorting{background-image:url("../images/sort_both.png")}table.dataTable thead .sorting_asc{background-image:url("../images/sort_asc.png")}table.dataTable thead .sorting_desc{background-image:url("../images/sort_desc.png")}table.dataTable thead .sorting_asc_disabled{background-image:url("../images/sort_asc_disabled.png")}table.dataTable thead .sorting_desc_disabled{background-image:url("../images/sort_desc_disabled.png")}table.dataTable tbody tr{background-color:#ffffff}table.dataTable tbody tr.selected{background-color:#B0BED9}table.dataTable tbody th,table.dataTable tbody td{padding:8px 10px}table.dataTable.row-border tbody th,table.dataTable.row-border tbody td,table.dataTable.display tbody th,table.dataTable.display tbody td{border-top:1px solid #ddd}table.dataTable.row-border tbody tr:first-child th,table.dataTable.row-border tbody tr:first-child td,table.dataTable.display tbody tr:first-child th,table.dataTable.display tbody tr:first-child td{border-top:none}table.dataTable.cell-border tbody th,table.dataTable.cell-border tbody td{border-top:1px solid #ddd;border-right:1px solid #ddd}table.dataTable.cell-border tbody tr th:first-child,table.dataTable.cell-border tbody tr td:first-child{border-left:1px solid #ddd}table.dataTable.cell-border tbody tr:first-child th,table.dataTable.cell-border tbody tr:first-child td{border-top:none}table.dataTable.stripe tbody tr.odd,table.dataTable.display tbody tr.odd{background-color:#f9f9f9}table.dataTable.stripe tbody tr.odd.selected,table.dataTable.display tbody tr.odd.selected{background-color:#acbad4}table.dataTable.hover tbody tr:hover,table.dataTable.display tbody tr:hover{background-color:#f6f6f6}table.dataTable.hover tbody tr:hover.selected,table.dataTable.display tbody tr:hover.selected{background-color:#aab7d1}table.dataTable.order-column tbody tr>.sorting_1,table.dataTable.order-column tbody tr>.sorting_2,table.dataTable.order-column tbody tr>.sorting_3,table.dataTable.display tbody tr>.sorting_1,table.dataTable.display tbody tr>.sorting_2,table.dataTable.display tbody tr>.sorting_3{background-color:#fafafa}table.dataTable.order-column tbody tr.selected>.sorting_1,table.dataTable.order-column tbody tr.selected>.sorting_2,table.dataTable.order-column tbody tr.selected>.sorting_3,table.dataTable.display tbody tr.selected>.sorting_1,table.dataTable.display tbody tr.selected>.sorting_2,table.dataTable.display tbody tr.selected>.sorting_3{background-color:#acbad5}table.dataTable.display tbody tr.odd>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd>.sorting_1{background-color:#f1f1f1}table.dataTable.display tbody tr.odd>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd>.sorting_2{background-color:#f3f3f3}table.dataTable.display tbody tr.odd>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd>.sorting_3{background-color:whitesmoke}table.dataTable.display tbody tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_1{background-color:#a6b4cd}table.dataTable.display tbody tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_2{background-color:#a8b5cf}table.dataTable.display tbody tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_3{background-color:#a9b7d1}table.dataTable.display tbody tr.even>.sorting_1,table.dataTable.order-column.stripe tbody tr.even>.sorting_1{background-color:#fafafa}table.dataTable.display tbody tr.even>.sorting_2,table.dataTable.order-column.stripe tbody tr.even>.sorting_2{background-color:#fcfcfc}table.dataTable.display tbody tr.even>.sorting_3,table.dataTable.order-column.stripe tbody tr.even>.sorting_3{background-color:#fefefe}table.dataTable.display tbody tr.even.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_1{background-color:#acbad5}table.dataTable.display tbody tr.even.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_2{background-color:#aebcd6}table.dataTable.display tbody tr.even.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_3{background-color:#afbdd8}table.dataTable.display tbody tr:hover>.sorting_1,table.dataTable.order-column.hover tbody tr:hover>.sorting_1{background-color:#eaeaea}table.dataTable.display tbody tr:hover>.sorting_2,table.dataTable.order-column.hover tbody tr:hover>.sorting_2{background-color:#ececec}table.dataTable.display tbody tr:hover>.sorting_3,table.dataTable.order-column.hover tbody tr:hover>.sorting_3{background-color:#efefef}table.dataTable.display tbody tr:hover.selected>.sorting_1,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_1{background-color:#a2aec7}table.dataTable.display tbody tr:hover.selected>.sorting_2,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_2{background-color:#a3b0c9}table.dataTable.display tbody tr:hover.selected>.sorting_3,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_3{background-color:#a5b2cb}table.dataTable.no-footer{border-bottom:1px solid #111}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable.compact thead th,table.dataTable.compact thead td{padding:4px 17px 4px 4px}table.dataTable.compact tfoot th,table.dataTable.compact tfoot td{padding:4px}table.dataTable.compact tbody th,table.dataTable.compact tbody td{padding:4px}table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center,table.dataTable td.dataTables_empty{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}table.dataTable,table.dataTable th,table.dataTable td{-webkit-box-sizing:content-box;box-sizing:content-box}.dataTables_wrapper{position:relative;clear:both;*zoom:1;zoom:1}.dataTables_wrapper .dataTables_length{float:left}.dataTables_wrapper .dataTables_filter{float:right;text-align:right}.dataTables_wrapper .dataTables_filter input{margin-left:0.5em}.dataTables_wrapper .dataTables_info{clear:both;float:left;padding-top:0.755em}.dataTables_wrapper .dataTables_paginate{float:right;text-align:right;padding-top:0.25em}.dataTables_wrapper .dataTables_paginate .paginate_button{box-sizing:border-box;display:inline-block;min-width:1.5em;padding:0.5em 1em;margin-left:2px;text-align:center;text-decoration:none !important;cursor:pointer;*cursor:hand;color:#333 !important;border:1px solid transparent;border-radius:2px}.dataTables_wrapper .dataTables_paginate .paginate_button.current,.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover{color:#333 !important;border:1px solid #979797;background-color:white;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #fff), color-stop(100%, #dcdcdc));background:-webkit-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-moz-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-ms-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-o-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:linear-gradient(to bottom, #fff 0%, #dcdcdc 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button.disabled,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active{cursor:default;color:#666 !important;border:1px solid transparent;background:transparent;box-shadow:none}.dataTables_wrapper .dataTables_paginate .paginate_button:hover{color:white !important;border:1px solid #111;background-color:#585858;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));background:-webkit-linear-gradient(top, #585858 0%, #111 100%);background:-moz-linear-gradient(top, #585858 0%, #111 100%);background:-ms-linear-gradient(top, #585858 0%, #111 100%);background:-o-linear-gradient(top, #585858 0%, #111 100%);background:linear-gradient(to bottom, #585858 0%, #111 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button:active{outline:none;background-color:#2b2b2b;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));background:-webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);box-shadow:inset 0 0 3px #111}.dataTables_wrapper .dataTables_paginate .ellipsis{padding:0 1em}.dataTables_wrapper .dataTables_processing{position:absolute;top:50%;left:50%;width:100%;height:40px;margin-left:-50%;margin-top:-25px;padding-top:20px;text-align:center;font-size:1.2em;background-color:white;background:-webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255,255,255,0)), color-stop(25%, rgba(255,255,255,0.9)), color-stop(75%, rgba(255,255,255,0.9)), color-stop(100%, rgba(255,255,255,0)));background:-webkit-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-moz-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-o-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%)}.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter,.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_processing,.dataTables_wrapper .dataTables_paginate{color:#333}.dataTables_wrapper .dataTables_scroll{clear:both}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody{*margin-top:-1px;-webkit-overflow-scrolling:touch}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody td{vertical-align:middle}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody td>div.dataTables_sizing{height:0;overflow:hidden;margin:0 !important;padding:0 !important}.dataTables_wrapper.no-footer .dataTables_scrollBody{border-bottom:1px solid #111}.dataTables_wrapper.no-footer div.dataTables_scrollHead table,.dataTables_wrapper.no-footer div.dataTables_scrollBody table{border-bottom:none}.dataTables_wrapper:after{visibility:hidden;display:block;content:"";clear:both;height:0}@media screen and (max-width: 767px){.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_paginate{float:none;text-align:center}.dataTables_wrapper .dataTables_paginate{margin-top:0.5em}}@media screen and (max-width: 640px){.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter{float:none;text-align:center}.dataTables_wrapper .dataTables_filter{margin-top:0.5em}} diff --git a/qiita_pet/static/vendor/css/jquery.tagit.css b/qiita_pet/static/vendor/css/jquery.tagit.css new file mode 100644 index 000000000..f18650d91 --- /dev/null +++ b/qiita_pet/static/vendor/css/jquery.tagit.css @@ -0,0 +1,69 @@ +ul.tagit { + padding: 1px 5px; + overflow: auto; + margin-left: inherit; /* usually we don't want the regular ul margins. */ + margin-right: inherit; +} +ul.tagit li { + display: block; + float: left; + margin: 2px 5px 2px 0; +} +ul.tagit li.tagit-choice { + position: relative; + line-height: inherit; +} +input.tagit-hidden-field { + display: none; +} +ul.tagit li.tagit-choice-read-only { + padding: .2em .5em .2em .5em; +} + +ul.tagit li.tagit-choice-editable { + padding: .2em 18px .2em .5em; +} + +ul.tagit li.tagit-new { + padding: .25em 4px .25em 0; +} + +ul.tagit li.tagit-choice a.tagit-label { + cursor: pointer; + text-decoration: none; +} +ul.tagit li.tagit-choice .tagit-close { + cursor: pointer; + position: absolute; + right: .1em; + top: 50%; + margin-top: -8px; + line-height: 17px; +} + +/* used for some custom themes that don't need image icons */ +ul.tagit li.tagit-choice .tagit-close .text-icon { + display: none; +} + +ul.tagit li.tagit-choice input { + display: block; + float: left; + margin: 2px 5px 2px 0; +} +ul.tagit input[type="text"] { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; + + border: none; + margin: 0; + padding: 0; + width: inherit; + background-color: inherit; + outline: none; +} diff --git a/qiita_pet/static/vendor/js/jquery.dataTables.min.js b/qiita_pet/static/vendor/js/jquery.dataTables.min.js index 3248d7490..c02af950f 100644 --- a/qiita_pet/static/vendor/js/jquery.dataTables.min.js +++ b/qiita_pet/static/vendor/js/jquery.dataTables.min.js @@ -1,155 +1,167 @@ -/*! DataTables 1.10.2 - * ©2008-2014 SpryMedia Ltd - datatables.net/license - */ -(function(za,O,l){var N=function(h){function T(a){var b,c,d={};h.each(a,function(e){if((b=e.match(/^([^A-Z]+?)([A-Z])/))&&-1!=="a aa ai ao as b fn i m o s ".indexOf(b[1]+" "))c=e.replace(b[0],b[2].toLowerCase()),d[c]=e,"o"===b[1]&&T(a[e])});a._hungarianMap=d}function G(a,b,c){a._hungarianMap||T(a);var d;h.each(b,function(e){d=a._hungarianMap[e];if(d!==l&&(c||b[d]===l))"o"===d.charAt(0)?(b[d]||(b[d]={}),h.extend(!0,b[d],b[e]),G(a[d],b[d],c)):b[d]=b[e]})}function N(a){var b=p.defaults.oLanguage,c=a.sZeroRecords; -!a.sEmptyTable&&(c&&"No data available in table"===b.sEmptyTable)&&D(a,a,"sZeroRecords","sEmptyTable");!a.sLoadingRecords&&(c&&"Loading..."===b.sLoadingRecords)&&D(a,a,"sZeroRecords","sLoadingRecords");a.sInfoThousands&&(a.sThousands=a.sInfoThousands);(a=a.sDecimal)&&cb(a)}function db(a){w(a,"ordering","bSort");w(a,"orderMulti","bSortMulti");w(a,"orderClasses","bSortClasses");w(a,"orderCellsTop","bSortCellsTop");w(a,"order","aaSorting");w(a,"orderFixed","aaSortingFixed");w(a,"paging","bPaginate"); -w(a,"pagingType","sPaginationType");w(a,"pageLength","iDisplayLength");w(a,"searching","bFilter");if(a=a.aoSearchCols)for(var b=0,c=a.length;b").css({position:"absolute",top:0,left:0,height:1,width:1,overflow:"hidden"}).append(h("
").css({position:"absolute",top:1,left:1,width:100, -overflow:"scroll"}).append(h('
').css({width:"100%",height:10}))).appendTo("body"),c=b.find(".test");a.bScrollOversize=100===c[0].offsetWidth;a.bScrollbarLeft=1!==c.offset().left;b.remove()}function gb(a,b,c,d,e,f){var g,j=!1;c!==l&&(g=c,j=!0);for(;d!==e;)a.hasOwnProperty(d)&&(g=j?b(g,a[d],d,a):a[d],j=!0,d+=f);return g}function Aa(a,b){var c=p.defaults.column,d=a.aoColumns.length,c=h.extend({},p.models.oColumn,c,{nTh:b?b:O.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML: -"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.mData:d,idx:d});a.aoColumns.push(c);c=a.aoPreSearchCols;c[d]=h.extend({},p.models.oSearch,c[d]);fa(a,d,null)}function fa(a,b,c){var b=a.aoColumns[b],d=a.oClasses,e=h(b.nTh);if(!b.sWidthOrig){b.sWidthOrig=e.attr("width")||null;var f=(e.attr("style")||"").match(/width:\s*(\d+[pxem%]+)/);f&&(b.sWidthOrig=f[1])}c!==l&&null!==c&&(eb(c),G(p.defaults.column,c),c.mDataProp!==l&&!c.mData&&(c.mData=c.mDataProp),c.sType&&(b._sManualType=c.sType),c.className&& -!c.sClass&&(c.sClass=c.className),h.extend(b,c),D(b,c,"sWidth","sWidthOrig"),"number"===typeof c.iDataSort&&(b.aDataSort=[c.iDataSort]),D(b,c,"aDataSort"));var g=b.mData,j=U(g),i=b.mRender?U(b.mRender):null,c=function(a){return"string"===typeof a&&-1!==a.indexOf("@")};b._bAttrSrc=h.isPlainObject(g)&&(c(g.sort)||c(g.type)||c(g.filter));b.fnGetData=function(a,b,c){var d=j(a,b,l,c);return i&&b?i(d,b,a,c):d};b.fnSetData=function(a,b,c){return Ba(g)(a,b,c)};a.oFeatures.bSort||(b.bSortable=!1,e.addClass(d.sSortableNone)); -a=-1!==h.inArray("asc",b.asSorting);c=-1!==h.inArray("desc",b.asSorting);!b.bSortable||!a&&!c?(b.sSortingClass=d.sSortableNone,b.sSortingClassJUI=""):a&&!c?(b.sSortingClass=d.sSortableAsc,b.sSortingClassJUI=d.sSortJUIAscAllowed):!a&&c?(b.sSortingClass=d.sSortableDesc,b.sSortingClassJUI=d.sSortJUIDescAllowed):(b.sSortingClass=d.sSortable,b.sSortingClassJUI=d.sSortJUI)}function V(a){if(!1!==a.oFeatures.bAutoWidth){var b=a.aoColumns;Ca(a);for(var c=0,d=b.length;co[f])d(m.length+ -o[f],n);else if("string"===typeof o[f]){j=0;for(i=m.length;jb&&a[e]--; -1!=d&&c===l&&a.splice(d,1)}function la(a,b,c,d){var e=a.aoData[b],f;if("dom"===c||(!c||"auto"===c)&&"dom"===e.src)e._aData= -ia(a,e).data;else{var g=e.anCells,j;if(g){c=0;for(f=g.length;c").appendTo(g));b=0;for(c=m.length;btr").attr("role","row");h(g).find(">tr>th, >tr>td").addClass(n.sHeaderTH);h(j).find(">tr>th, >tr>td").addClass(n.sFooterTH);if(null!==j){a=a.aoFooter[0];b=0;for(c=a.length;b=a.fnRecordsDisplay()?0:g,a.iInitDisplayStart=-1);var g=a._iDisplayStart,n=a.fnDisplayEnd();if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++,B(a,!1);else if(j){if(!a.bDestroying&&!jb(a))return}else a.iDraw++;if(0!==i.length){f=j?a.aoData.length:n;for(j=j?0:g;j",{"class":e?d[0]:""}).append(h("",{valign:"top",colSpan:Z(a),"class":a.oClasses.sRowEmpty}).html(c))[0];u(a,"aoHeaderCallback","header",[h(a.nTHead).children("tr")[0],Ha(a),g,n,i]);u(a,"aoFooterCallback","footer",[h(a.nTFoot).children("tr")[0], -Ha(a),g,n,i]);d=h(a.nTBody);d.children().detach();d.append(h(b));u(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function L(a,b){var c=a.oFeatures,d=c.bFilter;c.bSort&&kb(a);d?ca(a,a.oPreviousSearch):a.aiDisplay=a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;K(a);a._drawHold=!1}function lb(a){var b=a.oClasses,c=h(a.nTable),c=h("
").insertBefore(c),d=a.oFeatures,e=h("
",{id:a.sTableId+"_wrapper","class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)}); -a.nHolding=c[0];a.nTableWrapper=e[0];a.nTableReinsertBefore=a.nTable.nextSibling;for(var f=a.sDom.split(""),g,j,i,n,m,o,k=0;k")[0];n=f[k+1];if("'"==n||'"'==n){m="";for(o=2;f[k+o]!=n;)m+=f[k+o],o++;"H"==m?m=b.sJUIHeader:"F"==m&&(m=b.sJUIFooter);-1!=m.indexOf(".")?(n=m.split("."),i.id=n[0].substr(1,n[0].length-1),i.className=n[1]):"#"==m.charAt(0)?i.id=m.substr(1,m.length-1):i.className=m;k+=o}e.append(i);e=h(i)}else if(">"==j)e=e.parent();else if("l"== -j&&d.bPaginate&&d.bLengthChange)g=mb(a);else if("f"==j&&d.bFilter)g=nb(a);else if("r"==j&&d.bProcessing)g=ob(a);else if("t"==j)g=pb(a);else if("i"==j&&d.bInfo)g=qb(a);else if("p"==j&&d.bPaginate)g=rb(a);else if(0!==p.ext.feature.length){i=p.ext.feature;o=0;for(n=i.length;o',j=d.sSearch,j=j.match(/_INPUT_/)?j.replace("_INPUT_",g):j+g,b=h("
", -{id:!f.f?c+"_filter":null,"class":b.sFilter}).append(h("
").addClass(b.sLength); -a.aanFeatures.l||(i[0].id=c+"_length");i.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",e[0].outerHTML));h("select",i).val(a._iDisplayLength).bind("change.DT",function(){Pa(a,h(this).val());K(a)});h(a.nTable).bind("length.dt.DT",function(b,c,d){a===c&&h("select",i).val(d)});return i[0]}function rb(a){var b=a.sPaginationType,c=p.ext.pager[b],d="function"===typeof c,e=function(a){K(a)},b=h("
").addClass(a.oClasses.sPaging+b)[0],f=a.aanFeatures;d||c.fnInit(a,b,e);f.p||(b.id=a.sTableId+ -"_paginate",a.aoDrawCallback.push({fn:function(a){if(d){var b=a._iDisplayStart,i=a._iDisplayLength,h=a.fnRecordsDisplay(),m=-1===i,b=m?0:Math.ceil(b/i),i=m?1:Math.ceil(h/i),h=c(b,i),o,m=0;for(o=f.p.length;mf&&(d=0)):"first"==b?d=0:"previous"==b?(d=0<=e?d-e:0,0>d&&(d=0)):"next"== -b?d+e",{id:!a.aanFeatures.r?a.sTableId+"_processing":null,"class":a.oClasses.sProcessing}).html(a.oLanguage.sProcessing).insertBefore(a.nTable)[0]}function B(a,b){a.oFeatures.bProcessing&&h(a.aanFeatures.r).css("display",b?"block":"none");u(a,null,"processing",[a,b])}function pb(a){var b=h(a.nTable);b.attr("role", -"grid");var c=a.oScroll;if(""===c.sX&&""===c.sY)return a.nTable;var d=c.sX,e=c.sY,f=a.oClasses,g=b.children("caption"),j=g.length?g[0]._captionSide:null,i=h(b[0].cloneNode(!1)),n=h(b[0].cloneNode(!1)),m=b.children("tfoot");c.sX&&"100%"===b.attr("width")&&b.removeAttr("width");m.length||(m=null);c=h("
",{"class":f.sScrollWrapper}).append(h("
",{"class":f.sScrollHead}).css({overflow:"hidden",position:"relative",border:0,width:d?!d?null:s(d):"100%"}).append(h("
",{"class":f.sScrollHeadInner}).css({"box-sizing":"content-box", -width:c.sXInner||"100%"}).append(i.removeAttr("id").css("margin-left",0).append(b.children("thead")))).append("top"===j?g:null)).append(h("
",{"class":f.sScrollBody}).css({overflow:"auto",height:!e?null:s(e),width:!d?null:s(d)}).append(b));m&&c.append(h("
",{"class":f.sScrollFoot}).css({overflow:"hidden",border:0,width:d?!d?null:s(d):"100%"}).append(h("
",{"class":f.sScrollFootInner}).append(n.removeAttr("id").css("margin-left",0).append(b.children("tfoot")))).append("bottom"===j?g: -null));var b=c.children(),o=b[0],f=b[1],k=m?b[2]:null;d&&h(f).scroll(function(){var a=this.scrollLeft;o.scrollLeft=a;m&&(k.scrollLeft=a)});a.nScrollHead=o;a.nScrollBody=f;a.nScrollFoot=k;a.aoDrawCallback.push({fn:W,sName:"scrolling"});return c[0]}function W(a){var b=a.oScroll,c=b.sX,d=b.sXInner,e=b.sY,f=b.iBarWidth,g=h(a.nScrollHead),j=g[0].style,i=g.children("div"),n=i[0].style,m=i.children("table"),i=a.nScrollBody,o=h(i),k=i.style,l=h(a.nScrollFoot).children("div"),p=l.children("table"),r=h(a.nTHead), -q=h(a.nTable),da=q[0],M=da.style,J=a.nTFoot?h(a.nTFoot):null,u=a.oBrowser,v=u.bScrollOversize,y,t,x,w,z,A=[],B=[],C=[],D,E=function(a){a=a.style;a.paddingTop="0";a.paddingBottom="0";a.borderTopWidth="0";a.borderBottomWidth="0";a.height=0};q.children("thead, tfoot").remove();z=r.clone().prependTo(q);y=r.find("tr");x=z.find("tr");z.find("th, td").removeAttr("tabindex");J&&(w=J.clone().prependTo(q),t=J.find("tr"),w=w.find("tr"));c||(k.width="100%",g[0].style.width="100%");h.each(ma(a,z),function(b,c){D= -ga(a,b);c.style.width=a.aoColumns[D].sWidth});J&&F(function(a){a.style.width=""},w);b.bCollapse&&""!==e&&(k.height=o[0].offsetHeight+r[0].offsetHeight+"px");g=q.outerWidth();if(""===c){if(M.width="100%",v&&(q.find("tbody").height()>i.offsetHeight||"scroll"==o.css("overflow-y")))M.width=s(q.outerWidth()-f)}else""!==d?M.width=s(d):g==o.width()&&o.height()g-f&&(M.width=s(g))):M.width=s(g);g=q.outerWidth();F(E,x);F(function(a){C.push(a.innerHTML);A.push(s(h(a).css("width")))}, -x);F(function(a,b){a.style.width=A[b]},y);h(x).height(0);J&&(F(E,w),F(function(a){B.push(s(h(a).css("width")))},w),F(function(a,b){a.style.width=B[b]},t),h(w).height(0));F(function(a,b){a.innerHTML='
'+C[b]+"
";a.style.width=A[b]},x);J&&F(function(a,b){a.innerHTML="";a.style.width=B[b]},w);if(q.outerWidth()i.offsetHeight||"scroll"==o.css("overflow-y")?g+f:g;if(v&&(i.scrollHeight>i.offsetHeight||"scroll"==o.css("overflow-y")))M.width= -s(t-f);(""===c||""!==d)&&P(a,1,"Possible column misalignment",6)}else t="100%";k.width=s(t);j.width=s(t);J&&(a.nScrollFoot.style.width=s(t));!e&&v&&(k.height=s(da.offsetHeight+f));e&&b.bCollapse&&(k.height=s(e),b=c&&da.offsetWidth>i.offsetWidth?f:0,da.offsetHeighti.clientHeight||"scroll"==o.css("overflow-y");u="padding"+(u.bScrollbarLeft?"Left":"Right");n[u]=m?f+"px":"0px";J&&(p[0].style.width= -s(b),l[0].style.width=s(b),l[0].style[u]=m?f+"px":"0px");o.scroll();if((a.bSorted||a.bFiltered)&&!a._drawHold)i.scrollTop=0}function F(a,b,c){for(var d=0,e=0,f=b.length,g,j;e"));j.find("tfoot th, tfoot td").css("width","");var p=j.find("tbody tr"),i=ma(a,j.find("thead")[0]);for(k=0;k").css("width",s(a)).appendTo(b||O.body),d=c[0].offsetWidth;c.remove();return d}function Eb(a,b){var c= -a.oScroll;if(c.sX||c.sY)c=!c.sX?c.iBarWidth:0,b.style.width=s(h(b).outerWidth()-c)}function Db(a,b){var c=Fb(a,b);if(0>c)return null;var d=a.aoData[c];return!d.nTr?h("").html(A(a,c,b,"display"))[0]:d.anCells[b]}function Fb(a,b){for(var c,d=-1,e=-1,f=0,g=a.aoData.length;fd&&(d=c.length,e=f);return e}function s(a){return null===a?"0px":"number"==typeof a?0>a?"0px":a+"px":a.match(/\d$/)?a+"px":a}function Gb(){if(!p.__scrollbarWidth){var a= -h("

").css({width:"100%",height:200,padding:0})[0],b=h("

").css({position:"absolute",top:0,left:0,width:200,height:150,padding:0,overflow:"hidden",visibility:"hidden"}).append(a).appendTo("body"),c=a.offsetWidth;b.css("overflow","scroll");a=a.offsetWidth;c===a&&(a=b[0].clientWidth);b.remove();p.__scrollbarWidth=c-a}return p.__scrollbarWidth}function R(a){var b,c,d=[],e=a.aoColumns,f,g,j,i;b=a.aaSortingFixed;c=h.isPlainObject(b);var n=[];f=function(a){a.length&&!h.isArray(a[0])?n.push(a):n.push.apply(n, -a)};h.isArray(b)&&f(b);c&&b.pre&&f(b.pre);f(a.aaSorting);c&&b.post&&f(b.post);for(a=0;ae?1:0,0!==c)return"asc"===j.dir?c:-c;c=d[a];e=d[b];return ce?1:0}):i.sort(function(a,b){var c,g,j,i,l=h.length,p=f[a]._aSortData,r=f[b]._aSortData;for(j=0;jg?1:0})}a.bSorted=!0}function Ib(a){for(var b,c, -d=a.aoColumns,e=R(a),a=a.oLanguage.oAria,f=0,g=d.length;f/g,"");var i=c.nTh;i.removeAttribute("aria-sort");c.bSortable&&(0=f.length?0:b+1};"number"===typeof e[0]&&(e=a.aaSorting=[e]);c&&a.oFeatures.bSortMulti?(c=h.inArray(b,C(e,"0")),-1!==c?(b=g(e[c]),e[c][1]=f[b],e[c]._idx=b):(e.push([b,f[0],0]),e[e.length-1]._idx=0)):e.length&&e[0][0]==b?(b=g(e[0]),e.length=1,e[0][1]=f[b],e[0]._idx=b):(e.length=0,e.push([b,f[0]]),e[0]._idx=0);L(a);"function"==typeof d&&d(a)}function Ka(a,b,c,d){var e=a.aoColumns[c];Ta(b,{},function(b){!1!==e.bSortable&&(a.oFeatures.bProcessing?(B(a,!0),setTimeout(function(){Sa(a,c,b.shiftKey, -d);"ssp"!==z(a)&&B(a,!1)},0)):Sa(a,c,b.shiftKey,d))})}function sa(a){var b=a.aLastSort,c=a.oClasses.sSortColumn,d=R(a),e=a.oFeatures,f,g;if(e.bSort&&e.bSortClasses){e=0;for(f=b.length;ee?e+1:3));e=0;for(f=d.length;ee?e+1:3))}a.aLastSort=d}function Hb(a,b){var c=a.aoColumns[b],d=p.ext.order[c.sSortDataType],e;d&&(e=d.call(a.oInstance,a,b,Y(a,b)));for(var f,g=p.ext.type.order[c.sType+ -"-pre"],j=0,i=a.aoData.length;j= -d.length?[0,c[1]]:c)});h.extend(a.oPreviousSearch,zb(e.search));b=0;for(c=e.columns.length;bb)b=0;a._iDisplayStart=b}function La(a,b){var c= -a.renderer,d=p.ext.renderer[b];return h.isPlainObject(c)&&c[b]?d[c[b]]||d._:"string"===typeof c?d[c]||d._:d._}function z(a){return a.oFeatures.bServerSide?"ssp":a.ajax||a.sAjaxSource?"ajax":"dom"}function Ua(a,b){var c=[],c=Lb.numbers_length,d=Math.floor(c/2);b<=c?c=S(0,b):a<=d?(c=S(0,c-2),c.push("ellipsis"),c.push(b-1)):(a>=b-1-d?c=S(b-(c-2),b):(c=S(a-1,a+2),c.push("ellipsis"),c.push(b-1)),c.splice(0,0,"ellipsis"),c.splice(0,0,0));c.DT_el="span";return c}function cb(a){h.each({num:function(b){return va(b, -a)},"num-fmt":function(b){return va(b,a,Va)},"html-num":function(b){return va(b,a,wa)},"html-num-fmt":function(b){return va(b,a,wa,Va)}},function(b,c){t.type.order[b+a+"-pre"]=c})}function Mb(a){return function(){var b=[ua(this[p.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return p.ext.internal[a].apply(this,b)}}var p,t,q,r,v,Wa={},Nb=/[\r\n]/g,wa=/<.*?>/g,Yb=/^[\w\+\-]/,Zb=/[\w\+\-]$/,Vb=RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\^|\\-)","g"),Va=/[',$\u00a3\u20ac\u00a5%\u2009\u202F]/g, -H=function(a){return!a||!0===a||"-"===a?!0:!1},Ob=function(a){var b=parseInt(a,10);return!isNaN(b)&&isFinite(a)?b:null},Pb=function(a,b){Wa[b]||(Wa[b]=RegExp(Oa(b),"g"));return"string"===typeof a?a.replace(/\./g,"").replace(Wa[b],"."):a},Xa=function(a,b,c){var d="string"===typeof a;b&&d&&(a=Pb(a,b));c&&d&&(a=a.replace(Va,""));return H(a)||!isNaN(parseFloat(a))&&isFinite(a)},Qb=function(a,b,c){return H(a)?!0:!(H(a)||"string"===typeof a)?null:Xa(a.replace(wa,""),b,c)?!0:null},C=function(a,b,c){var d= -[],e=0,f=a.length;if(c!==l)for(;e")[0],Wb=qa.textContent!==l,Xb=/<.*?>/g;p=function(a){this.$=function(a,b){return this.api(!0).$(a,b)};this._=function(a,b){return this.api(!0).rows(a,b).data()};this.api=function(a){return a?new q(ua(this[t.iApiIndex])):new q(this)};this.fnAddData=function(a,b){var c=this.api(!0),d=h.isArray(a)&&(h.isArray(a[0])||h.isPlainObject(a[0]))?c.rows.add(a):c.row.add(a);(b===l||b)&&c.draw();return d.flatten().toArray()};this.fnAdjustColumnSizing= -function(a){var b=this.api(!0).columns.adjust(),c=b.settings()[0],d=c.oScroll;a===l||a?b.draw(!1):(""!==d.sX||""!==d.sY)&&W(c)};this.fnClearTable=function(a){var b=this.api(!0).clear();(a===l||a)&&b.draw()};this.fnClose=function(a){this.api(!0).row(a).child.hide()};this.fnDeleteRow=function(a,b,c){var d=this.api(!0),a=d.rows(a),e=a.settings()[0],h=e.aoData[a[0][0]];a.remove();b&&b.call(this,e,h);(c===l||c)&&d.draw();return h};this.fnDestroy=function(a){this.api(!0).destroy(a)};this.fnDraw=function(a){this.api(!0).draw(!a)}; -this.fnFilter=function(a,b,c,d,e,h){e=this.api(!0);null===b||b===l?e.search(a,c,d,h):e.column(b).search(a,c,d,h);e.draw()};this.fnGetData=function(a,b){var c=this.api(!0);if(a!==l){var d=a.nodeName?a.nodeName.toLowerCase():"";return b!==l||"td"==d||"th"==d?c.cell(a,b).data():c.row(a).data()||null}return c.data().toArray()};this.fnGetNodes=function(a){var b=this.api(!0);return a!==l?b.row(a).node():b.rows().nodes().flatten().toArray()};this.fnGetPosition=function(a){var b=this.api(!0),c=a.nodeName.toUpperCase(); -return"TR"==c?b.row(a).index():"TD"==c||"TH"==c?(a=b.cell(a).index(),[a.row,a.columnVisible,a.column]):null};this.fnIsOpen=function(a){return this.api(!0).row(a).child.isShown()};this.fnOpen=function(a,b,c){return this.api(!0).row(a).child(b,c).show().child()[0]};this.fnPageChange=function(a,b){var c=this.api(!0).page(a);(b===l||b)&&c.draw(!1)};this.fnSetColumnVis=function(a,b,c){a=this.api(!0).column(a).visible(b);(c===l||c)&&a.columns.adjust().draw()};this.fnSettings=function(){return ua(this[t.iApiIndex])}; -this.fnSort=function(a){this.api(!0).order(a).draw()};this.fnSortListener=function(a,b,c){this.api(!0).order.listener(a,b,c)};this.fnUpdate=function(a,b,c,d,e){var h=this.api(!0);c===l||null===c?h.row(b).data(a):h.cell(b,c).data(a);(e===l||e)&&h.columns.adjust();(d===l||d)&&h.draw();return 0};this.fnVersionCheck=t.fnVersionCheck;var b=this,c=a===l,d=this.length;c&&(a={});this.oApi=this.internal=t.internal;for(var e in p.ext.internal)e&&(this[e]=Mb(e));this.each(function(){var e={},g=1t<"F"ip>'),k.renderer)?h.isPlainObject(k.renderer)&&!k.renderer.header&&(k.renderer.header="jqueryui"):k.renderer="jqueryui":h.extend(n,p.ext.classes,g.oClasses);h(this).addClass(n.sTable);if(""!==k.oScroll.sX||""!==k.oScroll.sY)k.oScroll.iBarWidth=Gb();!0===k.oScroll.sX&&(k.oScroll.sX= -"100%");k.iInitDisplayStart===l&&(k.iInitDisplayStart=g.iDisplayStart,k._iDisplayStart=g.iDisplayStart);null!==g.iDeferLoading&&(k.bDeferLoading=!0,j=h.isArray(g.iDeferLoading),k._iRecordsDisplay=j?g.iDeferLoading[0]:g.iDeferLoading,k._iRecordsTotal=j?g.iDeferLoading[1]:g.iDeferLoading);""!==g.oLanguage.sUrl?(k.oLanguage.sUrl=g.oLanguage.sUrl,h.getJSON(k.oLanguage.sUrl,null,function(a){N(a);G(m.oLanguage,a);h.extend(true,k.oLanguage,g.oLanguage,a);ra(k)}),e=!0):h.extend(!0,k.oLanguage,g.oLanguage); -null===g.asStripeClasses&&(k.asStripeClasses=[n.sStripeOdd,n.sStripeEven]);var j=k.asStripeClasses,r=h("tbody tr:eq(0)",this);-1!==h.inArray(!0,h.map(j,function(a){return r.hasClass(a)}))&&(h("tbody tr",this).removeClass(j.join(" ")),k.asDestroyStripes=j.slice());var o=[],q,j=this.getElementsByTagName("thead");0!==j.length&&(aa(k.aoHeader,j[0]),o=ma(k));if(null===g.aoColumns){q=[];j=0;for(i=o.length;j").appendTo(this));k.nTHead= -i[0];i=h(this).children("tbody");0===i.length&&(i=h("").appendTo(this));k.nTBody=i[0];i=h(this).children("tfoot");if(0===i.length&&0").appendTo(this);0===i.length||0===i.children().length?h(this).addClass(n.sNoFooter):0a?new q(b[a],this[a]):null},filter:function(a){var b=[];if(y.filter)b=y.filter.call(this,a,this);else for(var c=0,d=this.length;c").addClass(b);h("td",c).addClass(b).html(a)[0].colSpan=Z(d);e.push(c[0])}};if(h.isArray(a)|| -a instanceof h)for(var g=0,j=a.length;g=0?c:f.length+c];var e=typeof a==="string"?a.match(ac):"";if(e)switch(e[2]){case "visIdx":case "visible":a=parseInt(e[1],10);if(a<0){c=h.map(f,function(a, -b){return a.bVisible?b:null});return[c[c.length+a]]}return[ga(b,a)];case "name":return h.map(g,function(a,b){return a===e[1]?b:null})}else return h(j).filter(a).map(function(){return h.inArray(this,j)}).toArray()})});c.selector.cols=a;c.selector.opts=b;return c});v("columns().header()","column().header()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTh})});v("columns().footer()","column().footer()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTf})}); -v("columns().data()","column().data()",function(){return this.iterator("column-rows",function(a,b,c,d,e){for(var c=[],d=0,f=e.length;dd;return!0};p.isDataTable=p.fnIsDataTable=function(a){var b=h(a).get(0),c=!1;h.each(p.settings,function(a,e){if(e.nTable===b||e.nScrollHead=== -b||e.nScrollFoot===b)c=!0});return c};p.tables=p.fnTables=function(a){return jQuery.map(p.settings,function(b){if(!a||a&&h(b.nTable).is(":visible"))return b.nTable})};p.camelToHungarian=G;r("$()",function(a,b){var c=this.rows(b).nodes(),c=h(c);return h([].concat(c.filter(a).toArray(),c.find(a).toArray()))});h.each(["on","one","off"],function(a,b){r(b+"()",function(){var a=Array.prototype.slice.call(arguments);a[0].match(/\.dt\b/)||(a[0]+=".dt");var d=h(this.tables().nodes());d[b].apply(d,a);return this})}); -r("clear()",function(){return this.iterator("table",function(a){ja(a)})});r("settings()",function(){return new q(this.context,this.context)});r("data()",function(){return this.iterator("table",function(a){return C(a.aoData,"_aData")}).flatten()});r("destroy()",function(a){a=a||!1;return this.iterator("table",function(b){var c=b.nTableWrapper.parentNode,d=b.oClasses,e=b.nTable,f=b.nTBody,g=b.nTHead,j=b.nTFoot,i=h(e),f=h(f),l=h(b.nTableWrapper),m=h.map(b.aoData,function(a){return a.nTr}),o;b.bDestroying= -!0;u(b,"aoDestroyCallback","destroy",[b]);a||(new q(b)).columns().visible(!0);l.unbind(".DT").find(":not(tbody *)").unbind(".DT");h(za).unbind(".DT-"+b.sInstance);e!=g.parentNode&&(i.children("thead").detach(),i.append(g));j&&e!=j.parentNode&&(i.children("tfoot").detach(),i.append(j));i.detach();l.detach();b.aaSorting=[];b.aaSortingFixed=[];sa(b);h(m).removeClass(b.asStripeClasses.join(" "));h("th, td",g).removeClass(d.sSortable+" "+d.sSortableAsc+" "+d.sSortableDesc+" "+d.sSortableNone);b.bJUI&& -(h("th span."+d.sSortIcon+", td span."+d.sSortIcon,g).detach(),h("th, td",g).each(function(){var a=h("div."+d.sSortJUIWrapper,this);h(this).append(a.contents());a.detach()}));!a&&c&&c.insertBefore(e,b.nTableReinsertBefore);f.children().detach();f.append(m);i.css("width",b.sDestroyWidth).removeClass(d.sTable);(o=b.asDestroyStripes.length)&&f.children().each(function(a){h(this).addClass(b.asDestroyStripes[a%o])});c=h.inArray(b,p.settings);-1!==c&&p.settings.splice(c,1)})});p.version="1.10.2";p.settings= -[];p.models={};p.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0};p.models.oRow={nTr:null,anCells:null,_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,_sRowStripe:"",src:null};p.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null,sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std", -sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null,sWidthOrig:null};p.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bJQueryUI:!1,bLengthChange:!0,bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1,bSort:!0,bSortMulti:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1, -fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(a){return a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null,fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnServerData:null,fnServerParams:null,fnStateLoadCallback:function(a){try{return JSON.parse((-1===a.iStateDuration?sessionStorage:localStorage).getItem("DataTables_"+a.sInstance+"_"+location.pathname))}catch(b){}},fnStateLoadParams:null, -fnStateLoaded:null,fnStateSaveCallback:function(a,b){try{(-1===a.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+a.sInstance+"_"+location.pathname,JSON.stringify(b))}catch(c){}},fnStateSaveParams:null,iStateDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",sSortDescending:": activate to sort column descending"},oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"}, -sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries",sInfoFiltered:"(filtered from _MAX_ total entries)",sInfoPostFix:"",sDecimal:"",sThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"},oSearch:h.extend({},p.models.oSearch),sAjaxDataProp:"data",sAjaxSource:null,sDom:"lfrtip",sPaginationType:"simple_numbers", -sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",renderer:null};T(p.defaults);p.defaults.column={aDataSort:null,iDataSort:-1,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null};T(p.defaults.column);p.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null, -bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null,bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1,bScrollbarLeft:!1},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aoColumns:[],aoHeader:[],aoFooter:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[], -aoHeaderCallback:[],aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,sPaginationType:"two_button",iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,json:l,oAjaxData:l, -fnServerData:null,aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,bJUI:null,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==z(this)?1*this._iRecordsTotal:this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==z(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a= -this._iDisplayLength,b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,e=this.oFeatures,f=e.bPaginate;return e.bServerSide?!1===f||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!f||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{}};p.ext=t={classes:{},errMode:"alert",feature:[],search:[],internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}},order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:p.fnVersionCheck, -iApiIndex:0,oJUIClasses:{},sVersion:p.version};h.extend(t,{afnFiltering:t.search,aTypes:t.type.detect,ofnSearch:t.type.search,oSort:t.type.order,afnSortData:t.order,aoFeatures:t.feature,oApi:t.internal,oStdClasses:t.classes,oPagination:t.pager});h.extend(p.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd",sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter", -sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_asc_disabled",sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody", -sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"",sJUIHeader:"",sJUIFooter:""});var ya="",ya="",E=ya+"ui-state-default",ea=ya+"css_right ui-icon ui-icon-",Ub=ya+"fg-toolbar ui-toolbar ui-widget-header ui-helper-clearfix";h.extend(p.ext.oJUIClasses,p.ext.classes,{sPageButton:"fg-button ui-button "+E,sPageButtonActive:"ui-state-disabled", -sPageButtonDisabled:"ui-state-disabled",sPaging:"dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi ui-buttonset-multi paging_",sSortAsc:E+" sorting_asc",sSortDesc:E+" sorting_desc",sSortable:E+" sorting",sSortableAsc:E+" sorting_asc_disabled",sSortableDesc:E+" sorting_desc_disabled",sSortableNone:E+" sorting_disabled",sSortJUIAsc:ea+"triangle-1-n",sSortJUIDesc:ea+"triangle-1-s",sSortJUI:ea+"carat-2-n-s",sSortJUIAscAllowed:ea+"carat-1-n",sSortJUIDescAllowed:ea+"carat-1-s",sSortJUIWrapper:"DataTables_sort_wrapper", -sSortIcon:"DataTables_sort_icon",sScrollHead:"dataTables_scrollHead "+E,sScrollFoot:"dataTables_scrollFoot "+E,sHeaderTH:E,sFooterTH:E,sJUIHeader:Ub+" ui-corner-tl ui-corner-tr",sJUIFooter:Ub+" ui-corner-bl ui-corner-br"});var Lb=p.ext.pager;h.extend(Lb,{simple:function(){return["previous","next"]},full:function(){return["first","previous","next","last"]},simple_numbers:function(a,b){return["previous",Ua(a,b),"next"]},full_numbers:function(a,b){return["first","previous",Ua(a,b),"next","last"]},_numbers:Ua, -numbers_length:7});h.extend(!0,p.ext.renderer,{pageButton:{_:function(a,b,c,d,e,f){var g=a.oClasses,j=a.oLanguage.oPaginate,i,l,m=0,o=function(b,d){var k,p,r,q,s=function(b){Ra(a,b.data.action,true)};k=0;for(p=d.length;k").appendTo(b);o(r,q)}else{l=i="";switch(q){case "ellipsis":b.append("");break;case "first":i=j.sFirst;l=q+(e>0?"":" "+g.sPageButtonDisabled);break;case "previous":i=j.sPrevious;l=q+(e>0?"":" "+g.sPageButtonDisabled); -break;case "next":i=j.sNext;l=q+(e",{"class":g.sPageButton+" "+l,"aria-controls":a.sTableId,"data-dt-idx":m,tabindex:a.iTabIndex,id:c===0&&typeof q==="string"?a.sTableId+"_"+q:null}).html(i).appendTo(b);Ta(r,{action:q},s);m++}}}};try{var k=h(O.activeElement).data("dt-idx");o(h(b).empty(),d);k!==null&&h(b).find("[data-dt-idx="+k+"]").focus()}catch(p){}}}}); -var va=function(a,b,c,d){if(!a||"-"===a)return-Infinity;b&&(a=Pb(a,b));a.replace&&(c&&(a=a.replace(c,"")),d&&(a=a.replace(d,"")));return 1*a};h.extend(t.type.order,{"date-pre":function(a){return Date.parse(a)||0},"html-pre":function(a){return H(a)?"":a.replace?a.replace(/<.*?>/g,"").toLowerCase():a+""},"string-pre":function(a){return H(a)?"":"string"===typeof a?a.toLowerCase():!a.toString?"":a.toString()},"string-asc":function(a,b){return ab?1:0},"string-desc":function(a,b){return a -b?-1:0}});cb("");h.extend(p.ext.type.detect,[function(a,b){var c=b.oLanguage.sDecimal;return Xa(a,c)?"num"+c:null},function(a){if(a&&(!Yb.test(a)||!Zb.test(a)))return null;var b=Date.parse(a);return null!==b&&!isNaN(b)||H(a)?"date":null},function(a,b){var c=b.oLanguage.sDecimal;return Xa(a,c,!0)?"num-fmt"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Qb(a,c)?"html-num"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Qb(a,c,!0)?"html-num-fmt"+c:null},function(a){return H(a)||"string"=== -typeof a&&-1!==a.indexOf("<")?"html":null}]);h.extend(p.ext.type.search,{html:function(a){return H(a)?a:"string"===typeof a?a.replace(Nb," ").replace(wa,""):""},string:function(a){return H(a)?a:"string"===typeof a?a.replace(Nb," "):a}});h.extend(!0,p.ext.renderer,{header:{_:function(a,b,c,d){h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(c.sSortingClass+" "+d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass)}})},jqueryui:function(a, -b,c,d){var e=c.idx;h("
").addClass(d.sSortJUIWrapper).append(b.contents()).append(h("").addClass(d.sSortIcon+" "+c.sSortingClassJUI)).appendTo(b);h(a.nTable).on("order.dt.DT",function(f,g,h,i){if(a===g){b.removeClass(d.sSortAsc+" "+d.sSortDesc).addClass(i[e]=="asc"?d.sSortAsc:i[e]=="desc"?d.sSortDesc:c.sSortingClass);b.find("span."+d.sSortIcon).removeClass(d.sSortJUIAsc+" "+d.sSortJUIDesc+" "+d.sSortJUI+" "+d.sSortJUIAscAllowed+" "+d.sSortJUIDescAllowed).addClass(i[e]=="asc"?d.sSortJUIAsc: -i[e]=="desc"?d.sSortJUIDesc:c.sSortingClassJUI)}})}}});p.render={number:function(a,b,c,d){return{display:function(e){var f=0>e?"-":"",e=Math.abs(parseFloat(e)),g=parseInt(e,10),e=c?b+(e-g).toFixed(c).substring(2):"";return f+(d||"")+g.toString().replace(/\B(?=(\d{3})+(?!\d))/g,a)+e}}}};h.extend(p.ext.internal,{_fnExternApiFunc:Mb,_fnBuildAjax:na,_fnAjaxUpdate:jb,_fnAjaxParameters:sb,_fnAjaxUpdateDraw:tb,_fnAjaxDataSrc:oa,_fnAddColumn:Aa,_fnColumnOptions:fa,_fnAdjustColumnSizing:V,_fnVisibleToColumnIndex:ga, -_fnColumnIndexToVisible:Y,_fnVisbleColumns:Z,_fnGetColumns:X,_fnColumnTypes:Da,_fnApplyColumnDefs:hb,_fnHungarianMap:T,_fnCamelToHungarian:G,_fnLanguageCompat:N,_fnBrowserDetect:fb,_fnAddData:I,_fnAddTr:ha,_fnNodeToDataIndex:function(a,b){return b._DT_RowIndex!==l?b._DT_RowIndex:null},_fnNodeToColumnIndex:function(a,b,c){return h.inArray(c,a.aoData[b].anCells)},_fnGetCellData:A,_fnSetCellData:Ea,_fnSplitObjNotation:Ga,_fnGetObjectDataFn:U,_fnSetObjectDataFn:Ba,_fnGetDataMaster:Ha,_fnClearTable:ja, -_fnDeleteIndex:ka,_fnInvalidateRow:la,_fnGetRowElements:ia,_fnCreateTr:Fa,_fnBuildHead:ib,_fnDrawHead:ba,_fnDraw:K,_fnReDraw:L,_fnAddOptionsHtml:lb,_fnDetectHeader:aa,_fnGetUniqueThs:ma,_fnFeatureHtmlFilter:nb,_fnFilterComplete:ca,_fnFilterCustom:wb,_fnFilterColumn:vb,_fnFilter:ub,_fnFilterCreateSearch:Na,_fnEscapeRegex:Oa,_fnFilterData:xb,_fnFeatureHtmlInfo:qb,_fnUpdateInfo:Ab,_fnInfoMacros:Bb,_fnInitialise:ra,_fnInitComplete:pa,_fnLengthChange:Pa,_fnFeatureHtmlLength:mb,_fnFeatureHtmlPaginate:rb, -_fnPageChange:Ra,_fnFeatureHtmlProcessing:ob,_fnProcessingDisplay:B,_fnFeatureHtmlTable:pb,_fnScrollDraw:W,_fnApplyToChildren:F,_fnCalculateColumnWidths:Ca,_fnThrottle:Ma,_fnConvertToWidth:Cb,_fnScrollingWidthAdjust:Eb,_fnGetWidestNode:Db,_fnGetMaxLenString:Fb,_fnStringToCss:s,_fnScrollBarWidth:Gb,_fnSortFlatten:R,_fnSort:kb,_fnSortAria:Ib,_fnSortListener:Sa,_fnSortAttachListener:Ka,_fnSortingClasses:sa,_fnSortData:Hb,_fnSaveState:ta,_fnLoadState:Jb,_fnSettingsFromNode:ua,_fnLog:P,_fnMap:D,_fnBindAction:Ta, -_fnCallbackReg:x,_fnCallbackFire:u,_fnLengthOverflow:Qa,_fnRenderer:La,_fnDataSource:z,_fnRowAttributes:Ia,_fnCalculateEnd:function(){}});h.fn.dataTable=p;h.fn.dataTableSettings=p.settings;h.fn.dataTableExt=p.ext;h.fn.DataTable=function(a){return h(this).dataTable(a).api()};h.each(p,function(a,b){h.fn.DataTable[a]=b});return h.fn.dataTable};"function"===typeof define&&define.amd?define("datatables",["jquery"],N):"object"===typeof exports?N(require("jquery")):jQuery&&!jQuery.fn.dataTable&&N(jQuery)})(window, -document); +/*! + DataTables 1.10.13 + ©2008-2016 SpryMedia Ltd - datatables.net/license +*/ +(function(h){"function"===typeof define&&define.amd?define(["jquery"],function(E){return h(E,window,document)}):"object"===typeof exports?module.exports=function(E,H){E||(E=window);H||(H="undefined"!==typeof window?require("jquery"):require("jquery")(E));return h(H,E,E.document)}:h(jQuery,window,document)})(function(h,E,H,k){function Y(a){var b,c,d={};h.each(a,function(e){if((b=e.match(/^([^A-Z]+?)([A-Z])/))&&-1!=="a aa ai ao as b fn i m o s ".indexOf(b[1]+" "))c=e.replace(b[0],b[2].toLowerCase()), +d[c]=e,"o"===b[1]&&Y(a[e])});a._hungarianMap=d}function J(a,b,c){a._hungarianMap||Y(a);var d;h.each(b,function(e){d=a._hungarianMap[e];if(d!==k&&(c||b[d]===k))"o"===d.charAt(0)?(b[d]||(b[d]={}),h.extend(!0,b[d],b[e]),J(a[d],b[d],c)):b[d]=b[e]})}function Fa(a){var b=m.defaults.oLanguage,c=a.sZeroRecords;!a.sEmptyTable&&(c&&"No data available in table"===b.sEmptyTable)&&F(a,a,"sZeroRecords","sEmptyTable");!a.sLoadingRecords&&(c&&"Loading..."===b.sLoadingRecords)&&F(a,a,"sZeroRecords","sLoadingRecords"); +a.sInfoThousands&&(a.sThousands=a.sInfoThousands);(a=a.sDecimal)&&fb(a)}function gb(a){A(a,"ordering","bSort");A(a,"orderMulti","bSortMulti");A(a,"orderClasses","bSortClasses");A(a,"orderCellsTop","bSortCellsTop");A(a,"order","aaSorting");A(a,"orderFixed","aaSortingFixed");A(a,"paging","bPaginate");A(a,"pagingType","sPaginationType");A(a,"pageLength","iDisplayLength");A(a,"searching","bFilter");"boolean"===typeof a.sScrollX&&(a.sScrollX=a.sScrollX?"100%":"");"boolean"===typeof a.scrollX&&(a.scrollX= +a.scrollX?"100%":"");if(a=a.aoSearchCols)for(var b=0,c=a.length;b").css({position:"fixed",top:0,left:-1*h(E).scrollLeft(),height:1,width:1,overflow:"hidden"}).append(h("
").css({position:"absolute", +top:1,left:1,width:100,overflow:"scroll"}).append(h("
").css({width:"100%",height:10}))).appendTo("body"),d=c.children(),e=d.children();b.barWidth=d[0].offsetWidth-d[0].clientWidth;b.bScrollOversize=100===e[0].offsetWidth&&100!==d[0].clientWidth;b.bScrollbarLeft=1!==Math.round(e.offset().left);b.bBounding=c[0].getBoundingClientRect().width?!0:!1;c.remove()}h.extend(a.oBrowser,m.__browser);a.oScroll.iBarWidth=m.__browser.barWidth}function jb(a,b,c,d,e,f){var g,j=!1;c!==k&&(g=c,j=!0);for(;d!== +e;)a.hasOwnProperty(d)&&(g=j?b(g,a[d],d,a):a[d],j=!0,d+=f);return g}function Ga(a,b){var c=m.defaults.column,d=a.aoColumns.length,c=h.extend({},m.models.oColumn,c,{nTh:b?b:H.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.mData:d,idx:d});a.aoColumns.push(c);c=a.aoPreSearchCols;c[d]=h.extend({},m.models.oSearch,c[d]);la(a,d,h(b).data())}function la(a,b,c){var b=a.aoColumns[b],d=a.oClasses,e=h(b.nTh);if(!b.sWidthOrig){b.sWidthOrig= +e.attr("width")||null;var f=(e.attr("style")||"").match(/width:\s*(\d+[pxem%]+)/);f&&(b.sWidthOrig=f[1])}c!==k&&null!==c&&(hb(c),J(m.defaults.column,c),c.mDataProp!==k&&!c.mData&&(c.mData=c.mDataProp),c.sType&&(b._sManualType=c.sType),c.className&&!c.sClass&&(c.sClass=c.className),h.extend(b,c),F(b,c,"sWidth","sWidthOrig"),c.iDataSort!==k&&(b.aDataSort=[c.iDataSort]),F(b,c,"aDataSort"));var g=b.mData,j=R(g),i=b.mRender?R(b.mRender):null,c=function(a){return"string"===typeof a&&-1!==a.indexOf("@")}; +b._bAttrSrc=h.isPlainObject(g)&&(c(g.sort)||c(g.type)||c(g.filter));b._setter=null;b.fnGetData=function(a,b,c){var d=j(a,b,k,c);return i&&b?i(d,b,a,c):d};b.fnSetData=function(a,b,c){return S(g)(a,b,c)};"number"!==typeof g&&(a._rowReadObject=!0);a.oFeatures.bSort||(b.bSortable=!1,e.addClass(d.sSortableNone));a=-1!==h.inArray("asc",b.asSorting);c=-1!==h.inArray("desc",b.asSorting);!b.bSortable||!a&&!c?(b.sSortingClass=d.sSortableNone,b.sSortingClassJUI=""):a&&!c?(b.sSortingClass=d.sSortableAsc,b.sSortingClassJUI= +d.sSortJUIAscAllowed):!a&&c?(b.sSortingClass=d.sSortableDesc,b.sSortingClassJUI=d.sSortJUIDescAllowed):(b.sSortingClass=d.sSortable,b.sSortingClassJUI=d.sSortJUI)}function Z(a){if(!1!==a.oFeatures.bAutoWidth){var b=a.aoColumns;Ha(a);for(var c=0,d=b.length;cq[f])d(l.length+q[f],n);else if("string"===typeof q[f]){j=0;for(i=l.length;jb&&a[e]--; -1!=d&&c===k&&a.splice(d,1)}function da(a,b,c,d){var e=a.aoData[b],f,g=function(c,d){for(;c.childNodes.length;)c.removeChild(c.firstChild); +c.innerHTML=B(a,b,d,"display")};if("dom"===c||(!c||"auto"===c)&&"dom"===e.src)e._aData=Ka(a,e,d,d===k?k:e._aData).data;else{var j=e.anCells;if(j)if(d!==k)g(j[d],d);else{c=0;for(f=j.length;c").appendTo(g));b=0;for(c=l.length;btr").attr("role","row");h(g).find(">tr>th, >tr>td").addClass(n.sHeaderTH);h(j).find(">tr>th, >tr>td").addClass(n.sFooterTH); +if(null!==j){a=a.aoFooter[0];b=0;for(c=a.length;b=a.fnRecordsDisplay()?0:g,a.iInitDisplayStart= +-1);var g=a._iDisplayStart,n=a.fnDisplayEnd();if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++,C(a,!1);else if(j){if(!a.bDestroying&&!nb(a))return}else a.iDraw++;if(0!==i.length){f=j?a.aoData.length:n;for(j=j?0:g;j",{"class":e?d[0]:""}).append(h("",{valign:"top",colSpan:ba(a),"class":a.oClasses.sRowEmpty}).html(c))[0];s(a,"aoHeaderCallback","header",[h(a.nTHead).children("tr")[0],Ma(a),g,n,i]);s(a,"aoFooterCallback","footer",[h(a.nTFoot).children("tr")[0],Ma(a),g,n,i]);d=h(a.nTBody);d.children().detach();d.append(h(b));s(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function T(a,b){var c=a.oFeatures,d=c.bFilter; +c.bSort&&ob(a);d?ga(a,a.oPreviousSearch):a.aiDisplay=a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;O(a);a._drawHold=!1}function pb(a){var b=a.oClasses,c=h(a.nTable),c=h("
").insertBefore(c),d=a.oFeatures,e=h("
",{id:a.sTableId+"_wrapper","class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)});a.nHolding=c[0];a.nTableWrapper=e[0];a.nTableReinsertBefore=a.nTable.nextSibling;for(var f=a.sDom.split(""),g,j,i,n,l,q,k=0;k")[0]; +n=f[k+1];if("'"==n||'"'==n){l="";for(q=2;f[k+q]!=n;)l+=f[k+q],q++;"H"==l?l=b.sJUIHeader:"F"==l&&(l=b.sJUIFooter);-1!=l.indexOf(".")?(n=l.split("."),i.id=n[0].substr(1,n[0].length-1),i.className=n[1]):"#"==l.charAt(0)?i.id=l.substr(1,l.length-1):i.className=l;k+=q}e.append(i);e=h(i)}else if(">"==j)e=e.parent();else if("l"==j&&d.bPaginate&&d.bLengthChange)g=qb(a);else if("f"==j&&d.bFilter)g=rb(a);else if("r"==j&&d.bProcessing)g=sb(a);else if("t"==j)g=tb(a);else if("i"==j&&d.bInfo)g=ub(a);else if("p"== +j&&d.bPaginate)g=vb(a);else if(0!==m.ext.feature.length){i=m.ext.feature;q=0;for(n=i.length;q',j=d.sSearch,j=j.match(/_INPUT_/)?j.replace("_INPUT_",g):j+g,b=h("
",{id:!f.f?c+"_filter":null,"class":b.sFilter}).append(h("
").addClass(b.sLength);a.aanFeatures.l||(i[0].id=c+"_length");i.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",e[0].outerHTML));h("select",i).val(a._iDisplayLength).on("change.DT",function(){Ta(a,h(this).val());O(a)});h(a.nTable).on("length.dt.DT",function(b,c,d){a===c&&h("select",i).val(d)});return i[0]}function vb(a){var b=a.sPaginationType,c=m.ext.pager[b],d="function"===typeof c,e=function(a){O(a)},b=h("
").addClass(a.oClasses.sPaging+ +b)[0],f=a.aanFeatures;d||c.fnInit(a,b,e);f.p||(b.id=a.sTableId+"_paginate",a.aoDrawCallback.push({fn:function(a){if(d){var b=a._iDisplayStart,i=a._iDisplayLength,h=a.fnRecordsDisplay(),l=-1===i,b=l?0:Math.ceil(b/i),i=l?1:Math.ceil(h/i),h=c(b,i),k,l=0;for(k=f.p.length;lf&& +(d=0)):"first"==b?d=0:"previous"==b?(d=0<=e?d-e:0,0>d&&(d=0)):"next"==b?d+e",{id:!a.aanFeatures.r?a.sTableId+"_processing":null,"class":a.oClasses.sProcessing}).html(a.oLanguage.sProcessing).insertBefore(a.nTable)[0]}function C(a,b){a.oFeatures.bProcessing&&h(a.aanFeatures.r).css("display",b?"block":"none"); +s(a,null,"processing",[a,b])}function tb(a){var b=h(a.nTable);b.attr("role","grid");var c=a.oScroll;if(""===c.sX&&""===c.sY)return a.nTable;var d=c.sX,e=c.sY,f=a.oClasses,g=b.children("caption"),j=g.length?g[0]._captionSide:null,i=h(b[0].cloneNode(!1)),n=h(b[0].cloneNode(!1)),l=b.children("tfoot");l.length||(l=null);i=h("
",{"class":f.sScrollWrapper}).append(h("
",{"class":f.sScrollHead}).css({overflow:"hidden",position:"relative",border:0,width:d?!d?null:v(d):"100%"}).append(h("
", +{"class":f.sScrollHeadInner}).css({"box-sizing":"content-box",width:c.sXInner||"100%"}).append(i.removeAttr("id").css("margin-left",0).append("top"===j?g:null).append(b.children("thead"))))).append(h("
",{"class":f.sScrollBody}).css({position:"relative",overflow:"auto",width:!d?null:v(d)}).append(b));l&&i.append(h("
",{"class":f.sScrollFoot}).css({overflow:"hidden",border:0,width:d?!d?null:v(d):"100%"}).append(h("
",{"class":f.sScrollFootInner}).append(n.removeAttr("id").css("margin-left", +0).append("bottom"===j?g:null).append(b.children("tfoot")))));var b=i.children(),k=b[0],f=b[1],r=l?b[2]:null;if(d)h(f).on("scroll.DT",function(){var a=this.scrollLeft;k.scrollLeft=a;l&&(r.scrollLeft=a)});h(f).css(e&&c.bCollapse?"max-height":"height",e);a.nScrollHead=k;a.nScrollBody=f;a.nScrollFoot=r;a.aoDrawCallback.push({fn:ma,sName:"scrolling"});return i[0]}function ma(a){var b=a.oScroll,c=b.sX,d=b.sXInner,e=b.sY,b=b.iBarWidth,f=h(a.nScrollHead),g=f[0].style,j=f.children("div"),i=j[0].style,n=j.children("table"), +j=a.nScrollBody,l=h(j),q=j.style,r=h(a.nScrollFoot).children("div"),m=r.children("table"),p=h(a.nTHead),o=h(a.nTable),u=o[0],s=u.style,t=a.nTFoot?h(a.nTFoot):null,x=a.oBrowser,U=x.bScrollOversize,ac=D(a.aoColumns,"nTh"),P,L,Q,w,Wa=[],y=[],z=[],A=[],B,C=function(a){a=a.style;a.paddingTop="0";a.paddingBottom="0";a.borderTopWidth="0";a.borderBottomWidth="0";a.height=0};L=j.scrollHeight>j.clientHeight;if(a.scrollBarVis!==L&&a.scrollBarVis!==k)a.scrollBarVis=L,Z(a);else{a.scrollBarVis=L;o.children("thead, tfoot").remove(); +t&&(Q=t.clone().prependTo(o),P=t.find("tr"),Q=Q.find("tr"));w=p.clone().prependTo(o);p=p.find("tr");L=w.find("tr");w.find("th, td").removeAttr("tabindex");c||(q.width="100%",f[0].style.width="100%");h.each(ta(a,w),function(b,c){B=$(a,b);c.style.width=a.aoColumns[B].sWidth});t&&I(function(a){a.style.width=""},Q);f=o.outerWidth();if(""===c){s.width="100%";if(U&&(o.find("tbody").height()>j.offsetHeight||"scroll"==l.css("overflow-y")))s.width=v(o.outerWidth()-b);f=o.outerWidth()}else""!==d&&(s.width= +v(d),f=o.outerWidth());I(C,L);I(function(a){z.push(a.innerHTML);Wa.push(v(h(a).css("width")))},L);I(function(a,b){if(h.inArray(a,ac)!==-1)a.style.width=Wa[b]},p);h(L).height(0);t&&(I(C,Q),I(function(a){A.push(a.innerHTML);y.push(v(h(a).css("width")))},Q),I(function(a,b){a.style.width=y[b]},P),h(Q).height(0));I(function(a,b){a.innerHTML='
'+z[b]+"
";a.style.width=Wa[b]},L);t&&I(function(a,b){a.innerHTML='
'+ +A[b]+"
";a.style.width=y[b]},Q);if(o.outerWidth()j.offsetHeight||"scroll"==l.css("overflow-y")?f+b:f;if(U&&(j.scrollHeight>j.offsetHeight||"scroll"==l.css("overflow-y")))s.width=v(P-b);(""===c||""!==d)&&K(a,1,"Possible column misalignment",6)}else P="100%";q.width=v(P);g.width=v(P);t&&(a.nScrollFoot.style.width=v(P));!e&&U&&(q.height=v(u.offsetHeight+b));c=o.outerWidth();n[0].style.width=v(c);i.width=v(c);d=o.height()>j.clientHeight||"scroll"==l.css("overflow-y");e="padding"+ +(x.bScrollbarLeft?"Left":"Right");i[e]=d?b+"px":"0px";t&&(m[0].style.width=v(c),r[0].style.width=v(c),r[0].style[e]=d?b+"px":"0px");o.children("colgroup").insertBefore(o.children("thead"));l.scroll();if((a.bSorted||a.bFiltered)&&!a._drawHold)j.scrollTop=0}}function I(a,b,c){for(var d=0,e=0,f=b.length,g,j;e").appendTo(j.find("tbody")); +j.find("thead, tfoot").remove();j.append(h(a.nTHead).clone()).append(h(a.nTFoot).clone());j.find("tfoot th, tfoot td").css("width","");n=ta(a,j.find("thead")[0]);for(m=0;m").css({width:p.sWidthOrig,margin:0,padding:0,border:0,height:1}));if(a.aoData.length)for(m=0;m").css(f||e?{position:"absolute",top:0,left:0,height:1,right:0,overflow:"hidden"}:{}).append(j).appendTo(k);f&&g?j.width(g):f?(j.css("width","auto"),j.removeAttr("width"),j.width()").css("width",v(a)).appendTo(b||H.body),d=c[0].offsetWidth;c.remove();return d}function Hb(a,b){var c=Ib(a,b);if(0>c)return null;var d=a.aoData[c];return!d.nTr?h("").html(B(a,c,b,"display"))[0]:d.anCells[b]}function Ib(a,b){for(var c,d=-1,e=-1,f=0,g=a.aoData.length;fd&&(d=c.length,e=f);return e}function v(a){return null===a?"0px":"number"==typeof a?0>a?"0px":a+"px":a.match(/\d$/)?a+"px":a}function W(a){var b,c,d=[],e=a.aoColumns,f,g,j,i;b=a.aaSortingFixed;c=h.isPlainObject(b);var n=[];f=function(a){a.length&&!h.isArray(a[0])?n.push(a):h.merge(n,a)};h.isArray(b)&&f(b);c&&b.pre&&f(b.pre);f(a.aaSorting);c&&b.post&&f(b.post);for(a=0;ae?1:0,0!==c)return"asc"===j.dir?c:-c;c=d[a];e=d[b];return ce?1:0}):i.sort(function(a,b){var c,g,j,i,k=h.length,m=f[a]._aSortData,p=f[b]._aSortData;for(j=0;jg?1:0})}a.bSorted=!0}function Kb(a){for(var b,c,d=a.aoColumns,e=W(a),a=a.oLanguage.oAria,f=0,g=d.length;f/g, +"");var i=c.nTh;i.removeAttribute("aria-sort");c.bSortable&&(0e?e+1:3));e=0;for(f=d.length;ee?e+1:3))}a.aLastSort=d}function Jb(a,b){var c=a.aoColumns[b],d=m.ext.order[c.sSortDataType],e;d&&(e=d.call(a.oInstance,a,b,aa(a,b)));for(var f,g=m.ext.type.order[c.sType+"-pre"],j=0,i=a.aoData.length;j=f.length?[0,c[1]]:c)}));b.search!== +k&&h.extend(a.oPreviousSearch,Db(b.search));if(b.columns){d=0;for(e=b.columns.length;d=c&&(b=c-d);b-=b%d;if(-1===d||0>b)b=0;a._iDisplayStart=b}function Pa(a,b){var c=a.renderer,d=m.ext.renderer[b];return h.isPlainObject(c)&&c[b]?d[c[b]]||d._:"string"===typeof c?d[c]||d._:d._}function y(a){return a.oFeatures.bServerSide?"ssp":a.ajax||a.sAjaxSource?"ajax":"dom"}function ia(a,b){var c=[],c=Nb.numbers_length,d=Math.floor(c/2);b<=c?c=X(0,b):a<=d?(c=X(0, +c-2),c.push("ellipsis"),c.push(b-1)):(a>=b-1-d?c=X(b-(c-2),b):(c=X(a-d+2,a+d-1),c.push("ellipsis"),c.push(b-1)),c.splice(0,0,"ellipsis"),c.splice(0,0,0));c.DT_el="span";return c}function fb(a){h.each({num:function(b){return Ba(b,a)},"num-fmt":function(b){return Ba(b,a,Za)},"html-num":function(b){return Ba(b,a,Ca)},"html-num-fmt":function(b){return Ba(b,a,Ca,Za)}},function(b,c){x.type.order[b+a+"-pre"]=c;b.match(/^html\-/)&&(x.type.search[b+a]=x.type.search.html)})}function Ob(a){return function(){var b= +[Aa(this[m.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return m.ext.internal[a].apply(this,b)}}var m=function(a){this.$=function(a,b){return this.api(!0).$(a,b)};this._=function(a,b){return this.api(!0).rows(a,b).data()};this.api=function(a){return a?new u(Aa(this[x.iApiIndex])):new u(this)};this.fnAddData=function(a,b){var c=this.api(!0),d=h.isArray(a)&&(h.isArray(a[0])||h.isPlainObject(a[0]))?c.rows.add(a):c.row.add(a);(b===k||b)&&c.draw();return d.flatten().toArray()};this.fnAdjustColumnSizing= +function(a){var b=this.api(!0).columns.adjust(),c=b.settings()[0],d=c.oScroll;a===k||a?b.draw(!1):(""!==d.sX||""!==d.sY)&&ma(c)};this.fnClearTable=function(a){var b=this.api(!0).clear();(a===k||a)&&b.draw()};this.fnClose=function(a){this.api(!0).row(a).child.hide()};this.fnDeleteRow=function(a,b,c){var d=this.api(!0),a=d.rows(a),e=a.settings()[0],h=e.aoData[a[0][0]];a.remove();b&&b.call(this,e,h);(c===k||c)&&d.draw();return h};this.fnDestroy=function(a){this.api(!0).destroy(a)};this.fnDraw=function(a){this.api(!0).draw(a)}; +this.fnFilter=function(a,b,c,d,e,h){e=this.api(!0);null===b||b===k?e.search(a,c,d,h):e.column(b).search(a,c,d,h);e.draw()};this.fnGetData=function(a,b){var c=this.api(!0);if(a!==k){var d=a.nodeName?a.nodeName.toLowerCase():"";return b!==k||"td"==d||"th"==d?c.cell(a,b).data():c.row(a).data()||null}return c.data().toArray()};this.fnGetNodes=function(a){var b=this.api(!0);return a!==k?b.row(a).node():b.rows().nodes().flatten().toArray()};this.fnGetPosition=function(a){var b=this.api(!0),c=a.nodeName.toUpperCase(); +return"TR"==c?b.row(a).index():"TD"==c||"TH"==c?(a=b.cell(a).index(),[a.row,a.columnVisible,a.column]):null};this.fnIsOpen=function(a){return this.api(!0).row(a).child.isShown()};this.fnOpen=function(a,b,c){return this.api(!0).row(a).child(b,c).show().child()[0]};this.fnPageChange=function(a,b){var c=this.api(!0).page(a);(b===k||b)&&c.draw(!1)};this.fnSetColumnVis=function(a,b,c){a=this.api(!0).column(a).visible(b);(c===k||c)&&a.columns.adjust().draw()};this.fnSettings=function(){return Aa(this[x.iApiIndex])}; +this.fnSort=function(a){this.api(!0).order(a).draw()};this.fnSortListener=function(a,b,c){this.api(!0).order.listener(a,b,c)};this.fnUpdate=function(a,b,c,d,e){var h=this.api(!0);c===k||null===c?h.row(b).data(a):h.cell(b,c).data(a);(e===k||e)&&h.columns.adjust();(d===k||d)&&h.draw();return 0};this.fnVersionCheck=x.fnVersionCheck;var b=this,c=a===k,d=this.length;c&&(a={});this.oApi=this.internal=x.internal;for(var e in m.ext.internal)e&&(this[e]=Ob(e));this.each(function(){var e={},g=1t<"F"ip>'),o.renderer)?h.isPlainObject(o.renderer)&& +!o.renderer.header&&(o.renderer.header="jqueryui"):o.renderer="jqueryui":h.extend(t,m.ext.classes,g.oClasses);q.addClass(t.sTable);o.iInitDisplayStart===k&&(o.iInitDisplayStart=g.iDisplayStart,o._iDisplayStart=g.iDisplayStart);null!==g.iDeferLoading&&(o.bDeferLoading=!0,e=h.isArray(g.iDeferLoading),o._iRecordsDisplay=e?g.iDeferLoading[0]:g.iDeferLoading,o._iRecordsTotal=e?g.iDeferLoading[1]:g.iDeferLoading);var v=o.oLanguage;h.extend(!0,v,g.oLanguage);v.sUrl&&(h.ajax({dataType:"json",url:v.sUrl,success:function(a){Fa(a); +J(l.oLanguage,a);h.extend(true,v,a);ha(o)},error:function(){ha(o)}}),n=!0);null===g.asStripeClasses&&(o.asStripeClasses=[t.sStripeOdd,t.sStripeEven]);var e=o.asStripeClasses,x=q.children("tbody").find("tr").eq(0);-1!==h.inArray(!0,h.map(e,function(a){return x.hasClass(a)}))&&(h("tbody tr",this).removeClass(e.join(" ")),o.asDestroyStripes=e.slice());e=[];r=this.getElementsByTagName("thead");0!==r.length&&(ea(o.aoHeader,r[0]),e=ta(o));if(null===g.aoColumns){r=[];j=0;for(i=e.length;j").appendTo(q)); +o.nTHead=b[0];b=q.children("tbody");b.length===0&&(b=h("").appendTo(q));o.nTBody=b[0];b=q.children("tfoot");if(b.length===0&&a.length>0&&(o.oScroll.sX!==""||o.oScroll.sY!==""))b=h("").appendTo(q);if(b.length===0||b.children().length===0)q.addClass(t.sNoFooter);else if(b.length>0){o.nTFoot=b[0];ea(o.aoFooter,o.nTFoot)}if(g.aaData)for(j=0;j/g,cc=/^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/,dc=RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\^|\\-)","g"),Za=/[',$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfk]/gi,M=function(a){return!a||!0===a||"-"===a?!0:!1},Qb=function(a){var b=parseInt(a,10);return!isNaN(b)&& +isFinite(a)?b:null},Rb=function(a,b){$a[b]||($a[b]=RegExp(Sa(b),"g"));return"string"===typeof a&&"."!==b?a.replace(/\./g,"").replace($a[b],"."):a},ab=function(a,b,c){var d="string"===typeof a;if(M(a))return!0;b&&d&&(a=Rb(a,b));c&&d&&(a=a.replace(Za,""));return!isNaN(parseFloat(a))&&isFinite(a)},Sb=function(a,b,c){return M(a)?!0:!(M(a)||"string"===typeof a)?null:ab(a.replace(Ca,""),b,c)?!0:null},D=function(a,b,c){var d=[],e=0,f=a.length;if(c!==k)for(;e")[0],$b=xa.textContent!==k,bc=/<.*?>/g,Qa=m.util.throttle,Ub=[],w=Array.prototype,ec=function(a){var b,c,d=m.settings,e=h.map(d,function(a){return a.nTable}); +if(a){if(a.nTable&&a.oApi)return[a];if(a.nodeName&&"table"===a.nodeName.toLowerCase())return b=h.inArray(a,e),-1!==b?[d[b]]:null;if(a&&"function"===typeof a.settings)return a.settings().toArray();"string"===typeof a?c=h(a):a instanceof h&&(c=a)}else return[];if(c)return c.map(function(){b=h.inArray(this,e);return-1!==b?d[b]:null}).toArray()};u=function(a,b){if(!(this instanceof u))return new u(a,b);var c=[],d=function(a){(a=ec(a))&&(c=c.concat(a))};if(h.isArray(a))for(var e=0,f=a.length;ea?new u(b[a],this[a]):null},filter:function(a){var b=[];if(w.filter)b=w.filter.call(this,a,this); +else for(var c=0,d=this.length;c").addClass(b),h("td",c).addClass(b).html(a)[0].colSpan=ba(d),e.push(c[0]))};f(a,b);c._details&&c._details.detach();c._details=h(e); +c._detailsShow&&c._details.insertAfter(c.nTr)}return this});p(["row().child.show()","row().child().show()"],function(){Wb(this,!0);return this});p(["row().child.hide()","row().child().hide()"],function(){Wb(this,!1);return this});p(["row().child.remove()","row().child().remove()"],function(){eb(this);return this});p("row().child.isShown()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]]._detailsShow||!1:!1});var fc=/^([^:]+):(name|visIdx|visible)$/,Xb=function(a,b, +c,d,e){for(var c=[],d=0,f=e.length;d=0?b:g.length+b];if(typeof a==="function"){var e=Da(c,f);return h.map(g,function(b,f){return a(f,Xb(c,f,0,0,e),i[f])?f:null})}var k=typeof a==="string"?a.match(fc): +"";if(k)switch(k[2]){case "visIdx":case "visible":b=parseInt(k[1],10);if(b<0){var m=h.map(g,function(a,b){return a.bVisible?b:null});return[m[m.length+b]]}return[$(c,b)];case "name":return h.map(j,function(a,b){return a===k[1]?b:null});default:return[]}if(a.nodeName&&a._DT_CellIndex)return[a._DT_CellIndex.column];b=h(i).filter(a).map(function(){return h.inArray(this,i)}).toArray();if(b.length||!a.nodeName)return b;b=h(a).closest("*[data-dt-column]");return b.length?[b.data("dt-column")]:[]},c,f)}, +1);c.selector.cols=a;c.selector.opts=b;return c});t("columns().header()","column().header()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTh},1)});t("columns().footer()","column().footer()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTf},1)});t("columns().data()","column().data()",function(){return this.iterator("column-rows",Xb,1)});t("columns().dataSrc()","column().dataSrc()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].mData}, +1)});t("columns().cache()","column().cache()",function(a){return this.iterator("column-rows",function(b,c,d,e,f){return ja(b.aoData,f,"search"===a?"_aFilterData":"_aSortData",c)},1)});t("columns().nodes()","column().nodes()",function(){return this.iterator("column-rows",function(a,b,c,d,e){return ja(a.aoData,e,"anCells",b)},1)});t("columns().visible()","column().visible()",function(a,b){var c=this.iterator("column",function(b,c){if(a===k)return b.aoColumns[c].bVisible;var f=b.aoColumns,g=f[c],j=b.aoData, +i,n,l;if(a!==k&&g.bVisible!==a){if(a){var m=h.inArray(!0,D(f,"bVisible"),c+1);i=0;for(n=j.length;id;return!0};m.isDataTable=m.fnIsDataTable=function(a){var b=h(a).get(0),c=!1;if(a instanceof m.Api)return!0;h.each(m.settings,function(a,e){var f=e.nScrollHead?h("table",e.nScrollHead)[0]:null,g=e.nScrollFoot? +h("table",e.nScrollFoot)[0]:null;if(e.nTable===b||f===b||g===b)c=!0});return c};m.tables=m.fnTables=function(a){var b=!1;h.isPlainObject(a)&&(b=a.api,a=a.visible);var c=h.map(m.settings,function(b){if(!a||a&&h(b.nTable).is(":visible"))return b.nTable});return b?new u(c):c};m.camelToHungarian=J;p("$()",function(a,b){var c=this.rows(b).nodes(),c=h(c);return h([].concat(c.filter(a).toArray(),c.find(a).toArray()))});h.each(["on","one","off"],function(a,b){p(b+"()",function(){var a=Array.prototype.slice.call(arguments); +a[0]=h.map(a[0].split(/\s/),function(a){return!a.match(/\.dt\b/)?a+".dt":a}).join(" ");var d=h(this.tables().nodes());d[b].apply(d,a);return this})});p("clear()",function(){return this.iterator("table",function(a){pa(a)})});p("settings()",function(){return new u(this.context,this.context)});p("init()",function(){var a=this.context;return a.length?a[0].oInit:null});p("data()",function(){return this.iterator("table",function(a){return D(a.aoData,"_aData")}).flatten()});p("destroy()",function(a){a=a|| +!1;return this.iterator("table",function(b){var c=b.nTableWrapper.parentNode,d=b.oClasses,e=b.nTable,f=b.nTBody,g=b.nTHead,j=b.nTFoot,i=h(e),f=h(f),k=h(b.nTableWrapper),l=h.map(b.aoData,function(a){return a.nTr}),p;b.bDestroying=!0;s(b,"aoDestroyCallback","destroy",[b]);a||(new u(b)).columns().visible(!0);k.off(".DT").find(":not(tbody *)").off(".DT");h(E).off(".DT-"+b.sInstance);e!=g.parentNode&&(i.children("thead").detach(),i.append(g));j&&e!=j.parentNode&&(i.children("tfoot").detach(),i.append(j)); +b.aaSorting=[];b.aaSortingFixed=[];ya(b);h(l).removeClass(b.asStripeClasses.join(" "));h("th, td",g).removeClass(d.sSortable+" "+d.sSortableAsc+" "+d.sSortableDesc+" "+d.sSortableNone);b.bJUI&&(h("th span."+d.sSortIcon+", td span."+d.sSortIcon,g).detach(),h("th, td",g).each(function(){var a=h("div."+d.sSortJUIWrapper,this);h(this).append(a.contents());a.detach()}));f.children().detach();f.append(l);g=a?"remove":"detach";i[g]();k[g]();!a&&c&&(c.insertBefore(e,b.nTableReinsertBefore),i.css("width", +b.sDestroyWidth).removeClass(d.sTable),(p=b.asDestroyStripes.length)&&f.children().each(function(a){h(this).addClass(b.asDestroyStripes[a%p])}));c=h.inArray(b,m.settings);-1!==c&&m.settings.splice(c,1)})});h.each(["column","row","cell"],function(a,b){p(b+"s().every()",function(a){var d=this.selector.opts,e=this;return this.iterator(b,function(f,g,h,i,m){a.call(e[b](g,"cell"===b?h:d,"cell"===b?d:k),g,h,i,m)})})});p("i18n()",function(a,b,c){var d=this.context[0],a=R(a)(d.oLanguage);a===k&&(a=b);c!== +k&&h.isPlainObject(a)&&(a=a[c]!==k?a[c]:a._);return a.replace("%d",c)});m.version="1.10.13";m.settings=[];m.models={};m.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0};m.models.oRow={nTr:null,anCells:null,_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,_sRowStripe:"",src:null,idx:-1};m.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null, +mData:null,mRender:null,nTh:null,nTf:null,sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null,sWidthOrig:null};m.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bJQueryUI:!1,bLengthChange:!0,bPaginate:!0,bProcessing:!1, +bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1,bSort:!0,bSortMulti:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(a){return a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null,fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnServerData:null,fnServerParams:null,fnStateLoadCallback:function(a){try{return JSON.parse((-1===a.iStateDuration? +sessionStorage:localStorage).getItem("DataTables_"+a.sInstance+"_"+location.pathname))}catch(b){}},fnStateLoadParams:null,fnStateLoaded:null,fnStateSaveCallback:function(a,b){try{(-1===a.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+a.sInstance+"_"+location.pathname,JSON.stringify(b))}catch(c){}},fnStateSaveParams:null,iStateDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{sSortAscending:": activate to sort column ascending", +sSortDescending:": activate to sort column descending"},oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries",sInfoFiltered:"(filtered from _MAX_ total entries)",sInfoPostFix:"",sDecimal:"",sThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"}, +oSearch:h.extend({},m.models.oSearch),sAjaxDataProp:"data",sAjaxSource:null,sDom:"lfrtip",searchDelay:null,sPaginationType:"simple_numbers",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",renderer:null,rowId:"DT_RowId"};Y(m.defaults);m.defaults.column={aDataSort:null,iDataSort:-1,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null, +sType:null,sWidth:null};Y(m.defaults.column);m.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null,bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1,bScrollbarLeft:!1,bBounding:!1,barWidth:0},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[], +aIds:{},aoColumns:[],aoHeader:[],aoFooter:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null, +searchDelay:null,sPaginationType:"two_button",iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,json:k,oAjaxData:k,fnServerData:null,aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,bJUI:null,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[], +fnRecordsTotal:function(){return"ssp"==y(this)?1*this._iRecordsTotal:this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==y(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a=this._iDisplayLength,b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,e=this.oFeatures,f=e.bPaginate;return e.bServerSide?!1===f||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!f||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null, +aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null};m.ext=x={buttons:{},classes:{},builder:"-source-",errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}},order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:m.fnVersionCheck,iApiIndex:0,oJUIClasses:{},sVersion:m.version};h.extend(x,{afnFiltering:x.search,aTypes:x.type.detect,ofnSearch:x.type.search,oSort:x.type.order,afnSortData:x.order,aoFeatures:x.feature, +oApi:x.internal,oStdClasses:x.classes,oPagination:x.pager});h.extend(m.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd",sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc", +sSortable:"sorting",sSortableAsc:"sorting_asc_disabled",sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"", +sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"",sJUIHeader:"",sJUIFooter:""});var Ea="",Ea="",G=Ea+"ui-state-default",ka=Ea+"css_right ui-icon ui-icon-",Yb=Ea+"fg-toolbar ui-toolbar ui-widget-header ui-helper-clearfix";h.extend(m.ext.oJUIClasses,m.ext.classes,{sPageButton:"fg-button ui-button "+G,sPageButtonActive:"ui-state-disabled",sPageButtonDisabled:"ui-state-disabled",sPaging:"dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi ui-buttonset-multi paging_",sSortAsc:G+" sorting_asc", +sSortDesc:G+" sorting_desc",sSortable:G+" sorting",sSortableAsc:G+" sorting_asc_disabled",sSortableDesc:G+" sorting_desc_disabled",sSortableNone:G+" sorting_disabled",sSortJUIAsc:ka+"triangle-1-n",sSortJUIDesc:ka+"triangle-1-s",sSortJUI:ka+"carat-2-n-s",sSortJUIAscAllowed:ka+"carat-1-n",sSortJUIDescAllowed:ka+"carat-1-s",sSortJUIWrapper:"DataTables_sort_wrapper",sSortIcon:"DataTables_sort_icon",sScrollHead:"dataTables_scrollHead "+G,sScrollFoot:"dataTables_scrollFoot "+G,sHeaderTH:G,sFooterTH:G,sJUIHeader:Yb+ +" ui-corner-tl ui-corner-tr",sJUIFooter:Yb+" ui-corner-bl ui-corner-br"});var Nb=m.ext.pager;h.extend(Nb,{simple:function(){return["previous","next"]},full:function(){return["first","previous","next","last"]},numbers:function(a,b){return[ia(a,b)]},simple_numbers:function(a,b){return["previous",ia(a,b),"next"]},full_numbers:function(a,b){return["first","previous",ia(a,b),"next","last"]},first_last_numbers:function(a,b){return["first",ia(a,b),"last"]},_numbers:ia,numbers_length:7});h.extend(!0,m.ext.renderer, +{pageButton:{_:function(a,b,c,d,e,f){var g=a.oClasses,j=a.oLanguage.oPaginate,i=a.oLanguage.oAria.paginate||{},m,l,p=0,r=function(b,d){var k,t,u,s,v=function(b){Va(a,b.data.action,true)};k=0;for(t=d.length;k").appendTo(b);r(u,s)}else{m=null;l="";switch(s){case "ellipsis":b.append('');break;case "first":m=j.sFirst;l=s+(e>0?"":" "+g.sPageButtonDisabled);break;case "previous":m=j.sPrevious;l=s+(e>0?"":" "+ +g.sPageButtonDisabled);break;case "next":m=j.sNext;l=s+(e",{"class":g.sPageButton+" "+l,"aria-controls":a.sTableId,"aria-label":i[s],"data-dt-idx":p,tabindex:a.iTabIndex,id:c===0&&typeof s==="string"?a.sTableId+"_"+s:null}).html(m).appendTo(b);Ya(u,{action:s},v);p++}}}},t;try{t=h(b).find(H.activeElement).data("dt-idx")}catch(u){}r(h(b).empty(), +d);t!==k&&h(b).find("[data-dt-idx="+t+"]").focus()}}});h.extend(m.ext.type.detect,[function(a,b){var c=b.oLanguage.sDecimal;return ab(a,c)?"num"+c:null},function(a){if(a&&!(a instanceof Date)&&!cc.test(a))return null;var b=Date.parse(a);return null!==b&&!isNaN(b)||M(a)?"date":null},function(a,b){var c=b.oLanguage.sDecimal;return ab(a,c,!0)?"num-fmt"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Sb(a,c)?"html-num"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Sb(a,c,!0)?"html-num-fmt"+ +c:null},function(a){return M(a)||"string"===typeof a&&-1!==a.indexOf("<")?"html":null}]);h.extend(m.ext.type.search,{html:function(a){return M(a)?a:"string"===typeof a?a.replace(Pb," ").replace(Ca,""):""},string:function(a){return M(a)?a:"string"===typeof a?a.replace(Pb," "):a}});var Ba=function(a,b,c,d){if(0!==a&&(!a||"-"===a))return-Infinity;b&&(a=Rb(a,b));a.replace&&(c&&(a=a.replace(c,"")),d&&(a=a.replace(d,"")));return 1*a};h.extend(x.type.order,{"date-pre":function(a){return Date.parse(a)||-Infinity}, +"html-pre":function(a){return M(a)?"":a.replace?a.replace(/<.*?>/g,"").toLowerCase():a+""},"string-pre":function(a){return M(a)?"":"string"===typeof a?a.toLowerCase():!a.toString?"":a.toString()},"string-asc":function(a,b){return ab?1:0},"string-desc":function(a,b){return ab?-1:0}});fb("");h.extend(!0,m.ext.renderer,{header:{_:function(a,b,c,d){h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(c.sSortingClass+" "+d.sSortAsc+" "+d.sSortDesc).addClass(h[e]== +"asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass)}})},jqueryui:function(a,b,c,d){h("
").addClass(d.sSortJUIWrapper).append(b.contents()).append(h("").addClass(d.sSortIcon+" "+c.sSortingClassJUI)).appendTo(b);h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass);b.find("span."+d.sSortIcon).removeClass(d.sSortJUIAsc+" "+d.sSortJUIDesc+" "+d.sSortJUI+" "+ +d.sSortJUIAscAllowed+" "+d.sSortJUIDescAllowed).addClass(h[e]=="asc"?d.sSortJUIAsc:h[e]=="desc"?d.sSortJUIDesc:c.sSortingClassJUI)}})}}});var Zb=function(a){return"string"===typeof a?a.replace(//g,">").replace(/"/g,"""):a};m.render={number:function(a,b,c,d,e){return{display:function(f){if("number"!==typeof f&&"string"!==typeof f)return f;var g=0>f?"-":"",h=parseFloat(f);if(isNaN(h))return Zb(f);h=h.toFixed(c);f=Math.abs(h);h=parseInt(f,10);f=c?b+(f-h).toFixed(c).substring(2): +"";return g+(d||"")+h.toString().replace(/\B(?=(\d{3})+(?!\d))/g,a)+f+(e||"")}}},text:function(){return{display:Zb}}};h.extend(m.ext.internal,{_fnExternApiFunc:Ob,_fnBuildAjax:ua,_fnAjaxUpdate:nb,_fnAjaxParameters:wb,_fnAjaxUpdateDraw:xb,_fnAjaxDataSrc:va,_fnAddColumn:Ga,_fnColumnOptions:la,_fnAdjustColumnSizing:Z,_fnVisibleToColumnIndex:$,_fnColumnIndexToVisible:aa,_fnVisbleColumns:ba,_fnGetColumns:na,_fnColumnTypes:Ia,_fnApplyColumnDefs:kb,_fnHungarianMap:Y,_fnCamelToHungarian:J,_fnLanguageCompat:Fa, +_fnBrowserDetect:ib,_fnAddData:N,_fnAddTr:oa,_fnNodeToDataIndex:function(a,b){return b._DT_RowIndex!==k?b._DT_RowIndex:null},_fnNodeToColumnIndex:function(a,b,c){return h.inArray(c,a.aoData[b].anCells)},_fnGetCellData:B,_fnSetCellData:lb,_fnSplitObjNotation:La,_fnGetObjectDataFn:R,_fnSetObjectDataFn:S,_fnGetDataMaster:Ma,_fnClearTable:pa,_fnDeleteIndex:qa,_fnInvalidate:da,_fnGetRowElements:Ka,_fnCreateTr:Ja,_fnBuildHead:mb,_fnDrawHead:fa,_fnDraw:O,_fnReDraw:T,_fnAddOptionsHtml:pb,_fnDetectHeader:ea, +_fnGetUniqueThs:ta,_fnFeatureHtmlFilter:rb,_fnFilterComplete:ga,_fnFilterCustom:Ab,_fnFilterColumn:zb,_fnFilter:yb,_fnFilterCreateSearch:Ra,_fnEscapeRegex:Sa,_fnFilterData:Bb,_fnFeatureHtmlInfo:ub,_fnUpdateInfo:Eb,_fnInfoMacros:Fb,_fnInitialise:ha,_fnInitComplete:wa,_fnLengthChange:Ta,_fnFeatureHtmlLength:qb,_fnFeatureHtmlPaginate:vb,_fnPageChange:Va,_fnFeatureHtmlProcessing:sb,_fnProcessingDisplay:C,_fnFeatureHtmlTable:tb,_fnScrollDraw:ma,_fnApplyToChildren:I,_fnCalculateColumnWidths:Ha,_fnThrottle:Qa, +_fnConvertToWidth:Gb,_fnGetWidestNode:Hb,_fnGetMaxLenString:Ib,_fnStringToCss:v,_fnSortFlatten:W,_fnSort:ob,_fnSortAria:Kb,_fnSortListener:Xa,_fnSortAttachListener:Oa,_fnSortingClasses:ya,_fnSortData:Jb,_fnSaveState:za,_fnLoadState:Lb,_fnSettingsFromNode:Aa,_fnLog:K,_fnMap:F,_fnBindAction:Ya,_fnCallbackReg:z,_fnCallbackFire:s,_fnLengthOverflow:Ua,_fnRenderer:Pa,_fnDataSource:y,_fnRowAttributes:Na,_fnCalculateEnd:function(){}});h.fn.dataTable=m;m.$=h;h.fn.dataTableSettings=m.settings;h.fn.dataTableExt= +m.ext;h.fn.DataTable=function(a){return h(this).dataTable(a).api()};h.each(m,function(a,b){h.fn.DataTable[a]=b});return h.fn.dataTable}); diff --git a/qiita_pet/static/vendor/js/tag-it.min.js b/qiita_pet/static/vendor/js/tag-it.min.js new file mode 100644 index 000000000..fd6140c8a --- /dev/null +++ b/qiita_pet/static/vendor/js/tag-it.min.js @@ -0,0 +1,17 @@ +(function(b){b.widget("ui.tagit",{options:{allowDuplicates:!1,caseSensitive:!0,fieldName:"tags",placeholderText:null,readOnly:!1,removeConfirmation:!1,tagLimit:null,availableTags:[],autocomplete:{},showAutocompleteOnFocus:!1,allowSpaces:!1,singleField:!1,singleFieldDelimiter:",",singleFieldNode:null,animate:!0,tabIndex:null,beforeTagAdded:null,afterTagAdded:null,beforeTagRemoved:null,afterTagRemoved:null,onTagClicked:null,onTagLimitExceeded:null,onTagAdded:null,onTagRemoved:null,tagSource:null},_create:function(){var a= +this;this.element.is("input")?(this.tagList=b("
    ").insertAfter(this.element),this.options.singleField=!0,this.options.singleFieldNode=this.element,this.element.addClass("tagit-hidden-field")):this.tagList=this.element.find("ul, ol").andSelf().last();this.tagInput=b('').addClass("ui-widget-content");this.options.readOnly&&this.tagInput.attr("disabled","disabled");this.options.tabIndex&&this.tagInput.attr("tabindex",this.options.tabIndex);this.options.placeholderText&&this.tagInput.attr("placeholder", +this.options.placeholderText);this.options.autocomplete.source||(this.options.autocomplete.source=function(a,e){var d=a.term.toLowerCase(),c=b.grep(this.options.availableTags,function(a){return 0===a.toLowerCase().indexOf(d)});this.options.allowDuplicates||(c=this._subtractArray(c,this.assignedTags()));e(c)});this.options.showAutocompleteOnFocus&&(this.tagInput.focus(function(b,d){a._showAutocomplete()}),"undefined"===typeof this.options.autocomplete.minLength&&(this.options.autocomplete.minLength= +0));b.isFunction(this.options.autocomplete.source)&&(this.options.autocomplete.source=b.proxy(this.options.autocomplete.source,this));b.isFunction(this.options.tagSource)&&(this.options.tagSource=b.proxy(this.options.tagSource,this));this.tagList.addClass("tagit").addClass("ui-widget ui-widget-content ui-corner-all").append(b('
  • ').append(this.tagInput)).click(function(d){var c=b(d.target);c.hasClass("tagit-label")?(c=c.closest(".tagit-choice"),c.hasClass("removed")||a._trigger("onTagClicked", +d,{tag:c,tagLabel:a.tagLabel(c)})):a.tagInput.focus()});var c=!1;if(this.options.singleField)if(this.options.singleFieldNode){var d=b(this.options.singleFieldNode),f=d.val().split(this.options.singleFieldDelimiter);d.val("");b.each(f,function(b,d){a.createTag(d,null,!0);c=!0})}else this.options.singleFieldNode=b(''),this.tagList.after(this.options.singleFieldNode);c||this.tagList.children("li").each(function(){b(this).hasClass("tagit-new")|| +(a.createTag(b(this).text(),b(this).attr("class"),!0),b(this).remove())});this.tagInput.keydown(function(c){if(c.which==b.ui.keyCode.BACKSPACE&&""===a.tagInput.val()){var d=a._lastTag();!a.options.removeConfirmation||d.hasClass("remove")?a.removeTag(d):a.options.removeConfirmation&&d.addClass("remove ui-state-highlight")}else a.options.removeConfirmation&&a._lastTag().removeClass("remove ui-state-highlight");if(c.which===b.ui.keyCode.COMMA&&!1===c.shiftKey||c.which===b.ui.keyCode.ENTER||c.which== +b.ui.keyCode.TAB&&""!==a.tagInput.val()||c.which==b.ui.keyCode.SPACE&&!0!==a.options.allowSpaces&&('"'!=b.trim(a.tagInput.val()).replace(/^s*/,"").charAt(0)||'"'==b.trim(a.tagInput.val()).charAt(0)&&'"'==b.trim(a.tagInput.val()).charAt(b.trim(a.tagInput.val()).length-1)&&0!==b.trim(a.tagInput.val()).length-1))c.which===b.ui.keyCode.ENTER&&""===a.tagInput.val()||c.preventDefault(),a.options.autocomplete.autoFocus&&a.tagInput.data("autocomplete-open")||(a.tagInput.autocomplete("close"),a.createTag(a._cleanedInput()))}).blur(function(b){a.tagInput.data("autocomplete-open")|| +a.createTag(a._cleanedInput())});if(this.options.availableTags||this.options.tagSource||this.options.autocomplete.source)d={select:function(b,c){a.createTag(c.item.value);return!1}},b.extend(d,this.options.autocomplete),d.source=this.options.tagSource||d.source,this.tagInput.autocomplete(d).bind("autocompleteopen.tagit",function(b,c){a.tagInput.data("autocomplete-open",!0)}).bind("autocompleteclose.tagit",function(b,c){a.tagInput.data("autocomplete-open",!1)}),this.tagInput.autocomplete("widget").addClass("tagit-autocomplete")}, +destroy:function(){b.Widget.prototype.destroy.call(this);this.element.unbind(".tagit");this.tagList.unbind(".tagit");this.tagInput.removeData("autocomplete-open");this.tagList.removeClass("tagit ui-widget ui-widget-content ui-corner-all tagit-hidden-field");this.element.is("input")?(this.element.removeClass("tagit-hidden-field"),this.tagList.remove()):(this.element.children("li").each(function(){b(this).hasClass("tagit-new")?b(this).remove():(b(this).removeClass("tagit-choice ui-widget-content ui-state-default ui-state-highlight ui-corner-all remove tagit-choice-editable tagit-choice-read-only"), +b(this).text(b(this).children(".tagit-label").text()))}),this.singleFieldNode&&this.singleFieldNode.remove());return this},_cleanedInput:function(){return b.trim(this.tagInput.val().replace(/^"(.*)"$/,"$1"))},_lastTag:function(){return this.tagList.find(".tagit-choice:last:not(.removed)")},_tags:function(){return this.tagList.find(".tagit-choice:not(.removed)")},assignedTags:function(){var a=this,c=[];this.options.singleField?(c=b(this.options.singleFieldNode).val().split(this.options.singleFieldDelimiter), +""===c[0]&&(c=[])):this._tags().each(function(){c.push(a.tagLabel(this))});return c},_updateSingleTagsField:function(a){b(this.options.singleFieldNode).val(a.join(this.options.singleFieldDelimiter)).trigger("change")},_subtractArray:function(a,c){for(var d=[],f=0;f=this.options.tagLimit)return this._trigger("onTagLimitExceeded",null,{duringInitialization:d}),!1;var g=b(this.options.onTagClicked?'':'').text(a),e=b("
  • ").addClass("tagit-choice ui-widget-content ui-state-default ui-corner-all").addClass(c).append(g); +this.options.readOnly?e.addClass("tagit-choice-read-only"):(e.addClass("tagit-choice-editable"),c=b("").addClass("ui-icon ui-icon-close"),c=b('\u00d7').addClass("tagit-close").append(c).click(function(a){f.removeTag(e)}),e.append(c));this.options.singleField||(g=g.html(),e.append(''));!1!==this._trigger("beforeTagAdded",null,{tag:e,tagLabel:this.tagLabel(e), +duringInitialization:d})&&(this.options.singleField&&(g=this.assignedTags(),g.push(a),this._updateSingleTagsField(g)),this._trigger("onTagAdded",null,e),this.tagInput.val(""),this.tagInput.parent().before(e),this._trigger("afterTagAdded",null,{tag:e,tagLabel:this.tagLabel(e),duringInitialization:d}),this.options.showAutocompleteOnFocus&&!d&&setTimeout(function(){f._showAutocomplete()},0))},removeTag:function(a,c){c="undefined"===typeof c?this.options.animate:c;a=b(a);this._trigger("onTagRemoved", +null,a);if(!1!==this._trigger("beforeTagRemoved",null,{tag:a,tagLabel:this.tagLabel(a)})){if(this.options.singleField){var d=this.assignedTags(),f=this.tagLabel(a),d=b.grep(d,function(a){return a!=f});this._updateSingleTagsField(d)}if(c){a.addClass("removed");var d=this._effectExists("blind")?["blind",{direction:"horizontal"},"fast"]:["fast"],g=this;d.push(function(){a.remove();g._trigger("afterTagRemoved",null,{tag:a,tagLabel:g.tagLabel(a)})});a.fadeOut("fast").hide.apply(a,d).dequeue()}else a.remove(), +this._trigger("afterTagRemoved",null,{tag:a,tagLabel:this.tagLabel(a)})}},removeTagByLabel:function(a,b){var d=this._findTagByLabel(a);if(!d)throw"No such tag exists with the name '"+a+"'";this.removeTag(d,b)},removeAll:function(){var a=this;this._tags().each(function(b,d){a.removeTag(d,!1)})}})})(jQuery); diff --git a/qiita_pet/static/vendor/licences/tag-it_license.txt b/qiita_pet/static/vendor/licences/tag-it_license.txt new file mode 100644 index 000000000..1f688168d --- /dev/null +++ b/qiita_pet/static/vendor/licences/tag-it_license.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2011-2014 Twitter, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/qiita_pet/support_files/doc/source/deblur_quality.rst b/qiita_pet/support_files/doc/source/deblur_quality.rst new file mode 100644 index 000000000..7ad9e8780 --- /dev/null +++ b/qiita_pet/support_files/doc/source/deblur_quality.rst @@ -0,0 +1,107 @@ +.. _deblur_quality: + +.. index:: deblur-quality + +======================== +Deblur quality filtering +======================== + +In the `Deblur Manuscript `__, many of the analyses performed quality filtered the sequence data based on the PHRED scores prior to the application of Deblur. The decision at the time was a motivation to reduce potential noise in the Deblur process, however an evaluation of whether the quality filtering actually mattered had not been performed. Herein, we explore the effect of quality filtering on Deblur as well as closed and open reference OTU picking using Mantel tests assessing the correlation between quality filtering levels. Based on our results, we do not see a practical reason to quality filter beyond the minimal recommendations made in `Bokulich et al. 2013 `__. + +------- +Methods +------- + +Preprocessed Illumina (MiSeq and HiSeq) artifacts targeting the 16S rRNA V4 region were selected from the public data in Qiita. The artifacts were choosen to span environments and with a bias toward longer read length to allow for the exploration of read trim lengths. The specific artifacts, studies and brief summaries of their environments used are summarized below: + +.. table:: + + +--------------+--------+--------------------------+------------------------------------------------+ + |Qiita Study ID|Artifact| Environment | URL | + +==============+========+==========================+================================================+ + | 10407| 3150|Lemur gut |https://qiita.ucsd.edu/study/description/10407 | + +--------------+--------+--------------------------+------------------------------------------------+ + | 10431| 3266|Soil |https://qiita.ucsd.edu/study/description/10431 | + +--------------+--------+--------------------------+------------------------------------------------+ + | 10729| 4743|Butterfly gut |https://qiita.ucsd.edu/study/description/10729 | + +--------------+--------+--------------------------+------------------------------------------------+ + | 10483| 4590|Mouse gut |https://qiita.ucsd.edu/study/description/10483 | + +--------------+--------+--------------------------+------------------------------------------------+ + | 10481| 3741|Microbial mat |https://qiita.ucsd.edu/study/description/10481 | + +--------------+--------+--------------------------+------------------------------------------------+ + | 10532| 3827|Human gut |https://qiita.ucsd.edu/study/description/10532 | + +--------------+--------+--------------------------+------------------------------------------------+ + | 10485| 3562|Bovine milk |https://qiita.ucsd.edu/study/description/10485 | + +--------------+--------+--------------------------+------------------------------------------------+ + | 10394| 3104|Dog and human gut |https://qiita.ucsd.edu/study/description/10394 | + +--------------+--------+--------------------------+------------------------------------------------+ + | 10470| 3595|Estuarine bacterioplankton|https://qiita.ucsd.edu/study/description/10470 | + +--------------+--------+--------------------------+------------------------------------------------+ + | 10346| 701|Porifera |https://qiita.ucsd.edu/study/description/10346 | + +--------------+--------+--------------------------+------------------------------------------------+ + | 10343| 3199|peruvian_ants |https://qiita.ucsd.edu/study/description/10343 | + +--------------+--------+--------------------------+------------------------------------------------+ + | 10360| 3531|atacama_soils |https://qiita.ucsd.edu/study/description/10360 | + +--------------+--------+--------------------------+------------------------------------------------+ + | 10297| 571|gambian_infants |https://qiita.ucsd.edu/study/description/10297 | + +--------------+--------+--------------------------+------------------------------------------------+ + | 10291| 548|chick_microbiome |https://qiita.ucsd.edu/study/description/10291 | + +--------------+--------+--------------------------+------------------------------------------------+ + | 10281| 713|fed_reactor |https://qiita.ucsd.edu/study/description/10281 | + +--------------+--------+--------------------------+------------------------------------------------+ + | 10485| 3565|bovine_milk |https://qiita.ucsd.edu/study/description/10485 | + +--------------+--------+--------------------------+------------------------------------------------+ + | 10485| 3564|bovine_milk |https://qiita.ucsd.edu/study/description/10485 | + +--------------+--------+--------------------------+------------------------------------------------+ + | 10485| 3563|bovine_milk |https://qiita.ucsd.edu/study/description/10485 | + +--------------+--------+--------------------------+------------------------------------------------+ + | 10485| 3560|bovine_milk |https://qiita.ucsd.edu/study/description/10485 | + +--------------+--------+--------------------------+------------------------------------------------+ + | 10485| 3561|bovine_milk |https://qiita.ucsd.edu/study/description/10485 | + +--------------+--------+--------------------------+------------------------------------------------+ + | 10423| 3225|office |https://qiita.ucsd.edu/study/description/10423 | + +--------------+--------+--------------------------+------------------------------------------------+ + | 10342| 3483|native_american_guts |https://qiita.ucsd.edu/study/description/10342 | + +--------------+--------+--------------------------+------------------------------------------------+ + +Each preprocessed artifact was filtered using the QIIME 1.9.1 standard parameters, as implemented in `q2-quality-filter `__, with the exception that the minimum allowable quality score was varied from 4 (the default from Bokulich et al), 19 or 20. N.B. in QIIME 1.9.1, these minimum quality score values are equivalent to -q 3, -q 18 and -q 19 respectively. The quality filtered data were run through Deblur 1.0.1 via `q2-deblur `__, QIIME 1.9.1’s default open reference OTU picking pipeline and QIIME 1.9.1’s default closed reference OTU picking pipeline. In the case of q2-deblur, the data were trimmed to different read lengths of 99, 124, 149, 250 (where feasible for the study) via exposed command line options; for QIIME 1.9.1, the data were trimmed using GNU cut 8.4. For the q2-deblur and open reference pipelines, trees were constructed using the respective defaults for QIIME2 and QIIME 1.9.1, where the former uses MAFFT with a dynamic entropy filter followed by FastTree and midpoint rooting, while the latter uses PyNAST with the Lane mask and FastTree. + +A high level characterization was performed by rarefying the tables to 1000 sequences per sample and computing unweighted and weighted UniFrac using the implementation in `q2-state-unifrac `__. Mantel tests, as implemented in scikit-bio 0.5.1, were performed between q4 vs. q20, or q19 vs. q20 while controlling for OTU type, trim length, and distance metric. + +For the rarefaction assessment, the picked data were rarefied to 1000 sequences per sample 10 times. Unweighted and weighted UniFrac were computed for each replicate using q2-state-unifrac. For a given artifact, OTU type, trim length and quality threshold, pairwise Mantel tests were performed between replicates. In addition, for a given artifact OTU type, and trim length, pairwise Mantel tests were performed between the quality replicates (e.g., all q4 vs all q20). + +------- +Results +------- + +We first set out to test whether higher quality filtering led to improved correlation between quality filtering levels. Our hypothesis being that a filtering level of N would have a higher correlation to a filtering level of N-1 then to the N-16 level. In order to assess this, we plotted the resulting Pearson r^2 values from Mantel tests between q4 vs. q20, against the Pearson r^2 values from Mantel tests between q19 vs. q20. As can be see with the unweighted UniFrac results in figure 1, the impact of quality filtering does not appear to improve correlation with more stringent filtering irrespective of the OTU methods tested. Paradoxically, there are a few examples where it appears that q4 has a higher correlation to q20 than q19. The effect is similar with weighted UniFrac (figure 2) in that studies which correlate poorly between q4 and q20 tend to correlate poorly between q19 and q20, however q2-Deblur tends to surprisingly have higher correlation between q4 and q20 in contrast to open reference. Closed reference OTU picking appears robust with weighted UniFrac. + +.. figure:: images/deblur_quality_fig1.png + :align: center + +**Figure 1.** Mantel correlations of unweighted UniFrac distances between quality filtering thresholds. From left to right, the plots depict a sequence trim of 99nt, 124nt, 149nt and 250nt. Green corresponds to q2-deblur, blue to open reference and red to closed reference. The x-axes depict the r^2 for a q19 filtered table vs. a q20 filtered table, and the y-axes depict the r^2 for a q4 filtered table vs. a q20 filtered table. + +.. figure:: images/deblur_quality_fig2.png + :align: center + +**Figure 2.** Mantel correlations of weighted UniFrac distances between quality filtering thresholds. From left to right, the plots depict a sequence trim of 99nt, 124nt, 149nt and 250nt. Green corresponds to q2-deblur, blue to open reference and red to closed reference. The x-axes depict the r^2 for a q19 filtered table vs. a q20 filtered table, and the y-axes depict the r^2 for a q4 filtered table vs. a q20 filtered table. + +Next, we sought to explore how the methods performed relative to each other. For this analysis, for each pair of methods (e.g., q2-deblur vs. q1-openref), for a common artifact, for a common trim and quality filtering level, we computed the difference between r^2 values. The distribution of the resulting differences were then plotted per-artifact in figure 3 for unweighted UniFrac comparing q2-deblur to open reference. + +.. figure:: images/deblur_quality_fig3.png + :align: center + +**Figure 3.** Correlation differences for unweighted UniFrac between q2-deblur and open-reference. For a given set of parameters (i.e., the artifact, trim length and quality filtering level pair), the observed r^2 values were subtracted and plotted. Values greater than 0 indicate whether q2-deblur yielded a higher correlation to q20 than open-reference. + +Finally, we examined the stability of the results by computing multiple rarefactions, and assessing Mantel correlations between the resulting tables. Specifically, we sought to understand the variability within a given quality threshold as it compares between quality thresholds (figure 4). + +.. figure:: images/deblur_quality_fig4.png + :align: center + +**Figure 4.** Within and between rarefaction assessment of Mantel correlations at a trim of 99nt. For a given artifact, at a given quality filtering level, 10 rarefactions were computed. Within a quality filtering level, all pairwise Mantel tests were performed. Between quality levels, all pairwise Mantel tests of the rarefactions between levels were computed. The results suggest that more aggressive quality filtering does not have an appreciable impact on the overall relationships between samples. + +---------- +Discussion +---------- + +The application of quality filtering on a sequencing run does not appear to share a monotonic relationship with Mantel test correlations. This result is surprising. Run quality should improve as more low quality sequences are removed. Instead, these data suggest that more aggressive quality filtering (in the case of 16S V4 data) only results in throwing away sequence data. This observation appears to hold across environments, sequencing instruments and OTU assessment methods. diff --git a/qiita_pet/support_files/doc/source/dev/index.rst b/qiita_pet/support_files/doc/source/dev/index.rst index fffea5391..07f043f41 100644 --- a/qiita_pet/support_files/doc/source/dev/index.rst +++ b/qiita_pet/support_files/doc/source/dev/index.rst @@ -7,6 +7,7 @@ The following is a full list of the available developer tutorials :maxdepth: 2 plugins + rest To request documentation on any developer use-cases not addressed here, please add an issue `here `__. diff --git a/qiita_pet/support_files/doc/source/dev/rest.rst b/qiita_pet/support_files/doc/source/dev/rest.rst new file mode 100644 index 000000000..41c63dde7 --- /dev/null +++ b/qiita_pet/support_files/doc/source/dev/rest.rst @@ -0,0 +1,35 @@ +.. _plugins: + +.. index :: rest + +Qiita REST API +============== + +The Qiita REST API is currently only for internal use and is composed of the +following endpoints: + ++--------+-----------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Action | URI | Description | ++========+===========================================================+==========================================================================================================================================================+ +|GET | ``/api/v1/study/`` | Get study details (title, contacts, abstract, description and alias). | ++--------+-----------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------+ +|GET | ``/api/v1/study//samples`` | Get samples associated with a study and the available metadata headers for these samples. | ++--------+-----------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------+ +|PATCH | ``/api/v1/study//samples`` | Update sample metadata or add samples to the sample information. | ++--------+-----------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------+ +|GET | ``/api/v1/study//samples?categories=foo,bar`` | Get metadata categories foo and bar for all samples in the study. | ++--------+-----------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------+ +|GET | ``/api/v1/study//status`` | The status of a study (whether or not the study: is public, has sample information, sample information has warnings and a list of existing preparations. | ++--------+-----------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------+ +|GET | ``/api/v1/person`` | Get list of persons. | ++--------+-----------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------+ +|GET | ``/api/v1/person?name=foo&affiliation=bar`` | See if a person exists. | ++--------+-----------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------+ +|POST | ``/api/v1/study`` | Create a study (mirrors study creation on qiita UI with minimal requirements). | ++--------+-----------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------+ +|POST | ``/api/v1/person?name=foo&affiliation=bar&email=address`` | Create a study person (ie lab person or PI). | ++--------+-----------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------+ +|POST | ``/api/v1/study//preparation`` | Associate a prep with a study. | ++--------+-----------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------+ +|POST | ``/api/v1/study//preparation//artifact`` | Associate filepaths to a preparation, assuming this filepaths are present in the uploads folder. | ++--------+-----------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------+ diff --git a/qiita_pet/support_files/doc/source/faq.rst b/qiita_pet/support_files/doc/source/faq.rst index 8b7369413..2b8f85844 100644 --- a/qiita_pet/support_files/doc/source/faq.rst +++ b/qiita_pet/support_files/doc/source/faq.rst @@ -65,3 +65,17 @@ A few more instructions: for the example above the workflow should be: perform closed OTU picking against the latest version of Greengenes and can be quite time consuming depending on the number of samples and the depth of sequencing. + +.. _issues_unzip: + +How to solve unzip errors? +-------------------------- + +When downloading large zip files within Qiita there is a change that you will get +an error like: **"start of central directory not found; zipfile corrupt"**. This issue +arises from using old versions of zip and you need to have unzip >= 6.0.0. To check +you unzip version you can run: `unzip -v`. + +To update your unzip for most operating systems you can simply use your regular package +admin program. However, for Mac we suggest using +`this version of unzip `__. diff --git a/qiita_pet/support_files/doc/source/images/deblur_quality_fig1.png b/qiita_pet/support_files/doc/source/images/deblur_quality_fig1.png new file mode 100644 index 000000000..389b7108e Binary files /dev/null and b/qiita_pet/support_files/doc/source/images/deblur_quality_fig1.png differ diff --git a/qiita_pet/support_files/doc/source/images/deblur_quality_fig2.png b/qiita_pet/support_files/doc/source/images/deblur_quality_fig2.png new file mode 100644 index 000000000..573ae617e Binary files /dev/null and b/qiita_pet/support_files/doc/source/images/deblur_quality_fig2.png differ diff --git a/qiita_pet/support_files/doc/source/images/deblur_quality_fig3.png b/qiita_pet/support_files/doc/source/images/deblur_quality_fig3.png new file mode 100644 index 000000000..d5ba97ad2 Binary files /dev/null and b/qiita_pet/support_files/doc/source/images/deblur_quality_fig3.png differ diff --git a/qiita_pet/support_files/doc/source/images/deblur_quality_fig4.png b/qiita_pet/support_files/doc/source/images/deblur_quality_fig4.png new file mode 100644 index 000000000..99367e8be Binary files /dev/null and b/qiita_pet/support_files/doc/source/images/deblur_quality_fig4.png differ diff --git a/qiita_pet/support_files/doc/source/index.rst b/qiita_pet/support_files/doc/source/index.rst index 499e449fa..9c89af0ca 100644 --- a/qiita_pet/support_files/doc/source/index.rst +++ b/qiita_pet/support_files/doc/source/index.rst @@ -16,6 +16,9 @@ standards as described by this documentation. Easily interface with the EBI repository for automated deposition. Query and interact with Qiita data programmatically. +The latests tutorials can be found in: +`CMI Qiita/GNPS workshop pages `__. + If you intend to use Qiita at the `central deployment `__ we recommend that you have a look at the following documents: @@ -30,3 +33,4 @@ following documents: faq.rst resources.rst processing-recommendations.rst + deblur_quality.rst diff --git a/qiita_pet/support_files/doc/source/qiita-philosophy/index.rst b/qiita_pet/support_files/doc/source/qiita-philosophy/index.rst index 7ed97ef8f..00f79ec0c 100644 --- a/qiita_pet/support_files/doc/source/qiita-philosophy/index.rst +++ b/qiita_pet/support_files/doc/source/qiita-philosophy/index.rst @@ -69,6 +69,77 @@ public, both in Qiita and the permanent repository, Figure 2. study listing page. +Qiita allows for complex study designs +-------------------------------------- + +As seen in Figure 1 studies are the main source of data for Qiita, and studies +can contain only one set of samples but can also contain multiple sets, each of +which can have a different preparations. + +The traditional study design includes a single sample and a single preparation +information file. However as technology improves, study designs become more +complex where a study with a defined set of collected samples can have subsets +prepared in different ways so we can answer different questions. For example, +let's imagine a study looking at how different `microbial communities changes +during mammalian corpse decomposition +`__; thus, your full study design +is to collect a set of samples, which you will then process with 16S, 18S and +ITS primers. This will result in 1 sample and 3 preparation information files, +`see it in Qiita `__. + +Now, let's imagine other more complex example: + +1. All of the samples were prepped for 16S and sequenced in two separate + MiSeq runs + +2. 50 of the samples were prepped for 18S and ITS, and sequenced in a single + MiSeq run + +3. 50 of the samples were prepped for WGS and sequenced on a single + HiSeq run + +4. 30 of the samples have metabolomic profiles + +To represent this project in Qiita, you will need to create a single +study with a single sample information file that contains all 100 of the +samples. Separately, you will need to create four prep information files that +describe the preparations for the corresponding samples. All raw data +uploaded will need to correspond to a specific preparation (prep) information +file. For instance, the data sets described above would require the following +data and prep information: + +1. All of the samples prepped for 16S and sequenced in two separate + MiSeq runs + + a) 1 prep information file describing the two MiSeq runs (use a + run\_prefix column to differentiate between the two MiSeq runs, more + on metadata below) where the 100 samples are represented + b) the 4-6 fastq raw data files without demultiplexing (i.e., the + forward, reverse (optional), and barcodes for each run) + +2. 50 of the samples prepped for 18S and ITS, and sequenced in a single + MiSeq run + + a) prep information files, one describing the 18S and the other describing the + ITS preparations + b) the 2-3 fastq raw data files (forward, reverse (optional), and + barcodes) + +3. 50 of the samples prepped for WGS and sequenced on a single HiSeq run + + a) 1 prep information files describing how the samples were multiplexed + b) the 2-3 fastq raw data files (forward, reverse (optional), and + barcodes). + c) NOTE: We currently do not have a processing pipeline for WGS but + should soon. + +4. 30 of the samples with metabolomic profiles + + a) 1 prep information file. the raw data file(s) from the metabolomic + characterization. + b) NOTE: We currently do not have a processing pipeline for metabolomics but + should soon. + Portals ------- diff --git a/qiita_pet/support_files/doc/source/tutorials/account-creation.rst b/qiita_pet/support_files/doc/source/tutorials/account-creation.rst deleted file mode 100644 index 4eb31c5b7..000000000 --- a/qiita_pet/support_files/doc/source/tutorials/account-creation.rst +++ /dev/null @@ -1,64 +0,0 @@ -.. _account-creation: - -.. index:: account-creation - -Creating an account -=================== - -Open your browser and go to the home page of your Qiita installation; -for the public Qiita instance, this is -`http://qiita.microbio.me `__. -Click on “New User”. - -.. figure:: images/image14.png - :align: center - -.. figure:: images/image07.png - :align: center - -The “New User” link brings you to a page on which you can create a new -account. Optional fields are indicated explicitly, while all other -fields are required. Once the form is submitted, an email will be sent -to you containing instructions on how to verify your email address. Note -that your account will NOT be created until your email address is -verified. If you do not receive the verification email, check your spam -folder. - -If you are in a single user installation you will need to be in a -network (like your house’s or a coffee shop’s) that does not block port -25. Most universities and corporations block this port for security -reasons. - -Logging into your account and resetting a forgotten password ------------------------------------------------------------- - -Once you have created your account, you can log into the system by -entering your email and password. - -.. figure:: images/image03.png - :align: center - -If you forget your password, you will need to reset it.  Click on -“Forgot Password”. - -.. figure:: images/image13.png - :align: center - -This will take you to a page on which to enter your email address; once -you click the “Reset Password” button, the system will send you further -instructions on how to reset your lost password. - -.. figure:: images/image05.png - :align: center - -Updating your settings and changing your password -------------------------------------------------- - -If you need to reset your password or change any general information in -your account, click on your email at the top right corner of the menu -bar to access the page on which you can perform these tasks. - -.. figure:: images/image19.png - :align: center -.. figure:: images/image10.png - :align: center diff --git a/qiita_pet/support_files/doc/source/tutorials/analyze-data.rst b/qiita_pet/support_files/doc/source/tutorials/analyze-data.rst deleted file mode 100644 index e3e977d9d..000000000 --- a/qiita_pet/support_files/doc/source/tutorials/analyze-data.rst +++ /dev/null @@ -1,9 +0,0 @@ -Analyses -======== - -.. warning:: - This tutorial doesn't exist yet, if you want to create a document describing - how to analyze data using Qiita, please see this `document - `__ and this - `issue `__. - diff --git a/qiita_pet/support_files/doc/source/tutorials/getting-started.rst b/qiita_pet/support_files/doc/source/tutorials/getting-started.rst deleted file mode 100644 index 162c36854..000000000 --- a/qiita_pet/support_files/doc/source/tutorials/getting-started.rst +++ /dev/null @@ -1,328 +0,0 @@ -.. _getting-started: - -.. index:: getting-started - -Getting started -=============== - - -.. note:: - This is a work in progress. Please let us know of any additions or changes - you will like to see by emailing us at `qiita.help@gmail.com - `__. - -If you have not yet created an account, please see the document -:doc:`account-creation`. - - -Managing your studies ---------------------- - -Studies are the main source of data for Qiita. Studies can contain only one set -of samples but can contain multiple sets of raw data, each of which can have a -different preparation. Many experiments will contain only one data set, which -includes data for all samples. Such experiments will include a single sample -template and a single prep template.   - -However, Qiita can also support more complex study designs. For example -imagine a study with 100 samples in which: - -1. All of the samples were prepped for 16S and sequenced in two separate - MiSeq runs -2. 50 of the samples were prepped for 18S and ITS, and sequenced in - a single MiSeq run -3. 50 of the samples were prepped for WGS and sequenced on a single - HiSeq run -4. 30 of the samples have metabolomic profiles - -To represent this project in Qiita, you will need to create a single -study with a single sample template that contains all 100 of the -samples. Separately, you will need to create four prep templates that -describe the preparations for the corresponding samples. All raw data -uploaded will need to correspond to a specific prep template. For -instance, the data sets described above would require the following data -and template information: - -1. All of the samples prepped for 16S and sequenced in two separate - MiSeq runs - - a) 1 preparation (prep) template describing the two MiSeq runs (use a - run\_prefix column to differentiate between the two MiSeq runs, more - on metadata below) where the 100 samples are represented - b) the 4-6 fastq raw data files without demultiplexing (i.e., the - forward, reverse (optional), and barcodes for each run) - -2. 50 of the samples prepped for 18S and ITS, and sequenced in a single - MiSeq run - - a) prep templates, one describing the 18S and the other describing the - ITS preparations - b) the 2-3 fastq raw data files (forward, reverse (optional), and - barcodes) - -3. 50 of the samples prepped for WGS and sequenced on a single HiSeq run - - a) 1 prep template describing how the samples were multiplexed - b) the 2-3 fastq raw data files (forward, reverse (optional), and - barcodes). - c) NOTE: We currently do not have a processing pipeline for WGS but - should soon. - -4. 30 of the samples with metabolomic profiles - - a) 1 prep template. the raw data file(s) from the metabolomic - characterization. - b) NOTE: We currently do not have a processing pipeline for metabolomics but - should soon. - -Creating a study ----------------- - -To create a study, click on the “Study” menu and then on “Create Study”. -This will take you to a new page that will gather some basic information -to create your study. - -.. figure:: images/image18.png - :align: center - -The “Study Title” has to be unique system-wide. Qiita will check this -when you try to create the study, and may ask you to alter the study -name if the one you provide is already in use. - -.. figure:: images/image02.png - :align: center - -A principal investigator is required, and a list of known PIs is -provided. If you cannot find the name you are looking for in this -list, you can choose to add a new one. - -Select the environmental package appropriate to your study. Different -packages will request different specific information about your samples. -This information is optional; for more details, see the metadata -section. - -Finally, select the kind of time series you have. The main options are: - -- No time series: the samples do not represent a time series. -- Single intervention: the study has only one intervention, the classic - before/after design. This can be also selected if you are only - following individuals/environments over time without an actual - intervention. -- Multiple intervention: the study includes multiple interventions, - such as 2-3 antibiotic (ABX) interventions. -- Combo: the  samples are a combination of those having single and - multiple interventions. - -Additionally, there is a distinction between real, pseudo or mixed -interventions: - -- Real: the study follows the same individuals over time, so there - are multiple samples from the same individuals. -- Pseudo: the study has time information from diverse individuals; for - example, it includes samples from individuals from 3 to 60 years of - age but has only one sample per individual. -- Mixed: the study is a combination of real and pseudo. - -Once your study has been created, you will be informed by a green -message; click on the study name to begin inserting your sample -template, raw data and/or prep templates. - -.. figure:: images/image04.png - :align: center - -Inserting sample templates --------------------------- - -The first point of entrance to a study is the study description -page. Here you will be able to edit the study info, upload files, and -manage all other aspects of your study. - -.. figure:: images/image09.png - :align: center - -The first step after study creation is uploading files. Click on the -“Upload” button: as shown in the figure below, you can now drag-and-drop -files into the grey area or simply click on “select from your computer” -to select the fastq, fastq.gz or txt files you want to upload. - -Uploads can be paused at any time and restarted again, as long as you do -not refresh or navigate away from the page, or log out of the system -from another page. - -.. figure:: images/image17.png - :align: center - -Once your file(s) have been uploaded, you can process them in Qiita. -From the upload tool, click on “Go to study description” and, once -there, click on the “Sample template” tab.  Select your sample template -from the dropdown menu and, lastly, click “Process sample template”. - -.. figure:: images/process-sample-template.png - :align: center - - If a sample template is processed successfully, a green message will appear; - if processing is unsuccessful, a red message describing the errors will - appear. In this case, please fix the described issues, re-upload your - file, and then re-attempt processing. - -You can download the processed sample template file from the “Sample -template” tab. If you are using a single-user install, you will see the -full path on your computer for downloads; alternately, if you have a multi-user -install, you will be able to download the files, see below: - -.. figure:: images/single-multi.png - :align: center - - An example of how downloads differ between the single- and multi-user - installs. In a single-user install, the file-path on your system is - provided. In a multi-user install, an actual download of the file is - available. - - -Adding a preparation template and linking it to raw data --------------------------------------------------------- - -Once the sample template is successfully processed, you will be able to -use the “Add prep template” tab. - -.. figure:: images/add-prep-template.png - :align: center - -After you've added a new prep template, you can either (a) select a new raw -data file from the drop-down menu of uploaded files or (b) add raw data from -another study to which you have access. The latter ability exists as a way to -avoid duplication of uploads, since some studies share the same raw data (for -example, the same fastq files). - -.. note:: - Prep templates are not shared, only raw data can be shared. - -Here you should select what kind of data you are processing (SFF, FASTQ, etc). -Once the selections are made you can “Link” your raw data. This action will -take you to a new page, where the moving/adding job is created, but you can -move out of there whenever you want. - -.. figure:: images/new-raw-data.png - :align: center - -.. note:: - From that moment until the job is finish, you will see a “Linking files” - message and you will not be able to add any more files or unlink them. - -Adding prep templates is similar to adding sample templates except that, -in addition to selecting the prep template file from the dropdown menu, -you will also need to select what kind of prep template (16S, 18S, etc) -and the corresponding investigation type. The investigation type is -optional for Qiita, but a requirement for submitting your data to -EBI. - -.. figure:: images/image11.png - :align: center - -Finally, when you add a new prep template, you will get two new links or -two full paths for those running Qiita on your local machine: one to -download the prep template you uploaded and another one that is a -QIIME-compatible mapping file. The QIIME mapping file is a combination -of the sample and the prep template. - -Preprocessing data ------------------- - -Once you have linked files to your raw data and your prep template has -been processed, you can then proceed to preprocessing your data. -`Here __` -a list of currently supported raw files files. - -.. figure:: images/image08.png - :align: center - -Once the preprocessing is finished you will have 4 new files: - -- preprocessed fasta: demultiplexed sequences in fasta format -- preprocessed demux: demultiplexed sequences in an HDF5 format (more - demultiplexing process below) -- log: the classic QIIME split libraries log that summarizes the -- preprocessed fastq: demultiplexed sequences in fastq format. - -The HDF5 demuliplexed file format allows (described in detail -`here `__) -for random access to sequences associated with samples, as well as -per-sample statistics. This format originated from the need to fetch -sequences associated with individual samples, which required substantial -overhead when working with ASCII formatted sequence files such as fasta -and fastq. The structure provided by HDF5 enables Qiita to rapidly -access the sequence data for any sample, and additionally, to -efficiently subset (potentially randomly) the corresponding sequences. - -HDF5 can be thought of internally as a filesystem, where directories are -called “groups” and files are called “datasets.” In the HDF5 demux -format, a sample is a group and the sequence data are decomposed into -multiple datasets. Specifically, the following datasets are directly -part of the sample group: - -- sequence, which contains the actual sequence data stored as a vector - of string. -- qual, which contains the quality scores per sequence per nucleotide, - stored as a matrix of integers. Sequences that do not have quality - scores associated (e.g., sourced from a Sanger file) will have zeros - for all positions. - -Barcode details can be found under the “barcode” group of the sample. -Within there are three datasets: - -- original, which contains the original barcodes associated with the - sequences stored as a vector of string. -- corrected, which contains the corrected barcodes (e.g., the result of - a corrected substitution error within the barcode) associated with - the sequences stored as a vector of string. -- error, which contain the number of observed barcode errors per - sequence stored as a vector of integer. - -All datasets within a sample are in index-order. In other words, the -sequence at index zero corresponds to the quality at row zero, -corresponds to the barcode at index zero, etc. - -Last, the following summary statistics are tracked per-sample -(accessible via the group attributes) and per-file (accessible via the -file attributes): - -- n, the number of sequences stored as an integer. -- max, the maximum sequence length stored as an integer. -- min, the minimum sequence length stored as an integer. -- mean, the mean sequence length stored as a floating point value. -- std, the standard deviation of sequence length stored as a floating - point value. -- median, the median sequence length stored as a floating point value. -- hist, a 10-bin histogram of sequence lengths stored as a vector of - integer. -- hist\_edge, the edges of each bin in the sequence length histogram - stored as a vector of integer. - -Once you are happy with these files and you are ready for publication, -you can contact one of the Qiita admins to submit to EBI, this process normally -takes a couple of days but can take more depending on availability and how busy -is the submitting queue. - -Study status ------------- - -- Sandbox. When a study is in this status, all the required metadata - columns must be present in the metadata files (sample and prep), but - the values don't have to be filled in or finalized yet. We suggest adding - TBD as the temporal values of these fields. The purpose - of this status is so that users can quickly upload their sequence - files and some (possibly incomplete) metadata in order to have a - preliminary look at their data. -- Private.  Moving from sandbox to private status requires the user to - correct and finalize their metadata. On the each study overview page, - there is a button that the user can use to request approval. Approval - must be provided by a Qiita admin, who will validate and finalize the - metadata. After a study moves from sandbox to private status, very - little can be changed about the study without reverting the study to - sandbox. -- Public. Once a study is made administrator-approved and becomes - private, the user can choose when to make it public. Making a study - public means that it will be available to anyone with a Qiita user - account (e.g., for data downloads and meta-analyses). When a study - is public it cannot be changed. All associated templates will be public - as well. diff --git a/qiita_pet/support_files/doc/source/tutorials/index.rst b/qiita_pet/support_files/doc/source/tutorials/index.rst index 57f4cfc08..de962aaf0 100644 --- a/qiita_pet/support_files/doc/source/tutorials/index.rst +++ b/qiita_pet/support_files/doc/source/tutorials/index.rst @@ -1,17 +1,17 @@ -Tutorials -========= +Qiita Guides +============ -The following is a full list of the available tutorials: +This is not a tutorial section, for tutorials please visit the +`CMI Qiita/GNPS workshop pages `__. + +The following is a full list of the available guides: .. toctree:: :maxdepth: 2 - account-creation prepare-information-files ebi-submission - getting-started sharing - analyze-data no-raw-sequences join-paired-end-reads diff --git a/qiita_pet/support_files/doc/source/tutorials/prepare-information-files.rst b/qiita_pet/support_files/doc/source/tutorials/prepare-information-files.rst index 72435f14e..ea8d68490 100644 --- a/qiita_pet/support_files/doc/source/tutorials/prepare-information-files.rst +++ b/qiita_pet/support_files/doc/source/tutorials/prepare-information-files.rst @@ -11,11 +11,11 @@ use from the system. As described in :doc:`../qiita-philosophy/index`, a Qiita study can have many biological samples, each with many preparations for different kinds of -multi-omic analysis. As described in :doc:`getting-started`, the study will -have a single *sample information file* that will define the biological context -of each sample. Each multi-omic data type prepared will have a separate -*preparation information file* that will describe the sequencing technology -or analytical chemistry used to generate that data set. +multi-omic analysis. Thus, the study will have a single *sample information +file* that will define the biological context of each sample. Each multi-omic +data type prepared will have a separate *preparation information file* that +will describe the sequencing technology or analytical chemistry used to +generate that data set. Please note that while *sample information* and *preparation information files* are similar to a `QIIME metadata file diff --git a/qiita_pet/templates/list_studies.html b/qiita_pet/templates/list_studies.html index 94656a1df..d3a4de4ac 100644 --- a/qiita_pet/templates/list_studies.html +++ b/qiita_pet/templates/list_studies.html @@ -1,8 +1,9 @@ {% extends sitebase.html %} {% block head %} - + +