diff options
Diffstat (limited to 'wqflask')
73 files changed, 3854 insertions, 2664 deletions
diff --git a/wqflask/base/data_set.py b/wqflask/base/data_set.py index 8906ab69..af248659 100644 --- a/wqflask/base/data_set.py +++ b/wqflask/base/data_set.py @@ -20,7 +20,7 @@ from dataclasses import dataclass from dataclasses import field from dataclasses import InitVar -from typing import Optional, Dict +from typing import Optional, Dict, List from db.call import fetchall, fetchone, fetch1 from utility.logger import getLogger from utility.tools import USE_GN_SERVER, USE_REDIS, flat_files, flat_file_exists, GN2_BASE_URL @@ -39,6 +39,9 @@ from db import webqtlDatabaseFunction from base import species from base import webqtlConfig from flask import Flask, g +from base.webqtlConfig import TMPDIR +from urllib.parse import urlparse +from utility.tools import SQL_URI import os import math import string @@ -50,6 +53,8 @@ import requests import gzip import pickle as pickle import itertools +import hashlib +import datetime from redis import Redis @@ -397,7 +402,8 @@ class DatasetGroup: self.parlist = [maternal, paternal] def get_study_samplelists(self): - study_sample_file = locate_ignore_error(self.name + ".json", 'study_sample_lists') + study_sample_file = locate_ignore_error( + self.name + ".json", 'study_sample_lists') try: f = open(study_sample_file) except: @@ -423,8 +429,6 @@ class DatasetGroup: if result is not None: self.samplelist = json.loads(result) else: - logger.debug("Cache not hit") - genotype_fn = locate_ignore_error(self.name + ".geno", 'genotype') if genotype_fn: self.samplelist = get_group_samplelists.get_samplelist( @@ -447,7 +451,6 @@ class DatasetGroup: # genotype_1 is Dataset Object without parents and f1 # genotype_2 is Dataset Object with parents and f1 (not for intercross) - # reaper barfs on unicode filenames, so here we ensure it's a string if self.genofile: if "RData" in self.genofile: # ZS: This is a temporary fix; I need to change the way the JSON files that point to multiple genotype files are structured to point to other file types like RData @@ -726,7 +729,6 @@ class DataSet: data_results = self.chunk_dataset(query_results, len(sample_ids)) self.samplelist = sorted_samplelist self.trait_data = data_results - def get_trait_data(self, sample_list=None): if sample_list: @@ -745,66 +747,75 @@ class DataSet: and Species.name = '{}' """.format(create_in_clause(self.samplelist), *mescape(self.group.species)) results = dict(g.db.execute(query).fetchall()) - sample_ids = [results[item] for item in self.samplelist] + sample_ids = [results.get(item) + for item in self.samplelist if item is not None] # MySQL limits the number of tables that can be used in a join to 61, # so we break the sample ids into smaller chunks # Postgres doesn't have that limit, so we can get rid of this after we transition chunk_size = 50 number_chunks = int(math.ceil(len(sample_ids) / chunk_size)) - trait_sample_data = [] - for sample_ids_step in chunks.divide_into_chunks(sample_ids, number_chunks): - if self.type == "Publish": - dataset_type = "Phenotype" - else: - dataset_type = self.type - temp = ['T%s.value' % item for item in sample_ids_step] - if self.type == "Publish": - query = "SELECT {}XRef.Id,".format(escape(self.type)) - else: - query = "SELECT {}.Name,".format(escape(dataset_type)) - data_start_pos = 1 - query += ', '.join(temp) - query += ' FROM ({}, {}XRef, {}Freeze) '.format(*mescape(dataset_type, - self.type, - self.type)) - - for item in sample_ids_step: - query += """ - left join {}Data as T{} on T{}.Id = {}XRef.DataId - and T{}.StrainId={}\n - """.format(*mescape(self.type, item, item, self.type, item, item)) - - if self.type == "Publish": - query += """ - WHERE {}XRef.InbredSetId = {}Freeze.InbredSetId - and {}Freeze.Name = '{}' - and {}.Id = {}XRef.{}Id - order by {}.Id - """.format(*mescape(self.type, self.type, self.type, self.name, - dataset_type, self.type, dataset_type, dataset_type)) - else: - query += """ - WHERE {}XRef.{}FreezeId = {}Freeze.Id - and {}Freeze.Name = '{}' - and {}.Id = {}XRef.{}Id - order by {}.Id - """.format(*mescape(self.type, self.type, self.type, self.type, - self.name, dataset_type, self.type, self.type, dataset_type)) - results = g.db.execute(query).fetchall() - trait_sample_data.append(results) + cached_results = fetch_cached_results(self.name, self.type) + if cached_results is None: + trait_sample_data = [] + for sample_ids_step in chunks.divide_into_chunks(sample_ids, number_chunks): + if self.type == "Publish": + dataset_type = "Phenotype" + else: + dataset_type = self.type + temp = ['T%s.value' % item for item in sample_ids_step] + if self.type == "Publish": + query = "SELECT {}XRef.Id,".format(escape(self.type)) + else: + query = "SELECT {}.Name,".format(escape(dataset_type)) + data_start_pos = 1 + query += ', '.join(temp) + query += ' FROM ({}, {}XRef, {}Freeze) '.format(*mescape(dataset_type, + self.type, + self.type)) + + for item in sample_ids_step: + query += """ + left join {}Data as T{} on T{}.Id = {}XRef.DataId + and T{}.StrainId={}\n + """.format(*mescape(self.type, item, item, self.type, item, item)) + + if self.type == "Publish": + query += """ + WHERE {}XRef.InbredSetId = {}Freeze.InbredSetId + and {}Freeze.Name = '{}' + and {}.Id = {}XRef.{}Id + order by {}.Id + """.format(*mescape(self.type, self.type, self.type, self.name, + dataset_type, self.type, dataset_type, dataset_type)) + else: + query += """ + WHERE {}XRef.{}FreezeId = {}Freeze.Id + and {}Freeze.Name = '{}' + and {}.Id = {}XRef.{}Id + order by {}.Id + """.format(*mescape(self.type, self.type, self.type, self.type, + self.name, dataset_type, self.type, self.type, dataset_type)) - trait_count = len(trait_sample_data[0]) - self.trait_data = collections.defaultdict(list) + results = g.db.execute(query).fetchall() + trait_sample_data.append([list(result) for result in results]) - # put all of the separate data together into a dictionary where the keys are - # trait names and values are lists of sample values - for trait_counter in range(trait_count): - trait_name = trait_sample_data[0][trait_counter][0] - for chunk_counter in range(int(number_chunks)): - self.trait_data[trait_name] += ( - trait_sample_data[chunk_counter][trait_counter][data_start_pos:]) + trait_count = len(trait_sample_data[0]) + self.trait_data = collections.defaultdict(list) + + data_start_pos = 1 + for trait_counter in range(trait_count): + trait_name = trait_sample_data[0][trait_counter][0] + for chunk_counter in range(int(number_chunks)): + self.trait_data[trait_name] += ( + trait_sample_data[chunk_counter][trait_counter][data_start_pos:]) + + cache_dataset_results( + self.name, self.type, self.trait_data) + else: + + self.trait_data = cached_results class PhenotypeDataSet(DataSet): @@ -1242,3 +1253,65 @@ def geno_mrna_confidentiality(ob): if confidential: return True + + +def parse_db_url(): + parsed_db = urlparse(SQL_URI) + + return (parsed_db.hostname, parsed_db.username, + parsed_db.password, parsed_db.path[1:]) + + +def query_table_timestamp(dataset_type: str): + """function to query the update timestamp of a given dataset_type""" + + # computation data and actions + + fetch_db_name = parse_db_url() + query_update_time = f""" + SELECT UPDATE_TIME FROM information_schema.tables + WHERE TABLE_SCHEMA = '{fetch_db_name[-1]}' + AND TABLE_NAME = '{dataset_type}Data' + """ + + date_time_obj = g.db.execute(query_update_time).fetchone()[0] + return date_time_obj.strftime("%Y-%m-%d %H:%M:%S") + + +def generate_hash_file(dataset_name: str, dataset_type: str, dataset_timestamp: str): + """given the trait_name generate a unique name for this""" + string_unicode = f"{dataset_name}{dataset_timestamp}".encode() + md5hash = hashlib.md5(string_unicode) + return md5hash.hexdigest() + + +def cache_dataset_results(dataset_name: str, dataset_type: str, query_results: List): + """function to cache dataset query results to file + input dataset_name and type query_results(already processed in default dict format) + """ + # data computations actions + # store the file path on redis + + table_timestamp = query_table_timestamp(dataset_type) + + + file_name = generate_hash_file(dataset_name, dataset_type, table_timestamp) + file_path = os.path.join(TMPDIR, f"{file_name}.json") + + with open(file_path, "w") as file_handler: + json.dump(query_results, file_handler) + + +def fetch_cached_results(dataset_name: str, dataset_type: str): + """function to fetch the cached results""" + + table_timestamp = query_table_timestamp(dataset_type) + + file_name = generate_hash_file(dataset_name, dataset_type, table_timestamp) + file_path = os.path.join(TMPDIR, f"{file_name}.json") + try: + with open(file_path, "r") as file_handler: + + return json.load(file_handler) + except FileNotFoundError: + pass diff --git a/wqflask/base/trait.py b/wqflask/base/trait.py index 96a09302..f0749858 100644 --- a/wqflask/base/trait.py +++ b/wqflask/base/trait.py @@ -7,7 +7,7 @@ from base.webqtlCaseData import webqtlCaseData from base.data_set import create_dataset from utility import hmac from utility.authentication_tools import check_resource_availability -from utility.tools import GN2_BASE_URL +from utility.tools import GN2_BASE_URL, GN_PROXY_URL from utility.redis_tools import get_redis_conn, get_resource_id from utility.db_tools import escape @@ -361,10 +361,10 @@ def retrieve_trait_info(trait, dataset, get_qtl_info=False): resource_id = get_resource_id(dataset, trait.name) if dataset.type == 'Publish': - the_url = "http://localhost:8080/run-action?resource={}&user={}&branch=data&action=view".format( + the_url = GN_PROXY_URL + "run-action?resource={}&user={}&branch=data&action=view".format( resource_id, g.user_session.user_id) else: - the_url = "http://localhost:8080/run-action?resource={}&user={}&branch=data&action=view&trait={}".format( + the_url = GN_PROXY_URL + "run-action?resource={}&user={}&branch=data&action=view&trait={}".format( resource_id, g.user_session.user_id, trait.name) try: diff --git a/wqflask/maintenance/quantile_normalize.py b/wqflask/maintenance/quantile_normalize.py index 0cc963e5..32780ca6 100644 --- a/wqflask/maintenance/quantile_normalize.py +++ b/wqflask/maintenance/quantile_normalize.py @@ -5,14 +5,10 @@ import urllib.parse import numpy as np import pandas as pd -from elasticsearch import Elasticsearch, TransportError -from elasticsearch.helpers import bulk from flask import Flask, g, request from wqflask import app -from utility.elasticsearch_tools import get_elasticsearch_connection -from utility.tools import ELASTICSEARCH_HOST, ELASTICSEARCH_PORT, SQL_URI def parse_db_uri(): @@ -106,20 +102,6 @@ if __name__ == '__main__': Conn = MySQLdb.Connect(**parse_db_uri()) Cursor = Conn.cursor() - # es = Elasticsearch([{ - # "host": ELASTICSEARCH_HOST, "port": ELASTICSEARCH_PORT - # }], timeout=60) if (ELASTICSEARCH_HOST and ELASTICSEARCH_PORT) else None - - es = get_elasticsearch_connection(for_user=False) - - #input_filename = "/home/zas1024/cfw_data/" + sys.argv[1] + ".txt" - #input_df = create_dataframe(input_filename) - #output_df = quantileNormalize(input_df) - - #output_df.to_csv('quant_norm.csv', sep='\t') - - #out_filename = sys.argv[1][:-4] + '_quantnorm.txt' - success, _ = bulk(es, set_data(sys.argv[1])) response = es.search( diff --git a/wqflask/tests/unit/wqflask/test_markdown_routes.py b/wqflask/tests/unit/wqflask/api/test_markdown_routes.py index 90e0f17c..1c513ac5 100644 --- a/wqflask/tests/unit/wqflask/test_markdown_routes.py +++ b/wqflask/tests/unit/wqflask/api/test_markdown_routes.py @@ -1,10 +1,10 @@ -"""Test functions in markdown utils""" +"""Test functions for wqflask/api/markdown.py""" import unittest from unittest import mock from dataclasses import dataclass -from wqflask.markdown_routes import render_markdown +from wqflask.api.markdown import render_markdown @dataclass @@ -24,9 +24,9 @@ This is another sub-heading""" class TestMarkdownRoutesFunctions(unittest.TestCase): - """Test cases for functions in markdown_routes""" + """Test cases for functions in markdown""" - @mock.patch('wqflask.markdown_routes.requests.get') + @mock.patch('wqflask.api.markdown.requests.get') def test_render_markdown_when_fetching_locally(self, requests_mock): requests_mock.return_value = MockRequests404() markdown_content = render_markdown("general/glossary/glossary.md") @@ -38,7 +38,7 @@ class TestMarkdownRoutesFunctions(unittest.TestCase): self.assertRegexpMatches(markdown_content, "Content for general/glossary/glossary.md not available.") - @mock.patch('wqflask.markdown_routes.requests.get') + @mock.patch('wqflask.api.markdown.requests.get') def test_render_markdown_when_fetching_remotely(self, requests_mock): requests_mock.return_value = MockRequests200() markdown_content = render_markdown("general/glossary/glossary.md") diff --git a/wqflask/tests/unit/wqflask/test_resource_manager.py b/wqflask/tests/unit/wqflask/test_resource_manager.py new file mode 100644 index 00000000..f4335425 --- /dev/null +++ b/wqflask/tests/unit/wqflask/test_resource_manager.py @@ -0,0 +1,94 @@ +"""Test cases for wqflask/resource_manager.py""" +import json +import unittest + +from unittest import mock +from gn3.authentication import get_user_membership +from gn3.authentication import get_highest_user_access_role +from gn3.authentication import DataRole +from gn3.authentication import AdminRole + + +class TestGetUserMembership(unittest.TestCase): + """Test cases for `get_user_membership`""" + + def setUp(self): + conn = mock.MagicMock() + conn.hgetall.return_value = { + '7fa95d07-0e2d-4bc5-b47c-448fdc1260b2': ( + '{"name": "editors", ' + '"admins": ["8ad942fe-490d-453e-bd37-56f252e41604", "rand"], ' + '"members": ["8ad942fe-490d-453e-bd37-56f252e41603", ' + '"rand"], ' + '"changed_timestamp": "Oct 06 2021 06:39PM", ' + '"created_timestamp": "Oct 06 2021 06:39PM"}')} + self.conn = conn + + def test_user_is_group_member_only(self): + """Test that a user is only a group member""" + self.assertEqual( + get_user_membership( + conn=self.conn, + user_id="8ad942fe-490d-453e-bd37-56f252e41603", + group_id="7fa95d07-0e2d-4bc5-b47c-448fdc1260b2"), + {"member": True, + "admin": False}) + + def test_user_is_group_admin_only(self): + """Test that a user is a group admin only""" + self.assertEqual( + get_user_membership( + conn=self.conn, + user_id="8ad942fe-490d-453e-bd37-56f252e41604", + group_id="7fa95d07-0e2d-4bc5-b47c-448fdc1260b2"), + {"member": False, + "admin": True}) + + def test_user_is_both_group_member_and_admin(self): + """Test that a user is both an admin and member of a group""" + self.assertEqual( + get_user_membership( + conn=self.conn, + user_id="rand", + group_id="7fa95d07-0e2d-4bc5-b47c-448fdc1260b2"), + {"member": True, + "admin": True}) + + +class TestCheckUserAccessRole(unittest.TestCase): + """Test cases for `get_highest_user_access_role`""" + + @mock.patch("wqflask.resource_manager.requests.get") + def test_edit_access(self, requests_mock): + """Test that the right access roles are set""" + response = mock.PropertyMock(return_value=json.dumps( + { + 'data': ['no-access', 'view', 'edit', ], + 'metadata': ['no-access', 'view', 'edit', ], + 'admin': ['not-admin', 'edit-access', ], + } + )) + type(requests_mock.return_value).content = response + self.assertEqual(get_highest_user_access_role( + resource_id="0196d92e1665091f202f", + user_id="8ad942fe-490d-453e-bd37"), + {"data": DataRole.EDIT, + "metadata": DataRole.EDIT, + "admin": AdminRole.EDIT_ACCESS}) + + @mock.patch("wqflask.resource_manager.requests.get") + def test_no_access(self, requests_mock): + response = mock.PropertyMock(return_value=json.dumps( + { + 'data': ['no-access', ], + 'metadata': ['no-access', ], + 'admin': ['not-admin', ], + } + )) + type(requests_mock.return_value).content = response + self.assertEqual(get_highest_user_access_role( + resource_id="0196d92e1665091f202f", + user_id=""), + {"data": DataRole.NO_ACCESS, + "metadata": DataRole.NO_ACCESS, + "admin": AdminRole.NOT_ADMIN}) diff --git a/wqflask/tests/unit/wqflask/wgcna/__init__.py b/wqflask/tests/unit/wqflask/wgcna/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/wqflask/tests/unit/wqflask/wgcna/__init__.py diff --git a/wqflask/tests/unit/wqflask/wgcna/test_wgcna.py b/wqflask/tests/unit/wqflask/wgcna/test_wgcna.py new file mode 100644 index 00000000..8e947e2f --- /dev/null +++ b/wqflask/tests/unit/wqflask/wgcna/test_wgcna.py @@ -0,0 +1,50 @@ + +"""module contains for processing gn3 wgcna data""" +from unittest import TestCase + +from wqflask.wgcna.gn3_wgcna import process_wgcna_data + + +class DataProcessingTests(TestCase): + """class contains data processing tests""" + + def test_data_processing(self): + """test for parsing data for datatable""" + output = { + "input": { + "sample_names": ["BXD1", "BXD2", "BXD3", "BXD4", "BXD5", "BXD6"], + + }, + "output": { + "ModEigens": { + "MEturquoise": [ + 0.0646677768085351, + 0.137200224277058, + 0.63451113720732, + -0.544002665501479, + -0.489487590361863, + 0.197111117570427 + ], + "MEgrey": [ + 0.213, + 0.214, + 0.3141, + -0.545, + -0.423, + 0.156, + ] + }}} + + row_data = [['BXD1', 0.065, 0.213], + ['BXD2', 0.137, 0.214], + ['BXD3', 0.635, 0.314], + ['BXD4', -0.544, -0.545], + ['BXD5', -0.489, -0.423], + ['BXD6', 0.197, 0.156]] + + expected_results = { + "col_names": ["sample_names", "MEturquoise", "MEgrey"], + "mod_dataset": row_data + } + + self.assertEqual(process_wgcna_data(output), expected_results) diff --git a/wqflask/utility/authentication_tools.py b/wqflask/utility/authentication_tools.py index 6802d689..afea69e1 100644 --- a/wqflask/utility/authentication_tools.py +++ b/wqflask/utility/authentication_tools.py @@ -4,11 +4,12 @@ import requests from flask import g from base import webqtlConfig - from utility.redis_tools import (get_redis_conn, get_resource_info, get_resource_id, add_resource) +from utility.tools import GN_PROXY_URL + Redis = get_redis_conn() def check_resource_availability(dataset, trait_id=None): @@ -24,19 +25,19 @@ def check_resource_availability(dataset, trait_id=None): if resource_id: resource_info = get_resource_info(resource_id) - # ZS: If resource isn't already in redis, add it with default + # If resource isn't already in redis, add it with default # privileges if not resource_info: resource_info = add_new_resource(dataset, trait_id) - # ZS: Check if super-user - we should probably come up with some + # Check if super-user - we should probably come up with some # way to integrate this into the proxy if g.user_session.user_id in Redis.smembers("super_users"): return webqtlConfig.SUPER_PRIVILEGES response = None - the_url = "http://localhost:8080/available?resource={}&user={}".format( + the_url = GN_PROXY_URL + "available?resource={}&user={}".format( resource_id, g.user_session.user_id) try: @@ -93,7 +94,7 @@ def get_group_code(dataset): def check_admin(resource_id=None): - the_url = "http://localhost:8080/available?resource={}&user={}".format( + the_url = GN_PROXY_URL + "available?resource={}&user={}".format( resource_id, g.user_session.user_id) try: response = json.loads(requests.get(the_url).content)['admin'] diff --git a/wqflask/utility/chunks.py b/wqflask/utility/chunks.py index 484b5de6..f6e88cbe 100644 --- a/wqflask/utility/chunks.py +++ b/wqflask/utility/chunks.py @@ -1,5 +1,4 @@ import math -import time def divide_into_chunks(the_list, number_chunks): diff --git a/wqflask/utility/elasticsearch_tools.py b/wqflask/utility/elasticsearch_tools.py deleted file mode 100644 index eae3ba03..00000000 --- a/wqflask/utility/elasticsearch_tools.py +++ /dev/null @@ -1,121 +0,0 @@ -# Elasticsearch support -# -# Some helpful commands to view the database: -# -# You can test the server being up with -# -# curl -H 'Content-Type: application/json' http://localhost:9200 -# -# List all indices -# -# curl -H 'Content-Type: application/json' 'localhost:9200/_cat/indices?v' -# -# To see the users index 'table' -# -# curl http://localhost:9200/users -# -# To list all user ids -# -# curl -H 'Content-Type: application/json' http://localhost:9200/users/local/_search?pretty=true -d ' -# { -# "query" : { -# "match_all" : {} -# }, -# "stored_fields": [] -# }' -# -# To view a record -# -# curl -H 'Content-Type: application/json' http://localhost:9200/users/local/_search?pretty=true -d ' -# { -# "query" : { -# "match" : { "email_address": "pjotr2017@thebird.nl"} -# } -# }' -# -# -# To delete the users index and data (dangerous!) -# -# curl -XDELETE -H 'Content-Type: application/json' 'localhost:9200/users' - - -from elasticsearch import Elasticsearch, TransportError -import logging - -from utility.logger import getLogger -logger = getLogger(__name__) - -from utility.tools import ELASTICSEARCH_HOST, ELASTICSEARCH_PORT - - -def test_elasticsearch_connection(): - es = Elasticsearch(['http://' + ELASTICSEARCH_HOST + \ - ":" + str(ELASTICSEARCH_PORT) + '/'], verify_certs=True) - if not es.ping(): - logger.warning("Elasticsearch is DOWN") - - -def get_elasticsearch_connection(for_user=True): - """Return a connection to ES. Returns None on failure""" - logger.info("get_elasticsearch_connection") - es = None - try: - assert(ELASTICSEARCH_HOST) - assert(ELASTICSEARCH_PORT) - logger.info("ES HOST", ELASTICSEARCH_HOST) - - es = Elasticsearch([{ - "host": ELASTICSEARCH_HOST, "port": ELASTICSEARCH_PORT - }], timeout=30, retry_on_timeout=True) if (ELASTICSEARCH_HOST and ELASTICSEARCH_PORT) else None - - if for_user: - setup_users_index(es) - - es_logger = logging.getLogger("elasticsearch") - es_logger.setLevel(logging.INFO) - es_logger.addHandler(logging.NullHandler()) - except Exception as e: - logger.error("Failed to get elasticsearch connection", e) - es = None - - return es - - -def setup_users_index(es_connection): - if es_connection: - index_settings = { - "properties": { - "email_address": { - "type": "keyword"}}} - - es_connection.indices.create(index='users', ignore=400) - es_connection.indices.put_mapping( - body=index_settings, index="users", doc_type="local") - - -def get_user_by_unique_column(es, column_name, column_value, index="users", doc_type="local"): - return get_item_by_unique_column(es, column_name, column_value, index=index, doc_type=doc_type) - - -def save_user(es, user, user_id): - es_save_data(es, "users", "local", user, user_id) - - -def get_item_by_unique_column(es, column_name, column_value, index, doc_type): - item_details = None - try: - response = es.search( - index=index, doc_type=doc_type, body={ - "query": {"match": {column_name: column_value}} - }) - if len(response["hits"]["hits"]) > 0: - item_details = response["hits"]["hits"][0]["_source"] - except TransportError as te: - pass - return item_details - - -def es_save_data(es, index, doc_type, data_item, data_id,): - from time import sleep - es.create(index, doc_type, body=data_item, id=data_id) - sleep(1) # Delay 1 second to allow indexing diff --git a/wqflask/utility/redis_tools.py b/wqflask/utility/redis_tools.py index de9dde46..a6c5875f 100644 --- a/wqflask/utility/redis_tools.py +++ b/wqflask/utility/redis_tools.py @@ -57,30 +57,6 @@ def get_user_by_unique_column(column_name, column_value): return item_details -def get_users_like_unique_column(column_name, column_value): - """Like previous function, but this only checks if the input is a - subset of a field and can return multiple results - - """ - matched_users = [] - - if column_value != "": - user_list = Redis.hgetall("users") - if column_name != "user_id": - for key in user_list: - user_ob = json.loads(user_list[key]) - if "user_id" not in user_ob: - set_user_attribute(key, "user_id", key) - user_ob["user_id"] = key - if column_name in user_ob: - if column_value in user_ob[column_name]: - matched_users.append(user_ob) - else: - matched_users.append(load_json_from_redis(user_list, column_value)) - - return matched_users - - def set_user_attribute(user_id, column_name, column_value): user_info = json.loads(Redis.hget("users", user_id)) user_info[column_name] = column_value @@ -165,52 +141,6 @@ def get_group_info(group_id): return group_info -def get_group_by_unique_column(column_name, column_value): - """ Get group by column; not sure if there's a faster way to do this """ - - matched_groups = [] - - all_group_list = Redis.hgetall("groups") - for key in all_group_list: - group_info = json.loads(all_group_list[key]) - # ZS: Since these fields are lists, search in the list - if column_name == "admins" or column_name == "members": - if column_value in group_info[column_name]: - matched_groups.append(group_info) - else: - if group_info[column_name] == column_value: - matched_groups.append(group_info) - - return matched_groups - - -def get_groups_like_unique_column(column_name, column_value): - """Like previous function, but this only checks if the input is a - subset of a field and can return multiple results - - """ - matched_groups = [] - - if column_value != "": - group_list = Redis.hgetall("groups") - if column_name != "group_id": - for key in group_list: - group_info = json.loads(group_list[key]) - # ZS: Since these fields are lists, search in the list - if column_name == "admins" or column_name == "members": - if column_value in group_info[column_name]: - matched_groups.append(group_info) - else: - if column_name in group_info: - if column_value in group_info[column_name]: - matched_groups.append(group_info) - else: - matched_groups.append( - load_json_from_redis(group_list, column_value)) - - return matched_groups - - def create_group(admin_user_ids, member_user_ids=[], group_name="Default Group Name"): group_id = str(uuid.uuid4()) @@ -352,11 +282,3 @@ def add_access_mask(resource_id, group_id, access_mask): Redis.hset("resources", resource_id, json.dumps(the_resource)) return the_resource - - -def change_resource_owner(resource_id, new_owner_id): - the_resource = get_resource_info(resource_id) - the_resource['owner_id'] = new_owner_id - - Redis.delete("resource") - Redis.hset("resources", resource_id, json.dumps(the_resource)) diff --git a/wqflask/utility/tools.py b/wqflask/utility/tools.py index e28abb48..db0b4320 100644 --- a/wqflask/utility/tools.py +++ b/wqflask/utility/tools.py @@ -194,7 +194,6 @@ def locate(name, subdir=None): if valid_path(base): lookfor = base + "/" + name if valid_file(lookfor): - logger.info("Found: file " + lookfor + "\n") return lookfor else: raise Exception("Can not locate " + lookfor) @@ -220,9 +219,7 @@ def locate_ignore_error(name, subdir=None): if valid_path(base): lookfor = base + "/" + name if valid_file(lookfor): - logger.debug("Found: file " + name + "\n") return lookfor - logger.info("WARNING: file " + name + " not found\n") return None @@ -266,6 +263,8 @@ WEBSERVER_MODE = get_setting('WEBSERVER_MODE') GN2_BASE_URL = get_setting('GN2_BASE_URL') GN2_BRANCH_URL = get_setting('GN2_BRANCH_URL') GN_SERVER_URL = get_setting('GN_SERVER_URL') +GN_PROXY_URL = get_setting('GN_PROXY_URL') +GN3_LOCAL_URL = get_setting('GN3_LOCAL_URL') SERVER_PORT = get_setting_int('SERVER_PORT') SQL_URI = get_setting('SQL_URI') LOG_LEVEL = get_setting('LOG_LEVEL') @@ -285,6 +284,7 @@ JS_GN_PATH = get_setting('JS_GN_PATH') GITHUB_CLIENT_ID = get_setting('GITHUB_CLIENT_ID') GITHUB_CLIENT_SECRET = get_setting('GITHUB_CLIENT_SECRET') +GITHUB_AUTH_URL = "" if GITHUB_CLIENT_ID != 'UNKNOWN' and GITHUB_CLIENT_SECRET: GITHUB_AUTH_URL = "https://github.com/login/oauth/authorize?client_id=" + \ GITHUB_CLIENT_ID + "&client_secret=" + GITHUB_CLIENT_SECRET @@ -299,10 +299,6 @@ if ORCID_CLIENT_ID != 'UNKNOWN' and ORCID_CLIENT_SECRET: "&redirect_uri=" + GN2_BRANCH_URL + "n/login/orcid_oauth2" ORCID_TOKEN_URL = get_setting('ORCID_TOKEN_URL') -ELASTICSEARCH_HOST = get_setting('ELASTICSEARCH_HOST') -ELASTICSEARCH_PORT = get_setting('ELASTICSEARCH_PORT') -# import utility.elasticsearch_tools as es -# es.test_elasticsearch_connection() SMTP_CONNECT = get_setting('SMTP_CONNECT') SMTP_USERNAME = get_setting('SMTP_USERNAME') diff --git a/wqflask/wqflask/__init__.py b/wqflask/wqflask/__init__.py index 8b2055cd..05e040ed 100644 --- a/wqflask/wqflask/__init__.py +++ b/wqflask/wqflask/__init__.py @@ -8,14 +8,23 @@ from flask import Flask from typing import Tuple from urllib.parse import urlparse from utility import formatting -from wqflask.markdown_routes import glossary_blueprint -from wqflask.markdown_routes import references_blueprint -from wqflask.markdown_routes import links_blueprint -from wqflask.markdown_routes import policies_blueprint -from wqflask.markdown_routes import environments_blueprint -from wqflask.markdown_routes import facilities_blueprint -from wqflask.markdown_routes import blogs_blueprint -from wqflask.markdown_routes import news_blueprint + +from gn3.authentication import DataRole, AdminRole + +from wqflask.group_manager import group_management +from wqflask.resource_manager import resource_management +from wqflask.metadata_edits import metadata_edit + +from wqflask.api.markdown import glossary_blueprint +from wqflask.api.markdown import references_blueprint +from wqflask.api.markdown import links_blueprint +from wqflask.api.markdown import policies_blueprint +from wqflask.api.markdown import environments_blueprint +from wqflask.api.markdown import facilities_blueprint +from wqflask.api.markdown import blogs_blueprint +from wqflask.api.markdown import news_blueprint + +from wqflask.jupyter_notebooks import jupyter_notebooks app = Flask(__name__) @@ -54,7 +63,11 @@ app.register_blueprint(environments_blueprint, url_prefix="/environments") app.register_blueprint(facilities_blueprint, url_prefix="/facilities") app.register_blueprint(blogs_blueprint, url_prefix="/blogs") app.register_blueprint(news_blueprint, url_prefix="/news") +app.register_blueprint(jupyter_notebooks, url_prefix="/jupyter_notebooks") +app.register_blueprint(resource_management, url_prefix="/resource-management") +app.register_blueprint(metadata_edit, url_prefix="/datasets/") +app.register_blueprint(group_management, url_prefix="/group-management") @app.before_request def before_request(): @@ -62,6 +75,16 @@ def before_request(): g.request_time = lambda: "%.5fs" % (time.time() - g.request_start_time) +@app.context_processor +def include_admin_role_class(): + return {'AdminRole': AdminRole} + + +@app.context_processor +def include_data_role_class(): + return {'DataRole': DataRole} + + from wqflask.api import router from wqflask import group_manager from wqflask import resource_manager diff --git a/wqflask/wqflask/api/correlation.py b/wqflask/wqflask/api/correlation.py index 870f3275..9b875c99 100644 --- a/wqflask/wqflask/api/correlation.py +++ b/wqflask/wqflask/api/correlation.py @@ -1,23 +1,13 @@ import collections - import scipy -from utility.db_tools import escape - -from flask import g - from base import data_set from base.trait import create_trait, retrieve_sample_data - -from wqflask.correlation.show_corr_results import generate_corr_json +from flask import g +from utility import corr_result_helpers +from utility.db_tools import escape from wqflask.correlation import correlation_functions -from utility import webqtlUtil, helper_functions, corr_result_helpers -from utility.benchmark import Bench - -import utility.logger -logger = utility.logger.getLogger(__name__) - def do_correlation(start_vars): assert('db' in start_vars) @@ -35,7 +25,6 @@ def do_correlation(start_vars): corr_results = calculate_results( this_trait, this_dataset, target_dataset, corr_params) - #corr_results = collections.OrderedDict(sorted(corr_results.items(), key=lambda t: -abs(t[1][0]))) final_results = [] for _trait_counter, trait in enumerate(list(corr_results.keys())[:corr_params['return_count']]): @@ -63,11 +52,7 @@ def do_correlation(start_vars): "#_strains": num_overlap, "p_value": sample_p } - final_results.append(result_dict) - - # json_corr_results = generate_corr_json(final_corr_results, this_trait, this_dataset, target_dataset, for_api = True) - return final_results diff --git a/wqflask/wqflask/api/gen_menu.py b/wqflask/wqflask/api/gen_menu.py index a699a484..5d239343 100644 --- a/wqflask/wqflask/api/gen_menu.py +++ b/wqflask/wqflask/api/gen_menu.py @@ -1,4 +1,8 @@ from gn3.db.species import get_all_species + +import utility.logger +logger = utility.logger.getLogger(__name__) + def gen_dropdown_json(conn): """Generates and outputs (as json file) the data for the main dropdown menus on the home page @@ -19,16 +23,16 @@ def get_groups(species, conn): with conn.cursor() as cursor: for species_name, _species_full_name in species: groups[species_name] = [] - cursor.execute( - ("SELECT InbredSet.Name, InbredSet.FullName, " + query = ("SELECT InbredSet.Name, InbredSet.FullName, " "IFNULL(InbredSet.Family, 'None') " "FROM InbredSet, Species WHERE Species.Name = '{}' " "AND InbredSet.SpeciesId = Species.Id GROUP by " "InbredSet.Name ORDER BY IFNULL(InbredSet.FamilyOrder, " "InbredSet.FullName) ASC, IFNULL(InbredSet.Family, " "InbredSet.FullName) ASC, InbredSet.FullName ASC, " - "InbredSet.MenuOrderId ASC") - .format(species_name)) + "InbredSet.MenuOrderId ASC").format(species_name) + # logger.debug(query) + cursor.execute(query) results = cursor.fetchall() for result in results: family_name = "Family:" + str(result[2]) diff --git a/wqflask/wqflask/markdown_routes.py b/wqflask/wqflask/api/markdown.py index 580b9ac0..580b9ac0 100644 --- a/wqflask/wqflask/markdown_routes.py +++ b/wqflask/wqflask/api/markdown.py diff --git a/wqflask/wqflask/collect.py b/wqflask/wqflask/collect.py index 01274ba9..76ef5ca4 100644 --- a/wqflask/wqflask/collect.py +++ b/wqflask/wqflask/collect.py @@ -12,6 +12,7 @@ from flask import flash from wqflask import app from utility import hmac from utility.formatting import numify +from utility.tools import GN_SERVER_URL from utility.redis_tools import get_redis_conn from base.trait import create_trait @@ -218,8 +219,10 @@ def view_collection(): json_version.append(jsonable(trait_ob)) - collection_info = dict(trait_obs=trait_obs, - uc=uc) + collection_info = dict( + trait_obs=trait_obs, + uc=uc, + heatmap_data_url=f"{GN_SERVER_URL}heatmaps/clustered") if "json" in params: return json.dumps(json_version) diff --git a/wqflask/wqflask/correlation/correlation_gn3_api.py b/wqflask/wqflask/correlation/correlation_gn3_api.py index a18bceaf..c2acd648 100644 --- a/wqflask/wqflask/correlation/correlation_gn3_api.py +++ b/wqflask/wqflask/correlation/correlation_gn3_api.py @@ -1,14 +1,18 @@ """module that calls the gn3 api's to do the correlation """ import json +import time +from functools import wraps from wqflask.correlation import correlation_functions - +from wqflask.correlation.pre_computes import fetch_precompute_results +from wqflask.correlation.pre_computes import cache_compute_results from base import data_set from base.trait import create_trait from base.trait import retrieve_sample_data from gn3.computations.correlations import compute_all_sample_correlation +from gn3.computations.correlations import fast_compute_all_sample_correlation from gn3.computations.correlations import map_shared_keys_to_values from gn3.computations.correlations import compute_all_lit_correlation from gn3.computations.correlations import compute_tissue_correlation @@ -19,9 +23,11 @@ def create_target_this_trait(start_vars): """this function creates the required trait and target dataset for correlation""" if start_vars['dataset'] == "Temp": - this_dataset = data_set.create_dataset(dataset_name="Temp", dataset_type="Temp", group_name=start_vars['group']) + this_dataset = data_set.create_dataset( + dataset_name="Temp", dataset_type="Temp", group_name=start_vars['group']) else: - this_dataset = data_set.create_dataset(dataset_name=start_vars['dataset']) + this_dataset = data_set.create_dataset( + dataset_name=start_vars['dataset']) target_dataset = data_set.create_dataset( dataset_name=start_vars['corr_dataset']) this_trait = create_trait(dataset=this_dataset, @@ -58,13 +64,19 @@ def test_process_data(this_trait, dataset, start_vars): return sample_data -def process_samples(start_vars, sample_names, excluded_samples=None): - """process samples""" +def process_samples(start_vars, sample_names=[], excluded_samples=[]): + """code to fetch correct samples""" sample_data = {} - if not excluded_samples: - excluded_samples = () - sample_vals_dict = json.loads(start_vars["sample_vals"]) + sample_vals_dict = json.loads(start_vars["sample_vals"]) + if sample_names: for sample in sample_names: + if sample in sample_vals_dict and sample not in excluded_samples: + val = sample_vals_dict[sample] + if not val.strip().lower() == "x": + sample_data[str(sample)] = float(val) + + else: + for sample in sample_vals_dict.keys(): if sample not in excluded_samples: val = sample_vals_dict[sample] if not val.strip().lower() == "x": @@ -147,6 +159,18 @@ def lit_for_trait_list(corr_results, this_dataset, this_trait): def fetch_sample_data(start_vars, this_trait, this_dataset, target_dataset): + corr_samples_group = start_vars["corr_samples_group"] + if corr_samples_group == "samples_primary": + sample_data = process_samples( + start_vars, this_dataset.group.all_samples_ordered()) + + elif corr_samples_group == "samples_other": + sample_data = process_samples( + start_vars, excluded_samples=this_dataset.group.samplelist) + + else: + sample_data = process_samples(start_vars) + sample_data = process_samples( start_vars, this_dataset.group.all_samples_ordered()) @@ -187,9 +211,9 @@ def compute_correlation(start_vars, method="pearson", compute_all=False): if corr_type == "sample": (this_trait_data, target_dataset_data) = fetch_sample_data( start_vars, this_trait, this_dataset, target_dataset) - correlation_results = compute_all_sample_correlation(corr_method=method, - this_trait=this_trait_data, - target_dataset=target_dataset_data) + + correlation_results = compute_all_sample_correlation( + corr_method=method, this_trait=this_trait_data, target_dataset=target_dataset_data) elif corr_type == "tissue": trait_symbol_dict = this_dataset.retrieve_genes("Symbol") @@ -290,7 +314,8 @@ def get_tissue_correlation_input(this_trait, trait_symbol_dict): """Gets tissue expression values for the primary trait and target tissues values""" primary_trait_tissue_vals_dict = correlation_functions.get_trait_symbol_and_tissue_values( symbol_list=[this_trait.symbol]) - if this_trait.symbol.lower() in primary_trait_tissue_vals_dict: + if this_trait.symbol and this_trait.symbol.lower() in primary_trait_tissue_vals_dict: + primary_trait_tissue_values = primary_trait_tissue_vals_dict[this_trait.symbol.lower( )] corr_result_tissue_vals_dict = correlation_functions.get_trait_symbol_and_tissue_values( diff --git a/wqflask/wqflask/correlation/pre_computes.py b/wqflask/wqflask/correlation/pre_computes.py new file mode 100644 index 00000000..975a53b8 --- /dev/null +++ b/wqflask/wqflask/correlation/pre_computes.py @@ -0,0 +1,158 @@ +import json +import os +import hashlib +from pathlib import Path + +from base.data_set import query_table_timestamp +from base.webqtlConfig import TMPDIR + + +def fetch_all_cached_metadata(dataset_name): + """in a gvein dataset fetch all the traits metadata""" + file_name = generate_filename(dataset_name, suffix="metadata") + + file_path = os.path.join(TMPDIR, file_name) + + try: + with open(file_path, "r+") as file_handler: + dataset_metadata = json.load(file_handler) + return (file_path, dataset_metadata) + + except FileNotFoundError: + Path(file_path).touch(exist_ok=True) + return (file_path, {}) + + +def cache_new_traits_metadata(dataset_metadata: dict, new_traits_metadata, file_path: str): + """function to cache the new traits metadata""" + + if bool(new_traits_metadata): + dataset_metadata.update(new_traits_metadata) + + with open(file_path, "w+") as file_handler: + json.dump(dataset_metadata, file_handler) + + +def generate_filename(*args, suffix="", file_ext="json"): + """given a list of args generate a unique filename""" + + string_unicode = f"{*args,}".encode() + return f"{hashlib.md5(string_unicode).hexdigest()}_{suffix}.{file_ext}" + + +def cache_compute_results(base_dataset_type, + base_dataset_name, + target_dataset_name, + corr_method, + correlation_results, + trait_name): + """function to cache correlation results for heavy computations""" + + base_timestamp = query_table_timestamp(base_dataset_type) + + target_dataset_timestamp = base_timestamp + + file_name = generate_filename( + base_dataset_name, target_dataset_name, + base_timestamp, target_dataset_timestamp, + suffix="corr_precomputes") + + file_path = os.path.join(TMPDIR, file_name) + + try: + with open(file_path, "r+") as json_file_handler: + data = json.load(json_file_handler) + + data[trait_name] = correlation_results + + json_file_handler.seek(0) + + json.dump(data, json_file_handler) + + json_file_handler.truncate() + + except FileNotFoundError: + with open(file_path, "w+") as file_handler: + data = {} + data[trait_name] = correlation_results + + json.dump(data, file_handler) + + +def fetch_precompute_results(base_dataset_name, + target_dataset_name, + dataset_type, + trait_name): + """function to check for precomputed results""" + + base_timestamp = target_dataset_timestamp = query_table_timestamp( + dataset_type) + file_name = generate_filename( + base_dataset_name, target_dataset_name, + base_timestamp, target_dataset_timestamp, + suffix="corr_precomputes") + + file_path = os.path.join(TMPDIR, file_name) + + try: + with open(file_path, "r+") as json_handler: + correlation_results = json.load(json_handler) + + return correlation_results.get(trait_name) + + except FileNotFoundError: + pass + + +def pre_compute_dataset_vs_dataset(base_dataset, + target_dataset, + corr_method): + """compute sample correlation between dataset vs dataset + wn:heavy function should be invoked less frequently + input:datasets_data(two dicts),corr_method + + output:correlation results for entire dataset against entire dataset + """ + dataset_correlation_results = {} + + target_traits_data, base_traits_data = get_datasets_data( + base_dataset, target_dataset_data) + + for (primary_trait_name, strain_values) in base_traits_data: + + this_trait_data = { + "trait_sample_data": strain_values, + "trait_id": primary_trait_name + } + + trait_correlation_result = compute_all_sample_correlation( + corr_method=corr_method, + this_trait=this_trait_data, + target_dataset=target_traits_data) + + dataset_correlation_results[primary_trait_name] = trait_correlation_result + + return dataset_correlation_results + + +def get_datasets_data(base_dataset, target_dataset_data): + """required to pass data in a given format to the pre compute + function + + (works for bxd only probeset datasets) + + output:two dicts for datasets with key==trait and value==strains + """ + samples_fetched = base_dataset.group.all_samples_ordered() + target_traits_data = target_dataset.get_trait_data( + samples_fetched) + + base_traits_data = base_dataset.get_trait_data( + samples_fetched) + + target_results = map_shared_keys_to_values( + samples_fetched, target_traits_data) + base_results = map_shared_keys_to_values( + samples_fetched, base_traits_data) + + return (target_results, base_results) diff --git a/wqflask/wqflask/correlation/show_corr_results.py b/wqflask/wqflask/correlation/show_corr_results.py index d73965da..1c391386 100644 --- a/wqflask/wqflask/correlation/show_corr_results.py +++ b/wqflask/wqflask/correlation/show_corr_results.py @@ -19,10 +19,17 @@ # This module is used by GeneNetwork project (www.genenetwork.org) import json +import os +from pathlib import Path from base.trait import create_trait, jsonable from base.data_set import create_dataset +from base.webqtlConfig import TMPDIR +from wqflask.correlation.pre_computes import fetch_all_cached_metadata +from wqflask.correlation.pre_computes import cache_new_traits_metadata + +from utility.authentication_tools import check_resource_availability from utility import hmac @@ -31,7 +38,8 @@ def set_template_vars(start_vars, correlation_data): corr_method = start_vars['corr_sample_method'] if start_vars['dataset'] == "Temp": - this_dataset_ob = create_dataset(dataset_name="Temp", dataset_type="Temp", group_name=start_vars['group']) + this_dataset_ob = create_dataset( + dataset_name="Temp", dataset_type="Temp", group_name=start_vars['group']) else: this_dataset_ob = create_dataset(dataset_name=start_vars['dataset']) this_trait = create_trait(dataset=this_dataset_ob, @@ -82,13 +90,29 @@ def correlation_json_for_table(correlation_data, this_trait, this_dataset, targe corr_results = correlation_data['correlation_results'] results_list = [] + + new_traits_metadata = {} + + (file_path, dataset_metadata) = fetch_all_cached_metadata( + target_dataset['name']) + for i, trait_dict in enumerate(corr_results): trait_name = list(trait_dict.keys())[0] trait = trait_dict[trait_name] - target_trait_ob = create_trait(dataset=target_dataset_ob, - name=trait_name, - get_qtl_info=True) - target_trait = jsonable(target_trait_ob, target_dataset_ob) + + target_trait = dataset_metadata.get(trait_name) + if target_trait is None: + target_trait_ob = create_trait(dataset=target_dataset_ob, + name=trait_name, + get_qtl_info=True) + target_trait = jsonable(target_trait_ob, target_dataset_ob) + new_traits_metadata[trait_name] = target_trait + else: + if target_dataset['type'] == "Publish": + permissions = check_resource_availability(target_dataset_ob, trait_name) + if permissions['metadata'] == "no-access": + continue + if target_trait['view'] == False: continue results_dict = {} @@ -163,6 +187,10 @@ def correlation_json_for_table(correlation_data, this_trait, this_dataset, targe results_list.append(results_dict) + cache_new_traits_metadata(dataset_metadata, + new_traits_metadata, + file_path) + return json.dumps(results_list) diff --git a/wqflask/wqflask/decorators.py b/wqflask/wqflask/decorators.py index 54aa6795..41d23084 100644 --- a/wqflask/wqflask/decorators.py +++ b/wqflask/wqflask/decorators.py @@ -1,36 +1,80 @@ """This module contains gn2 decorators""" -from flask import g +import redis + +from flask import current_app, g, redirect, request, url_for from typing import Dict +from urllib.parse import urljoin from functools import wraps -from utility.hmac import hmac_creation +from gn3.authentication import AdminRole +from gn3.authentication import DataRole import json import requests +def login_required(f): + """Use this for endpoints where login is required""" + @wraps(f) + def wrap(*args, **kwargs): + user_id = ((g.user_session.record.get(b"user_id") or + b"").decode("utf-8") + or g.user_session.record.get("user_id") or "") + redis_conn = redis.from_url(current_app.config["REDIS_URL"], + decode_responses=True) + if not redis_conn.hget("users", user_id): + return "You need to be logged in!", 401 + return f(*args, **kwargs) + return wrap + + def edit_access_required(f): - """Use this for endpoints where admins are required""" + """Use this for endpoints where people with admin or edit privileges +are required""" @wraps(f) def wrap(*args, **kwargs): resource_id: str = "" - if kwargs.get("inbredset_id"): # data type: dataset-publish - resource_id = hmac_creation("dataset-publish:" - f"{kwargs.get('inbredset_id')}:" - f"{kwargs.get('name')}") - if kwargs.get("dataset_name"): # data type: dataset-probe - resource_id = hmac_creation("dataset-probeset:" - f"{kwargs.get('dataset_name')}") + if request.args.get("resource-id"): + resource_id = request.args.get("resource-id") + elif kwargs.get("resource_id"): + resource_id = kwargs.get("resource_id") + response: Dict = {} + try: + user_id = ((g.user_session.record.get(b"user_id") or + b"").decode("utf-8") + or g.user_session.record.get("user_id") or "") + response = json.loads( + requests.get(urljoin( + current_app.config.get("GN2_PROXY"), + ("available?resource=" + f"{resource_id}&user={user_id}"))).content) + except: + response = {} + if max([DataRole(role) for role in response.get( + "data", ["no-access"])]) < DataRole.EDIT: + return redirect(url_for("no_access_page")) + return f(*args, **kwargs) + return wrap + + +def edit_admins_access_required(f): + """Use this for endpoints where ownership of a resource is required""" + @wraps(f) + def wrap(*args, **kwargs): + resource_id: str = kwargs.get("resource_id", "") response: Dict = {} try: - _user_id = g.user_session.record.get(b"user_id", - "").decode("utf-8") + user_id = ((g.user_session.record.get(b"user_id") or + b"").decode("utf-8") + or g.user_session.record.get("user_id") or "") response = json.loads( - requests.get("http://localhost:8080/" - "available?resource=" - f"{resource_id}&user={_user_id}").content) + requests.get(urljoin( + current_app.config.get("GN2_PROXY"), + ("available?resource=" + f"{resource_id}&user={user_id}"))).content) except: response = {} - if "edit" not in response.get("data", []): - return "You need to be admin", 401 + if max([AdminRole(role) for role in response.get( + "admin", ["not-admin"])]) < AdminRole.EDIT_ADMINS: + return redirect(url_for("no_access_page")) return f(*args, **kwargs) return wrap diff --git a/wqflask/wqflask/do_search.py b/wqflask/wqflask/do_search.py index 761ae326..99272ee3 100644 --- a/wqflask/wqflask/do_search.py +++ b/wqflask/wqflask/do_search.py @@ -81,16 +81,31 @@ class MrnaAssaySearch(DoSearch): DoSearch.search_types['ProbeSet'] = "MrnaAssaySearch" - base_query = """SELECT distinct ProbeSet.Name as TNAME, - 0 as thistable, - ProbeSetXRef.Mean as TMEAN, - ProbeSetXRef.LRS as TLRS, - ProbeSetXRef.PVALUE as TPVALUE, - ProbeSet.Chr_num as TCHR_NUM, - ProbeSet.Mb as TMB, - ProbeSet.Symbol as TSYMBOL, - ProbeSet.name_num as TNAME_NUM - FROM ProbeSetXRef, ProbeSet """ + base_query = """ + SELECT + ProbeSetFreeze.`Name`, + ProbeSetFreeze.`FullName`, + ProbeSet.`Name`, + ProbeSet.`Symbol`, + CAST(ProbeSet.`description` AS BINARY), + CAST(ProbeSet.`Probe_Target_Description` AS BINARY), + ProbeSet.`Chr`, + ProbeSet.`Mb`, + ProbeSetXRef.`Mean`, + ProbeSetXRef.`LRS`, + ProbeSetXRef.`Locus`, + ProbeSetXRef.`pValue`, + ProbeSetXRef.`additive`, + Geno.`Chr` as geno_chr, + Geno.`Mb` as geno_mb + FROM Species + INNER JOIN InbredSet ON InbredSet.`SpeciesId`= Species.`Id` + INNER JOIN ProbeFreeze ON ProbeFreeze.`InbredSetId` = InbredSet.`Id` + INNER JOIN Tissue ON ProbeFreeze.`TissueId` = Tissue.`Id` + INNER JOIN ProbeSetFreeze ON ProbeSetFreeze.`ProbeFreezeId` = ProbeFreeze.`Id` + INNER JOIN ProbeSetXRef ON ProbeSetXRef.`ProbeSetFreezeId` = ProbeSetFreeze.`Id` + INNER JOIN ProbeSet ON ProbeSet.`Id` = ProbeSetXRef.`ProbeSetId` + LEFT JOIN Geno ON ProbeSetXRef.`Locus` = Geno.`Name` AND Geno.`SpeciesId` = Species.`Id` """ header_fields = ['Index', 'Record', @@ -193,10 +208,25 @@ class PhenotypeSearch(DoSearch): DoSearch.search_types['Publish'] = "PhenotypeSearch" base_query = """SELECT PublishXRef.Id, - PublishFreeze.createtime as thistable, - Publication.PubMed_ID as Publication_PubMed_ID, - Phenotype.Post_publication_description as Phenotype_Name - FROM Phenotype, PublishFreeze, Publication, PublishXRef """ + CAST(Phenotype.`Pre_publication_description` AS BINARY), + CAST(Phenotype.`Post_publication_description` AS BINARY), + Publication.`Authors`, + Publication.`Year`, + Publication.`PubMed_ID`, + PublishXRef.`mean`, + PublishXRef.`LRS`, + PublishXRef.`additive`, + PublishXRef.`Locus`, + InbredSet.`InbredSetCode`, + Geno.`Chr`, + Geno.`Mb` + FROM Species + INNER JOIN InbredSet ON InbredSet.`SpeciesId` = Species.`Id` + INNER JOIN PublishXRef ON PublishXRef.`InbredSetId` = InbredSet.`Id` + INNER JOIN PublishFreeze ON PublishFreeze.`InbredSetId` = InbredSet.`Id` + INNER JOIN Publication ON Publication.`Id` = PublishXRef.`PublicationId` + INNER JOIN Phenotype ON Phenotype.`Id` = PublishXRef.`PhenotypeId` + LEFT JOIN Geno ON PublishXRef.Locus = Geno.Name AND Geno.SpeciesId = Species.Id """ search_fields = ('Phenotype.Post_publication_description', 'Phenotype.Pre_publication_description', @@ -382,12 +412,10 @@ class RifSearch(MrnaAssaySearch): DoSearch.search_types['ProbeSet_RIF'] = "RifSearch" def get_from_clause(self): - return ", GeneRIF_BASIC " + return f" INNER JOIN GeneRIF_BASIC ON GeneRIF_BASIC.`symbol` = { self.dataset.type }.`symbol` " def get_where_clause(self): - where_clause = """( %s.symbol = GeneRIF_BASIC.symbol and - MATCH (GeneRIF_BASIC.comment) - AGAINST ('+%s' IN BOOLEAN MODE)) """ % (self.dataset.type, self.search_term[0]) + where_clause = f"(MATCH (GeneRIF_BASIC.comment) AGAINST ('+{ self.search_term[0] }' IN BOOLEAN MODE)) " return where_clause @@ -487,10 +515,7 @@ class LrsSearch(DoSearch): self.search_term = converted_search_term - if len(self.search_term) > 2: - from_clause = ", Geno" - else: - from_clause = "" + from_clause = "" return from_clause diff --git a/wqflask/wqflask/export_traits.py b/wqflask/wqflask/export_traits.py index 5459dc31..f4e04a4b 100644 --- a/wqflask/wqflask/export_traits.py +++ b/wqflask/wqflask/export_traits.py @@ -48,7 +48,7 @@ def export_search_results_csv(targs): if targs['filter_term'] != "None": metadata.append(["Search Filter Terms: " + targs['filter_term']]) metadata.append(["Exported Row Number: " + str(len(table_rows))]) - metadata.append(["Funding for The GeneNetwork: NIGMS (R01 GM123489, 2017-2021), NIDA (P30 DA044223, 2017-2022), NIA (R01AG043930, 2013-2018), NIAAA (U01 AA016662, U01 AA013499, U24 AA013513, U01 AA014425, 2006-2017), NIDA/NIMH/NIAAA (P20-DA 21131, 2001-2012), NCI MMHCC (U01CA105417), NCRR/BIRN (U24 RR021760)"]) + metadata.append(["Funding for The GeneNetwork: NIGMS (R01 GM123489, 2017-2026), NIDA (P30 DA044223, 2017-2022), NIA (R01AG043930, 2013-2018), NIAAA (U01 AA016662, U01 AA013499, U24 AA013513, U01 AA014425, 2006-2017), NIDA/NIMH/NIAAA (P20-DA 21131, 2001-2012), NCI MMHCC (U01CA105417), NCRR/BIRN (U24 RR021760)"]) metadata.append([]) trait_list = [] diff --git a/wqflask/wqflask/group_manager.py b/wqflask/wqflask/group_manager.py index 04a100ba..3936e36e 100644 --- a/wqflask/wqflask/group_manager.py +++ b/wqflask/wqflask/group_manager.py @@ -1,175 +1,157 @@ -import random -import string - -from flask import (Flask, g, render_template, url_for, request, make_response, - redirect, flash) - -from wqflask import app -from wqflask.user_login import send_verification_email, send_invitation_email, basic_info, set_password - -from utility.redis_tools import get_user_groups, get_group_info, save_user, create_group, delete_group, add_users_to_group, remove_users_from_group, \ - change_group_name, save_verification_code, check_verification_code, get_user_by_unique_column, get_resources, get_resource_info - -from utility.logger import getLogger -logger = getLogger(__name__) - - -@app.route("/groups/manage", methods=('GET', 'POST')) -def manage_groups(): - params = request.form if request.form else request.args - if "add_new_group" in params: - return redirect(url_for('add_group')) - else: - admin_groups, member_groups = get_user_groups(g.user_session.user_id) - return render_template("admin/group_manager.html", admin_groups=admin_groups, member_groups=member_groups) - - -@app.route("/groups/view", methods=('GET', 'POST')) -def view_group(): - params = request.form if request.form else request.args - group_id = params['id'] - group_info = get_group_info(group_id) - admins_info = [] - user_is_admin = False - if g.user_session.user_id in group_info['admins']: - user_is_admin = True - for user_id in group_info['admins']: - if user_id: - user_info = get_user_by_unique_column("user_id", user_id) - admins_info.append(user_info) - members_info = [] - for user_id in group_info['members']: - if user_id: - user_info = get_user_by_unique_column("user_id", user_id) - members_info.append(user_info) - - # ZS: This whole part might not scale well with many resources - resources_info = [] - all_resources = get_resources() - for resource_id in all_resources: - resource_info = get_resource_info(resource_id) - group_masks = resource_info['group_masks'] - if group_id in group_masks: - this_resource = {} - privileges = group_masks[group_id] - this_resource['id'] = resource_id - this_resource['name'] = resource_info['name'] - this_resource['data'] = privileges['data'] - this_resource['metadata'] = privileges['metadata'] - this_resource['admin'] = privileges['admin'] - resources_info.append(this_resource) - - return render_template("admin/view_group.html", group_info=group_info, admins=admins_info, members=members_info, user_is_admin=user_is_admin, resources=resources_info) - - -@app.route("/groups/remove", methods=('POST',)) -def remove_groups(): - group_ids_to_remove = request.form['selected_group_ids'] - for group_id in group_ids_to_remove.split(":"): - delete_group(g.user_session.user_id, group_id) - - return redirect(url_for('manage_groups')) - - -@app.route("/groups/remove_users", methods=('POST',)) -def remove_users(): - group_id = request.form['group_id'] - admin_ids_to_remove = request.form['selected_admin_ids'] - member_ids_to_remove = request.form['selected_member_ids'] - - remove_users_from_group(g.user_session.user_id, admin_ids_to_remove.split( - ":"), group_id, user_type="admins") - remove_users_from_group(g.user_session.user_id, member_ids_to_remove.split( - ":"), group_id, user_type="members") - - return redirect(url_for('view_group', id=group_id)) - - -@app.route("/groups/add_<path:user_type>", methods=('POST',)) -def add_users(user_type='members'): - group_id = request.form['group_id'] - if user_type == "admins": - user_emails = request.form['admin_emails_to_add'].split(",") - add_users_to_group(g.user_session.user_id, group_id, - user_emails, admins=True) - elif user_type == "members": - user_emails = request.form['member_emails_to_add'].split(",") - add_users_to_group(g.user_session.user_id, group_id, - user_emails, admins=False) - - return redirect(url_for('view_group', id=group_id)) - - -@app.route("/groups/change_name", methods=('POST',)) -def change_name(): - group_id = request.form['group_id'] - new_name = request.form['new_name'] - group_info = change_group_name(g.user_session.user_id, group_id, new_name) - - return new_name - - -@app.route("/groups/create", methods=('GET', 'POST')) -def add_or_edit_group(): - params = request.form if request.form else request.args - if "group_name" in params: - member_user_ids = set() - admin_user_ids = set() - # ZS: Always add the user creating the group as an admin - admin_user_ids.add(g.user_session.user_id) - if "admin_emails_to_add" in params: - admin_emails = params['admin_emails_to_add'].split(",") - for email in admin_emails: - user_details = get_user_by_unique_column( - "email_address", email) - if user_details: - admin_user_ids.add(user_details['user_id']) - #send_group_invites(params['group_id'], user_email_list = admin_emails, user_type="admins") - if "member_emails_to_add" in params: - member_emails = params['member_emails_to_add'].split(",") - for email in member_emails: - user_details = get_user_by_unique_column( - "email_address", email) - if user_details: - member_user_ids.add(user_details['user_id']) - #send_group_invites(params['group_id'], user_email_list = user_emails, user_type="members") - - create_group(list(admin_user_ids), list( - member_user_ids), params['group_name']) - return redirect(url_for('manage_groups')) - else: - return render_template("admin/create_group.html") - -# ZS: Will integrate this later, for now just letting users be added directly - - -def send_group_invites(group_id, user_email_list=[], user_type="members"): - for user_email in user_email_list: - user_details = get_user_by_unique_column("email_address", user_email) - if user_details: - group_info = get_group_info(group_id) - # ZS: Probably not necessary since the group should normally always exist if group_id is being passed here, - # but it's technically possible to hit it if Redis is cleared out before submitting the new users or something - if group_info: - # ZS: Don't add user if they're already an admin or if they're being added a regular user and are already a regular user, - # but do add them if they're a regular user and are added as an admin - if (user_details['user_id'] in group_info['admins']) or \ - ((user_type == "members") and (user_details['user_id'] in group_info['members'])): - continue - else: - send_verification_email(user_details, template_name="email/group_verification.txt", - key_prefix="verification_code", subject="You've been invited to join a GeneNetwork user group") - else: - temp_password = ''.join(random.choice( - string.ascii_uppercase + string.digits) for _ in range(6)) - user_details = { - 'user_id': str(uuid.uuid4()), - 'email_address': user_email, - 'registration_info': basic_info(), - 'password': set_password(temp_password), - 'confirmed': 0 - } - save_user(user_details, user_details['user_id']) - send_invitation_email(user_email, temp_password) - -# @app.route() +import json +import redis +import datetime + +from flask import current_app +from flask import Blueprint +from flask import g +from flask import render_template +from flask import request +from flask import redirect +from flask import url_for +from gn3.authentication import get_groups_by_user_uid +from gn3.authentication import get_user_info_by_key +from gn3.authentication import create_group +from wqflask.decorators import login_required + +group_management = Blueprint("group_management", __name__) + + +@group_management.route("/groups") +@login_required +def display_groups(): + groups = get_groups_by_user_uid( + user_uid=(g.user_session.record.get(b"user_id", + b"").decode("utf-8") or + g.user_session.record.get("user_id", "")), + conn=redis.from_url( + current_app.config["REDIS_URL"], + decode_responses=True)) + return render_template("admin/group_manager.html", + admin_groups=groups.get("admin"), + member_groups=groups.get("member")) + + +@group_management.route("/groups/create", methods=("GET",)) +@login_required +def view_create_group_page(): + return render_template("admin/create_group.html") + + +@group_management.route("/groups/create", methods=("POST",)) +@login_required +def create_new_group(): + conn = redis.from_url(current_app.config["REDIS_URL"], + decode_responses=True) + if group_name := request.form.get("group_name"): + members_uid, admins_uid = set(), set() + admins_uid.add(user_uid := ( + g.user_session.record.get( + b"user_id", + b"").decode("utf-8") or + g.user_session.record.get("user_id", ""))) + if admin_string := request.form.get("admin_emails_to_add"): + for email in admin_string.split(","): + user_info = get_user_info_by_key(key="email_address", + value=email, + conn=conn) + if user_uid := user_info.get("user_id"): + admins_uid.add(user_uid) + if member_string := request.form.get("member_emails_to_add"): + for email in member_string.split(","): + user_info = get_user_info_by_key(key="email_address", + value=email, + conn=conn) + if user_uid := user_info.get("user_id"): + members_uid.add(user_uid) + + # Create the new group: + create_group(conn=conn, + group_name=group_name, + member_user_uids=list(members_uid), + admin_user_uids=list(admins_uid)) + return redirect(url_for('group_management.display_groups')) + return redirect(url_for('group_management.create_groups')) + + +@group_management.route("/groups/delete", methods=("POST",)) +@login_required +def delete_groups(): + conn = redis.from_url(current_app.config["REDIS_URL"], + decode_responses=True) + user_uid = (g.user_session.record.get(b"user_id", b"").decode("utf-8") or + g.user_session.record.get("user_id", "")) + current_app.logger.info(request.form.get("selected_group_ids")) + for group_uid in request.form.get("selected_group_ids", "").split(":"): + if group_info := conn.hget("groups", group_uid): + group_info = json.loads(group_info) + # A user who is an admin can delete things + if user_uid in group_info.get("admins"): + conn.hdel("groups", group_uid) + return redirect(url_for('group_management.display_groups')) + + +@group_management.route("/groups/<group_id>") +@login_required +def view_group(group_id: str): + conn = redis.from_url(current_app.config["REDIS_URL"], + decode_responses=True) + user_uid = (g.user_session.record.get(b"user_id", b"").decode("utf-8") or + g.user_session.record.get("user_id", "")) + + resource_info = [] + for resource_uid, resource in conn.hgetall("resources").items(): + resource = json.loads(resource) + if group_id in (group_mask := resource.get("group_masks")): + __dict = {} + for val in group_mask.values(): + __dict.update(val) + __dict.update({ + "id": resource_uid, + "name": resource.get("name"), + }) + resource_info.append(__dict) + group_info = json.loads(conn.hget("groups", + group_id)) + group_info["guid"] = group_id + + return render_template( + "admin/view_group.html", + group_info=group_info, + admins=[get_user_info_by_key(key="user_id", + value=user_id, + conn=conn) + for user_id in group_info.get("admins")], + members=[get_user_info_by_key(key="user_id", + value=user_id, + conn=conn) + for user_id in group_info.get("members")], + is_admin = (True if user_uid in group_info.get("admins") else False), + resources=resource_info) + + +@group_management.route("/groups/<group_id>", methods=("POST",)) +def update_group(group_id: str): + conn = redis.from_url(current_app.config["REDIS_URL"], + decode_responses=True) + user_uid = (g.user_session.record.get(b"user_id", b"").decode("utf-8") or + g.user_session.record.get("user_id", "")) + group = json.loads(conn.hget("groups", group_id)) + timestamp = group["changed_timestamp"] + timestamp_ = datetime.datetime.utcnow().strftime('%b %d %Y %I:%M%p') + if user_uid in group.get("admins"): + if name := request.form.get("new_name"): + group["name"] = name + group["changed_timestamp"] = timestamp_ + if admins := request.form.get("admin_emails_to_add"): + group["admins"] = list(set(admins.split(":") + + group.get("admins"))) + group["changed_timestamp"] = timestamp_ + if members := request.form.get("member_emails_to_add"): + print(f"\n+++++\n{members}\n+++++\n") + group["members"] = list(set(members.split(":") + + group.get("members"))) + group["changed_timestamp"] = timestamp_ + conn.hset("groups", group_id, json.dumps(group)) + return redirect(url_for('group_management.view_group', + group_id=group_id)) diff --git a/wqflask/wqflask/gsearch.py b/wqflask/wqflask/gsearch.py index 2516e4fb..53a124d0 100644 --- a/wqflask/wqflask/gsearch.py +++ b/wqflask/wqflask/gsearch.py @@ -82,13 +82,14 @@ class GSearch: this_trait['species'] = line[0] this_trait['group'] = line[1] this_trait['tissue'] = line[2] - this_trait['symbol'] = line[6] + this_trait['symbol'] = "N/A" + if line[6]: + this_trait['symbol'] = line[6] + this_trait['description'] = "N/A" if line[7]: this_trait['description'] = line[7].decode( 'utf-8', 'replace') - else: - this_trait['description'] = "N/A" - this_trait['location_repr'] = 'N/A' + this_trait['location_repr'] = "N/A" if (line[8] != "NULL" and line[8] != "") and (line[9] != 0): this_trait['location_repr'] = 'Chr%s: %.6f' % ( line[8], float(line[9])) @@ -118,7 +119,7 @@ class GSearch: this_trait['dataset_id'] = line[15] dataset_ob = SimpleNamespace( - id=this_trait["dataset_id"], type="ProbeSet", species=this_trait["species"]) + id=this_trait["dataset_id"], type="ProbeSet", name=this_trait["dataset"], species=this_trait["species"]) if dataset_ob.id not in dataset_to_permissions: permissions = check_resource_availability(dataset_ob) dataset_to_permissions[dataset_ob.id] = permissions diff --git a/wqflask/wqflask/jupyter_notebooks.py b/wqflask/wqflask/jupyter_notebooks.py new file mode 100644 index 00000000..7d76828e --- /dev/null +++ b/wqflask/wqflask/jupyter_notebooks.py @@ -0,0 +1,17 @@ +from flask import Blueprint, render_template + +jupyter_notebooks = Blueprint('jupyter_notebooks', __name__) + +@jupyter_notebooks.route("/launcher", methods=("GET",)) +def launcher(): + links = ( + { + "main_url": "http://notebook.genenetwork.org/34301/notebooks/genenetwork-api-using-r.ipynb", + "notebook_name": "R notebook showing how to query the GeneNetwork API.", + "src_link_url": "https://github.com/jgarte/genenetwork-api-r-jupyter-notebook"}, + { + "main_url": "http://notebook.genenetwork.org/57675/notebooks/genenetwork.ipynb", + "notebook_name": "Querying the GeneNetwork API declaratively with python.", + "src_link_url": "https://github.com/jgarte/genenetwork-jupyter-notebook-example"}) + + return render_template("jupyter_notebooks.html", links=links) diff --git a/wqflask/wqflask/marker_regression/display_mapping_results.py b/wqflask/wqflask/marker_regression/display_mapping_results.py index 6254b9b9..920a8d30 100644 --- a/wqflask/wqflask/marker_regression/display_mapping_results.py +++ b/wqflask/wqflask/marker_regression/display_mapping_results.py @@ -2471,12 +2471,6 @@ class DisplayMappingResults: thisLRSColor = self.colorCollection[0] if qtlresult['chr'] != previous_chr and self.selectedChr == -1: if self.manhattan_plot != True: - # im_drawer.polygon( - # xy=LRSCoordXY, - # outline=thisLRSColor - # # , closed=0, edgeWidth=lrsEdgeWidth, - # # clipX=(xLeftOffset, xLeftOffset + plotWidth) - # ) draw_open_polygon(canvas, xy=LRSCoordXY, outline=thisLRSColor, width=lrsEdgeWidth) @@ -2497,25 +2491,21 @@ class DisplayMappingResults: im_drawer.line( xy=((Xc0, Yc0), (Xcm, yZero)), fill=plusColor, width=lineWidth - # , clipX=(xLeftOffset, xLeftOffset + plotWidth) ) im_drawer.line( xy=((Xcm, yZero), (Xc, yZero - (Yc - yZero))), fill=minusColor, width=lineWidth - # , clipX=(xLeftOffset, xLeftOffset + plotWidth) ) else: im_drawer.line( xy=((Xc0, yZero - (Yc0 - yZero)), (Xcm, yZero)), fill=minusColor, width=lineWidth - # , clipX=(xLeftOffset, xLeftOffset + plotWidth) ) im_drawer.line( xy=((Xcm, yZero), (Xc, Yc)), fill=plusColor, width=lineWidth - # , clipX=(xLeftOffset, xLeftOffset + plotWidth) ) elif (Yc0 - yZero) * (Yc - yZero) > 0: if Yc < yZero: @@ -2523,14 +2513,12 @@ class DisplayMappingResults: xy=((Xc0, Yc0), (Xc, Yc)), fill=plusColor, width=lineWidth - # , clipX=(xLeftOffset, xLeftOffset + plotWidth) ) else: im_drawer.line( xy=((Xc0, yZero - (Yc0 - yZero)), (Xc, yZero - (Yc - yZero))), fill=minusColor, width=lineWidth - # , clipX=(xLeftOffset, xLeftOffset + plotWidth) ) else: minYc = min(Yc - yZero, Yc0 - yZero) @@ -2538,14 +2526,12 @@ class DisplayMappingResults: im_drawer.line( xy=((Xc0, Yc0), (Xc, Yc)), fill=plusColor, width=lineWidth - # , clipX=(xLeftOffset, xLeftOffset + plotWidth) ) else: im_drawer.line( xy=((Xc0, yZero - (Yc0 - yZero)), (Xc, yZero - (Yc - yZero))), fill=minusColor, width=lineWidth - # , clipX=(xLeftOffset, xLeftOffset + plotWidth) ) LRSCoordXY = [] @@ -2558,28 +2544,29 @@ class DisplayMappingResults: startPosX += newStartPosX oldStartPosX = newStartPosX - # ZS: This is because the chromosome value stored in qtlresult['chr'] can be (for example) either X or 20 depending upon the mapping method/scale used + # This is because the chromosome value stored in qtlresult['chr'] can be (for example) either X or 20 depending upon the mapping method/scale used this_chr = str(self.ChrList[self.selectedChr][0]) if self.plotScale != "physic": this_chr = str(self.ChrList[self.selectedChr][1] + 1) if self.selectedChr == -1 or str(qtlresult['chr']) == this_chr: if self.plotScale != "physic" and self.mapping_method == "reaper" and not self.manhattan_plot: - Xc = startPosX + (qtlresult['cM'] - startMb) * plotXScale + start_cm = self.genotype[self.selectedChr - 1][0].cM + Xc = startPosX + (qtlresult['cM'] - start_cm) * plotXScale if hasattr(self.genotype, "filler"): if self.genotype.filler: if self.selectedChr != -1: - start_cm = self.genotype[self.selectedChr - 1][0].cM Xc = startPosX + \ (qtlresult['Mb'] - start_cm) * plotXScale else: - start_cm = self.genotype[previous_chr_as_int][0].cM Xc = startPosX + ((qtlresult['Mb'] - start_cm - startMb) * plotXScale) * ( ((qtlresult['Mb'] - start_cm - startMb) * plotXScale) / ((qtlresult['Mb'] - start_cm - startMb + self.GraphInterval) * plotXScale)) else: if self.selectedChr != -1 and qtlresult['Mb'] > endMb: Xc = startPosX + endMb * plotXScale else: + if qtlresult['Mb'] - startMb < 0: + continue Xc = startPosX + (qtlresult['Mb'] - startMb) * plotXScale # updated by NL 06-18-2011: @@ -2646,9 +2633,8 @@ class DisplayMappingResults: AdditiveHeightThresh / additiveMax AdditiveCoordXY.append((Xc, Yc)) - if self.selectedChr != -1 and qtlresult['Mb'] > endMb: + if self.selectedChr != -1 and qtlresult['Mb'] > endMb and endMb != -1: break - m += 1 if self.manhattan_plot != True: diff --git a/wqflask/wqflask/marker_regression/rqtl_mapping.py b/wqflask/wqflask/marker_regression/rqtl_mapping.py index 63e8c334..65896e06 100644 --- a/wqflask/wqflask/marker_regression/rqtl_mapping.py +++ b/wqflask/wqflask/marker_regression/rqtl_mapping.py @@ -12,13 +12,11 @@ import numpy as np from base.webqtlConfig import TMPDIR from base.trait import create_trait -from utility.tools import locate +from utility.tools import locate, GN3_LOCAL_URL import utility.logger logger = utility.logger.getLogger(__name__) -GN3_RQTL_URL = "http://localhost:8086/api/rqtl/compute" -GN3_TMP_PATH = "/export/local/home/zas1024/genenetwork3/tmp" def run_rqtl(trait_name, vals, samples, dataset, pair_scan, mapping_scale, model, method, num_perm, perm_strata_list, do_control, control_marker, manhattan_plot, cofactors): """Run R/qtl by making a request to the GN3 endpoint and reading in the output file(s)""" @@ -52,7 +50,7 @@ def run_rqtl(trait_name, vals, samples, dataset, pair_scan, mapping_scale, model if perm_strata_list: post_data["pstrata"] = True - rqtl_output = requests.post(GN3_RQTL_URL, data=post_data).json() + rqtl_output = requests.post(GN3_LOCAL_URL + "api/rqtl/compute", data=post_data).json() if num_perm > 0: return rqtl_output['perm_results'], rqtl_output['suggestive'], rqtl_output['significant'], rqtl_output['results'] else: diff --git a/wqflask/wqflask/marker_regression/run_mapping.py b/wqflask/wqflask/marker_regression/run_mapping.py index 640cf9cd..cf217a96 100644 --- a/wqflask/wqflask/marker_regression/run_mapping.py +++ b/wqflask/wqflask/marker_regression/run_mapping.py @@ -229,7 +229,7 @@ class RunMapping: self.perm_strata = get_perm_strata( self.this_trait, primary_samples, self.categorical_vars, self.samples) - self.score_type = "LOD" + self.score_type = "-logP" self.control_marker = start_vars['control_marker'] self.do_control = start_vars['do_control'] if 'mapmethod_rqtl' in start_vars: @@ -531,7 +531,8 @@ def export_mapping_results(dataset, trait, markers, results_path, mapping_method transform_text = "" output_file.write(transform_text + "\n") if dataset.type == "ProbeSet": - output_file.write("Gene Symbol: " + trait.symbol + "\n") + if trait.symbol: + output_file.write("Gene Symbol: " + trait.symbol + "\n") output_file.write("Location: " + str(trait.chr) + \ " @ " + str(trait.mb) + " Mb\n") if len(covariates) > 0: diff --git a/wqflask/wqflask/metadata_edits.py b/wqflask/wqflask/metadata_edits.py new file mode 100644 index 00000000..dc738f88 --- /dev/null +++ b/wqflask/wqflask/metadata_edits.py @@ -0,0 +1,557 @@ +import datetime +import json +import os +import re + +from collections import namedtuple +from itertools import groupby +from typing import Dict + +import MySQLdb +import difflib +import redis + +from flask import Blueprint +from flask import Response +from flask import current_app +from flask import flash +from flask import g +from flask import redirect +from flask import render_template +from flask import request +from flask import url_for + +from wqflask.decorators import edit_access_required +from wqflask.decorators import edit_admins_access_required +from wqflask.decorators import login_required + +from gn3.authentication import AdminRole +from gn3.authentication import DataRole +from gn3.authentication import get_highest_user_access_role +from gn3.authentication import get_user_membership +from gn3.commands import run_cmd +from gn3.db import diff_from_dict +from gn3.db import fetchall +from gn3.db import fetchone +from gn3.db import insert +from gn3.db import update +from gn3.db.metadata_audit import MetadataAudit +from gn3.db.phenotypes import Phenotype +from gn3.db.phenotypes import Probeset +from gn3.db.phenotypes import Publication +from gn3.db.phenotypes import PublishXRef +from gn3.db.phenotypes import probeset_mapping +from gn3.db.traits import get_trait_csv_sample_data +from gn3.db.traits import update_sample_data +from gn3.db.traits import delete_sample_data +from gn3.db.traits import insert_sample_data + + +metadata_edit = Blueprint('metadata_edit', __name__) + + +def _get_diffs(diff_dir: str, + user_id: str, + redis_conn: redis.Redis, + gn_proxy_url: str): + def __get_file_metadata(file_name: str) -> Dict: + author, resource_id, time_stamp, *_ = file_name.split(".") + + return { + "resource_id": resource_id, + "file_name": file_name, + "author": json.loads(redis_conn.hget("users", + author)).get("full_name"), + "time_stamp": time_stamp, + "roles": get_highest_user_access_role( + resource_id=resource_id, + user_id=user_id, + gn_proxy_url=gn_proxy_url), + } + + approved, rejected, waiting = [], [], [] + if os.path.exists(diff_dir): + for name in os.listdir(diff_dir): + file_metadata = __get_file_metadata(file_name=name) + admin_status = file_metadata["roles"].get("admin") + append_p = (user_id in name or + admin_status > AdminRole.EDIT_ACCESS) + if name.endswith(".rejected") and append_p: + rejected.append(__get_file_metadata(file_name=name)) + elif name.endswith(".approved") and append_p: + approved.append(__get_file_metadata(file_name=name)) + elif append_p: # Normal file + waiting.append(__get_file_metadata(file_name=name)) + return { + "approved": approved, + "rejected": rejected, + "waiting": waiting, + } + + +def edit_phenotype(conn, name, dataset_id): + publish_xref = fetchone( + conn=conn, + table="PublishXRef", + where=PublishXRef(id_=name, + inbred_set_id=dataset_id)) + phenotype_ = fetchone( + conn=conn, + table="Phenotype", + where=Phenotype(id_=publish_xref.phenotype_id)) + publication_ = fetchone( + conn=conn, + table="Publication", + where=Publication(id_=publish_xref.publication_id)) + json_data = fetchall( + conn, + "metadata_audit", + where=MetadataAudit(dataset_id=publish_xref.id_)) + Edit = namedtuple("Edit", ["field", "old", "new", "diff"]) + Diff = namedtuple("Diff", ["author", "diff", "timestamp"]) + diff_data = [] + for data in json_data: + json_ = json.loads(data.json_data) + timestamp = json_.get("timestamp") + author = json_.get("author") + for key, value in json_.items(): + if isinstance(value, dict): + for field, data_ in value.items(): + diff_data.append( + Diff(author=author, + diff=Edit(field, + data_.get("old"), + data_.get("new"), + "\n".join(difflib.ndiff( + [data_.get("old")], + [data_.get("new")]))), + timestamp=timestamp)) + diff_data_ = None + if len(diff_data) > 0: + diff_data_ = groupby(diff_data, lambda x: x.timestamp) + return { + "diff": diff_data_, + "publish_xref": publish_xref, + "phenotype": phenotype_, + "publication": publication_, + } + + +def edit_probeset(conn, name): + probeset_ = fetchone(conn=conn, + table="ProbeSet", + columns=list(probeset_mapping.values()), + where=Probeset(name=name)) + json_data = fetchall( + conn, + "metadata_audit", + where=MetadataAudit(dataset_id=probeset_.id_)) + Edit = namedtuple("Edit", ["field", "old", "new", "diff"]) + Diff = namedtuple("Diff", ["author", "diff", "timestamp"]) + diff_data = [] + for data in json_data: + json_ = json.loads(data.json_data) + timestamp = json_.get("timestamp") + author = json_.get("author") + for key, value in json_.items(): + if isinstance(value, dict): + for field, data_ in value.items(): + diff_data.append( + Diff(author=author, + diff=Edit(field, + data_.get("old"), + data_.get("new"), + "\n".join(difflib.ndiff( + [data_.get("old")], + [data_.get("new")]))), + timestamp=timestamp)) + diff_data_ = None + if len(diff_data) > 0: + diff_data_ = groupby(diff_data, lambda x: x.timestamp) + return { + "diff": diff_data_, + "probeset": probeset_, + } + + +@metadata_edit.route("/<dataset_id>/traits/<name>") +@edit_access_required +@login_required +def display_phenotype_metadata(dataset_id: str, name: str): + conn = MySQLdb.Connect(db=current_app.config.get("DB_NAME"), + user=current_app.config.get("DB_USER"), + passwd=current_app.config.get("DB_PASS"), + host=current_app.config.get("DB_HOST")) + _d = edit_phenotype(conn=conn, name=name, dataset_id=dataset_id) + return render_template( + "edit_phenotype.html", + diff=_d.get("diff"), + publish_xref=_d.get("publish_xref"), + phenotype=_d.get("phenotype"), + publication=_d.get("publication"), + dataset_id=dataset_id, + resource_id=request.args.get("resource-id"), + version=os.environ.get("GN_VERSION"), + ) + + +@metadata_edit.route("/traits/<name>") +@edit_access_required +@login_required +def display_probeset_metadata(name: str): + conn = MySQLdb.Connect(db=current_app.config.get("DB_NAME"), + user=current_app.config.get("DB_USER"), + passwd=current_app.config.get("DB_PASS"), + host=current_app.config.get("DB_HOST")) + _d = edit_probeset(conn=conn, name=name) + return render_template( + "edit_probeset.html", + diff=_d.get("diff"), + probeset=_d.get("probeset"), + name=name, + resource_id=request.args.get("resource-id"), + version=os.environ.get("GN_VERSION"), + ) + + +@metadata_edit.route("/<dataset_id>/traits/<name>", methods=("POST",)) +@edit_access_required +@login_required +def update_phenotype(dataset_id: str, name: str): + conn = MySQLdb.Connect(db=current_app.config.get("DB_NAME"), + user=current_app.config.get("DB_USER"), + passwd=current_app.config.get("DB_PASS"), + host=current_app.config.get("DB_HOST")) + data_ = request.form.to_dict() + TMPDIR = current_app.config.get("TMPDIR") + author = ((g.user_session.record.get(b"user_id") or b"").decode("utf-8") + or g.user_session.record.get("user_id") or "") + phenotype_id = str(data_.get('phenotype-id')) + if not (file_ := request.files.get("file")): + flash("No sample-data has been uploaded", "warning") + else: + if not os.path.exists(SAMPLE_DATADIR := os.path.join(TMPDIR, "sample-data")): + os.makedirs(SAMPLE_DATADIR) + if not os.path.exists(os.path.join(SAMPLE_DATADIR, + "diffs")): + os.makedirs(os.path.join(SAMPLE_DATADIR, + "diffs")) + if not os.path.exists(os.path.join(SAMPLE_DATADIR, + "updated")): + os.makedirs(os.path.join(SAMPLE_DATADIR, + "updated")) + current_time = str(datetime.datetime.now().isoformat()) + _file_name = (f"{author}.{request.args.get('resource-id')}." + f"{current_time}") + new_file_name = (os.path.join(TMPDIR, + f"sample-data/updated/{_file_name}.csv")) + uploaded_file_name = (os.path.join( + TMPDIR, "sample-data/updated/", + f"{_file_name}.csv.uploaded")) + file_.save(new_file_name) + with open(uploaded_file_name, "w") as f_: + f_.write(get_trait_csv_sample_data( + conn=conn, + trait_name=str(name), + phenotype_id=str(phenotype_id))) + r = run_cmd(cmd=("csvdiff " + f"'{uploaded_file_name}' '{new_file_name}' " + "--format json")) + + # Edge case where the csv file has not been edited! + if not any(json.loads(r.get("output")).values()): + flash(f"You have not modified the csv file you downloaded!", + "warning") + return redirect(f"/datasets/{dataset_id}/traits/{name}" + f"?resource-id={request.args.get('resource-id')}") + diff_output = (f"{TMPDIR}/sample-data/diffs/" + f"{_file_name}.json") + with open(diff_output, "w") as f: + dict_ = json.loads(r.get("output")) + dict_.update({ + "trait_name": str(name), + "phenotype_id": str(phenotype_id), + "author": author, + "timestamp": datetime.datetime.now().strftime( + "%Y-%m-%d %H:%M:%S") + }) + f.write(json.dumps(dict_)) + flash("Sample-data has been successfully uploaded", "success") + # Run updates: + phenotype_ = { + "pre_pub_description": data_.get("pre-pub-desc"), + "post_pub_description": data_.get("post-pub-desc"), + "original_description": data_.get("orig-desc"), + "units": data_.get("units"), + "pre_pub_abbreviation": data_.get("pre-pub-abbrev"), + "post_pub_abbreviation": data_.get("post-pub-abbrev"), + "lab_code": data_.get("labcode"), + "submitter": data_.get("submitter"), + "owner": data_.get("owner"), + "authorized_users": data_.get("authorized-users"), + } + updated_phenotypes = update( + conn, "Phenotype", + data=Phenotype(**phenotype_), + where=Phenotype(id_=data_.get("phenotype-id"))) + diff_data = {} + if updated_phenotypes: + diff_data.update({"Phenotype": diff_from_dict(old={ + k: data_.get(f"old_{k}") for k, v in phenotype_.items() + if v is not None}, new=phenotype_)}) + publication_ = { + "abstract": data_.get("abstract"), + "authors": data_.get("authors"), + "title": data_.get("title"), + "journal": data_.get("journal"), + "volume": data_.get("volume"), + "pages": data_.get("pages"), + "month": data_.get("month"), + "year": data_.get("year") + } + updated_publications = update( + conn, "Publication", + data=Publication(**publication_), + where=Publication(id_=data_.get("pubmed-id", + data_.get("old_id_")))) + if updated_publications: + diff_data.update({"Publication": diff_from_dict(old={ + k: data_.get(f"old_{k}") for k, v in publication_.items() + if v is not None}, new=publication_)}) + if diff_data: + diff_data.update({ + "phenotype_id": str(phenotype_id), + "dataset_id": name, + "resource_id": request.args.get('resource-id'), + "author": author, + "timestamp": (datetime + .datetime + .now() + .strftime("%Y-%m-%d %H:%M:%S")), + }) + insert(conn, + table="metadata_audit", + data=MetadataAudit(dataset_id=name, + editor=author, + json_data=json.dumps(diff_data))) + flash(f"Diff-data: \n{diff_data}\nhas been uploaded", "success") + return redirect(f"/datasets/{dataset_id}/traits/{name}" + f"?resource-id={request.args.get('resource-id')}") + + +@metadata_edit.route("/traits/<name>", methods=("POST",)) +@edit_access_required +@login_required +def update_probeset(name: str): + conn = MySQLdb.Connect(db=current_app.config.get("DB_NAME"), + user=current_app.config.get("DB_USER"), + passwd=current_app.config.get("DB_PASS"), + host=current_app.config.get("DB_HOST")) + data_ = request.form.to_dict() + probeset_ = { + "id_": data_.get("id"), + "symbol": data_.get("symbol"), + "description": data_.get("description"), + "probe_target_description": data_.get("probe_target_description"), + "chr_": data_.get("chr"), + "mb": data_.get("mb"), + "alias": data_.get("alias"), + "geneid": data_.get("geneid"), + "homologeneid": data_.get("homologeneid"), + "unigeneid": data_.get("unigeneid"), + "omim": data_.get("OMIM"), + "refseq_transcriptid": data_.get("refseq_transcriptid"), + "blatseq": data_.get("blatseq"), + "targetseq": data_.get("targetseq"), + "strand_probe": data_.get("Strand_Probe"), + "probe_set_target_region": data_.get("probe_set_target_region"), + "probe_set_specificity": data_.get("probe_set_specificity"), + "probe_set_blat_score": data_.get("probe_set_blat_score"), + "probe_set_blat_mb_start": data_.get("probe_set_blat_mb_start"), + "probe_set_blat_mb_end": data_.get("probe_set_blat_mb_end"), + "probe_set_strand": data_.get("probe_set_strand"), + "probe_set_note_by_rw": data_.get("probe_set_note_by_rw"), + "flag": data_.get("flag") + } + diff_data = {} + author = ((g.user_session.record.get(b"user_id") or b"").decode("utf-8") + or g.user_session.record.get("user_id") or "") + if (updated_probeset := update( + conn, "ProbeSet", + data=Probeset(**probeset_), + where=Probeset(id_=data_.get("id")))): + diff_data.update({"Probeset": diff_from_dict(old={ + k: data_.get(f"old_{k}") for k, v in probeset_.items() + if v is not None}, new=probeset_)}) + if diff_data: + diff_data.update({"probeset_name": data_.get("probeset_name")}) + diff_data.update({"author": author}) + diff_data.update({"resource_id": request.args.get('resource-id')}) + diff_data.update({"timestamp": datetime.datetime.now().strftime( + "%Y-%m-%d %H:%M:%S")}) + insert(conn, + table="metadata_audit", + data=MetadataAudit(dataset_id=data_.get("id"), + editor=author, + json_data=json.dumps(diff_data))) + return redirect(f"/datasets/traits/{name}" + f"?resource-id={request.args.get('resource-id')}") + + +@metadata_edit.route("/<dataset_id>/traits/<phenotype_id>/csv") +@login_required +def get_sample_data_as_csv(dataset_id: str, phenotype_id: int): + return Response( + get_trait_csv_sample_data( + conn=MySQLdb.Connect(db=current_app.config.get("DB_NAME"), + user=current_app.config.get("DB_USER"), + passwd=current_app.config.get("DB_PASS"), + host=current_app.config.get("DB_HOST")), + trait_name=str(dataset_id), + phenotype_id=str(phenotype_id)), + mimetype="text/csv", + headers={"Content-disposition": + "attachment; filename=myplot.csv"} + ) + + +@metadata_edit.route("/diffs") +@login_required +def list_diffs(): + files = _get_diffs( + diff_dir=f"{current_app.config.get('TMPDIR')}/sample-data/diffs", + user_id=((g.user_session.record.get(b"user_id") or + b"").decode("utf-8") + or g.user_session.record.get("user_id") or ""), + redis_conn=redis.from_url(current_app.config["REDIS_URL"], + decode_responses=True), + gn_proxy_url=current_app.config.get("GN2_PROXY")) + return render_template( + "display_files.html", + approved=sorted(files.get("approved"), + reverse=True, + key=lambda d: d.get("time_stamp")), + rejected=sorted(files.get("rejected"), + reverse=True, + key=lambda d: d.get("time_stamp")), + waiting=sorted(files.get("waiting"), + reverse=True, + key=lambda d: d.get("time_stamp"))) + + +@metadata_edit.route("/diffs/<name>") +def show_diff(name): + TMPDIR = current_app.config.get("TMPDIR") + with open(os.path.join(f"{TMPDIR}/sample-data/diffs", + name), 'r') as myfile: + content = myfile.read() + content = json.loads(content) + for data in content.get("Modifications"): + data["Diff"] = "\n".join(difflib.ndiff([data.get("Original")], + [data.get("Current")])) + return render_template( + "display_diffs.html", + diff=content + ) + + +@metadata_edit.route("<resource_id>/diffs/<file_name>/reject") +@edit_admins_access_required +@login_required +def reject_data(resource_id: str, file_name: str): + TMPDIR = current_app.config.get("TMPDIR") + os.rename(os.path.join(f"{TMPDIR}/sample-data/diffs", file_name), + os.path.join(f"{TMPDIR}/sample-data/diffs", + f"{file_name}.rejected")) + flash(f"{file_name} has been rejected!", "success") + return redirect(url_for('metadata_edit.list_diffs')) + + +@metadata_edit.route("<resource_id>/diffs/<file_name>/approve") +@edit_admins_access_required +@login_required +def approve_data(resource_id:str, file_name: str): + sample_data = {file_name: str} + conn = MySQLdb.Connect(db=current_app.config.get("DB_NAME"), + user=current_app.config.get("DB_USER"), + passwd=current_app.config.get("DB_PASS"), + host=current_app.config.get("DB_HOST")) + TMPDIR = current_app.config.get("TMPDIR") + with open(os.path.join(f"{TMPDIR}/sample-data/diffs", + file_name), 'r') as myfile: + sample_data = json.load(myfile) + for modification in ( + modifications := [d for d in sample_data.get("Modifications")]): + if modification.get("Current"): + (strain_name, + value, se, count) = modification.get("Current").split(",") + update_sample_data( + conn=conn, + trait_name=sample_data.get("trait_name"), + strain_name=strain_name, + phenotype_id=int(sample_data.get("phenotype_id")), + value=value, + error=se, + count=count) + + n_deletions = 0 + for deletion in (deletions := [d for d in sample_data.get("Deletions")]): + strain_name, _, _, _ = deletion.split(",") + __deletions, _, _ = delete_sample_data( + conn=conn, + trait_name=sample_data.get("trait_name"), + strain_name=strain_name, + phenotype_id=int(sample_data.get("phenotype_id"))) + if __deletions: + n_deletions += 1 + # Remove any data that already exists from sample_data deletes + else: + sample_data.get("Deletions").remove(deletion) + + n_insertions = 0 + for insertion in ( + insertions := [d for d in sample_data.get("Additions")]): + (strain_name, + value, se, count) = insertion.split(",") + __insertions, _, _ = insert_sample_data( + conn=conn, + trait_name=sample_data.get("trait_name"), + strain_name=strain_name, + phenotype_id=int(sample_data.get("phenotype_id")), + value=value, + error=se, + count=count) + if __insertions: + n_insertions += 1 + # Remove any data that already exists from sample_data inserts + else: + sample_data.get("Additions").remove(insertion) + if any([sample_data.get("Additions"), + sample_data.get("Modifications"), + sample_data.get("Deletions")]): + insert(conn, + table="metadata_audit", + data=MetadataAudit( + dataset_id=sample_data.get("trait_name"), + editor=sample_data.get("author"), + json_data=json.dumps(sample_data))) + # Once data is approved, rename it! + os.rename(os.path.join(f"{TMPDIR}/sample-data/diffs", file_name), + os.path.join(f"{TMPDIR}/sample-data/diffs", + f"{file_name}.approved")) + message = "" + if n_deletions: + flash(f"# Deletions: {n_deletions}", "success") + if n_insertions: + flash("# Additions: {len(modifications)", "success") + if len(modifications): + flash("# Modifications: {len(modifications)}", "success") + else: # Edge case where you need to automatically reject the file + os.rename(os.path.join(f"{TMPDIR}/sample-data/diffs", file_name), + os.path.join(f"{TMPDIR}/sample-data/diffs", + f"{file_name}.rejected")) + flash(("Automatically rejecting this file since no " + "changes could be applied."), "warning") + + return redirect(url_for('metadata_edit.list_diffs')) + diff --git a/wqflask/wqflask/resource_manager.py b/wqflask/wqflask/resource_manager.py index c54dd0b3..c0717314 100644 --- a/wqflask/wqflask/resource_manager.py +++ b/wqflask/wqflask/resource_manager.py @@ -1,144 +1,169 @@ import json +import redis +import requests -from flask import (Flask, g, render_template, url_for, request, make_response, - redirect, flash) - -from wqflask import app - -from utility.authentication_tools import check_owner_or_admin -from utility.redis_tools import get_resource_info, get_group_info, get_groups_like_unique_column, get_user_id, get_user_by_unique_column, get_users_like_unique_column, add_access_mask, add_resource, change_resource_owner - - - -@app.route("/resources/manage", methods=('GET', 'POST')) -def manage_resource(): - params = request.form if request.form else request.args - if 'resource_id' in request.args: - resource_id = request.args['resource_id'] - admin_status = check_owner_or_admin(resource_id=resource_id) - - resource_info = get_resource_info(resource_id) - group_masks = resource_info['group_masks'] - group_masks_with_names = get_group_names(group_masks) - default_mask = resource_info['default_mask']['data'] - owner_id = resource_info['owner_id'] - - owner_display_name = None - if owner_id != "none": - try: # ZS: User IDs are sometimes stored in Redis as bytes and sometimes as strings, so this is just to avoid any errors for the time being - owner_id = str.encode(owner_id) - except: - pass - owner_info = get_user_by_unique_column("user_id", owner_id) - if 'name' in owner_info: - owner_display_name = owner_info['full_name'] - elif 'user_name' in owner_info: - owner_display_name = owner_info['user_name'] - elif 'email_address' in owner_info: - owner_display_name = owner_info['email_address'] - - return render_template("admin/manage_resource.html", owner_name=owner_display_name, resource_id=resource_id, resource_info=resource_info, default_mask=default_mask, group_masks=group_masks_with_names, admin_status=admin_status) - - -@app.route("/search_for_users", methods=('POST',)) -def search_for_user(): - params = request.form - user_list = [] - user_list += get_users_like_unique_column("full_name", params['user_name']) - user_list += get_users_like_unique_column( - "email_address", params['user_email']) - - return json.dumps(user_list) - - -@app.route("/search_for_groups", methods=('POST',)) -def search_for_groups(): - params = request.form - group_list = [] - group_list += get_groups_like_unique_column("id", params['group_id']) - group_list += get_groups_like_unique_column("name", params['group_name']) - - user_list = [] - user_list += get_users_like_unique_column("full_name", params['user_name']) - user_list += get_users_like_unique_column( - "email_address", params['user_email']) - for user in user_list: - group_list += get_groups_like_unique_column("admins", user['user_id']) - group_list += get_groups_like_unique_column("members", user['user_id']) - - return json.dumps(group_list) - - -@app.route("/resources/change_owner", methods=('POST',)) -def change_owner(): - resource_id = request.form['resource_id'] - if 'new_owner' in request.form: - admin_status = check_owner_or_admin(resource_id=resource_id) - if admin_status == "owner": - new_owner_id = request.form['new_owner'] - change_resource_owner(resource_id, new_owner_id) - flash("The resource's owner has beeen changed.", "alert-info") - return redirect(url_for("manage_resource", resource_id=resource_id)) - else: - flash("You lack the permissions to make this change.", "error") - return redirect(url_for("manage_resource", resource_id=resource_id)) - else: - return render_template("admin/change_resource_owner.html", resource_id=resource_id) - - -@app.route("/resources/change_default_privileges", methods=('POST',)) -def change_default_privileges(): - resource_id = request.form['resource_id'] - admin_status = check_owner_or_admin(resource_id=resource_id) - if admin_status == "owner" or admin_status == "edit-admins": - resource_info = get_resource_info(resource_id) - default_mask = resource_info['default_mask'] - if request.form['open_to_public'] == "True": - default_mask['data'] = 'view' - else: - default_mask['data'] = 'no-access' - resource_info['default_mask'] = default_mask - add_resource(resource_info) - flash("Your changes have been saved.", "alert-info") - return redirect(url_for("manage_resource", resource_id=resource_id)) - else: - return redirect(url_for("no_access_page")) - - -@app.route("/resources/add_group", methods=('POST',)) -def add_group_to_resource(): - resource_id = request.form['resource_id'] - admin_status = check_owner_or_admin(resource_id=resource_id) - if admin_status == "owner" or admin_status == "edit-admins" or admin_status == "edit-access": - if 'selected_group' in request.form: - group_id = request.form['selected_group'] - resource_info = get_resource_info(resource_id) - default_privileges = resource_info['default_mask'] - return render_template("admin/set_group_privileges.html", resource_id=resource_id, group_id=group_id, default_privileges=default_privileges) - elif all(key in request.form for key in ('data_privilege', 'metadata_privilege', 'admin_privilege')): - group_id = request.form['group_id'] - group_name = get_group_info(group_id)['name'] - access_mask = { - 'data': request.form['data_privilege'], - 'metadata': request.form['metadata_privilege'], - 'admin': request.form['admin_privilege'] - } - add_access_mask(resource_id, group_id, access_mask) - flash("Privileges have been added for group {}.".format( - group_name), "alert-info") - return redirect(url_for("manage_resource", resource_id=resource_id)) - else: - return render_template("admin/search_for_groups.html", resource_id=resource_id) - else: - return redirect(url_for("no_access_page")) +from flask import Blueprint +from flask import current_app +from flask import flash +from flask import g +from flask import redirect +from flask import render_template +from flask import request +from flask import url_for + +from gn3.authentication import AdminRole +from gn3.authentication import DataRole +from gn3.authentication import get_user_membership +from gn3.authentication import get_highest_user_access_role + +from typing import Dict, Tuple +from urllib.parse import urljoin + + +from wqflask.decorators import edit_access_required +from wqflask.decorators import edit_admins_access_required +from wqflask.decorators import login_required -def get_group_names(group_masks): - group_masks_with_names = {} - for group_id, group_mask in list(group_masks.items()): - this_mask = group_mask - group_name = get_group_info(group_id)['name'] - this_mask['name'] = group_name - group_masks_with_names[group_id] = this_mask +resource_management = Blueprint('resource_management', __name__) - return group_masks_with_names + +def add_extra_resource_metadata(conn: redis.Redis, + resource_id: str, + resource: Dict) -> Dict: + """If resource['owner_id'] exists, add metadata about that user. Also, +if the resource contains group masks, add the group name into the +resource dict. Note that resource['owner_id'] and the group masks are +unique identifiers so they aren't human readable names. + + Args: + - conn: A redis connection with the responses decoded. + - resource_id: The unique identifier of the resource. + - resource: A dict containing details(metadata) about a + given resource. + + Returns: + An embellished dictionary with its resource id; the human + readable names of the group masks; and the owner id if it was set. + + """ + resource["resource_id"] = resource_id + + # Embellish the resource information with owner details if the + # owner is set + if (owner_id := resource.get("owner_id", "none").lower()) == "none": + resource["owner_id"] = None + resource["owner_details"] = None + else: + user_details = json.loads(conn.hget("users", owner_id)) + resource["owner_details"] = { + "email_address": user_details.get("email_address"), + "full_name": user_details.get("full_name"), + "organization": user_details.get("organization"), + } + + # Embellish the resources information with the group name if the + # group masks are present + if groups := resource.get('group_masks', {}): + for group_id in groups.keys(): + resource['group_masks'][group_id]["group_name"] = ( + json.loads(conn.hget("groups", group_id)).get('name')) + return resource + + +@resource_management.route("/resources/<resource_id>") +@login_required +def view_resource(resource_id: str): + user_id = (g.user_session.record.get(b"user_id", + b"").decode("utf-8") or + g.user_session.record.get("user_id", "")) + redis_conn = redis.from_url( + current_app.config["REDIS_URL"], + decode_responses=True) + # Abort early if the resource can't be found + if not (resource := redis_conn.hget("resources", resource_id)): + return f"Resource: {resource_id} Not Found!", 401 + + return render_template( + "admin/manage_resource.html", + resource_info=(add_extra_resource_metadata( + conn=redis_conn, + resource_id=resource_id, + resource=json.loads(resource))), + access_role=get_highest_user_access_role( + resource_id=resource_id, + user_id=user_id, + gn_proxy_url=current_app.config.get("GN2_PROXY"))) + + +@resource_management.route("/resources/<resource_id>/make-public", + methods=('POST',)) +@edit_access_required +@login_required +def update_resource_publicity(resource_id: str): + redis_conn = redis.from_url( + current_app.config["REDIS_URL"], + decode_responses=True) + resource_info = json.loads(redis_conn.hget("resources", resource_id)) + + if (is_open_to_public := request + .form + .to_dict() + .get("open_to_public")) == "True": + resource_info['default_mask'] = { + 'data': DataRole.VIEW.value, + 'admin': AdminRole.NOT_ADMIN.value, + 'metadata': DataRole.VIEW.value, + } + elif is_open_to_public == "False": + resource_info['default_mask'] = { + 'data': DataRole.NO_ACCESS.value, + 'admin': AdminRole.NOT_ADMIN.value, + 'metadata': DataRole.NO_ACCESS.value, + } + redis_conn.hset("resources", resource_id, json.dumps(resource_info)) + return redirect(url_for("resource_management.view_resource", + resource_id=resource_id)) + + +@resource_management.route("/resources/<resource_id>/change-owner") +@edit_admins_access_required +@login_required +def view_resource_owner(resource_id: str): + return render_template( + "admin/change_resource_owner.html", + resource_id=resource_id) + + +@resource_management.route("/resources/<resource_id>/change-owner", + methods=('POST',)) +@edit_admins_access_required +@login_required +def change_owner(resource_id: str): + if user_id := request.form.get("new_owner"): + redis_conn = redis.from_url( + current_app.config["REDIS_URL"], + decode_responses=True) + resource = json.loads(redis_conn.hget("resources", resource_id)) + resource["owner_id"] = user_id + redis_conn.hset("resources", resource_id, json.dumps(resource)) + flash("The resource's owner has been changed.", "alert-info") + return redirect(url_for("resource_management.view_resource", + resource_id=resource_id)) + + +@resource_management.route("<resource_id>/users/search", methods=('POST',)) +@edit_admins_access_required +@login_required +def search_user(resource_id: str): + results = {} + for user in (users := redis.from_url( + current_app.config["REDIS_URL"], + decode_responses=True).hgetall("users")): + user = json.loads(users[user]) + for q in (request.form.get("user_name"), + request.form.get("user_email")): + if q and (q in user.get("email_address") or + q in user.get("full_name")): + results[user.get("user_id", "")] = user + return json.dumps(tuple(results.values())) diff --git a/wqflask/wqflask/search_results.py b/wqflask/wqflask/search_results.py index 3cbda3dd..cf2905c9 100644 --- a/wqflask/wqflask/search_results.py +++ b/wqflask/wqflask/search_results.py @@ -4,6 +4,8 @@ from math import * import time import re import requests +from types import SimpleNamespace +import unicodedata from pprint import pformat as pf @@ -11,6 +13,7 @@ import json from base.data_set import create_dataset from base.trait import create_trait +from base.webqtlConfig import PUBMEDLINK_URL from wqflask import parser from wqflask import do_search from db import webqtlDatabaseFunction @@ -18,13 +21,13 @@ from db import webqtlDatabaseFunction from flask import Flask, g from utility import hmac, helper_functions +from utility.authentication_tools import check_resource_availability from utility.tools import GN2_BASE_URL from utility.type_checking import is_str from utility.logger import getLogger logger = getLogger(__name__) - class SearchResultPage: #maxReturn = 3000 @@ -40,9 +43,7 @@ class SearchResultPage: self.uc_id = uuid.uuid4() self.go_term = None - logger.debug("uc_id:", self.uc_id) # contains a unique id - logger.debug("kw is:", kw) # dict containing search terms if kw['search_terms_or']: self.and_or = "or" self.search_terms = kw['search_terms_or'] @@ -55,15 +56,17 @@ class SearchResultPage: rx = re.compile( r'.*\W(href|http|sql|select|update)\W.*', re.IGNORECASE) if rx.match(search): - logger.info("Regex failed search") + logger.debug("Regex failed search") self.search_term_exists = False return else: self.search_term_exists = True self.results = [] + max_result_count = 100000 # max number of results to display type = kw.get('type') if type == "Phenotypes": # split datatype on type field + max_result_count = 50000 dataset_type = "Publish" elif type == "Genotypes": dataset_type = "Geno" @@ -72,9 +75,8 @@ class SearchResultPage: assert(is_str(kw.get('dataset'))) self.dataset = create_dataset(kw['dataset'], dataset_type) - logger.debug("search_terms:", self.search_terms) - # ZS: I don't like using try/except, but it seems like the easiest way to account for all possible bad searches here + # I don't like using try/except, but it seems like the easiest way to account for all possible bad searches here try: self.search() except: @@ -82,7 +84,7 @@ class SearchResultPage: self.too_many_results = False if self.search_term_exists: - if len(self.results) > 50000: + if len(self.results) > max_result_count: self.trait_list = [] self.too_many_results = True else: @@ -97,88 +99,125 @@ class SearchResultPage: trait_list = [] json_trait_list = [] - species = webqtlDatabaseFunction.retrieve_species( - self.dataset.group.name) # result_set represents the results for each search term; a search of # "shh grin2b" would have two sets of results, one for each term - logger.debug("self.results is:", pf(self.results)) + + if self.dataset.type == "ProbeSet": + self.header_data_names = ['index', 'display_name', 'symbol', 'description', 'location', 'mean', 'lrs_score', 'lrs_location', 'additive'] + elif self.dataset.type == "Publish": + self.header_data_names = ['index', 'display_name', 'description', 'mean', 'authors', 'pubmed_text', 'lrs_score', 'lrs_location', 'additive'] + elif self.dataset.type == "Geno": + self.header_data_names = ['index', 'display_name', 'location'] for index, result in enumerate(self.results): if not result: continue - #### Excel file needs to be generated #### - trait_dict = {} - trait_id = result[0] - this_trait = create_trait( - dataset=self.dataset, name=trait_id, get_qtl_info=True, get_sample_info=False) - if this_trait: - trait_dict['index'] = index + 1 - trait_dict['name'] = this_trait.name - if this_trait.dataset.type == "Publish": - trait_dict['display_name'] = this_trait.display_name + trait_dict['index'] = index + 1 + + trait_dict['dataset'] = self.dataset.name + if self.dataset.type == "ProbeSet": + trait_dict['display_name'] = result[2] + trait_dict['hmac'] = hmac.data_hmac('{}:{}'.format(trait_dict['display_name'], trait_dict['dataset'])) + trait_dict['symbol'] = "N/A" if result[3] is None else result[3].strip() + description_text = "" + if result[4] is not None and str(result[4]) != "": + description_text = unicodedata.normalize("NFKD", result[4].decode('latin1')) + + target_string = result[5].decode('utf-8') if result[5] else "" + description_display = description_text if target_string is None or str(target_string) == "" else description_text + "; " + str(target_string).strip() + trait_dict['description'] = description_display + + trait_dict['location'] = "N/A" + if (result[6] is not None) and (result[6] != "") and (result[7] is not None) and (result[7] != 0): + trait_dict['location'] = f"Chr{result[6]}: {float(result[7]):.6f}" + + trait_dict['mean'] = "N/A" if result[8] is None or result[8] == "" else f"{result[8]:.3f}" + trait_dict['additive'] = "N/A" if result[12] is None or result[12] == "" else f"{result[12]:.3f}" + trait_dict['lod_score'] = "N/A" if result[9] is None or result[9] == "" else f"{float(result[9]) / 4.61:.1f}" + trait_dict['lrs_location'] = "N/A" if result[13] is None or result[13] == "" or result[14] is None else f"Chr{result[13]}: {float(result[14]):.6f}" + elif self.dataset.type == "Geno": + trait_dict['display_name'] = str(result[0]) + trait_dict['hmac'] = hmac.data_hmac('{}:{}'.format(trait_dict['display_name'], trait_dict['dataset'])) + trait_dict['location'] = "N/A" + if (result[4] != "NULL" and result[4] != "") and (result[5] != 0): + trait_dict['location'] = f"Chr{result[4]}: {float(result[5]):.6f}" + elif self.dataset.type == "Publish": + # Check permissions on a trait-by-trait basis for phenotype traits + trait_dict['name'] = trait_dict['display_name'] = str(result[0]) + trait_dict['hmac'] = hmac.data_hmac('{}:{}'.format(trait_dict['name'], trait_dict['dataset'])) + permissions = check_resource_availability(self.dataset, trait_dict['display_name']) + if "view" not in permissions['data']: + continue + + if result[10]: + trait_dict['display_name'] = str(result[10]) + "_" + str(result[0]) + trait_dict['description'] = "N/A" + trait_dict['pubmed_id'] = "N/A" + trait_dict['pubmed_link'] = "N/A" + trait_dict['pubmed_text'] = "N/A" + trait_dict['mean'] = "N/A" + trait_dict['additive'] = "N/A" + pre_pub_description = "N/A" if result[1] is None else result[1].strip() + post_pub_description = "N/A" if result[2] is None else result[2].strip() + if result[5] != "NULL" and result[5] != None: + trait_dict['pubmed_id'] = result[5] + trait_dict['pubmed_link'] = PUBMEDLINK_URL % trait_dict['pubmed_id'] + trait_dict['description'] = post_pub_description else: - trait_dict['display_name'] = this_trait.name - trait_dict['dataset'] = this_trait.dataset.name - trait_dict['hmac'] = hmac.data_hmac( - '{}:{}'.format(this_trait.name, this_trait.dataset.name)) - if this_trait.dataset.type == "ProbeSet": - trait_dict['symbol'] = this_trait.symbol if this_trait.symbol else "N/A" - trait_dict['description'] = "N/A" - if this_trait.description_display: - trait_dict['description'] = this_trait.description_display - trait_dict['location'] = this_trait.location_repr - trait_dict['mean'] = "N/A" - trait_dict['additive'] = "N/A" - if this_trait.mean != "" and this_trait.mean != None: - trait_dict['mean'] = f"{this_trait.mean:.3f}" - try: - trait_dict['lod_score'] = f"{float(this_trait.LRS_score_repr) / 4.61:.1f}" - except: - trait_dict['lod_score'] = "N/A" - trait_dict['lrs_location'] = this_trait.LRS_location_repr - if this_trait.additive != "": - trait_dict['additive'] = f"{this_trait.additive:.3f}" - elif this_trait.dataset.type == "Geno": - trait_dict['location'] = this_trait.location_repr - elif this_trait.dataset.type == "Publish": - trait_dict['description'] = "N/A" - if this_trait.description_display: - trait_dict['description'] = this_trait.description_display - trait_dict['authors'] = this_trait.authors - trait_dict['pubmed_id'] = "N/A" - if this_trait.pubmed_id: - trait_dict['pubmed_id'] = this_trait.pubmed_id - trait_dict['pubmed_link'] = this_trait.pubmed_link - trait_dict['pubmed_text'] = this_trait.pubmed_text - trait_dict['mean'] = "N/A" - if this_trait.mean != "" and this_trait.mean != None: - trait_dict['mean'] = f"{this_trait.mean:.3f}" + trait_dict['description'] = pre_pub_description + + if result[4].isdigit(): + trait_dict['pubmed_text'] = result[4] + + trait_dict['authors'] = result[3] + + if result[6] != "" and result[6] != None: + trait_dict['mean'] = f"{result[6]:.3f}" + + try: + trait_dict['lod_score'] = f"{float(result[7]) / 4.61:.1f}" + except: + trait_dict['lod_score'] = "N/A" + + try: + trait_dict['lrs_location'] = f"Chr{result[11]}: {float(result[12]):.6f}" + except: + trait_dict['lrs_location'] = "N/A" + + trait_dict['additive'] = "N/A" if not result[8] else f"{result[8]:.3f}" + + # Convert any bytes in dict to a normal utf-8 string + for key in trait_dict.keys(): + if isinstance(trait_dict[key], bytes): try: - trait_dict['lod_score'] = f"{float(this_trait.LRS_score_repr) / 4.61:.1f}" - except: - trait_dict['lod_score'] = "N/A" - trait_dict['lrs_location'] = this_trait.LRS_location_repr - trait_dict['additive'] = "N/A" - if this_trait.additive != "": - trait_dict['additive'] = f"{this_trait.additive:.3f}" - # Convert any bytes in dict to a normal utf-8 string - for key in trait_dict.keys(): - if isinstance(trait_dict[key], bytes): trait_dict[key] = trait_dict[key].decode('utf-8') - trait_list.append(trait_dict) + except UnicodeDecodeError: + trait_dict[key] = trait_dict[key].decode('latin-1') + + trait_list.append(trait_dict) + + if self.results: + self.max_widths = {} + for i, trait in enumerate(trait_list): + for key in trait.keys(): + if key == "authors": + authors_string = ",".join(str(trait[key]).split(",")[:6]) + ", et al." + self.max_widths[key] = max(len(authors_string), self.max_widths[key]) if key in self.max_widths else len(str(trait[key])) + else: + self.max_widths[key] = max(len(str(trait[key])), self.max_widths[key]) if key in self.max_widths else len(str(trait[key])) - self.trait_list = trait_list + self.wide_columns_exist = False + if self.dataset.type == "Publish": + if (self.max_widths['display_name'] > 25 or self.max_widths['description'] > 100 or self.max_widths['authors']> 80): + self.wide_columns_exist = True + if self.dataset.type == "ProbeSet": + if (self.max_widths['display_name'] > 25 or self.max_widths['symbol'] > 25 or self.max_widths['description'] > 100): + self.wide_columns_exist = True - if self.dataset.type == "ProbeSet": - self.header_data_names = ['index', 'display_name', 'symbol', 'description', - 'location', 'mean', 'lrs_score', 'lrs_location', 'additive'] - elif self.dataset.type == "Publish": - self.header_data_names = ['index', 'display_name', 'description', 'mean', - 'authors', 'pubmed_text', 'lrs_score', 'lrs_location', 'additive'] - elif self.dataset.type == "Geno": - self.header_data_names = ['index', 'display_name', 'location'] + + self.trait_list = trait_list def search(self): """ @@ -186,14 +225,12 @@ class SearchResultPage: """ self.search_terms = parser.parse(self.search_terms) - logger.debug("After parsing:", self.search_terms) combined_from_clause = "" combined_where_clause = "" # The same table can't be referenced twice in the from clause previous_from_clauses = [] - logger.debug("len(search_terms)>1") symbol_list = [] if self.dataset.type == "ProbeSet": for a_search in self.search_terms: diff --git a/wqflask/wqflask/show_trait/show_trait.py b/wqflask/wqflask/show_trait/show_trait.py index 52d7d308..d9821d9c 100644 --- a/wqflask/wqflask/show_trait/show_trait.py +++ b/wqflask/wqflask/show_trait/show_trait.py @@ -20,15 +20,16 @@ from base import data_set from utility import helper_functions from utility.authentication_tools import check_owner_or_admin from utility.tools import locate_ignore_error +from utility.tools import GN_PROXY_URL from utility.redis_tools import get_redis_conn, get_resource_id -from utility.logger import getLogger +from gn3.authentication import AdminRole +from gn3.authentication import DataRole +from gn3.authentication import get_highest_user_access_role Redis = get_redis_conn() ONE_YEAR = 60 * 60 * 24 * 365 -logger = getLogger(__name__) - ############################################### # # Todo: Put in security to ensure that user has permission to access @@ -38,14 +39,18 @@ logger = getLogger(__name__) class ShowTrait: - def __init__(self, kw): + def __init__(self, user_id, kw): + self.admin_status = None if 'trait_id' in kw and kw['dataset'] != "Temp": self.temp_trait = False self.trait_id = kw['trait_id'] helper_functions.get_species_dataset_trait(self, kw) - self.resource_id = get_resource_id(self.dataset, self.trait_id) - self.admin_status = check_owner_or_admin( - resource_id=self.resource_id) + self.resource_id = get_resource_id(self.dataset, + self.trait_id) + self.admin_status = get_highest_user_access_role( + user_id=user_id, + resource_id=(self.resource_id or ""), + gn_proxy_url=GN_PROXY_URL) elif 'group' in kw: self.temp_trait = True self.trait_id = "Temp_" + kw['species'] + "_" + kw['group'] + \ @@ -62,9 +67,6 @@ class ShowTrait: self.this_trait = create_trait(dataset=self.dataset, name=self.trait_id, cellid=None) - - self.admin_status = check_owner_or_admin( - dataset=self.dataset, trait_id=self.trait_id) else: self.temp_trait = True self.trait_id = kw['trait_id'] @@ -75,10 +77,7 @@ class ShowTrait: self.this_trait = create_trait(dataset=self.dataset, name=self.trait_id, cellid=None) - self.trait_vals = Redis.get(self.trait_id).split() - self.admin_status = check_owner_or_admin( - dataset=self.dataset, trait_id=self.trait_id) # ZS: Get verify/rna-seq link URLs try: @@ -528,10 +527,6 @@ class ShowTrait: sample_group_type='primary', header="%s Only" % (self.dataset.group.name)) self.sample_groups = (primary_samples,) - print("\nttttttttttttttttttttttttttttttttttttttttttttt\n") - print(self.sample_groups) - print("\nttttttttttttttttttttttttttttttttttttttttttttt\n") - self.primary_sample_names = primary_sample_names self.dataset.group.allsamples = all_samples_ordered @@ -617,7 +612,6 @@ def get_nearest_marker(this_trait, this_db): GenoFreeze.Id = GenoXRef.GenoFreezeId AND GenoFreeze.Name = '{}' ORDER BY ABS( Geno.Mb - {}) LIMIT 1""".format(this_chr, this_db.group.name + "Geno", this_mb) - logger.sql(query) result = g.db.execute(query).fetchall() if result == []: diff --git a/wqflask/wqflask/static/gif/waitAnima2.gif b/wqflask/wqflask/static/gif/waitAnima2.gif Binary files differnew file mode 100644 index 00000000..50aff7f2 --- /dev/null +++ b/wqflask/wqflask/static/gif/waitAnima2.gif diff --git a/wqflask/wqflask/static/new/css/jupyter_notebooks.css b/wqflask/wqflask/static/new/css/jupyter_notebooks.css new file mode 100644 index 00000000..db972a17 --- /dev/null +++ b/wqflask/wqflask/static/new/css/jupyter_notebooks.css @@ -0,0 +1,16 @@ +.jupyter-links { + padding: 1.5em; +} + +.jupyter-links:nth-of-type(2n) { + background: #EEEEEE; +} + +.jupyter-links .main-link { + font-size: larger; + display: block; +} + +.jupyter-links .src-link { + font-size: smaller; +} diff --git a/wqflask/wqflask/static/new/css/show_trait.css b/wqflask/wqflask/static/new/css/show_trait.css index b0514e01..3780a8f1 100644 --- a/wqflask/wqflask/static/new/css/show_trait.css +++ b/wqflask/wqflask/static/new/css/show_trait.css @@ -67,7 +67,7 @@ table.dataTable.cell-border tbody tr td:first-child { } .showtrait-main-div { - min-width: 1100px; + min-width: 1400px; } table.dataTable tbody td.column_name-Checkbox { @@ -250,7 +250,6 @@ div.export-code-container { table.sample-table { float: left; - width:100%; } input.trait-value-input { diff --git a/wqflask/wqflask/static/new/css/trait_list.css b/wqflask/wqflask/static/new/css/trait_list.css index c7249721..ce3075d4 100644 --- a/wqflask/wqflask/static/new/css/trait_list.css +++ b/wqflask/wqflask/static/new/css/trait_list.css @@ -51,4 +51,3 @@ div.dts div.dataTables_scrollBody table { div.dts div.dataTables_paginate,div.dts div.dataTables_length{ display:none } - diff --git a/wqflask/wqflask/static/new/javascript/group_manager.js b/wqflask/wqflask/static/new/javascript/group_manager.js index 4c172cbf..cd56133a 100644 --- a/wqflask/wqflask/static/new/javascript/group_manager.js +++ b/wqflask/wqflask/static/new/javascript/group_manager.js @@ -16,23 +16,22 @@ $('#clear_members').click(function(){ function add_emails(user_type){ - var email_address = $('input[name=user_email]').val(); - var email_list_string = $('input[name=' + user_type + '_emails_to_add]').val().trim() - console.log(email_list_string) + let email_address = $('input[name=user_email]').val(); + let email_list_string = $('input[name=' + user_type + '_emails_to_add]').val().trim() if (email_list_string == ""){ - var email_set = new Set(); + let email_set = new Set(); } else { - var email_set = new Set(email_list_string.split(",")) + let email_set = new Set(email_list_string.split(",")) } email_set.add(email_address) $('input[name=' + user_type + '_emails_to_add]').val(Array.from(email_set).join(',')) - var emails_display_string = Array.from(email_set).join('\n') + let emails_display_string = Array.from(email_set).join('\n') $('.added_' + user_type + 's').val(emails_display_string) } function clear_emails(user_type){ $('input[name=' + user_type + '_emails_to_add]').val("") $('.added_' + user_type + 's').val("") -}
\ No newline at end of file +} diff --git a/wqflask/wqflask/static/new/javascript/initialize_show_trait_tables.js b/wqflask/wqflask/static/new/javascript/initialize_show_trait_tables.js index 0a060cdc..e1026f8c 100644 --- a/wqflask/wqflask/static/new/javascript/initialize_show_trait_tables.js +++ b/wqflask/wqflask/static/new/javascript/initialize_show_trait_tables.js @@ -15,6 +15,8 @@ build_columns = function() { 'data': null, 'orderDataType': "dom-checkbox", 'searchable' : false, + 'targets': 0, + 'width': "25px", 'render': function(data, type, row, meta) { return '<input type="checkbox" name="searchResult" class="checkbox edit_sample_checkbox" value="">' } @@ -23,12 +25,16 @@ build_columns = function() { 'title': "ID", 'type': "natural", 'searchable' : false, + 'targets': 1, + 'width': "35px", 'data': "this_id" }, { 'title': "Sample", 'type': "natural", 'data': null, + 'targets': 2, + 'width': "60px", 'render': function(data, type, row, meta) { return '<span class="edit_sample_sample_name">' + data.name + '</span>' } @@ -38,6 +44,8 @@ build_columns = function() { 'orderDataType': "dom-input", 'type': "cust-txt", 'data': null, + 'targets': 3, + 'width': "60px", 'render': function(data, type, row, meta) { if (data.value == null) { return '<input type="text" data-value="x" data-qnorm="x" data-zscore="x" name="value:' + data.name + '" style="text-align: right;" class="trait_value_input edit_sample_value" value="x" size=' + js_data.max_digits[0] + '>' @@ -48,13 +56,17 @@ build_columns = function() { } ]; + attr_start = 4 if (js_data.se_exists) { + attr_start += 2 column_list.push( { 'bSortable': false, 'type': "natural", 'data': null, + 'targets': 4, 'searchable' : false, + 'width': "25px", 'render': function(data, type, row, meta) { return '±' } @@ -64,6 +76,8 @@ build_columns = function() { 'orderDataType': "dom-input", 'type': "cust-txt", 'data': null, + 'targets': 5, + 'width': "60px", 'render': function(data, type, row, meta) { if (data.variance == null) { return '<input type="text" data-value="x" data-qnorm="x" data-zscore="x" name="value:' + data.name + '" class="trait_value_input edit_sample_se" value="x" size=6>' @@ -73,24 +87,49 @@ build_columns = function() { } } ); - } - if (js_data.has_num_cases === true) { - column_list.push( - { - 'title': "<div style='text-align: right;'>N</div>", - 'orderDataType': "dom-input", - 'type': "cust-txt", - 'data': null, - 'render': function(data, type, row, meta) { - if (data.num_cases == null || data.num_cases == undefined) { - return '<input type="text" data-value="x" data-qnorm="x" data-zscore="x" name="value:' + data.name + '" class="trait_value_input edit_sample_num_cases" value="x" size=4 maxlength=4>' - } else { - return '<input type="text" data-value="' + data.num_cases + '" data-qnorm="x" data-zscore="x" name="value:' + data.name + '" class="trait_value_input edit_sample_num_cases" value="' + data.num_cases + '" size=4 maxlength=4>' + if (js_data.has_num_cases === true) { + attr_start += 1 + column_list.push( + { + 'title': "<div style='text-align: right;'>N</div>", + 'orderDataType': "dom-input", + 'type': "cust-txt", + 'data': null, + 'targets': 6, + 'width': "60px", + 'render': function(data, type, row, meta) { + if (data.num_cases == null || data.num_cases == undefined) { + return '<input type="text" data-value="x" data-qnorm="x" data-zscore="x" name="value:' + data.name + '" class="trait_value_input edit_sample_num_cases" value="x" size=4 maxlength=4>' + } else { + return '<input type="text" data-value="' + data.num_cases + '" data-qnorm="x" data-zscore="x" name="value:' + data.name + '" class="trait_value_input edit_sample_num_cases" value="' + data.num_cases + '" size=4 maxlength=4>' + } } } - } - ); + ); + } + } + else { + if (js_data.has_num_cases === true) { + attr_start += 1 + column_list.push( + { + 'title': "<div style='text-align: right;'>N</div>", + 'orderDataType': "dom-input", + 'type': "cust-txt", + 'data': null, + 'targets': 4, + 'width': "60px", + 'render': function(data, type, row, meta) { + if (data.num_cases == null || data.num_cases == undefined) { + return '<input type="text" data-value="x" data-qnorm="x" data-zscore="x" name="value:' + data.name + '" class="trait_value_input edit_sample_num_cases" value="x" size=4 maxlength=4>' + } else { + return '<input type="text" data-value="' + data.num_cases + '" data-qnorm="x" data-zscore="x" name="value:' + data.name + '" class="trait_value_input edit_sample_num_cases" value="' + data.num_cases + '" size=4 maxlength=4>' + } + } + } + ); + } } attr_keys = Object.keys(js_data.attributes).sort((a, b) => (js_data.attributes[a].id > js_data.attributes[b].id) ? 1 : -1) @@ -100,6 +139,7 @@ build_columns = function() { 'title': "<div title='" + js_data.attributes[attr_keys[i]].description + "' style='text-align: " + js_data.attributes[attr_keys[i]].alignment + "'>" + js_data.attributes[attr_keys[i]].name + "</div>", 'type': "natural", 'data': null, + 'targets': attr_start + i, 'render': function(data, type, row, meta) { attr_name = Object.keys(data.extra_attributes).sort((a, b) => (parseInt(a) > parseInt(b)) ? 1 : -1)[meta.col - data.first_attr_col] @@ -119,14 +159,27 @@ build_columns = function() { return column_list } -var primary_table = $('#samples_primary').DataTable( { - 'initComplete': function(settings, json) { - $('.edit_sample_value').change(function() { - edit_data_change(); - }); - }, +columnDefs = build_columns() + +loadDataTable(first_run=true, table_id="samples_primary", table_data=js_data['sample_lists'][0]) +if (js_data.sample_lists.length > 1){ + loadDataTable(first_run=true, table_id="samples_other", table_data=js_data['sample_lists'][1]) +} + +function loadDataTable(first_run=false, table_id, table_data){ + if (!first_run){ + setUserColumnsDefWidths(table_id); + } + + if (table_id == "samples_primary"){ + table_type = "Primary" + } else { + table_type = "Other" + } + + table_settings = { 'createdRow': function ( row, data, index ) { - $(row).attr('id', "Primary_" + data.this_id) + $(row).attr('id', table_type + "_" + data.this_id) $(row).addClass("value_se"); if (data.outlier) { $(row).addClass("outlier"); @@ -155,76 +208,79 @@ var primary_table = $('#samples_primary').DataTable( { $('td', row).eq(attribute_start_pos + i + 1).attr("style", "text-align: " + js_data.attributes[attr_keys[i]].alignment + "; padding-top: 2px; padding-bottom: 0px;") } }, - 'data': js_data['sample_lists'][0], - 'columns': build_columns(), - 'order': [[1, "asc"]], - 'sDom': "Ztr", - 'autoWidth': true, - 'orderClasses': true, + 'data': table_data, + 'columns': columnDefs, + "order": [[1, "asc" ]], + "sDom": "iti", + "destroy": true, + "autoWidth": false, + "bSortClasses": false, "scrollY": "100vh", - 'scroller': true, - 'scrollCollapse': true -} ); + "scrollCollapse": true, + "scroller": true, + "iDisplayLength": -1, + "initComplete": function (settings) { + //Add JQueryUI resizable functionality to each th in the ScrollHead table + $('#' + table_id + '_wrapper .dataTables_scrollHead thead th').resizable({ + handles: "e", + alsoResize: '#' + table_id + '_wrapper .dataTables_scrollHead table', //Not essential but makes the resizing smoother + resize: function( event, ui ) { + width_change = ui.size.width - ui.originalSize.width; + }, + stop: function () { + saveColumnSettings(table_id, the_table); + loadDataTable(first_run=false, table_id, table_data); + } + }); + } + } + + if (!first_run){ + $('#' + table_type.toLowerCase() + '_container').css("width", String($('#' + table_id).width() + width_change + 17) + "px"); //ZS : Change the container width by the change in width of the adjusted column, so the overall table size adjusts properly -primary_table.draw(); //ZS: This makes the table adjust its height properly on initial load + let checked_rows = get_checked_rows(table_id); + the_table = $('#' + table_id).DataTable(table_settings); + if (checked_rows.length > 0){ + recheck_rows(the_table, checked_rows); + } + } else { + the_table = $('#' + table_id).DataTable(table_settings); + the_table.draw(); + } -primary_table.on( 'order.dt search.dt draw.dt', function () { - primary_table.column(1, {search:'applied', order:'applied'}).nodes().each( function (cell, i) { + the_table.on( 'order.dt search.dt draw.dt', function () { + the_table.column(1, {search:'applied', order:'applied'}).nodes().each( function (cell, i) { cell.innerHTML = i+1; } ); -} ).draw(); + } ).draw(); -$('#primary_searchbox').on( 'keyup', function () { - primary_table.search($(this).val()).draw(); -} ); + if (first_run){ + $('#' + table_type.toLowerCase() + '_container').css("width", String($('#' + table_id).width() + 17) + "px"); + } -if (js_data.sample_lists.length > 1){ - var other_table = $('#samples_other').DataTable( { - 'initComplete': function(settings, json) { - $('.edit_sample_value').change(function() { - edit_data_change(); - }); - }, - 'createdRow': function ( row, data, index ) { - $(row).attr('id', "Primary_" + data.this_id) - $(row).addClass("value_se"); - if (data.outlier) { - $(row).addClass("outlier"); - $(row).attr("style", "background-color: orange;"); - } - $('td', row).eq(1).addClass("column_name-Index") - $('td', row).eq(2).addClass("column_name-Sample") - $('td', row).eq(3).addClass("column_name-Value") - if (js_data.se_exists) { - $('td', row).eq(5).addClass("column_name-SE") - if (js_data.has_num_cases === true) { - $('td', row).eq(6).addClass("column_name-num_cases") - } else { - if (js_data.has_num_cases === true) { - $('td', row).eq(4).addClass("column_name-num_cases") - } - } + $('#' + table_type.toLowerCase() + '_searchbox').on( 'keyup', function () { + the_table.search($(this).val()).draw(); + } ); + + $('.toggle-vis').on('click', function (e) { + e.preventDefault(); + + function toggle_column(column) { + //ZS: Toggle column visibility + column.visible( ! column.visible() ); + if (column.visible()){ + $(this).removeClass("active"); } else { - if (js_data.has_num_cases === true) { - $('td', row).eq(4).addClass("column_name-num_cases") - } + $(this).addClass("active"); } + } - for (i=0; i < attr_keys.length; i++) { - $('td', row).eq(attribute_start_pos + i + 1).addClass("column_name-" + js_data.attributes[attr_keys[i]].name) - $('td', row).eq(attribute_start_pos + i + 1).attr("style", "text-align: " + js_data.attributes[attr_keys[i]].alignment + "; padding-top: 2px; padding-bottom: 0px;") - } - }, - 'data': js_data['sample_lists'][1], - 'columns': build_columns(), - 'order': [[1, "asc"]], - 'sDom': "Ztr", - 'autoWidth': true, - 'orderClasses': true, - "scrollY": "100vh", - 'scroller': true, - 'scrollCollapse': true + // Get the column API object + var target_cols = $(this).attr('data-column').split(",") + for (let i = 0; i < target_cols.length; i++){ + var column = the_table.column( target_cols[i] ); + toggle_column(column); + } } ); - other_table.draw(); //ZS: This makes the table adjust its height properly on initial load } diff --git a/wqflask/wqflask/static/new/javascript/show_trait.js b/wqflask/wqflask/static/new/javascript/show_trait.js index f050d4ae..6b81b47e 100644 --- a/wqflask/wqflask/static/new/javascript/show_trait.js +++ b/wqflask/wqflask/static/new/javascript/show_trait.js @@ -1770,6 +1770,10 @@ $('#filter_by_value').click(function(){ edit_data_change(); }) +$('.edit_sample_value').change(function() { + edit_data_change(); +}); + $('#exclude_group').click(edit_data_change); $('#block_outliers').click(edit_data_change); $('#reset').click(edit_data_change); diff --git a/wqflask/wqflask/static/new/javascript/show_trait_mapping_tools.js b/wqflask/wqflask/static/new/javascript/show_trait_mapping_tools.js index d3b44309..737a0b82 100644 --- a/wqflask/wqflask/static/new/javascript/show_trait_mapping_tools.js +++ b/wqflask/wqflask/static/new/javascript/show_trait_mapping_tools.js @@ -208,6 +208,7 @@ $(".gemma-tab, #gemma_compute").on("click", (function(_this) { var form_data, url; url = "/loading"; $('input[name=method]').val("gemma"); + $('input[name=mapping_scale]').val('physic'); $('input[name=selected_chr]').val($('#chr_gemma').val()); $('input[name=num_perm]').val(0); $('input[name=genofile]').val($('#genofile_gemma').val()); diff --git a/wqflask/wqflask/static/new/javascript/table_functions.js b/wqflask/wqflask/static/new/javascript/table_functions.js new file mode 100644 index 00000000..745563c2 --- /dev/null +++ b/wqflask/wqflask/static/new/javascript/table_functions.js @@ -0,0 +1,88 @@ +recheck_rows = function(the_table, checked_rows){ + //ZS: This is meant to recheck checkboxes after columns are resized + check_cells = the_table.column(0).nodes().to$(); + for (let i = 0; i < check_cells.length; i++) { + if (checked_rows.includes(i)){ + check_cells[i].childNodes[0].checked = true; + } + } + + check_rows = trait_table.rows().nodes(); + for (let i =0; i < check_rows.length; i++) { + if (checked_rows.includes(i)){ + check_rows[i].classList.add("selected") + } + } +} + +get_checked_rows = function(table_id){ + let checked_rows = [] + $("#" + table_id + " input").each(function(index){ + if ($(this).prop("checked") == true){ + checked_rows.push(index); + } + }); + + return checked_rows +} + +function setUserColumnsDefWidths(table_id) { + var userColumnDef; + + // Get the settings for this table from localStorage + var userColumnDefs = JSON.parse(localStorage.getItem(table_id)) || []; + + if (userColumnDefs.length === 0 ) return; + + columnDefs.forEach( function(columnDef) { + // Check if there is a width specified for this column + userColumnDef = userColumnDefs.find( function(column) { + return column.targets === columnDef.targets; + }); + + // If there is, set the width of this columnDef in px + if ( userColumnDef ) { + + columnDef.sWidth = userColumnDef.width + 'px'; + columnDef.width = userColumnDef.width + 'px'; + + $('.toggle-vis').each(function(){ + if ($(this).attr('data-column') == columnDef.targets){ + if ($(this).hasClass("active")){ + columnDef.bVisible = false + } else { + columnDef.bVisible = true + } + } + }) + } + }); +} + +function saveColumnSettings(table_id, trait_table) { + var userColumnDefs = JSON.parse(localStorage.getItem(table_id)) || []; + var width, header, existingSetting; + + trait_table.columns().every( function ( targets ) { + // Check if there is a setting for this column in localStorage + existingSetting = userColumnDefs.findIndex( function(column) { return column.targets === targets;}); + + // Get the width of this column + header = this.header(); + width = $(header).width(); + + if ( existingSetting !== -1 ) { + // Update the width + userColumnDefs[existingSetting].width = width; + } else { + // Add the width for this column + userColumnDefs.push({ + targets: targets, + width: width, + }); + } + }); + + // Save (or update) the settings in localStorage + localStorage.setItem(table_id, JSON.stringify(userColumnDefs)); +} diff --git a/wqflask/wqflask/templates/admin/change_resource_owner.html b/wqflask/wqflask/templates/admin/change_resource_owner.html index ae9409b0..7fd84387 100644 --- a/wqflask/wqflask/templates/admin/change_resource_owner.html +++ b/wqflask/wqflask/templates/admin/change_resource_owner.html @@ -10,8 +10,7 @@ <div class="page-header"> <h1>Search for user to assign ownership to:</h1> </div> - <form id="change_owner_form" action="/resources/change_owner" method="POST"> - <input type="hidden" name="resource_id" value="{{ resource_id }}"> + <form id="change_owner_form" action="/resource-management/resources/{{ resource_id }}/change-owner" method="POST"> <div style="min-width: 600px; max-width: 800px;"> <fieldset> <div class="form-horizontal" style="width: 900px;"> @@ -57,7 +56,7 @@ $('#find_users').click(function() { $.ajax({ method: "POST", - url: "/search_for_users", + url: "/resource-management/{{ resource_id }}/users/search", data: { user_name: $('input[name=user_name]').val(), user_email: $('input[name=user_email]').val() @@ -67,42 +66,41 @@ }) populate_users = function(json_user_list){ - var user_list = JSON.parse(json_user_list) - - var the_html = "" + let user_list = JSON.parse(json_user_list) + let searchResultsHtml = "" if (user_list.length > 0){ - the_html += "<table id='users_table' style='padding-top: 10px; width: 100%;' class='table-hover table-striped cell-border'>"; - the_html += "<thead><tr><th></th><th>Index</th><th>Name</th><th>E-mail Address</th><th>Organization</th></tr></thead>"; - the_html += "<tbody>"; + searchResultsHtml += "<table id='users_table' style='padding-top: 10px; width: 100%;' class='table-hover table-striped cell-border'>"; + searchResultsHtml += "<thead><tr><th></th><th>Index</th><th>Name</th><th>E-mail Address</th><th>Organization</th></tr></thead>"; + searchResultsHtml += "<tbody>"; for (_i = 0, _len = user_list.length; _i < _len; _i++) { this_user = user_list[_i] - the_html += "<tr>"; - the_html += "<td align='center' class='select_user'><input type='radio' name='new_owner' value='" + this_user.user_id + "'></td>"; - the_html += "<td>" + (_i + 1).toString() + "</td>" + searchResultsHtml += "<tr>"; + searchResultsHtml += "<td align='center' class='select_user'><input type='radio' name='new_owner' value='" + this_user.user_id + "'></td>"; + searchResultsHtml += "<td>" + (_i + 1).toString() + "</td>" if ("full_name" in this_user) { - the_html += "<td>" + this_user.full_name + "</td>"; + searchResultsHtml += "<td>" + this_user.full_name + "</td>"; } else { - the_html += "<td>N/A</td>" + searchResultsHtml += "<td>N/A</td>" } if ("email_address" in this_user) { - the_html += "<td>" + this_user.email_address + "</td>"; + searchResultsHtml += "<td>" + this_user.email_address + "</td>"; } else { - the_html += "<td>N/A</td>" + searchResultsHtml += "<td>N/A</td>" } if ("organization" in this_user) { - the_html += "<td>" + this_user.organization + "</td>"; + searchResultsHtml += "<td>" + this_user.organization + "</td>"; } else { - the_html += "<td>N/A</td>" + searchResultsHtml += "<td>N/A</td>" } - the_html += "</tr>" + searchResultsHtml += "</tr>" } - the_html += "</tbody>"; - the_html += "</table>"; + searchResultsHtml += "</tbody>"; + searchResultsHtml += "</table>"; } else { - the_html = "<span>No users were found matching the entered criteria.</span>" + searchResultsHtml = "<span>No users were found matching the entered criteria.</span>" } - $('#user_results').html(the_html) + $('#user_results').html(searchResultsHtml) if (user_list.length > 0){ $('#users_table').dataTable({ 'order': [[1, "asc" ]], diff --git a/wqflask/wqflask/templates/admin/create_group.html b/wqflask/wqflask/templates/admin/create_group.html index 21ef5653..b1d214ea 100644 --- a/wqflask/wqflask/templates/admin/create_group.html +++ b/wqflask/wqflask/templates/admin/create_group.html @@ -6,7 +6,8 @@ <div class="page-header"> <h1>Create Group</h1> </div> - <form action="/groups/create" method="POST"> + <form action="{{ url_for('group_management.create_new_group') }}" + method="POST"> <input type="hidden" name="admin_emails_to_add" value=""> <input type="hidden" name="member_emails_to_add" value=""> <fieldset> @@ -73,17 +74,11 @@ </form> </div> - - <!-- End of body --> - {% endblock %} {% block js %} <script language="javascript" type="text/javascript" src="{{ url_for('js', filename='DataTables/js/jquery.js') }}"></script> <script language="javascript" type="text/javascript" src="/static/new/javascript/group_manager.js"></script> <script language="javascript" type="text/javascript" src="{{ url_for('js', filename='js_alt/underscore.min.js') }}"></script> - - <script type="text/javascript" charset="utf-8"> - </script> {% endblock %} diff --git a/wqflask/wqflask/templates/admin/group_manager.html b/wqflask/wqflask/templates/admin/group_manager.html index 692a7abc..eedfe138 100644 --- a/wqflask/wqflask/templates/admin/group_manager.html +++ b/wqflask/wqflask/templates/admin/group_manager.html @@ -12,8 +12,15 @@ <h1>Manage Groups</h1> {% if admin_groups|length != 0 or member_groups|length != 0 %} <div style="display: inline;"> - <button type="button" id="create_group" class="btn btn-primary" data-url="/groups/create">Create Group</button> - <button type="button" id="remove_groups" class="btn btn-primary" data-url="/groups/remove">Remove Selected Groups</button> + <a href="{{ url_for('group_management.view_create_group_page') }}" target="_blank"> + <button type="button" class="btn btn-primary"> + Create Group + </button> + </a> + <button type="button" id="remove_groups" class="btn btn-primary" + data-url="{{ url_for('group_management.delete_groups') }}"> + Remove Selected Groups + </button> </div> {% endif %} </div> @@ -23,7 +30,11 @@ {% if admin_groups|length == 0 and member_groups|length == 0 %} <h4>You currently aren't a member or admin of any groups.</h4> <br> - <button type="button" id="create_group" class="btn btn-primary" data-url="/groups/create">Create a new group</button> + <a href="{{ url_for('group_management.view_create_group_page') }}" target="_blank"> + <button type="button" class="btn btn-primary"> + Create Group + </button> + </a> {% else %} <div style="margin-top: 20px;"><h2>Admin Groups</h2></div> <hr> @@ -47,11 +58,12 @@ <tr> <td><input type="checkbox" name="group_id" value="{{ group.id }}"></td> <td align="right">{{ loop.index }}</td> - <td><a href="/groups/view?id={{ group.id }}">{{ group.name }}</a></td> + {% set group_url = url_for('group_management.view_group', group_id=group.uuid) %} + <td><a href="{{ group_url }}">{{ group.name }}</a></td> <td align="right">{{ group.admins|length + group.members|length }}</td> <td>{{ group.created_timestamp }}</td> <td>{{ group.changed_timestamp }}</td> - <td>{{ group.id }}</td> + <td>{{ group.uuid }}</td> </tr> {% endfor %} </tbody> @@ -81,7 +93,8 @@ <tr> <td><input type="checkbox" name="read" value="{{ group.id }}"></td> <td>{{ loop.index }}</td> - <td><a href="/groups/view?id={{ group.id }}">{{ group.name }}</a></td> + {% set group_url = url_for('group_management.view_group', group_id=group.uuid) %} + <td><a href="{{ group_url }}">{{ group.name }}</a></td> <td>{{ group.admins|length + group.members|length }}</td> <td>{{ group.created_timestamp }}</td> <td>{{ group.changed_timestamp }}</td> @@ -119,11 +132,6 @@ return $("#groups_form").submit(); }; - $("#create_group").on("click", function() { - url = $(this).data("url") - return submit_special(url) - }); - $("#remove_groups").on("click", function() { url = $(this).data("url") groups = [] diff --git a/wqflask/wqflask/templates/admin/manage_resource.html b/wqflask/wqflask/templates/admin/manage_resource.html index 33a37594..64d4b6eb 100644 --- a/wqflask/wqflask/templates/admin/manage_resource.html +++ b/wqflask/wqflask/templates/admin/manage_resource.html @@ -1,76 +1,95 @@ {% extends "base.html" %} {% block title %}Resource Manager{% endblock %} -{% block css %} - <link rel="stylesheet" type="text/css" href="{{ url_for('css', filename='DataTables/css/jquery.dataTables.css') }}" /> - <link rel="stylesheet" type="text/css" href="/static/new/css/show_trait.css" /> -{% endblock %} {% block content %} <!-- Start of body --> - <div class="container"> - {{ flash_me() }} - <div class="page-header" style="display: inline-block;"> - <h1>Resource Manager</h1> - <h3>{% if owner_name is not none %}Current Owner: {{ owner_name }}{% endif %} {% if admin_status == "owner" %}<button id="change_owner" class="btn btn-danger" data-url="/resources/change_owner" style="margin-left: 20px;">Change Owner</button>{% endif %}</h3> - </div> - <form id="manage_resource" action="/resources/manage" method="POST"> - <input type="hidden" name="resource_id" value="{{ resource_id }}"> - <div style="min-width: 600px; max-width: 800px;"> +<div class="container"> + <section> + {{ flash_me() }} + {% set DATA_ACCESS = access_role.get('data') %} + {% set METADATA_ACCESS = access_role.get('metadata') %} + {% set ADMIN_STATUS = access_role.get('admin') %} + {% set ADMIN_STATUS = access_role.get('admin') %} + <h1>Resource Manager</h1> + {% if resource_info.get('owner_id') %} + {% set user_details = resource_info.get('owner_details') %} + <h3> + Current Owner: {{ user_details.get('full_name') }} + </h3> + {% if user_details.get('organization') %} + <h3> + Organization: {{ user_details.get('organization')}} + </h3> + {% endif %} + {% if DATA_ACCESS > DataRole.VIEW and ADMIN_STATUS > AdminRole.NOT_ADMIN %} + <a class="btn btn-danger" target="_blank" + href="/resource-management/resources/{{ resource_info.get('resource_id') }}/change-owner"> + Change Owner + </a> + {% endif %} + {% endif %} + </section> + + <section class="container" style="margin-top: 2em;"> + <form class="container-fluid" action="/resource-management/resources/{{ resource_info.get('resource_id') }}/make-public" method="POST"> + <input type="hidden" name="resource_id" value="{{ resource_info.get('resource_id') }}"> + <div> <fieldset> <div class="form-horizontal" style="width: 900px; margin-bottom: 50px;"> <div class="form-group" style="padding-left: 20px;"> <label for="group_name" class="col-xs-3" style="float: left; font-size: 18px;">Resource Name:</label> <div class="controls input-append col-xs-9" style="display: flex; padding-left: 20px; float: left;"> - {{ resource_info.name }} + {{ resource_info.get('name') }} </div> </div> - {% if admin_status == "owner" %} + {% if DATA_ACCESS > DataRole.VIEW and ADMIN_STATUS > AdminRole.NOT_ADMIN %} + {% set is_open_to_public = DataRole(resource_info.get('default_mask').get('data')) > DataRole.NO_ACCESS %} <div class="form-group" style="padding-left: 20px;"> <label for="user_email" class="col-xs-3" style="float: left; font-size: 18px;">Open to Public:</label> <div class="controls input-append col-xs-9" style="display: flex; padding-left: 20px; float: left;"> <label class="radio-inline"> - <input type="radio" name="open_to_public" value="True" {% if default_mask != 'no-access' %}checked{% endif %}> + <input type="radio" name="open_to_public" value="True" {{ 'checked' if is_open_to_public }}> Yes </label> <label class="radio-inline"> - <input type="radio" name="open_to_public" value="False" {% if default_mask == 'no-access' %}checked{% endif %}> + <input type="radio" name="open_to_public" value="False" {{ 'checked' if not is_open_to_public }}> No - </label> + </label> </div> </div> <div class="form-group" style="padding-left: 20px;"> <label class="col-xs-3" style="float: left; font-size: 18px;"></label> <div class="controls input-append col-xs-9" style="display: flex; padding-left: 20px; float: left;"> - <button id="save_changes" class="btn btn-primary" data-url="/resources/change_default_privileges">Save Changes</button> + <button id="save_changes" class="btn btn-primary" data-url="/resource-management/resources/change_default_privileges">Save Changes</button> </div> </div> {% endif %} </div> </fieldset> </div> - {% if admin_status == "owner" or admin_status == "edit-admins" or admin_status == "edit-access" %} + {% if ADMIN_STATUS > AdminRole.NOT_ADMIN %} <div style="min-width: 600px; max-width: 800px;"> <hr> <button id="add_group_to_resource" class="btn btn-primary" style="margin-bottom: 30px;" data-url="/resources/add_group">Add Group</button> <br> - {% if group_masks|length > 0 %} + {% if resource_info.get('group_masks', [])|length > 0 %} <h2>Current Group Permissions</h2> <hr> - <table id="groups_table" class="table-hover table-striped cell-border"> + <table id="groups_table" class="table table-hover table-striped cell-border"> <thead> <tr> + <th>Id</th> <th>Name</th> <th>Data</th> <th>Metadata</th> - <th>Admin</th> </tr> </thead> <tbody> - {% for key, value in group_masks.items() %} + {% for key, value in resource_info.get('group_masks').items() %} <tr> - <td>{{ value.name }}</td> + <td>{{ key }}</td> + <td>{{ value.group_name}}</td> <td>{{ value.data }}</td> <td>{{ value.metadata }}</td> - <td>{{ value.admin }}</td> </tr> {% endfor %} </tbody> @@ -81,28 +100,25 @@ </div> {% endif %} </form> - </div> - - - -<!-- End of body --> + </section> -{% endblock %} + <!-- End of body --> -{% block js %} + {% endblock %} + {% block js %} <script language="javascript" type="text/javascript" src="{{ url_for('js', filename='DataTables/js/jquery.dataTables.min.js') }}"></script> <script type="text/javascript" charset="utf-8"> - $('#add_group_to_resource, #save_changes, #change_owner').click(function(){ - url = $(this).data("url"); - $('#manage_resource').attr("action", url) - $('#manage_resource').submit() - }) + $('#add_group_to_resource, #save_changes, #change_owner').click(function(){ + url = $(this).data("url"); + $('#manage_resource').attr("action", url) + $('#manage_resource').submit() + }) - {% if group_masks|length > 0 %} - $('#groups_table').dataTable({ - 'sDom': 'tr', - }); - {% endif %} + {% if group_masks|length > 0 %} + $('#groups_table').dataTable({ + 'sDom': 'tr', + }); + {% endif %} </script> -{% endblock %} + {% endblock %} diff --git a/wqflask/wqflask/templates/admin/view_group.html b/wqflask/wqflask/templates/admin/view_group.html index 26692fe8..c88ce0e7 100644 --- a/wqflask/wqflask/templates/admin/view_group.html +++ b/wqflask/wqflask/templates/admin/view_group.html @@ -1,26 +1,30 @@ {% extends "base.html" %} {% block title %}View and Edit Group{% endblock %} {% block css %} - <link rel="stylesheet" type="text/css" href="{{ url_for('css', filename='DataTables/css/jquery.dataTables.css') }}" /> - <link rel="stylesheet" type="text/css" href="{{ url_for('css', filename='DataTablesExtensions/buttonStyles/css/buttons.dataTables.min.css') }}" /> - <link rel="stylesheet" type="text/css" href="/static/new/css/show_trait.css" /> +<link rel="stylesheet" type="text/css" href="{{ url_for('css', filename='DataTables/css/jquery.dataTables.css') }}" /> +<link rel="stylesheet" type="text/css" href="{{ url_for('css', filename='DataTablesExtensions/buttonStyles/css/buttons.dataTables.min.css') }}" /> +<link rel="stylesheet" type="text/css" href="/static/new/css/show_trait.css" /> {% endblock %} {% block content %} <!-- Start of body --> - <div class="container"> +{% set GROUP_URL = url_for('group_management.view_group', group_id=group_info.guid) %} +{% set UPDATE_GROUP_URL = url_for('group_management.update_group', group_id=group_info.guid) %} +<div class="container"> <div class="page-header"> <h1> - <span id="group_name">{{ group_info.name }}</span> - <input type="text" name="new_group_name" style="font-size: 20px; display: none; width: 500px;" class="form-control" placeholder="{{ group_info.name }}"> + <span id="group_name">Name: {{ group_info.name }}</span> + <input type="text" name="new_group_name" style="font-size: 20px; display: none; width: 500px;" class="form-control" placeholder="{{ group_info.name }}"> + {% if is_admin %} <button class="btn btn-default" style="display: inline;" id="change_group_name">Change Group Name</button> + {% endif %} </h1> - {% if user_is_admin == true %} + {% if is_admin %} <div style="display: inline;"> <button type="button" id="remove_users" class="btn btn-danger" data-url="/groups/remove_users">Remove Selected Users from Group</button> </div> {% endif %} </div> - <form id="group_form" action="/groups/view" method="POST"> + <form id="group_form" action="{{ UPDATE_GROUP_URL }}" method="POST"> <input type="hidden" name="group_id" value="{{ group_info.id }}"> <input type="hidden" name="selected_admin_ids" value=""> <input type="hidden" name="selected_member_ids" value=""> @@ -37,6 +41,9 @@ <th>Name</th> <th>Email Address</th> <th>Organization</th> + {% if is_admin %} + <th>UID</th> + {% endif %} </tr> </thead> <tbody> @@ -47,17 +54,20 @@ <td>{% if 'full_name' in admin %}{{ admin.full_name }}{% elif 'name' in admin %}{{ admin.name }}{% else %}N/A{% endif %}</td> <td>{% if 'email_address' in admin %}{{ admin.email_address }}{% else %}N/A{% endif %}</td> <td>{% if 'organization' in admin %}{{ admin.organization }}{% else %}N/A{% endif %}</td> + {% if is_admin %} + <td>{{admin.user_id}}</td> + {% endif %} </tr> {% endfor %} </tbody> </table> - {% if user_is_admin == true %} + {% if is_admin %} <div style="margin-top: 20px;"> <span>E-mail of user to add to admins (multiple e-mails can be added separated by commas):</span> <input type="text" size="60" name="admin_emails_to_add" placeholder="Enter E-mail(s)" value=""> </div> <div style="margin-bottom: 30px; margin-top: 20px;"> - <button type="button" id="add_admins" class="btn btn-primary" data-usertype="admin" data-url="/groups/add_admins">Add Admin(s)</button> + <button type="button" id="add_admins" class="btn btn-primary" data-usertype="admin" data-url="{{ UPDATE_GROUP_URL }}">Add Admin(s)</button> </div> {% endif %} </div> @@ -74,38 +84,50 @@ <th>Name</th> <th>Email Address</th> <th>Organization</th> + {% if is_admin %} + <th>UID</th> + {% endif %} </tr> </thead> <tbody> {% for member in members %} <tr> - <td style="text-align: center; padding: 0px 10px 2px 10px;"><input type="checkbox" name="member_id" value="{{ member.user_id }}"></td> + + <td style="text-align: center; padding: 0px 10px 2px 10px;"> + {% if is_admin %} + <input type="checkbox" name="member_id" value="{{ member.user_id }}"> + {% endif %} + </td> <td align="right">{{ loop.index }}</td> <td>{% if 'full_name' in member %}{{ member.full_name }}{% elif 'name' in admin %}{{ admin.name }}{% else %}N/A{% endif %}</td> <td>{% if 'email_address' in member %}{{ member.email_address }}{% else %}N/A{% endif %}</td> <td>{% if 'organization' in member %}{{ member.organization }}{% else %}N/A{% endif %}</td> + {% if is_admin %} + <td>{{ member }}</td> + {% endif %} + </tr> {% endfor %} </tbody> </table> - {% if user_is_admin == true %} + {% if is_admin %} <div style="margin-top: 20px;"> <span>E-mail of user to add to members (multiple e-mails can be added separated by commas):</span> <input type="text" size="60" name="member_emails_to_add" placeholder="Enter E-mail(s)" value=""> </div> <div style="margin-bottom: 30px; margin-top: 20px;"> - <button type="button" id="add_members" class="btn btn-primary" data-usertype="member" data-url="/groups/add_members">Add Member(s)</button> + <button type="button" id="add_members" class="btn btn-primary" data-usertype="member" data-url="{{ GROUP_URL }}">Add Member(s)</button> </div> {% endif %} {% else %} There are currently no members in this group. - {% if user_is_admin == true %} + {% if is_admin %} <div style="margin-top: 20px;"> <span>E-mail of user to add to members (multiple e-mails can be added separated by commas):</span> <input type="text" size="60" name="member_emails_to_add" placeholder="Enter E-mail(s)" value=""> </div> <div style="margin-bottom: 30px; margin-top: 20px;"> - <button type="button" id="add_members" class="btn btn-primary" data-usertype="member" data-url="/groups/add_members">Add Member(s)</button> + <button type="button" id="add_members" class="btn btn-primary" data-usertype="member" data-url="{{ GROUP_URL }}">Add Member(s)</button> </div> {% endif %} {% endif %} @@ -219,6 +241,7 @@ $("#add_admins, #add_members").on("click", function() { url = $(this).data("url"); + console.log(url) return submit_special(url) }); @@ -230,7 +253,7 @@ new_name = $('input[name=new_group_name]').val() $.ajax({ type: "POST", - url: "/groups/change_name", + url: "{{ GROUP_URL }} ", data: { group_id: $('input[name=group_id]').val(), new_name: new_name diff --git a/wqflask/wqflask/templates/base.html b/wqflask/wqflask/templates/base.html index 14e6bc88..ad2e3744 100644 --- a/wqflask/wqflask/templates/base.html +++ b/wqflask/wqflask/templates/base.html @@ -68,7 +68,7 @@ <a href="/help" class="dropdow-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Help <span class="caret"></a> <ul class="dropdown-menu"> <li><a href="{{ url_for('references_blueprint.references') }}">References</a></li> - <li><a href="/tutorials">Tutorials/Primers</a></li> + <li><a href="/tutorials">Webinars, Tutorials/Primers</a></li> <li><a href="{{ url_for('blogs_blueprint.blogs_list') }}">Blogs</a></li> <li><a href="{{ url_for('glossary_blueprint.glossary') }}">Glossary of Term</a></li> <li><a href="http://gn1.genenetwork.org/faq.html">FAQ</a></li> @@ -87,7 +87,7 @@ <li><a href="https://systems-genetics.org/">Systems Genetics PheWAS</a></li> <li><a href="http://ucscbrowser.genenetwork.org/">Genome Browser</a></li> <li><a href="http://power.genenetwork.org">BXD Power Calculator</a></li> - <li><a href="http://notebook.genenetwork.org/">Jupyter Notebook Launcher</a></li> + <li><a href="{{url_for('jupyter_notebooks.launcher')}}">Jupyter Notebooks</a></li> <li><a href="http://datafiles.genenetwork.org">Interplanetary File System</a></li> </ul> </li> @@ -159,17 +159,16 @@ </a>; June 15, 2001 as <a href="https://www.ncbi.nlm.nih.gov/pubmed/15043217">WebQTL</a>; and Jan 5, 2005 as GeneNetwork. </p> <p> - This site is currently operated by + This site is currently operated by <a href="mailto:rwilliams@uthsc.edu">Rob Williams</a>, - <a href="http://thebird.nl/">Pjotr Prins</a>, + <a href="https://thebird.nl/">Pjotr Prins</a>, <a href="http://www.senresearch.org">Saunak Sen</a>, <a href="mailto:zachary.a.sloan@gmail.com">Zachary Sloan</a>, <a href="mailto:acenteno@uthsc.edu">Arthur Centeno</a>, - <a href="mailto:cfische7@uthsc.edu">Christian Fischer</a> and <a href="mailto:bonfacemunyoki@gmail.com">Bonface Munyoki</a>. </p> - <p>Design and code by Pjotr Prins, Zach Sloan, Arthur Centeno, Christan Fischer, Bonface Munyoki, Danny Arends, Sam Ockman, Lei Yan, Xiaodong Zhou, Christian Fernandez, - Ning Liu, Rudi Alberts, Elissa Chesler, Sujoy Roy, Evan G. Williams, Alexander G. Williams, Kenneth Manly, Jintao Wang, Robert W. Williams, and + <p>Design and code by Pjotr Prins, Zach Sloan, Arthur Centeno, Christan Fischer, Bonface Munyoki, Danny Arends, Arun Isaac, Alex Mwangi, Fred Muriithi, Sam Ockman, Lei Yan, Xiaodong Zhou, Christian Fernandez, + Ning Liu, Rudi Alberts, Elissa Chesler, Sujoy Roy, Evan G. Williams, Alexander G. Williams, Kenneth Manly, Jintao Wang, Robert W. Williams, and <!--<a href="http://genenetwork.org/credit.html">colleagues</a>.</p>--> <a href="/credits">colleagues</a>.</p> <br /> @@ -182,7 +181,12 @@ </li> <li> <a href="https://www.nigms.nih.gov/">NIGMS</a> - Systems Genetics and Precision Medicine Project (R01 GM123489, 2017-2021) + Systems Genetics and Precision Medicine Project (R01 GM123489, 2017-2026) + </li> + <li> + <a href="https://www.nsf.gov/awardsearch/showAward?AWD_ID=2118709">NSF</a> + Panorama: Integrated Rack-Scale Acceleration for Computational Pangenomics + (PPoSS 2118709, 2021-2016) </li> <li> <a href="https://www.drugabuse.gov/">NIDA</a> @@ -223,6 +227,7 @@ <a href="http://www.neuinfo.org" target="_blank"> <img src="/static/new/images/Nif.png" alt="Registered with Nif" border="0"> </a> + <script type="text/javascript" src="//rf.revolvermaps.com/0/0/8.js?i=526mdlpknyd&m=0&c=ff0000&cr1=ffffff&f=arial&l=33" async="async"></script> </div> </div> </footer> @@ -256,7 +261,8 @@ }) </script> <script src="{{ url_for('js', filename='jquery-cookie/jquery.cookie.js') }}" type="text/javascript"></script> - <script src="{{ url_for('js', filename='jquery-ui/jquery-ui.min.js') }}" type="text/javascript"></script> + <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script> + <!-- <script src="{{ url_for('js', filename='jquery-ui/jquery-ui.min.js') }}" type="text/javascript"></script> --> <script language="javascript" type="text/javascript" src="{{ url_for('js', filename='colorbox/jquery.colorbox-min.js') }}"></script> diff --git a/wqflask/wqflask/templates/collections/view.html b/wqflask/wqflask/templates/collections/view.html index a3090bcf..0ded66a6 100644 --- a/wqflask/wqflask/templates/collections/view.html +++ b/wqflask/wqflask/templates/collections/view.html @@ -36,6 +36,28 @@ <div> <br /> + <form id="heatmaps_form"> + <fieldset> + <legend>Heatmap Orientation</legend> + <label for="heatmap-orient-vertical">Vertical</label> + <input id="heatmap-orient-vertical" + type="radio" + name="vertical" + value="true" /> + <label for="heatmap-orient-horizontal">Horizontal</label> + <input id="heatmap-orient-horizontal" + type="radio" + name="vertical" + value="false" /> + </fieldset> + <button id="clustered-heatmap" + class="btn btn-primary" + data-url="{{heatmap_data_url}}" + title="Generate heatmap from this collection"> + Generate Heatmap + </button> + </form> + <div class="collection-table-options"> <form id="export_form" method="POST" action="/export_traits_csv"> <button class="btn btn-default" id="select_all" type="button"><span class="glyphicon glyphicon-ok"></span> Select All</button> @@ -52,6 +74,8 @@ <button id="delete" class="btn btn-danger submit_special" data-url="/collections/delete" type="button" title="Delete this collection" > Delete Collection</button> </form> </div> + <div id="clustered-heatmap-image-area"> + </div> <div style="margin-top: 10px; margin-bottom: 5px;"> <b>Show/Hide Columns:</b> </div> @@ -139,6 +163,8 @@ <script language="javascript" type="text/javascript" src="{{ url_for('js', filename='DataTablesExtensions/buttons/js/buttons.colVis.min.js') }}"></script> <script type="text/javascript" src="/static/new/javascript/search_results.js"></script> + <script type="text/javascript" src="{{ url_for('js', filename='plotly/plotly.min.js') }}"></script> + <script language="javascript" type="text/javascript"> $(document).ready( function () { @@ -247,6 +273,81 @@ $("#make_default").on("click", function(){ make_default(); }); + + $("#heatmaps_form").submit(function(e) { + e.preventDefault(); + }); + + function clear_heatmap_area() { + area = document.getElementById("clustered-heatmap-image-area"); + area.querySelectorAll("*").forEach(function(child) { + child.remove(); + }); + } + + function generate_progress_indicator() { + count = 0; + default_message = "Computing" + return function() { + message = default_message; + if(count >= 10) { + count = 0; + } + for(i = 0; i < count; i++) { + message = message + " ."; + } + clear_heatmap_area(); + $("#clustered-heatmap-image-area").append( + '<div class="alert alert-info"' + + ' style="font-weigh: bold; font-size: 150%;">' + + message + '</div>'); + count = count + 1; + }; + } + + function display_clustered_heatmap(heatmap_data) { + clear_heatmap_area(); + image_area = document.getElementById("clustered-heatmap-image-area") + Plotly.newPlot(image_area, heatmap_data) + } + + function process_clustered_heatmap_error(xhr, status, error) { + clear_heatmap_area() + $("#clustered-heatmap-image-area").append( + $( + '<div class="alert alert-danger">ERROR: ' + + xhr.responseJSON.message + + '</div>')); + } + + $("#clustered-heatmap").on("click", function() { + clear_heatmap_area(); + intv = window.setInterval(generate_progress_indicator(), 300); + vert_element = document.getElementById("heatmap-orient-vertical"); + vert_true = vert_element == null ? false : vert_element.checked; + heatmap_url = $(this).attr("data-url") + traits = $(".trait_checkbox:checked").map(function() { + return this.value + }).get(); + $.ajax({ + type: "POST", + url: heatmap_url, + contentType: "application/json", + data: JSON.stringify({ + "traits_names": traits, + "vertical": vert_true + }), + dataType: "JSON", + success: function(data, status, xhr) { + window.clearInterval(intv); + display_clustered_heatmap(data); + }, + error: function(xhr, status, error) { + window.clearInterval(intv); + process_clustered_heatmap_error(xhr, status, error); + } + }); + }); }); </script> diff --git a/wqflask/wqflask/templates/correlation_matrix.html b/wqflask/wqflask/templates/correlation_matrix.html index 8275f1dd..3da6981c 100644 --- a/wqflask/wqflask/templates/correlation_matrix.html +++ b/wqflask/wqflask/templates/correlation_matrix.html @@ -73,8 +73,13 @@ <br> {% if pca_works == "True" %} <h2>PCA Traits</h2> -<div style="margin-bottom: 20px; overflow:hidden;"> +<div style="margin-bottom: 20px; overflow:hidden; width: 450px;"> <table id="pca_table" class="table-hover table-striped cell-border" id='trait_table' style="float: left;"> + <colgroup> + <col span="1" style="width: 30px;"> + <col span="1" style="width: 50px;"> + <col span="1"> + </colgroup> <thead> <tr> <th></th> diff --git a/wqflask/wqflask/templates/display_diffs.html b/wqflask/wqflask/templates/display_diffs.html new file mode 100644 index 00000000..e787e468 --- /dev/null +++ b/wqflask/wqflask/templates/display_diffs.html @@ -0,0 +1,95 @@ +{% extends "base.html" %} +{% block title %}Trait Submission{% endblock %} + +{% block css %} +<link rel="stylesheet" type="text/css" href="{{ url_for('css', filename='DataTables/css/jquery.dataTables.css') }}" /> +{% endblock %} + +{% block content %} +<!-- Start of body --> +<div class="container"> + {% set additions = diff.get("Additions") %} + {% set modifications = diff.get("Modifications") %} + {% set deletions = diff.get("Deletions") %} + + {% if additions %} + <h2>Additions Data:</h2> + <div class="row"> + <div class="col-md-8"> + <table class="table-responsive table-hover table-striped cell-border" id="table-additions"> + <thead> + <th scope="col">Added Data</</th> + </thead> + <tbody> + {% for data in additions %} + <tr> + <td>{{ data }}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + {% endif %} + + {% if modifications %} + <h2>Modified Data:</h2> + + <div class="row"> + <div class="col-md-8"> + <table class="table-responsive table-hover table-striped cell-border" id="table-modifications"> + <thead> + <th scope="col">Original</</th> + <th scope="col">Current</</th> + <th scope="col">Diff</</th> + </thead> + <tbody> + {% for data in modifications %} + <tr> + <td>{{ data.get("Original") }}</td> + <td>{{ data.get("Current") }}</td> + <td><pre>{{data.get("Diff")}}</pre></td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + {% endif %} + + {% if deletions %} + <h2>Deleted Data:</h2> + <div class="row"> + <div class="col-md-8"> + <table class="table-responsive table-hover table-striped cell-border" id="table-deletions"> + <thead> + <th scope="col">Deleted Data</</th> + </thead> + <tbody> + {% for data in deletions %} + <tr> + <td>{{ data }}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + {% endif %} + +</div> +{%endblock%} + +{% block js %} +<script language="javascript" type="text/javascript" src="{{ url_for('js', filename='DataTables/js/jquery.js') }}"></script> +<script language="javascript" type="text/javascript" src="{{ url_for('js', filename='DataTables/js/jquery.dataTables.min.js') }}"></script> +<script language="javascript" type="text/javascript"> + gn_server_url = "{{ gn_server_url }}"; + + $(document).ready( function() { + $('#table-additions').dataTable(); + $('#table-modifications').dataTable(); + $('#table-deletions').dataTable(); + }); +</script> +{% endblock %} diff --git a/wqflask/wqflask/templates/display_files.html b/wqflask/wqflask/templates/display_files.html new file mode 100644 index 00000000..5fad5d14 --- /dev/null +++ b/wqflask/wqflask/templates/display_files.html @@ -0,0 +1,127 @@ +{% extends "base.html" %} +{% block title %}Trait Submission{% endblock %} + +{% block css %} +<link rel="stylesheet" type="text/css" href="{{ url_for('css', filename='DataTables/css/jquery.dataTables.css') }}" /> +{% endblock %} + +{% block content %} +<!-- Start of body --> +{% with messages = get_flashed_messages(with_categories=true) %} +{% if messages %} +{% for category, message in messages %} +<div class="container-fluid bg-{{ category }}"> + <p>{{ message }}</p> +</div> +{% endfor %} +{% endif %} +{% endwith %} + +<div class="container"> + {% if waiting %} + <h2>Files for approval:</h2> + <div class="row"> + <div class="col-md-7"> + <table class="table table-hover table-striped cell-border"> + <thead> + <th scope="col">Resource Id</</th> + <th scope="col">Author</th> + <th scope="col">TimeStamp</th> + <th scope="col"></th> + <th scope="col"></th> + </thead> + <tbody> + {% for data in waiting %} + <tr> + {% set file_url = url_for('metadata_edit.show_diff', name=data.get('file_name')) %} + <td><a href="{{ file_url }}" target="_blank">{{ data.get("resource_id") }}</a></td> + <td>{{ data.get("author")}}</td> + <td>{{ data.get("time_stamp")}}</td> + {% if data.get("roles").get("admin") > AdminRole.EDIT_ACCESS %} + {% set reject_url = url_for('metadata_edit.reject_data', resource_id=data.get('resource_id'), file_name=data.get('file_name')) %} + {% set approve_url = url_for('metadata_edit.approve_data', resource_id=data.get('resource_id'), file_name=data.get('file_name')) %} + <td> + <button type="button" + class="btn btn-secondary btn-sm"> + <a href="{{ reject_url }}">Reject</a> + </button> + </td> + <td> + <button type="button" + class="btn btn-warning btn-sm"> + <a href="{{ approve_url }}">Approve</a> + </button> + </td> + {% endif %} + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + {% endif %} + + {% if approved %} + <h2>Approved Data:</h2> + <div class="row"> + <div class="col-md-8"> + <table class="table-responsive table-hover table-striped cell-border" id="table-approved"> + <thead> + <th scope="col">Resource Id</</th> + <th scope="col">Author</th> + <th scope="col">TimeStamp</th> + </thead> + <tbody> + {% for data in approved %} + <tr> + {% set file_url = url_for('metadata_edit.show_diff', name=data.get('file_name')) %} + <td><a href="{{ file_url }}" target="_blank">{{ data.get("resource_id") }}</a></td> + <td>{{ data.get("author")}}</td> + <td>{{ data.get("time_stamp")}}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + {% endif %} + + {% if rejected %} + <h2>Rejected Files:</h2> + <div class="row"> + <div class="col-md-8"> + <table class="table-responsive table-hover table-striped cell-border" id="table-rejected"> + <thead> + <th scope="col">Resource Id</</th> + <th scope="col">Author</th> + <th scope="col">TimeStamp</th> + </thead> + <tbody> + {% for data in rejected %} + <tr> + {% set file_url = url_for('metadata_edit.show_diff', name=data.get('file_name')) %} + <td><a href="{{ file_url }}" target="_blank">{{ data.get("resource_id") }}</a></td> + <td>{{ data.get("author")}}</td> + <td>{{ data.get("time_stamp")}}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + {% endif %} +</div> +{%endblock%} + +{% block js %} +<script language="javascript" type="text/javascript" src="{{ url_for('js', filename='DataTables/js/jquery.js') }}"></script> +<script language="javascript" type="text/javascript" src="{{ url_for('js', filename='DataTables/js/jquery.dataTables.min.js') }}"></script> +<script language="javascript" type="text/javascript"> + gn_server_url = "{{ gn_server_url }}"; + + $(document).ready( function() { + $('#table-approved').dataTable(); + $('#table-rejected').dataTable(); + }); +</script> +{% endblock %} diff --git a/wqflask/wqflask/templates/display_files_admin.html b/wqflask/wqflask/templates/display_files_admin.html deleted file mode 100644 index 4b4babc4..00000000 --- a/wqflask/wqflask/templates/display_files_admin.html +++ /dev/null @@ -1,32 +0,0 @@ -{% extends "base.html" %} -{% block title %}Trait Submission{% endblock %} -{% block content %} -<!-- Start of body --> -{% with messages = get_flashed_messages(with_categories=true) %} -{% if messages %} -{% for category, message in messages %} -<div class="container-fluid bg-{{ category }}"> - <p>{{ message }}</p> -</div> -{% endfor %} -{% endif %} -{% endwith %} -Show files for approval - -<div> - <ul> - {% for file in files %} - <li><a href="/display-file/{{ file }}" target="_blank">{{ file }}</a><br/> - <button><a href="/data-samples/approve/{{ file }}">Approve</a></button> - <button><a href="/data-samples/reject/{{ file }}">Reject</a></button></li> - {% endfor %} - </ul> -</div> -{%endblock%} - -{% block js %} -<script> - gn_server_url = "{{ gn_server_url }}"; - -</script> -{% endblock %} diff --git a/wqflask/wqflask/templates/display_files_user.html b/wqflask/wqflask/templates/display_files_user.html deleted file mode 100644 index b6bab709..00000000 --- a/wqflask/wqflask/templates/display_files_user.html +++ /dev/null @@ -1,31 +0,0 @@ -{% extends "base.html" %} -{% block title %}Trait Submission{% endblock %} -{% block content %} -<!-- Start of body --> -{% with messages = get_flashed_messages(with_categories=true) %} -{% if messages %} -{% for category, message in messages %} -<div class="container-fluid bg-{{ category }}"> - <p>{{ message }}</p> -</div> -{% endfor %} -{% endif %} -{% endwith %} -Show files for approval - -<div> - <ul> - {% for file in files %} - <li><a href="/display-file/{{ file }}" target="_blank">{{ file }}</a><br/> - <button><a href="/data-samples/reject/{{ file }}">Reject</a></button></li> - {% endfor %} - </ul> -</div> -{%endblock%} - -{% block js %} -<script> - gn_server_url = "{{ gn_server_url }}"; - -</script> -{% endblock %} diff --git a/wqflask/wqflask/templates/edit_phenotype.html b/wqflask/wqflask/templates/edit_phenotype.html index 7a841793..0daea51d 100644 --- a/wqflask/wqflask/templates/edit_phenotype.html +++ b/wqflask/wqflask/templates/edit_phenotype.html @@ -62,8 +62,7 @@ </div> {% endif %} - -<form id="edit-form" class="form-horizontal" method="post" action="/trait/update" enctype=multipart/form-data> +<form id="edit-form" class="form-horizontal" method="post" action="/datasets/{{dataset_id}}/traits/{{ publish_xref.id_ }}?resource-id={{ resource_id }}" enctype='multipart/form-data'> <h2 class="text-center">Trait Information:</h2> <div class="form-group"> <label for="pubmed-id" class="col-sm-2 control-label">Pubmed ID:</label> @@ -218,7 +217,7 @@ </div> </div> <div style="margin-left: 13%;"> - <a href="/trait/{{ publish_xref.id_ }}/sampledata/{{ publish_xref.phenotype_id }}" class="btn btn-link btn-sm"> + <a href="/datasets/{{ publish_xref.id_ }}/traits/{{ publish_xref.phenotype_id }}/csv?resource-id={{ resource_id }}" class="btn btn-link btn-sm"> Sample Data(CSV Download) </a> </div> @@ -226,7 +225,6 @@ <input type = "file" class="col-sm-4 control-label" name = "file" /> </div> <div class="controls center-block" style="width: max-content;"> - <input name="dataset-name" class="changed" type="hidden" value="{{ publish_xref.id_ }}"/> <input name="inbred-set-id" class="changed" type="hidden" value="{{ publish_xref.inbred_set_id }}"/> <input name="phenotype-id" class="changed" type="hidden" value="{{ publish_xref.phenotype_id }}"/> <input name="comments" class="changed" type="hidden" value="{{ publish_xref.comments }}"/> diff --git a/wqflask/wqflask/templates/edit_probeset.html b/wqflask/wqflask/templates/edit_probeset.html index 85d49561..ab91b701 100644 --- a/wqflask/wqflask/templates/edit_probeset.html +++ b/wqflask/wqflask/templates/edit_probeset.html @@ -9,52 +9,52 @@ Submit Trait | Reset <div class="container"> <details class="col-sm-12 col-md-10 col-lg-12"> - <summary> - <h2>Update History</h2> - </summary> - <table class="table"> - <tbody> - <tr> - <th>Timestamp</th> - <th>Editor</th> - <th>Field</th> - <th>Diff</th> - </tr> - {% set ns = namespace(display_cell=True) %} + <summary> + <h2>Update History</h2> + </summary> + <table class="table"> + <tbody> + <tr> + <th>Timestamp</th> + <th>Editor</th> + <th>Field</th> + <th>Diff</th> + </tr> + {% set ns = namespace(display_cell=True) %} - {% for timestamp, group in diff %} - {% set ns.display_cell = True %} - {% for i in group %} - <tr> - {% if ns.display_cell and i.timestamp == timestamp %} + {% for timestamp, group in diff %} + {% set ns.display_cell = True %} + {% for i in group %} + <tr> + {% if ns.display_cell and i.timestamp == timestamp %} - {% set author = i.author %} - {% set timestamp_ = i.timestamp %} + {% set author = i.author %} + {% set timestamp_ = i.timestamp %} - {% else %} + {% else %} - {% set author = "" %} - {% set timestamp_ = "" %} + {% set author = "" %} + {% set timestamp_ = "" %} - {% endif %} - <td>{{ timestamp_ }}</td> - <td>{{ author }}</td> - <td>{{ i.diff.field }}</td> - <td><pre>{{ i.diff.diff }}</pre></td> - {% set ns.display_cell = False %} - </tr> - {% endfor %} - {% endfor %} - </tbody> - </table> + {% endif %} + <td>{{ timestamp_ }}</td> + <td>{{ author }}</td> + <td>{{ i.diff.field }}</td> + <td><pre>{{ i.diff.diff }}</pre></td> + {% set ns.display_cell = False %} + </tr> + {% endfor %} + {% endfor %} + </tbody> + </table> </details> </div> {% endif %} -<form id="edit-form" class="form-horizontal" method="post" action="/probeset/update"> - <h2 class="text-center">Probeset Information:</h2> +<form id="edit-form" class="form-horizontal" method="post" action="/datasets/traits/{{ name }}?resource-id={{ resource_id }}"> + <h2 class="text-center">Probeset Information:</h2> <div class="form-group"> <label for="symbol" class="col-sm-2 control-label">Symbol:</label> <div class="col-sm-4"> diff --git a/wqflask/wqflask/templates/gsearch_gene.html b/wqflask/wqflask/templates/gsearch_gene.html index 5549ac8a..69281ec5 100644 --- a/wqflask/wqflask/templates/gsearch_gene.html +++ b/wqflask/wqflask/templates/gsearch_gene.html @@ -2,6 +2,7 @@ {% block title %}Search Results{% endblock %} {% block css %} <link rel="stylesheet" type="text/css" href="{{ url_for('css', filename='DataTables/css/jquery.dataTables.css') }}" /> + <link href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css" rel="stylesheet" type="text/css" /> <link rel="stylesheet" type="text/css" href="/static/new/css/show_trait.css" /> {% endblock %} {% block content %} @@ -31,7 +32,7 @@ </form> <br /> <br /> - <div style="min-width: 2000px; width: 100%;"> + <div id="table_container" style="width: 2000px;"> <table id="trait_table" class="table-hover table-striped cell-border" style="float: left;"> <tbody> <td colspan="100%" align="center"><br><b><font size="15">Loading...</font></b><br></td> @@ -54,6 +55,7 @@ <script language="javascript" type="text/javascript" src="{{ url_for('js', filename='DataTablesExtensions/colReorder/js/dataTables.colReorder.js') }}"></script> <script language="javascript" type="text/javascript" src="{{ url_for('js', filename='DataTablesExtensions/colResize/dataTables.colResize.js') }}"></script> <script language="javascript" type="text/javascript" src="/static/new/javascript/search_results.js"></script> + <script language="javascript" type="text/javascript" src="/static/new/javascript/table_functions.js"></script> <script type='text/javascript'> var getParams = function(url) { @@ -73,51 +75,152 @@ <script type="text/javascript" charset="utf-8"> $(document).ready( function () { - - $('#trait_table tr').click(function(event) { - if (event.target.type !== 'checkbox') { - $(':checkbox', this).trigger('click'); - } - }); + var tableId = "trait_table"; - function change_buttons() { - buttons = ["#add", "#remove"]; - num_checked = $('.trait_checkbox:checked').length; - if (num_checked === 0) { - for (_i = 0, _len = buttons.length; _i < _len; _i++) { - button = buttons[_i]; - $(button).prop("disabled", true); - } - } else { - for (_j = 0, _len2 = buttons.length; _j < _len2; _j++) { - button = buttons[_j]; - $(button).prop("disabled", false); - } + var width_change = 0; //ZS: For storing the change in width so overall table width can be adjusted by that amount + + columnDefs = [ + { + 'orderDataType': "dom-checkbox", + 'width': "5px", + 'data': null, + 'targets': 0, + 'render': function(data, type, row, meta) { + return '<input type="checkbox" name="searchResult" class="trait_checkbox checkbox" value="' + data.hmac + '">' } - //}); - if ($(this).is(":checked")) { - if (!$(this).closest('tr').hasClass('selected')) { - $(this).closest('tr').addClass('selected') - } + }, + { + 'title': "Index", + 'type': "natural", + 'width': "30px", + 'targets': 1, + 'data': "index" + }, + { + 'title': "Record", + 'type': "natural", + 'orderDataType': "dom-inner-text", + 'width': "60px", + 'data': null, + 'targets': 2, + 'render': function(data, type, row, meta) { + return '<a target="_blank" href="/show_trait?trait_id=' + data.name + '&dataset=' + data.dataset + '">' + data.name + '</a>' } - else { - if ($(this).closest('tr').hasClass('selected')) { - $(this).closest('tr').removeClass('selected') - } + }, + { + 'title': "Species", + 'type': "natural", + 'width': "60px", + 'targets': 3, + 'data': "species" + }, + { + 'title': "Group", + 'type': "natural", + 'width': "150px", + 'targets': 4, + 'data': "group" + }, + { + 'title': "Tissue", + 'type': "natural", + 'width': "150px", + 'targets': 5, + 'data': "tissue" + }, + { + 'title': "Dataset", + 'type': "natural", + 'targets': 6, + 'width': "320px", + 'data': "dataset_fullname" + }, + { + 'title': "Symbol", + 'type': "natural", + 'width': "60px", + 'targets': 7, + 'data': "symbol" + }, + { + 'title': "Description", + 'type': "natural", + 'data': null, + 'width': "120px", + 'targets': 8, + 'render': function(data, type, row, meta) { + try { + return decodeURIComponent(escape(data.description)) + } catch(err) { + return escape(data.description) + } } - } + }, + { + 'title': "Location", + 'type': "natural-minus-na", + 'width': "125px", + 'targets': 9, + 'data': "location_repr" + }, + { + 'title': "Mean", + 'type': "natural-minus-na", + 'orderSequence': [ "desc", "asc"], + 'width': "30px", + 'targets': 10, + 'data': "mean" + }, + { + 'title': "Max<br>LRS<a href=\"{{ url_for('glossary_blueprint.glossary') }}#LRS\" target=\"_blank\" style=\"color: white;\"><sup>?</sup></a>", + 'type': "natural-minus-na", + 'width': "60px", + 'targets': 11, + 'data': "LRS_score_repr", + 'orderSequence': [ "desc", "asc"] + }, + { + 'title': "Max LRS Location", + 'type': "natural-minus-na", + 'width': "125px", + 'targets': 12, + 'data': "max_lrs_text" + }, + { + 'title': "Additive<br>Effect<a href=\"{{ url_for('glossary_blueprint.glossary') }}#A\" target=\"_blank\" style=\"color: white;\"><sup>?</sup></a>", + 'type': "natural-minus-na", + 'width': "50px", + 'targets': 13, + 'data': "additive", + 'orderSequence': [ "desc", "asc"] + } + ] + + loadDataTable(true); - var the_table = $('#trait_table').DataTable( { + function loadDataTable(first_run=false){ + + if (!first_run){ + setUserColumnsDefWidths(tableId); + } + + table_settings = { 'drawCallback': function( settings ) { - $('#trait_table tr').click(function(event) { - if (event.target.type !== 'checkbox' && event.target.tagName.toLowerCase() !== 'a') { - $(':checkbox', this).trigger('click'); - } - }); - $('.trait_checkbox:checkbox').on("change", change_buttons); + $('#' + tableId + ' tr').off().on("click", function(event) { + if (event.target.type !== 'checkbox' && event.target.tagName.toLowerCase() !== 'a') { + var obj =$(this).find('input'); + obj.prop('checked', !obj.is(':checked')); + } + if ($(this).hasClass("selected") && event.target.tagName.toLowerCase() !== 'a'){ + $(this).removeClass("selected") + } else if (event.target.tagName.toLowerCase() !== 'a') { + $(this).addClass("selected") + } + change_buttons() + }); }, 'createdRow': function ( row, data, index ) { - $('td', row).eq(0).attr("style", "text-align: center; padding: 0px 10px 2px 13px;"); + $('td', row).eq(0).attr("style", "text-align: center; padding: 0px 10px 2px 10px;"); $('td', row).eq(1).attr("align", "right"); $('td', row).eq(4).attr('title', $('td', row).eq(4).text()); if ($('td', row).eq(4).text().length > 30) { @@ -155,144 +258,60 @@ $('td', row).eq(13).attr('data-export', $('td', row).eq(13).text()); }, 'data': trait_list, - 'columns': [ - { - 'orderDataType': "dom-checkbox", - 'width': "10px", - 'data': null, - 'render': function(data, type, row, meta) { - return '<input type="checkbox" name="searchResult" class="trait_checkbox checkbox" value="' + data.hmac + '">' - } - }, - { - 'title': "Index", - 'type': "natural", - 'width': "30px", - 'data': "index" - }, - { - 'title': "Record", - 'type': "natural", - 'orderDataType': "dom-inner-text", - 'width': "60px", - 'data': null, - 'render': function(data, type, row, meta) { - return '<a target="_blank" href="/show_trait?trait_id=' + data.name + '&dataset=' + data.dataset + '">' + data.name + '</a>' - } - }, - { - 'title': "Species", - 'type': "natural", - 'width': "60px", - 'data': "species" - }, - { - 'title': "Group", - 'type': "natural", - 'width': "150px", - 'data': "group" - }, - { - 'title': "Tissue", - 'type': "natural", - 'width': "150px", - 'data': "tissue" - }, - { - 'title': "Dataset", - 'type': "natural", - 'data': "dataset_fullname" - }, - { - 'title': "Symbol", - 'type': "natural", - 'width': "60px", - 'data': "symbol" - }, - { - 'title': "Description", - 'type': "natural", - 'data': null, - 'render': function(data, type, row, meta) { - try { - return decodeURIComponent(escape(data.description)) - } catch(err) { - return escape(data.description) - } - } - }, - { - 'title': "Location", - 'type': "natural-minus-na", - 'width': "125px", - 'data': "location_repr" - }, - { - 'title': "Mean", - 'type': "natural-minus-na", - 'orderSequence': [ "desc", "asc"], - 'width': "30px", - 'data': "mean" - }, - { - 'title': "Max<br>LRS<a href=\"{{ url_for('glossary_blueprint.glossary') }}#LRS\" target=\"_blank\" style=\"color: white;\"><sup>?</sup></a>", - 'type': "natural-minus-na", - 'width': "60px", - 'data': "LRS_score_repr", - 'orderSequence': [ "desc", "asc"] - }, - { - 'title': "Max LRS Location", - 'type': "natural-minus-na", - 'width': "125px", - 'data': "max_lrs_text" - }, - { - 'title': "Additive<br>Effect<a href=\"{{ url_for('glossary_blueprint.glossary') }}#A\" target=\"_blank\" style=\"color: white;\"><sup>?</sup></a>", - 'type': "natural-minus-na", - 'width': "50px", - 'data': "additive", - 'orderSequence': [ "desc", "asc"] - } - ], + 'columns': columnDefs, "order": [[1, "asc" ]], 'sDom': "iti", - "autoWidth": true, + "destroy": true, + "deferRender": true, "bSortClasses": false, - 'processing': true, {% if trait_count > 20 %} "scrollY": "100vh", "scroller": true, - "scrollCollapse": true + "scrollCollapse": true, {% else %} - "iDisplayLength": -1 + "iDisplayLength": -1, {% endif %} - } ); + "initComplete": function (settings) { + //Add JQueryUI resizable functionality to each th in the ScrollHead table + $('#' + tableId + '_wrapper .dataTables_scrollHead thead th').resizable({ + handles: "e", + alsoResize: '#' + tableId + '_wrapper .dataTables_scrollHead table', //Not essential but makes the resizing smoother + resize: function( event, ui ) { + width_change = ui.size.width - ui.originalSize.width; + }, + stop: function () { + saveColumnSettings(tableId, trait_table); + loadDataTable(); + } + }); + } + } - $('#trait_table').append( - '<tfoot>' + - '<tr>' + - '<th></th>' + - '<th>Index</th>' + - '<th>Record ID</th>' + - '<th>Species</th> ' + - '<th>Group</th>' + - '<th>Tissue</th>' + - '<th>Dataset</th>' + - '<th>Symbol</th>' + - '<th>Description</th>' + - '<th>Location</th>' + - '<th>Mean</th>' + - '<th>Max LRS <a href="{{ url_for('glossary_blueprint.glossary') }}#LRS" target="_blank" style="color: white;"><sup>?</sup></a></th>' + - '<th>Max LRS Location</th>' + - '<th>Additive Effect <a href="{{ url_for('glossary_blueprint.glossary') }}#A" target="_blank" style="color: white;"><sup>?</sup></a></th>' + - '</tr>' + - '</tfoot>' - ); + if (!first_run){ + table_settings['autoWidth'] = false; + $('#table_container').css("width", String($('#trait_table').width() + width_change {% if trait_list|length > 20 %}+ 17{% endif %}) + "px"); //ZS : Change the container width by the change in width of the adjusted column, so the overall table size adjusts properly + } + + let checked_rows = get_checked_rows(tableId); + trait_table = $('#' + tableId).DataTable(table_settings); + if (checked_rows.length > 0){ + recheck_rows(trait_table, checked_rows); + } + + if (first_run){ + {% if trait_list|length > 20 %} + $('#table_container').css("width", String($('#trait_table').width() + 17) + "px"); + {% endif %} + trait_table.draw(); + } + } + + $('#redraw').click(function() { + var table = $('#' + tableId).DataTable(); + table.colReorder.reset() + }); - the_table.draw(); }); </script> - <script language="javascript" type="text/javascript" src="/static/new/javascript/search_results.js"></script> {% endblock %} diff --git a/wqflask/wqflask/templates/gsearch_pheno.html b/wqflask/wqflask/templates/gsearch_pheno.html index 89316cbc..7abdb222 100644 --- a/wqflask/wqflask/templates/gsearch_pheno.html +++ b/wqflask/wqflask/templates/gsearch_pheno.html @@ -2,6 +2,7 @@ {% block title %}Search Results{% endblock %} {% block css %} <link rel="stylesheet" type="text/css" href="{{ url_for('css', filename='DataTables/css/jquery.dataTables.css') }}" /> + <link href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css" rel="stylesheet" type="text/css" /> <link rel="stylesheet" type="text/css" href="/static/new/css/show_trait.css" /> {% endblock %} {% block content %} @@ -21,8 +22,8 @@ <button class="btn btn-default" id="deselect_all"><span class="glyphicon glyphicon-remove"></span> Deselect All</button> <button class="btn btn-default" id="invert"><span class="glyphicon glyphicon-resize-vertical"></span> Invert</button> <button class="btn btn-success" id="add" disabled ><span class="glyphicon glyphicon-plus-sign"></span> Add</button> - <input type="text" id="searchbox" class="form-control" style="width: 200px; display: inline;" placeholder="Search This Table For ..."> - <input type="text" id="select_top" class="form-control" style="width: 200px; display: inline;" placeholder="Select Top ..."> + <input type="text" id="searchbox" class="form-control" style="width: 180px; display: inline;" placeholder="Search This Table For ..."> + <input type="text" id="select_top" class="form-control" style="width: 120px; display: inline;" placeholder="Select Top ..."> <form id="export_form" method="POST" action="/export_traits_csv" style="display: inline;"> <input type="hidden" name="headers" id="headers" value="{% for field in header_fields %}{{ field }},{% endfor %}"> <input type="hidden" name="database_name" id="database_name" value="None"> @@ -31,7 +32,7 @@ </form> <br /> <br /> - <div style="min-width: 2000px; width: 100%;"> + <div id="table_container" style="width: 2000px;"> <table id="trait_table" class="table-hover table-striped cell-border" style="float: left;"> <tbody> <td colspan="100%" align="center"><br><b><font size="15">Loading...</font></b><br></td> @@ -54,6 +55,7 @@ <script language="javascript" type="text/javascript" src="{{ url_for('js', filename='DataTablesExtensions/colReorder/js/dataTables.colReorder.js') }}"></script> <script language="javascript" type="text/javascript" src="{{ url_for('js', filename='DataTablesExtensions/colResize/dataTables.colResize.js') }}"></script> <script language="javascript" type="text/javascript" src="/static/new/javascript/search_results.js"></script> + <script language="javascript" type="text/javascript" src="/static/new/javascript/table_functions.js"></script> <script type='text/javascript'> var getParams = function(url) { @@ -73,218 +75,233 @@ <script type="text/javascript" charset="utf-8"> $(document).ready( function () { + var tableId = "trait_table"; - $('#trait_table tr').click(function(event) { - if (event.target.type !== 'checkbox') { - $(':checkbox', this).trigger('click'); - } - }); + var width_change = 0; //ZS: For storing the change in width so overall table width can be adjusted by that amount - function change_buttons() { - buttons = ["#add", "#remove"]; - num_checked = $('.trait_checkbox:checked').length; - if (num_checked === 0) { - for (_i = 0, _len = buttons.length; _i < _len; _i++) { - button = buttons[_i]; - $(button).prop("disabled", true); - } + columnDefs = [ + { + 'data': null, + 'orderDataType': "dom-checkbox", + 'width': "10px", + 'targets': 0, + 'render': function(data, type, row, meta) { + return '<input type="checkbox" name="searchResult" class="trait_checkbox checkbox" value="' + data.hmac + '">' + } + }, + { + 'title': "Index", + 'type': "natural", + 'width': "30px", + 'targets': 1, + 'data': "index" + }, + { + 'title': "Species", + 'type': "natural", + 'width': "60px", + 'targets': 2, + 'data': "species" + }, + { + 'title': "Group", + 'type': "natural", + 'width': "100px", + 'targets': 3, + 'data': "group" + }, + { + 'title': "Record", + 'type': "natural", + 'data': null, + 'width': "60px", + 'targets': 4, + 'orderDataType': "dom-inner-text", + 'render': function(data, type, row, meta) { + return '<a target="_blank" href="/show_trait?trait_id=' + data.name + '&dataset=' + data.dataset + '">' + data.display_name + '</a>' + } + }, + { + 'title': "Description", + 'type': "natural", + 'width': "500px", + 'targets': 5, + 'data': null, + 'render': function(data, type, row, meta) { + try { + return decodeURIComponent(escape(data.description)) + } catch(err) { + return data.description + } + } + }, + { + 'title': "Mean", + 'type': "natural-minus-na", + 'width': "30px", + 'targets': 6, + 'data': "mean" + }, + { + 'title': "Authors", + 'type': "natural", + 'width': "300px", + 'targets': 7, + 'data': null, + 'render': function(data, type, row, meta) { + author_list = data.authors.split(",") + if (author_list.length >= 6) { + author_string = author_list.slice(0, 6).join(",") + ", et al." + } else{ + author_string = data.authors + } + return author_string + } + }, + { + 'title': "Year", + 'type': "natural-minus-na", + 'data': null, + 'orderDataType': "dom-inner-text", + 'width': "25px", + 'targets': 8, + 'render': function(data, type, row, meta) { + if (data.pubmed_id != "N/A"){ + return '<a href="' + data.pubmed_link + '">' + data.pubmed_text + '</a>' } else { - for (_j = 0, _len2 = buttons.length; _j < _len2; _j++) { - button = buttons[_j]; - $(button).prop("disabled", false); - } + return data.pubmed_text } - //}); - if ($(this).is(":checked")) { - if (!$(this).closest('tr').hasClass('selected')) { - $(this).closest('tr').addClass('selected') - } + }, + 'orderSequence': [ "desc", "asc"] + }, + { + 'title': "Max LRS<a href=\"{{ url_for('glossary_blueprint.glossary') }}#LRS\" target=\"_blank\" style=\"color: white;\"><sup>?</sup></a>", + 'type': "natural-minus-na", + 'data': "LRS_score_repr", + 'width': "60px", + 'targets': 9, + 'orderSequence': [ "desc", "asc"] + }, + { + 'title': "Max LRS Location", + 'type': "natural-minus-na", + 'width': "125px", + 'targets': 10, + 'data': "max_lrs_text" + }, + { + 'title': "Additive Effect<a href=\"{{ url_for('glossary_blueprint.glossary') }}#A\" target=\"_blank\" style=\"color: white;\"><sup>?</sup></a>", + 'type': "natural-minus-na", + 'data': "additive", + 'width': "60px", + 'targets': 11, + 'orderSequence': [ "desc", "asc"] + } + ] + + loadDataTable(true); + + function loadDataTable(first_run=false){ + + if (!first_run){ + setUserColumnsDefWidths(tableId); + } + + table_settings = { + 'drawCallback': function( settings ) { + $('#' + tableId + ' tr').off().on("click", function(event) { + if (event.target.type !== 'checkbox' && event.target.tagName.toLowerCase() !== 'a') { + var obj =$(this).find('input'); + obj.prop('checked', !obj.is(':checked')); + } + if ($(this).hasClass("selected") && event.target.tagName.toLowerCase() !== 'a'){ + $(this).removeClass("selected") + } else if (event.target.tagName.toLowerCase() !== 'a') { + $(this).addClass("selected") + } + change_buttons() + }); + }, + "createdRow": function ( row, data, index ) { + $('td', row).eq(0).attr("style", "text-align: center; padding: 4px 10px 2px 10px;"); + $('td', row).eq(1).attr("align", "right"); + $('td', row).eq(5).attr('title', $('td', row).eq(5).text()); + if ($('td', row).eq(5).text().length > 150) { + $('td', row).eq(5).text($('td', row).eq(5).text().substring(0, 150)); + $('td', row).eq(5).text($('td', row).eq(5).text() + '...') } - else { - if ($(this).closest('tr').hasClass('selected')) { - $(this).closest('tr').removeClass('selected') - } + $('td', row).eq(6).attr('title', $('td', row).eq(6).text()); + if ($('td', row).eq(6).text().length > 150) { + $('td', row).eq(6).text($('td', row).eq(6).text().substring(0, 150)); + $('td', row).eq(6).text($('td', row).eq(6).text() + '...') } + $('td', row).eq(6).attr("align", "right"); + $('td', row).slice(8,11).attr("align", "right"); + $('td', row).eq(1).attr('data-export', $('td', row).eq(1).text()); + $('td', row).eq(2).attr('data-export', $('td', row).eq(2).text()); + $('td', row).eq(3).attr('data-export', $('td', row).eq(3).text()); + $('td', row).eq(4).attr('data-export', $('td', row).eq(4).text()); + $('td', row).eq(5).attr('data-export', $('td', row).eq(5).text()); + $('td', row).eq(6).attr('data-export', $('td', row).eq(6).text()); + $('td', row).eq(7).attr('data-export', $('td', row).eq(7).text()); + $('td', row).eq(8).attr('data-export', $('td', row).eq(8).text()); + $('td', row).eq(9).attr('data-export', $('td', row).eq(9).text()); + $('td', row).eq(10).attr('data-export', $('td', row).eq(10).text()); + }, + 'data': trait_list, + 'columns': columnDefs, + "order": [[1, "asc" ]], + 'sDom': "iti", + "destroy": true, + "deferRender": true, + "bSortClasses": false, + {% if trait_count > 20 %} + "scrollY": "100vh", + "scroller": true, + "scrollCollapse": true, + {% else %} + "iDisplayLength": -1, + {% endif %} + "initComplete": function (settings) { + //Add JQueryUI resizable functionality to each th in the ScrollHead table + $('#' + tableId + '_wrapper .dataTables_scrollHead thead th').resizable({ + handles: "e", + alsoResize: '#' + tableId + '_wrapper .dataTables_scrollHead table', //Not essential but makes the resizing smoother + resize: function( event, ui ) { + width_change = ui.size.width - ui.originalSize.width; + }, + stop: function () { + saveColumnSettings(tableId, trait_table); + loadDataTable(); + } + }); + } } - var the_table = $('#trait_table').DataTable( { - 'drawCallback': function( settings ) { - $('#trait_table tr').click(function(event) { - if (event.target.type !== 'checkbox' && event.target.tagName.toLowerCase() !== 'a') { - $(':checkbox', this).trigger('click'); - } - }); - $('.trait_checkbox:checkbox').on("change", change_buttons); - }, - "createdRow": function ( row, data, index ) { - $('td', row).eq(0).attr("style", "text-align: center; padding: 4px 10px 2px 10px;"); - $('td', row).eq(1).attr("align", "right"); - $('td', row).eq(5).attr('title', $('td', row).eq(5).text()); - if ($('td', row).eq(5).text().length > 150) { - $('td', row).eq(5).text($('td', row).eq(5).text().substring(0, 150)); - $('td', row).eq(5).text($('td', row).eq(5).text() + '...') - } - $('td', row).eq(6).attr('title', $('td', row).eq(6).text()); - if ($('td', row).eq(6).text().length > 150) { - $('td', row).eq(6).text($('td', row).eq(6).text().substring(0, 150)); - $('td', row).eq(6).text($('td', row).eq(6).text() + '...') - } - $('td', row).eq(6).attr("align", "right"); - $('td', row).slice(8,11).attr("align", "right"); - $('td', row).eq(1).attr('data-export', $('td', row).eq(1).text()); - $('td', row).eq(2).attr('data-export', $('td', row).eq(2).text()); - $('td', row).eq(3).attr('data-export', $('td', row).eq(3).text()); - $('td', row).eq(4).attr('data-export', $('td', row).eq(4).text()); - $('td', row).eq(5).attr('data-export', $('td', row).eq(5).text()); - $('td', row).eq(6).attr('data-export', $('td', row).eq(6).text()); - $('td', row).eq(7).attr('data-export', $('td', row).eq(7).text()); - $('td', row).eq(8).attr('data-export', $('td', row).eq(8).text()); - $('td', row).eq(9).attr('data-export', $('td', row).eq(9).text()); - $('td', row).eq(10).attr('data-export', $('td', row).eq(10).text()); - }, - 'data': trait_list, - 'columns': [ - { - 'data': null, - 'orderDataType': "dom-checkbox", - 'width': "10px", - 'render': function(data, type, row, meta) { - return '<input type="checkbox" name="searchResult" class="trait_checkbox checkbox" value="' + data.hmac + '">' - } - }, - { - 'title': "Index", - 'type': "natural", - 'width': "30px", - 'data': "index" - }, - { - 'title': "Species", - 'type': "natural", - 'width': "60px", - 'data': "species" - }, - { - 'title': "Group", - 'type': "natural", - 'width': "100px", - 'data': "group" - }, - { - 'title': "Record", - 'type': "natural", - 'data': null, - 'width': "60px", - 'orderDataType': "dom-inner-text", - 'render': function(data, type, row, meta) { - return '<a target="_blank" href="/show_trait?trait_id=' + data.name + '&dataset=' + data.dataset + '">' + data.display_name + '</a>' - } - }, - { - 'title': "Description", - 'type': "natural", - 'width': "500px", - 'data': null, - 'render': function(data, type, row, meta) { - try { - return decodeURIComponent(escape(data.description)) - } catch(err) { - return data.description - } - } - }, - { - 'title': "Mean", - 'type': "natural-minus-na", - 'width': "30px", - 'data': "mean" - }, - { - 'title': "Authors", - 'type': "natural", - 'width': "300px", - 'data': null, - 'render': function(data, type, row, meta) { - author_list = data.authors.split(",") - if (author_list.length >= 6) { - author_string = author_list.slice(0, 6).join(",") + ", et al." - } else{ - author_string = data.authors - } - return author_string - } - }, - { - 'title': "Year", - 'type': "natural-minus-na", - 'data': null, - 'orderDataType': "dom-inner-text", - 'width': "25px", - 'render': function(data, type, row, meta) { - if (data.pubmed_id != "N/A"){ - return '<a href="' + data.pubmed_link + '">' + data.pubmed_text + '</a>' - } else { - return data.pubmed_text - } - }, - 'orderSequence': [ "desc", "asc"] - }, - { - 'title': "Max LRS<a href=\"{{ url_for('glossary_blueprint.glossary') }}#LRS\" target=\"_blank\" style=\"color: white;\"><sup>?</sup></a>", - 'type': "natural-minus-na", - 'data': "LRS_score_repr", - 'width': "60px", - 'orderSequence': [ "desc", "asc"] - }, - { - 'title': "Max LRS Location", - 'type': "natural-minus-na", - 'width': "125px", - 'data': "max_lrs_text" - }, - { - 'title': "Additive Effect<a href=\"{{ url_for('glossary_blueprint.glossary') }}#A\" target=\"_blank\" style=\"color: white;\"><sup>?</sup></a>", - 'type': "natural-minus-na", - 'data': "additive", - 'width': "60px", - 'orderSequence': [ "desc", "asc"] - } - ], - "order": [[1, "asc" ]], - 'sDom': "iti", - "autoWidth": true, - "bSortClasses": false, - 'processing': true, - {% if trait_count > 20 %} - "scrollY": "100vh", - "scroller": true, - "scrollCollapse": true - {% else %} - "iDisplayLength": -1 - {% endif %} - } ); + if (!first_run){ + table_settings['autoWidth'] = false; + $('#table_container').css("width", String($('#trait_table').width() + width_change {% if trait_list|length > 20 %}+ 17{% endif %}) + "px"); // Change the container width by the change in width of the adjusted column, so the overall table size adjusts properly + } - $('#trait_table').append( - '<tfoot>' + - '<tr>' + - '<th></th>' + - '<th>Index</th>' + - '<th>Species</th> ' + - '<th>Group</th>' + - '<th>Record</th>' + - '<th>Description</th>' + - '<th>Authors</th>' + - '<th>Year</th>' + - '<th>Max LRS</th>' + - '<th>Max LRS Location</th>' + - '<th>Additive Effect</th>' + - '</tr>' + - '</tfoot>' - ); + let checked_rows = get_checked_rows(tableId); + trait_table = $('#' + tableId).DataTable(table_settings); + if (checked_rows.length > 0){ + recheck_rows(trait_table, checked_rows); + } - the_table.draw(); + if (first_run){ + {% if trait_list|length > 20 %} + $('#table_container').css("width", String($('#trait_table').width() + 17) + "px"); + {% endif %} + } + + trait_table.draw(); + } + + $('#redraw').click(function() { + var table = $('#' + tableId).DataTable(); + table.colReorder.reset() + }); }); - </script> - <script language="javascript" type="text/javascript" src="/static/new/javascript/search_results.js"></script> {% endblock %} diff --git a/wqflask/wqflask/templates/index_page.html b/wqflask/wqflask/templates/index_page.html index 7b103305..3a490658 100755 --- a/wqflask/wqflask/templates/index_page.html +++ b/wqflask/wqflask/templates/index_page.html @@ -1,17 +1,18 @@ {% extends "base.html" %} {% block title %}GeneNetwork{% endblock %} {% block css %} +<!-- UIkit CSS --> +<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.7.4/dist/css/uikit.min.css" /> + +<!-- UIkit JS --> +<script src="https://cdn.jsdelivr.net/npm/uikit@3.7.4/dist/js/uikit.min.js"></script> +<script src="https://cdn.jsdelivr.net/npm/uikit@3.7.4/dist/js/uikit-icons.min.js"></script> <style TYPE="text/css"> p.interact { display: none; } - - - .tweet { - padding:5px; color:#000; - } .media { @@ -26,7 +27,24 @@ border-radius: 5px; /*transform: scale(1.1); image small?*/ border:1px solid #c8ccc9; + } + + div#tweets { + margin:0 auto; + width: 590px; + height: 160px; + background: white; + position: relative; + border-radius:10px; + background-color: #F9F9F9; + } + + h2 { + margin-bottom: 0px; + } + ul { + margin-top: 0px; } </style> {% endblock %} @@ -40,12 +58,12 @@ <div class="col-xs-4" style="margin-right:50px; min-width: 530px; max-width: 550px;"> <section id="search"> - <div> - <h1>Select and search</h1> + <div class="page-header"> + <h2>Select and search</h2> </div> <form method="get" action="/search" target="_blank" name="SEARCHFORM"> <fieldset> - <div style="padding: 20px" class="form-horizontal"> + <div style="padding-left: 20px; padding-right: 20px;" class="form-horizontal"> <div class="form-group"> <label for="species" class="col-xs-1 control-label" style="width: 65px !important;">Species:</label> @@ -195,53 +213,74 @@ </div> <div class="col-xs-4" style="width: 600px !important;"> - <section id="affiliates"> - <div class="page-header"> - <h1>Affiliates</h1> - <ul> - <li><b><a href="http://gn1.genenetwork.org">GeneNetwork 1</a> at UTHSC</b></li> - <li><a href="https://systems-genetics.org/">Systems Genetics</a> at EPFL</li> - <li><a href="http://bnw.genenetwork.org/">Bayesian Network Web Server</a> at UTHSC</li> - <li><a href="https://www.geneweaver.org/">GeneWeaver</a></li> - <li><a href="https://phenogen.org/">PhenoGen</a> at University of Colorado</li> - <li><a href="http://www.webgestalt.org/">WebGestalt</a> at Baylor</li> - </ul> + <section id="tutorials"> + <div class="page-header"> + <h2>Tutorials</h2> + </div> + <div class="uk-grid-match uk-child-width-1-3@m" uk-grid> + <div> + <strong><a class="uk-link-text" href="/tutorials">Webinars & Courses</a></strong><br> + In-person courses, live webinars and webinar recordings<br> + <a href="/tutorials" class="uk-icon-link" uk-icon="laptop" ratio="2"></a> + </div> + <div> + <strong><a class="uk-link-text" href="/tutorials">Tutorials</a></strong><br> + Tutorials: Training materials in HTML, PDF and video formats<br> + <a href="/tutorials" class="uk-icon-link" uk-icon="file-text" ratio="2"></a> + </div> + <div> + <strong><a class="uk-link-text" href="/tutorials">Documentation</a></strong><br> + Online manuals, handbooks, fact sheets and FAQs<br> + <a href="/tutorials" class="uk-icon-link" uk-icon="album" ratio="2"></a> + </div> </div> </section> <section id="news-section"> <div class="page-header"> - <h1>News</h1> - <div id="tweets" style="height: 300px; overflow: scroll; overflow-x: hidden;"></div> - <div align="right"> - <a href="https://twitter.com/GeneNetwork2">more news items...</a> - </div> + <h2>News</h2> </div> - </section> - <section id="websites"> - <div class="page-header"> - <h1>Github</h1> - <ul> - <li><a href="https://github.com/genenetwork/genenetwork2">GN2 Source Code</a></li> - <li><a href="https://github.com/genenetwork/genenetwork">GN1 Source Code</a></li> - <li><a href="https://github.com/genenetwork/sysmaintenance">System Maintenance Code</a></li> - </ul> + <div id="tweets" style="height: 300px; overflow: scroll; overflow-x: hidden;"></div> + <div align="right"> + <a href="https://twitter.com/GeneNetwork2">more news items...</a> </div> - </section> + </section> <section id="websites"> <div class="page-header"> - <h1>Links</h1> + <h2>Links</h2> </div> - <h3>GeneNetwork v2:</h3> - <ul> - <li><a href="http://genenetwork.org/">Main website</a> at UTHSC</li> - </ul> - <h3>GeneNetwork v1:</h3> <ul> - <li><a href="http://gn1.genenetwork.org/">Main website</a> at UTHSC</li> - <li><span class="broken_link" href="http://artemis.uthsc.edu/">Time Machine</span>: Full GN versions from 2009 to 2016 (mm9)</li> + <li>Github</li> + <ul> + <li><a href="https://github.com/genenetwork/genenetwork2">GN2 Source Code</a></li> + <li><a href="https://github.com/genenetwork/genenetwork">GN1 Source Code</a></li> + <li><a href="https://github.com/genenetwork/sysmaintenance">System Maintenance Code</a></li> + </ul> + </ul> + <ul> + <li>GeneNetwork v2</li> + <ul> + <li><a href="http://genenetwork.org/">Main website</a> at UTHSC</li> + </ul> + </ul> + <ul> + <li>GeneNetwork v1</li> + <ul> + <li><a href="http://gn1.genenetwork.org/">Main website</a> at UTHSC</li> + <li><span class="broken_link" href="http://artemis.uthsc.edu/">Time Machine</span>: Full GN versions from 2009 to 2016 (mm9)</li> Cloud (EC2)</a></li> + </ul> + </ul> + <ul> + <li>Affiliates</li> + <ul> + <li><b><a href="http://gn1.genenetwork.org">GeneNetwork 1</a> at UTHSC</b></li> + <li><a href="https://systems-genetics.org/">Systems Genetics</a> at EPFL</li> + <li><a href="http://bnw.genenetwork.org/">Bayesian Network Web Server</a> at UTHSC</li> + <li><a href="https://www.geneweaver.org/">GeneWeaver</a></li> + <li><a href="https://phenogen.org/">PhenoGen</a> at University of Colorado</li> + <li><a href="http://www.webgestalt.org/">WebGestalt</a> at Baylor</li> + </ul> </ul> - <script type="text/javascript" src="//rf.revolvermaps.com/0/0/8.js?i=526mdlpknyd&m=0&c=ff0000&cr1=ffffff&f=arial&l=33" async="async"></script> </section> </div> </div> diff --git a/wqflask/wqflask/templates/jupyter_notebooks.html b/wqflask/wqflask/templates/jupyter_notebooks.html new file mode 100644 index 00000000..afc95a15 --- /dev/null +++ b/wqflask/wqflask/templates/jupyter_notebooks.html @@ -0,0 +1,28 @@ +{%extends "base.html"%} + +{%block title%} +Jupyter Notebooks +{%endblock%} + +{%block css%} +<link rel="stylesheet" type="text/css" href="/static/new/css/jupyter_notebooks.css" /> +{%endblock%} + +{%block content%} + +<div class="container"> + <h1>Current Notebooks</h1> + + {%for item in links:%} + <div class="jupyter-links"> + <a href="{{item.get('main_url')}}" + title="Access running notebook for '{{item.get('notebook_name')}}' on GeneNetwork" + class="main-link">{{item.get("notebook_name")}}</a> + <a href="{{item.get('src_link_url')}}" + title="Link to the notebook repository for {{item.get('notebook_name', '_')}}" + class="src-link">View Source</a> + </div> + {%endfor%} +</div> + +{%endblock%} diff --git a/wqflask/wqflask/templates/loading.html b/wqflask/wqflask/templates/loading.html index ccf810b0..b9e31ad0 100644 --- a/wqflask/wqflask/templates/loading.html +++ b/wqflask/wqflask/templates/loading.html @@ -66,11 +66,11 @@ {% endif %} {% endif %} {% else %} - <h1>Loading {{ start_vars.tool_used }} Results...</h1> + <h1> {{ start_vars.tool_used }} Computation in progress ...</h1> {% endif %} <br><br> <div style="text-align: center;"> - <img align="center" src="/static/gif/89.gif"> + <img align="center" src="/static/gif/waitAnima2.gif"> </div> {% if start_vars.vals_diff|length != 0 and start_vars.transform == "" %} <br><br> diff --git a/wqflask/wqflask/templates/mapping_results.html b/wqflask/wqflask/templates/mapping_results.html index f2d11e89..84db288c 100644 --- a/wqflask/wqflask/templates/mapping_results.html +++ b/wqflask/wqflask/templates/mapping_results.html @@ -12,7 +12,7 @@ {% endblock %} {% from "base_macro.html" import header %} {% block content %} - <div class="container" style="min-width: 900px;"> + <div class="container"> <form method="post" target="_blank" action="/run_mapping" name="marker_regression" id="marker_regression_form"> <input type="hidden" name="temp_uuid" value="{{ temp_uuid }}"> {% if temp_trait is defined %} @@ -39,6 +39,7 @@ <input type="hidden" name="maf" value="{{ maf }}"> <input type="hidden" name="use_loco" value="{{ use_loco }}"> <input type="hidden" name="selected_chr" value="{{ selectedChr }}"> + <input type="hidden" name="mapping_scale" value="{{ plotScale }}"> <input type="hidden" name="manhattan_plot" value="{{ manhattan_plot }}"> {% if manhattan_plot == True %} <input type="hidden" name="color_scheme" value="alternating"> diff --git a/wqflask/wqflask/templates/search_result_page.html b/wqflask/wqflask/templates/search_result_page.html index c499aa8f..f73cba17 100644 --- a/wqflask/wqflask/templates/search_result_page.html +++ b/wqflask/wqflask/templates/search_result_page.html @@ -5,6 +5,7 @@ <link rel="stylesheet" type="text/css" href="{{ url_for('css', filename='fontawesome/css/font-awesome.min.css') }}" /> <link rel="stylesheet" type="text/css" href="{{ url_for('js', filename='DataTablesExtensions/buttonStyles/css/buttons.dataTables.min.css') }}"> <link rel="stylesheet" type="text/css" href="{{ url_for('css', filename='fontawesome/css/all.min.css') }}"/> + <link href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css" rel="stylesheet" type="text/css" /> <link rel="stylesheet" type="text/css" href="/static/new/css/show_trait.css" /> <link rel="stylesheet" type="text/css" href="static/new/css/trait_list.css" /> {% endblock %} @@ -125,8 +126,8 @@ {% endif %} </div> {% endif %} - <div id="table_container" {% if dataset.type == 'ProbeSet' or dataset.type == 'Publish' %}style="min-width: 1500px; max-width:100%;"{% endif %}> - <table class="table-hover table-striped cell-border" id='trait_table' style="float: left; width: {% if dataset.type == 'Geno' %}380px{% else %}100%{% endif %};"> + <div id="table_container" style="width: {% if dataset.type == 'Geno' %}270{% else %}100%{% endif %}px;"> + <table class="table-hover table-striped cell-border" id='trait_table' style="float: left;"> <tbody> <td colspan="100%" align="center"><br><b><font size="15">Loading...</font></b><br></td> </tbody> @@ -147,7 +148,6 @@ {% block js %} <script language="javascript" type="text/javascript" src="{{ url_for('js', filename='js_alt/md5.min.js') }}"></script> - <script language="javascript" type="text/javascript" src="{{ url_for('js', filename='DataTables/js/jquery.dataTables.min.js') }}"></script> <script language="javascript" type="text/javascript" src="{{ url_for('js', filename='DataTablesExtensions/scroller/js/dataTables.scroller.min.js') }}"></script> <script language="javascript" type="text/javascript" src="{{ url_for('js', filename='jszip/jszip.min.js') }}"></script> @@ -157,6 +157,7 @@ <script language="javascript" type="text/javascript" src="{{ url_for('js', filename='fontawesome/js/all.min.js') }}"></script> <script language="javascript" type="text/javascript" src="/static/new/javascript/search_results.js"></script> + <script language="javascript" type="text/javascript" src="/static/new/javascript/table_functions.js"></script> <script type='text/javascript'> var trait_list = {{ trait_list|safe }}; @@ -175,11 +176,222 @@ return params; }; - {% if results|count > 0 %} - //ZS: Need to make sort by symbol, also need to make sure blank symbol fields at the bottom and symbols starting with numbers below letters - trait_table = $('#trait_table').DataTable( { + {% if results|count > 0 and not too_many_results %} + var tableId = "trait_table"; + + var width_change = 0; //ZS: For storing the change in width so overall table width can be adjusted by that amount + + columnDefs = [ + { + 'data': null, + 'width': "5px", + 'orderDataType': "dom-checkbox", + 'targets': 0, + 'render': function(data, type, row, meta) { + return '<input type="checkbox" name="searchResult" class="checkbox trait_checkbox" value="' + data.hmac + '">' + } + }, + { + 'title': "Index", + 'type': "natural", + 'width': "35px", + 'targets': 1, + 'data': "index" + } + {% if dataset.type == 'ProbeSet' %}, + { + 'title': "Record", + 'type': "natural-minus-na", + 'data': null, + 'width': "{{ max_widths.display_name * 8 }}px", + 'targets': 2, + 'render': function(data, type, row, meta) { + return '<a target="_blank" href="/show_trait?trait_id=' + data.display_name + '&dataset=' + data.dataset + '">' + data.display_name + '</a>' + } + }, + { + 'title': "Symbol", + 'type': "natural", + 'width': "{{ max_widths.symbol * 8 }}px", + 'targets': 3, + 'data': "symbol" + }, + { + 'title': "Description", + 'type': "natural", + 'data': null, + 'targets': 4, + 'render': function(data, type, row, meta) { + try { + return decodeURIComponent(escape(data.description)) + } catch(err){ + return escape(data.description) + } + } + }, + { + 'title': "<div style='text-align: right;'>Location</div>", + 'type': "natural-minus-na", + 'width': "125px", + 'targets': 5, + 'data': "location" + }, + { + 'title': "<div style='text-align: right;'>Mean</div>", + 'type': "natural-minus-na", + 'width': "40px", + 'data': "mean", + 'targets': 6, + 'orderSequence': [ "desc", "asc"] + }, + { + 'title': "<div style='text-align: right;'>Peak <a href=\"{{ url_for('glossary_blueprint.glossary') }}#LRS\" target=\"_blank\" style=\"color: white;\"> <i class=\"fa fa-info-circle\" aria-hidden=\"true\"></i></a></div><div style='text-align: right;'>LOD  </div>", + 'type': "natural-minus-na", + 'data': "lod_score", + 'width': "60px", + 'targets': 7, + 'orderSequence': [ "desc", "asc"] + }, + { + 'title': "<div style='text-align: right;'>Peak Location</div>", + 'type': "natural-minus-na", + 'width': "125px", + 'targets': 8, + 'data': "lrs_location" + }, + { + 'title': "<div style='text-align: right;'>Effect <a href=\"{{ url_for('glossary_blueprint.glossary') }}#A\" target=\"_blank\" style=\"color: white;\"> <i class=\"fa fa-info-circle\" aria-hidden=\"true\"></i></a></div><div style='text-align: right;'>Size  </div>", + 'type': "natural-minus-na", + 'data': "additive", + 'width': "65px", + 'targets': 9, + 'orderSequence': [ "desc", "asc"] + }{% elif dataset.type == 'Publish' %}, + { + 'title': "Record", + 'type': "natural-minus-na", + 'width': "{{ max_widths.display_name * 9 }}px", + 'data': null, + 'targets': 2, + 'render': function(data, type, row, meta) { + return '<a target="_blank" href="/show_trait?trait_id=' + data.name + '&dataset=' + data.dataset + '">' + data.display_name + '</a>' + } + }, + { + 'title': "Description", + 'type': "natural", + {% if (max_widths.description * 7) < 500 %} + 'width': "{{ max_widths.description * 7 }}px", + {% else %} + 'width': "500px", + {% endif %} + 'data': null, + 'targets': 3, + 'render': function(data, type, row, meta) { + try { + return decodeURIComponent(escape(data.description)) + } catch(err){ + return data.description + } + } + }, + { + 'title': "<div style='text-align: right;'>Mean</div>", + 'type': "natural-minus-na", + 'width': "60px", + 'data': "mean", + 'targets': 4, + 'orderSequence': [ "desc", "asc"] + }, + { + 'title': "Authors", + 'type': "natural", + {% if (max_widths.authors * 7) < 500 %} + 'width': "{{ max_widths.authors * 7 }}px", + {% else %} + 'width': "500px", + {% endif %} + 'data': null, + 'targets': 5, + 'render': function(data, type, row, meta) { + author_list = data.authors.split(",") + if (author_list.length >= 6) { + author_string = author_list.slice(0, 6).join(",") + ", et al." + } else{ + author_string = data.authors + } + return author_string + } + }, + { + 'title': "<div style='text-align: right;'>Year</div>", + 'type': "natural-minus-na", + 'data': null, + 'width': "50px", + 'targets': 6, + 'render': function(data, type, row, meta) { + if (data.pubmed_id != "N/A"){ + return '<a href="' + data.pubmed_link + '">' + data.pubmed_text + '</a>' + } else { + return data.pubmed_text + } + }, + 'orderSequence': [ "desc", "asc"] + }, + { + 'title': "<div style='text-align: right;'>Peak <a href=\"{{ url_for('glossary_blueprint.glossary') }}#LRS\" target=\"_blank\" style=\"color: white;\"> <i class=\"fa fa-info-circle\" aria-hidden=\"true\"></i></a></div><div style='text-align: right;'>LOD  </div>", + 'type': "natural-minus-na", + 'data': "lod_score", + 'targets': 7, + 'width': "60px", + 'orderSequence': [ "desc", "asc"] + }, + { + 'title': "<div style='text-align: right;'>Peak Location</div>", + 'type': "natural-minus-na", + 'width': "120px", + 'targets': 8, + 'data': "lrs_location" + }, + { + 'title': "<div style='text-align: right;'>Effect <a href=\"{{ url_for('glossary_blueprint.glossary') }}#A\" target=\"_blank\" style=\"color: white;\"> <i class=\"fa fa-info-circle\" aria-hidden=\"true\"></i></a></div><div style='text-align: right;'>Size  </div>", + 'type': "natural-minus-na", + 'width': "60px", + 'data': "additive", + 'targets': 9, + 'orderSequence': [ "desc", "asc"] + }{% elif dataset.type == 'Geno' %}, + { + 'title': "Record", + 'type': "natural-minus-na", + 'width': "{{ max_widths.display_name * 9 }}px", + 'data': null, + 'targets': 2, + 'render': function(data, type, row, meta) { + return '<a target="_blank" href="/show_trait?trait_id=' + data.display_name + '&dataset=' + data.dataset + '">' + data.display_name + '</a>' + } + }, + { + 'title': "<div style='text-align: right;'>Location</div>", + 'type': "natural-minus-na", + 'width': "120px", + 'targets': 2, + 'data': "location" + }{% endif %} + ]; + + loadDataTable(true); + + function loadDataTable(first_run=false){ + + if (!first_run){ + setUserColumnsDefWidths(tableId); + } + + //ZS: Need to make sort by symbol, also need to make sure blank symbol fields at the bottom and symbols starting with numbers below letters + table_settings = { 'drawCallback': function( settings ) { - $('#trait_table tr').off().on("click", function(event) { + $('#' + tableId + ' tr').off().on("click", function(event) { if (event.target.type !== 'checkbox' && event.target.tagName.toLowerCase() !== 'a') { var obj =$(this).find('input'); obj.prop('checked', !obj.is(':checked')); @@ -193,7 +405,7 @@ }); }, 'createdRow': function ( row, data, index ) { - $('td', row).eq(0).attr("style", "text-align: center; padding: 0px 10px 2px 13px;"); + $('td', row).eq(0).attr("style", "text-align: center; padding: 0px 10px 2px 10px;"); $('td', row).eq(1).attr("align", "right"); $('td', row).eq(1).attr('data-export', index+1); $('td', row).eq(2).attr('data-export', $('td', row).eq(2).text()); @@ -228,173 +440,60 @@ $('td', row).eq(3).attr('data-export', $('td', row).eq(3).text()); {% endif %} }, - 'data': trait_list, - 'columns': [ - { - 'data': null, - 'width': "10px", - 'orderDataType': "dom-checkbox", - 'orderable': false, - 'render': function(data, type, row, meta) { - return '<input type="checkbox" name="searchResult" class="checkbox trait_checkbox" value="' + data.hmac + '">' - } - }, - { - 'title': "Index", - 'type': "natural", - 'width': "30px", - 'data': "index" - }, - { - 'title': "Record", - 'type': "natural-minus-na", - 'data': null, - 'width': "60px", - 'render': function(data, type, row, meta) { - return '<a target="_blank" href="/show_trait?trait_id=' + data.name + '&dataset=' + data.dataset + '">' + data.display_name + '</a>' - } - }{% if dataset.type == 'ProbeSet' %}, - { - 'title': "Symbol", - 'type': "natural", - 'width': "120px", - 'data': "symbol" - }, - { - 'title': "Description", - 'type': "natural", - 'data': null, - 'render': function(data, type, row, meta) { - try { - return decodeURIComponent(escape(data.description)) - } catch(err){ - return escape(data.description) - } - } - }, - { - 'title': "<div style='text-align: right;'>Location</div>", - 'type': "natural-minus-na", - 'width': "125px", - 'data': "location" - }, - { - 'title': "<div style='text-align: right;'>Mean</div>", - 'type': "natural-minus-na", - 'width': "30px", - 'data': "mean", - 'orderSequence': [ "desc", "asc"] - }, - { - 'title': "<div style='text-align: right;'>Peak <a href=\"{{ url_for('glossary_blueprint.glossary') }}#LRS\" target=\"_blank\" style=\"color: white;\"> <i class=\"fa fa-info-circle\" aria-hidden=\"true\"></i></a></div><div style='text-align: right;'>LOD  </div>", - 'type': "natural-minus-na", - 'data': "lod_score", - 'width': "60px", - 'orderSequence': [ "desc", "asc"] - }, - { - 'title': "<div style='text-align: right;'>Peak Location</div>", - 'type': "natural-minus-na", - 'width': "125px", - 'data': "lrs_location" - }, - { - 'title': "<div style='text-align: right;'>Effect <a href=\"{{ url_for('glossary_blueprint.glossary') }}#A\" target=\"_blank\" style=\"color: white;\"> <i class=\"fa fa-info-circle\" aria-hidden=\"true\"></i></a></div><div style='text-align: right;'>Size  </div>", - 'type': "natural-minus-na", - 'data': "additive", - 'width': "60px", - 'orderSequence': [ "desc", "asc"] - }{% elif dataset.type == 'Publish' %}, - { - 'title': "Description", - 'type': "natural", - 'width': "500px", - 'data': null, - 'render': function(data, type, row, meta) { - try { - return decodeURIComponent(escape(data.description)) - } catch(err){ - return data.description - } - } - }, - { - 'title': "<div style='text-align: right;'>Mean</div>", - 'type': "natural-minus-na", - 'width': "30px", - 'data': "mean", - 'orderSequence': [ "desc", "asc"] - }, - { - 'title': "Authors", - 'type': "natural", - 'width': "300px", - 'data': null, - 'render': function(data, type, row, meta) { - author_list = data.authors.split(",") - if (author_list.length >= 6) { - author_string = author_list.slice(0, 6).join(",") + ", et al." - } else{ - author_string = data.authors - } - return author_string - } - }, - { - 'title': "<div style='text-align: right;'>Year</div>", - 'type': "natural-minus-na", - 'data': null, - 'width': "25px", - 'render': function(data, type, row, meta) { - if (data.pubmed_id != "N/A"){ - return '<a href="' + data.pubmed_link + '">' + data.pubmed_text + '</a>' - } else { - return data.pubmed_text - } - }, - 'orderSequence': [ "desc", "asc"] - }, - { - 'title': "<div style='text-align: right;'>Peak <a href=\"{{ url_for('glossary_blueprint.glossary') }}#LRS\" target=\"_blank\" style=\"color: white;\"> <i class=\"fa fa-info-circle\" aria-hidden=\"true\"></i></a></div><div style='text-align: right;'>LOD  </div>", - 'type': "natural-minus-na", - 'data': "lod_score", - 'width': "60px", - 'orderSequence': [ "desc", "asc"] - }, - { - 'title': "<div style='text-align: right;'>Peak Location</div>", - 'type': "natural-minus-na", - 'width': "120px", - 'data': "lrs_location" - }, - { - 'title': "<div style='text-align: right;'>Effect <a href=\"{{ url_for('glossary_blueprint.glossary') }}#A\" target=\"_blank\" style=\"color: white;\"> <i class=\"fa fa-info-circle\" aria-hidden=\"true\"></i></a></div><div style='text-align: right;'>Size  </div>", - 'type': "natural-minus-na", - 'width': "60px", - 'data': "additive", - 'orderSequence': [ "desc", "asc"] - }{% elif dataset.type == 'Geno' %}, - { - 'title': "<div style='text-align: right;'>Location</div>", - 'type': "natural-minus-na", - 'width': "120px", - 'data': "location" - }{% endif %} - ], + "data": trait_list, + "columns": columnDefs, "order": [[1, "asc" ]], - 'sDom': "iti", - "autoWidth": true, + "sDom": "iti", + "destroy": true, + "autoWidth": false, "bSortClasses": false, - {% if trait_list|length > 20 %} - "scrollY": "100vh", + "scrollY": "500px", + "scrollCollapse": true, + {% if trait_list|length > 5 %} "scroller": true, - "scrollCollapse": true + {% endif %} + "iDisplayLength": -1, + "initComplete": function (settings) { + //Add JQueryUI resizable functionality to each th in the ScrollHead table + $('#' + tableId + '_wrapper .dataTables_scrollHead thead th').resizable({ + handles: "e", + alsoResize: '#' + tableId + '_wrapper .dataTables_scrollHead table', //Not essential but makes the resizing smoother + resize: function( event, ui ) { + width_change = ui.size.width - ui.originalSize.width; + }, + stop: function () { + saveColumnSettings(tableId, trait_table); + loadDataTable(); + } + }); + } + } + + if (!first_run){ + $('#table_container').css("width", String($('#trait_table').width() + width_change {% if trait_list|length > 20 %}+ 17{% endif %}) + "px"); //ZS : Change the container width by the change in width of the adjusted column, so the overall table size adjusts properly + + let checked_rows = get_checked_rows(tableId); + trait_table = $('#' + tableId).DataTable(table_settings); + if (checked_rows.length > 0){ + recheck_rows(trait_table, checked_rows); + } + } else { + trait_table = $('#' + tableId).DataTable(table_settings); + trait_table.draw(); + } + + if (first_run){ + {% if trait_list|length > 20 %} + $('#table_container').css("width", String($('#trait_table').width() + 17) + "px"); {% else %} - "iDisplayLength": -1 + $('#table_container').css("width", String($('#trait_table').width()) + "px"); {% endif %} - } ); + } + } - trait_table.draw(); //ZS: This makes the table adjust its height properly on initial load + window.addEventListener('resize', function(){ + trait_table.columns.adjust(); + }); $('.toggle-vis').on( 'click', function (e) { e.preventDefault(); @@ -412,9 +511,8 @@ } } ); - $('#redraw').click(function() { - var table = $('#trait_table').DataTable(); + var table = $('#' + tableId).DataTable(); table.colReorder.reset() }); {% endif %} diff --git a/wqflask/wqflask/templates/show_trait.html b/wqflask/wqflask/templates/show_trait.html index f3fa1332..7074e21e 100644 --- a/wqflask/wqflask/templates/show_trait.html +++ b/wqflask/wqflask/templates/show_trait.html @@ -5,9 +5,10 @@ <link rel="stylesheet" type="text/css" href="/static/new/css/box_plot.css" /> <link rel="stylesheet" type="text/css" href="/static/new/css/prob_plot.css" /> <link rel="stylesheet" type="text/css" href="/static/new/css/scatter-matrix.css" /> - <link rel="stylesheet" type="text/css" href="{{ url_for('css', filename='d3-tip/d3-tip.css') }}" /> + <link rel="stylesheet" type="text/css" href="{{ url_for('css', filename='d3-tip/d3-tip.css') }}" /> <link rel="stylesheet" type="text/css" href="{{ url_for('css', filename='DataTables/css/jquery.dataTables.css') }}" /> <link rel="stylesheet" type="text/css" href="{{ url_for('css', filename='nouislider/nouislider.min.css') }}" /> + <link href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css" rel="stylesheet" type="text/css" /> <link rel="stylesheet" type="text/css" href="/static/new/css/trait_list.css" /> <link rel="stylesheet" type="text/css" href="/static/new/css/show_trait.css" /> @@ -155,6 +156,7 @@ <script type="text/javascript" src="/static/new/javascript/show_trait.js"></script> <script type="text/javascript" src="/static/new/javascript/validation.js"></script> <script type="text/javascript" src="/static/new/javascript/get_covariates_from_collection.js"></script> + <script type="text/javascript" src="/static/new/javascript/table_functions.js"></script> <script type="text/javascript" charset="utf-8"> @@ -211,16 +213,6 @@ } }); - primary_table.on( 'order.dt search.dt draw.dt', function () { - primary_table.column(1, {search:'applied', order:'applied'}).nodes().each( function (cell, i) { - cell.innerHTML = i+1; - } ); - } ).draw(); - - $('#primary_searchbox').on( 'keyup', function () { - primary_table.search($(this).val()).draw(); - } ); - $('.toggle-vis').on('click', function (e) { e.preventDefault(); @@ -237,7 +229,6 @@ // Get the column API object var target_cols = $(this).attr('data-column').split(",") for (let i = 0; i < target_cols.length; i++){ - console.log("THE COL:", target_cols[i]) var column = primary_table.column( target_cols[i] ); toggle_column(column); @@ -248,11 +239,7 @@ } } ); - {% if sample_groups|length != 1 %} - $('#other_searchbox').on( 'keyup', function () { - other_table.search($(this).val()).draw(); - } ); - {% endif %} + $('#samples_primary, #samples_other').find("tr.outlier").css('background-color', 'orange') $('.edit_sample_checkbox:checkbox').change(function() { if ($(this).is(":checked")) { diff --git a/wqflask/wqflask/templates/show_trait_details.html b/wqflask/wqflask/templates/show_trait_details.html index 2a21dd24..4e9ea0fb 100644 --- a/wqflask/wqflask/templates/show_trait_details.html +++ b/wqflask/wqflask/templates/show_trait_details.html @@ -234,16 +234,16 @@ {% endif %} {% endif %} <button type="button" id="view_in_gn1" class="btn btn-primary" title="View Trait in GN1" onclick="window.open('http://gn1.genenetwork.org/webqtl/main.py?cmd=show&db={{ this_trait.dataset.name }}&probeset={{ this_trait.name }}', '_blank')">Go to GN1</button> - {% if admin_status == "owner" or admin_status == "edit-admins" or admin_status == "edit-access" %} + {% if admin_status != None and admin_status.get('metadata', DataRole.VIEW) > DataRole.VIEW %} {% if this_trait.dataset.type == 'Publish' %} - <button type="button" id="edit_resource" class="btn btn-success" title="Edit Resource" onclick="window.open('/trait/{{ this_trait.name }}/edit/inbredset-id/{{ this_trait.dataset.id }}', '_blank')">Edit</button> + <button type="button" id="edit_resource" class="btn btn-success" title="Edit Resource" onclick="window.open('/datasets/{{ this_trait.dataset.group.id }}/traits/{{ this_trait.name }}?resource-id={{ resource_id }}', '_blank')">Edit</button> {% endif %} {% if this_trait.dataset.type == 'ProbeSet' %} - <button type="button" id="edit_resource" class="btn btn-success" title="Edit Resource" onclick="window.open('/trait/edit/probeset-name/{{ this_trait.name }}', '_blank')">Edit</button> + <button type="button" id="edit_resource" class="btn btn-success" title="Edit Resource" onclick="window.open('/datasets/traits/{{ this_trait.name }}?resource-id={{ resource_id }}', '_blank')">Edit</button> {% endif %} - {% if admin_status == "owner" or admin_status == "edit-admins" or admin_status == "edit-access" %} - <button type="button" id="edit_resource" class="btn btn-success" title="Edit Resource" onclick="window.open('./resources/manage?resource_id={{ resource_id }}', '_blank')">Edit Privileges</button> + {% if admin_status.get('metadata', DataRole.VIEW) > DataRole.VIEW %} + <button type="button" id="edit_resource" class="btn btn-success" title="Edit Privileges" onclick="window.open('/resource-management/resources/{{ resource_id }}', '_blank')">Edit Privileges</button> {% endif %} {% endif %} </div> diff --git a/wqflask/wqflask/templates/show_trait_edit_data.html b/wqflask/wqflask/templates/show_trait_edit_data.html index 5939c953..e288e4d5 100644 --- a/wqflask/wqflask/templates/show_trait_edit_data.html +++ b/wqflask/wqflask/templates/show_trait_edit_data.html @@ -53,12 +53,12 @@ </div> </div> {% set outer_loop = loop %} - <div class="sample_group" style="width:{{ trait_table_width }}px;"> + <div class="sample_group"> <div style="position: relative;"> <div class="inline-div"><h3 style="float: left;">{{ sample_type.header }}<span name="transform_text"></span></h3></div> </div> - <div id="table_container"> - <table class="table-hover table-striped cell-border sample-table" id="samples_{{ sample_type.sample_group_type }}"> + <div id="{{ sample_type.sample_group_type }}_container" style="width: {{ trait_table_width }}px;"> + <table class="table-hover table-striped cell-border" id="samples_{{ sample_type.sample_group_type }}"> <tbody> <td colspan="100%" align="center"><br><b><font size="15">Loading...</font></b><br></td> </tbody> diff --git a/wqflask/wqflask/templates/test_wgcna_results.html b/wqflask/wqflask/templates/test_wgcna_results.html new file mode 100644 index 00000000..952f479e --- /dev/null +++ b/wqflask/wqflask/templates/test_wgcna_results.html @@ -0,0 +1,165 @@ +{% extends "base.html" %} +{% block title %}WCGNA results{% endblock %} +{% block content %} +<!-- Start of body --> + +<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/xterm/3.14.5/xterm.min.css" integrity="sha512-iLYuqv+v/P4u9erpk+KM83Ioe/l7SEmr7wB6g+Kg1qmEit8EShDKnKtLHlv2QXUp7GGJhmqDI+1PhJYLTsfb8w==" crossorigin="anonymous" referrerpolicy="no-referrer" /> + +<link rel="stylesheet" href="https://cdn.datatables.net/1.11.3/css/jquery.dataTables.min.css"> + + +<style type="text/css"> + + +.container { + min-height: 100vh; + width: 100vw; + padding: 20px; + +} + +.grid_container { + + + width: 80vw; + margin: auto; + padding: 20px; + + + display: grid; + grid-template-columns: repeat(7, 1fr); + /*grid-gap: 5px;*/ + border: 1px solid black; + grid-column-gap: 20px; + +} + +.control_sft_column { + text-align: center; +} + +.grid_container div:not(:last-child) { + border-right: 1px solid #000; +} + +.grid_container .control_sft_column h3 { + font-weight: bold; + font-size: 18px; +} + +.control_net_colors { + + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + text-align: center; +} + + +.control_mod_eigens { + display: grid; + grid-template-columns: repeat(2, 200px); +} + +.control-image{ + display: block; + margin-left: auto; + margin-right: auto; + width: 80vw; +} +</style> +<div class="container"> + {% if error!='null' %} + <h4 style="text-align: center;">{{error}}</h4> + + {% else %} + <div> + <div > + <h2 style="text-align:center">Soft Thresholds </h2> + <div class="grid_container"> + + {% for key, value in results["data"]["output"]["soft_threshold"].items()%} + <div class="control_sft_column"> + <h3>{{key}}</h3> + {% for val in value %} + <p>{{val|round(3)}}</p> + {% endfor %} + </div> + {% endfor %} + </div> + </div> + + <div> + + {% if image["image_generated"] %} + <div > + <img class="control-image" src="data:image/jpeg;base64,{{ image['image_data']| safe }}"> + </div> + + {% endif %} +<!-- <div > + <img class="control-image" src="data:image/jpeg;base64,{{ results['data']['output']['image_data2']| safe }}"> + </div> --> + </div> + + <div> + <h2 style="text-align:center;"> Module eigen genes </h2> + <table id="eigens" class="display" width="80vw"></table> + </div> + + <div> + <h2 style="text-align:center;">Phenotype modules </h2> + + <table id="phenos" class="display" width="40vw" ></table> + </div> + </div> + +{% endif %} + +</div> + +{% endblock %} + +{% block js %} + +<script src="https://cdnjs.cloudflare.com/ajax/libs/xterm/3.14.5/xterm.min.js"></script> + +<script language="javascript" type="text/javascript" src="{{ url_for('js', filename='DataTables/js/jquery.js') }}"></script> +<script language="javascript" type="text/javascript" src="{{ url_for('js', filename='DataTables/js/jquery.dataTables.min.js') }}"></script> +<script language="javascript" type="text/javascript" src="{{ url_for('js', filename='DataTablesExtensions/scroller/js/dataTables.scroller.min.js') }}"></script> + + +<script type="text/javascript"> + + +let results = {{results|safe}} + +let phenoModules = results["data"]["output"]["net_colors"] +let phenotypes = Object.keys(phenoModules) +let phenoMods = Object.values(phenoModules) + +let {col_names,mod_dataset} = {{data|safe}} + $('#eigens').DataTable( { + data: mod_dataset, + columns: col_names.map((name)=>{ + return { + title:name + } + }) + } ); + $('#phenos').DataTable( { + data:phenotypes.map((phenoName,idx)=>{ + return [phenoName,phenoMods[idx]] + }), + columns: [{ + title:"Phenotypes" + }, + { + title: "Modules" + }] + } ); + + +</script> +{% endblock %}
\ No newline at end of file diff --git a/wqflask/wqflask/templates/tool_buttons.html b/wqflask/wqflask/templates/tool_buttons.html index 3f9d8211..3ee5be19 100644 --- a/wqflask/wqflask/templates/tool_buttons.html +++ b/wqflask/wqflask/templates/tool_buttons.html @@ -18,13 +18,13 @@ BNW </button> -<!-- <button id="wgcna_setup" class="btn btn-primary submit_special" data-url="/wgcna_setup" title="WGCNA Analysis" > +<button id="wgcna_setup" class="btn btn-primary submit_special" data-url="/wgcna_setup" title="WGCNA Analysis" > WGCNA </button> <button id="ctl_setup" class="btn btn-primary submit_special" data-url="/ctl_setup" title="CTL Analysis" > CTL Maps -</button> --> +</button> <button id="heatmap" class="btn btn-primary submit_special" data-url="/heatmap" title="Heatmap" > MultiMap diff --git a/wqflask/wqflask/templates/tutorials.html b/wqflask/wqflask/templates/tutorials.html index 89143809..aa6a818d 100644 --- a/wqflask/wqflask/templates/tutorials.html +++ b/wqflask/wqflask/templates/tutorials.html @@ -3,7 +3,7 @@ {% block content %} <head> - <title>OPAR - OSGA webinar series</title> + <title>GeneNetwork Webinar Series, Tutorials and Short Video Tours</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- <link rel="stylesheet" href="uikit-3/css/uikit.min.css" />--> @@ -17,21 +17,37 @@ <script src="https://cdn.jsdelivr.net/npm/uikit@3.5.4/dist/js/uikit-icons.min.js"></script> <!-- DataTables--> - <!--<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/dt/jq-3.3.1/dt-1.10.21/cr-1.5.2/datatables.min.css"/>--> + <!-- <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/dt/jq-3.3.1/dt-1.10.21/cr-1.5.2/datatables.min.css"/> --> <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.10.22/css/jquery.dataTables.min.css"/> - <!-- <script type="text/javascript" src="https://cdn.datatables.net/v/dt/jq-3.3.1/dt-1.10.21/cr-1.5.2/datatables.min.js"></script>--> + <script type="text/javascript" src="https://cdn.datatables.net/v/dt/jq-3.3.1/dt-1.10.21/cr-1.5.2/datatables.min.js"></script> <script type="text/javascript" src="https://code.jquery.com/jquery-3.5.1.js"></script> <script type="text/javascript" src="https://cdn.datatables.net/1.10.22/js/jquery.dataTables.min.js"></script> </head> <body> - <div class="uk-margin-small uk-card uk-card-default uk-card-body"> -<h2 class="uk-heading-small">Webinar Series - Quantitative Genetics Tools for Mapping Trait Variation to Mechanisms, Therapeutics, and Interventions</h2> -<p>The NIDA Center of Excellence in Omics, Systems Genetics, and the Addictome has put together a webinar series, Quantitative Genetics Tools for Mapping Trait Variation to Mechanisms, Therapeutics, and Interventions. The goal of this series is to transverse the path from trait variance to QTL to gene variant to molecular networks to mechanisms to therapeutic and interventions. The target audience for this series are those new to the field of quantitative genetics, so please pass this information on to your trainees or colleagues.</p> + <!-- <div class="row">--> + <!-- <div class="col-lg-12"> + <p>Primary Sponsor</p> + <img class="img-responsive" src="images/illumina_logo.jpg" alt=""> <h2 class="page-header">Program (Preliminary)</h2> + </div>--> + <div class="col-lg-12"> - <table id="myTable" class="display"> + <ul id="myTab" class="nav nav-tabs nav-justified"> + <li class="active"><a href="#service-one" data-toggle="tab"><i class="fa fa-file-text-o"></i>Webinars</a> + </li> + <li class=""><a href="#service-two" data-toggle="tab"><i class="fa fa-file-text-o"></i>Short Video Tours</a> + </li> + <li class=""><a href="#service-three" data-toggle="tab"><i class="fa fa-file-text-o"></i>Tutorials</a> + </li> + <li class=""><a href="#service-four" data-toggle="tab"><i class="fa fa-file-text-o"></i>Documentation</a> + </li> + </ul> +<p></p> + <div id="myTabContent" class="tab-content"> + <div class="tab-pane fade active in" id="service-one"> + <table id="myTable" class="display"> <thead> <tr> <th>Title/Description</th> @@ -40,9 +56,7 @@ </thead> <tbody> <tr> - <td><p><h3>Webinar #01 - Introduction to Quantitative Trait Loci (QTL) Analysis</h3> - <p><i>Friday, May 8th, 2020<br> - 10am PDT/ 11am MDT/ 12pm CDT/ 1pm EDT</i></p> + <td><p><h3>Introduction to Quantitative Trait Loci (QTL) Analysis</h3> <p>Goals of this webinar (trait variance to QTL):</p> <ul> <li>Define quantitative trait locus (QTL)</li> @@ -60,10 +74,7 @@ University of Tennessee Health Science Center <iframe width="560" height="315" src="https://www.youtube.com/embed/leY3kPmnLaI" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td> </tr> <tr> - <td><p><h3>Webinar #02 - Mapping Addiction and Behavioral Traits and Getting at Causal Gene Variants with GeneNetwork</h3> - <p><i>Friday, May 22nd. 2020 - 10am PDT/ 11am MDT/ 12pm CDT/ 1pm EDT</i> -</p> + <td><p><h3>Mapping Addiction and Behavioral Traits and Getting at Causal Gene Variants with GeneNetwork</h3> <p>Goals of this webinar (QTL to gene variant):</p> <ul> <li>Demonstrate mapping a quantitative trait using GeneNetwork (GN)</li> @@ -78,438 +89,10 @@ University of Tennessee Health Science Center </p></td> <td><iframe width="560" height="315" src="https://www.youtube.com/embed/LwpXzLHX9aM" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td> </tr> - <tr> - <td><p><h3>Webinar #03 - Introduction to expression (e)QTL and their role in connecting QTL to genes and molecular networks</h3> - <p><i>Friday, June 12, 2020 10am PDT/ 11am MDT/ 12pm CDT/ 1pm EDT</i> - <!--<p>1 hour presentation followed by 30 minutes of discussion</p>--> - <p>Goals of this webinar (QTL to gene/molecular networks):</p> - <ul> - <li>Define eQTL</li> - <li>Examine the role of eQTL in the relationship of genes and molecular networks with phenotypic QTL</li> - <li>eQTL for co-expression networks</li> - </ul> - <p>Presented by:<br> -Dr. Laura Saba<br> -Associate Professor<br> -Department of Pharmaceutical Sciences<br> -University of Colorado Anschutz Medical Campus -</p><p> -<a href="/pdf/webinar_flyer_2020-06-12.pdf" target="_blank">Webinar flyer (pdf)</a><br> -<a href="https://github.com/OSGA-OPAR/quant-genetics-webinars/blob/master/2020-06-12/README.md">Link to course material</a><br> -</td> - <td><iframe width="560" height="315" src="https://www.youtube.com/embed/8jiNHuOgr1A" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td> - </tr> - <tr> - <td><p><h3>Webinar #04 - From Candidate Genes to Causal Variants—Strategies for and Examples of Identifying Genes and Sequence Variants in Rodent Populations</h3> - <p><i>Friday, June 26, 2020 10am PDT/ 11am MDT/ 12pm CDT/ 1pm EDT</i> - - <p>Goals of this webinar (candidate genes to causal variants):</p> - <ul> - <li>To understand when it is practical or (just as often) not practical to try to "clone" the gene or nucleotide variant modulating trait variants -</li> - <li>To understand that defining the crucial causal nucleotide variant is usually a bonus and often not for the translational or even mechanistic utility of discoveries. -</li> - <li>To review new sequence-based methods to identify common and rare variants—the reduced complexity cross and epoch-effects in reference populations -</li> - </ul> - <p>Presented by:<br> -Dr. Rob Williams<br> -Professor and Chair<br> -Department of Genetics, Genomics, and Informatics<br> -University of Tennessee Health Science Center -</p> - -<p> -<a href="https://github.com/OSGA-OPAR/quant-genetics-webinars/blob/master/2020-06-26/README.md">Link to course material</a><br> -Link to course material in powerpoint pptx: [<a href="pdf/P30_Webinar_on_QTGenes_26Jun2020v3.pptx" target="_blank">P30_Webinar_on_QTGenes_26Jun2020v3.pptx</a>]<br> -</p></td> - <td><iframe width="560" height="315" src="https://www.youtube.com/embed/u9d5dYxM5q4" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td> - </tr> - <tr> - <td><p><h3>Webinar #05 - Identifying genes from QTL using RNA expression and the PhenoGen website (<a href="http://phenogen.org" target="_blank">http://phenogen.org</a>)</h3> - <p><i>Friday, August 28, 2020 10am PDT/ 11am MDT/ 12pm CDT/ 1pm EDT<br> -1-hour presentation followed by 30 minutes of discussion</i> - - <p>Goals of this webinar (candidate genes to causal variants): - <p>Demonstrate how to use the PhenoGen website to identify transcripts:</p> - <ul> - <li>Physically located within a QTL</li> - <li>Physically located within a QTL and expressed in brain</li> - <li>With a brain cis eQTL within the QTL</li> - <li>With any brain eQTL within the QTL</li> - <li>Within a co-expression network controlled from the same region as the QTL</li> - </ul> - <p>Presented by:<br> -Dr. Laura Saba<br> -Associate Professor<br> -Department of Pharmaceutical Sciences<br> -University of Colorado Anschutz Medical Campus -</p> -<p> -<a href="https://github.com/OSGA-OPAR/quant-genetics-webinars/blob/master/2020-08-28/README.md">Link to course material</a><br> -</p></td> - <td><iframe width="560" height="315" src="https://www.youtube.com/embed/9DJm5cJgVis" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td> - </tr> - <tr> - <td><p><h3>Webinar #06 - Sex as a Biological Covariate in QTL Studies</h3> - <p><i>Friday, September 11th, 2020 10am PDT/ 11am MDT/ 12pm CDT/ 1pm EDT <br> -1-hour presentation followed by 30 minutes of discussion</i> - - <p>Goals of this webinar (trait variance to QTL): - - <ul> - <li>Review QTL mapping </li> - <li>Understand the role of sex in QTL study design </li> - <li>Use sex as a covariate in QTL analysis </li> - <li>Understand X chromosome segregation in crosses</li> - <li>Make adjustments for X chromosome in QTL analysis </li> - </ul> - <p>Presented by:<br> -Dr. Saunak Sen<br> -Professor and Chief of Biostatistics<br> -Department of Preventative Medicine<br> -University of Tennessee Health Science Center -<p><a href="https://github.com/OSGA-OPAR/quant-genetics-webinars/blob/master/2020-09-11/README.md">Link to course material</a> -</p></td> - <td><iframe width="560" height="315" src="https://www.youtube.com/embed/dYeJcBbJjRU" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td> - </tr> - <tr> - <td><h3>Webinar #07 - Introduction to Weighted Gene Co-expression Network Analysis</h3> - <p><i>Friday, September 25th at 10am PDT/ 11am MDT/ 12pm CDT/ 1pm EDT <br> -1-hour presentation followed by 30 minutes of discussion </i> - - <p>Goals of this webinar (molecular networks): - <ul> - <li>Introduction and motivation for co-expression network analysis </li> - <li>Basics of weighted gene co-expression network analysis </li> - <li>Step-by-step guide to WGCNA using the wgcna package in R. </li> - </ul> - <p>Background reading available at: <a href="http://bit.ly/osga_wgcna">http://bit.ly/osga_wgcna</a></p> - <p>Presented by:<br> -Dr. Laura Saba<br> -Associate Professor<br> -Department of Pharmaceutical Sciences<br> -University of Colorado Anschutz Medical Campus -<p><a href="https://github.com/OSGA-OPAR/quant-genetics-webinars/blob/master/2020-09-11/README.md">Link to course material</a> -</p></td> - <td><iframe width="560" height="315" src="https://www.youtube.com/embed/OpWEHazyQLA" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td> - </tr> - <tr> - <td><p><h3>Webinar #08 - Using genetic and non-genetic covariates in QTL studies</h3> - <p><i>Friday, October 9th at 10am PDT/ 11am MDT/ 12pm CDT/ 1pm EDT <br> -1-hour presentation followed by 30 minutes of discussion </i> - - <p>Goals of this webinar (quantitative trait to genetic loci): - <ul> - <li>Identify covariates and mediators in QTL studies </li> - <li>Adjust for covariates in QTL scans </li> - <li>Review genetic relatedness in segregating populations </li> - <li>Adjust for genetic relatedness using linear mixed models </li> - </ul> - <!--<p>Background reading available at: <a href="http://bit.ly/osga_wgcna">http://bit.ly/osga_wgcna</a></p>--> - <p>Presented by:<br> -Dr. Saunak Sen<br> -Professor and Chief of Biostatistics<br> -Department of Preventative Medicine<br> -University of Tennessee Health Science Center -</p><p><a href="https://github.com/OSGA-OPAR/quant-genetics-webinars/blob/master/2020-10-09/README.md">Link to course material</a></td> - <td><iframe width="560" height="315" src="https://www.youtube.com/embed/1U_4DCSDq9U" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td> - </tr> - <tr> - <td><p><h3>Webinar #09 - Introduction to GeneWeaver: Integrating and analyzing heterogeneous functional genomics data</h3> - <p><i>Friday, October 23th at 10am PDT/ 11am MDT/ 12pm CDT/ 1pm EDT <br> -1-hour presentation followed by 30 minutes of discussion </i> - <p>Goals of this webinar: - <ul> - <li>Compare a user's gene list with multiple functional genomics data sets </li> - <li>Compare and contrast gene lists with data currently available and integrated in GeneWeaver</li> - <li>Explore functional relationships among genes and disease across species </li> - </ul> - <!--<p>Background reading available at: <a href="http://bit.ly/osga_wgcna">http://bit.ly/osga_wgcna</a></p>--> - <p>Presented by:<br> -Dr. Elissa Chesler<br> -Professor The Jackson Laboratory -</p> -<p> -Dr. Erich Baker<br> -Professor and Chair<br> -Department of Computer Science<br> -Baylor University -<p><a href="https://github.com/OSGA-OPAR/quant-genetics-webinars/blob/master/2020-10-23/README.md">Link to course material</a></p> -<!--After the presentation, the recording will be made available for download at <a href="http://opar.io">http://opar.io</a><br>--></p> - - -<p></td> - <!--<td><strong>After the presentation, the recording will be made available here.</strong></td>--> - <td><iframe width="560" height="315" src="https://www.youtube.com/embed/Vq3vocdMWLQ" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td> - </tr> - - - - -<tr> - <td><p><h3>Webinar #10 - Sketching alternate realities: An introduction to causal inference in genetic studies</h3> - <p><i>Friday, November 20th at 10am PDT/ 11am MDT/ 12pm CDT/ 1pm EDT<br> - 1-hour presentation followed by 30 minutes of discussion</i> - - <p>Goals of this webinar: - <p>Determination of cause is an important goal of biological studies, and genetic studies provide unique opportunities. In this introductory lecture we will frame causal inference as a missing data problem to clarify challenges, assumptions, and strategies necessary for assigning cause. We will survey the use of directed acyclic graphs (DAGs) to express causal information and to guide analytic strategies. - <ul> - <li>Express causal inference as a missing data problem (counterfactual framework) </li> - <li>Outline assumptions needed for causal inference </li> - <li>Express causal information as (directed acyclic) graphs </li> - <li>Outline how to use graphs to guide analytic strategy </li> - </ul> - - <p>Presented by:<br> -Dr. Saunak Sen<br> -Professor and Chief of Biostatistics<br> -Department of Preventative Medicine<br> -University of Tennessee Health Science Center -<p><a href="https://github.com/OSGA-OPAR/quant-genetics-webinars/blob/master/2020-11-20/README.md">Link to course material</a> -</p> -<!--<p>There is no fee associated with this webinar, but users are asked to register to receive the Zoom link and password. -Registration: <a href="https://bit.ly/osga_2020-11-20">https://bit.ly/osga_2020-11-20</a>--> -</td> - <!--<td><strong>After the presentation, the recording will be made available here.</strong></td>--> - <td><iframe width="560" height="315" src="https://www.youtube.com/embed/twJNYOL3qfA" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> - </tr> - <!--NEW WEBINAR STARTS HERE--> <tr> - <td><p><h3>Webinar #11 - Beginner's guide to bulk RNA-Seq analysis</h3> - <p><i>Friday, February 12th, 2021 at 10am PDT/ 11am MDT/ 12pm CDT/ 1pm EDT <br> - 1-hour presentation followed by 30 minutes of discussion</i> - - <p>Goals of this webinar: - <p>The use of high throughput short read RNA sequencing has become common place in many scientific laboratories. The analysis tools for quantitating a transcriptome have matured becoming relatively simple to use. The goals of this webinar are: - <ul> - <li>To give a general overview of the popular Illumina technology for sequencing RNA. </li> - <li>To outline several of the key aspects to consider when designing an RNA-Seq study </li> - <li>To provide guidance on methods and tools for transforming reads to quantitative expression measurements. </li> - <li>To describe statistical models that are typically used for differential expression and why these specialized models are needed.</li> - </ul> - - <p>Presented by:<br> -Dr. Laura Saba<br> -Associate Professor<br> -Department of Pharmaceutical Sciences<br> -University of Colorado Anschutz Medical Campus -<p><a href="https://github.com/OSGA-OPAR/quant-genetics-webinars/blob/master/2021-02-12/README.md">Link to course material</a> -</p> -<p><a href="/pdf/webinar_flyer_2021-02-12.pdf" target="_blank">Webinar flyer (pdf)</a></p> -<!--<p>There is no fee associated with this webinar, but users are asked to register to receive the Zoom link and password. -Registration: <a href="http://bit.ly/osga_2021-02-12">http://bit.ly/osga_2021-02-12</a>--> -<p>This webinar series is sponsored by the NIDA Center of Excellence in Omics, Systems Genetics, and the Addictome (P30 DA044223) and the NIAAA-funded PhenoGen Website (R24 AA013162).</p> -</td> - <!--<td><strong>After the presentation, the recording will be made available here.</strong></td>--> - <td><iframe width="560" height="315" src="https://www.youtube.com/embed/WW94W-DBf2U" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td> - </tr> - <!--WEBINAR ENDS HERE--> - - <!--NEW WEBINAR STARTS HERE--> - <tr> - <td><p><h3>Webinar #12 - From GWAS to gene: What are the essential analyses and how do we bring them together using heterogeneous stock rats?</h3> - <p><i>Friday, February 26th at 10am PST/ 11am MST/ 12pm CST/ 1pm EST<br> - 1-hour presentation followed by 30 minutes of discussion</i> - - <p>Goals of this webinar: - <p>Heterogeneous stock (HS) rats are an outbred population that was created in 1984 by intercrossing 8 inbred strains. The Center for GWAS in Outbred Rats (www.ratgenes.org) has developed a suite of analysis tools for analyzing genome wide association studies (GWAS) in HS rats - <ul> - <li>Explain the HS rat population and their history</li> - <li>Describe the automated pipeline that performs GWAS in HS rats</li> - <li>Explore the fine mapping of associated regions and explain the various secondary analyses that we use to prioritize genes within associated intervals</li> - </ul> - - <p>Presented by:<br> -Abraham A. Palmer, Ph.D.<br> -Professor & Vice Chair for Basic Research<br> -Department of Psychiatry<br> -University of California San Diego -</p> -<p><a href="/pdf/webinar_flyer_2021-02-26.pdf" target="_blank">Webinar flyer (pdf)</a></p> -<p><a href="https://github.com/OSGA-OPAR/quant-genetics-webinars/blob/master/2021-02-26/README.md">Link to course material</a> -<p>Link to course material in pptx:<a href="/pdf/Palmer_talk_2-26-21.pptx" target="_blank"> Palmer_talk_2-26-21.pptx</a></p> -<!--<p>There is no fee associated with this webinar, but users are asked to register to receive the Zoom link and password. -Registration: <a href="http://bit.ly/osga_2021-02-26 ">http://bit.ly/osga_2021-02-26</a>--> -<p>This webinar series is sponsored by the NIDA Center of Excellence in Omics, Systems Genetics, and the Addictome (P30 DA044223) and the NIDA-funded Center for GWAS in Outbred Rats (P50 DA037844).</p> -</td> - <!--<td><strong>After the presentation, the recording will be made available here.</strong></td>--> - <td><iframe width="560" height="315" src="https://www.youtube.com/embed/aWUxNZ9wS3E" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> - </tr> - <!--WEBINAR ENDS HERE--> - - <!--NEW WEBINAR STARTS HERE--> - <tr> - <td><p><h3>Webinar #13 - Become a UseR: A brief tour of R</h3> - <p><i>Friday, March 12th at 10am PST/ 11am MST/ 12pm CST/ 1pm EST<br> - 1-hour presentation followed by 30 minutes of discussion</i> - <p>We will introduce R programming language and outline the benefits of learning R. We will give a brief tour of basic concepts and tasks: variables, objects, functions, basic statistics, visualization, and data import/export. We will showcase a practical example demonstrating statistical analysis. - - <p>Goals of this webinar: - <ul> - <li>Why should one use/learn R?</li> - <li>How to install R/Rstudio</li> - <li>Learn about R basics: variables, programming, functions</li> - <li>Learn about the R package ecosystem that extends its capabilities</li> - <li>See a basic statistical analysis example</li> - <li>Learn about additional resources</li> - </ul> - - <p>Presented by:<br> -Gregory Farage, PhD and Saunak Sen, PhD<br> -Department of Preventive Medicine<br> -University of Tennessee Health Science Center -<p><a href="https://github.com/OSGA-OPAR/quant-genetics-webinars/blob/master/2021-03-12/README.md">Link to course material</a> -</p> -<p><a href="/pdf/webinar_flyer_2021-03-12.pdf" target="_blank">Webinar flyer (pdf)</a></p> -<!--<p>Link to course material in pptx:<a href="/pdf/Palmer_talk_2-26-21.pptx" target="_blank"> Palmer_talk_2-26-21.pptx</a></p>--> -<!--<p>There is no fee associated with this webinar, but users are asked to register to receive the Zoom link and password. -Registration: <a href="http://bit.ly/osga_2021-03-12">http://bit.ly/osga_2021-03-12</a></p>--> -</td> - <!--<td><strong>After the presentation, the recording will be made available here.</strong></td>--> - <td><iframe width="560" height="315" src="https://www.youtube.com/embed/25-X8_oXBSY" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> - </tr> - <!--WEBINAR ENDS HERE--> - - <!--NEW WEBINAR STARTS HERE--> - <tr> - <td><p><h3>Webinar #14 - Landing on Jupyter: A guided tour of interactive notebooks</h3> - <p><i>Friday, March 26th at 10am PDT/ 11am MDT/ 12pm CDT/ 1pm EDT<br> - 1-hour presentation followed by 30 minutes of discussion</i> - <p>Jupyter is an interactive interface to data science and scientific computing across a variety of programming languages. We will present the Jupyter notebook, and explain some key concepts (e.g., kernel, cells). We will show how to create a new notebook; modify an existing notebook; save, export, and publish a notebook. We will discuss several possible use cases: developing code, writing reports, taking notes, and teaching/presenting. - - <p>Goals of this webinar: - <ul> - <li>Learn what Jupyter notebooks are</li> - <li>Learn how to install, configure, and use Jupyter notebooks</li> - <li>Learn how to use Jupyter notebooks for research, teaching, or code - development </li> - </ul> - - <p>Presented by:<br> -Gregory Farage, PhD and Saunak Sen, PhD<br> -Department of Preventive Medicine<br> -University of Tennessee Health Science Center -<p><a href="https://github.com/OSGA-OPAR/quant-genetics-webinars/blob/master/2021-03-26/README.md">Link to course material</a> -</p> -<p><a href="/pdf/webinar_flyer_2021-03-26.pdf" target="_blank">Webinar flyer (pdf)</a></p> -<!--<p>Link to course material in pptx:<a href="/pdf/Palmer_talk_2-26-21.pptx" target="_blank"> Palmer_talk_2-26-21.pptx</a></p>--> -<!--<p>There is no fee associated with this webinar, but users are asked to register to receive the Zoom link and password. -Registration: <a href="http://bit.ly/osga_2021-03-26">http://bit.ly/osga_2021-03-26</a>--> -</td> - <!--<td><strong>After the presentation, the recording will be made available here.</strong></td>--> - <td><iframe width="560" height="315" src="https://www.youtube.com/embed/GVzUNEmpanI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></iframe> - </tr> - <!--WEBINAR ENDS HERE--> - - <!--NEW WEBINAR STARTS HERE--> - <tr> - <td><p><h3>Webinar #15 – Introduction to Metabolomics Platforms and Data Analysis</h3> - <p><i>Friday, April 9th at 10am PDT/ 11am MDT/ 12pm CDT/ 1pm EDT <br> - 1-hour presentation followed by 30 minutes of discussion</i> - <p>Goals of this webinar: - <p>The use of metabolomics to profile small molecules is now widespread in biomedical research. The goals of this webinar are: - <ul> - <li>To describe research questions that can be addressed using metabolomics</li> - <li>To give a general overview of metabolomics technologies</li> - <li>To outline steps in a metabolomics data analysis pipeline</li> - <li>To provide information on common resources and databases</li> - </ul> - - <p>Presented by:<br> -Katerina Kechris, PhD <br> -Professor<br> -Department of Biostatistics and Informatics <br> -Colorado School of Public Health <br> -University of Colorado Anschutz Medical Campus -<p><a href="https://github.com/OSGA-OPAR/quant-genetics-webinars/blob/master/2021-04-09/README.md">Link to course material</a> -</p> -<p><a href="/pdf/webinar_flyer_2021-04-09.pdf" target="_blank">Webinar flyer (pdf)</a></p> -<!--<p>Link to course material in pptx:<a href="/pdf/Palmer_talk_2-26-21.pptx" target="_blank"> Palmer_talk_2-26-21.pptx</a></p> -<p>There is no fee associated with this webinar, but users are asked to register to receive the Zoom link and password. -Registration: <a href="https://bit.ly/osga_2021-04-09">https://bit.ly/osga_2021-04-09</a></p>--> -</td> - <!--<td><strong>After the presentation, the recording will be made available here.</strong></td>--> - <td><iframe width="560" height="315" src="https://www.youtube.com/embed/oB1Khk6mt_8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td> - </tr> - <!--WEBINAR ENDS HERE--> - - <!--NEW WEBINAR STARTS HERE--> - <tr> - <td><p><h3>Webinar #16 – Introduction to the Hybrid Rat Diversity Panel: A renewable rat panel for genetic studies of addiction-related traits</h3> - <p><i>Friday, April 23rd at 10am PDT/ 11am MDT/ 12pm CDT/ 1pm EDT <br> - 1-hour presentation followed by 30 minutes of discussion</i> - <p>Goals of this webinar: - <p>The Hybrid Rat Diversity Panel (HRDP) is an inbred panel of rats that included two recombinant inbred panels and a panel of classic inbred strains. - <ul> - <li>To describe hybrid diversity panels, in particular the HRDP, including advantages and disadvantages when studying the role of genetics is substance use disorders, e.g., renewable genomes and the accumulation of behavioral and physiological phenotypes and high throughput omics data. </li> - <li>To outline current resources and resources that are being generated. </li> - <li>To demonstrate the utility of a renewable genetically diverse rodent population when exploring the interaction between genetics, drug exposure, and behavior. </li> - </ul> - - <p>Presented by:<br> - Hao Chen, PhD<br> -Associate Professor<br> -Department of Pharmacology, Addiction Science, and Toxicology <br> -University of Tennessee Health Science Center -<p> -Dr. Laura Saba<br> -Associate Professor<br> -Department of Pharmaceutical Sciences<br> -University of Colorado Anschutz Medical Campus -<p><a href="https://github.com/OSGA-OPAR/quant-genetics-webinars/blob/master/2021-04-23/README.md">Link to course material</a> -</p> -<p><a href="/pdf/webinar_flyer_2021-04-09.pdf" target="_blank">Webinar flyer (pdf)</a></p> -<!--<p>Link to course material in pptx:<a href="/pdf/Palmer_talk_2-26-21.pptx" target="_blank"> Palmer_talk_2-26-21.pptx</a></p> -<p>There is no fee associated with this webinar, but users are asked to register to receive the Zoom link and password. -Registration: <a href="http://bit.ly/osga_2021-04-23">http://bit.ly/osga_2021-04-23</a> -<p>This webinar series is sponsored by the NIDA Center of Excellence in Omics, Systems Genetics, and the Addictome (P30 DA044223) and RGAP: The heritable transcriptome and alcoholism (R24 AA013162). </p> -</td> - <td><strong>After the presentation, the recording will be made available here.</strong></td>--> - <td><iframe width="560" height="315" src="https://www.youtube.com/embed/dYIiv01IetQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td> - </tr> - <!--WEBINAR ENDS HERE--> - - <!--NEW WEBINAR STARTS HERE--> - <tr> - <td><p><h3>Webinar #17 – Identifying sample mix-ups in eQTL data</h3> - <p><i>Friday, June 11th at 10am PDT/ 11am MDT/ 12pm CDT/ 1pm EDT <br> - 1-hour presentation followed by 30 minutes of discussion</i> - <p>Goals of this webinar: - <p>Sample mix-ups interfere with our ability to detect genotype-phenotype associations. However, the presence of numerous eQTL with strong effects provides the opportunity to not just identify sample mix-ups, but also to correct them. - <ul> - <li>To illustrate methods for identifying sample duplicates and errors in sex annotations. </li> - <li>To illustrate methods for identifying sample mix-ups in DNA and RNA samples from experimental cross data.</li> - </ul> - - <p>Presented by:<br> - Karl Broman, PhD <br> -Professor<br> -Department of Biostatistics & Medical Informatics <br> -University of Wisconsin-Madison -<p> - -<p><a href="/pdf/webinar_flyer_2021-06-11.pdf" target="_blank">Webinar flyer (pdf)</a></p> -<p>Link to course material:<a href="kbroman.org/Talk_OSGA2021" target="_blank">kbroman.org/Talk_OSGA2021</a></p> -<!--<p>There is no fee associated with this webinar, but users are asked to register to receive the Zoom link and password. -Registration: <a href="http://bit.ly/osga_2021-06-11">http://bit.ly/osga_2021-06-11</a>--> - <!--<p>This webinar series is sponsored by the NIDA Center of Excellence in Omics, Systems Genetics, and the Addictome (P30 DA044223) and RGAP: The heritable transcriptome and alcoholism (R24 AA013162). </p>--> -</td> - <!-- <td><strong>After the presentation, the recording will be made available here.</strong></td>--> - <td><iframe width="560" height="315" src="https://www.youtube.com/embed/h5gF7YnffeI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td> - </tr> - <!--WEBINAR ENDS HERE--> - - - - - <!--NEW WEBINAR STARTS HERE--> - <tr> - <td><p><h3>Bonus 1 - Data structure, disease risk, GXE, and causal modeling</h3> - <p><i>Friday, November 20th at 9am PDT/ 11pm CDT/ 12pm EDT<br> - 1-hour presentation followed by 30 minutes of discussion</i> + <td><p><h3>Data structure, disease risk, GXE, and causal modeling</h3> <p>Human disease is mainly due to complex interactions between genetic and environmental factors (GXE). We need to acquire the right "smart" data types—coherent and multiplicative data—required to make accurate predictions about risk and outcome for n = 1 individuals—a daunting task. We have developed large families of fully sequenced mice that mirror the genetic complexity of humans. We are using these Reference Populations to generate multiplicatively useful data and to build and test causal quantitative models of disease mechanisms with a special focus on diseases of aging, addiction, and neurological and psychiatric disease. @@ -530,9 +113,28 @@ Registration: <a href="https://bit.ly/osga_2020-11-20">https://bit.ly/osga_2020- </tr> <!--WEBINAR ENDS HERE--> + </tbody> + </table> + +<script> +$('#myTable').dataTable( { + "lengthMenu": [ 50, 75, 100 ] +} ); +</script> + </div> + <div class="tab-pane fade" id="service-two"> + <table id="myTable2" class="display"> + <thead> + <tr> + <th>Title/Description</th> + <th>Presentation</th> + </tr> + </thead> + <tbody> + <!--NEW WEBINAR STARTS HERE--> <tr> - <td><p><h3>Bonus 2 - Introduction to Gene Network</h3> + <td><p><h3>Introduction to Gene Network</h3> <p><i>Please note that this tutorial is based on GeneNetwork v1</i> <p>GeneNetwork is a group of linked data sets and tools used to study complex networks of genes, molecules, and higher order gene function and phenotypes. GeneNetwork combines more than 25 years of legacy data generated by hundreds of scientists together with sequence data (SNPs) and massive transcriptome data sets (expression genetic or eQTL data sets). The quantitative trait locus (QTL) mapping module that is built into GN is optimized for fast on-line analysis of traits that are controlled by combinations of gene variants and environmental factors. GeneNetwork can be used to study humans, mice (BXD, AXB, LXS, etc.), rats (HXB), Drosophila, and plant species (barley and Arabidopsis). Most of these population data sets are linked with dense genetic maps (genotypes) that can be used to locate the genetic modifiers that cause differences in expression and phenotypes, including disease susceptibility. @@ -550,39 +152,102 @@ Registration: <a href="https://bit.ly/osga_2020-11-20">https://bit.ly/osga_2020- <!--<p>This webinar series is sponsored by the NIDA Center of Excellence in Omics, Systems Genetics, and the Addictome (P30 DA044223). --> </td> <!--<td><strong>After the presentation, the recording will be made available here.</strong></td>--> - <td><iframe width="560" height="315" src="https://www.youtube.com/embed/B3g_0q-ldJ8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></iframe> + <td> + <iframe width="560" height="315" src="https://www.youtube.com/embed/B3g_0q-ldJ8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> </tr> <!--WEBINAR ENDS HERE--> + + <!--NEW WEBINAR STARTS HERE--> + <tr> + <td><p><h3>How to search in GeneNetwork</h3><br>Presented by Rob Williams University of Tennessee Health Science Center</td> + <!--<td><p><h3>How to search in GeneNetwork</h3> +</td>--> + <td> + <iframe width="560" height="315" src="https://www.youtube.com/embed/5exnkka5Tso" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> </td> + </tr> + <!--WEBINAR ENDS HERE--> + + <!--NEW WEBINAR STARTS HERE--> + <tr> + <td><p><h3>GeneNetwork.org: genetic analysis for all neuroscientists</h3><br>Presented by David G. Ashbrook Assistant Professor University of Tennessee Health Science Center +</td> + <td> + <iframe width="560" height="315" src="https://www.youtube.com/embed/JmlVLki09Q8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td> + </tr> + <!--WEBINAR ENDS HERE--> </tbody> </table> -</div> <script> -$('#myTable').dataTable( { +$('#myTable2').dataTable( { "lengthMenu": [ 50, 75, 100 ] } ); </script> + </div> + <div class="tab-pane fade" id="service-three"> + <table id="myTable3" class="display"> + <thead> + <tr> + <th>Title</th> + <th>Speaker</th> + <th>Video link</th> + </tr> + </thead> + <tbody> + <tr><td>Diallel Crosses, Artificial Intelligence, and Mouse Models of Alzheimer’s Disease</td> + <td>David G. Ashbrook<br>Assistant Professor<br>University of Tennessee Health Science Center</td> + <td><a href="https://www.youtube.com/watch?v=HKfYc8CJwqM">YouTube link</a></td> + </tr> -<!--<script> -$(document).ready( function () { -$('#myTable').DataTable({ - "lengthMenu": [ [50, 75, 100] + + </tbody> + </table> + <script> +$('#myTable3').dataTable( { + "lengthMenu": [ 50, 75, 100 ] } ); +</script> + </div> + + <div class="tab-pane fade" id="service-four"> + <table id="myTable4" class="display"> + <thead> + <tr> + <th>Title</th> + </tr> + </thead> + <tbody> + <tr><td><a href="https://www.biorxiv.org/content/10.1101/2020.12.23.424047v1">GeneNetwork: a continuously updated tool for systems genetics analyses</a></td></tr> + <tr><td><a href="https://www.biorxiv.org/content/10.1101/2021.05.24.445383v1">Old data and friends improve with age: Advancements with the updated tools of GeneNetwork</a></td> + <tr><td><a href="https://www.opar.io/pdf/Rat_HRDP_Brain_Proteomics_Wang_WIlliams_08Oct2021.pdf">A Primer on Brain Proteomics and protein-QTL Analysis for Substance Use Disorders</a></td></tr> + + </tbody> + </table> + <script> +$('#myTable4').dataTable( { + "lengthMenu": [ 50, 75, 100 ] } ); -</script>--> +</script> + </div> + </div> -<!--<script> -$(document).ready( function () { -$('#myTable').DataTable({ - "lengthMenu": [ [50, 75, 100, -1], [50, 75, 100, "All"] ]); -} ); -</script>--> + </div> + </div> -<!--<script> -$(document).ready( function () { -$('#myTable').DataTable(); -} ); -</script>--> +</div> + + <hr> + + + + </div> + <!-- /.container --> + + <!-- jQuery --> + <script src="js/jquery.js"></script> + + <!-- Bootstrap Core JavaScript --> + <script src="js/bootstrap.min.js"></script> </body> diff --git a/wqflask/wqflask/templates/wgcna_setup.html b/wqflask/wqflask/templates/wgcna_setup.html index c5461497..d7acd5f2 100644 --- a/wqflask/wqflask/templates/wgcna_setup.html +++ b/wqflask/wqflask/templates/wgcna_setup.html @@ -1,49 +1,142 @@ {% extends "base.html" %} {% block title %}WCGNA analysis{% endblock %} +{% block content %} +<!-- Start of body --> +<style type="text/css"> + +#terminal { + margin-top: 10px; +} + +</style> + +<link rel="stylesheet" type="text/css" href="{{ url_for('css', filename='xterm/xterm.min.css') }}" /> -{% block content %} <!-- Start of body --> -<h1> WGCNA analysis parameters</h1> <div class="container"> - {% if request.form['trait_list'].split(",")|length < 4 %} - <div class="alert alert-danger" role="alert"> - <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> - <span class="sr-only">Error:</span> - <h2>Too few phenotypes as input</h2> - Please make sure you select enough phenotypes / genes to perform WGCNA. Your collection needs to contain at least 4 different phenotypes. You provided {{request.form['trait_list'].split(',')|length}} phenotypes as input. - </div> - {% else %} - <form action="/wgcna_results" method="post" class="form-horizontal"> - <input type="hidden" name="trait_list" id="trait_list" value= "{{request.form['trait_list']}}"> - <div class="form-group"> - <label for="SoftThresholds"> Soft threshold: </label> - <div class="col-sm-10"> - <input type="text" class="form-inline" name="SoftThresholds" id="SoftThresholds" value="1,2,3,4,5,6,7,8,9"> - </div> - </div> - <div class="form-group"> - <label for="MinModuleSize"> Minimum module size: </label> - <div class="col-sm-10"> - <input type="text" class="form-inline" name="MinModuleSize" id="MinModuleSize" value="30"> - </div> - </div> - <div class="form-group"> - <label for="TOMtype"> TOMtype: </label> - <div class="col-sm-10"> - <input type="text" class="form-inline" name="TOMtype" id="TOMtype" value="unsigned"> - </div> - </div> - <div class="form-group"> - <label for="mergeCutHeight"> mergeCutHeight: </label> - <div class="col-sm-10"> - <input type="text" class="form-inline" name="mergeCutHeight" id="mergeCutHeight" value="0.25"> - </div> - </div> - <div class="form-group"> - <div class="col-sm-10"> - <input type="submit" class="btn btn-primary" value="Run WGCNA using these settings" /> - </div> + <div class="col-md-5"> + <h1 class="mx-3 my-2 "> WGCNA analysis parameters</h1> + {% if request.form['trait_list'].split(",")|length < 4 %} <div class="alert alert-danger" role="alert"> + <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> + <span class="sr-only">Error:</span> + <h2>Too few phenotypes as input</h2> + Please make sure you select enough phenotypes / genes to perform WGCNA. Your collection needs to contain at least 4 different phenotypes. You provided {{request.form['trait_list'].split(',')|length}} phenotypes as input. </div> - </form> - {% endif %} + {% else %} + <form class="col-md-12" action="/wgcna_results" method="post" class="form-horizontal" id="wgcna_form"> + <input type="hidden" name="trait_list" id="trait_list" value="{{request.form['trait_list']}}"> + <div class="form-group row "> + <label for="SoftThresholds" class="col-md-3 col-form-label col-form-label-sm">Soft threshhold</label> + <div class="col-md-9"> + <input type="text" class="form-control form-control-md" value="1,2,3,4,5,6,7,8,9" id="SoftThresholds" name="SoftThresholds"> + </div> + </div> + <div class="form-group row "> + <label for="MinModuleSize" class="col-md-3 col-form-label col-form-label-sm">Minimum module size:</label> + <div class="col-md-9"> + <input type="text" class="form-control form-control-md" id="MinModuleSize" value="30" name="MinModuleSize"> + </div> + </div> + + <div class="form-group row"> + <label for="TOMtype" class="col-md-3 col-form-label col-form-label-sm">TOMtype:</label> + <div class="col-md-9"> + <select class="form-control" id="TOMtype" name="TOMtype"> + <option value="unsigned">unsigned</option> + <option value="signed">signed</option> + </select> + </div> + + </div> + <div class="form-group row "> + <label for="mergeCutHeight" class="col-md-3 col-form-label col-form-label-sm">mergeCutHeight:</label> + <div class="col-md-9"> + <input type="text" class="form-control form-control-md" id="mergeCutHeight" value="0.25" name="mergeCutHeight"> + </div> + </div> + + <div class="form-group row"> + <label for="corType" class="col-md-3 col-form-label col-form-label-sm">corType:</label> + <div class="col-md-9"> + <select class="form-control col-md-9" id="corType" name="corType"> + <option value="pearson">pearson</option> + <option value="bicor">bicor</option> + </select> + </div> + + </div> + <div class="form-group"> + <div class="text-center"> + <input type="submit" class="btn btn-primary" value="Run WGCNA using these settings" /> + </div> + </div> + + + + </form> + {% endif %} </div> -{% endblock %} +<div class="col-md-7"> + <div id="terminal" class="mt-2"> + </div> +</div> +</div> + +<script src="{{ url_for('js', filename='xterm/xterm.min.js') }}" type="text/javascript"></script> +<script src="{{ url_for('js', filename='jquery/jquery.min.js') }}" type="text/javascript"></script> +<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.min.js"></script> + +<script> +document.addEventListener('DOMContentLoaded', function() { +let term = new Terminal({ + cursorBlink: true, + lineHeight: 1.3, + scrollback: true, + macOptionIsMeta: true +}); + +let termDebugs = { + general: "Computation process to be displayed here....", + success: "Computation in process ......", + fail: "Too few phenotypes as input must be >=4" +} + +const fitAddon = new FitAddon.FitAddon() +term.loadAddon(fitAddon) + +term.open(document.getElementById('terminal')); +term.setOption('theme', { + background: '#300a24' +}); +term.writeln(termDebugs.general) + +wgcnaForm = document.querySelector("#wgcna_form") + +fitAddon.fit() +term.onData((data) => { + term.write(data) +}) + + +if (wgcnaForm) { +} else { + term.writeln(termDebugs.fail) +} + +$(document).on('submit', '#wgcna_form', function(e) { + term.writeln(termDebugs.success) + + e.preventDefault(); + var form = $(this); + $.ajax({ + type: 'POST', + url: '/wgcna_results', + data: form.serialize(), + success: function(data) { + document.write(data) + } + }) +}) +}) + +</script> +{% endblock %}
\ No newline at end of file diff --git a/wqflask/wqflask/user_session.py b/wqflask/wqflask/user_session.py index 67e2e158..cef50cd9 100644 --- a/wqflask/wqflask/user_session.py +++ b/wqflask/wqflask/user_session.py @@ -10,7 +10,6 @@ from flask import (Flask, g, render_template, url_for, request, make_response, from wqflask import app from utility import hmac -#from utility.elasticsearch_tools import get_elasticsearch_connection from utility.redis_tools import get_redis_conn, get_user_id, get_user_by_unique_column, set_user_attribute, get_user_collections, save_collections Redis = get_redis_conn() @@ -23,7 +22,6 @@ THIRTY_DAYS = 60 * 60 * 24 * 30 @app.before_request def get_user_session(): - logger.info("@app.before_request get_session") g.user_session = UserSession() # ZS: I think this should solve the issue of deleting the cookie and redirecting to the home page when a user's session has expired if not g.user_session: diff --git a/wqflask/wqflask/views.py b/wqflask/wqflask/views.py index 6936ce78..be3d9238 100644 --- a/wqflask/wqflask/views.py +++ b/wqflask/wqflask/views.py @@ -4,7 +4,6 @@ import MySQLdb import array import base64 import csv -import difflib import datetime import flask import io # Todo: Use cStringIO? @@ -20,8 +19,6 @@ import traceback import uuid import xlsxwriter -from itertools import groupby -from collections import namedtuple from zipfile import ZipFile from zipfile import ZIP_DEFLATED @@ -30,19 +27,12 @@ from wqflask import app from gn3.commands import run_cmd from gn3.computations.gemma import generate_hash_of_string from gn3.db import diff_from_dict -from gn3.db import fetchall -from gn3.db import fetchone from gn3.db import insert from gn3.db import update from gn3.db.metadata_audit import MetadataAudit from gn3.db.phenotypes import Phenotype from gn3.db.phenotypes import Probeset from gn3.db.phenotypes import Publication -from gn3.db.phenotypes import PublishXRef -from gn3.db.phenotypes import probeset_mapping -from gn3.db.traits import get_trait_csv_sample_data -from gn3.db.traits import update_sample_data - from flask import current_app from flask import g @@ -79,6 +69,7 @@ from wqflask.correlation_matrix import show_corr_matrix from wqflask.correlation import corr_scatter_plot # from wqflask.wgcna import wgcna_analysis # from wqflask.ctl import ctl_analysis +from wqflask.wgcna.gn3_wgcna import run_wgcna from wqflask.snp_browser import snp_browser from wqflask.search_results import SearchResultPage from wqflask.export_traits import export_search_results_csv @@ -118,43 +109,16 @@ logger = utility.logger.getLogger(__name__) @app.before_request def connect_db(): - logger.info("@app.before_request connect_db") db = getattr(g, '_database', None) if db is None: g.db = g._database = sqlalchemy.create_engine( SQL_URI, encoding="latin1") - logger.debug(g.db) - - -@app.before_request -def check_access_permissions(): - logger.debug("@app.before_request check_access_permissions") - if 'dataset' in request.args: - permissions = DEFAULT_PRIVILEGES - if request.args['dataset'] != "Temp": - dataset = create_dataset(request.args['dataset']) - - if dataset.type == "Temp": - permissions = DEFAULT_PRIVILEGES - elif 'trait_id' in request.args: - permissions = check_resource_availability( - dataset, request.args['trait_id']) - elif dataset.type != "Publish": - permissions = check_resource_availability(dataset) - - if type(permissions['data']) is list: - if 'view' not in permissions['data']: - return redirect(url_for("no_access_page")) - else: - if permissions['data'] == 'no-access': - return redirect(url_for("no_access_page")) @app.teardown_appcontext def shutdown_session(exception=None): db = getattr(g, '_database', None) if db is not None: - logger.debug("remove db_session") db_session.remove() g.db = None @@ -167,9 +131,8 @@ def handle_generic_exceptions(e): time_str = now.strftime('%l:%M%p UTC %b %d, %Y') # get the stack trace and send it to the logger exc_type, exc_value, exc_traceback = sys.exc_info() - formatted_lines = {f"{request.url} ({time_str}) " - f" {traceback.format_exc().splitlines()}"} - + formatted_lines = (f"{request.url} ({time_str}) \n" + f"{traceback.format_exc()}") _message_templates = { werkzeug.exceptions.NotFound: ("404: Not Found: " f"{time_str}: {request.url}"), @@ -178,8 +141,8 @@ def handle_generic_exceptions(e): werkzeug.exceptions.RequestTimeout: ("408: Request Timeout: " f"{time_str}: {request.url}")} # Default to the lengthy stack trace! - logger.error(_message_templates.get(exc_type, - formatted_lines)) + app.logger.error(_message_templates.get(exc_type, + formatted_lines)) # Handle random animations # Use a cookie to have one animation on refresh animation = request.cookies.get(err_msg[:32]) @@ -188,7 +151,7 @@ def handle_generic_exceptions(e): "./wqflask/static/gif/error") if fn.endswith(".gif")]) resp = make_response(render_template("error.html", message=err_msg, - stack=formatted_lines, + stack={formatted_lines}, error_image=animation, version=GN_VERSION)) @@ -376,6 +339,12 @@ def wcgna_setup(): return render_template("wgcna_setup.html", **request.form) +@app.route("/wgcna_results", methods=('POST',)) +def wcgna_results(): + """call the gn3 api to get wgcna response data""" + results = run_wgcna(dict(request.form)) + return render_template("test_wgcna_results.html", **results) + @app.route("/ctl_setup", methods=('POST',)) def ctl_setup(): # We are going to get additional user input for the analysis @@ -419,289 +388,6 @@ def submit_trait_form(): version=GN_VERSION) -@app.route("/trait/<name>/edit/inbredset-id/<inbredset_id>") -@edit_access_required -def edit_phenotype(name, inbredset_id): - conn = MySQLdb.Connect(db=current_app.config.get("DB_NAME"), - user=current_app.config.get("DB_USER"), - passwd=current_app.config.get("DB_PASS"), - host=current_app.config.get("DB_HOST")) - publish_xref = fetchone( - conn=conn, - table="PublishXRef", - where=PublishXRef(id_=name, - inbred_set_id=inbredset_id)) - phenotype_ = fetchone( - conn=conn, - table="Phenotype", - where=Phenotype(id_=publish_xref.phenotype_id)) - publication_ = fetchone( - conn=conn, - table="Publication", - where=Publication(id_=publish_xref.publication_id)) - json_data = fetchall( - conn, - "metadata_audit", - where=MetadataAudit(dataset_id=publish_xref.id_)) - - Edit = namedtuple("Edit", ["field", "old", "new", "diff"]) - Diff = namedtuple("Diff", ["author", "diff", "timestamp"]) - diff_data = [] - for data in json_data: - json_ = json.loads(data.json_data) - timestamp = json_.get("timestamp") - author = json_.get("author") - for key, value in json_.items(): - if isinstance(value, dict): - for field, data_ in value.items(): - diff_data.append( - Diff(author=author, - diff=Edit(field, - data_.get("old"), - data_.get("new"), - "\n".join(difflib.ndiff( - [data_.get("old")], - [data_.get("new")]))), - timestamp=timestamp)) - diff_data_ = None - if len(diff_data) > 0: - diff_data_ = groupby(diff_data, lambda x: x.timestamp) - return render_template( - "edit_phenotype.html", - diff=diff_data_, - publish_xref=publish_xref, - phenotype=phenotype_, - publication=publication_, - version=GN_VERSION, - ) - - -@app.route("/trait/edit/probeset-name/<dataset_name>") -@edit_access_required -def edit_probeset(dataset_name): - conn = MySQLdb.Connect(db=current_app.config.get("DB_NAME"), - user=current_app.config.get("DB_USER"), - passwd=current_app.config.get("DB_PASS"), - host=current_app.config.get("DB_HOST")) - probeset_ = fetchone(conn=conn, - table="ProbeSet", - columns=list(probeset_mapping.values()), - where=Probeset(name=dataset_name)) - json_data = fetchall( - conn, - "metadata_audit", - where=MetadataAudit(dataset_id=probeset_.id_)) - Edit = namedtuple("Edit", ["field", "old", "new", "diff"]) - Diff = namedtuple("Diff", ["author", "diff", "timestamp"]) - diff_data = [] - for data in json_data: - json_ = json.loads(data.json_data) - timestamp = json_.get("timestamp") - author = json_.get("author") - for key, value in json_.items(): - if isinstance(value, dict): - for field, data_ in value.items(): - diff_data.append( - Diff(author=author, - diff=Edit(field, - data_.get("old"), - data_.get("new"), - "\n".join(difflib.ndiff( - [data_.get("old")], - [data_.get("new")]))), - timestamp=timestamp)) - diff_data_ = None - if len(diff_data) > 0: - diff_data_ = groupby(diff_data, lambda x: x.timestamp) - return render_template( - "edit_probeset.html", - diff=diff_data_, - probeset=probeset_) - - -@app.route("/trait/update", methods=["POST"]) -@edit_access_required -def update_phenotype(): - conn = MySQLdb.Connect(db=current_app.config.get("DB_NAME"), - user=current_app.config.get("DB_USER"), - passwd=current_app.config.get("DB_PASS"), - host=current_app.config.get("DB_HOST")) - data_ = request.form.to_dict() - TMPDIR = current_app.config.get("TMPDIR") - author = g.user_session.record.get(b'user_name') - if 'file' not in request.files: - flash("No sample-data has been uploaded", "warning") - else: - file_ = request.files['file'] - trait_name = str(data_.get('dataset-name')) - phenotype_id = str(data_.get('phenotype-id', 35)) - SAMPLE_DATADIR = os.path.join(TMPDIR, "sample-data") - if not os.path.exists(SAMPLE_DATADIR): - os.makedirs(SAMPLE_DATADIR) - if not os.path.exists(os.path.join(SAMPLE_DATADIR, - "diffs")): - os.makedirs(os.path.join(SAMPLE_DATADIR, - "diffs")) - if not os.path.exists(os.path.join(SAMPLE_DATADIR, - "updated")): - os.makedirs(os.path.join(SAMPLE_DATADIR, - "updated")) - current_time = str(datetime.datetime.now().isoformat()) - new_file_name = (os.path.join(TMPDIR, - "sample-data/updated/", - (f"{author.decode('utf-8')}." - f"{trait_name}.{phenotype_id}." - f"{current_time}.csv"))) - uploaded_file_name = (os.path.join( - TMPDIR, - "sample-data/updated/", - (f"updated.{author.decode('utf-8')}." - f"{trait_name}.{phenotype_id}." - f"{current_time}.csv"))) - file_.save(new_file_name) - publishdata_id = "" - lines = [] - with open(new_file_name, "r") as f: - lines = f.read() - first_line = lines.split('\n', 1)[0] - publishdata_id = first_line.split("Id:")[-1].strip() - with open(new_file_name, "w") as f: - f.write(lines.split("\n\n")[-1]) - csv_ = get_trait_csv_sample_data(conn=conn, - trait_name=str(trait_name), - phenotype_id=str(phenotype_id)) - with open(uploaded_file_name, "w") as f_: - f_.write(csv_.split("\n\n")[-1]) - r = run_cmd(cmd=("csvdiff " - f"'{uploaded_file_name}' '{new_file_name}' " - "--format json")) - diff_output = (f"{TMPDIR}/sample-data/diffs/" - f"{trait_name}.{author.decode('utf-8')}." - f"{phenotype_id}.{current_time}.json") - with open(diff_output, "w") as f: - dict_ = json.loads(r.get("output")) - dict_.update({ - "author": author.decode('utf-8'), - "publishdata_id": publishdata_id, - "dataset_id": data_.get("dataset-name"), - "timestamp": datetime.datetime.now().strftime( - "%Y-%m-%d %H:%M:%S") - }) - f.write(json.dumps(dict_)) - flash("Sample-data has been successfully uploaded", "success") - # Run updates: - phenotype_ = { - "pre_pub_description": data_.get("pre-pub-desc"), - "post_pub_description": data_.get("post-pub-desc"), - "original_description": data_.get("orig-desc"), - "units": data_.get("units"), - "pre_pub_abbreviation": data_.get("pre-pub-abbrev"), - "post_pub_abbreviation": data_.get("post-pub-abbrev"), - "lab_code": data_.get("labcode"), - "submitter": data_.get("submitter"), - "owner": data_.get("owner"), - "authorized_users": data_.get("authorized-users"), - } - updated_phenotypes = update( - conn, "Phenotype", - data=Phenotype(**phenotype_), - where=Phenotype(id_=data_.get("phenotype-id"))) - diff_data = {} - if updated_phenotypes: - diff_data.update({"Phenotype": diff_from_dict(old={ - k: data_.get(f"old_{k}") for k, v in phenotype_.items() - if v is not None}, new=phenotype_)}) - publication_ = { - "abstract": data_.get("abstract"), - "authors": data_.get("authors"), - "title": data_.get("title"), - "journal": data_.get("journal"), - "volume": data_.get("volume"), - "pages": data_.get("pages"), - "month": data_.get("month"), - "year": data_.get("year") - } - updated_publications = update( - conn, "Publication", - data=Publication(**publication_), - where=Publication(id_=data_.get("pubmed-id", - data_.get("old_id_")))) - if updated_publications: - diff_data.update({"Publication": diff_from_dict(old={ - k: data_.get(f"old_{k}") for k, v in publication_.items() - if v is not None}, new=publication_)}) - if diff_data: - diff_data.update({"dataset_id": data_.get("dataset-name")}) - diff_data.update({"author": author.decode('utf-8')}) - diff_data.update({"timestamp": datetime.datetime.now().strftime( - "%Y-%m-%d %H:%M:%S")}) - insert(conn, - table="metadata_audit", - data=MetadataAudit(dataset_id=data_.get("dataset-name"), - editor=author.decode("utf-8"), - json_data=json.dumps(diff_data))) - flash(f"Diff-data: \n{diff_data}\nhas been uploaded", "success") - return redirect(f"/trait/{data_.get('dataset-name')}" - f"/edit/inbredset-id/{data_.get('inbred-set-id')}") - - -@app.route("/probeset/update", methods=["POST"]) -@edit_access_required -def update_probeset(): - conn = MySQLdb.Connect(db=current_app.config.get("DB_NAME"), - user=current_app.config.get("DB_USER"), - passwd=current_app.config.get("DB_PASS"), - host=current_app.config.get("DB_HOST")) - data_ = request.form.to_dict() - probeset_ = { - "id_": data_.get("id"), - "symbol": data_.get("symbol"), - "description": data_.get("description"), - "probe_target_description": data_.get("probe_target_description"), - "chr_": data_.get("chr"), - "mb": data_.get("mb"), - "alias": data_.get("alias"), - "geneid": data_.get("geneid"), - "homologeneid": data_.get("homologeneid"), - "unigeneid": data_.get("unigeneid"), - "omim": data_.get("OMIM"), - "refseq_transcriptid": data_.get("refseq_transcriptid"), - "blatseq": data_.get("blatseq"), - "targetseq": data_.get("targetseq"), - "strand_probe": data_.get("Strand_Probe"), - "probe_set_target_region": data_.get("probe_set_target_region"), - "probe_set_specificity": data_.get("probe_set_specificity"), - "probe_set_blat_score": data_.get("probe_set_blat_score"), - "probe_set_blat_mb_start": data_.get("probe_set_blat_mb_start"), - "probe_set_blat_mb_end": data_.get("probe_set_blat_mb_end"), - "probe_set_strand": data_.get("probe_set_strand"), - "probe_set_note_by_rw": data_.get("probe_set_note_by_rw"), - "flag": data_.get("flag") - } - updated_probeset = update( - conn, "ProbeSet", - data=Probeset(**probeset_), - where=Probeset(id_=data_.get("id"))) - - diff_data = {} - author = g.user_session.record.get(b'user_name') - if updated_probeset: - diff_data.update({"Probeset": diff_from_dict(old={ - k: data_.get(f"old_{k}") for k, v in probeset_.items() - if v is not None}, new=probeset_)}) - if diff_data: - diff_data.update({"probeset_name": data_.get("probeset_name")}) - diff_data.update({"author": author.decode('utf-8')}) - diff_data.update({"timestamp": datetime.datetime.now().strftime( - "%Y-%m-%d %H:%M:%S")}) - insert(conn, - table="metadata_audit", - data=MetadataAudit(dataset_id=data_.get("id"), - editor=author.decode("utf-8"), - json_data=json.dumps(diff_data))) - return redirect(f"/trait/edit/probeset-name/{data_.get('probeset_name')}") - - @app.route("/create_temp_trait", methods=('POST',)) def create_temp_trait(): logger.info(request.url) @@ -712,12 +398,9 @@ def create_temp_trait(): @app.route('/export_trait_excel', methods=('POST',)) def export_trait_excel(): """Excel file consisting of the sample data from the trait data and analysis page""" - logger.info("In export_trait_excel") - logger.info("request.form:", request.form) - logger.info(request.url) trait_name, sample_data = export_trait_data.export_sample_table( request.form) - + app.logger.info(request.url) logger.info("sample_data - type: %s -- size: %s" % (type(sample_data), len(sample_data))) @@ -836,8 +519,10 @@ def export_perm_data(): @app.route("/show_temp_trait", methods=('POST',)) def show_temp_trait_page(): - logger.info(request.url) - template_vars = show_trait.ShowTrait(request.form) + user_id = ((g.user_session.record.get(b"user_id") or b"").decode("utf-8") + or g.user_session.record.get("user_id") or "") + template_vars = show_trait.ShowTrait(user_id=user_id, + kw=request.form) template_vars.js_data = json.dumps(template_vars.js_data, default=json_default_handler, indent=" ") @@ -846,8 +531,10 @@ def show_temp_trait_page(): @app.route("/show_trait") def show_trait_page(): - logger.info(request.url) - template_vars = show_trait.ShowTrait(request.args) + user_id = ((g.user_session.record.get(b"user_id") or b"").decode("utf-8") + or g.user_session.record.get("user_id") or "") + template_vars = show_trait.ShowTrait(user_id=user_id, + kw=request.args) template_vars.js_data = json.dumps(template_vars.js_data, default=json_default_handler, indent=" ") @@ -1356,22 +1043,6 @@ def json_default_handler(obj): type(obj), repr(obj))) -@app.route("/trait/<trait_name>/sampledata/<phenotype_id>") -def get_sample_data_as_csv(trait_name: int, phenotype_id: int): - conn = MySQLdb.Connect(db=current_app.config.get("DB_NAME"), - user=current_app.config.get("DB_USER"), - passwd=current_app.config.get("DB_PASS"), - host=current_app.config.get("DB_HOST")) - csv_ = get_trait_csv_sample_data(conn, str(trait_name), - str(phenotype_id)) - return Response( - csv_, - mimetype="text/csv", - headers={"Content-disposition": - "attachment; filename=myplot.csv"} - ) - - @app.route("/admin/data-sample/diffs/") @edit_access_required def display_diffs_admin(): @@ -1401,65 +1072,3 @@ def display_diffs_users(): files=files) -@app.route("/data-samples/approve/<name>") -def approve_data(name): - sample_data = {} - conn = MySQLdb.Connect(db=current_app.config.get("DB_NAME"), - user=current_app.config.get("DB_USER"), - passwd=current_app.config.get("DB_PASS"), - host=current_app.config.get("DB_HOST")) - TMPDIR = current_app.config.get("TMPDIR") - with open(os.path.join(f"{TMPDIR}/sample-data/diffs", - name), 'r') as myfile: - sample_data = json.load(myfile) - PUBLISH_ID = sample_data.get("publishdata_id") - modifications = [d for d in sample_data.get("Modifications")] - row_counts = len(modifications) - for modification in modifications: - if modification.get("Current"): - (strain_id, - strain_name, - value, se, count) = modification.get("Current").split(",") - update_sample_data( - conn=conn, - strain_name=strain_name, - strain_id=int(strain_id), - publish_data_id=int(PUBLISH_ID), - value=value, - error=se, - count=count - ) - insert(conn, - table="metadata_audit", - data=MetadataAudit( - dataset_id=name.split(".")[0], # use the dataset name - editor=sample_data.get("author"), - json_data=json.dumps(sample_data))) - if modifications: - # Once data is approved, rename it! - os.rename(os.path.join(f"{TMPDIR}/sample-data/diffs", name), - os.path.join(f"{TMPDIR}/sample-data/diffs", - f"{name}.approved")) - flash((f"Just updated data from: {name}; {row_counts} " - "row(s) modified!"), - "success") - return redirect("/admin/data-sample/diffs/") - - -@app.route("/data-samples/reject/<name>") -def reject_data(name): - TMPDIR = current_app.config.get("TMPDIR") - os.rename(os.path.join(f"{TMPDIR}/sample-data/diffs", name), - os.path.join(f"{TMPDIR}/sample-data/diffs", - f"{name}.rejected")) - flash(f"{name} has been rejected!", "success") - return redirect("/admin/data-sample/diffs/") - - -@app.route("/display-file/<name>") -def display_file(name): - TMPDIR = current_app.config.get("TMPDIR") - with open(os.path.join(f"{TMPDIR}/sample-data/diffs", - name), 'r') as myfile: - content = myfile.read() - return Response(content, mimetype='text/json') diff --git a/wqflask/wqflask/wgcna/gn3_wgcna.py b/wqflask/wqflask/wgcna/gn3_wgcna.py new file mode 100644 index 00000000..7bf5c62b --- /dev/null +++ b/wqflask/wqflask/wgcna/gn3_wgcna.py @@ -0,0 +1,111 @@ +"""module contains code to consume gn3-wgcna api +and process data to be rendered by datatables +""" + +import requests +from types import SimpleNamespace + +from utility.helper_functions import get_trait_db_obs +from utility.tools import GN3_LOCAL_URL + + +def fetch_trait_data(requestform): + """fetch trait data""" + db_obj = SimpleNamespace() + get_trait_db_obs(db_obj, + [trait.strip() + for trait in requestform['trait_list'].split(',')]) + + return process_dataset(db_obj.trait_list) + + +def process_dataset(trait_list): + """process datasets and strains""" + + input_data = {} + traits = [] + strains = [] + + for trait in trait_list: + traits.append(trait[0].name) + + input_data[trait[0].name] = {} + for strain in trait[0].data: + strains.append(strain) + input_data[trait[0].name][strain] = trait[0].data[strain].value + + return { + "input": input_data, + "trait_names": traits, + "sample_names": strains + } + + +def process_wgcna_data(response): + """function for processing modeigene genes + for create row data for datataba""" + mod_eigens = response["output"]["ModEigens"] + + sample_names = response["input"]["sample_names"] + + mod_dataset = [[sample] for sample in sample_names] + + for _, mod_values in mod_eigens.items(): + for (index, _sample) in enumerate(sample_names): + mod_dataset[index].append(round(mod_values[index], 3)) + + return { + "col_names": ["sample_names", *mod_eigens.keys()], + "mod_dataset": mod_dataset + } + + +def process_image(response): + """function to process image check if byte string is empty""" + image_data = response["output"]["image_data"] + return ({ + "image_generated": True, + "image_data": image_data + } if image_data else { + "image_generated": False + }) + + +def run_wgcna(form_data): + """function to run wgcna""" + + wgcna_api = f"{GN3_LOCAL_URL}/api/wgcna/run_wgcna" + + # parse form data + + trait_dataset = fetch_trait_data(form_data) + form_data["minModuleSize"] = int(form_data["MinModuleSize"]) + + form_data["SoftThresholds"] = [int(threshold.strip()) + for threshold in form_data['SoftThresholds'].rstrip().split(",")] + + try: + + response = requests.post(wgcna_api, json={ + "sample_names": list(set(trait_dataset["sample_names"])), + "trait_names": trait_dataset["trait_names"], + "trait_sample_data": list(trait_dataset["input"].values()), + **form_data + + } + ) + + status_code = response.status_code + response = response.json() + + return {"error": response} if status_code != 200 else { + "error": 'null', + "results": response, + "data": process_wgcna_data(response["data"]), + "image": process_image(response["data"]) + } + + except requests.exceptions.ConnectionError: + return { + "error": "A connection error to perform computation occurred" + } |