From 8ef2429ba3e6346ca16df3a5a5d1a72ea1c968f0 Mon Sep 17 00:00:00 2001 From: Daniel E Cook Date: Mon, 7 Sep 2020 14:46:58 -0400 Subject: [PATCH 001/288] update change log (#165) --- .travis.yml | 2 +- base/static/content/help/Change-Log.md | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 22088ff8..433f4c24 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: bash install: - openssl aes-256-cbc -K $encrypted_53077b9a3e95_key -iv $encrypted_53077b9a3e95_iv -in env_config.zip.enc -out env_config.zip -d - unzip -qo env_config.zip -- export VERSION_NUM=1-5-1 +- export VERSION_NUM=1-5-2 - export APP_CONFIG=master - if [ "${TRAVIS_BRANCH}" != "master" ]; then export APP_CONFIG=development; fi; - export GAE_VERSION=${APP_CONFIG}-${VERSION_NUM} diff --git a/base/static/content/help/Change-Log.md b/base/static/content/help/Change-Log.md index 000da7d7..f0fc719c 100644 --- a/base/static/content/help/Change-Log.md +++ b/base/static/content/help/Change-Log.md @@ -2,6 +2,11 @@ --- +##### v1.5.2 (2020-09-07) + +* A divergent region summmary track has been added to the primer indel tool. +* Sweep haplotypes have been added to the latest release. + ##### v1.5.1 (2020-08-30) * The [primer indel tool](/tools/pairwise_indel_finder) has been released. From 752c9267e7584977271b535f43935dfdaad7b3ea Mon Sep 17 00:00:00 2001 From: Daniel E Cook Date: Sun, 27 Sep 2020 20:29:09 -0400 Subject: [PATCH 002/288] Development (#167) * open up the strain catalog * Added back the missing slash which signals the end of italics (#166) * update change log (#165) * Added back the missing slack which signals the end of italics * add download for divergent regions Co-authored-by: Daniel E Cook Co-authored-by: Dan Lu --- .travis.yml | 2 +- base/application.py | 1 + base/static/content/news/2020-08-30-Version-1.5.3.md | 3 +++ base/templates/data_v2.html | 12 ++++++++++-- base/templates/strain/strain_catalog.html | 10 +++++----- base/views/order.py | 4 ---- base/views/strains.py | 3 +-- 7 files changed, 21 insertions(+), 14 deletions(-) create mode 100644 base/static/content/news/2020-08-30-Version-1.5.3.md diff --git a/.travis.yml b/.travis.yml index 433f4c24..9d3e3e97 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: bash install: - openssl aes-256-cbc -K $encrypted_53077b9a3e95_key -iv $encrypted_53077b9a3e95_iv -in env_config.zip.enc -out env_config.zip -d - unzip -qo env_config.zip -- export VERSION_NUM=1-5-2 +- export VERSION_NUM=1-5-3 - export APP_CONFIG=master - if [ "${TRAVIS_BRANCH}" != "master" ]; then export APP_CONFIG=development; fi; - export GAE_VERSION=${APP_CONFIG}-${VERSION_NUM} diff --git a/base/application.py b/base/application.py index 4d1188f6..5de26672 100644 --- a/base/application.py +++ b/base/application.py @@ -112,6 +112,7 @@ def register_extensions(app): cache.init_app(app, config={'CACHE_TYPE': 'base.utils.cache.datastore_cache'}) sqlalchemy(app) CSRFProtect(app) + app.config['csrf'] = CSRFProtect(app) def register_blueprints(app): diff --git a/base/static/content/news/2020-08-30-Version-1.5.3.md b/base/static/content/news/2020-08-30-Version-1.5.3.md new file mode 100644 index 00000000..3570788c --- /dev/null +++ b/base/static/content/news/2020-08-30-Version-1.5.3.md @@ -0,0 +1,3 @@ +##### v1.5.3 (2020-09-27) + +* The Strain catalogue has been opened. \ No newline at end of file diff --git a/base/templates/data_v2.html b/base/templates/data_v2.html index c250f2d5..0c165a14 100644 --- a/base/templates/data_v2.html +++ b/base/templates/data_v2.html @@ -175,8 +175,8 @@

Datasets

- Sweep haplotypes - The most frequent haplotype that covers at least 25% of the chromosome and is found on chromosome centers was determined and classified as a selective sweep. For more details, see Andersen et al. and Lee et al.. The plot shows red (swept), gray (non-swept), and white (not classified) regions. + Sweep Haplotypes + The most frequent haplotype that covers at least 25% of the chromosome and is found on chromosome centers was determined and classified as a selective sweep. For more details, see Andersen et al. and Lee et al.. The plot shows red (swept), gray (non-swept), and white (not classified) regions. sweep.pdf
@@ -184,6 +184,14 @@

Datasets

+ + Hyper-Divergent Regions + The hyper-divergent regions are characterized by higher-than-average density of small variants and large genomic spans where short sequence reads fail to align to the N2 reference genome. They were identified as described in Lee et al. + + divergent_regions_strain.bed.gz + + + Download BAMs Script You can batch download individual strain BAMs using this script. diff --git a/base/templates/strain/strain_catalog.html b/base/templates/strain/strain_catalog.html index c572e8b0..11be6456 100644 --- a/base/templates/strain/strain_catalog.html +++ b/base/templates/strain/strain_catalog.html @@ -9,6 +9,7 @@ {% endblock %} {% block content %}
+

Strain Sets

@@ -70,7 +71,8 @@

Strain Sets

{% for i in range(1,9) %} - + {# REVERT - enable mapping set 7 and 8 #} + = 7 %}disabled{% endif %} value="set_{{ i }}"/> {{ len(strain_sets["{}".format(i)]) }} @@ -213,8 +215,7 @@

Individual Strains

$("input:checkbox").click(function(x) { highlight_set_buttons(); - // [ ] REVERT; Checkout button temporary ban on orders - // $('#checkoutBox').prop('disabled', !$("input:checkbox").is(":checked")); + $('#checkoutBox').prop('disabled', !$("input:checkbox").is(":checked")); }); $("#submitPaste").click(function() { @@ -237,8 +238,6 @@

Individual Strains

$('#myModal').modal('hide'); }) - - $("button[id^='set']").click(function() { set_name = $(this).attr("id"); if ($("." + set_name).first().prop("checked") == false) { @@ -258,6 +257,7 @@

Individual Strains

}); + $(document).ready(function() { $('#filter').keydown(function(event) { if (event.keyCode == 13) { diff --git a/base/views/order.py b/base/views/order.py index 31af157c..629f56b6 100644 --- a/base/views/order.py +++ b/base/views/order.py @@ -40,10 +40,6 @@ def order_page(): """ This view handles the order page. """ - - # [ ] REVERT; TEMPORARY BLOCK ON ORDERS - return redirect("strains.strain_catalog", **locals()) - form = order_form() if session.get('user') and not form.email.data: form.email.data = session.get('user')['user_email'] diff --git a/base/views/strains.py b/base/views/strains.py index c8f28fb9..7300b687 100644 --- a/base/views/strains.py +++ b/base/views/strains.py @@ -110,8 +110,7 @@ def isotype_page(isotype_name, release=config['DATASET_RELEASE']): @strain_bp.route('/catalog', methods=['GET', 'POST']) @cache.memoize(50) def strain_catalog(): - # [ ] REVERT; TEMPORARY BAN ON NEW ORDERS - flash(Markup("Due to COVID-19, we are unable to accept new orders until further notice."), category="danger") + flash(Markup("Strain mapping sets 7 and 8 will not be available until later this year."), category="warning") VARS = {"title": "Strain Catalog", "warning": request.args.get('warning'), "strain_listing": get_strains(), From cd89173633bf8392f5765726673c11e2d9b74ef4 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 22 Mar 2021 02:53:40 -0500 Subject: [PATCH 003/288] Northwestern MTI Release --- Dockerfile | 5 + base/application.py | 49 ++- base/auth.py | 97 ----- base/cloud_config.py | 202 ++++++++++ base/config.py | 76 ++-- base/constants.py | 47 ++- base/database/__init__.py | 47 ++- base/database/etl_strains.py | 11 +- base/extensions.py | 5 +- base/forms.py | 104 ++++- base/manage.py | 7 + base/models.py | 190 ++++++++- base/static/img/logos/wwworms.png | Bin 0 -> 10624 bytes ...ariant-calling.png => variant-summary.png} | Bin .../20160408/{pipelines.md => methods.md} | 2 +- .../20170531/{pipelines.md => methods.md} | 2 +- .../20180527/{pipelines.md => methods.md} | 2 +- .../20200815/{pipelines.md => methods.md} | 2 +- base/templates/_includes/footer.html | 2 +- base/templates/_includes/head.html | 4 + base/templates/_includes/navbar.html | 27 +- base/templates/_layouts/clean.html | 1 - base/templates/_layouts/default.html | 8 +- base/templates/about/about.html | 108 +++--- base/templates/admin/admin.html | 6 + base/templates/admin/data_edit.html | 239 ++++++++++++ base/templates/admin/data_list.html | 56 +++ base/templates/admin/google_sheet.html | 11 + base/templates/admin/users_edit.html | 51 +++ base/templates/admin/users_list.html | 53 +++ base/templates/alignment.html | 18 + base/templates/basic_login.html | 23 ++ base/templates/browser.html | 8 +- base/templates/data.html | 6 +- base/templates/data_v2.html | 64 ++-- base/templates/download.html | 20 + base/templates/download_script.sh | 18 +- base/templates/errors/400.html | 9 + base/templates/errors/401.html | 8 + base/templates/errors/404.html | 2 +- base/templates/errors/405.html | 2 +- base/templates/errors/500.html | 2 +- base/templates/errors/generic.html | 2 +- base/templates/primary/home.html | 6 +- .../releases/download_tab_isotype_v1.html | 2 +- .../{login.html => select_login.html} | 8 +- base/templates/strain/global_strain_map.html | 238 ------------ base/templates/strain/isotype.html | 140 ++++--- base/templates/strain/strain_list.html | 361 ++++++++++++++++++ base/templates/strain_issues.html | 14 + base/templates/user.html | 49 --- base/templates/user/profile.html | 37 ++ base/templates/user/register.html | 26 ++ base/templates/user/update.html | 33 ++ base/utils/cache.py | 11 +- base/utils/data_utils.py | 19 +- base/utils/gcloud.py | 92 ++++- base/utils/jwt.py | 90 +++++ base/views/about.py | 14 +- base/views/admin/__init__.py | 4 + base/views/admin/admin.py | 38 ++ base/views/admin/data.py | 247 ++++++++++++ base/views/admin/users.py | 68 ++++ base/views/api/api_popgen.py | 15 +- base/views/api/api_strain.py | 5 +- base/views/api/api_variant.py | 11 +- base/views/auth/__init__.py | 5 + base/views/auth/auth.py | 81 ++++ base/views/auth/oauth.py | 55 +++ base/views/auth/saml.py | 173 +++++++++ base/views/data.py | 129 +++++-- base/views/maintenance.py | 14 + base/views/mapping.py | 2 + base/views/order.py | 14 +- base/views/strains.py | 25 +- base/views/tools/indel_primer.py | 9 +- base/views/user.py | 72 +++- cloud_buckets/README.md | 0 cloud_buckets/deploy.sh | 3 + cloud_config.txt | 1 + cloud_functions/README.md | 8 + .../generate_thumbnails/.gcloudignore | 12 + cloud_functions/generate_thumbnails/README.md | 12 + cloud_functions/generate_thumbnails/deploy.sh | 3 + cloud_functions/generate_thumbnails/main.py | 26 ++ .../generate_thumbnails/requirements.txt | 2 + cron.yaml | 4 + env.yaml | 32 +- index.yaml | 7 +- main.py | 12 +- mapping_worker/utils/gcloud.py | 7 +- mapping_worker/utils/interval.py | 3 +- requirements.txt | 11 +- 93 files changed, 2999 insertions(+), 847 deletions(-) delete mode 100644 base/auth.py create mode 100644 base/cloud_config.py create mode 100644 base/static/img/logos/wwworms.png rename base/static/img/main/{variant-calling.png => variant-summary.png} (100%) rename base/static/reports/20160408/{pipelines.md => methods.md} (98%) rename base/static/reports/20170531/{pipelines.md => methods.md} (98%) rename base/static/reports/20180527/{pipelines.md => methods.md} (98%) rename base/static/reports/20200815/{pipelines.md => methods.md} (99%) create mode 100644 base/templates/admin/admin.html create mode 100644 base/templates/admin/data_edit.html create mode 100644 base/templates/admin/data_list.html create mode 100644 base/templates/admin/google_sheet.html create mode 100644 base/templates/admin/users_edit.html create mode 100644 base/templates/admin/users_list.html create mode 100644 base/templates/alignment.html create mode 100644 base/templates/basic_login.html create mode 100644 base/templates/download.html create mode 100644 base/templates/errors/400.html create mode 100644 base/templates/errors/401.html rename base/templates/{login.html => select_login.html} (56%) delete mode 100644 base/templates/strain/global_strain_map.html create mode 100644 base/templates/strain/strain_list.html create mode 100644 base/templates/strain_issues.html delete mode 100644 base/templates/user.html create mode 100644 base/templates/user/profile.html create mode 100644 base/templates/user/register.html create mode 100644 base/templates/user/update.html create mode 100644 base/utils/jwt.py create mode 100644 base/views/admin/__init__.py create mode 100644 base/views/admin/admin.py create mode 100644 base/views/admin/data.py create mode 100644 base/views/admin/users.py create mode 100644 base/views/auth/__init__.py create mode 100644 base/views/auth/auth.py create mode 100644 base/views/auth/oauth.py create mode 100644 base/views/auth/saml.py create mode 100644 base/views/maintenance.py create mode 100644 cloud_buckets/README.md create mode 100644 cloud_buckets/deploy.sh create mode 100644 cloud_config.txt create mode 100644 cloud_functions/README.md create mode 100644 cloud_functions/generate_thumbnails/.gcloudignore create mode 100644 cloud_functions/generate_thumbnails/README.md create mode 100755 cloud_functions/generate_thumbnails/deploy.sh create mode 100644 cloud_functions/generate_thumbnails/main.py create mode 100644 cloud_functions/generate_thumbnails/requirements.txt diff --git a/Dockerfile b/Dockerfile index 3312cf15..5e795449 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,11 @@ tabix \ graphviz \ libgraphviz-dev \ pkg-config \ +libxml2 \ +xmlsec1 \ +libxml2-dev \ +libxmlsec1-dev \ +libxmlsec1-openssl \ && rm -rf /var/lib/apt/lists/* ENV BCFTOOLS_BIN="bcftools-1.10.tar.bz2" \ diff --git a/base/application.py b/base/application.py index 5de26672..21ce5206 100644 --- a/base/application.py +++ b/base/application.py @@ -2,13 +2,15 @@ import json import requests from os.path import basename -from base.config import config from flask import Flask, render_template from flask_wtf.csrf import CSRFProtect -from base.utils.text_utils import render_markdown from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.exceptions import HTTPException + +from base.constants import GOOGLE_CLOUD_BUCKET +from base.config import config +from base.utils.text_utils import render_markdown from base.manage import (initdb, update_strains, update_credentials, @@ -26,6 +28,12 @@ from base.views.mapping import mapping_bp from base.views.gene import gene_bp from base.views.user import user_bp +from base.views.maintenance import maintenance_bp +from base.views.admin.admin import admin_bp +from base.views.admin.users import users_bp +from base.views.admin.data import data_admin_bp + + # Tools from base.views.tools import (tools_bp, @@ -42,9 +50,9 @@ from base.views.api.api_data import api_data_bp # Auth -from base.auth import (auth_bp, - google_bp, - github_bp) +from base.views.auth import (auth_bp, + google_bp, + saml_bp) # ---- End Routing ---- # @@ -54,7 +62,8 @@ cache, debug_toolbar, sslify, - sqlalchemy) + sqlalchemy, + jwt) # Template filters from base.filters import (comma, format_release) @@ -110,9 +119,15 @@ def register_template_filters(app): def register_extensions(app): markdown(app) cache.init_app(app, config={'CACHE_TYPE': 'base.utils.cache.datastore_cache'}) - sqlalchemy(app) - CSRFProtect(app) - app.config['csrf'] = CSRFProtect(app) + #sqlalchemy(app) + sqlalchemy.init_app(app) + # protect all routes (except the ones listed) from cross site request forgery + csrf = CSRFProtect(app) + csrf.exempt(auth_bp) + csrf.exempt(saml_bp) + csrf.exempt(maintenance_bp) + app.config['csrf'] = csrf + jwt.init_app(app) def register_blueprints(app): @@ -124,6 +139,8 @@ def register_blueprints(app): app.register_blueprint(data_bp, url_prefix='/data') app.register_blueprint(mapping_bp, url_prefix='') app.register_blueprint(gene_bp, url_prefix='/gene') + + # User app.register_blueprint(user_bp, url_prefix='/user') # Tools @@ -138,16 +155,22 @@ def register_blueprints(app): app.register_blueprint(api_data_bp, url_prefix='/api') # Auth - app.register_blueprint(auth_bp, url_prefix='') + app.register_blueprint(auth_bp, url_prefix='/auth') + app.register_blueprint(saml_bp, url_prefix='/saml') app.register_blueprint(google_bp, url_prefix='/login') - app.register_blueprint(github_bp, url_prefix='/login') - # Healthchecks + # Admin + app.register_blueprint(admin_bp, url_prefix='/admin') + app.register_blueprint(users_bp, url_prefix='/admin/users') + app.register_blueprint(data_admin_bp, url_prefix='/admin/data') + + # Healthchecks/Maintenance + app.register_blueprint(maintenance_bp, url_prefix='/tasks') app.register_blueprint(check_bp, url_prefix='') def gs_static(url, prefix='static'): - return f"https://storage.googleapis.com/elegansvariation.org/{prefix}/{url}" + return f"https://storage.googleapis.com/{GOOGLE_CLOUD_BUCKET}/{prefix}/{url}" def configure_jinja(app): diff --git a/base/auth.py b/base/auth.py deleted file mode 100644 index d05f09c5..00000000 --- a/base/auth.py +++ /dev/null @@ -1,97 +0,0 @@ -import arrow -import os -from flask import (redirect, - render_template, - url_for, - session, - request, - flash) -from functools import wraps -from base.models import user_ds -from base.utils.data_utils import unique_id -from slugify import slugify -from logzero import logger - -from flask_dance.contrib.google import make_google_blueprint, google -from flask_dance.contrib.github import make_github_blueprint, github -from flask_dance.consumer import oauth_authorized - -from flask import Blueprint -auth_bp = Blueprint('auth', - __name__, - template_folder='') - -google_bp = make_google_blueprint(scope=["https://www.googleapis.com/auth/userinfo.profile", - "https://www.googleapis.com/auth/userinfo.email"], - offline=True) -github_bp = make_github_blueprint(scope="user:email") -# dropbox_bp = make_dropbox_blueprint() - - -@auth_bp.route("/login/select", methods=['GET']) -def choose_login(error=None): - # Relax scope for Google - if not session.get("login_referrer", "").endswith("/login/select"): - session["login_referrer"] = request.referrer - os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = "true" - VARS = {'page_title': 'Choose Login'} - if error: - flash(error, 'danger') - return render_template('login.html', **VARS) - - -@oauth_authorized.connect -def authorized(blueprint, token): - if google.authorized: - user_info = google.get("/oauth2/v2/userinfo") - assert user_info.ok - user_info = {'google': user_info.json()} - user_email = user_info['google']['email'].lower() - elif github.authorized: - user_emails = github.get("/user/emails") - user_email = [x for x in user_emails.json() if x['primary']][0]["email"].lower() - user_info = {'github': github.get('/user').json()} - user_info['github']['email'] = user_email - else: - flash("Error logging in!") - return redirect(url_for("auth.choose_login")) - - # Create or get existing user. - user = user_ds(user_email) - if not user._exists: - user.user_email = user_email - user.user_info = user_info - user.email_confirmation_code = unique_id() - user.user_id = unique_id()[0:8] - user.username = slugify("{}_{}".format(user_email.split("@")[0], unique_id()[0:4])) - - user.last_login = arrow.utcnow().datetime - user.save() - - session['user'] = user.to_dict() - logger.debug(session) - - flash("Successfully logged in!", 'success') - return redirect(session.get("login_referrer", url_for('primary.primary'))) - - -def login_required(f): - @wraps(f) - def func(*args, **kwargs): - if not session.get('user'): - logger.info(session) - with app.app_context(): - session['redirect_url'] = request.url - return redirect(url_for('auth.choose_login')) - return f(*args, **kwargs) - return func - - -@auth_bp.route('/logout') -def logout(): - """ - Logs the user out. - """ - session.clear() - flash("Successfully logged out", "success") - return redirect(request.referrer) diff --git a/base/cloud_config.py b/base/cloud_config.py new file mode 100644 index 00000000..4d126ef0 --- /dev/null +++ b/base/cloud_config.py @@ -0,0 +1,202 @@ +# Application Cloud Configuration for Site Static Content hosted externally +import os +import shutil +import json + +from os import path +from logzero import logger +from google.oauth2 import service_account +from google.cloud import datastore, storage + +from base.constants import REPORT_V1_FILE_LIST, REPORT_V2_FILE_LIST +from base.utils.data_utils import dump_json, unique_id +from base.utils.gcloud import download_file + +class CloudConfig: + + ds_client = None + storage_client = None + kind = 'cloud-config' + default_cc = { 'releases' : [{'dataset': '20200815', 'wormbase': 'WS276', 'version': 'v2'}, + {'dataset': '20180527', 'wormbase': 'WS263', 'version': 'v1'}, + {'dataset': '20170531', 'wormbase': 'WS258', 'version': 'v1'}, + {'dataset': '20160408', 'wormbase': 'WS245', 'version': 'v1'}] } + + def __init__(self, name, cc=default_cc, local=True): + self.name = name + self.filename = f"{name}.txt" + self.cc = cc + self.local = local + + def get_ds_client(self): + if not self.ds_client: + self.ds_client = datastore.Client(credentials=service_account.Credentials.from_service_account_file('env_config/client-secret.json')) + return self.ds_client + + def get_storage_client(self): + if not self.storage_client: + self.storage_client = storage.Client(credentials=service_account.Credentials.from_service_account_file('env_config/client-secret.json')) + return self.storage_client + + def ds_save(self): + data = {'cloud_config': self.cc} + m = datastore.Entity(key=self.get_ds_client().key(self.kind, self.name)) + for key, value in data.items(): + if isinstance(value, dict): + m[key] = 'JSON:' + dump_json(value) + else: + m[key] = value + logger.debug(f"store: {self.kind} - {self.name}") + self.get_ds_client().put(m) + + def ds_load(self): + """ Retrieves a cloud config object from datastore """ + result = self.get_ds_client().get(self.get_ds_client().key(self.kind, self.name)) + logger.debug(f"get: {self.kind} - {self.name}") + try: + result_out = {'_exists': True} + for k, v in result.items(): + if isinstance(v, str) and v.startswith("JSON:"): + result_out[k] = json.loads(v[5:]) + elif v: + result_out[k] = v + self.cc = result_out.get('cloud_config') + except AttributeError: + return None + + def file_load(self): + """ Retrieves a cloud config object from a local file """ + if path.exists(self.filename): + with open(self.filename) as json_file: + data = json.load(json_file) + cc = data.get('cloud_config') if data else None + self.cc = cc + + def file_save(self): + """ Saves a cloud config object to a local file """ + with open(self.filename, 'w') as outfile: + data = {'cloud_config': self.cc} + json.dump(data, outfile) + + def save(self): + if self.local: + self.file_save() + else: + self.ds_save() + + def load(self): + if self.local: + self.file_load() + else: + self.ds_load() + + def remove_release(self, dataset): + ''' Removes a data release from the cloud config object ''' + releases = self.cc['releases'] + for i, r in enumerate(releases): + if r['dataset'] == dataset: + del releases[i] + + self.cc['releases'] = releases + self.save() + + def remove_release_files(self, dataset): + ''' Removes files linked to a data release from the GAE server ''' + report_path = f"base/static/reports/{dataset}" + if os.path.exists(report_path): + shutil.rmtree(report_path) + + def remove_release_db(self, dataset, wormbase): + ''' Removes sqlite db linked to a data release from the GAE server ''' + db_path = f"base/cendr.{dataset}.{wormbase}.db" + os.remove(db_path) + + def add_release(self, dataset, wormbase, version): + ''' Adds a data release to the cloud config object ''' + releases = self.cc['releases'] + # remove dataset if there is an existing one in the config + for i, r in enumerate(releases): + if r['dataset'] == dataset: + del releases[i] + + releases = [{'dataset': dataset, 'wormbase': wormbase, 'version': version}] + releases + self.cc['releases'] = releases + self.save() + + def get_release_files(self, dataset, files, refresh=False): + ''' Downloads files linked to a data release from the cloud bucket to the GAE server''' + local_path = 'base/static/reports/{}'.format(dataset) + if os.path.exists(local_path): + if refresh == True: + shutil.rmtree(local_path) + else: + return + + os.makedirs(local_path) + name_str = 'data_reports/{}/{}' + fname_str = '{}/{}' + + try: + for n in files: + name = f"data_reports/{dataset}/{n}" + fname = f"{local_path}/{n}" + download_file(name=name, fname=fname) + except: + return None + return files + + def get_release_db(self, dataset, wormbase, refresh=False): + db_name = f"db/cendr.{dataset}.{wormbase}.db" + db_fname = f"base/cendr.{dataset}.{wormbase}.db" + if os.path.exists(db_fname): + if refresh == True: + os.remove(db_fname) + else: + return + + download_file(name=db_name, fname=db_fname) + return True + + def create_backup(self): + name = self.name + self.name = '{}_{}'.format(name, unique_id()) + self.save() + self.name = name + + def get_properties(self): + ''' Converts the cloud_config object into a format that matches the regular config object ''' + releases = self.cc['releases'] + RELEASES = [] + for r in releases: + RELEASES.append((r['dataset'], r['wormbase'])) + RELEASES.sort(reverse=True) + + # Set the most recent release + DATASET_RELEASE, WORMBASE_VERSION = RELEASES[0] + + # SQLITE DATABASE + SQLITE_PATH = f"base/cendr.{DATASET_RELEASE}.{WORMBASE_VERSION}.db" + SQLALCHEMY_DATABASE_URI = f"sqlite:///{SQLITE_PATH}".replace('base/', '') + + return {'DATASET_RELEASE': DATASET_RELEASE, + 'WORMBASE_VERSION': WORMBASE_VERSION, + 'RELEASES': RELEASES, + 'SQLALCHEMY_DATABASE_URI': SQLALCHEMY_DATABASE_URI, + 'SQLITE_PATH': SQLITE_PATH} + + def get_external_content(self): + releases = self.cc['releases'] + current_release = releases[0] + + # get data reports + for r in releases: + files = [] + if r['version'] == 'v1': + files = REPORT_V1_FILE_LIST + elif r['version'] == 'v2': + files = REPORT_V2_FILE_LIST + self.get_release_files(r['dataset'], files, refresh=False) + + # get sqlite db + self.get_release_db(current_release['dataset'], current_release['wormbase'], refresh=False) + diff --git a/base/config.py b/base/config.py index 5c513097..34dab0ae 100644 --- a/base/config.py +++ b/base/config.py @@ -2,20 +2,25 @@ import os import yaml from base.utils.data_utils import json_encoder +from base.constants import DEFAULT_CLOUD_CONFIG +from base.cloud_config import CloudConfig + +# Whether or not to load config properties from cloud datastore +try: + CLOUD_CONFIG = os.environ['CLOUD_CONFIG'] +except: + CLOUD_CONFIG = 0 # CeNDR Version APP_CONFIG, CENDR_VERSION = os.environ['GAE_VERSION'].split("-", 1) if APP_CONFIG not in ['development', 'master']: - APP_CONFIG = 'development' + APP_CONFIG = 'development' CENDR_VERSION = CENDR_VERSION.replace("-", '.') # BUILDS AND RELEASES # The first release is the current release # (RELEASE, ANNOTATION_GENOME) -RELEASES = [("20200815", "WS276"), - ("20180527", "WS263"), - ("20170531", "WS258"), - ("20160408", "WS245")] +RELEASES = [("20200815", "WS276"), ("20180527", "WS263"), ("20170531", "WS258"), ("20160408", "WS245")] # The most recent release DATASET_RELEASE, WORMBASE_VERSION = RELEASES[0] @@ -23,35 +28,44 @@ # SQLITE DATABASE SQLITE_PATH = f"base/cendr.{DATASET_RELEASE}.{WORMBASE_VERSION}.db" +SAML_PATH = "env_config" def load_yaml(path): - return yaml.load(open(path), Loader=yaml.SafeLoader) - + return yaml.load(open(path), Loader=yaml.SafeLoader) # CONFIG def get_config(APP_CONFIG): - """Load all configuration information including - constants defined above. - - (BASE_VARS are the same regardless of whether we are debugging or in production) - """ - config = dict() - BASE_VARS = load_yaml("env_config/base.yaml") - APP_CONFIG_VARS = load_yaml(f"env_config/{APP_CONFIG}.yaml") - config.update(BASE_VARS) - config.update(APP_CONFIG_VARS) - # Add configuration variables - # Remove base prefix for SQLAlchemy as it is loaded - # from application folder - config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{SQLITE_PATH}".replace("base/", "") - config['json_encoder'] = json_encoder - config.update({"CENDR_VERSION": CENDR_VERSION, - "APP_CONFIG": APP_CONFIG, - "DATASET_RELEASE": DATASET_RELEASE, - "WORMBASE_VERSION": WORMBASE_VERSION, - "RELEASES": RELEASES}) - return config - - -# Generate the configuration + """Load all configuration information including + constants defined above. + + (BASE_VARS are the same regardless of whether we are debugging or in production) + """ + config = dict() + BASE_VARS = load_yaml("env_config/base.yaml") + APP_CONFIG_VARS = load_yaml(f"env_config/{APP_CONFIG}.yaml") + config.update(BASE_VARS) + config.update(APP_CONFIG_VARS) + + config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{SQLITE_PATH}".replace("base/", "") + config['json_encoder'] = json_encoder + config['SAML_PATH'] = SAML_PATH + config.update({"CENDR_VERSION": CENDR_VERSION, + "APP_CONFIG": APP_CONFIG, + "DATASET_RELEASE": DATASET_RELEASE, + "WORMBASE_VERSION": WORMBASE_VERSION, + "RELEASES": RELEASES}) + + cc = None + local = True if CLOUD_CONFIG == 1 else False + # Add configuration variables from cloud + cc = CloudConfig(DEFAULT_CLOUD_CONFIG, local=local) + cc.load() + cc.get_external_content() + props = cc.get_properties() + config.update(props) + config['cloud_config'] = cc + + return config + + config = get_config(APP_CONFIG) diff --git a/base/constants.py b/base/constants.py index 1111a850..980dcb70 100644 --- a/base/constants.py +++ b/base/constants.py @@ -6,15 +6,24 @@ Author: Daniel E. Cook (danielecook@gmail.com) """ -from base.config import WORMBASE_VERSION +WORMBASE_VERSION = 'WS276' + +USER_ROLES = [('user', 'User'), ('admin', 'Admin')] class PRICES: - DIVERGENT_SET = 160 - STRAIN_SET = 640 - STRAIN = 15 - SHIPPING = 65 + DIVERGENT_SET = 160 + STRAIN_SET = 640 + STRAIN = 15 + SHIPPING = 65 + +SHIPPING_OPTIONS = [('UPS', 'UPS'), + ('FEDEX', 'FEDEX'), + ('Flat Rate Shipping', '${} Flat Fee'.format(PRICES.SHIPPING))] + +PAYMENT_OPTIONS = [('check', 'Check'), + ('credit_card', 'Credit Card')] # Maps chromosome in roman numerals to integer CHROM_NUMERIC = {"I": 1, @@ -25,6 +34,10 @@ class PRICES: "X": 6, "MtDNA": 7} + +GOOGLE_CLOUD_BUCKET = 'elegansvariation' +GOOGLE_CLOUD_PROJECT_ID = 'andersen-lab-302418' + # WI Strain Info Dataset GOOGLE_SHEETS = {"orders": "1BCnmdJNRjQR3Bx8fMjD_IlTzmh3o7yj8ZQXTkk6tTXM", "WI": "1V6YHzblaDph01sFDI8YK_fP0H7sVebHQTXypGdiQIjI"} @@ -39,35 +52,28 @@ class URLS: URLs are stored here so they can be easily integrated into the database for provenance purposes. """ - # - # AWS URLS + # BAMs are now hosted on google cloud buckets # - BAM_URL_PREFIX = "https://s3.us-east-2.amazonaws.com/elegansvariation.org/bam" + BAM_URL_PREFIX = f"https://storage.googleapis.com/{GOOGLE_CLOUD_BUCKET}/bam" """ Wormbase URLs """ - # Gene GTF - GENE_GTF_URL = f"ftp://ftp.wormbase.org/pub/wormbase/releases/{WORMBASE_VERSION}/species/c_elegans/PRJNA13758/c_elegans.PRJNA13758.{WORMBASE_VERSION}.canonical_geneset.gtf.gz" - + GENE_GTF_URL = "ftp://ftp.wormbase.org/pub/wormbase/releases/{WB}/species/c_elegans/PRJNA13758/c_elegans.PRJNA13758.{WB}.canonical_geneset.gtf.gz" # GENE GFF_URL - GENE_GFF_URL = f"ftp://ftp.wormbase.org/pub/wormbase/releases/{WORMBASE_VERSION}/species/c_elegans/PRJNA13758/c_elegans.PRJNA13758.{WORMBASE_VERSION}.annotations.gff3.gz" - + GENE_GFF_URL = "ftp://ftp.wormbase.org/pub/wormbase/releases/{WB}/species/c_elegans/PRJNA13758/c_elegans.PRJNA13758.{WB}.annotations.gff3.gz" # Maps wormbase ID to locus name GENE_IDS_URL = "ftp://ftp.wormbase.org/pub/wormbase/species/c_elegans/annotation/geneIDs/c_elegans.PRJNA13758.current.geneIDs.txt.gz" - # Lists C. elegans orthologs ORTHOLOG_URL = "ftp://ftp.wormbase.org/pub/wormbase/species/c_elegans/PRJNA13758/annotation/orthologs/c_elegans.PRJNA13758.current_development.orthologs.txt" # # Ortholog URLs # - # Homologene HOMOLOGENE_URL = 'https://ftp.ncbi.nih.gov/pub/HomoloGene/current/homologene.data' - # Taxon IDs TAXON_ID_URL = 'ftp://ftp.ncbi.nih.gov/pub/taxonomy/taxdump.tar.gz' @@ -91,4 +97,11 @@ class URLS: TABLE_COLORS = {"LOW": 'success', "MODERATE": 'warning', - "HIGH": 'danger'} \ No newline at end of file + "HIGH": 'danger'} + + +DEFAULT_CLOUD_CONFIG = 'default' + +REPORT_VERSIONS = ['', 'v1', 'v2'] +REPORT_V1_FILE_LIST = ['methods.md'] +REPORT_V2_FILE_LIST = ['alignment_report.html', 'concordance_report.html', 'gatk_report.html', 'methods.md', 'reads_mapped_by_strain.tsv', 'release_notes.md'] \ No newline at end of file diff --git a/base/database/__init__.py b/base/database/__init__.py index bc094675..968f28e8 100644 --- a/base/database/__init__.py +++ b/base/database/__init__.py @@ -1,10 +1,12 @@ import os import arrow import pickle - from rich.console import Console +from google.cloud import storage + from base import constants -from base.constants import URLS +from base.constants import URLS, GOOGLE_CLOUD_BUCKET +from base.config import config from base.utils.data_utils import download from base.utils.gcloud import upload_file from base.models import (db, @@ -13,11 +15,6 @@ Metadata, WormbaseGene, WormbaseGeneSummary) -from base.config import (CENDR_VERSION, - APP_CONFIG, - DATASET_RELEASE, - WORMBASE_VERSION, - RELEASES) # ETL Pipelines - fetch and format data for # input into the sqlite database from base.database.etl_homologene import fetch_homologene @@ -29,7 +26,6 @@ console = Console() DOWNLOAD_PATH = ".download" - def download_fname(download_path: str, download_url: str): return os.path.join(download_path, download_url.split("/")[-1]) @@ -46,6 +42,7 @@ def initialize_sqlite_database(sel_wormbase_version, start = arrow.utcnow() console.log("Initializing Database") + DATASET_RELEASE = config['DATASET_RELEASE'] SQLITE_PATH = f"base/cendr.{DATASET_RELEASE}.{sel_wormbase_version}.db" SQLITE_BASENAME = os.path.basename(SQLITE_PATH) @@ -59,24 +56,26 @@ def initialize_sqlite_database(sel_wormbase_version, # Parallel URL download console.log("Downloading Wormbase Data") - download([URLS.GENE_GFF_URL, - URLS.GENE_GTF_URL, + GENE_GFF_URL = URLS.GENE_GFF_URL.format(WB=sel_wormbase_version) + GENE_GTF_URL = URLS.GENE_GTF_URL.format(WB=sel_wormbase_version) + download([GENE_GFF_URL, + GENE_GTF_URL, URLS.GENE_IDS_URL, URLS.HOMOLOGENE_URL, URLS.ORTHOLOG_URL, URLS.TAXON_ID_URL], - DOWNLOAD_PATH) + DOWNLOAD_PATH) - gff_fname = download_fname(DOWNLOAD_PATH, URLS.GENE_GFF_URL) - gtf_fname = download_fname(DOWNLOAD_PATH, URLS.GENE_GTF_URL) + gff_fname = download_fname(DOWNLOAD_PATH, GENE_GFF_URL) + gtf_fname = download_fname(DOWNLOAD_PATH, GENE_GTF_URL) gene_ids_fname = download_fname(DOWNLOAD_PATH, URLS.GENE_IDS_URL) homologene_fname = download_fname(DOWNLOAD_PATH, URLS.HOMOLOGENE_URL) ortholog_fname = download_fname(DOWNLOAD_PATH, URLS.ORTHOLOG_URL) from base.application import create_app app = create_app() - app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{SQLITE_BASENAME}" app.app_context().push() + app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{SQLITE_BASENAME}" if strain_only is True: db.metadata.drop_all(bind=db.engine, checkfirst=True, tables=[Strain.__table__]) @@ -105,11 +104,11 @@ def initialize_sqlite_database(sel_wormbase_version, console.log('Inserting metadata') metadata = {} metadata.update(vars(constants)) - metadata.update({"CENDR_VERSION": CENDR_VERSION, - "APP_CONFIG": APP_CONFIG, - "DATASET_RELEASE": DATASET_RELEASE, + metadata.update({"CENDR_VERSION": config['CENDR_VERSION'], + "APP_CONFIG": config['APP_CONFIG'], + "DATASET_RELEASE": config['DATASET_RELEASE'], "WORMBASE_VERSION": sel_wormbase_version, - "RELEASES": RELEASES, + "RELEASES": config['RELEASES'], "DATE": arrow.utcnow()}) for k, v in metadata.items(): if not k.startswith("_"): @@ -174,6 +173,12 @@ def initialize_sqlite_database(sel_wormbase_version, def download_sqlite_database(): - SQLITE_PATH = f"base/cendr.{DATASET_RELEASE}.{WORMBASE_VERSION}.db" - SQLITE_BASENAME = os.path.basename(SQLITE_PATH) - download([f"https://storage.googleapis.com/elegansvariation.org/db/{SQLITE_BASENAME}"], "base") + DATASET_RELEASE = config['DATASET_RELEASE'] + WORMBASE_VERSION = config['WORMBASE_VERSION'] + SQLITE_FILE = f"cendr.{DATASET_RELEASE}.{WORMBASE_VERSION}.db" + blob_path = f"db/{SQLITE_FILE}" + file_path = f"base/{SQLITE_FILE}" + storage_client = storage.Client.from_service_account_json('env_config/client-secret.json') + bucket = storage_client.bucket(GOOGLE_CLOUD_BUCKET) + blob = bucket.blob(blob_path) + blob.download_to_file(open(file_path, 'wb')) diff --git a/base/database/etl_strains.py b/base/database/etl_strains.py index 7f45c04b..d8da4e1c 100644 --- a/base/database/etl_strains.py +++ b/base/database/etl_strains.py @@ -15,6 +15,7 @@ from logzero import logger from base.config import config +NULL_VALS = ["None", "", "NA", None] def elevation_cache(func): """quick and simple cache for lat/lon""" @@ -71,16 +72,18 @@ def fetch_andersen_strains(): WI = get_google_sheet(config['ANDERSEN_LAB_STRAIN_SHEET']) strain_records = WI.get_all_records() # Only take records with a release reported - strain_records = list(filter(lambda x: x.get('release') not in ['', None, 'NA'], strain_records)) + strain_records = list(filter(lambda x: x.get('release') not in NULL_VALS, strain_records)) results = [] for n, record in enumerate(strain_records): record = {k.lower(): v for k, v in record.items()} for k, v in record.items(): # Set NA to None - if v in ["NA", '']: + if v in NULL_VALS: v = None record[k] = v if k in ['sampling_date'] and v: + print('k: ' + k) + print('v: ' + v) record[k] = parser.parse(v) if record['latitude']: @@ -95,12 +98,12 @@ def fetch_andersen_strains(): record["issues"] = record["issues"] == "TRUE" # Set isotype_ref_strain = FALSE if no isotype is assigned. - if record['isotype'] in [None, "", "NA"]: + if record['isotype'] in NULL_VALS: record['isotype_ref_strain'] = False record['wgs_seq'] = False # Skip strains that lack an isotype - if record['isotype'] in [None, "", "NA"] and record['issues'] is False: + if record['isotype'] in NULL_VALS and record['issues'] is False: continue diff --git a/base/extensions.py b/base/extensions.py index ed5042e3..c846102e 100644 --- a/base/extensions.py +++ b/base/extensions.py @@ -5,9 +5,12 @@ from flask_sslify import SSLify from flask_debugtoolbar import DebugToolbarExtension from flask_sqlalchemy import SQLAlchemy +from flask_jwt_extended import JWTManager -sqlalchemy = SQLAlchemy + +sqlalchemy = SQLAlchemy() markdown = Markdown cache = Cache(config={'CACHE_TYPE': 'base.utils.cache.datastore_cache'}) sslify = SSLify debug_toolbar = DebugToolbarExtension +jwt = JWTManager() diff --git a/base/forms.py b/base/forms.py index 09bbe586..33b6653e 100644 --- a/base/forms.py +++ b/base/forms.py @@ -2,20 +2,27 @@ import pandas as pd import numpy as np -from flask_wtf import Form, RecaptchaField +from flask_wtf import FlaskForm, RecaptchaField, Form from wtforms import (StringField, + DateField, + BooleanField, TextAreaField, IntegerField, SelectField, + SelectMultipleField, + widgets, FieldList, HiddenField, RadioField) -from wtforms.validators import Required, Length, Email, DataRequired +from wtforms.fields.simple import PasswordField +from wtforms.validators import Required, Length, Email, DataRequired, EqualTo, Optional from wtforms.validators import ValidationError +from wtforms.fields.html5 import EmailField +from base.constants import PRICES, USER_ROLES, SHIPPING_OPTIONS, PAYMENT_OPTIONS from base.utils.gcloud import query_item -from base.constants import PRICES +from base.models import user_ds from base.views.api.api_strain import query_strains from base.utils.data_utils import is_number, list_duplicates from slugify import slugify @@ -23,24 +30,83 @@ from logzero import logger - -class donation_form(Form): - """ - The donation form - """ - name = StringField('Name', [Required(), Length(min=3, max=100)]) - address = TextAreaField('Address', [Length(min=10, max=200)]) - email = StringField('Email', [Email(), Length(min=3, max=100)]) - total = IntegerField('Donation Amount') - recaptcha = RecaptchaField() +class MultiCheckboxField(SelectMultipleField): + widget = widgets.ListWidget(prefix_label=False) + option_widget = widgets.CheckboxInput() + + +class basic_login_form(FlaskForm): + """ + The simple username/password login form + """ + username = StringField('Username', [Required(), Length(min=5, max=30)]) + password = PasswordField('Password', [Required(), Length(min=5, max=30)]) + recaptcha = RecaptchaField() + + +class markdown_form(FlaskForm): + """ + markdown editing form + """ + title = StringField('Title', [Optional()]) + content = StringField('Content', [Optional()]) + date = DateField('Date (mm-dd-YYYY)', [Optional()], format='%m-%d-%Y') + type = StringField('Type', [Optional()]) + publish = BooleanField('Publish', [Optional()]) + + +class user_register_form(FlaskForm): + """ + Register as a new user with username/password + """ + username = StringField('Username', [Required(), Length(min=5, max=30)]) + full_name = StringField('Full Name', [Required(), Length(min=5, max=50)]) + email = EmailField('Email Address', [Required(), Email(), Length(min=6, max=50)]) + password = PasswordField('Password', [Required(), EqualTo('confirm_password', message='Passwords must match'), Length(min=5, max=30)]) + confirm_password = PasswordField('Confirm Password', [Required(), EqualTo('password', message='Passwords must match'), Length(min=5, max=30)]) + recaptcha = RecaptchaField() + + def validate_username(form, field): + user = user_ds(field.data) + if user._exists: + raise ValidationError("Username already exists") + + +class user_update_form(FlaskForm): + """ + Modifies an existing users profile + """ + full_name = StringField('Full Name', [Required(), Length(min=5, max=50)]) + email = EmailField('Email Address', [Required(), Email(), Length(min=6, max=50)]) + password = PasswordField('Password', [Optional(), EqualTo('confirm_password', message='Passwords must match'), Length(min=5, max=30)]) + confirm_password = PasswordField('Confirm Password', [Optional(), EqualTo('password', message='Passwords must match'), Length(min=5, max=30)]) + + +class admin_edit_user_form(FlaskForm): + """ + A form for one or more roles + """ + roles = MultiCheckboxField('User Roles', choices=USER_ROLES) + + +class data_report_form(FlaskForm): + """ + A form for creating a data release + """ + dataset = SelectField('Release Dataset', validators=[Required()]) + wormbase = StringField('Wormbase Version WS:', validators=[Required()]) + version = SelectField('Report Version', validators=[Required()]) -SHIPPING_OPTIONS = [('UPS', 'UPS'), - ('FEDEX', 'FEDEX'), - ('Flat Rate Shipping', '${} Flat Fee'.format(PRICES.SHIPPING))] - -PAYMENT_OPTIONS = [('check', 'Check'), - ('credit_card', 'Credit Card')] +class donation_form(Form): + """ + The donation form + """ + name = StringField('Name', [Required(), Length(min=3, max=100)]) + address = TextAreaField('Address', [Length(min=10, max=200)]) + email = StringField('Email', [Email(), Length(min=3, max=100)]) + total = IntegerField('Donation Amount') + recaptcha = RecaptchaField() class order_form(Form): diff --git a/base/manage.py b/base/manage.py index eef1eded..85bd73f2 100644 --- a/base/manage.py +++ b/base/manage.py @@ -46,6 +46,10 @@ def update_credentials(): from base.application import create_app app = create_app() app.app_context().push() + + print(app.config['SQLALCHEMY_DATABASE_URI']) + print(app.config['SQLITE_BASENAME']) + click.secho("Zipping env_config", fg='green') zipdir('env_config/', 'env_config.zip') zip_creds = get_item('credential', 'travis-ci-cred') @@ -73,6 +77,9 @@ def decrypt_credentials(): from base.application import create_app app = create_app() app.app_context().push() + + print(app.config['SQLALCHEMY_DATABASE_URI']) + click.secho("Decrypting env_config.zip.enc", fg='green') zip_creds = get_item('credential', 'travis-ci-cred') comm = ['travis', diff --git a/base/models.py b/base/models.py index fd1264cf..e40e4d3d 100644 --- a/base/models.py +++ b/base/models.py @@ -1,4 +1,5 @@ import os +import re import arrow import json import pandas as pd @@ -9,15 +10,16 @@ from flask import Markup, url_for from flask_sqlalchemy import SQLAlchemy from sqlalchemy import or_, func -from logzero import logger +from werkzeug.security import safe_str_cmp -from base.constants import URLS +from base.constants import GOOGLE_CLOUD_BUCKET +from base.extensions import sqlalchemy from base.utils.gcloud import get_item, store_item, query_item, get_cendr_bucket, check_blob from base.utils.aws import get_aws_client +from base.utils.data_utils import hash_password, unique_id from gcloud.datastore.entity import Entity from collections import defaultdict from botocore.exceptions import ClientError -from base.config import DATASET_RELEASE db = SQLAlchemy() @@ -233,9 +235,9 @@ def gs_base_url(self): The URL schema changed from REPORT_VERSION v1 to v2. """ if self.REPORT_VERSION == 'v2': - return f"https://storage.googleapis.com/elegansvariation.org/reports/{self.gs_path}" + return f"https://storage.googleapis.com/{GOOGLE_CLOUD_BUCKET}/reports/{self.gs_path}" elif self.REPORT_VERSION == 'v1': - return f"https://storage.googleapis.com/elegansvariation.org/reports/{self.gs_path}" + return f"https://storage.googleapis.com/{GOOGLE_CLOUD_BUCKET}/reports/{self.gs_path}" def get_gs_as_dataset(self, fname): """ @@ -260,7 +262,7 @@ def list_report_files(self): cendr_bucket = get_cendr_bucket() items = cendr_bucket.list_blobs(prefix=f"reports/{self.gs_path}") - return {os.path.basename(x.name): f"https://storage.googleapis.com/elegansvariation.org/{x.name}" for x in items} + return {os.path.basename(x.name): f"https://storage.googleapis.com/{GOOGLE_CLOUD_BUCKET}/{x.name}" for x in items} def file_url(self, fname): """ @@ -291,8 +293,28 @@ class user_ds(datastore_model): def __init__(self, *args, **kwargs): super(user_ds, self).__init__(*args, **kwargs) + + def set_properties(self, **kwargs): + if 'username' in kwargs: + self.username = kwargs.get('username') + if 'full_name' in kwargs: + self.full_name = kwargs.get('full_name') + if 'password' in kwargs: + self.set_password(kwargs.get('password'), kwargs.get('salt')) + if 'email' in kwargs: + self.set_email(kwargs.get('email')) + if 'roles' in kwargs: + self.roles = kwargs.get('roles') + + def save(self, *args, **kwargs): + now = arrow.utcnow().datetime + if not self._exists: + self.created_on = now + self.modified_on = now + super(user_ds, self).save(*args, **kwargs) + def reports(self): - filters = [('user_id', '=', self.user_id)] + filters = [('user_id', '=', self.name)] # Note this requires a composite index defined very precisely. results = query_item('trait', filters=filters, order=['user_id', '-created_on']) results = sorted(results, key=lambda x: x['created_on'], reverse=True) @@ -302,17 +324,134 @@ def reports(self): # Generate report objects return results_out + def get_all(keys_only=False): + results = query_item('user', keys_only=keys_only) + return results + + def set_password(self, password, salt): + # calling set_password with self.password + if hasattr(self, 'password'): + if (len(password) > 0) and (password != self.password): + self.password = hash_password(password + salt) + else: + self.password = hash_password(password + salt) + + def set_email(self, email): + if hasattr(self, 'email'): + if not safe_str_cmp(email, self.email): + self.email = email + self.email_confirmation_code = unique_id() + self.verified_email = False + else: + self.email = email + self.email_confirmation_code = unique_id() + self.verified_email = False + + def check_password(self, password, salt): + return safe_str_cmp(self.password, hash_password(password + salt)) + + +class markdown_ds(datastore_model): + """ + The Markdown model - for creating and retrieving + documents uploaded to the site + """ + kind = 'markdown' + + def __init__(self, *args, **kwargs): + super(markdown_ds, self).__init__(*args, **kwargs) + + def get_all(keys_only=False): + results = query_item('markdown', keys_only=keys_only) + return results + + def query_by_type(type, keys_only=False): + filters = [('type', '=', type)] + results = query_item('markdown', filters=filters, keys_only=keys_only) + return results + + def save(self, *args, **kwargs): + now = arrow.utcnow().datetime + self.modified_on = now + if not self._exists: + self.created_on = now + super(markdown_ds, self).save(*args, **kwargs) + + +class data_report_ds(datastore_model): + """ + The Data Report model - for creating and retrieving + releases of genomic data + """ + kind = 'data-report' + + def init(self): + self.dataset = '' + self.wormbase = '' + self.version = '' + self.initialized = False + self.published_on = '' + self.publish = False + self.created_on = arrow.utcnow().datetime + self.report_synced_on = '' + self.db_synced_on = '' + + def __init__(self, *args, **kwargs): + super(data_report_ds, self).__init__(*args, **kwargs) + + def get_all(keys_only=False): + results = query_item('data-report', keys_only=keys_only) + return results + + def list_bucket_dirs(): + """ + Lists 'directories' in GCP Bucket 'data_reports' (unique blob prefixes matching date format) + """ + cendr_bucket = get_cendr_bucket() + items = cendr_bucket.list_blobs(prefix=f"data_reports/") + dirs = [] + pattern = r"^(data_reports\/)([0-9]{8})/" + for i in items: + match = re.search(pattern, i.name) + if match: + dir = match.group(2) + if not dir in dirs: + dirs.append(dir) + + return dirs + + def save(self, *args, **kwargs): + now = arrow.utcnow().datetime + self.modified_on = now + super(data_report_ds, self).save(*args, **kwargs) + + +class config_ds(datastore_model): + """ + The Data Config model - Config stored in the cloud + for the site's data sources + """ + kind = 'config' + + def __init__(self, *args, **kwargs): + super(config_ds, self).__init__(*args, **kwargs) + + def save(self, *args, **kwargs): + now = arrow.utcnow().datetime + self.modified_on = now + if not self._exists: + self.created_on = now + super(config_ds, self).save(*args, **kwargs) class DictSerializable(object): - def _asdict(self): - result = {} - for key in self.__mapper__.c.keys(): - result[key] = getattr(self, key) - return result + def _asdict(self): + result = {} + for key in self.__mapper__.c.keys(): + result[key] = getattr(self, key) + return result # --------- Break datastore here ---------# - class Metadata(DictSerializable, db.Model): """ Table for storing information about other tables @@ -374,17 +513,27 @@ def strain_photo_url(self): except AttributeError: return None + def strain_thumbnail_url(self): + # Checks if thumbnail exists and returns URL if it does + try: + return check_blob(f"photos/isolation/{self.strain}.thumb.jpg").public_url + except AttributeError: + return None + def strain_bam_url(self): """ Return bam / bam_index url set """ - + bam_file=self.strain + '.bam' + bai_file=self.strain + '.bam.bai' + bam_download_link = url_for('data.download_bam_url', blob_name=bam_file) + bai_download_link = url_for('data.download_bam_url', blob_name=bai_file) url_set = Markup(f""" - + BAM / - + bai """.strip()) @@ -404,13 +553,16 @@ def isotype_bam_url(self): """ Return bam / bam_index url set """ - + bam_file=self.isotype + '.bam' + bai_file=self.isotype + '.bam.bai' + bam_download_link = url_for('data.download_bam_url', blob_name=bam_file) + bai_download_link = url_for('data.download_bam_url', blob_name=bai_file) url_set = Markup(f""" - + BAM / - + bai """.strip()) diff --git a/base/static/img/logos/wwworms.png b/base/static/img/logos/wwworms.png new file mode 100644 index 0000000000000000000000000000000000000000..7a24c2ae59714d0d00f4f1a85a5b98e43f2b6ba4 GIT binary patch literal 10624 zcmaKS1yozj_AlB7*A|E16nEF)El>)S0)?Up?!g^`I}{1pAO(sPcXzi^DAM9qthmEV zzyH1Wy|>o8FDp4`X0mN&|0a9yIf>NNP{zZi#703u!BbUH&_dqNke>y>6XZ29(yte{XQaULEI4-ak+0d7YZOCCNkF|ofg`1!ez6kM)e4p381E(ce}e@jpR zyPCULJ3*}-9e{r&nwmMfL8Tawmj2Ha?4AB2*1`2(%Y-C|_*a#axV#J46zb@ra za)dg%S~)ra<+X)@Z11ccEF3*tU;IO_t}d?X;0iT$FbAtDNHHKKa9dkji1YI+$;m4S z3-N>Gl=%1*#YBbV_(39k!U_sv^1LGaLjUGfa5Q(b2RlIj&1><$d4>Ng?_Vg`J0UeI zfL*NJ!4^s`j`qNRrYvs#-)#~2uk!xIYw_Q05&W;bJV;}B{yO&maP+^2km&i_{10rA zo&NwI?0`hO3li2e*$q`FD9mT73UWH03;RY`o;u&pJ8B^O^F!UlvncGr!7z4)dfl#O zoqG#Xn(xVu9;;{Q1o4q@YmPtD5>l?%2|rx=-%fQ)KCECM zLellj4&os7#|O+D3=Nq|3zS+@QlADda;5xI+in;MTKVRm?rX~xi53K)K`UxMzoYmR zyBz9lcNjSz&l}zJ;mZHjF5|xD`B7-cH|h=mi6P!tQk|GdUnAI=$9RB>=d;sGbuuC8R!3 z74r67wRvohes1li?0!HEhV6?Qc>O(hm^UZT}3|AkO3v(0QG{Bs<9~RnB;+app5dlqi z@5T0+s!v*W`hD>qktDCzn`*ktOUkD&hcs?}7tO%9%uVc!UNeApl3-8xmkHKbA`IwUct=GAD=lMCDt}PxPR29DTJB`!padrqdTN0ECs5QUd+?J;-p>_wSI-jehzH-}xEc^(_0H8Xd?FER8j$KP zlUU&`s7cM~uosC0&^xF*yb%v{t}t(5KQwW{XNjCOyV!f!u78YQ@IL?bJ?}*; zUhOua5eMeLwdGmc=pzS%P?r-7f6mSJO@W2dN>YlJBcEZ;@8tTJ>|aH)>6+h&BkOzi z-!;uuFU?%ubB4>=jXk{9t6^NL!6@)0p1wW=bb* zgl!UJp(+oM&W!JvM6qMx0BT;he*SHG$ zep|&zv3N=BdC6t>6XAhTl*Z(f*kJ^WIWUUN*Yk7jsONbddd3im-30)Mg+1ZVw{Q&p zSg{%HJZ*cw+83-EYoLc!pE73p^h-s|Rd$F#t||I7S?DT6ZQ(--2E1kBIJ{N`!r)KY){U{CKA zI$YO}p1^`sW#bKb$HeZtC4WAmN;qcU%WgJ&0VgVwZ%$N*FlpoyB)l=YN+&Om`P4HM zwiY_sQwuN<9cfA}WZEZQk_(N=knB!Q?JJ0~bduufYp>8Kds63M+5- zuF6>Tmuo%58}gcuc0HoX`$Klz#(xie=o62 zQt{Px*p+oY`b72>Su%jAadtA#eKvcv(Z@#q_|+Rmfo_|U-7^}St=u?iE(A<=y<0haJE6n&r}K< }ELNogVB)H~81rmff2^*#f1%GvCodN| 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 literal 0 HcmV?d00001 diff --git a/base/static/img/main/variant-calling.png b/base/static/img/main/variant-summary.png similarity index 100% rename from base/static/img/main/variant-calling.png rename to base/static/img/main/variant-summary.png diff --git a/base/static/reports/20160408/pipelines.md b/base/static/reports/20160408/methods.md similarity index 98% rename from base/static/reports/20160408/pipelines.md rename to base/static/reports/20160408/methods.md index 36280ca3..992b9566 100644 --- a/base/static/reports/20160408/pipelines.md +++ b/base/static/reports/20160408/methods.md @@ -1,4 +1,4 @@ -# Methods / Pipelines +# Methods __Note__: These methods operated on sequence data at the isotype level. diff --git a/base/static/reports/20170531/pipelines.md b/base/static/reports/20170531/methods.md similarity index 98% rename from base/static/reports/20170531/pipelines.md rename to base/static/reports/20170531/methods.md index 36280ca3..992b9566 100644 --- a/base/static/reports/20170531/pipelines.md +++ b/base/static/reports/20170531/methods.md @@ -1,4 +1,4 @@ -# Methods / Pipelines +# Methods __Note__: These methods operated on sequence data at the isotype level. diff --git a/base/static/reports/20180527/pipelines.md b/base/static/reports/20180527/methods.md similarity index 98% rename from base/static/reports/20180527/pipelines.md rename to base/static/reports/20180527/methods.md index 36280ca3..992b9566 100644 --- a/base/static/reports/20180527/pipelines.md +++ b/base/static/reports/20180527/methods.md @@ -1,4 +1,4 @@ -# Methods / Pipelines +# Methods __Note__: These methods operated on sequence data at the isotype level. diff --git a/base/static/reports/20200815/pipelines.md b/base/static/reports/20200815/methods.md similarity index 99% rename from base/static/reports/20200815/pipelines.md rename to base/static/reports/20200815/methods.md index 311a92c0..464ca73f 100644 --- a/base/static/reports/20200815/pipelines.md +++ b/base/static/reports/20200815/methods.md @@ -1,4 +1,4 @@ -# Methods / Pipelines +# Methods This tab links to the nextflow pipelines used to process wild isolate sequence data. diff --git a/base/templates/_includes/footer.html b/base/templates/_includes/footer.html index 56c9de39..d0816ee6 100644 --- a/base/templates/_includes/footer.html +++ b/base/templates/_includes/footer.html @@ -37,7 +37,7 @@ })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga'); ga('create', 'UA-24677584-4', 'auto', - { userID: "{{ user.user_id }}"}); + { user: "{{ user }}"}); ga('send', 'pageview'); diff --git a/base/templates/_includes/head.html b/base/templates/_includes/head.html index a33a81a9..a0a09cc6 100644 --- a/base/templates/_includes/head.html +++ b/base/templates/_includes/head.html @@ -24,4 +24,8 @@ +{# Mapbox #} + + + CeNDR | {% if page_title %}{{ page_title }}{% else %}{{ title }}{% endif %}{% if subtitle %} {{ subtitle }}{% endif %} \ No newline at end of file diff --git a/base/templates/_includes/navbar.html b/base/templates/_includes/navbar.html index 5943244c..9a5dde8a 100644 --- a/base/templates/_includes/navbar.html +++ b/base/templates/_includes/navbar.html @@ -34,9 +34,9 @@
  • User
  • +
  • Profile
  • {% else %} {% if request.blueprint != "primary" %}
  • {{ request.blueprint|title }}
  • @@ -88,6 +86,6 @@

    {{ title }} {% if subtitle %} {{ subtitle }}{% endif %}

    - {% if config.DEBUG %}{{ user }}{% endif %} + {% if config.DEBUG %}{{ session }}{% endif %} \ No newline at end of file diff --git a/base/templates/about/about.html b/base/templates/about/about.html index ffc5535f..91114cf7 100644 --- a/base/templates/about/about.html +++ b/base/templates/about/about.html @@ -1,6 +1,5 @@ {% extends "_layouts/default.html" %} - {% block content %}
    @@ -37,10 +36,19 @@

    Global Distribution of wild isolates

    -
    +
    + +
    -
    @@ -78,67 +86,45 @@

    CeNDR Goals

    {% block script %} {% endblock %} \ No newline at end of file diff --git a/base/templates/admin/admin.html b/base/templates/admin/admin.html new file mode 100644 index 00000000..f4451f85 --- /dev/null +++ b/base/templates/admin/admin.html @@ -0,0 +1,6 @@ +{% extends "_layouts/default.html" %} + +{% block content %} + + +{% endblock %} diff --git a/base/templates/admin/data_edit.html b/base/templates/admin/data_edit.html new file mode 100644 index 00000000..2b180ae6 --- /dev/null +++ b/base/templates/admin/data_edit.html @@ -0,0 +1,239 @@ +{% extends "_layouts/default.html" %} + +{% block custom_head %} + + + +{% endblock %} + + +{% block content %} +{% from "macros.html" import render_field %} + + +{% if report.initialized == True %} + +
    +
    +
    + +
    +

    +
    +
    {{ report.kind }} +
    {{ report.name }} +
    +
    +

    +
    +
    +
    +
    +
    + +
    +
    +
    + Dataset: +
    +
    + {{ report.dataset }} +
    +
    + +
    +
    +
    + Wormbase: +
    +
    + {{ report.wormbase }} +
    +
    + +
    +
    +
    + Version: +
    +
    + {{ report.version }} +
    +
    + +
    +
    +
    + Created On: +
    +
    + {{ report.created_on }} +
    +
    + +
    +
    +
    + Report Cloud Location: +
    +
    + data-reports/{{report.dataset}}/ +
    +
    + +
    +
    +
    + Report Last Synced: +
    +
    + {{report.report_synced_on}} +
    +
    + +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + DB Cloud Location: +
    +
    + db/cendr.{{report.dataset}}.{{report.wormbase}}.db +
    +
    + +
    +
    +
    + DB Last Synced: +
    +
    + {{ report.db_synced_on }} +
    +
    + +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + Published On: +
    +
    + {{ report.published_on }} +
    +
    + +
    +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    + +{% else %} + + + {{ form.csrf_token }} + + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + {{ render_field(form.dataset) }} +
    +
    + +
    +
    +
    + {{ render_field(form.wormbase) }} +
    +
    + +
    +
    +
    + {{ render_field(form.version) }} +
    +
    + + + + +{% endif %} + + +{% endblock %} + +{% block script %} + +{% endblock %} diff --git a/base/templates/admin/data_list.html b/base/templates/admin/data_list.html new file mode 100644 index 00000000..f9104ccd --- /dev/null +++ b/base/templates/admin/data_list.html @@ -0,0 +1,56 @@ +{% extends "_layouts/default.html" %} + +{% block content %} + +
    +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + {% for item in items %} + + {% if item %} + + + + + + + {% endif %} + + {% endfor %} + +
    Dataset Wormbase Version Report Version ID Edit Delete
    {{ item.dataset }} {{ item.wormbase }} {{ item.version }} {{ item.key.name }} + + Edit + + + + Delete + +
    +
    +
    + + +{% endblock %} diff --git a/base/templates/admin/google_sheet.html b/base/templates/admin/google_sheet.html new file mode 100644 index 00000000..26252300 --- /dev/null +++ b/base/templates/admin/google_sheet.html @@ -0,0 +1,11 @@ +{% extends "_layouts/default.html" %} + +{% block content %} + + + New Window + + + + +{% endblock %} diff --git a/base/templates/admin/users_edit.html b/base/templates/admin/users_edit.html new file mode 100644 index 00000000..90695488 --- /dev/null +++ b/base/templates/admin/users_edit.html @@ -0,0 +1,51 @@ +{% extends "_layouts/default.html" %} + +{% block content %} +{% from "macros.html" import render_field %} + +
    + {{ form.csrf_token }} + + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + {{ render_field(form.roles) }} +
    +
    + +
    + +
    +
    +
    +
    + + + + + + + + + +
    Username{{ user.username }}
    Full Name{{ user.full_name }}
    Email{{ user.email }}
    Verified{{ user.verified_email }}
    Password************
    Registered{{ user.created }}
    Last Modified{{ user.modified_on }}
    Last Login{{ user.last_login }}
    +
    +
    + +
    + + +{% endblock %} diff --git a/base/templates/admin/users_list.html b/base/templates/admin/users_list.html new file mode 100644 index 00000000..04954f15 --- /dev/null +++ b/base/templates/admin/users_list.html @@ -0,0 +1,53 @@ +{% extends "_layouts/default.html" %} + +{% block content %} + +
    +
    + + + + + + + + + + + + + + + + + {% for user in users %} + + {% if user %} + + + + + + + + + + + {% endif %} + + {% endfor %} + +
    ID Username Full Name Email Verified Email Roles Created On Last Login Edit Delete
    {{ user.key.name }} {{ user.username }} {{ user.full_name }} {{ user.email }} {{ user.verified_email }} {{ user.roles }} {{ user.created_on }} {{ user.last_login }} + + Edit + + + + Delete + +
    +
    +
    + + +{% endblock %} diff --git a/base/templates/alignment.html b/base/templates/alignment.html new file mode 100644 index 00000000..9e7ac99d --- /dev/null +++ b/base/templates/alignment.html @@ -0,0 +1,18 @@ +{% extends "_layouts/default.html" %} + +{% block content %} + +

    Bolded strains are isotype reference strains, and tabbed non-bold face strains are strains within an isotype group but not the isotype reference strain.

    +

    Only strains with whole-genome sequencing data have BAM files for download. If you do not see a strain, check the Strain Issues page. +Some strains have been flagged and removed from distribution and analysis pipelines for a variety of reasons.

    + +{% include('releases/download_tab_strain_v2.html') %} + +{% endblock %} + +{% block script %} + + + +{% endblock %} diff --git a/base/templates/basic_login.html b/base/templates/basic_login.html new file mode 100644 index 00000000..eaec0936 --- /dev/null +++ b/base/templates/basic_login.html @@ -0,0 +1,23 @@ +{% extends "_layouts/default.html" %} +{% block content %} +{% from "macros.html" import render_field %} + +
    +
    +
    +
    +
    Login
    +
    +
    + {{ form.csrf_token }} + {{ render_field(form.username) }} + {{ render_field(form.password) }} + {{ form.recaptcha }} +
    + +
    +
    +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/base/templates/browser.html b/base/templates/browser.html index 1745fdd1..12f2bc6d 100644 --- a/base/templates/browser.html +++ b/base/templates/browser.html @@ -411,8 +411,8 @@

    Variants

    {{ strain }}_bam : { id: "{{ strain.strain }}_bam", name: "{{ strain.strain }}", - url: "//s3.us-east-2.amazonaws.com/elegansvariation.org/bam/strain/{{ strain }}.bam", - indexURL: "//s3.us-east-2.amazonaws.com/elegansvariation.org/bam/strain/{{ strain }}.bam.bai", + url: "//storage.googleapis.com/elegansvariation.org/bam/{{ strain }}.bam", + indexURL: "//storage.googleapis.com/elegansvariation.org/bam/{{ strain }}.bam.bai", order: 100, visibilityWindow: 20000, searchable: false @@ -765,10 +765,6 @@
    Other
    }); - - - - diff --git a/base/templates/data.html b/base/templates/data.html index 391eecb3..c5bb6047 100644 --- a/base/templates/data.html +++ b/base/templates/data.html @@ -35,7 +35,7 @@

    Releases

    {% if int(selected_release) >= 20170531 %}
  • Images
  • {% endif %} -
  • Pipelines / Methods
  • +
  • Methods
  • @@ -57,8 +57,8 @@

    Releases

    -
    - {{ render_markdown(selected_release + "/pipelines.md", directory="base/static/reports") }} +
    + {{ render_markdown(selected_release + "/methods.md", directory="base/static/reports") }}
    {% if int(selected_release) >= 20170531 %} diff --git a/base/templates/data_v2.html b/base/templates/data_v2.html index 0c165a14..950abdc2 100644 --- a/base/templates/data_v2.html +++ b/base/templates/data_v2.html @@ -33,13 +33,13 @@

    Releases

    @@ -81,6 +81,18 @@

    Datasets

    CelegansStrainData.tsv + + Strain Issues + This link contains all strain issues for this release + + + + + Alignment Data + This link contains all alignment data as BAM or BAI files. + + + Soft-Filtered Variants The soft-filtered VCF includes all variants and annotations called by the GATK pipeline. The QC status of each variant (INFO field=FILTER) and genotype (Format Field=FT) is specified by a VCF Field. @@ -195,7 +207,7 @@

    Datasets

    Download BAMs Script You can batch download individual strain BAMs using this script. - download_bams.sh + download_bams.sh @@ -208,41 +220,33 @@

    Datasets

    {# DATASETS #} -
    - -

    Alignment (BAM)

    - -

    Only strains with whole-genome sequencing data have BAM files for download. If you do not see a strain, check the Strain Issues tab. - Some strains have been flagged and removed from distribution and analysis pipelines for a variety of reasons.

    - - {% set show_issues = False %} - {% include('releases/download_tab_strain_v2.html') %} +
    + {{ render_markdown(selected_release + "/methods.md", directory="base/static/reports") }}
    -
    - {% set show_issues = True %} - {% include('releases/download_tab_strain_v2_issues.html') %} -
    - -
    - {{ render_markdown(selected_release + "/pipelines.md", directory="base/static/reports") }} +
    +
    -
    - +
    +
    -
    - +
    +
    - + +
    + +
    +
    @@ -282,12 +286,6 @@

    Alignment (BAM)

    // save the latest tab; use cookies if you like 'em better: sessionStorage.setItem('lastTab', $(this).attr('href')); }); - - // go to the latest tab, if it exists: - var lastTab = sessionStorage.getItem('lastTab'); - if (lastTab) { - $('[href="' + lastTab + '"]').tab('show'); - } }); $('#filter').keydown(function(event) { diff --git a/base/templates/download.html b/base/templates/download.html new file mode 100644 index 00000000..af3d51f9 --- /dev/null +++ b/base/templates/download.html @@ -0,0 +1,20 @@ +{% extends "_layouts/default.html" %} + +{% block content %} + +

    +{{ title }} download will begin shortly... +

    + +{% endblock %} + +{% block script %} + + + +{% endblock %} diff --git a/base/templates/download_script.sh b/base/templates/download_script.sh index d4d3bb58..ebef3945 100644 --- a/base/templates/download_script.sh +++ b/base/templates/download_script.sh @@ -8,18 +8,6 @@ # We recommend using Homebrew (brew.sh) for Unix/Mac OS or Cygwin (cygwin.com) on windows. # -{% for isotype, strains in strain_listing|groupby('isotype') -%} -{% if v2 %} -# {{ isotype }} strains -{% for strain in strains -%} -{% if strain.sequenced -%} -wget https://elegansvariation.org.s3.amazonaws.com/bam/strain/{{ strain }}.bam -wget https://elegansvariation.org.s3.amazonaws.com/bam/strain/{{ strain }}.bam.bai -{% endif -%} -{% endfor -%} - -{%- else %} -wget https://elegansvariation.org.s3.amazonaws.com/bam/{{ strains[0].isotype }}.bam -wget https://elegansvariation.org.s3.amazonaws.com/bam/{{ strains[0].isotype }}.bam.bai -{% endif %} -{%- endfor -%} \ No newline at end of file +{% autoescape off %} +{{script_content}} +{% endautoescape %} diff --git a/base/templates/errors/400.html b/base/templates/errors/400.html new file mode 100644 index 00000000..6e3deb55 --- /dev/null +++ b/base/templates/errors/400.html @@ -0,0 +1,9 @@ +{% extends "_layouts/default.html" %} + +{% block title %}Internal Server Error{% endblock %} + +{% block content %} +

    Internal Server Error :(

    +

    We're having some problems

    +

    Instead, look at all these worms!

    +{% endblock %} \ No newline at end of file diff --git a/base/templates/errors/401.html b/base/templates/errors/401.html new file mode 100644 index 00000000..77b1b5ec --- /dev/null +++ b/base/templates/errors/401.html @@ -0,0 +1,8 @@ +{% extends "_layouts/default.html" %} + +{% block title %}Internal Server Error{% endblock %} + +{% block content %} +

    You don't have permission to do that :(

    +

    Login or register here

    +{% endblock %} \ No newline at end of file diff --git a/base/templates/errors/404.html b/base/templates/errors/404.html index fdc476fd..8a3621eb 100644 --- a/base/templates/errors/404.html +++ b/base/templates/errors/404.html @@ -5,5 +5,5 @@ {% block content %}

    Page Not Found :(

    What you were looking for is just not here.

    -

    Instead, look at all these worms!

    +

    Instead, look at all these worms!

    {% endblock %} \ No newline at end of file diff --git a/base/templates/errors/405.html b/base/templates/errors/405.html index 2c6e4f83..e2929ccc 100644 --- a/base/templates/errors/405.html +++ b/base/templates/errors/405.html @@ -5,5 +5,5 @@ {% block content %}

    Page Not Found :(

    What you were looking for is just not here.

    -

    Instead, look at all these worms!

    +

    Instead, look at all these worms!

    {% endblock %} \ No newline at end of file diff --git a/base/templates/errors/500.html b/base/templates/errors/500.html index f6829a9c..b8ab685d 100644 --- a/base/templates/errors/500.html +++ b/base/templates/errors/500.html @@ -5,5 +5,5 @@ {% block content %}

    Internal Server Error :(

    We're having some problems

    -

    Instead, look at all these worms!

    +

    Instead, look at all these worms!

    {% endblock %} \ No newline at end of file diff --git a/base/templates/errors/generic.html b/base/templates/errors/generic.html index 402d1ebb..6e3deb55 100644 --- a/base/templates/errors/generic.html +++ b/base/templates/errors/generic.html @@ -5,5 +5,5 @@ {% block content %}

    Internal Server Error :(

    We're having some problems

    -

    Instead, look at all these worms!

    +

    Instead, look at all these worms!

    {% endblock %} \ No newline at end of file diff --git a/base/templates/primary/home.html b/base/templates/primary/home.html index 50bfd5c0..809feb87 100644 --- a/base/templates/primary/home.html +++ b/base/templates/primary/home.html @@ -28,16 +28,16 @@
    - +
    -

    Strains

    +

    Strains

    Wild C. elegans isolates and information are available.

    - +

    Data

    Aligned sequence data and variant data for C. elegans wild isolates are available.

    diff --git a/base/templates/releases/download_tab_isotype_v1.html b/base/templates/releases/download_tab_isotype_v1.html index 55d96cb8..93fe47ef 100644 --- a/base/templates/releases/download_tab_isotype_v1.html +++ b/base/templates/releases/download_tab_isotype_v1.html @@ -27,7 +27,7 @@

    Alignment Data

    Learn about alignment data

    Downloading All Alignment Data

    You can download all alignment data using the script below. Before this script will work, you need to download and install wget. We recommend using Homebrew for this installation (Unix/Mac OS), or Cygwin on windows. See the FAQ for details on installing wget.

    -

    download_bams.sh

    +

    download_bams.sh

    Methods

    Information regarding alignment, variant calling, and annotation are available here.

    diff --git a/base/templates/login.html b/base/templates/select_login.html similarity index 56% rename from base/templates/login.html rename to base/templates/select_login.html index 5404d44b..e571ae55 100644 --- a/base/templates/login.html +++ b/base/templates/select_login.html @@ -9,11 +9,15 @@

    Select Login



    + +
    diff --git a/base/templates/strain/global_strain_map.html b/base/templates/strain/global_strain_map.html deleted file mode 100644 index 2b5f113b..00000000 --- a/base/templates/strain/global_strain_map.html +++ /dev/null @@ -1,238 +0,0 @@ -{% extends "_layouts/default.html" %} - - -{% block content %} -
    -
    -
    -
    - Hover over or click a pin to see information about a C. elegans wild isolate -
    {# /text-center #} -
    {# /col-md-8 #} -
    -
    -
    Strain Information
    -
      - -
    • - - - Isotype - - -
    • - -
    • - - - Strain - - -
    • - - -
    • - - - Reference Strain - -
    • -
    • Release
    • -
    • Isolation Date
    • -
    • Latitude, Longitude
    • -
    • Elevation
    • -
    • Landscape
    • -
    • Substrate
    • -
    • Sampled By
    • -
    -
    - Submit Strains

    -
    {# /col-md-4 #} -
    {# /col-md-8 #} -{% endblock %} - - -{% block script %} - - - -{% endblock %} diff --git a/base/templates/strain/isotype.html b/base/templates/strain/isotype.html index 5ddedac7..af2a0ac3 100644 --- a/base/templates/strain/isotype.html +++ b/base/templates/strain/isotype.html @@ -1,7 +1,6 @@ {% extends "_layouts/default.html" %} {% block content %} -
    @@ -26,15 +25,23 @@

    Alternative Names

    {% endif %}
    {# col-md-6 #} -
    - {% if isotype_ref_strain.strain_photo_url() %} -

    Photo

    - - {% endif %} + {% for i in isotype %} + {% if i.strain_photo_url() %} + + + + {% endif %} + {% endfor %}
    {# /col-md-6 #} -
    {# row #} - +
    {# row #}
    {# col-md-8 #}
    @@ -47,7 +54,17 @@

    Photo

    • {% if isotype_ref_strain.latitude %} -
      +
      + +
      {% else %}
      No Location
      {% endif %} @@ -130,7 +147,7 @@

      Photo

    • - Data in this table is for the reference strain. + Data in this table is for the isotype reference strain.
    @@ -138,78 +155,55 @@

    Photo

    {# col-md-4 #}
    - - - {% endblock %} - {% block script %} + {% endblock %} diff --git a/base/templates/strain/strain_list.html b/base/templates/strain/strain_list.html new file mode 100644 index 00000000..8db481d7 --- /dev/null +++ b/base/templates/strain/strain_list.html @@ -0,0 +1,361 @@ +{% extends "_layouts/default.html" %} + + +{% block content %} + +
    +
    + + + + + +
    + + +
    +
    +
    +
    + Hover over or click a pin to see information about a C. elegans wild isolate +
    {# /text-center #} +
    {# /col-md-8 #} + +
    +
    +
    + + Strain Information +
    +
      + +
    • + + Isotype + +
      +
      +
    • + +
    • + + Strain + +
      +
      +
    • + +
    • + + Reference Strain + +
      +
      +
    • + +
    • Release
    • +
    • Isolation Date
    • +
    • Latitude, Longitude
    • +
    • Elevation
    • +
    • Landscape
    • +
    • Substrate
    • +
    • Sampled By
    • +
    +
    {# /panel #} + Submit Strains

    +
    {# /col-md-4 #} +
    {# /tabpanel #} + + +
    +
    +
    +
    + +
    {# /col-md-4 #} +
    {# /row #} +
    + + + + + + + + + + + + + + {% for isotype, strains in strain_listing|groupby('isotype') %} + + + + + + {% if strains[0].previous_names %} + + {% else %} + + {% endif %} + + + {% endfor %} + +
    + # + + + Reference Strain + + + + Isotype + + + + Strains + + + + Alternative Names + + + + Release + +
    {{ loop.index }} + {% set isotype_loop_index = loop.index %} + + + {{ isotype }} + + {{ strains|join(", ") }} + {{ strains[0].previous_names }} + {{ strains[0]['release'] }} +
    +
    {# /row #} +
    {# /tabpanel #} + + +
    + {% include('releases/download_tab_strain_v2_issues.html') %} +
    {# /tabpanel #} + + + {# /tabpanel #} + + +
    {# /tab-content #} +
    {# /col-md-12 #} +
    {# /row #} + +{% endblock %} + + +{% block script %} + + + +{% endblock %} diff --git a/base/templates/strain_issues.html b/base/templates/strain_issues.html new file mode 100644 index 00000000..45cfb9bb --- /dev/null +++ b/base/templates/strain_issues.html @@ -0,0 +1,14 @@ +{% extends "_layouts/default.html" %} + +{% block content %} + +{% include('releases/download_tab_strain_v2_issues.html') %} + +{% endblock %} + +{% block script %} + + + +{% endblock %} diff --git a/base/templates/user.html b/base/templates/user.html deleted file mode 100644 index c9b28d7f..00000000 --- a/base/templates/user.html +++ /dev/null @@ -1,49 +0,0 @@ -{% extends "_layouts/default.html" %} - -{% block content %} -
    -
    - -

    Submitted Mappings

    - - - - - - - - - - - - - - - - {% for report_group, traits in user_obj.reports().items() %} - {% set report = list(traits)[0] %} - - - - - - - - - - {% endfor %} -
    #Report[ Status ] TraitsReport VersionData ReleaseAvailabilityDescriptionSubmitted
    {{ loop.index }}{{ report.report_slug }}{% for trait in list(traits) %} - {{ '[ {:20}]'.format(trait.status) }} - - {% if trait.is_significant %} - {{ trait.trait_name }} - {% else %} - {{ trait.trait_name }} - {% endif %} - -
    - {% endfor %}
    {{ report.REPORT_VERSION }}{{ report.DATASET_RELEASE }}{% if report.is_public %}Public{% else %}Private{% endif %}{{ report.description }}{{ report['created_on'].strftime("%Y-%m-%d %H:%m:%S")|safe }} -
    -
    -
    -{% endblock %} \ No newline at end of file diff --git a/base/templates/user/profile.html b/base/templates/user/profile.html new file mode 100644 index 00000000..b12744b1 --- /dev/null +++ b/base/templates/user/profile.html @@ -0,0 +1,37 @@ +{% extends "_layouts/default.html" %} +{% from "macros.html" import render_field %} + +{% block content %} + +
    +
    +
    +
    + + + + + + + + + + +
    Username{{ user.username }}
    Full Name{{ user.full_name }}
    Email{{ user.email }}
    Verified{{ user.verified_email }}
    Password************
    Roles{{ user.roles }}
    Registered{{ user.created_on }}
    Last Modified{{ user.modified_on }}
    Last Login{{ user.last_login }}
    +
    +
    + +
    +
    +
    +
    + +
    +
    + + +{% endblock %} diff --git a/base/templates/user/register.html b/base/templates/user/register.html new file mode 100644 index 00000000..b1e6d3fc --- /dev/null +++ b/base/templates/user/register.html @@ -0,0 +1,26 @@ +{% extends "_layouts/default.html" %} +{% block content %} +{% from "macros.html" import render_field %} + +
    +
    +
    +
    +
    Register
    +
    +
    + {{ form.csrf_token }} + {{ render_field(form.username) }} + {{ render_field(form.full_name) }} + {{ render_field(form.email) }} + {{ render_field(form.password) }} + {{ render_field(form.confirm_password) }} + {{ form.recaptcha }} +
    + +
    +
    +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/base/templates/user/update.html b/base/templates/user/update.html new file mode 100644 index 00000000..4ac55ec8 --- /dev/null +++ b/base/templates/user/update.html @@ -0,0 +1,33 @@ +{% extends "_layouts/default.html" %} +{% from "macros.html" import render_field %} + +{% block content %} + +
    +
    +
    +
    +
    + {{ form.csrf_token }} + + {{ render_field(form.full_name) }} + {{ render_field(form.email) }} + {{ render_field(form.password) }} + {{ render_field(form.confirm_password) }} +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    + +{% endblock %} diff --git a/base/utils/cache.py b/base/utils/cache.py index 4932ac8a..ee49fafc 100644 --- a/base/utils/cache.py +++ b/base/utils/cache.py @@ -9,7 +9,7 @@ import pickle import base64 from cachelib import BaseCache -from base.utils.gcloud import get_item, store_item +from base.utils.gcloud import get_item, store_item, delete_items_by_query from time import time from base.config import config @@ -23,7 +23,7 @@ def set(self, key, value, timeout=None): expires = time() + timeout try: value = base64.b64encode(pickle.dumps(value)) - store_item('cache', self.key_prefix + "/" + key, value=value, expires=expires, exclude_from_indexes=['value', 'expires']) + store_item('cache', self.key_prefix + "/" + key, value=value, expires=expires, exclude_from_indexes=['value']) return True except: return False @@ -67,3 +67,10 @@ def set_many(self, mapping, timeout): def datastore_cache(app, config, args, kwargs): return DatastoreCache(*args, **kwargs) + + +def delete_expired_cache(): + epoch_time = int(time()) + filters = [("expires", "<", epoch_time)] + num_deleted = delete_items_by_query('cache', filters=filters, projection=['expires']) + return num_deleted \ No newline at end of file diff --git a/base/utils/data_utils.py b/base/utils/data_utils.py index 72587c60..d6c1d219 100644 --- a/base/utils/data_utils.py +++ b/base/utils/data_utils.py @@ -12,19 +12,17 @@ import os import uuid import zipfile -from collections import Counter -from datetime import datetime as dt - import pytz import yaml + +from collections import Counter +from datetime import datetime as dt from flask import g, json from gcloud import storage from logzero import logger - from concurrent.futures import ThreadPoolExecutor from typing import Iterable from urllib.request import urlretrieve - from rich.progress import ( BarColumn, DownloadColumn, @@ -35,6 +33,7 @@ TaskID, ) +from base.constants import GOOGLE_CLOUD_BUCKET, GOOGLE_CLOUD_PROJECT_ID def flatten_dict(d, max_depth=1): def expand(key, value): @@ -57,14 +56,13 @@ def load_yaml(yaml_file): def get_gs(): """ - Gets the elegansvariation.org google storage bucket which + Gets the google storage bucket which stores static assets and report data. """ if not hasattr(g, 'gs'): - g.gs = storage.Client(project='andersen-lab').get_bucket('elegansvariation.org') + g.gs = storage.Client(project=GOOGLE_CLOUD_PROJECT_ID).get_bucket(GOOGLE_CLOUD_BUCKET) return g.gs - class json_encoder(json.JSONEncoder): def default(self, o): if hasattr(o, "to_json"): @@ -102,6 +100,11 @@ def hash_it(object, length=10): return hashlib.sha1(str(object).encode('utf-8')).hexdigest()[0:length] +def hash_password(password): + h = hashlib.md5(password.encode()) + return h.hexdigest() + + def chicago_date(): return dt.now(pytz.timezone("America/Chicago")).date().isoformat() diff --git a/base/utils/gcloud.py b/base/utils/gcloud.py index 2d7988b7..2651dbde 100644 --- a/base/utils/gcloud.py +++ b/base/utils/gcloud.py @@ -1,11 +1,15 @@ import json +import datetime +import googleapiclient.discovery + from flask import g -from base.utils.data_utils import dump_json from gcloud import datastore, storage from logzero import logger -import googleapiclient.discovery from google.oauth2 import service_account +from base.constants import GOOGLE_CLOUD_BUCKET, GOOGLE_CLOUD_PROJECT_ID +from base.utils.data_utils import dump_json + def google_datastore(open=False): """ @@ -14,7 +18,7 @@ def google_datastore(open=False): Args: open - Return the client without storing it in the g object. """ - client = datastore.Client(project='andersen-lab') + client = datastore.Client(project=GOOGLE_CLOUD_PROJECT_ID) if open: return client if not hasattr(g, 'ds'): @@ -29,6 +33,42 @@ def delete_item(item): batch.commit() +def delete_by_ref(kind, id): + ds = google_datastore() + key = ds.key(kind, id) + batch = ds.batch() + batch.delete(key) + batch.commit() + + +def delete_items_by_query(kind, filters=None, projection=()): + """ + Deletes all items that are returned by a query. + Items are deleted in page-sized batches as the results are being returned + Returns the number of items deleted + """ + # filters: + # [("var_name", "=", 1)] + ds = google_datastore() + query = ds.query(kind=kind, projection=projection) + deleted_items = 0 + if filters: + for var, op, val in filters: + query.add_filter(var, op, val) + + query = query.fetch() + while True: + data, more, cursor = query.next_page() + keys = [] + for entity in data: + keys.append(entity.key) + ds.delete_multi(keys) + deleted_items += len(keys) + if more is False: + break + return deleted_items + + def store_item(kind, name, **kwargs): ds = google_datastore() try: @@ -48,7 +88,7 @@ def store_item(kind, name, **kwargs): ds.put(m) -def query_item(kind, filters=None, projection=(), order=None, limit=None): +def query_item(kind, filters=None, projection=(), order=None, limit=None, keys_only=False): """ Filter items from google datastore using a query """ @@ -56,6 +96,8 @@ def query_item(kind, filters=None, projection=(), order=None, limit=None): # [("var_name", "=", 1)] ds = google_datastore() query = ds.query(kind=kind, projection=projection) + if keys_only: + query.keys_only() if order: query.order = order if filters: @@ -101,12 +143,13 @@ def google_storage(open=False): Args: open - Return the client without storing it in the g object. """ - client = storage.Client(project='andersen-lab') + client = storage.Client(project=GOOGLE_CLOUD_PROJECT_ID) if open: - return client - if not hasattr(g, 'gs'): - g.gs = client - return g.gs + return client + if g and not hasattr(g, 'gs'): + g.gs = client + return g.gs + return client def get_cendr_bucket(): @@ -114,7 +157,7 @@ def get_cendr_bucket(): Returns the CeNDR bucket """ gs = google_storage() - return gs.get_bucket("elegansvariation.org") + return gs.get_bucket(GOOGLE_CLOUD_BUCKET) def upload_file(blob, obj, as_string = False): @@ -166,7 +209,7 @@ def list_release_files(prefix): cendr_bucket = get_cendr_bucket() items = cendr_bucket.list_blobs(prefix=prefix) - return list([f"https://storage.googleapis.com/elegansvariation.org/{x.name}" for x in items]) + return list([f"https://storage.googleapis.com/{GOOGLE_CLOUD_BUCKET}/{x.name}" for x in items]) def google_analytics(): @@ -177,3 +220,30 @@ def google_analytics(): scopes=['https://www.googleapis.com/auth/analytics.readonly']) return googleapiclient.discovery.build('analyticsreporting', 'v4', credentials=credentials) + +def generate_download_signed_url_v4(blob_path, expiration=datetime.timedelta(minutes=15)): + """Generates a v4 signed URL for downloading a blob. """ + # blob_name = 'your-object-name' + storage_client = storage.Client.from_service_account_json('env_config/client-secret.json') + bucket = storage_client.bucket(GOOGLE_CLOUD_BUCKET) + blob = bucket.blob(blob_path) + + url = blob.generate_signed_url( + expiration=expiration, + method="GET" + ) + return url + + +def generate_upload_signed_url_v4(blob_name, content_type="application/octet-stream"): + """Generates a v4 signed URL for uploading a blob using HTTP PUT. """ + storage_client = storage.Client.from_service_account_json('env_config/client-secret.json') + bucket = storage_client.bucket(GOOGLE_CLOUD_BUCKET) + blob = bucket.blob(blob_name) + + url = blob.generate_signed_url( + expiration=datetime.timedelta(minutes=15), + method="PUT", + content_type=content_type + ) + return url diff --git a/base/utils/jwt.py b/base/utils/jwt.py new file mode 100644 index 00000000..3d4bcf62 --- /dev/null +++ b/base/utils/jwt.py @@ -0,0 +1,90 @@ +from functools import wraps +from flask import (request, + redirect, + abort, + url_for, + session, + make_response) +from flask_jwt_extended import (create_access_token, + create_refresh_token, + set_access_cookies, + set_refresh_cookies, + unset_jwt_cookies, + unset_access_cookies, + get_jwt, + get_jwt_identity, + get_current_user, + verify_jwt_in_request, + jwt_required) + +from base.models import user_ds +from base.extensions import jwt + +def assign_access_refresh_tokens(id, roles, url, refresh=True): + resp = make_response(redirect(url, 302)) + access_token = create_access_token(identity=str(id), additional_claims={'roles': roles}) + set_access_cookies(resp, access_token) + if refresh: + refresh_token = create_refresh_token(identity=str(id)) + set_refresh_cookies(resp, refresh_token) + session['is_logged_in'] = True + session['is_admin'] = ('admin' in roles) + return resp + + +def unset_jwt(): + resp = make_response(redirect('/', 302)) + session["is_logged_in"] = False + session["is_admin"] = False + unset_jwt_cookies(resp) + return resp + + +def admin_required(): + def wrapper(fn): + @wraps(fn) + def decorator(*args, **kwargs): + verify_jwt_in_request() + claims = get_jwt() + if claims["roles"] and ('admin' in claims["roles"]): + return fn(*args, **kwargs) + else: + return abort(401) + + return decorator + return wrapper + + +@jwt.user_identity_loader +def user_identity_lookup(sub): + return sub + + +@jwt.user_lookup_loader +def user_lookup_callback(_jwt_header, jwt_data): + id = jwt_data["sub"] + return user_ds(id) + + +@jwt.unauthorized_loader +def unauthorized_callback(reason): + return redirect(url_for('auth.choose_login')), 302 + + +@jwt.invalid_token_loader +def invalid_token_callback(callback): + # Invalid Fresh/Non-Fresh Access token in auth header + resp = make_response(redirect(url_for('auth.choose_login'))) + session["is_logged_in"] = False + session["is_admin"] = False + unset_jwt_cookies(resp) + return resp, 302 + + +@jwt.expired_token_loader +def expired_token_callback(_jwt_header, jwt_data): + # Expired auth header + session['login_referrer'] = request.base_url + resp = make_response(redirect(url_for('auth.refresh'))) + unset_access_cookies(resp) + return resp, 302 diff --git a/base/views/about.py b/base/views/about.py index c3577cad..1a8aa03a 100644 --- a/base/views/about.py +++ b/base/views/about.py @@ -20,6 +20,7 @@ from base.utils.email import send_email, DONATE_SUBMISSION_EMAIL from base.utils.data_utils import load_yaml, chicago_date, hash_it from base.utils.plots import time_series_plot +from base.utils.jwt import jwt_required, get_current_user about_bp = Blueprint('about', __name__, @@ -75,6 +76,7 @@ def staff(): @about_bp.route('/donate/', methods=['GET', 'POST']) +@jwt_required(optional=True) def donate(): """ Process donation @@ -83,8 +85,9 @@ def donate(): form = donation_form(request.form) # Autofill email - if session.get('user') and not form.email.data: - form.email.data = session.get('user')['user_email'] + user = get_current_user() + if user and hasattr(user, 'email') and not form.email.data: + form.email.data = user.email if form.validate_on_submit(): # order_number is generated as a unique string @@ -183,8 +186,11 @@ def publications(): List of publications that have referenced CeNDR """ title = "Publications" - req = requests.get( - "https://docs.google.com/spreadsheets/d/1ghJG6E_9YPsHu0H3C9s_yg_-EAjTUYBbO15c3RuePIs/export?format=csv&id=1ghJG6E_9YPsHu0H3C9s_yg_-EAjTUYBbO15c3RuePIs&gid=0") + csv_prefix = config['GOOGLE_SHEET_PREFIX'] + sheet_id = config['CENDR_PUBLICATIONS_STRAIN_SHEET'] + csv_export_suffix = 'export?format=csv&id={}&gid=0'.format(sheet_id) + url = '{}/{}/{}'.format(csv_prefix, sheet_id, csv_export_suffix) + req = requests.get(url) df = pd.read_csv(StringIO(req.content.decode("UTF-8"))) df['pmid'] = df['pmid'].astype(int) df = df.sort_values(by='pmid', ascending=False) diff --git a/base/views/admin/__init__.py b/base/views/admin/__init__.py new file mode 100644 index 00000000..a5e9d041 --- /dev/null +++ b/base/views/admin/__init__.py @@ -0,0 +1,4 @@ +from .admin import admin_bp +from .data import data_admin_bp +from .users import users_bp + diff --git a/base/views/admin/admin.py b/base/views/admin/admin.py new file mode 100644 index 00000000..2dc9ecae --- /dev/null +++ b/base/views/admin/admin.py @@ -0,0 +1,38 @@ +from flask import (render_template, + Blueprint) +from base.config import config +from base.utils.jwt import admin_required + +# Admin blueprint +admin_bp = Blueprint('admin', + __name__, + template_folder='admin') + + +@admin_bp.route('/') +@admin_required() +def admin(): + VARS = {"title": "Admin"} + return render_template('admin/admin.html', **VARS) + + +@admin_bp.route('/strain_sheet/') +@admin_required() +def admin_strain_sheet(): + title = "Andersen Lab Strain Sheet" + id = config['ANDERSEN_LAB_STRAIN_SHEET'] + prefix = config['GOOGLE_SHEET_PREFIX'] + sheet_url = '{}/{}'.format(prefix, id) + return render_template('admin/google_sheet.html', **locals()) + + + +@admin_bp.route('/publications/') +@admin_required() +def admin_publications_sheet(): + title = "CeNDR Publications Sheet" + id = config['CENDR_PUBLICATIONS_STRAIN_SHEET'] + prefix = config['GOOGLE_SHEET_PREFIX'] + sheet_url = '{}/{}'.format(prefix, id) + return render_template('admin/google_sheet.html', **locals()) + diff --git a/base/views/admin/data.py b/base/views/admin/data.py new file mode 100644 index 00000000..964f7965 --- /dev/null +++ b/base/views/admin/data.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Author: Sam Wachspress + +Data Release administration + +""" +import arrow + +from flask import abort, flash, request, render_template, Blueprint, redirect, url_for + +from base.constants import REPORT_V2_FILE_LIST, REPORT_V1_FILE_LIST, REPORT_VERSIONS +from base.config import config +from base.models import data_report_ds +from base.forms import data_report_form +from base.utils.jwt import get_jwt, admin_required, get_current_user +from base.utils.data_utils import unique_id +from base.utils.gcloud import delete_by_ref + + +data_admin_bp = Blueprint('data_admin', + __name__, + template_folder='admin') + + +cloud_config = config['cloud_config'] + +@data_admin_bp.route('/', methods=["GET"]) +@data_admin_bp.route('/', methods=["GET"]) +@admin_required() +def data_admin(id=None): + if id is None: + title = 'All' + items = data_report_ds.get_all() + else: + return redirect(url_for('data_admin.data_edit', id=id)) + + return render_template('admin/data_list.html', **locals()) + + +@data_admin_bp.route('/create/', methods=["GET"]) +@admin_required() +def data_create(id=None): + user = get_current_user() + id = unique_id() + report = data_report_ds(id) + report.init() + report.save() + return redirect(url_for('data_admin.data_edit', id=id)) + + +@data_admin_bp.route('//edit/', methods=["GET", "POST"]) +@admin_required() +def data_edit(id=None): + if id is None: + flash('Error: No Report ID Provided!') + abort(500) + + title = "Edit" + jwt_csrf_token = (get_jwt() or {}).get("csrf") + form = data_report_form(request.form) + + report = data_report_ds(id) + if not report._exists: + flash(f"Error: Report {id} does not exist!") + abort(500) + + # Get content of cloud bucket + report_dirs = [''] + data_report_ds.list_bucket_dirs() + form.dataset.choices = [(f, f) for f in report_dirs] + form.version.choices = [(v, v) for v in REPORT_VERSIONS] + + if request.method == 'GET': + form.dataset.data = report.dataset if hasattr(report, 'dataset') else '' + form.wormbase.data = report.wormbase if hasattr(report, 'wormbase') else '' + form.version.data = report.version if hasattr(report, 'version') else '' + + if request.method == 'POST' and form.validate(): + # if changes then re-sync + report.dataset = request.form.get('dataset') + report.wormbase = request.form.get('wormbase') + report.version = request.form.get('version') + report.initialized = True + report.save() + return redirect(url_for('data_admin.data_admin')) + return render_template('admin/data_edit.html', **locals()) + + +@data_admin_bp.route('//delete/', methods=["GET"]) +@admin_required() +def data_delete(id=None): + if id is None: + flash('Error: No Report ID Provided!') + abort(500) + + report = data_report_ds(id) + if not report._exists: + flash(f"Error: Report {id} does not exist!") + abort(500) + + if hasattr(report, 'dataset'): + dataset = str(report.dataset) + cloud_config.create_backup() + cloud_config.remove_release(dataset) + cloud_config.remove_release_files(dataset) + props = cloud_config.get_properties() + config.update(props) + delete_by_ref('data-report', id) + + return redirect(url_for('data_admin.data_admin')) + + +@data_admin_bp.route('//sync-report', methods=["GET"]) +@admin_required() +def data_sync_report(id=None): + """ + Fetches static content from a google cloud bucket and copies it locally to serve + """ + if id is None: + flash('Error: No Report ID Provided!') + abort(500) + + report = data_report_ds(id) + if not report._exists or not hasattr(report, 'dataset'): + flash(f"Error: Report {id} does not exist!") + abort(500) + + dataset = report.dataset if hasattr(report, 'dataset') else None + wormbase = report.wormbase if hasattr(report, 'wormbase') else None + version = report.version if hasattr(report, 'version') else None + if dataset is None or wormbase is None or version is None: + flash(f"Error: Report {id} is missing required properties!") + abort(500) + + files = [] + if version == 'v1': + files = REPORT_V1_FILE_LIST + elif version == 'v2': + files = REPORT_V2_FILE_LIST + + result = cloud_config.get_release_files(dataset, files, refresh=True) + if not result is None: + now = arrow.utcnow().datetime + report.report_synced_on = now + report.save() + else: + report.save() + flash(f"Failed to sync report: {id}!") + abort(500) + + return redirect(url_for('data_admin.data_admin')) + + +@data_admin_bp.route('//sync-db', methods=["GET"]) +@admin_required() +def data_sync_db(id=None): + """ + Fetches sqlite db file from google cloud bucket and copies it locally to serve + """ + if id is None: + flash('Error: No Report ID Provided!') + abort(500) + + report = data_report_ds(id) + if not report._exists or not hasattr(report, 'dataset'): + flash(f"Error: Report {id} does not exist!") + abort(500) + + dataset = report.dataset if hasattr(report, 'dataset') else None + wormbase = report.wormbase if hasattr(report, 'wormbase') else None + if dataset is None or wormbase is None: + flash(f"Error: Report {id} is missing required properties!") + abort(500) + + result = cloud_config.get_release_db(dataset, wormbase, refresh=True) + if not result is None: + now = arrow.utcnow().datetime + report.db_synced_on = now + report.save() + else: + report.save() + flash(f"Failed to sync report: {id}!") + abort(500) + + return redirect(url_for('data_admin.data_admin')) + + +@data_admin_bp.route('//hide', methods=["GET"]) +@admin_required() +def data_hide_report(id=None): + """ Updates the cloud config to hide the release """ + if id is None: + flash('Error: No Report ID Provided!') + abort(500) + + report = data_report_ds(id) + if not report._exists or not hasattr(report, 'dataset'): + flash(f"Error: Report {id} does not exist or is missing required properties!") + abort(500) + + # update the config + dataset = report.dataset + cloud_config.create_backup() + cloud_config.remove_release(dataset) + props = cloud_config.get_properties() + config.update(props) + + # update the datastore report object + report.publish = False + report.published_on = '' + report.save() + + return redirect(url_for('data_admin.data_admin')) + + +@data_admin_bp.route('//publish', methods=["GET"]) +@admin_required() +def data_publish_report(id=None): + """ Updates the cloud config to recognize the release """ + if id is None: + flash('Error: No Report ID Provided!') + abort(500) + + report = data_report_ds(id) + if not report._exists: + flash(f"Error: Report {id} does not exist!") + abort(500) + + dataset = report.dataset if hasattr(report, 'dataset') else None + wormbase = report.wormbase if hasattr(report, 'wormbase') else None + version = report.version if hasattr(report, 'version') else None + if dataset is None or wormbase is None or version is None: + flash(f"Error: Report {id} is missing required properties!") + abort(500) + + cloud_config.create_backup() + cloud_config.add_release(dataset, wormbase, version) + props = cloud_config.get_properties() + config.update(props) + + # update the datastore report object + report.publish = True + report.published_on = arrow.utcnow().datetime + report.save() + + return redirect(url_for('data_admin.data_admin')) diff --git a/base/views/admin/users.py b/base/views/admin/users.py new file mode 100644 index 00000000..2014d8cb --- /dev/null +++ b/base/views/admin/users.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Author: Sam Wachspress + +User administration + +""" +import arrow +from flask import request, render_template, Blueprint, redirect, url_for + +from base.models import user_ds +from base.forms import admin_edit_user_form +from base.utils.jwt import jwt_required, get_jwt, admin_required +from base.utils.gcloud import delete_by_ref + + +users_bp = Blueprint('users', + __name__, + template_folder='admin') + + +@users_bp.route('/', methods=["GET"]) +@users_bp.route('/', methods=["GET"]) +@admin_required() +def users(id=None): + if id is None: + title = 'All' + users = user_ds.get_all() + return render_template('admin/users_list.html', **locals()) + else: + return redirect(url_for('users.users_edit'), id=id) + + +@users_bp.route('//edit/', methods=["GET", "POST"]) +@admin_required() +def users_edit(id=None): + if id is None: + # todo: fix redirect + return render_template('500.html'), 500 + + title = "Edit" + jwt_csrf_token = (get_jwt() or {}).get("csrf") + form = admin_edit_user_form(request.form) + user = user_ds(id) + + if request.method == 'GET': + form.roles.data = user.roles if hasattr(user, 'roles') else ['user'] + + if request.method == 'POST' and form.validate(): + user.roles = request.form.getlist('roles') + user.modified = arrow.utcnow().datetime + user.save() + return redirect(url_for('users.users')) + + # todo: fix redirect here + return render_template('admin/users_edit.html', **locals()) + + +@users_bp.route('//delete', methods=["GET"]) +@admin_required() +def users_delete(id=None): + if id is None: + # todo: fix redirect + return render_template('500.html'), 500 + + delete_by_ref('user', id) + return redirect(url_for('users.users')) diff --git a/base/views/api/api_popgen.py b/base/views/api/api_popgen.py index 4ead6b6a..f767b5ac 100644 --- a/base/views/api/api_popgen.py +++ b/base/views/api/api_popgen.py @@ -1,17 +1,18 @@ from flask import jsonify -from base.application import app from subprocess import Popen, PIPE -from base.constants import DATASET_RELEASE + +from base.constants import GOOGLE_CLOUD_BUCKET +from base.config import config +from base.application import app from base.views.api.api_variant import variant_query from base.views.api.api_strain import get_isotypes from base.utils.decorators import jsonify_request -from logzero import logger @app.route('/api/popgen/tajima///') @app.route('/api/popgen/tajima////') @jsonify_request -def tajima(chrom, start, end, release = DATASET_RELEASE): +def tajima(chrom, start, end, release = config['DATASET_RELEASE']): """ Args: chrom @@ -30,7 +31,7 @@ def tajima(chrom, start, end, release = DATASET_RELEASE): # No tajima bedfile exists for 20160408 - so use next version. if release < 20170531: release = 20170531 - url = f"http://storage.googleapis.com/elegansvariation.org/releases/{release}/popgen/WI.{release}.tajima.bed.gz" + url = f"http://storage.googleapis.com/{GOOGLE_CLOUD_BUCKET}/releases/{release}/popgen/WI.{release}.tajima.bed.gz" comm = ['tabix', url, "{chrom}:{start}-{end}".format(**locals())] out, err = Popen(comm, stdout=PIPE, stderr=PIPE).communicate() @@ -46,13 +47,15 @@ def tajima(chrom, start, end, release = DATASET_RELEASE): @app.route('/api/popgen/gt//') @app.route('/api/popgen/gt///') @jsonify_request -def get_allele_geo(chrom, pos, isotypes=None, release = DATASET_RELEASE): +def get_allele_geo(chrom, pos, isotypes=None, release=None): """ Args: chrom pos isotypes """ + if release == None: + release = config['DATASET_RELEASE'] try: variant = variant_query(f"{chrom}:{pos}-{pos+1}", list_all_strains=True, release=release)[0] except IndexError: diff --git a/base/views/api/api_strain.py b/base/views/api/api_strain.py index fc0dbf43..25dcd459 100644 --- a/base/views/api/api_strain.py +++ b/base/views/api/api_strain.py @@ -31,7 +31,7 @@ def search_strains(query): @api_strain_bp.route('/strain/') @api_strain_bp.route('/strain/isotype/') @jsonify_request -def query_strains(strain_name=None, isotype_name=None, release=None, all_strain_names=False, resolve_isotype=False, issues=False): +def query_strains(strain_name=None, isotype_name=None, release=None, all_strain_names=False, resolve_isotype=False, issues=False, is_sequenced=False): """ Return the full strain database set @@ -55,6 +55,9 @@ def query_strains(strain_name=None, isotype_name=None, release=None, all_strain_ else: query = query + if is_sequenced is True: + query = query.filter(Strain.sequenced == True) + if issues is False: query = query.filter(Strain.issues == False) query = query.filter(Strain.isotype != None) diff --git a/base/views/api/api_variant.py b/base/views/api/api_variant.py index d5c9ce24..78362cfe 100644 --- a/base/views/api/api_variant.py +++ b/base/views/api/api_variant.py @@ -5,18 +5,21 @@ """ import re import pickle + from cyvcf2 import VCF from flask import request, Response from tempfile import NamedTemporaryFile from subprocess import Popen, PIPE from collections import OrderedDict -from base.utils.decorators import jsonify_request -from base.config import config from collections import Counter from logzero import logger - from flask import Blueprint +from base.constants import GOOGLE_CLOUD_BUCKET +from base.config import config +from base.utils.decorators import jsonify_request + + api_variant_bp = Blueprint('api_variant', __name__, template_folder='api') @@ -42,7 +45,7 @@ def get_vcf(release=config["DATASET_RELEASE"], filter_type="hard"): - return "http://storage.googleapis.com/elegansvariation.org/releases/{release}/variation/WI.{release}.{filter_type}-filter.isotype.vcf.gz".format(release=release, filter_type=filter_type) + return f"http://storage.googleapis.com/{GOOGLE_CLOUD_BUCKET}/releases/{release}/variation/WI.{release}.{filter_type}-filter.isotype.vcf.gz" gt_set_keys = ["SAMPLE", "GT", "FT", "TGT"] diff --git a/base/views/auth/__init__.py b/base/views/auth/__init__.py new file mode 100644 index 00000000..ecef2883 --- /dev/null +++ b/base/views/auth/__init__.py @@ -0,0 +1,5 @@ +from .auth import auth_bp +from .saml import saml_bp +from .oauth import google_bp + + diff --git a/base/views/auth/auth.py b/base/views/auth/auth.py new file mode 100644 index 00000000..08f2c860 --- /dev/null +++ b/base/views/auth/auth.py @@ -0,0 +1,81 @@ +import os +import arrow +from flask import (abort, + redirect, + render_template, + session, + request, + make_response, + flash, + jsonify, + Blueprint) +from slugify import slugify + +from base.models import user_ds +from base.forms import basic_login_form +from base.utils.jwt import (create_access_token, + set_access_cookies, + get_jwt_identity, + jwt_required, + assign_access_refresh_tokens, + unset_jwt) +from base.config import config + +auth_bp = Blueprint('auth', + __name__, + template_folder='') + + +@auth_bp.route('/refresh', methods=['GET']) +@jwt_required(refresh=True) +def refresh(): + ''' Refreshing expired Access token ''' + username = get_jwt_identity() + user = user_ds(username) + if user._exists: + referrer = session.get('login_referrer', '/') + return assign_access_refresh_tokens(username, user.roles, referrer, refresh=False) + + return abort(401) + + +@auth_bp.route("/login/select", methods=['GET']) +def choose_login(error=None): + # Relax scope for Google + referrer = session.get("login_referrer") or "" + if not referrer.endswith("/login/select"): + session["login_referrer"] = request.referrer + os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = "true" + VARS = {'page_title': 'Choose Login'} + if error: + flash(error, 'danger') + return render_template('select_login.html', **VARS) + + +@auth_bp.route("/login/basic", methods=["GET", "POST"]) +def basic_login(): + page_title = "Login" + form = basic_login_form(request.form) + if request.method == 'POST' and form.validate(): + username = slugify(request.form.get("username")) + password = request.form.get("password") + user = user_ds(username) + if user._exists: + if user.check_password(password, config['PASSWORD_SALT']): + user.last_login = arrow.utcnow().datetime + user.save() + return assign_access_refresh_tokens(username, user.roles, '/') + flash('Wrong username or password', 'error') + return redirect(request.referrer) + return render_template('basic_login.html', **locals()) + + +@auth_bp.route('/logout') +def logout(): + """ + Logs the user out. + """ + session.clear() + resp = unset_jwt() + flash("Successfully logged out", "success") + return resp diff --git a/base/views/auth/oauth.py b/base/views/auth/oauth.py new file mode 100644 index 00000000..49e1f2ce --- /dev/null +++ b/base/views/auth/oauth.py @@ -0,0 +1,55 @@ +import arrow +from flask import (redirect, + url_for, + session, + flash) +from flask_dance.contrib.google import make_google_blueprint, google +from flask_dance.consumer import oauth_authorized + +from base.config import config +from base.models import user_ds +from base.utils.data_utils import unique_id +from base.utils.jwt import assign_access_refresh_tokens + + +google_bp = make_google_blueprint(client_id=config['GOOGLE_CLIENT_ID'], + client_secret=config['GOOGLE_CLIENT_SECRET'], + scope=["https://www.googleapis.com/auth/userinfo.profile", + "https://www.googleapis.com/auth/userinfo.email", + "openid"], + offline=True) + + +def create_or_update_google_user(user_info): + # Get up-to-date properties + user_id = user_info['google']['id'] + user_email = user_info['google']['email'] + user_name = user_info['google']['name'] + user = user_ds(user_id) + now = arrow.utcnow().datetime + if not user._exists: + user.roles = ['user'] + user.created_on = now + + # Save updated properties + user.modified_on = now + user.last_login = now + user.set_properties(username=user_email, password=unique_id(), salt=config['PASSWORD_SALT'], full_name=user_name, email=user_email.lower()) + user.verified_email = True; + user.save() + return user + + +@oauth_authorized.connect +def authorized(blueprint, token): + if not google.authorized: + flash("Error logging in!") + return redirect(url_for("auth.choose_login")) + + user_info = google.get("/oauth2/v2/userinfo") + assert user_info.ok + user_info = {'google': user_info.json()} + user = create_or_update_google_user(user_info) + + flash("Successfully logged in!", 'success') + return assign_access_refresh_tokens(user.name, user.roles, session.get("login_referrer")) diff --git a/base/views/auth/saml.py b/base/views/auth/saml.py new file mode 100644 index 00000000..9daf5ac9 --- /dev/null +++ b/base/views/auth/saml.py @@ -0,0 +1,173 @@ +import arrow + +from flask import (redirect, + url_for, + session, + request, + make_response, + flash, + Blueprint) +from slugify import slugify +from base.models import user_ds +from base.utils.jwt import assign_access_refresh_tokens + +from urllib.parse import urlparse +from onelogin.saml2.auth import OneLogin_Saml2_Auth +from onelogin.saml2.settings import OneLogin_Saml2_Settings +from onelogin.saml2.utils import OneLogin_Saml2_Utils + + +saml_bp = Blueprint('saml', + __name__, + template_folder='') + +pd = { + 'eduPersonPrincipalName': 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6', + 'mail': 'urn:oid:0.9.2342.19200300.100.1.3', + 'o': 'urn:oid:2.5.4.10', + 'displayName': 'urn:oid:2.16.840.1.113730.3.1.241', + 'uid': 'urn:oid:0.9.2342.19200300.100.1.1', +} + + +def get_or_register_user(saml_auth): + try: + attributes = saml_auth.get_attributes() + username = attributes[pd.get('eduPersonPrincipalName')] + username = username[0] + id = slugify(username) + if id is None: + return None + + user = user_ds(id) + now = arrow.utcnow().datetime + if not user._exists: + user.created_on = now + user.roles = ['user'] + + user.username = username + user.email = attributes[pd['mail']] + user.verified_email = True + user.o = attributes[pd['o']] if hasattr(attributes, pd['o']) else [''] + user.full_name = attributes[pd['displayName']] if hasattr(attributes, pd['displayName']) else [''] + user.uid = attributes[pd['uid']] if hasattr(attributes, pd['uid']) else [''] + + # properties are initially arrays + user.email = user.email[0] + user.o = user.o[0] + user.full_name = user.full_name[0] + user.uid = user.uid[0] + + # store the rest of the saml info + user.samlUserdata = attributes + user.samlNameId = saml_auth.get_nameid() + user.samlNameIdFormat = saml_auth.get_nameid_format() + user.samlNameIdNameQualifier = saml_auth.get_nameid_nq() + user.samlNameIdSPNameQualifier = saml_auth.get_nameid_spnq() + user.samlSessionIndex = saml_auth.get_session_index() + user.last_login = now + user.save() + return user + except: + return None + +def init_saml_auth(req): + """ + Loads the saml config from settings.json + to generate the SAML XML + """ + saml_auth = OneLogin_Saml2_Auth(req, custom_base_path=f"env_config/saml") + return saml_auth + + +def prepare_flask_request(request): + """ + Preprocesser for request data + """ + # If server is behind proxys or balancers use the HTTP_X_FORWARDED fields + url_data = urlparse(request.url) + return { + 'https': 'on' if request.scheme == 'https' else 'off', + 'http_host': request.host, + 'server_port': url_data.port, + 'script_name': request.path, + 'get_data': request.args.copy(), + # Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144 + # 'lowercase_urlencoding': True, + 'post_data': request.form.copy() + } + + +@saml_bp.route('/sso2', methods=['GET', 'POST']) +def saml_sso2(): + """ + Single Sign On (2) route for SAML which includes user attributes + """ + print('SSO2') + req = prepare_flask_request(request) + saml_auth = init_saml_auth(req) + return_to = session.get("login_referrer") + return redirect(saml_auth.login(return_to)) + + +@saml_bp.route('/acs', methods=['GET', 'POST']) +def saml_acs(): + """ + Assertion Consumer Service route for SAML + """ + req = prepare_flask_request(request) + saml_auth = init_saml_auth(req) + settings = saml_auth.get_settings() + errors = [] + error_reason = None + is_auth = False + + request_id = None + if 'AuthNRequestID' in session: + request_id = session['AuthNRequestID'] + + saml_auth.process_response(request_id=request_id) + errors = saml_auth.get_errors() + is_auth = saml_auth.is_authenticated() + + if (len(errors) == 0) and is_auth: + if 'AuthNRequestID' in session: + del session['AuthNRequestID'] + + user = get_or_register_user(saml_auth) + if user is None: + flash('Failed to retrieve attributes from Identity Provider', 'error') + return redirect(url_for('auth.logout')) + + self_url = OneLogin_Saml2_Utils.get_self_url(req) + referrer = session.get("login_referrer",'/') + if 'RelayState' in request.form and self_url != request.form['RelayState']: + referrer = request.form['RelayState'] + + return assign_access_refresh_tokens(user.name, user.roles, referrer) + + elif settings.is_debug_active(): + error_reason = saml_auth.get_last_error_reason() + + flash('Wrong username or password', 'error') + return redirect(request.referrer) + + +@saml_bp.route('/metadata/') +def saml_metadata(): + """ + Generates metadata.xml for SAML Service Provider from settings.json + """ + req = prepare_flask_request(request) + saml_auth = init_saml_auth(req) + settings = saml_auth.get_settings() + metadata = settings.get_sp_metadata() + errors = settings.validate_metadata(metadata) + + if len(errors) == 0: + resp = make_response(metadata, 200) + resp.headers['Content-Type'] = 'text/xml' + else: + resp = make_response(', '.join(errors), 500) + return resp + diff --git a/base/views/data.py b/base/views/data.py index 841e8cc7..ead1dfcb 100644 --- a/base/views/data.py +++ b/base/views/data.py @@ -1,12 +1,15 @@ import requests + +from datetime import timedelta +from base.utils.jwt import jwt_required from simplejson.errors import JSONDecodeError -from flask import make_response -from flask import render_template -from flask import Blueprint -from base.views.api.api_strain import get_isotypes, query_strains +from flask import make_response, render_template, Blueprint + from base.config import config +from base.constants import GOOGLE_CLOUD_BUCKET +from base.views.api.api_strain import get_isotypes, query_strains from base.models import Strain -from base.utils.gcloud import list_release_files +from base.utils.gcloud import list_release_files, generate_download_signed_url_v4 data_bp = Blueprint('data', __name__, @@ -15,23 +18,25 @@ # ============= # # Data Page # # ============= # + @data_bp.route('/release/latest') @data_bp.route('/release/') -@data_bp.route('/release/') -def data(selected_release=config["DATASET_RELEASE"]): +def data(selected_release=None): """ Default data page - lists available releases. """ + if selected_release is None: + selected_release = config['DATASET_RELEASE'] + # Pre-2020 releases used BAMs grouped by isotype. if int(selected_release) < 20200101: return data_v01(selected_release) # Post-2020 releases keep strain-level bams separate. - title = "Releases" + title = "Genomic Data" sub_page = selected_release strain_listing = query_strains(release=selected_release) - strain_listing_issues = query_strains(release=selected_release, issues=True) release_summary = Strain.release_summary(selected_release) RELEASES = config["RELEASES"] DATASET_RELEASE, WORMBASE_VERSION = list(filter(lambda x: x[0] == selected_release, RELEASES))[0] @@ -41,11 +46,11 @@ def data(selected_release=config["DATASET_RELEASE"]): def data_v01(selected_release): # Legacy releases (Pre 20200101) - title = "Releases" + title = "Genomic Data" subtitle = selected_release strain_listing = query_strains(release=selected_release) # Fetch variant data - url = "https://storage.googleapis.com/elegansvariation.org/releases/{selected_release}/multiqc_bcftools_stats.json".format(selected_release=selected_release) + url = f"https://storage.googleapis.com/{GOOGLE_CLOUD_BUCKET}/releases/{selected_release}/multiqc_bcftools_stats.json" try: vcf_summary = requests.get(url).json() except JSONDecodeError: @@ -59,34 +64,104 @@ def data_v01(selected_release): wormbase_genome_version = dict(config["RELEASES"])[selected_release] return render_template('data.html', **locals()) +# ======================= # +# Alignment Data Page # +# ======================= # +@data_bp.route('/release/latest/alignment') +@data_bp.route('/release//alignment') +def alignment_data(selected_release=None): + """ + Alignment data page + """ + if selected_release is None: + selected_release = config['DATASET_RELEASE'] + # Pre-2020 releases don't have data organized the same way + if int(selected_release) < 20200101: + return + + # Post-2020 releases + title = "Alignment Data" + strain_listing = query_strains(release=selected_release) + RELEASES = config["RELEASES"] + DATASET_RELEASE, WORMBASE_VERSION = list(filter(lambda x: x[0] == selected_release, RELEASES))[0] + REPORTS = ["alignment"] + return render_template('alignment.html', **locals()) + +# =========================== # +# Strain Issues Data Page # +# =========================== # +@data_bp.route('/release/latest/strain_issues') +@data_bp.route('/release//strain_issues') +def strain_issues(selected_release=None): + """ + Strain Issues page + """ + if selected_release is None: + selected_release = config['DATASET_RELEASE'] + # Pre-2020 releases don't have data organized the same way + if int(selected_release) < 20200101: + return + + # Post-2020 releases + title = "Strain Issues" + strain_listing_issues = query_strains(release=selected_release, issues=True) + RELEASES = config["RELEASES"] + DATASET_RELEASE, WORMBASE_VERSION = list(filter(lambda x: x[0] == selected_release, RELEASES))[0] + return render_template('strain_issues.html', **locals()) # =================== # # Download Script # # =================== # +@data_bp.route('/release//download/download_isotype_bams.sh') +@jwt_required() +def download_script(selected_release): + script_content = generate_bam_download_script(release=selected_release) + download_page = render_template('download_script.sh', **locals()) + response = make_response(download_page) + response.headers["Content-Type"] = "text/plain" + return response + + +@data_bp.route('/release/latest/download/download_strain_bams.sh') +@data_bp.route('/release//download/download_strain_bams.sh') +@jwt_required() +def download_script_strain_v2(selected_release=None): + if selected_release is None: + selected_release = config['DATASET_RELEASE'] + script_content = generate_bam_download_script(release=selected_release) + download_page = render_template('download_script.sh', **locals()) + response = make_response(download_page) + response.headers["Content-Type"] = "text/plain" + return response -@data_bp.route('/download/download_isotype_bams.sh') -def download_script(): - strain_listing = query_strains(release=config["DATASET_RELEASE"]) - download_page = render_template('download_script.sh', **locals()) - response = make_response(download_page) - response.headers["Content-Type"] = "text/plain" - return response -@data_bp.route('/download/download_strain_bams.sh') -def download_script_strain_v2(): - v2 = True - strain_listing = query_strains(release=config["DATASET_RELEASE"]) - download_page = render_template('download_script.sh', **locals()) - response = make_response(download_page) - response.headers["Content-Type"] = "text/plain" - return response +@data_bp.route('/download/files/') +@jwt_required() +def download_bam_url(blob_name=''): + title = blob_name + blob_path = 'bam/' + blob_name + signed_download_url = generate_download_signed_url_v4(blob_path) + return render_template('download.html', **locals()) + + +def generate_bam_download_script(release): + ''' Generates signed downloads urls for every sequenced strain and creates a script to download them ''' + script_content = '' + expiration = timedelta(days=7) + strain_listing = query_strains(release=release, is_sequenced=True) + for strain in strain_listing: + bam_path = 'bam/{}.bam'.format(strain) + bai_path = 'bam/{}.bam.bai'.format(strain) + script_content += '\n\n# Strain: {}'.format(strain) + script_content += '\nwget "{}"'.format(generate_download_signed_url_v4(bam_path, expiration=expiration)) + script_content += '\nwget "{}"'.format(generate_download_signed_url_v4(bai_path, expiration=expiration)) + return script_content # ============= # # Browser # # ============= # - @data_bp.route('/browser') @data_bp.route('/browser/') @data_bp.route('/browser//') diff --git a/base/views/maintenance.py b/base/views/maintenance.py new file mode 100644 index 00000000..9535f53e --- /dev/null +++ b/base/views/maintenance.py @@ -0,0 +1,14 @@ +import time +from flask import jsonify, Blueprint +from base.utils.cache import delete_expired_cache + +maintenance_bp = Blueprint('maintenance', + __name__) + + +@maintenance_bp.route('/cleanup_cache', methods=['POST']) +def cleanup_cache(): + result = delete_expired_cache() + response = jsonify({"result": result}) + response.status_code = 200 + return response diff --git a/base/views/mapping.py b/base/views/mapping.py index 6fdd32b2..0a9ec672 100644 --- a/base/views/mapping.py +++ b/base/views/mapping.py @@ -45,6 +45,7 @@ def mapping(): VARS = {'title': 'Perform Mapping', 'form': form} + # todo: replace session user id and props with datastore user and props user = session.get('user') if form.validate_on_submit() and user: transaction = g.ds.transaction() @@ -141,6 +142,7 @@ def report_view(report_slug, trait_name=None, rerun=None): trait_name=trait_name)) # Verify user has permission to view report + # todo: replace session user id and props with datastore user and props user = session.get('user') if not trait.get('is_public'): if user: diff --git a/base/views/order.py b/base/views/order.py index 629f56b6..885a3bd5 100644 --- a/base/views/order.py +++ b/base/views/order.py @@ -7,13 +7,14 @@ """ import uuid +from flask import render_template, request, url_for, redirect, Blueprint, abort, flash + from base.forms import order_form from base.config import config from base.utils.email import send_email, ORDER_SUBMISSION_EMAIL from base.utils.google_sheets import add_to_order_ws, lookup_order -from flask import render_template, request, url_for, redirect, Blueprint, abort, flash, session -from datetime import datetime -from base.utils.data_utils import chicago_date, hash_it +from base.utils.data_utils import chicago_date +from base.utils.jwt import jwt_required, get_current_user order_bp = Blueprint('order', __name__, @@ -34,15 +35,16 @@ def order(): return redirect(url_for('strain.strain_catalog')) - @order_bp.route('/create', methods=['GET', 'POST']) +@jwt_required(optional=True) def order_page(): """ This view handles the order page. """ form = order_form() - if session.get('user') and not form.email.data: - form.email.data = session.get('user')['user_email'] + user = get_current_user() + if user and hasattr(user, 'email') and not form.email.data: + form.email.data = user.email # Fetch items items = form.items.data diff --git a/base/views/strains.py b/base/views/strains.py index 7300b687..545bd0b7 100644 --- a/base/views/strains.py +++ b/base/views/strains.py @@ -22,34 +22,31 @@ strain_bp = Blueprint('strain', __name__, template_folder='strain') - # -# Global Strain Map +# Strain List Page # @strain_bp.route('/') def strain(): """ - Redirect base route to the global strain map + Redirect base route to the strain list page """ - return redirect(url_for('strain.map_page')) - + return redirect(url_for('strain.strain_list')) -@strain_bp.route('/global-strain-map') +@strain_bp.route('/strain_list') @cache.memoize(50) -def map_page(): +def strain_list(): """ - Global strain map shows the locations of all wild isolates - within the SQLite database. + Strain list shows global strain map with the locations of all wild isolates + within the SQLite database and a table of all strains """ - VARS = {'title': "Global Strain Map", - 'strain_listing': dump_json(get_strains(known_origin=True))} - return render_template('strain/global_strain_map.html', **VARS) - + VARS = {'title': "Strains", + 'strain_listing_issues': get_strains(issues=True), + 'strain_listing': get_strains()} + return render_template('strain/strain_list.html', **VARS) # # Strain Data # - @strain_bp.route('/CelegansStrainData.tsv') def strain_data_tsv(): """ diff --git a/base/views/tools/indel_primer.py b/base/views/tools/indel_primer.py index ae978221..caab1643 100644 --- a/base/views/tools/indel_primer.py +++ b/base/views/tools/indel_primer.py @@ -17,12 +17,12 @@ from logzero import logger from wtforms import IntegerField, SelectField from wtforms.validators import Required, ValidationError +from threading import Thread from base.config import config +from base.constants import CHROM_NUMERIC, GOOGLE_CLOUD_BUCKET from base.utils.gcloud import check_blob, upload_file from base.utils.data_utils import hash_it -from base.constants import CHROM_NUMERIC -from threading import Thread # Tools blueprint indel_primer_bp = Blueprint('indel_primer', @@ -39,8 +39,9 @@ # Initial load of strain list from sv_data # This is run when the server is started. # NOTE: Tabix cannot make requests over https! -SV_BED_URL = "http://storage.googleapis.com/elegansvariation.org/tools/pairwise_indel_primer/sv.20200815.bed.gz" -SV_VCF_URL = "https://storage.googleapis.com/elegansvariation.org/tools/pairwise_indel_primer/sv.20200815.vcf.gz" +SV_BED_URL = f"http://storage.googleapis.com/{GOOGLE_CLOUD_BUCKET}/tools/pairwise_indel_primer/sv.20200815.bed.gz" +SV_VCF_URL = f"http://storage.googleapis.com/{GOOGLE_CLOUD_BUCKET}/tools/pairwise_indel_primer/sv.20200815.vcf.gz" + SV_STRAINS = VCF(SV_VCF_URL).samples SV_COLUMNS = ["CHROM", "START", diff --git a/base/views/user.py b/base/views/user.py index eacb7592..4707c297 100644 --- a/base/views/user.py +++ b/base/views/user.py @@ -6,24 +6,70 @@ User profile """ -from flask import render_template, Blueprint, session, flash, redirect, url_for +from flask import request, render_template, Blueprint, redirect, url_for +from flask_jwt_extended import jwt_required, get_jwt +from flask_jwt_extended.utils import get_current_user +from slugify import slugify +from base.utils.jwt import assign_access_refresh_tokens from base.models import user_ds -from logzero import logger +from base.forms import user_register_form, user_update_form +from base.config import config user_bp = Blueprint('user', __name__) +@user_bp.route('/') +def user(): + """ + Redirect base route to the strain list page + """ + return redirect(url_for('user.user_profile')) -@user_bp.route('/profile/') -def user(username): - """ The User Profile - """ - user_obj = session.get('user') - if user_obj is None: - flash("You must be logged in to view your profile", 'danger') - return redirect(url_for('primary.primary')) - VARS = {'title': username, - 'user_obj': user_ds(user_obj['name'])} - return render_template('user.html', **VARS) + +@user_bp.route("/register", methods=["GET", "POST"]) +def user_register(): + title = 'Register' + form = user_register_form(request.form) + if request.method == 'POST' and form.validate(): + username = request.form.get('username') + password = request.form.get('password') + full_name = request.form.get('full_name') + email = request.form.get('email') + roles = ['user'] + id = slugify(username) + user = user_ds(id) + user.set_properties(username=username, password=password, salt=config['PASSWORD_SALT'], full_name=full_name, email=email, roles=roles) + user.save() + return assign_access_refresh_tokens(username, user.roles, url_for("user.user_profile")) + return render_template('user/register.html', **locals()) + + +@user_bp.route("/profile", methods=["GET"]) +@jwt_required() +def user_profile(): + """ The User Account Profile + """ + title = 'Profile' + user = get_current_user() + return render_template('user/profile.html', **locals()) + + +@user_bp.route("/update", methods=["GET", "POST"]) +@jwt_required() +def user_update(): + """ Modify The User Account Profile + """ + title = 'Profile' + jwt_csrf_token = (get_jwt() or {}).get("csrf") + user = get_current_user() + form = user_update_form(request.form, full_name=user.full_name, email=user.email) + if request.method == 'POST' and form.validate(): + email = request.form.get('email') + full_name = request.form.get('full_name') + password = request.form.get('password') + user.set_properties(email=email, full_name=full_name, password=password) + user.save() + return redirect(url_for('user.user_profile')) + return render_template('user/update.html', **locals()) diff --git a/cloud_buckets/README.md b/cloud_buckets/README.md new file mode 100644 index 00000000..e69de29b diff --git a/cloud_buckets/deploy.sh b/cloud_buckets/deploy.sh new file mode 100644 index 00000000..2a9f9000 --- /dev/null +++ b/cloud_buckets/deploy.sh @@ -0,0 +1,3 @@ +#!/usr/bin/bash + +gsutil cors set cors.json gs://elegansvariation \ No newline at end of file diff --git a/cloud_config.txt b/cloud_config.txt new file mode 100644 index 00000000..c0396fa2 --- /dev/null +++ b/cloud_config.txt @@ -0,0 +1 @@ +{"cloud_config": {"created_on": "2021-03-17T15:07:08.405836+00:00", "modified_on": "2021-03-17T15:07:08.405836+00:00", "releases": [{"dataset": "20200815", "version": "v2", "wormbase": "WS276"}, {"dataset": "20180527", "version": "v1", "wormbase": "WS263"}, {"dataset": "20170531", "version": "v1", "wormbase": "WS258"}, {"dataset": "20160408", "version": "v1", "wormbase": "WS245"}]}} \ No newline at end of file diff --git a/cloud_functions/README.md b/cloud_functions/README.md new file mode 100644 index 00000000..53f60805 --- /dev/null +++ b/cloud_functions/README.md @@ -0,0 +1,8 @@ +# Google Cloud Functions +Google Cloud Function are serverless resources which are used to perform specific site maintenance tasks + +# Testing +To test changes to Cloud Functions, you MUST alter the function name BEFORE deploying or you will OVERWRITE the Function in PRODUCTION + +# Deploy +Deploy to production with deploy.sh diff --git a/cloud_functions/generate_thumbnails/.gcloudignore b/cloud_functions/generate_thumbnails/.gcloudignore new file mode 100644 index 00000000..a55ed18e --- /dev/null +++ b/cloud_functions/generate_thumbnails/.gcloudignore @@ -0,0 +1,12 @@ +# This file specifies files that are *not* uploaded to Google Cloud Platform +# using gcloud. It follows the same syntax as .gitignore, with the addition of +# "#!include" directives (which insert the entries of the given .gitignore-style +# file at that point). +# +# For more information, run: +# $ gcloud topic gcloudignore +# +.gcloudignore +# If you would like to upload your .git directory, .gitignore file or files +# from your .gitignore file, remove the corresponding line +# below: diff --git a/cloud_functions/generate_thumbnails/README.md b/cloud_functions/generate_thumbnails/README.md new file mode 100644 index 00000000..3d4f4a17 --- /dev/null +++ b/cloud_functions/generate_thumbnails/README.md @@ -0,0 +1,12 @@ +# Generate Thumbnails +Google Cloud Function to monitor isolation photo bucket +and generate thumbnails when new images are added + +# Testing +To test changes to Cloud Functions, you MUST alter the function name BEFORE deploying or you will OVERWRITE the Function in PRODUCTION + + +# Deploy +Deploy to production with deploy.sh +Script must be executed from inside the cloud function directory +(ie: cd cloud_functions/generate_thumbnails; deploy.sh) diff --git a/cloud_functions/generate_thumbnails/deploy.sh b/cloud_functions/generate_thumbnails/deploy.sh new file mode 100755 index 00000000..37bc3d8f --- /dev/null +++ b/cloud_functions/generate_thumbnails/deploy.sh @@ -0,0 +1,3 @@ +#!/usr/bin/bash + +gcloud beta functions deploy generate_thumbnails --runtime python37 --trigger-bucket gs://elegansvariation diff --git a/cloud_functions/generate_thumbnails/main.py b/cloud_functions/generate_thumbnails/main.py new file mode 100644 index 00000000..ee318da7 --- /dev/null +++ b/cloud_functions/generate_thumbnails/main.py @@ -0,0 +1,26 @@ +import re +from wand.image import Image +from google.cloud import storage + +client = storage.Client() + +def generate_thumbnails(data, context): + thumbnail_regex = "^photos\/isolation\/.*\.thumb\.jpg$" + image_regex = "^photos\/isolation\/.*\.jpg$" + + # Only generate thumbnails for matching paths + is_image = re.search(image_regex, data['name']) + is_thumbnail = re.search(thumbnail_regex, data['name']) + + if (is_image and not is_thumbnail): + # Download the image and resize it + bucket = client.get_bucket(data['bucket']) + thumbnail = Image(blob=bucket.get_blob(data['name']).download_as_string()) + thumbnail.transform(resize='x200') + + # Upload the thumbnail with modified name + path = data['name'].rsplit('.', 1) + blob_name = path[0] + ".thumb." + path[1] + thumbnail_blob = bucket.blob(blob_name) + thumbnail_blob.upload_from_string(thumbnail.make_blob()) + \ No newline at end of file diff --git a/cloud_functions/generate_thumbnails/requirements.txt b/cloud_functions/generate_thumbnails/requirements.txt new file mode 100644 index 00000000..b7a466da --- /dev/null +++ b/cloud_functions/generate_thumbnails/requirements.txt @@ -0,0 +1,2 @@ +google-cloud-storage +wand \ No newline at end of file diff --git a/cron.yaml b/cron.yaml index bdae372e..49dcb9f1 100644 --- a/cron.yaml +++ b/cron.yaml @@ -1,4 +1,8 @@ cron: - description: test_mapping_pipeline url: /report/1c28542b/telomere-resids + schedule: every 24 hours + +- description: delete_expired_cache_entries + url: /tasks/cleanup_cache schedule: every 24 hours \ No newline at end of file diff --git a/env.yaml b/env.yaml index e488f8c9..58ea68f4 100644 --- a/env.yaml +++ b/env.yaml @@ -5,22 +5,42 @@ channels: - bioconda - defaults dependencies: - - ca-certificates=2020.6.20=hecda079_0 - - certifi=2020.6.20=py38h32f6830_0 + - ca-certificates=2021.1.19=hecd8cb5_1 + - certifi=2020.12.5=py38hecd8cb5_0 + - cryptography=3.4.6=py38h2fd3fbb_0 + - defusedxml=0.7.1=pyhd3eb1b0_0 + - flask=1.1.2=pyhd3eb1b0_0 + - flask-jwt-extended=4.1.0=pyhd3eb1b0_0 - gawk=5.1.0=h55c7030_0 - gettext=0.19.8.1=h1f1d5ed_1 + - icu=58.2=h0a44026_3 + - isodate=0.6.0=py_1 + - itsdangerous=1.1.0=pyhd3eb1b0_0 - libcxx=9.0.1=1 - libffi=3.2.1=h6de7cb9_1006 + - libgcrypt=1.8.6=haf1e3a3_0 + - libgpg-error=1.32=h0a44026_0 + - libiconv=1.16=h1de35cc_0 + - libtool=2.4.6=haf1e3a3_1005 + - libxml2=2.9.9=hf6e021a_1 + - libxmlsec1=1.2.29=h6f9f905_0 + - libxslt=1.1.33=h33a18ac_0 + - markupsafe=1.1.1=py38h1de35cc_1 - ncurses=6.1=h0a44026_1002 - - openssl=1.1.1g=haf1e3a3_1 + - openssl=1.1.1j=h9ed2024_0 - pip=20.1.1=py_1 + - pycparser=2.20=py_2 + - pyjwt=1.7.1=py38_0 - python=3.8.2=hd5f0129_5_cpython + - python3-saml=1.9.0=py_0 - python_abi=3.8=1_cp38 - readline=8.0=hcfe32e1_0 - setuptools=49.1.3=py38h32f6830_0 + - six=1.15.0=py38hecd8cb5_0 - sqlite=3.30.1=h93121df_0 - tk=8.6.10=hbbe82c9_0 - wheel=0.34.2=py_1 + - xmlsec=1.3.3=py38h7166777_0 - xz=5.2.4=h1de35cc_1001 - zlib=1.2.11=h0b31af3_1006 - pip: @@ -46,7 +66,6 @@ dependencies: - docutils==0.15.2 - feedgen==0.9.0 - flake8==3.8.3 - - flask==1.1.2 - flask-caching==1.3.3 - flask-dance==3.0.0 - flask-debugtoolbar==0.10.1 @@ -74,7 +93,6 @@ dependencies: - idna==2.10 - ipython==7.16.1 - ipython-genutils==0.2.0 - - itsdangerous==1.1.0 - jedi==0.17.1 - jinja2==2.11.2 - jmespath==0.10.0 @@ -84,7 +102,6 @@ dependencies: - logzero==1.3.1 - lxml==4.5.2 - markdown==2.6.11 - - markupsafe==1.1.1 - mccabe==0.6.1 - nbformat==5.0.7 - numpy==1.19.0 @@ -105,7 +122,6 @@ dependencies: - pyasn1==0.4.8 - pyasn1-modules==0.2.8 - pycodestyle==2.6.0 - - pycparser==2.20 - pyflakes==2.2.0 - pygments==2.6.1 - pyrsistent==0.16.0 @@ -123,7 +139,6 @@ dependencies: - s3transfer==0.3.3 - scipy==1.5.1 - simplejson==3.13.2 - - six==1.15.0 - sqlalchemy==1.3.18 - traitlets==4.3.3 - typing-extensions==3.7.4.2 @@ -141,4 +156,3 @@ dependencies: - zope-event==4.4 - zope-interface==5.1.0 prefix: /Users/dec/opt/anaconda3/envs/cendr - diff --git a/index.yaml b/index.yaml index 7d9d9939..2d26d6a2 100644 --- a/index.yaml +++ b/index.yaml @@ -6,7 +6,6 @@ indexes: - name: created_on direction: desc - - kind: trait properties: - name: report_slug @@ -33,5 +32,11 @@ indexes: - kind: trait properties: - name: user_id + - name: created_on + direction: desc + +- kind: data-config + properties: + - name: created_by - name: created_on direction: desc \ No newline at end of file diff --git a/main.py b/main.py index 0cd8203e..58a9826e 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,14 @@ from base.application import create_app +# Attach Debugger +try: + import googleclouddebugger + googleclouddebugger.enable( + breakpoint_enable_canary=False, + service_account_json_file='env_config/client-secret.json') +except ImportError: + pass + + # Initialize application -app = create_app() +app = create_app() \ No newline at end of file diff --git a/mapping_worker/utils/gcloud.py b/mapping_worker/utils/gcloud.py index 45f5c39c..cb18dc61 100644 --- a/mapping_worker/utils/gcloud.py +++ b/mapping_worker/utils/gcloud.py @@ -7,13 +7,10 @@ """ import os -import arrow import json -import pandas as pd -from io import StringIO from gcloud import datastore, storage -from logzero import logger +from base.constants import GOOGLE_CLOUD_BUCKET def get_item(kind, name): """ @@ -121,7 +118,7 @@ def upload_files(self, file_list): Stores uploaded files. """ gs = storage.Client(project='andersen-lab') - cendr_bucket = gs.get_bucket("elegansvariation.org") + cendr_bucket = gs.get_bucket(GOOGLE_CLOUD_BUCKET) for fname in file_list: base_name = os.path.basename(fname) report_base = f"reports/{self.REPORT_VERSION}/{self.name}/{base_name}" diff --git a/mapping_worker/utils/interval.py b/mapping_worker/utils/interval.py index e0987c7a..7faf4f49 100644 --- a/mapping_worker/utils/interval.py +++ b/mapping_worker/utils/interval.py @@ -11,6 +11,7 @@ from subprocess import Popen from utils.vcf_np import VCF_DataFrame from logzero import logger +from base.constants import GOOGLE_CLOUD_BUCKET DATASET_RELEASE = os.environ['DATASET_RELEASE'] @@ -29,7 +30,7 @@ def process_interval(interval): df = pd.read_csv("df.tsv", sep='\t') isotype_list = ','.join(df['ISOTYPE'].values) if not os.path.exists(interval_out): - comm = f"bcftools view -O z --samples {isotype_list} https://storage.googleapis.com/elegansvariation.org/releases/{DATASET_RELEASE}/variation/WI.{DATASET_RELEASE}.soft-filter.vcf.gz {interval} > {interval_out} && bcftools index {interval_out}" + comm = f"bcftools view -O z --samples {isotype_list} https://storage.googleapis.com/{GOOGLE_CLOUD_BUCKET}/releases/{DATASET_RELEASE}/variation/WI.{DATASET_RELEASE}.soft-filter.vcf.gz {interval} > {interval_out} && bcftools index {interval_out}" out, err = Popen(comm, shell=True).communicate() vcf = VCF_DataFrame.from_vcf(interval_out, interval) diff --git a/requirements.txt b/requirements.txt index f70aab85..aad3965b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,7 +27,7 @@ PyYAML>=4.2b1 requests gunicorn simplejson==3.13.2 -SQLAlchemy>=1.3.18 +SQLAlchemy==1.3.18 unicode_slugify==0.1.3 webapp2==3.0.0b1 Werkzeug==1.0.0 @@ -40,4 +40,11 @@ Flask-Dance==3.0.0 feedgen==0.9.0 cachelib pytabix - +python3-saml +flask_jwt_extended +google-cloud-storage +google-cloud-datastore +google_cloud_logging +google-api-core +grpcio +google-python-cloud-debugger From 21ce48dc651e67d4e1d29fab9f0fe177af2b5a41 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 22 Mar 2021 03:19:57 -0500 Subject: [PATCH 004/288] hidden files --- .envrc | 1 + .gitignore | 6 ++++-- .travis.yml | 7 ++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.envrc b/.envrc index 740d348d..eb4922c6 100644 --- a/.envrc +++ b/.envrc @@ -1,5 +1,6 @@ export FLASK_APP=main:app export GAE_VERSION=development-`cat .travis.yml | grep 'VERSION_NUM=' | cut -f 2 -d '='` +export CLOUD_CONFIG=1 export GOOGLE_APPLICATION_CREDENTIALS=env_config/client-secret.json export PYTHONPATH=$(pwd) export WERKZEUG_DEBUG_PIN=off diff --git a/.gitignore b/.gitignore index 533f72d4..0118777e 100644 --- a/.gitignore +++ b/.gitignore @@ -61,7 +61,7 @@ target/ #Random *.DS_Store -*.json +package-lock.json tmp/ @@ -92,4 +92,6 @@ photos/* *.done # Heritability run go tool -invoke \ No newline at end of file +invoke + +.vscode/launch.json diff --git a/.travis.yml b/.travis.yml index 9d3e3e97..50887b65 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,11 @@ language: bash install: -- openssl aes-256-cbc -K $encrypted_53077b9a3e95_key -iv $encrypted_53077b9a3e95_iv -in env_config.zip.enc -out env_config.zip -d +- openssl aes-256-cbc -K $encrypted_86f5a1ab1ccf_key -iv $encrypted_86f5a1ab1ccf_iv -in env_config.zip.enc -out env_config.zip -d - unzip -qo env_config.zip -- export VERSION_NUM=1-5-3 +- export VERSION_NUM=1-5-57 - export APP_CONFIG=master +- export CLOUD_CONFIG=1 - if [ "${TRAVIS_BRANCH}" != "master" ]; then export APP_CONFIG=development; fi; - export GAE_VERSION=${APP_CONFIG}-${VERSION_NUM} - export GOOGLE_APPLICATION_CREDENTIALS=env_config/client-secret.json @@ -12,7 +13,7 @@ install: deploy: provider: gae version: "${GAE_VERSION}" - project: andersen-lab + project: andersen-lab-302418 keyfile: env_config/client-secret.json on: all_branches: true From 5f56bee9637baf9f114ee157d2d5fbb3d48ef774 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 22 Mar 2021 03:56:47 -0500 Subject: [PATCH 005/288] fix download code --- base/cloud_config.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/base/cloud_config.py b/base/cloud_config.py index 4d126ef0..6bb68491 100644 --- a/base/cloud_config.py +++ b/base/cloud_config.py @@ -8,9 +8,8 @@ from google.oauth2 import service_account from google.cloud import datastore, storage -from base.constants import REPORT_V1_FILE_LIST, REPORT_V2_FILE_LIST +from base.constants import REPORT_V1_FILE_LIST, REPORT_V2_FILE_LIST, GOOGLE_CLOUD_BUCKET from base.utils.data_utils import dump_json, unique_id -from base.utils.gcloud import download_file class CloudConfig: @@ -38,6 +37,12 @@ def get_storage_client(self): self.storage_client = storage.Client(credentials=service_account.Credentials.from_service_account_file('env_config/client-secret.json')) return self.storage_client + def download_file(self, name, fname): + client = self.get_storage_client() + bucket = client.get_bucket(GOOGLE_CLOUD_BUCKET) + blob = bucket.blob(name) + blob.download_to_file(open(fname, 'wb')) + def ds_save(self): data = {'cloud_config': self.cc} m = datastore.Entity(key=self.get_ds_client().key(self.kind, self.name)) @@ -140,7 +145,7 @@ def get_release_files(self, dataset, files, refresh=False): for n in files: name = f"data_reports/{dataset}/{n}" fname = f"{local_path}/{n}" - download_file(name=name, fname=fname) + self.download_file(name=name, fname=fname) except: return None return files @@ -154,7 +159,7 @@ def get_release_db(self, dataset, wormbase, refresh=False): else: return - download_file(name=db_name, fname=db_fname) + self.download_file(name=db_name, fname=db_fname) return True def create_backup(self): From c6e67bfd35301e7b7e138250787071313d3ca687 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 29 Mar 2021 13:05:04 -0500 Subject: [PATCH 006/288] update logging, constants --- base/constants.py | 4 ++-- base/database/__init__.py | 2 ++ base/database/etl_strains.py | 2 -- base/manage.py | 5 ----- base/views/auth/saml.py | 1 - 5 files changed, 4 insertions(+), 10 deletions(-) diff --git a/base/constants.py b/base/constants.py index 980dcb70..ab59eb01 100644 --- a/base/constants.py +++ b/base/constants.py @@ -35,8 +35,8 @@ class PRICES: "MtDNA": 7} -GOOGLE_CLOUD_BUCKET = 'elegansvariation' -GOOGLE_CLOUD_PROJECT_ID = 'andersen-lab-302418' +GOOGLE_CLOUD_BUCKET = 'elegansvariation.org' +GOOGLE_CLOUD_PROJECT_ID = 'andersen-lab' # WI Strain Info Dataset GOOGLE_SHEETS = {"orders": "1BCnmdJNRjQR3Bx8fMjD_IlTzmh3o7yj8ZQXTkk6tTXM", diff --git a/base/database/__init__.py b/base/database/__init__.py index 968f28e8..3f4370ae 100644 --- a/base/database/__init__.py +++ b/base/database/__init__.py @@ -181,4 +181,6 @@ def download_sqlite_database(): storage_client = storage.Client.from_service_account_json('env_config/client-secret.json') bucket = storage_client.bucket(GOOGLE_CLOUD_BUCKET) blob = bucket.blob(blob_path) + console.log(f"Downloading DB file STARTED: {SQLITE_FILE}") blob.download_to_file(open(file_path, 'wb')) + console.log(f"Downloading DB file COMPLETE: {SQLITE_FILE}") diff --git a/base/database/etl_strains.py b/base/database/etl_strains.py index d8da4e1c..48c1fd06 100644 --- a/base/database/etl_strains.py +++ b/base/database/etl_strains.py @@ -82,8 +82,6 @@ def fetch_andersen_strains(): v = None record[k] = v if k in ['sampling_date'] and v: - print('k: ' + k) - print('v: ' + v) record[k] = parser.parse(v) if record['latitude']: diff --git a/base/manage.py b/base/manage.py index 85bd73f2..e08fc066 100644 --- a/base/manage.py +++ b/base/manage.py @@ -46,9 +46,6 @@ def update_credentials(): from base.application import create_app app = create_app() app.app_context().push() - - print(app.config['SQLALCHEMY_DATABASE_URI']) - print(app.config['SQLITE_BASENAME']) click.secho("Zipping env_config", fg='green') zipdir('env_config/', 'env_config.zip') @@ -77,8 +74,6 @@ def decrypt_credentials(): from base.application import create_app app = create_app() app.app_context().push() - - print(app.config['SQLALCHEMY_DATABASE_URI']) click.secho("Decrypting env_config.zip.enc", fg='green') zip_creds = get_item('credential', 'travis-ci-cred') diff --git a/base/views/auth/saml.py b/base/views/auth/saml.py index 9daf5ac9..548971c4 100644 --- a/base/views/auth/saml.py +++ b/base/views/auth/saml.py @@ -103,7 +103,6 @@ def saml_sso2(): """ Single Sign On (2) route for SAML which includes user attributes """ - print('SSO2') req = prepare_flask_request(request) saml_auth = init_saml_auth(req) return_to = session.get("login_referrer") From 1a419d5eea897b4d1f9b41e7a5a26594e6bde37b Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 29 Mar 2021 14:12:19 -0500 Subject: [PATCH 007/288] add new content to cloud config --- base/cloud_config.py | 3 +- .../reports/20210121/alignment_report.html | 2926 +++++++++++++ .../reports/20210121/concordance_report.html | 2995 +++++++++++++ base/static/reports/20210121/gatk_report.html | 3898 +++++++++++++++++ base/static/reports/20210121/pipelines.md | 113 + .../20210121/reads_mapped_by_strain.tsv | 1239 ++++++ base/static/reports/20210121/release_notes.md | 1 + 7 files changed, 11174 insertions(+), 1 deletion(-) create mode 100644 base/static/reports/20210121/alignment_report.html create mode 100644 base/static/reports/20210121/concordance_report.html create mode 100644 base/static/reports/20210121/gatk_report.html create mode 100644 base/static/reports/20210121/pipelines.md create mode 100644 base/static/reports/20210121/reads_mapped_by_strain.tsv create mode 100644 base/static/reports/20210121/release_notes.md diff --git a/base/cloud_config.py b/base/cloud_config.py index 6bb68491..b5b7f89d 100644 --- a/base/cloud_config.py +++ b/base/cloud_config.py @@ -16,7 +16,8 @@ class CloudConfig: ds_client = None storage_client = None kind = 'cloud-config' - default_cc = { 'releases' : [{'dataset': '20200815', 'wormbase': 'WS276', 'version': 'v2'}, + default_cc = { 'releases' : [{'dataset': '20210121', 'wormbase': 'WS276', 'version': 'v2'}, + {'dataset': '20200815', 'wormbase': 'WS276', 'version': 'v2'}, {'dataset': '20180527', 'wormbase': 'WS263', 'version': 'v1'}, {'dataset': '20170531', 'wormbase': 'WS258', 'version': 'v1'}, {'dataset': '20160408', 'wormbase': 'WS245', 'version': 'v1'}] } diff --git a/base/static/reports/20210121/alignment_report.html b/base/static/reports/20210121/alignment_report.html new file mode 100644 index 00000000..7a94a49a --- /dev/null +++ b/base/static/reports/20210121/alignment_report.html @@ -0,0 +1,2926 @@ + + + + + + + + + + + + + + +alignment_report.utf8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + +
    +

    Overview

    +
      +
    • Total strains : 1238
    • +
    • Sequenced libraries : 2152
    • +
    • Median mapped reads : 36 million
    • +
    • Median coverage : 32x
    • +
    +


    +
    +
    +

    Reads Mapped by Strain

    +
    + + +


    +
    +
    +

    Alignment Metrics

    +

    +
    + + + + +
    + + + + + + + + + + + + + + + diff --git a/base/static/reports/20210121/concordance_report.html b/base/static/reports/20210121/concordance_report.html new file mode 100644 index 00000000..5c93f031 --- /dev/null +++ b/base/static/reports/20210121/concordance_report.html @@ -0,0 +1,2995 @@ + + + + + + + + + + + + + + +concordance_report.utf8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + +
    +

    Overview

    +

    Concordance analysis allows us to group strains that are genetically almost identical into an isotype. The following table summarizes the number of isotypes from previous and current releases, and the number of strains that belong to those isotypes.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    IsotypesStrains IncludedStrains with WGS dataStrains with RAD-seq data
    Isotypes from Previous Release400*910770140
    New Isotypes from Current Release1404684680
    Total54013781238140
    +

    *Four strains were reduced to a single isotype group so this number was reduced from 403 to 400 (see below for details)

    +


    +
    +
    +

    Concordance score distribution and cutoff

    +

    We examined the pairwise concordance scores of all strains. Concordance values for every pair of strains were calculated as the number of shared variant sites divided by the total number of variants called for each pair. If the concordance score was more than 0.9997, the strain pair is grouped into the same isotype.

    +

    +


    +
    +
    +

    Search for concordance for strain pairs

    +

    Strain comparisons are listed in the table below. Only concordance scores > 0.999 are shown.

    +
    + +


    +
      +
    • ECA2649 had high concordance to two distinct isotype groups. However, upon investigating the relationships to each group, we chose to manually place ECA2649 with isotype ECA2551, not ECA2672.
    • +
    +

    +
    +
    +

    Changes from previous release

    +


    +
      +
    • This release used only SNVs for isotype assignment.

    • +
    • Four strains (ECA2677, ECA2678, ECA2679, and ECA2686) were removed because they were frozen as “dirty” strains and have now been cleaned, frozen, and re-sequenced. Because these four strains were isotype reference strains, a new isotype reference strain was assigned. It appears that the other six strains in these isotype groups changed isotypes, but they remain in the same group as before with a new, clean isotype reference strain. Details can be found below.

    • +
    +


    + +++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Dirty Strain (Old)Clean Strain (New)Previous IsotypeNew IsotypeOther Strains in Isotype Group
    ECA2677ECA1202ECA2677ECA1202ECA1201
    ECA2678ECA1206ECA2678ECA1206ECA1973, ECA1979, ECA1983
    ECA2679ECA1211ECA2679ECA1212ECA1209, ECA1211
    ECA2686NA*ECA2686ECA1243NA
    +

    *Clean strain for ECA2686 is ECA2803 but has not been sequenced yet.

    +


    +
      +
    • Strains ECA1465, ECA1467, ECA1493, ECA1515 were each their own isotype in 20200815 release. They were grouped into the same isotype in this release, which resulted in a reduce of count of previous isotypes from 403 to 400. Below is their pairwise concordance value in this release (top) and in 20200815 release (bottom).
    • +
    +


    +

    +


    +

    +
    + + + + +
    + + + + + + + + + + + + + + + diff --git a/base/static/reports/20210121/gatk_report.html b/base/static/reports/20210121/gatk_report.html new file mode 100644 index 00000000..462df631 --- /dev/null +++ b/base/static/reports/20210121/gatk_report.html @@ -0,0 +1,3898 @@ + + + + + + + + + + + + + + +gatk_report_v1.10c.utf8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + +
    +

    Overview

    +
      +
    • Total strains : 1,238
    • +
    • Total SNVs before filter : 5,065,794
    • +
    • Total SNVs after filter : 2,970,933
    • +
    • Total indels before filter : 1,908,754
    • +
    • Total indels after filter : 611,052
    • +
    +


    +
    +
    +

    Site-level quality filters

    +

    +


    +
    +
    +

    Number of variant sites removed by each filter

    +

    Each bar shows counts of variant sites removed by the combinations of filters indicated by the dots below.

    +

    +


    +
    +
    +

    Pre-filter statistics

    +

    Variant counts for each strain based on vcf containing all variant sites called by GATK (“soft-filter.vcf”).

    +
    + +

    +


    +
    +
    +

    Post-filter statistics

    +

    Variant count for each strain based on the VCF containing only sites pass all filters (“hard-filter.vcf”). All heterozygous sites on main chromosomes were converted to either homozygous or missing. The remaining heterozygous sites are all located on mitochondria chromosomes.

    +
    + +

    +
    +

    +


    +
    +
    +

    Relationship between heterozyous SNVs and total SNVs

    +

    Number of variants versus the number of heterozygous calls shows strains that might have mixed genotypes or low quality calls (high het but low variation).

    +
    + +
    + + + + +
    + + + + + + + + + + + + + + + diff --git a/base/static/reports/20210121/pipelines.md b/base/static/reports/20210121/pipelines.md new file mode 100644 index 00000000..3b7c1f6e --- /dev/null +++ b/base/static/reports/20210121/pipelines.md @@ -0,0 +1,113 @@ +# Methods / Pipelines + +This tab links to the nextflow pipelines used to process wild isolate sequence data. + +![](/static/img/overview.drawio.svg) + +### FASTQ QC and Trimming +__[andersenlab/trim-fq-nf](https://github.com/andersenlab/trim-fq-nf) -- (Latest [d637d0b](https://github.com/AndersenLab/trim-fq-nf/tree/d637d0b))__ + +Adapters and low quality sequences were trimmed off of raw reads using [fastp (0.20.0)](https://github.com/OpenGene/fastp) and default parameters. Reads shorter than 20 bp after trimming were discarded. + +### __Alignment__ + +__[andersenlab/alignment-nf](https://github.com/andersenlab/alignment-nf) -- ([1c96b4a](https://github.com/AndersenLab/alignment-nf/tree/1c96b4a))__ + +Trimmed reads were aligned to _C. elegans_ reference genome (project PRJNA13758 version WS276 from the [Wormbase](https://wormbase.org/)) using `bwa mem` [BWA (0.7.17)](http://bio-bwa.sourceforge.net/). Libraries of the same strain were merged together and indexed by [sambamba (0.7.0)](https://lomereiter.github.io/sambamba/). Duplicates were flagged with [Picard (2.21.3)](https://broadinstitute.github.io/picard/). + +Strains with less than 14x coverage were not included in the alignment report and subsequent analyses. + +### __Variant Calling__ + +__[andersenlab/wi-gatk](https://github.com/andersenlab/wi-gatk) -- ([a84ba4f](https://github.com/AndersenLab/wi-gatk/tree/a84ba4f))__ + +Variants for each strain were called using `gatk HaplotypeCaller`. After the initial variant calling, variants were combined and then recalled jointly using `gatk GenomicsDBImport` and `gatk GenotypeGVCFs` [GATK (4.1.4.0)](https://gatk.broadinstitute.org/hc/en-us/sections/360007279452-4-1-4-0?page=6#articles). + +The variants were further processed and filtered with custom-written scripts for [heterozygous SNV polarization](https://github.com/AndersenLab/wi-gatk/blob/master/env/het_polarization.nim), GATK (4.1.4.0), and [bcftools (1.10)](http://samtools.github.io/bcftools/bcftools.html). + + +
    + +Warning
    + +Heterozygous polarization and filtering thresholds were optimized for single nucleotide variants (SNVs). + +

    +Additionally, insertion or deletion (indel) variants less than 50 bp are more reliably called than indel variants greater than this size. In general, indel variants should be considered less reliable than SNVs. +
    + +#### Site-level filtering and annotation + +__[andersenlab/post-gatk-nf](https://github.com/andersenlab/post-gatk-nf) -- ([84d3a28](https://github.com/AndersenLab/post-gatk-nf/tree/add_annotation/84d3a28))__ + +1. __Heterozygous SNV polarization__: Because _C. elegans_ is a selfing species, heterozygous SNV sites are most likely errors. Biallelic heterozygous SNVs were converted to homozygous REF or ALT if we had sufficient evidence for conversion. Only biallelic SNVs that are not on mitochondria DNA were included in this step. Specifically, the SNV was converted if the normalized Phred-scaled likelihoods (PL) met the following criteria (a smaller PL means more confidence). Any heterozygous SNVs that did not meet these criteria were left unchanged. + * If PL-ALT/PL-REF <= 0.5 and PL-ALT <= 200, convert to homozygous ALT + * If PL-REF/PL-ALT <= 0.5 and PL-REF <= 200, convert to homozygous REF + +2. __Soft filtering__: Low quality sites were flagged but not modified or removed. + + For the __site-level__ soft filter, variant sites that meet the following conditions were flagged as PASS. These stats were computed across all samples for each site. + + * Variant quality (QUAL) > 30 (this filter is very lenient, only three sites failed) + * Variant quality normalized by read depth (QD) > 20 + * Strand bias of ALT calls: strand odds ratio (SOR) < 5 + * Strand bias of ALT calls: Fisherstrand (FS) < 100 + * Fraction of samples with missing genotype < 95% + * Fraction of samples with heterozygous genotype after heterozygous site polarization < 10% + + For the __sample-level__ soft filter, genotypes that meet the following filters were flagged as PASS for each site in each sample: + + * Read depth (DP) > 5 + * Site is not heterozygous + +3. __SnpEff Annotation__: The predicted impact of each variant site was annotated with [SnpEff (4.3.1t)](https://pcingola.github.io/SnpEff/SnpEff.html). + +4. For the hard-filtered VCF, low quality sites were modified or removed using the following criteria. + + * For the __site-level__ hard filter, variant sites not flagged as PASS were removed. + * For the __sample-level__ hard filter, genotypes not flagged as PASS were converted to missing (`./.`), with the exception that heterozygous sites on mitochondria where kept unchanged. + + After the steps above, sites that are invariant (`0/0` or `1/1` across all samples, not counting missing `./.`) were removed. + +5. __BCSQ Annotation__: Variant impacts were then annotated using `bcftools csq`, which takes into consideration nearby variants and annotates variant impacts based on haplotypes. + +#### Determination of filter thresholds + +We re-examined our filter thresholds for this release. A variant simulation pipeline was used as part of this process: + +* __Variant Simulations - __[andersenlab/variant-simulations-nf](https://github.com/andersenlab/variant-simulations-nf) + +Please see the [filter optimization report](/static/reports/filter_optimization/20200803_optimization_report.html) for further details. + +### __Isotype Assignment__ +__[andersenlab/concordance-nf](https://github.com/andersenlab/concordance-nf) -- ([5160f9f](https://github.com/andersenlab/concordance-nf/tree/5160f9f))__ + +Isotype groups contain strains that are likely identical to each other and were sampled from the same isolation locations. For any phenotypic assay, only the isotype reference strain needs to be scored. Users interested in individual strain genotypes can use the strain-level data. + +Strains were grouped into isotypes using the following steps: + +1. Using all high quality variants (only SNPs from the hard-filtered VCF) and `bcftools gtcheck`, concordance for each pair of strains was calculated as a fraction of shared variants over the total variants in each pair. + +2. Strain pairs with concordance > 0.9997 were grouped into the same isotype group. The threshold 0.9997 was determined by: + + * Examining the distribution of concordance scores. + * Capturing similarity between strains to minimize the number of strains that get assigned to multiple isotype groups. + * Agreement with the isotype groups in previous releases. + +3. The following issues, which were rare, were resolved on a case-by-case basis: + + * If one strain was assigned to multiple isotypes. + * If one isotype from previous releases matches to multiple new isotype groups. + * If one new isotype group contains strains from multiple isotypes from previous releases. + +When issues arose, the pairwise concordance between all strains within an isotype were examined manually. Strains and isotypes may be re-assigned with the goal that strains within the same isotype group should have high concordance with each other, and strains from different isotype groups should have lower concordance. + +### __Tree Generation__ + +__[andersenlab/post-gatk-nf](https://github.com/andersenlab/post-gatk-nf) -- ([84d3a28](https://github.com/AndersenLab/post-gatk-nf/tree/add_annotation/84d3a28))__ + +Trees were generated by converting the hard-filtered VCF to Phylip format using [vcf2phylip (030b8d)](https://github.com/edgardomortiz/vcf2phylip/tree/030b8d). Then, the Phylip format was converted to Stockholm format using [Bioconvert (0.3.0)](https://bioconvert.readthedocs.io/en/master/index.html), which was then used to construct a tree with [QuickTree (2.5)](https://github.com/tseemann/quicktree) using default settings. The trees were plotted with [FigTree (1.4.4)](http://tree.bio.ed.ac.uk/software/figtree/) rooting on the most diverse strain XZ1516. + +### __Imputation__ + +Imputation was not done for this release. diff --git a/base/static/reports/20210121/reads_mapped_by_strain.tsv b/base/static/reports/20210121/reads_mapped_by_strain.tsv new file mode 100644 index 00000000..05bdc06f --- /dev/null +++ b/base/static/reports/20210121/reads_mapped_by_strain.tsv @@ -0,0 +1,1239 @@ +Sample raw_total_sequences reads_mapped reads_mapped_percent coverage +AB1 85.02 84.5 99.4 64 +AB4 71.15 70.56 99.2 52 +BRC20067 38.8 35.9 92.5 30 +BRC20113 43.18 41.42 95.9 38 +BRC20231 56.18 47.58 84.7 43 +BRC20263 51.1 48.94 95.8 45 +CB4852 204.11 203.16 99.5 144 +CB4854 51.09 50.83 99.5 38 +CB4856 86.55 85.21 98.5 61 +CB4932 60.81 60.53 99.5 45 +CX11254 63.1 62.09 98.4 46 +CX11262 49.89 49.3 98.8 38 +CX11264 71.94 71.13 98.9 52 +CX11271 50.57 50.32 99.5 39 +CX11276 60.66 59.95 98.8 45 +CX11285 109.76 108.51 98.9 82 +CX11292 54.06 53.65 99.2 39 +CX11307 70.35 69.66 99 51 +CX11314 71.66 70.94 99 53 +CX11315 61.66 60.93 98.8 49 +DL200 44.16 43.85 99.3 35 +DL226 97.65 96.7 99 71 +DL238 66.56 65.36 98.2 49 +ECA1069 74.79 74.43 99.5 51 +ECA1070 22 21.84 99.3 25 +ECA1071 59.23 58.9 99.4 34 +ECA1072 34.69 34.53 99.5 20 +ECA1073 36.52 36.22 99.2 24 +ECA1074 65.67 65.29 99.4 39 +ECA1184 38.71 38.28 98.9 34 +ECA1185 33.35 32.82 98.4 26 +ECA1186 32.73 32.21 98.4 25 +ECA1187 42.37 42.05 99.2 36 +ECA1188 36.13 35.84 99.2 33 +ECA1189 25.32 25.09 99.1 23 +ECA1190 35.58 35.28 99.2 32 +ECA1191 27.19 26.83 98.7 22 +ECA1192 43.2 42.82 99.1 38 +ECA1193 29.44 28.35 96.3 23 +ECA1194 23.62 23.42 99.2 20 +ECA1195 38.17 35.32 92.5 28 +ECA1196 16.59 16.44 99.1 16 +ECA1197 30.04 29.78 99.1 27 +ECA1198 37.19 36.73 98.8 34 +ECA1199 35.22 34.9 99.1 32 +ECA1200 35.4 35.04 99 32 +ECA1201 28.31 27.85 98.4 22 +ECA1202 33.26 32.43 97.5 30 +ECA1203 29.92 29.48 98.5 24 +ECA1204 31.5 30.78 97.7 27 +ECA1205 29.34 28.63 97.6 22 +ECA1206 37.45 35.77 95.5 32 +ECA1207 34.88 34.33 98.4 30 +ECA1208 31.3 30.94 98.9 26 +ECA1209 29.27 29.02 99.1 27 +ECA1210 33.7 33.42 99.2 30 +ECA1211 23.6 23.3 98.8 22 +ECA1212 39.75 38.93 97.9 31 +ECA1213 26.66 26.14 98 22 +ECA1214 32.23 31.8 98.7 26 +ECA1215 27.19 26.66 98 23 +ECA1216 36.07 35.74 99.1 29 +ECA1217 37.84 36.67 96.9 29 +ECA1218 22.34 22.03 98.6 19 +ECA1220 49.25 48.76 99 39 +ECA1221 16.44 16.31 99.2 15 +ECA1222 20.68 20.05 96.9 17 +ECA1223 45.01 43.73 97.2 35 +ECA1224 34.71 33.96 97.8 27 +ECA1225 22.41 21.83 97.4 18 +ECA1226 18.86 18.68 99.1 16 +ECA1227 28.07 27.79 99 25 +ECA1228 22.21 22.01 99.1 19 +ECA1229 38.85 38.35 98.7 30 +ECA1230 33.28 32.71 98.3 30 +ECA1231 25.08 24.82 98.9 23 +ECA1232 28.52 28.21 98.9 22 +ECA1233 26.48 26.23 99.1 21 +ECA1234 27.43 27.21 99.2 22 +ECA1235 38.4 38.07 99.1 34 +ECA1236 18.98 18.78 98.9 16 +ECA1237 38.12 37.83 99.2 31 +ECA1238 53.65 53.22 99.2 42 +ECA1239 36.28 36 99.2 29 +ECA1240 32.69 32.43 99.2 26 +ECA1241 27.94 27.69 99.1 23 +ECA1242 20.38 20.2 99.1 17 +ECA1243 24.61 24.45 99.4 21 +ECA1244 27.98 27.82 99.4 23 +ECA1245 23.42 23.27 99.4 19 +ECA1246 23.77 23.57 99.2 19 +ECA1247 38.58 38.22 99.1 31 +ECA1249 37.26 36.95 99.2 33 +ECA1250 18.19 17.87 98.2 15 +ECA1251 23.79 23.45 98.6 19 +ECA1252 40.5 40.07 98.9 36 +ECA1253 39.76 39.34 98.9 31 +ECA1254 33.21 32.93 99.2 31 +ECA1255 36.77 35.99 97.9 29 +ECA1256 31.2 30.57 98 24 +ECA1257 28.03 27.68 98.7 24 +ECA1258 24.8 24.49 98.7 21 +ECA1259 25.91 25.72 99.3 21 +ECA1260 34.17 33.89 99.2 26 +ECA1261 21.32 21.14 99.1 18 +ECA1262 29.11 28.83 99 23 +ECA1263 26.3 26.04 99 20 +ECA1264 25.22 24.88 98.6 21 +ECA1265 19.88 19.48 98 16 +ECA1266 50.19 49.5 98.6 38 +ECA1267 22.53 22.26 98.8 19 +ECA1268 28.01 27.5 98.2 21 +ECA1269 28.2 27.13 96.2 22 +ECA1270 22.72 21.8 96 19 +ECA1271 24.43 24.24 99.2 20 +ECA1272 30.09 29.84 99.1 24 +ECA1273 37.22 36.86 99 29 +ECA1274 21.12 20.89 98.9 18 +ECA1275 26.47 26.04 98.4 21 +ECA1276 32.86 32.44 98.7 27 +ECA1277 28.93 28.7 99.2 23 +ECA1278 29.15 28.91 99.2 24 +ECA1279 37.17 36.88 99.2 29 +ECA1281 43.3 42.98 99.3 35 +ECA1282 29.06 28.86 99.3 24 +ECA1283 38.07 37.77 99.2 29 +ECA1284 35.75 35.47 99.2 28 +ECA1285 29.4 29.14 99.1 24 +ECA1286 40.76 40.41 99.1 32 +ECA1287 28.3 28.05 99.1 22 +ECA1288 51.62 51.04 98.9 40 +ECA1289 33.46 32.89 98.3 26 +ECA1290 26.86 26.51 98.7 21 +ECA1291 36.57 36.39 99.5 29 +ECA1292 36.06 35.86 99.4 28 +ECA1293 55.17 54.71 99.2 45 +ECA1294 29.34 29.09 99.2 23 +ECA1295 37.42 37.21 99.4 30 +ECA1296 34.8 34.6 99.4 27 +ECA1297 25.67 25.47 99.2 24 +ECA1298 49.6 49.34 99.5 38 +ECA1316 66.18 65.97 99.7 47 +ECA1385 26.97 26.71 99.1 21 +ECA1389 34.54 34.36 99.5 28 +ECA1391 30.3 30.12 99.4 23 +ECA1409 46.05 45.52 98.8 29 +ECA1413 36.27 35.84 98.8 28 +ECA1415 27.68 27.29 98.6 22 +ECA1441 28.26 27.95 98.9 23 +ECA1465 25.56 25.14 98.4 20 +ECA1467 28.23 27.76 98.3 22 +ECA1493 42.71 36.01 84.3 28 +ECA1515 35.46 34.11 96.2 26 +ECA1689 25.94 25.76 99.3 19 +ECA1691 33.81 33.56 99.3 28 +ECA1693 23.73 23.58 99.3 21 +ECA1695 39.7 39.41 99.3 32 +ECA1709 21.19 20.28 95.7 18 +ECA1711 29.63 29.07 98.1 25 +ECA1713 35.68 34.78 97.5 29 +ECA1715 37.1 33.06 89.1 29 +ECA1717 35.32 32.59 92.3 28 +ECA1718 25.78 25.49 98.9 24 +ECA1721 27.11 26.87 99.1 20 +ECA1723 41.15 40.52 98.5 34 +ECA1725 42.03 41.53 98.8 27 +ECA1727 33.87 33.35 98.5 27 +ECA1729 21.63 21.31 98.5 18 +ECA1731 45.75 37.16 81.2 25 +ECA1733 37.52 37.21 99.2 25 +ECA1735 39.78 39.38 99 28 +ECA1737 33.67 33.18 98.5 28 +ECA1739 35.81 35.33 98.7 24 +ECA1741 41.25 40.76 98.8 34 +ECA1743 16.23 16 98.6 15 +ECA1745 41.55 41.18 99.1 28 +ECA1747 47.45 46.57 98.1 39 +ECA1749 44.39 43.6 98.2 36 +ECA1751 50.84 50.18 98.7 41 +ECA1753 34.98 34.46 98.5 30 +ECA1755 33.33 33.14 99.4 23 +ECA1757 42.31 42.07 99.4 28 +ECA1759 27.04 26.86 99.3 18 +ECA1761 27.02 26.71 98.8 19 +ECA1763 46.41 45.79 98.7 35 +ECA1765 25.81 25.53 98.9 18 +ECA1767 23.01 22.71 98.7 19 +ECA1769 49.42 48.63 98.4 40 +ECA1771 39.53 38.99 98.7 33 +ECA1779 30.36 30.05 99 24 +ECA1781 59.92 59.52 99.3 43 +ECA1783 30.81 30.56 99.2 25 +ECA1785 27.88 27.68 99.3 20 +ECA1787 18.85 18.71 99.2 14 +ECA1789 19.52 19.35 99.1 16 +ECA1791 23.12 22.93 99.2 17 +ECA1793 25.15 24.8 98.6 22 +ECA1795 24.35 24.12 99.1 18 +ECA1797 24.65 24.44 99.1 17 +ECA1799 28.64 28.49 99.5 24 +ECA1801 44.76 44.5 99.4 42 +ECA1803 31.69 31.5 99.4 25 +ECA1805 39.45 39.24 99.5 34 +ECA1807 34.1 33.79 99.1 27 +ECA1809 30.27 30.02 99.2 24 +ECA1811 26.45 26.21 99.1 22 +ECA1813 32.66 32.46 99.4 28 +ECA1815 34.97 34.73 99.3 28 +ECA1817 35.07 34.78 99.2 27 +ECA1819 28.35 28.13 99.2 23 +ECA1821 45.27 43.07 95.2 36 +ECA1823 24.65 22.72 92.1 19 +ECA1825 34.02 30.06 88.4 24 +ECA1827 30.78 27.07 87.9 21 +ECA1829 56.58 51.18 90.5 41 +ECA1831 178.32 18.98 10.6 16 +ECA1833 31.37 29.9 95.3 24 +ECA1835 43.79 43.41 99.1 30 +ECA1837 26.33 25.82 98.1 22 +ECA1839 39.32 38 96.7 31 +ECA1841 122.4 63.48 51.9 45 +ECA1843 48.16 26.34 54.7 24 +ECA1845 22.65 22.35 98.7 16 +ECA1847 26.51 24.29 91.6 22 +ECA1849 29.26 27.71 94.7 23 +ECA1851 43.69 41.63 95.3 36 +ECA1853 88.25 45.67 51.7 39 +ECA1855 15.37 15.08 98.1 15 +ECA1857 17.02 16.59 97.5 16 +ECA1859 118.55 44.12 37.2 33 +ECA1861 63.31 37.21 58.8 30 +ECA1863 51.16 34.67 67.8 27 +ECA1865 35.71 34.63 97 27 +ECA1867 38.92 37.62 96.6 29 +ECA1869 20.2 19.88 98.4 17 +ECA1871 41.18 40.81 99.1 32 +ECA1873 31.16 30.72 98.6 26 +ECA1875 44.71 44.4 99.3 35 +ECA1877 22.85 22.49 98.4 16 +ECA1878 22.02 21.78 98.9 20 +ECA1885 27.54 27.18 98.7 18 +ECA1887 40.38 39.82 98.6 26 +ECA1889 24.99 24.79 99.2 17 +ECA189 44.32 43.73 98.7 35 +ECA1891 26.16 25.98 99.3 17 +ECA1895 31.82 31.53 99.1 24 +ECA1897 46.84 46.48 99.2 34 +ECA190 57.19 56.47 98.8 44 +ECA1901 33.73 32.9 97.6 28 +ECA1903 41.78 40.11 96 32 +ECA1907 35.01 34.64 98.9 23 +ECA1909 28.71 28.28 98.5 19 +ECA191 47.12 46.1 97.8 42 +ECA1911 25.86 21.19 82 15 +ECA1913 22.71 22.49 99 15 +ECA1915 35.28 35.03 99.3 25 +ECA1917 57.11 56.55 99 51 +ECA1919 29.14 28.87 99.1 26 +ECA192 44.35 43.8 98.7 29 +ECA1921 26.35 26.15 99.3 20 +ECA1923 23.86 23.69 99.3 17 +ECA1925 46.75 45.98 98.4 36 +ECA1927 27.52 27.26 99.1 25 +ECA1929 21.79 21.61 99.2 17 +ECA193 49.83 49.17 98.7 41 +ECA1931 28.44 28.26 99.4 24 +ECA1933 32.61 32.4 99.4 26 +ECA1935 36.38 36.09 99.2 30 +ECA1937 31.57 31.36 99.3 27 +ECA1939 34.02 33.8 99.4 29 +ECA1943 40.66 40.4 99.4 33 +ECA1951 33.05 32.82 99.3 27 +ECA1953 36.33 36.08 99.3 28 +ECA1967 22.75 22.51 98.9 16 +ECA1969 44.99 44.58 99.1 30 +ECA1971 43.43 41.22 94.9 32 +ECA1973 29.22 28.6 97.9 24 +ECA1975 52.97 52.04 98.2 34 +ECA1977 29.93 29.68 99.2 20 +ECA1979 37.19 34.91 93.9 29 +ECA1981 31.97 29.1 91 20 +ECA1983 33.92 33.35 98.3 22 +ECA1985 25.99 24.33 93.6 17 +ECA1987 39.58 37.69 95.2 31 +ECA1989 28.34 27.79 98.1 19 +ECA1991 37.42 33.4 89.3 22 +ECA1995 36.72 36.33 98.9 25 +ECA1997 34.36 33.88 98.6 27 +ECA2041 39.88 39.63 99.4 32 +ECA2043 26.4 26.15 99.1 22 +ECA2065 28.87 28.64 99.2 23 +ECA2067 35.28 35.03 99.3 28 +ECA2069 28.88 28.65 99.2 23 +ECA2071 28.03 27.84 99.3 23 +ECA2073 39.56 39.28 99.3 33 +ECA2075 29.58 29.34 99.2 24 +ECA2079 20.45 20.26 99.1 17 +ECA2081 48.34 47.93 99.2 40 +ECA2085 37.56 37.25 99.2 31 +ECA2091 109.69 107.5 98 76 +ECA2095 40.02 39.23 98 31 +ECA2097 29.48 29.02 98.4 19 +ECA2099 34.42 34.01 98.8 29 +ECA2101 35.84 35.44 98.9 24 +ECA2103 40.95 40.5 98.9 27 +ECA2107 30.74 30.55 99.4 22 +ECA2109 24.72 24.42 98.8 18 +ECA2111 39.49 35.27 89.3 26 +ECA2117 37.44 37.11 99.1 25 +ECA2119 53.52 53.08 99.2 37 +ECA2121 34.46 34.07 98.9 22 +ECA2122 21.37 21.14 98.9 20 +ECA2125 30.36 29.98 98.8 20 +ECA2127 51.16 50.69 99.1 33 +ECA2131 25.47 25.24 99.1 19 +ECA2135 25.81 25.53 98.9 18 +ECA2139 23.66 23.4 98.9 16 +ECA2143 18.41 18.24 99.1 15 +ECA2147 20.61 20.41 99.1 15 +ECA2151 50.69 50.13 98.9 36 +ECA2155 45.17 44.76 99.1 32 +ECA2159 53.85 53.28 98.9 38 +ECA2163 21.27 21.04 98.9 16 +ECA2167 31.24 30.9 98.9 22 +ECA2171 19.3 19.11 99 14 +ECA2175 50.26 49.75 99 35 +ECA2179 26.8 26.46 98.7 19 +ECA2183 21.01 20.77 98.9 16 +ECA2187 30.08 29.76 98.9 20 +ECA2191 26.44 26 98.3 19 +ECA2195 32.39 31.92 98.5 22 +ECA2199 24.83 24.47 98.6 18 +ECA2203 26.45 26.08 98.6 18 +ECA2207 29.12 28.7 98.5 20 +ECA2247 30.52 30.28 99.2 26 +ECA2249 23.47 23.27 99.1 19 +ECA2251 34.95 34.7 99.3 28 +ECA2253 23.46 23.25 99.1 20 +ECA2255 22.37 22.19 99.2 18 +ECA2281 68.17 67.38 98.8 46 +ECA2283 19.46 19.22 98.8 14 +ECA2285 22.55 22.28 98.8 17 +ECA2287 29.28 28.92 98.8 21 +ECA2289 18.74 18.55 99 15 +ECA2291 31.33 30.98 98.9 24 +ECA2307 36.95 36.62 99.1 27 +ECA2309 46.13 45.56 98.8 41 +ECA2311 48.02 47.4 98.7 42 +ECA2313 18.29 18.09 98.9 14 +ECA2315 24.06 23.79 98.9 18 +ECA2319 35.25 34.89 99 24 +ECA2321 64.49 63.5 98.5 59 +ECA2322 20.11 19.96 99.3 17 +ECA2324 35.18 34.88 99.2 29 +ECA2326 28.47 28.27 99.3 24 +ECA2328 30.68 30.46 99.3 24 +ECA2330 19.7 19.54 99.2 17 +ECA2332 34.54 34.32 99.4 29 +ECA2334 30.93 30.5 98.6 25 +ECA2336 43.73 43.17 98.7 30 +ECA2338 34.11 33.65 98.7 28 +ECA2340 26.5 26.09 98.5 20 +ECA2342 28.44 28.25 99.3 23 +ECA2344 52.88 52.03 98.4 42 +ECA2348 29.89 29.64 99.2 24 +ECA2350 31.93 31.69 99.3 26 +ECA2352 23.13 22.99 99.4 20 +ECA2354 27.09 26.81 99 22 +ECA2356 22.57 22.35 99 19 +ECA2358 30.05 29.79 99.1 26 +ECA2360 23.67 23.48 99.2 18 +ECA2362 41.48 41.09 99 33 +ECA2365 38.08 37.76 99.2 26 +ECA2367 27.95 27.68 99 18 +ECA2375 32.56 31.87 97.9 22 +ECA2377 34.27 33.74 98.5 24 +ECA2401 38.36 27.45 71.6 20 +ECA2403 31.99 31.6 98.8 23 +ECA2405 30.72 30.35 98.8 20 +ECA2413 33.7 33.17 98.4 25 +ECA2415 35.42 34.88 98.5 24 +ECA2417 59.72 58.78 98.4 41 +ECA2419 41.51 41.13 99.1 30 +ECA2421 36.47 34.2 93.8 24 +ECA2423 40.92 40.54 99.1 30 +ECA2429 36.87 34.94 94.8 23 +ECA243 78.84 77.43 98.2 56 +ECA2431 91.41 90.75 99.3 64 +ECA2433 24.48 24.21 98.9 19 +ECA2435 85.25 84.55 99.2 59 +ECA2437 38.05 37.81 99.4 34 +ECA2439 18.27 18.05 98.8 14 +ECA2443 40.17 39.78 99 30 +ECA2445 26.38 25.98 98.5 20 +ECA245 50.76 50.35 99.2 39 +ECA2452 19.97 19.75 98.9 18 +ECA246 54.9 54.44 99.2 44 +ECA2467 35.19 34.75 98.7 25 +ECA2473 78.61 78.1 99.4 57 +ECA2475 80.55 79.95 99.3 59 +ECA2477 47.36 40.88 86.3 37 +ECA2479 49.44 49.05 99.2 45 +ECA248 44.84 44.46 99.2 31 +ECA2481 55.95 55.53 99.2 52 +ECA2482 26.64 23.83 89.4 22 +ECA2485 43.81 43 98.2 40 +ECA2487 39.67 39.07 98.5 36 +ECA2489 109.1 108.41 99.4 76 +ECA249 66.66 66.13 99.2 51 +ECA250 46.35 45.96 99.2 34 +ECA251 84.22 83.46 99.1 66 +ECA2521 23.71 23.58 99.5 16 +ECA2522 26.61 26.48 99.5 20 +ECA2523 24.25 24.11 99.4 18 +ECA2524 20.77 20.65 99.4 15 +ECA2525 24.09 23.94 99.4 17 +ECA2526 29.88 29.77 99.6 21 +ECA2527 25.85 25.71 99.5 19 +ECA2528 25.23 25.1 99.5 17 +ECA2529 34.67 34.53 99.6 25 +ECA253 46.8 46.77 99.9 35 +ECA2532 26.5 26.36 99.5 19 +ECA2533 42.83 42.6 99.5 30 +ECA2534 26.57 26.42 99.5 19 +ECA2535 35.2 35.07 99.6 25 +ECA2536 40.01 39.85 99.6 28 +ECA2537 31.59 31.41 99.5 22 +ECA254 19.46 18.64 95.8 13 +ECA2546 31.92 31.78 99.6 23 +ECA2547 25.6 24.46 95.6 18 +ECA2548 22.14 22.06 99.6 16 +ECA2549 45.72 45.61 99.8 32 +ECA2550 26.11 26.02 99.6 19 +ECA2551 30.32 30.21 99.6 22 +ECA2552 26.24 26.15 99.7 19 +ECA2553 25.44 25.31 99.5 18 +ECA2554 22.03 21.98 99.8 16 +ECA2555 22.89 22.81 99.7 16 +ECA2556 29.37 29.29 99.7 20 +ECA2557 25.88 25.76 99.6 19 +ECA2558 19.47 19.37 99.5 14 +ECA2559 29.9 29.81 99.7 21 +ECA2560 28.89 28.33 98.1 22 +ECA2561 27.34 27.19 99.5 20 +ECA2562 33.27 33.2 99.8 24 +ECA2563 26.81 26.76 99.8 20 +ECA2564 33.7 33.63 99.8 24 +ECA2565 35.1 35.04 99.8 26 +ECA2566 31.11 31.05 99.8 23 +ECA2567 26.82 26.77 99.8 19 +ECA2568 20.59 20.55 99.8 15 +ECA2569 22.35 21.56 96.5 16 +ECA2570 19.35 19.24 99.4 15 +ECA2571 21.59 21.44 99.3 16 +ECA2572 26.07 25.89 99.3 18 +ECA2573 29.06 28.87 99.3 20 +ECA2574 26.05 26 99.8 18 +ECA2575 18.3 18.27 99.8 14 +ECA2576 27.57 27.42 99.5 20 +ECA2577 29.21 29.15 99.8 21 +ECA2578 25.37 25.33 99.8 18 +ECA2579 22.67 22.53 99.4 17 +ECA2580 50.97 50.65 99.4 46 +ECA2581 33.08 32.86 99.3 23 +ECA2582 26.48 24.66 93.2 18 +ECA2583 50.43 50.15 99.4 45 +ECA2584 31.33 31.21 99.6 22 +ECA2585 30.39 30.26 99.6 22 +ECA2586 28.72 28.57 99.5 20 +ECA2589 24.69 24.6 99.6 17 +ECA259 46.64 46.24 99.1 36 +ECA2590 35.83 35.53 99.2 29 +ECA2591 25.36 25.29 99.7 19 +ECA2592 28.96 28.87 99.7 21 +ECA2593 21.94 21.79 99.3 15 +ECA2594 34.88 34.78 99.7 24 +ECA2595 25.82 25.64 99.3 19 +ECA2596 25.75 25.66 99.6 19 +ECA2597 19.29 19.18 99.5 14 +ECA2598 26.18 26.11 99.7 19 +ECA2599 30.18 29.94 99.2 24 +ECA2600 20.03 19.88 99.2 17 +ECA2601 66.64 66.12 99.2 53 +ECA2602 30.93 30.84 99.7 22 +ECA2603 34.95 34.8 99.6 24 +ECA2605 33.47 33.23 99.3 28 +ECA2606 42.01 41.84 99.6 34 +ECA2607 37.32 37.2 99.7 32 +ECA2608 39.1 38.9 99.5 31 +ECA2609 37.79 37.66 99.7 27 +ECA2610 34.27 34.14 99.6 25 +ECA2611 33.21 32.93 99.2 28 +ECA2612 37.29 37.01 99.2 29 +ECA2615 28.81 28.74 99.8 21 +ECA2641 55.06 54.86 99.6 36 +ECA2642 39.99 39.84 99.6 27 +ECA2643 29.1 28.95 99.5 24 +ECA2644 26.72 23.1 86.4 18 +ECA2648 37.1 36.97 99.6 31 +ECA2649 26.62 26.49 99.5 22 +ECA2650 21.3 21.25 99.7 18 +ECA2651 22.01 21.86 99.3 19 +ECA2652 30.01 29.88 99.6 25 +ECA2653 20.07 19.91 99.2 16 +ECA2654 30.64 30.52 99.6 26 +ECA2656 28.84 28.74 99.7 25 +ECA2657 26.4 26.25 99.4 23 +ECA2658 26.37 26.18 99.3 23 +ECA2659 37.98 37.67 99.2 31 +ECA2660 29.73 29.54 99.4 21 +ECA2672 36.39 36.26 99.6 26 +ECA2673 22.41 22.33 99.6 15 +ECA2674 25.84 25.75 99.6 19 +ECA2675 20.78 20.71 99.7 16 +ECA2676 29.62 29.48 99.5 21 +ECA347 42.94 42.22 98.3 43 +ECA348 51.96 49.24 94.8 50 +ECA349 42.7 42.21 98.9 38 +ECA350 20.28 20.13 99.3 23 +ECA36 41.14 40.31 98 32 +ECA363 54.68 52.7 96.4 47 +ECA369 24.47 24.09 98.4 27 +ECA372 54.51 53.32 97.8 48 +ECA393 29.48 29.25 99.2 33 +ECA394 48.8 48.25 98.9 48 +ECA395 22.6 22.27 98.6 25 +ECA396 38.15 37.72 98.9 40 +ECA397 34.03 33.64 98.9 37 +ECA398 25.13 24.84 98.8 28 +ECA399 37.35 36.84 98.6 39 +ECA551 38.44 38.11 99.1 16 +ECA552 71.03 70.54 99.3 47 +ECA571 85.16 84.55 99.3 56 +ECA572 68.45 67.98 99.3 47 +ECA589 86.18 85.62 99.4 72 +ECA592 68.17 67.15 98.5 45 +ECA593 66.84 65.76 98.4 45 +ECA594 75.56 74.35 98.4 47 +ECA615 26.17 25.95 99.2 29 +ECA616 57.6 57.09 99.1 51 +ECA640 40.15 39.87 99.3 38 +ECA694 25.35 25.21 99.4 22 +ECA695 16.01 15.91 99.4 15 +ECA701 31.34 30.5 97.3 30 +ECA702 31.45 30.15 95.8 29 +ECA703 30.75 30.46 99 29 +ECA704 29.77 29.5 99.1 30 +ECA705 37.11 36.71 98.9 36 +ECA706 35.66 35.23 98.8 35 +ECA707 34.55 34.22 99.1 33 +ECA708 31.64 31.3 98.9 31 +ECA709 37.15 36.79 99 36 +ECA710 39.82 39.39 98.9 38 +ECA711 31.56 31.27 99.1 31 +ECA712 36.82 36.43 98.9 35 +ECA713 28.33 28.08 99.1 27 +ECA714 31.07 30.79 99.1 31 +ECA715 36 35.63 99 35 +ECA716 35.12 34.75 98.9 34 +ECA717 36.01 33.76 93.8 33 +ECA718 36.45 33.05 90.7 32 +ECA719 31.7 31.38 99 31 +ECA720 35.44 35.08 99 34 +ECA721 34.17 27.31 79.9 26 +ECA722 41.19 39.92 96.9 38 +ECA723 41.39 40.28 97.3 38 +ECA724 36.54 34.78 95.2 34 +ECA725 29.81 28.95 97.1 29 +ECA726 31.39 31.01 98.8 30 +ECA727 31.84 31.4 98.6 32 +ECA728 32.6 32.19 98.7 31 +ECA729 39.47 38.73 98.1 37 +ECA730 32.91 32.48 98.7 31 +ECA731 35.23 34 96.5 33 +ECA732 32.98 32.19 97.6 32 +ECA733 35.52 35.15 99 34 +ECA734 33.04 32.75 99.1 32 +ECA735 28.82 28.53 99 29 +ECA736 30.91 30.52 98.8 30 +ECA737 34.95 34.56 98.9 34 +ECA738 38.14 37.69 98.8 36 +ECA739 37.61 37.18 98.9 37 +ECA740 44.07 33.47 75.9 31 +ECA741 34.31 33.8 98.5 33 +ECA742 38.53 37.96 98.5 36 +ECA743 33.26 32.89 98.9 33 +ECA744 30.33 29.98 98.9 30 +ECA745 44.64 44.06 98.7 41 +ECA746 35.24 34.76 98.7 35 +ECA747 30.65 30.28 98.8 30 +ECA748 37.95 37.6 99.1 36 +ECA749 33.67 33.29 98.9 33 +ECA750 35.33 34.96 99 34 +ECA751 34.43 33.97 98.7 34 +ECA752 36.58 36.26 99.1 35 +ECA753 32.04 31.5 98.3 31 +ECA754 36.14 34.19 94.6 33 +ECA755 32.14 31.43 97.8 31 +ECA756 35.89 27.95 77.9 27 +ECA757 35.02 34.7 99.1 34 +ECA758 38.34 34.54 90.1 33 +ECA759 32.53 31.89 98 32 +ECA760 39.69 39.23 98.8 38 +ECA761 37.64 36.23 96.3 36 +ECA762 39.52 39.12 99 37 +ECA763 34.09 33.85 99.3 33 +ECA764 33.11 32.63 98.5 32 +ECA765 35.73 33.77 94.5 34 +ECA766 32.3 31.3 96.9 31 +ECA767 33.78 30.48 90.2 30 +ECA768 38.01 33.88 89.1 32 +ECA769 38.12 37.08 97.3 36 +ECA770 34.74 30.7 88.3 30 +ECA771 35.56 32.02 90 32 +ECA772 35.45 32.82 92.6 31 +ECA773 34.39 32.79 95.4 32 +ECA774 40.99 34.65 84.5 34 +ECA775 35.6 34.16 96 34 +ECA776 41.29 40.87 99 39 +ECA777 35.67 34.98 98.1 35 +ECA778 47.44 46.51 98.1 43 +ECA779 29.92 23.09 77.2 23 +ECA780 38.48 37.79 98.2 37 +ECA781 44.06 43.25 98.1 41 +ECA782 36.31 35.7 98.3 35 +ECA783 36.11 35.68 98.8 35 +ECA784 33.96 32.87 96.8 33 +ECA785 33.3 32.99 99.1 33 +ECA786 44.49 44 98.9 42 +ECA787 40.38 35.78 88.6 35 +ECA807 35.13 34.8 99.1 34 +ECA808 35.01 34.71 99.2 34 +ECA809 38.15 37.76 99 38 +ECA810 33.66 33.29 98.9 33 +ECA811 37.5 36.79 98.1 35 +ECA812 44.31 43.89 99 42 +ECA813 38.79 38.36 98.9 37 +ECA822 32.76 31.37 95.8 31 +ECA922 29.88 29.65 99.2 31 +ECA923 35.87 35.59 99.2 37 +ECA924 23.93 23.77 99.3 26 +ECA925 27.63 27.4 99.1 30 +ECA926 28.25 27.76 98.3 29 +ECA927 26.66 26.4 99.1 29 +ECA928 28.48 28.29 99.3 30 +ECA930 28.07 27.88 99.3 30 +ED3005 80.67 79.87 99 58 +ED3011 60.19 59.48 98.8 45 +ED3012 37.73 37.58 99.6 29 +ED3017 63.11 62.64 99.3 47 +ED3040 66.61 66.25 99.5 49 +ED3046 66.63 65.98 99 50 +ED3048 66.87 66.53 99.5 49 +ED3049 60.54 58.08 95.9 47 +ED3052 68.74 68.19 99.2 53 +ED3073 73.77 73.26 99.3 53 +ED3077 66.69 66.22 99.3 51 +EG4347 57.15 56.92 99.6 43 +EG4349 46.73 46.25 99 37 +EG4724 48.82 48.37 99.1 38 +EG4725 63.18 62.46 98.9 42 +EG4946 46.22 44.21 95.6 33 +GXW1 47.16 46.81 99.3 35 +JR4305 25.44 25.26 99.3 23 +JT11398 71.07 70.74 99.5 57 +JU1088 62.44 61.74 98.9 45 +JU1172 54.41 53.99 99.2 39 +JU1200 55.51 55.4 99.8 42 +JU1212 49.33 48.99 99.3 37 +JU1213 42.23 41.87 99.1 33 +JU1242 74.09 73.54 99.3 50 +JU1246 66.21 65.72 99.3 47 +JU1249 46.55 45.87 98.5 40 +JU1395 24.86 24.75 99.5 21 +JU1400 82.12 76.96 93.7 53 +JU1409 63.81 63.47 99.5 48 +JU1440 61.15 60.77 99.4 47 +JU1491 62.36 61.9 99.3 47 +JU1511 49.69 49.43 99.5 30 +JU1516 94.73 93.2 98.4 63 +JU1530 37.37 37.14 99.4 30 +JU1543 30.03 29.94 99.7 34 +JU1568 78.99 78.82 99.8 59 +JU1580 93.62 92.43 98.7 72 +JU1581 46.26 45.89 99.2 34 +JU1586 62.68 62.44 99.6 49 +JU1652 41.36 40.97 99 31 +JU1656 28.71 28.43 99 25 +JU1666 50.67 50.09 98.9 45 +JU1762 40.64 40.24 99 37 +JU1770 54.46 53.93 99 49 +JU1792 56.77 56.42 99.4 52 +JU1793 55.23 54.48 98.6 51 +JU1807 58.72 57.43 97.8 52 +JU1808 42.41 41.99 99 39 +JU1896 61.97 61.6 99.4 45 +JU1920 42.32 41.8 98.8 39 +JU1922 38.31 36.55 95.4 34 +JU1924 31.48 31.13 98.9 28 +JU1926 48.89 48.31 98.8 30 +JU1929 36.79 36.32 98.7 34 +JU1931 79.35 78.8 99.3 44 +JU1934 53.19 52.74 99.2 49 +JU1941 38.68 38.44 99.4 24 +JU1960 60.35 59.8 99.1 49 +JU2001 76.77 75.84 98.8 60 +JU2007 59.54 59.13 99.3 47 +JU2016 47.57 43.4 91.2 40 +JU2017 46.71 42.76 91.5 39 +JU2106 50.18 49.57 98.8 46 +JU2131 36.25 34.07 94 32 +JU2139 31.41 31.02 98.8 28 +JU2141 42.22 41.87 99.2 39 +JU2151 33.58 33.12 98.6 31 +JU2234 51.19 50.69 99 47 +JU2250 39.81 36.84 92.5 34 +JU2257 35.01 34.85 99.5 31 +JU2287 34.51 34.23 99.2 33 +JU2316 62.62 52.92 84.5 48 +JU2460 66.18 60.69 91.7 50 +JU2464 36.31 35.96 99 32 +JU2466 57.43 56.78 98.9 54 +JU2467 58.41 57.89 99.1 49 +JU2468 38.42 38.07 99.1 32 +JU2478 60.61 56.39 93 50 +JU2513 41.85 40.93 97.8 35 +JU2519 56.07 47.3 84.4 46 +JU2522 44.38 41.32 93.1 35 +JU2526 39.04 34.95 89.5 30 +JU2527 57.53 57.06 99.2 52 +JU2534 77.16 76.71 99.4 64 +JU2565 34.86 34.68 99.5 33 +JU2566 49.3 49.06 99.5 44 +JU2570 56.67 56.21 99.2 51 +JU2572 62.3 61.77 99.2 63 +JU2575 46.94 45.28 96.5 42 +JU2576 59.29 58.54 98.7 53 +JU2578 43.93 43.51 99 40 +JU258 77.62 76.48 98.5 56 +JU2581 35.23 34.8 98.8 31 +JU2586 45.73 45.16 98.8 42 +JU2587 58.31 56.75 97.3 36 +JU2592 45.35 44.89 99 41 +JU2593 61.28 60.34 98.5 50 +JU2600 43.98 43.46 98.8 40 +JU2604 47.51 46.92 98.8 43 +JU2605 34.44 33.96 98.6 32 +JU2610 58.79 58.18 99 49 +JU2619 44.93 38.45 85.6 38 +JU2800 70.14 69.59 99.2 58 +JU2802 35.9 35.5 98.9 33 +JU2811 42.82 42.33 98.9 37 +JU2825 51.01 50.42 98.9 50 +JU2828 33.63 33.27 98.9 30 +JU2829 50.22 49.66 98.9 45 +JU2830 37.85 37.22 98.3 34 +JU2838 59.68 55.87 93.6 50 +JU2841 47.83 42.07 88 39 +JU2853 58.53 58.21 99.4 53 +JU2860 41.6 39.29 94.5 36 +JU2862 51.63 48.94 94.8 45 +JU2866 61.25 60.66 99 50 +JU2878 86.16 85.3 99 69 +JU2879 70.51 69.65 98.8 57 +JU2906 35.1 34.85 99.3 32 +JU2907 42.49 42.23 99.4 39 +JU2908 40.96 40.72 99.4 38 +JU310 61.46 61.1 99.4 43 +JU311 51.68 51.41 99.5 38 +JU312 51.41 51.01 99.2 45 +JU3125 34.89 34.69 99.4 38 +JU3127 25.19 24.89 98.8 28 +JU3128 25.58 25.34 99 28 +JU3131 17.85 17.71 99.2 21 +JU3132 54.95 54 98.3 57 +JU3133 25.18 25.07 99.6 28 +JU3134 45.39 45.12 99.4 49 +JU3135 23.52 23.22 98.7 26 +JU3136 31.51 31.35 99.5 35 +JU3137 32.03 31.87 99.5 36 +JU3138 29.4 28.62 97.3 32 +JU3139 17.39 17.22 99 20 +JU3140 50.04 49.64 99.2 49 +JU3141 45.32 43.92 96.9 44 +JU3142 22.32 22.16 99.3 25 +JU3144 23.86 23.64 99.1 26 +JU315 80.77 80.32 99.4 60 +JU3166 81.52 80.32 98.5 53 +JU3167 53.54 52.78 98.6 39 +JU3169 64.89 64.06 98.7 43 +JU3224 20.19 20.08 99.4 22 +JU3225 27.08 26.97 99.6 29 +JU3226 22.76 22.49 98.8 24 +JU3227 30.6 30.45 99.5 33 +JU3228 35.53 35.35 99.5 38 +JU323 70.96 70.48 99.3 52 +JU3271 37.94 37.65 99.2 37 +JU3280 31.11 30.87 99.2 31 +JU3282 35.39 35.15 99.3 35 +JU3291 35.68 35.37 99.1 35 +JU3318 23.49 23.31 99.2 27 +JU3398 50.78 50.19 98.8 31 +JU3399 80.07 79.57 99.4 49 +JU3400 47.26 46.94 99.3 32 +JU3401 57.67 57.18 99.2 35 +JU3402 78.9 78.41 99.4 50 +JU3403 54.83 54.44 99.3 37 +JU345 28.49 28.28 99.3 26 +JU346 60.57 59.3 97.9 44 +JU360 69.83 68.95 98.7 54 +JU363 80.34 78.49 97.7 62 +JU367 69.05 68.74 99.5 52 +JU3785 35.2 30.81 87.5 31 +JU3786 25.74 25.6 99.5 27 +JU3791 68.22 67.25 98.6 71 +JU3795 53.11 52.94 99.7 55 +JU393 51.46 51 99.1 38 +JU394 58.87 58.27 99 51 +JU397 73.79 73.33 99.4 54 +JU4047 44.58 44.3 99.4 32 +JU4048 28.12 27.99 99.5 18 +JU4054 35.15 35.02 99.6 24 +JU406 48.5 48.21 99.4 40 +JU4067 28.87 28.75 99.6 19 +JU4069 27.03 26.91 99.6 19 +JU4071 21.48 21.4 99.7 14 +JU4072 27.2 27.08 99.6 19 +JU4073 36.88 36.75 99.6 24 +JU4074 25.81 25.71 99.6 18 +JU4075 21.91 21.81 99.6 15 +JU4082 32.83 32.65 99.4 22 +JU4085 27.61 27.44 99.4 20 +JU4098 28.74 28.68 99.8 20 +JU440 55.89 55.68 99.6 42 +JU561 53.92 53.56 99.3 41 +JU642 59.1 58.74 99.4 41 +JU751 74.35 73.45 98.8 55 +JU755 37.44 36.34 97.1 24 +JU774 62.86 62.27 99.1 46 +JU775 53.01 52.25 98.6 40 +JU778 108.21 106.71 98.6 83 +JU782 85.47 84.35 98.7 68 +JU792 96.64 95.94 99.3 76 +JU830 75.55 74.73 98.9 56 +JU847 43.37 42.65 98.3 33 +KR314 71.48 70.86 99.1 52 +LKC34 60.25 59.91 99.4 45 +MY1 60.12 59.7 99.3 45 +MY10 52.71 52.1 98.8 41 +MY16 59.63 58.75 98.5 44 +MY18 72.15 71.31 98.8 51 +MY2001 56.87 56.46 99.3 53 +MY2004 54.39 53.95 99.2 44 +MY2011 31.92 31.67 99.2 22 +MY2014 61.15 60.66 99.2 49 +MY2022 46.18 45.83 99.2 35 +MY2024 27.22 26.96 99 25 +MY2042 49.68 49.32 99.3 39 +MY2050 49.44 49.04 99.2 43 +MY2051 28.32 28.04 99 26 +MY2054 47.96 47.55 99.2 34 +MY2078 24.21 23.96 99 23 +MY2079 50.45 50.05 99.2 30 +MY2097 52.79 52.44 99.3 50 +MY2099 21.98 21.73 98.9 21 +MY2109 39.86 39.37 98.8 27 +MY2121 38.81 38.48 99.1 25 +MY2137 43.91 43.44 98.9 40 +MY2138 46 45.68 99.3 36 +MY2142 42.95 42.67 99.4 31 +MY2143 43.56 43.26 99.3 37 +MY2144 30.05 29.85 99.3 20 +MY2147 44.59 44.3 99.4 35 +MY2198 31.37 31.17 99.4 21 +MY2199 55.18 54.62 99 47 +MY2208 25.91 25.75 99.4 17 +MY2212 53.31 52.78 99 48 +MY2224 27.4 27.2 99.3 26 +MY2239 50.28 50 99.4 48 +MY2282 49.48 49.16 99.3 39 +MY2288 23.08 22.85 99 22 +MY2291 36.19 35.86 99.1 33 +MY2294 49.46 49.18 99.4 37 +MY23 87.63 86.52 98.7 64 +MY2338 37.89 37.61 99.3 35 +MY2339 57.76 57.28 99.2 46 +MY2344 29.56 29.37 99.4 20 +MY2347 24.91 24.7 99.2 24 +MY2373 46.45 46.06 99.2 42 +MY2406 55.95 55.57 99.3 46 +MY2434 48.19 47.92 99.4 38 +MY2443 45.91 45.62 99.4 33 +MY2453 93.27 92.32 99 80 +MY2479 50.47 50.12 99.3 47 +MY2481 31 30.77 99.3 24 +MY2491 37.47 37.24 99.4 26 +MY2502 49.25 48.85 99.2 39 +MY2530 38.2 37.83 99.1 35 +MY2532 54.35 54.01 99.4 44 +MY2535 57.99 57.65 99.4 46 +MY2541 37.09 36.78 99.2 26 +MY2573 50.53 50.19 99.3 43 +MY2579 38.01 37.69 99.2 27 +MY2585 40.59 40.28 99.2 28 +MY2622 50.85 50.46 99.2 48 +MY2623 49.38 49.02 99.3 37 +MY2630 44.86 44.51 99.2 38 +MY2635 40.1 39.74 99.1 31 +MY2636 54.08 53.66 99.2 46 +MY2640 46.63 46.25 99.2 39 +MY2679 47.01 46.57 99.1 37 +MY2681 36.07 35.78 99.2 24 +MY2684 46.88 46.41 99 42 +MY2685 48.06 47.54 98.9 43 +MY2688 70.14 69.63 99.3 58 +MY2689 37.67 37.29 99 25 +MY2691 55.7 55.23 99.2 46 +MY2692 42.63 41.96 98.4 39 +MY2693 65.87 65.29 99.1 51 +MY2713 64.18 63.62 99.1 53 +MY2719 24.55 24.29 98.9 23 +MY2741 46.26 45.67 98.7 35 +MY508 38.85 38.61 99.4 37 +MY518 41.32 40.97 99.2 29 +MY524 35.64 35.28 99 33 +MY538 31.19 30.88 99 22 +MY559 36.12 35.89 99.4 26 +MY561 57.42 56.8 98.9 51 +MY564 33.32 32.97 99 31 +MY570 54.24 53.8 99.2 45 +MY579 38.72 38.5 99.4 33 +MY589 46.29 45.99 99.3 36 +MY673 52.7 52.24 99.1 50 +MY679 31.15 30.83 99 29 +MY684 23.77 23.54 99 22 +MY710 38.22 38 99.4 32 +MY713 40.91 40.57 99.2 33 +MY741 32.23 31.99 99.3 22 +MY772 70.82 70.32 99.3 58 +MY792 50.41 50.01 99.2 40 +MY795 37.46 37.01 98.8 34 +MY803 37.13 36.71 98.9 34 +MY804 43.36 43.12 99.4 42 +MY819 63.15 62.64 99.2 53 +MY864 55.72 55.28 99.2 42 +MY881 46.06 45.76 99.4 37 +MY882 46.2 45.85 99.2 35 +MY887 26.23 25.99 99.1 24 +MY904 37.93 37.69 99.4 35 +MY920 74.34 73.84 99.3 56 +MY934 45.54 45.18 99.2 41 +MY965 46.22 45.85 99.2 37 +MY990 32.27 31.99 99.2 30 +MY991 27.07 26.85 99.2 18 +N2 35.13 35.12 99.9 28 +NIC1 54.59 45.27 82.9 35 +NIC1049 39.6 39.38 99.5 36 +NIC1107 65.14 64.8 99.5 59 +NIC1119 44.38 44.09 99.3 43 +NIC1604 34.54 34.4 99.6 34 +NIC166 48.02 45.82 95.4 35 +NIC1779 51.96 51.69 99.5 46 +NIC1780 64.38 63.92 99.3 57 +NIC1781 35.25 30.84 87.5 28 +NIC1782 24.9 24.77 99.5 23 +NIC1783 67.87 67.42 99.3 59 +NIC1785 58.36 58.03 99.4 50 +NIC1786 74.57 74.11 99.4 65 +NIC1787 109.46 65.81 60.1 59 +NIC1788 69 65.24 94.5 57 +NIC1789 77.39 76.77 99.2 67 +NIC1790 26.76 26.58 99.3 19 +NIC1791 36.18 35.96 99.4 24 +NIC1792 24.78 24.63 99.4 18 +NIC1793 76.7 76.11 99.2 64 +NIC1794 86.43 85.94 99.4 75 +NIC1795 56.33 55.94 99.3 48 +NIC1796 54.41 53.88 99 47 +NIC1797 53.15 52.25 98.3 42 +NIC1798 59.65 59.18 99.2 55 +NIC1799 70.71 70.17 99.2 61 +NIC1800 62.99 62.63 99.4 54 +NIC1801 57.54 57.04 99.1 48 +NIC1802 58.59 58.2 99.3 52 +NIC1803 58.06 54.94 94.6 50 +NIC1804 31.59 28.78 91.1 20 +NIC1805 59.76 59.48 99.5 53 +NIC1806 71.18 70.71 99.3 62 +NIC1807 27.05 26.87 99.3 18 +NIC1808 60.48 60.15 99.5 55 +NIC1809 77.5 77.18 99.6 67 +NIC1810 49.3 44.54 90.4 41 +NIC1811 62.46 49.4 79.1 44 +NIC1812 52.99 52.71 99.5 47 +NIC1832 74.76 74.22 99.3 65 +NIC195 58.59 56.59 96.6 42 +NIC196 64.84 57.38 88.5 43 +NIC197 53.39 47.87 89.7 36 +NIC1977 27.92 25.86 92.6 18 +NIC198 44.03 35.96 81.7 27 +NIC1980 36.23 34.03 93.9 24 +NIC1985 40.04 39.73 99.2 28 +NIC199 47.81 47.08 98.5 36 +NIC2 67 49.33 73.6 39 +NIC200 56.73 54.43 95.9 39 +NIC2002 33.87 33.76 99.7 23 +NIC2004 25.5 25.41 99.7 18 +NIC2011 33.59 31.84 94.8 22 +NIC207 54.91 54.58 99.4 41 +NIC231 56.31 51.06 90.7 37 +NIC232 90.45 89.75 99.2 70 +NIC236 35.53 33.89 95.4 27 +NIC237 57.51 55.68 96.8 41 +NIC242 37.34 37.04 99.2 31 +NIC251 40.1 39.59 98.7 31 +NIC252 54.49 53.74 98.6 51 +NIC255 62.38 60.4 96.8 33 +NIC256 28.93 28.64 99 25 +NIC258 39.48 38.98 98.7 32 +NIC259 55.08 53.82 97.7 51 +NIC260 80.31 79.45 98.9 67 +NIC261 44.99 44.45 98.8 37 +NIC262 63.6 60.85 95.7 34 +NIC263 27.2 26.86 98.8 23 +NIC265 60.44 58.81 97.3 56 +NIC266 36.17 35.82 99 30 +NIC267 43.96 43.72 99.4 38 +NIC268 41.19 40.66 98.7 34 +NIC269 48.67 48.05 98.7 40 +NIC270 38.32 38.09 99.4 32 +NIC271 35.71 35.35 99 31 +NIC272 40.09 39.1 97.5 34 +NIC273 33.28 32.86 98.8 25 +NIC274 77.54 66.52 85.8 50 +NIC275 62.06 61.46 99 61 +NIC276 51.81 51.14 98.7 40 +NIC277 37.97 37.72 99.4 32 +NIC3 62.19 34.59 55.6 27 +NIC4 26.7 22.31 83.6 19 +NIC501 35.19 34.78 98.8 33 +NIC508 41.21 40.96 99.4 38 +NIC511 50.98 50.66 99.4 47 +NIC512 44.27 43.81 99 40 +NIC513 86.17 85.31 99 75 +NIC514 36.17 35.69 98.7 34 +NIC515 42.98 42.39 98.6 39 +NIC521 31.01 30.74 99.1 28 +NIC522 35.3 34.8 98.6 33 +NIC523 50.16 49.69 99.1 46 +NIC526 34.67 34.38 99.2 32 +NIC527 33.37 33.08 99.1 31 +NIC528 78.89 77.98 98.8 70 +NIC529 44.6 44.33 99.4 40 +PB303 47.3 46.93 99.2 36 +PS2025 55.84 55.14 98.8 41 +PX179 38.26 38.15 99.7 30 +QG2075 67.14 66.32 98.8 59 +QG2810 39.48 38.68 98 38 +QG2811 39.7 39.5 99.5 39 +QG2812 35.29 35.07 99.4 33 +QG2813 35 34.81 99.5 34 +QG2818 41.91 41.73 99.6 40 +QG2823 38.94 38.72 99.4 38 +QG2824 35.63 35.46 99.5 35 +QG2825 33.55 33.34 99.3 34 +QG2826 38.65 38.44 99.5 37 +QG2827 39.02 38.71 99.2 38 +QG2828 33.43 33.13 99.1 33 +QG2829 36.05 35.77 99.2 36 +QG2830 38.16 37.9 99.3 37 +QG2831 34.43 34.24 99.5 34 +QG2832 37.93 37.54 99 37 +QG2833 36.24 34.67 95.7 34 +QG2834 32.24 32 99.3 32 +QG2835 37.84 37.56 99.2 37 +QG2836 34.11 33.81 99.1 33 +QG2837 42.78 42.47 99.3 41 +QG2838 44.98 44.65 99.3 42 +QG2839 39.11 38.85 99.3 37 +QG2840 32.57 32.4 99.5 32 +QG2841 41.68 41.22 98.9 39 +QG2842 32.81 32.53 99.2 32 +QG2843 35.41 35.21 99.5 34 +QG2844 34.44 34.14 99.1 34 +QG2845 30.81 30.54 99.1 30 +QG2846 36.14 35.84 99.2 36 +QG2850 39.22 39.02 99.5 38 +QG2851 36.46 36.07 98.9 36 +QG2852 31.43 30.96 98.5 31 +QG2853 32.7 32.45 99.2 32 +QG2854 40.49 40.26 99.4 39 +QG2855 34.53 34.3 99.3 34 +QG2856 34.78 34.57 99.4 34 +QG2857 31.37 31.13 99.2 31 +QG2858 37.01 36.76 99.3 36 +QG2859 29.65 29.47 99.4 29 +QG2872 37.59 37.33 99.3 37 +QG2873 38.88 38.65 99.4 37 +QG2874 34.94 34.74 99.4 34 +QG2875 43.26 42.97 99.3 42 +QG2876 34.04 33.7 99 33 +QG2877 39.69 39.42 99.3 39 +QG2878 37.76 37.53 99.4 36 +QG2927 35.74 35.43 99.1 35 +QG2928 21.07 20.52 97.4 21 +QG2931 35.8 35.64 99.6 35 +QG2932 38.89 38.65 99.4 39 +QG4003 58.62 58.39 99.6 60 +QG4004 36.54 36.4 99.6 40 +QG4005 50.83 50.66 99.7 55 +QG4006 55.89 55.75 99.8 60 +QG4008 65.45 65.26 99.7 68 +QG4009 54.13 53.97 99.7 58 +QG4010 84.92 84.64 99.7 86 +QG4011 48.44 48.2 99.5 53 +QG4012 53.48 53.22 99.5 56 +QG4014 57.82 57.38 99.2 63 +QG4015 48.89 48.71 99.6 52 +QG4016 47.48 47.32 99.7 50 +QG4017 80.92 80.45 99.4 85 +QG4018 54.94 54.66 99.5 58 +QG4019 66.88 66.58 99.6 71 +QG4021 51.53 51.32 99.6 54 +QG4076 25.32 25.2 99.5 27 +QG4077 24.86 24.75 99.5 26 +QG4078 36.29 36.11 99.5 38 +QG4079 38.57 38.4 99.5 40 +QG4080 44.58 44.4 99.6 46 +QG4103 32.62 32.41 99.4 33 +QG4134 37.24 37.13 99.7 38 +QG4135 40.12 39.95 99.6 40 +QG4137 32.43 32.32 99.6 33 +QG4138 36.85 36.71 99.6 38 +QG4139 33.84 33.71 99.6 34 +QG4151 39.56 39.38 99.5 40 +QG4158 28.81 28.69 99.6 29 +QG4159 35.4 35.31 99.7 37 +QG4160 29.33 29.27 99.8 29 +QG4161 28.53 28.46 99.8 29 +QG4162 27.48 27.36 99.6 27 +QG4163 28.08 27.92 99.4 28 +QG4166 27.59 27.47 99.6 28 +QG4182 39.58 39.45 99.7 42 +QG4184 30.2 30.11 99.7 30 +QG4185 27.83 27.71 99.6 28 +QG4186 39.33 39.21 99.7 39 +QG4193 41.6 41.45 99.6 41 +QG4194 25.9 25.8 99.6 27 +QG4226 32.28 32.13 99.5 32 +QG4228 26.79 26.68 99.6 26 +QG536 50.54 50.03 99 40 +QG537 67.07 66.49 99.1 48 +QG538 41.35 40.99 99.1 31 +QG556 57.78 57.09 98.8 45 +QG557 59.82 59.28 99.1 48 +QG558 40.83 40.35 98.8 31 +QW947 37.02 36.2 97.8 32 +QX1211 79.47 77.91 98 55 +QX1212 58.52 57.93 99 42 +QX1213 64.32 63.63 98.9 47 +QX1214 38.95 38.52 98.9 31 +QX1215 63.19 61.93 98 46 +QX1216 67.3 65.92 98 47 +QX1233 68.61 68.02 99.1 49 +QX1791 70.85 69.66 98.3 51 +QX1792 64.78 64.03 98.8 48 +QX1793 48.88 48.25 98.7 39 +QX1794 92.08 90.5 98.3 66 +RC301 38.08 37.82 99.3 29 +TWN2530 45.05 44.81 99.5 35 +TWN2542 64.92 64.47 99.3 57 +TWN2794 60.39 60.03 99.4 44 +TWN2803 47.17 46.87 99.4 40 +WN2001 42.2 41.77 99 29 +WN2002 57.16 56.69 99.2 43 +WN2010 54.44 53.77 98.8 39 +WN2011 60.33 59.63 98.8 45 +WN2013 65.53 64.77 98.8 46 +WN2014 50 49.46 98.9 37 +WN2016 75.65 74.76 98.8 55 +WN2017 68.8 67.6 98.2 46 +WN2018 50.23 49.34 98.2 37 +WN2019 62.5 61.68 98.7 48 +WN2020 53.45 52.72 98.6 39 +WN2021 28.52 28.14 98.7 20 +WN2033 39.87 39.55 99.2 41 +WN2035 33.77 33.39 98.9 31 +WN2039 57.04 56.36 98.8 50 +WN2050 38.22 37.82 98.9 35 +WN2056 44.47 43.98 98.9 40 +WN2059 33.35 26.91 80.7 29 +WN2060 80.25 79.62 99.2 48 +WN2061 45.45 45.27 99.6 30 +WN2062 59.63 58.88 98.7 44 +WN2063 81.95 81.2 99.1 49 +WN2064 87.56 86.76 99.1 44 +WN2065 70.51 69.99 99.3 49 +WN2066 85.22 84.63 99.3 54 +WN2067 42.31 41.88 99 46 +WN2068 39.16 38.95 99.5 42 +WN2069 37.97 37.73 99.4 41 +WN2070 21.46 21.35 99.5 25 +WN2071 50.47 50.24 99.5 31 +WN2073 42.94 42.8 99.7 27 +WN2074 24.48 24.35 99.5 28 +WN2075 34.58 34.46 99.6 38 +WN2076 19.55 19.47 99.6 22 +WN2077 26.6 26.51 99.7 18 +WN2078 30.53 30.47 99.8 22 +WN2079 41.19 41.02 99.6 27 +WN2080 41.94 39.45 94.1 26 +WN2081 25.05 24.99 99.7 17 +WN2082 31.38 31.29 99.7 22 +WN2083 25.79 25.69 99.6 18 +WN2086 28.25 27.87 98.7 20 +WN2088 34.03 33.86 99.5 23 +WN2089 23.17 23.04 99.4 17 +WN2090 34.32 34.18 99.6 23 +WN2091 26.7 26.59 99.6 19 +WN2092 27.13 27.02 99.6 18 +WN2093 25.25 25.16 99.6 18 +WN2095 20.27 20.22 99.8 14 +WN2096 24.51 24.26 99 16 +XZ1513 42.62 40.08 94 37 +XZ1514 47.3 42.96 90.8 40 +XZ1515 73.88 70.4 95.3 66 +XZ1516 42.31 39.82 94.1 35 +XZ1672 25.38 25.22 99.4 27 +XZ1734 34.42 34.12 99.1 36 +XZ1735 27.35 27.2 99.5 29 +XZ1756 28.42 28.23 99.3 30 +XZ2018 29.67 29.58 99.7 33 +XZ2019 34.81 34.42 98.9 37 +XZ2020 24.1 23.76 98.6 25 +XZ2210 69.47 61.96 89.2 53 +XZ2211 45.38 44.96 99.1 41 +XZ2212 63.26 62.1 98.2 55 +XZ2213 30.19 29.86 98.9 28 diff --git a/base/static/reports/20210121/release_notes.md b/base/static/reports/20210121/release_notes.md new file mode 100644 index 00000000..420b2076 --- /dev/null +++ b/base/static/reports/20210121/release_notes.md @@ -0,0 +1 @@ +The 20200121 release includes genotypes from whole-genome sequences and reduced representation (RAD) sequencing. Genotypes are compared for concordance, and strains that are 99.95% identical to each other are [grouped into isotypes]({{ url_for("primary.help_item", filename="FAQ", _anchor="strain-groups") }}). One strain within each isotype is the reference strain for that isotype. To look up isotype assignment, see Alignment Data tab. All isotype reference strains are [available on CeNDR]({{ url_for("strain.strain_catalog") }}). From eef2f1996959f88db12106d182517d7772b4c5f9 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 29 Mar 2021 14:12:57 -0500 Subject: [PATCH 008/288] update config and constant paths --- base/config.py | 2 +- base/constants.py | 3 +++ base/models.py | 6 +++--- base/templates/_includes/footer.html | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/base/config.py b/base/config.py index 34dab0ae..9d401c53 100644 --- a/base/config.py +++ b/base/config.py @@ -20,7 +20,7 @@ # BUILDS AND RELEASES # The first release is the current release # (RELEASE, ANNOTATION_GENOME) -RELEASES = [("20200815", "WS276"), ("20180527", "WS263"), ("20170531", "WS258"), ("20160408", "WS245")] +RELEASES = [("20210121", "WS276"), ("20200815", "WS276"), ("20180527", "WS263"), ("20170531", "WS258"), ("20160408", "WS245")] # The most recent release DATASET_RELEASE, WORMBASE_VERSION = RELEASES[0] diff --git a/base/constants.py b/base/constants.py index ab59eb01..a9f32cda 100644 --- a/base/constants.py +++ b/base/constants.py @@ -9,6 +9,9 @@ WORMBASE_VERSION = 'WS276' +STRAIN_PHOTO_PATH = 'photos/Celegans/' +STRAIN_THUMBNAIL_PATH = 'photos/Celegans/thumbnails/' + USER_ROLES = [('user', 'User'), ('admin', 'Admin')] class PRICES: diff --git a/base/models.py b/base/models.py index e40e4d3d..a6479f16 100644 --- a/base/models.py +++ b/base/models.py @@ -12,7 +12,7 @@ from sqlalchemy import or_, func from werkzeug.security import safe_str_cmp -from base.constants import GOOGLE_CLOUD_BUCKET +from base.constants import GOOGLE_CLOUD_BUCKET, STRAIN_THUMBNAIL_PATH, STRAIN_PHOTO_PATH from base.extensions import sqlalchemy from base.utils.gcloud import get_item, store_item, query_item, get_cendr_bucket, check_blob from base.utils.aws import get_aws_client @@ -509,14 +509,14 @@ def to_json(self): def strain_photo_url(self): # Checks if photo exists and returns URL if it does try: - return check_blob(f"photos/isolation/{self.strain}.jpg").public_url + return check_blob(f"{STRAIN_PHOTO_PATH}{self.strain}.jpg").public_url except AttributeError: return None def strain_thumbnail_url(self): # Checks if thumbnail exists and returns URL if it does try: - return check_blob(f"photos/isolation/{self.strain}.thumb.jpg").public_url + return check_blob(f"{STRAIN_THUMBNAIL_PATH}{self.strain}.jpg").public_url except AttributeError: return None diff --git a/base/templates/_includes/footer.html b/base/templates/_includes/footer.html index d0816ee6..912104ec 100644 --- a/base/templates/_includes/footer.html +++ b/base/templates/_includes/footer.html @@ -37,7 +37,7 @@ })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga'); ga('create', 'UA-24677584-4', 'auto', - { user: "{{ user }}"}); + { user: "{{ user.name }}"}); ga('send', 'pageview'); From b92c96964e51a489dbe5bc08491657695c2daffb Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 29 Mar 2021 14:13:18 -0500 Subject: [PATCH 009/288] rename proj settings --- cloud_buckets/deploy.sh | 2 +- cloud_functions/generate_thumbnails/deploy.sh | 2 +- cloud_functions/generate_thumbnails/main.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cloud_buckets/deploy.sh b/cloud_buckets/deploy.sh index 2a9f9000..295fdffe 100644 --- a/cloud_buckets/deploy.sh +++ b/cloud_buckets/deploy.sh @@ -1,3 +1,3 @@ #!/usr/bin/bash -gsutil cors set cors.json gs://elegansvariation \ No newline at end of file +gsutil cors set cors.json gs://elegansvariation.org \ No newline at end of file diff --git a/cloud_functions/generate_thumbnails/deploy.sh b/cloud_functions/generate_thumbnails/deploy.sh index 37bc3d8f..9a8c2242 100755 --- a/cloud_functions/generate_thumbnails/deploy.sh +++ b/cloud_functions/generate_thumbnails/deploy.sh @@ -1,3 +1,3 @@ #!/usr/bin/bash -gcloud beta functions deploy generate_thumbnails --runtime python37 --trigger-bucket gs://elegansvariation +gcloud beta functions deploy generate_thumbnails --runtime python37 --trigger-bucket gs://elegansvariation.org diff --git a/cloud_functions/generate_thumbnails/main.py b/cloud_functions/generate_thumbnails/main.py index ee318da7..725d0b51 100644 --- a/cloud_functions/generate_thumbnails/main.py +++ b/cloud_functions/generate_thumbnails/main.py @@ -5,8 +5,8 @@ client = storage.Client() def generate_thumbnails(data, context): - thumbnail_regex = "^photos\/isolation\/.*\.thumb\.jpg$" - image_regex = "^photos\/isolation\/.*\.jpg$" + thumbnail_regex = "^photos\/isolation\/thumbnails\/.*\.(jpg|jpeg)$" + image_regex = "^photos\/isolation\/.*\.(jpg|jpeg)$" # Only generate thumbnails for matching paths is_image = re.search(image_regex, data['name']) From 838088a1d6ae013af46c75a20902afbd9ca1388a Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 29 Mar 2021 14:16:42 -0500 Subject: [PATCH 010/288] update release notes --- base/static/content/news/2021-03-29-Version-1.6.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 base/static/content/news/2021-03-29-Version-1.6.md diff --git a/base/static/content/news/2021-03-29-Version-1.6.md b/base/static/content/news/2021-03-29-Version-1.6.md new file mode 100644 index 00000000..a10a7290 --- /dev/null +++ b/base/static/content/news/2021-03-29-Version-1.6.md @@ -0,0 +1,11 @@ +##### v1.6 (2021-03-29) + +* Fix Mapbox errors +* Reorganize content and update link names +* Create Cloud Configuration for dynamic creation of Dataset Releases +* Add support for SAML/Shibboleth User authentication +* Add username/password User authentication +* Migrate user auth from Session to JWT +* Add user roles +* Create Site Admin views for managing site content +* Migrates some existing content from AWS to GCP \ No newline at end of file From 97f212b3f151e881dbb4466d37cf56e844f53b64 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 29 Mar 2021 14:22:11 -0500 Subject: [PATCH 011/288] update travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 50887b65..36dff0d6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: bash install: - openssl aes-256-cbc -K $encrypted_86f5a1ab1ccf_key -iv $encrypted_86f5a1ab1ccf_iv -in env_config.zip.enc -out env_config.zip -d - unzip -qo env_config.zip -- export VERSION_NUM=1-5-57 +- export VERSION_NUM=1-6-0 - export APP_CONFIG=master - export CLOUD_CONFIG=1 - if [ "${TRAVIS_BRANCH}" != "master" ]; then export APP_CONFIG=development; fi; From 3f478bfac68fc7afed9564d84d11a2851ee852ce Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 29 Mar 2021 14:45:00 -0500 Subject: [PATCH 012/288] update script --- cloud_functions/generate_thumbnails/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud_functions/generate_thumbnails/main.py b/cloud_functions/generate_thumbnails/main.py index 725d0b51..abf4d5b7 100644 --- a/cloud_functions/generate_thumbnails/main.py +++ b/cloud_functions/generate_thumbnails/main.py @@ -5,8 +5,8 @@ client = storage.Client() def generate_thumbnails(data, context): - thumbnail_regex = "^photos\/isolation\/thumbnails\/.*\.(jpg|jpeg)$" - image_regex = "^photos\/isolation\/.*\.(jpg|jpeg)$" + thumbnail_regex = "^photos\/Celegans\/thumbnails\/.*\.(jpg|jpeg)$" + image_regex = "^photos\/Celegans\/.*\.(jpg|jpeg)$" # Only generate thumbnails for matching paths is_image = re.search(image_regex, data['name']) From bf02bb480be3b514004b9798c882f566ef73c354 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 29 Mar 2021 14:47:39 -0500 Subject: [PATCH 013/288] update env_config --- env_config.zip.enc | Bin 3984 -> 14128 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/env_config.zip.enc b/env_config.zip.enc index a35b5bda88dc903882487f1884b47f9c4ec7ce23..484875bc69dcc1dc0d000bf5945939a4ea4a168c 100644 GIT binary patch literal 14128 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 literal 3984 zcmV;B4{z|`g^|#hW>5~%v7anhWy?wZE_b%?_LnW{-H?E?Z%s7J$reJYNM=6B0dinppX%(FBDR47Q)asGgR?{ULC9Rm9>GZuL=# z-c!dY`_MT)1|9qcRG3uBO?t97xnQ&pX%3$X-T`Rwpq%~K4Dg)I^BjN1$^Nv(J4~4DFmhJ2REFWHTSP zDAL}daVXlI+9=k&KE$qywJ`vQu07~aHwkyk1>KnRoJ7gi#h^M0YOpB!A0G_`JQ2tQ zA`FN4z(u)`8Oph!wJ^Gem7`GBnGbhC^7SZ({!UVUcj-WP+RH17zFsR%*uLh!&cO9~ z=83#si<1LGfmmSGs^O?6P@}z3fESFqzx0KfZJj0k8V=sjsXK)F}?ZUv5%?`bf z#EpxVtx1#H?EfheTbqV>$l{JmFAzBY29LBtu?wisOTHT7?k4AC4l|)v-iNAY%$9{N zpMEIZ(C0rpJ)fAhaYC$2k9t&C%&9j1o@uwkfTo_CS62)WsFqfPIupmZ#-iZ(*!CC+ z_eTfyNa6>}3=_sZUoJnRC+qoeN*U2v8EEE<`Z>T53V8dTp^8}c%5y!%Z%6kwpMrYk zdn15+IEk-P%*w+wxgDMRdH$hlSzx|zPWEp1+|7(!S&QUl6M9{;w|YFm@Q}XaIy($? z;G@gH5W0D$m}C54SCRX-6)qf;3D1UYmr@0;bo>sbcepeX|;P#O)uG=IYY;>Pvs+ z#gg?Z93%0EEZ)e&r(Y0|Vy@V{8pxyaZB&g$Pa<=KDYn;{zCSQBur?a7JRM2(@S)MaVgI07+7pjXvE2n z@zWbPfaQFlbe6tY__k+}E*EjTqCTGBXkrP%Iy3#JbO{xEda{Ron5%TDCKE`VLE6hU zmoCfbltebVEKQ?T27JT@`6>?=ELU~Oa>l3rSpg+%%3-xSY4Mkh_eZP z_ly`oZ(c$S+1x;@Za*Cfl-ICxZs}!D^_NZ+dP1497;RG*C1eQP%`2##7*Pr_aNtv{ zn)ZO!ezIiqclY;-u{>frI_+Y8PdPK0uhULuVhith*!~M++dM8r3Xk9tM!jVgciAL1{3a2-O5v@FtTya0! zYfQK8_iQ$DB=P_xXhcAWiH|X-?H1gIr22yhZYHUz0&@l4R@e$U_=-oum26XBr|hX> zb~?+;;(bpK^AilNtz_%g*i~bee>8qM!E?c0VR;DmAZ<~P^`uq-bC>$F@`gl`Ab7UU$)zfAepypw}MCf|!f#S0PI z^z~}_Ut)inQ?CunpM{yvwFWv)-_WXvPAm^Dgi2r>kvhr!CV;b}y_D@y<%6W97M~o1 z%w9eUO9sLocv!C0SLupN4SkJ6Jnq*8)17hRnAvYa&F``bHx1>9DtZC3V$nYDfv>eU zm7Fv$s)No&S90y)s8VK>aK7VFAQ=u{rPJ zj?FQ&>sdw=&l(EfD#DW+x8G%E-Vt*}xlyZ7x-K(yIXYV8!S{i@hHndTw+lp$&QUKD%q5LBSwd+7rh;t>1jR*%o9J<>Q zGh<0hCP`Zs+D8=s3e)M7 z@>PyvPrNJLdQ|g2$Bj=!WdtbmclgKTkp-ODuL20ow-iz^PYC-E&`{XDw=uv=*1p~7 zkAu>deHc;w2r>NinPf@)?kPrrj5b_06ye~s zD2E6$2L^pi0Gr5-6_LUeBqG0uN0E}ncgQ&KE%_QdS}aMZ+`qKzr_C(a{q9AyJijB= zs5i*D-#8{dckbw$TVxAMY|IOgFrfE?34ep7#ETo*tM#L%H8I7gHcng)so4D%As>LT z9&T1z4G32(GkFo0QDAi-3$1w=3&wC|@VRUwH%**rcThIEXA9m^#Tl!{?4B6{Q9q~$ z>00`N{W8!JOl4O#tJN#RoC#=G<4&Yrb+U_~N);DYhjyb0XX)M(Wh*5AExO(Y;ncU+ zMt~idYkaiz2&Gb;-r2OWEeb)I)@Pi6gaICJcBpTPCre>jRbAX z!wM7dy%RCAUXFdMwASD+tWZ@%xnDfp#x#5W@-W>ps$CSIu&RqjPY(cSD2_aE?A;MG zC;5)**0j)?$l;P{6V{*o$;D|fsZD68g*|>xaD|P1eAE(B{m0R@uHS*b9vdMZM$>BO z)5|BAi$Putxhf0rxQWi^jEGn--2OMUaab?oQ_^KtlF2QQAHP+4wi?b#a6{`Q<`!S;1l=v|U=IU&ES^IK+5L>(~0JLHJ=}H=7B+$78d;Nw zHgX&d4ijPY2#UEgH;OAr-DttN{7M`&m%;29qJek~ENs4MIaP3&M=xRN#2yxzk_$cu z>*f)b8g}vbt*)_iLJ0&o_O|JNNB>zgl}nh2Z;}FG^U^E zgXkS`O=txtp{Es_!JZ9Ds`D-870u8Y*zB9hMyNU6_@e(eE7ZLhZao`gK( zM0o1qOs8f7?iu{rk9P7$pvHF>+s{(>57&yy8X>5qBou1ceFqM})#-AZ6B-EhINc{q z<*<9&W$4*2o>0C_V)}u&sv|9~)rYIZT2CrVO5;ER_f*7(T*NcuPEl4fcUeFU^pJKj+kG$?zRpjW_Ko*97+ zF1BDj?iFBJ(Y;-V6ezj}2q-%mztuk=P?ZxX>Fo!hZF#ErwE=&gwMMNkB&~UK8Y9p` z;_cAml_1MZk}I~tFSseU_x_tNCvuZlew!=qN9q3y$)ttpvKB&QUm#i)DYs=hF+XD` z*EB(iee-piYQVAjxtfk(gjRlcaMQKcLFw+I@` qz~&Esnn+0M1zouA-yV3{toKNbd~&Cz(A2D_tN6hE69>Yg>2zg^TeCd? From db4ff587f3939f59396afe8b7afe397ea3d16468 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 29 Mar 2021 15:06:52 -0500 Subject: [PATCH 014/288] restore thumbnail path --- base/constants.py | 1 - base/models.py | 4 ++-- cloud_functions/generate_thumbnails/main.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/base/constants.py b/base/constants.py index a9f32cda..ec717b89 100644 --- a/base/constants.py +++ b/base/constants.py @@ -10,7 +10,6 @@ WORMBASE_VERSION = 'WS276' STRAIN_PHOTO_PATH = 'photos/Celegans/' -STRAIN_THUMBNAIL_PATH = 'photos/Celegans/thumbnails/' USER_ROLES = [('user', 'User'), ('admin', 'Admin')] diff --git a/base/models.py b/base/models.py index a6479f16..f70365b5 100644 --- a/base/models.py +++ b/base/models.py @@ -12,7 +12,7 @@ from sqlalchemy import or_, func from werkzeug.security import safe_str_cmp -from base.constants import GOOGLE_CLOUD_BUCKET, STRAIN_THUMBNAIL_PATH, STRAIN_PHOTO_PATH +from base.constants import GOOGLE_CLOUD_BUCKET, STRAIN_PHOTO_PATH from base.extensions import sqlalchemy from base.utils.gcloud import get_item, store_item, query_item, get_cendr_bucket, check_blob from base.utils.aws import get_aws_client @@ -516,7 +516,7 @@ def strain_photo_url(self): def strain_thumbnail_url(self): # Checks if thumbnail exists and returns URL if it does try: - return check_blob(f"{STRAIN_THUMBNAIL_PATH}{self.strain}.jpg").public_url + return check_blob(f"{STRAIN_PHOTO_PATH}{self.strain}.thumb.jpg").public_url except AttributeError: return None diff --git a/cloud_functions/generate_thumbnails/main.py b/cloud_functions/generate_thumbnails/main.py index abf4d5b7..c8634d05 100644 --- a/cloud_functions/generate_thumbnails/main.py +++ b/cloud_functions/generate_thumbnails/main.py @@ -5,7 +5,7 @@ client = storage.Client() def generate_thumbnails(data, context): - thumbnail_regex = "^photos\/Celegans\/thumbnails\/.*\.(jpg|jpeg)$" + thumbnail_regex = "^photos\/Celegans\/.*\.thumb.(jpg|jpeg)$" image_regex = "^photos\/Celegans\/.*\.(jpg|jpeg)$" # Only generate thumbnails for matching paths From 3901837278552eb33b3c384a2fa8fda5eb118694 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 29 Mar 2021 15:11:35 -0500 Subject: [PATCH 015/288] fix footer issue --- base/templates/_includes/footer.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/templates/_includes/footer.html b/base/templates/_includes/footer.html index 912104ec..813406ca 100644 --- a/base/templates/_includes/footer.html +++ b/base/templates/_includes/footer.html @@ -37,7 +37,7 @@ })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga'); ga('create', 'UA-24677584-4', 'auto', - { user: "{{ user.name }}"}); + { user: "{{ user['name'] }}"}); ga('send', 'pageview'); From 09f31bf40fb9fc38c63c93a5984a8814d2b9e59a Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 29 Mar 2021 15:12:43 -0500 Subject: [PATCH 016/288] fix footer issue --- base/templates/_includes/footer.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/templates/_includes/footer.html b/base/templates/_includes/footer.html index 912104ec..624cae33 100644 --- a/base/templates/_includes/footer.html +++ b/base/templates/_includes/footer.html @@ -37,7 +37,7 @@ })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga'); ga('create', 'UA-24677584-4', 'auto', - { user: "{{ user.name }}"}); + { user: "{{ user['name'] if user else None }}"}); ga('send', 'pageview'); From ae6da5ea015a952db95353a29c7ef67120e44811 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 29 Mar 2021 15:54:29 -0500 Subject: [PATCH 017/288] update travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 36dff0d6..ac0e4e36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ install: deploy: provider: gae version: "${GAE_VERSION}" - project: andersen-lab-302418 + project: andersen-lab keyfile: env_config/client-secret.json on: all_branches: true From ef9cc71dca623b8c042092e70f804c9f4f9adfe1 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Tue, 30 Mar 2021 12:18:12 -0500 Subject: [PATCH 018/288] update travis --- .travis.yml | 2 +- env_config.zip.enc | Bin 14128 -> 14128 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ac0e4e36..ec1104a2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: bash install: -- openssl aes-256-cbc -K $encrypted_86f5a1ab1ccf_key -iv $encrypted_86f5a1ab1ccf_iv -in env_config.zip.enc -out env_config.zip -d +- openssl aes-256-cbc -K $encrypted_0cdf204cee1e_key -iv $encrypted_0cdf204cee1e_iv -in env_config.zip.enc -out ./env_config.zip -d - unzip -qo env_config.zip - export VERSION_NUM=1-6-0 - export APP_CONFIG=master diff --git a/env_config.zip.enc b/env_config.zip.enc index 484875bc69dcc1dc0d000bf5945939a4ea4a168c..7b44c2a0e84bcf955a2457bd7cd6f36f80c8c959 100644 GIT binary patch literal 14128 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 z z2?UPFKcDmZC02W-qaXH43p>%ol<83Ku{AMTkdnmf7&}?8%?B9`Qy9b0R8~~m#_`opg6z48 zd7o0+O(%Smmfo^CJPMD&tKN?eM&W4a$Y+{5(aoi?q9J%V&rQafa>1?JK%B1M{Ykdj zP0b^Xv>aj9WynU?J!XTP{*bx%gN8x7vv;-5NT?`|?v#akZfDXD>O)n}Sw%vsYBk^* z!Y@fs`$wK<;U2)8dGsSXbsZ*GI3Jft!KaU(LKx+AT~`u7Gt>7&AIA=MkwDRzaq;iE?(aR3ELn+TdyvZiNp&yilDbL$JnW1|ASOG0#|aQ%ZVKiU_m+ zAS3ZVE_1y-WD0LI<|sL|HrYW90&WBrdy0T!WCcDM?(kKXCr(L{*fS77X0qKc0-O01 z8a8Ojd=BZDIvn1payz6;9@Eb~w2zOYEyg&2v5J3(;hkt&Fjf@Rq7IaVR;|^N0)^6e zZZWMo;Z50`41?OqzkEM4MLiO}TUeN)6jPX#s-e;8HR}7fjj^_JaEOq~aXZ160JxG< z9;0&F)E1t4tJnKC5Y*Nb{N=jYN{C|-UufkE*FGG68w?r|dh#8#L+kBSfr$n!F^DR+ zTw3)UW3vmQe~SlU4d)hZ8*QOeq`QGI$3X5u-%CDeY9y|AI%+bZmsj2b=pj!>Q`T0!88a zfe~{N8TT^rJnl{GXc^%zSK)Mx5~{oWfB`**B@U%huZQmNd2gR2H#>Irm$DnAg(oKz)1e%o6!crj3>n8wF< zA*|v^wJ2|a1PN9h?igZ@6jrRd+ucnS>$n|9I%%B{nmK{;qcEIJJdW%6`ggnDHWoqn zRZOYg=e$U!r6JIR8eHHjN|e+;ytnBa(p}%ZiB(`nmCLa~gH_Kqi8U?D`ir(@pe~Gf zrq^?&`RJo)jQd~ckwxspZ1oF-hy*!K*MT=6ic@F^pb6{l4#xaG3Atem@OtG|oD!^Q z?qy3{QeHIxNm%TXARLp_O!ac70Oy<_{~)dMrt0qW0a&0?Ml*ZkP)z(9mt2I!=pgL> zpkbP5S%!P?%_K_zE0)kJA*?lHhcxT`c+yR*LtZ4uDVQ#`5A@R8A}K7l*TtimqvzEM z!!91yq?91d8_msWRui}`o4PI~q9Wjg^YyK|7F!$X-j+Auh-#iTvK3DpIprAMG@n&= zk7_CGb8%if&^pWf6FOY^hlH+SzrK7J3tkSi!)nlADC=S0w0TL?ILZak?j};q<<4;Y zW>*DC8-M%ff;c9ek_sCtp3kt-@f%BN0_Gz=CxY4z4zv#vF-wsJ;y)snlrM5NkJKjq zoAZ`R7Y`qILzZ6m-(~HHWg>joSv^XZeJQrdfym?lnEZoIWk+3Q!JOhN)F2m?u;xV3f-`#wK<%$HT|FS2G*4hfS_~!+ zb*4>pF^T%IMvU4()ge-YUu0~;fHv;i`?#DZg7jCpua}vHK~a!gYn&~$vy48ttFOIJ zMFI?W_PTj{T#KqH{LXEf!G}8X{E{6axs%dCG>3diavr_8uf7wQGZ%|{N-P@T!Of9k zgBf=$diqiG%}i^8=x;3iqI>yIw%(1TSUYSMxb!PcwVX)r!ZYWiL%-k4J~HLklcPG} zvHH(tXlN=+-xoNRHS1}7H>${=1F9KDb7Q!}&Mw2Sju4Zn;z(;FW8^TgD+WwOXlg*4 zRw{Do%m4gOX~h0CMMKh;J;EU+V+ft_T^Vx^XXi)0R^Ql#>^;>aHYiLbthl%sTV-t* zHM$p7rme7lp9VD}40?NRI;^zIu}Rwyl)Kl5c+oC`Fr0)x_AocE(-_WZG35vaB#bSQ z-K~2he&Uom0OT9p2=!cz;}G&2$NxTj2MG23H~OGe&)!pd-K*VhrVg%x9~98L+6y{s zRBjU3S~@ymIU86<)*ladbKP{1saB#p?llBmBMcl*sQ*GHDPi1bU1HyC?T*bX9`Lv1#W;y#4vcKywG< zKsNVjrQ=h4rp>jJY?J5aXxqk(EqK!YQZooWOG3Gf*=cK}nIcyv1rT4jQS{QAVwe?Z-Wdq!5-iePcrDEK1>+&Vs5^$^q~Zi2%4|`ZP7|I z((%erxln2}a`3vu{7!5`)H~Dvuym6sSDud?R|BmtFD-;}~u} zF@td7)&~jkmOXL`k1#j%_eW3&1?%K=~*g$OU(w zCAC3xG$UW>idCR=MG+y(9Jo#t@i57s^te70oISC72W_I3k7S$D05RlU;M;*(|0Q0` zDAC$~N>+X;q=R@wOiEOBIj z3Z#dLurU@(-*z6|OR>wr+{6RIhA>uesuuFO7dW7~p9xhs*K><%nL#%dk$)pJHZw(r z!Z`xph3CL49Y}35f9Vw3JPOl-Z_xmq^;Rkl@k=P;@vheX0%v3IfNg`KH?jzCX<87h zg6>5Abk>tzAbV1+a&TZ?uBbTgS%>@@fj1?zm0e0?Wzznb28&FYuMJx?gyjf{it}mp zhr&Ts{k}2m7?4FzgIAI0zJy{nnLS*|HymZ>L2Lu$Eiu# zDAfDn1pSgY1m!}QBSno&y~CdLGh`!%bPRkTpDkHFP)p!2?PCy8ESifGzg$4P7)4OE zxGv0l{Zwsu8w<+V*-oB7`Bf6522Vvu%@7}t9iv-V_q>?mn^WMcY&6JN9K_QA#l4q2e1sJ+6PeNO~JmH!obd*VWr zW;o7>@qHC>q7e@>zIsySu1*3@VQ*H0BLz{#xSe0mSJ*NQcIg=)JA6nvxDp+QDw!8u zUs;LIF78XG#GnKTD=$`sw;ZX{fc8xc#65((+(?JBZae$d0RfO-`fJ~a9_Bq@3s zVnata`GErdjP@9^|I-$*Ckr=ef%j zwX_(kEmdK`;z6I`0vtuShvz5#3nc@3%`xG_3NgXDq3u*Yga`N!7EcXx^y>dWx7HO^ zXg7yw>&)|gZE?oa5m5Yo>1?YvG z9G$`VZ4)0m$A|uVqgO8~Op-cbFF84%A7AsZZ`*ti z(ved2uX4=piOorR)4h15(rgO~IR8lE<7wnJM02i( zg3t%pTZh@gVIKc9rlHI4#@Hp2M&QS1cYUe0i|^~^P4NCzd<3>YmJ-nu-(W`JFOBrU`hv;NX0*=Of&4)ypXdiv9%gt$Y(-ZE1j?;?#0`GoyKi^Ym?6Zn@H*ftJffs zGO(_j22TqmX~h5Q)51!~B?n*rS4e|HcYx}=8l8Wm;N*YC65he%s4n>XW;tTwQ@`4u zY1pi%!V-#G(9H80=*4lim;3V)Sqy%21UoVtEM{lDI9WY>^jW!hcNP<6q#Kcob3(1} zk57xqcZqLgO7EyureDaUU)fRS)4fWCT(#(P9tmcV^d)zH!a<5F^mXuaXPe@S1DYV^ zaY!87GB-scZ_o-YJVM}#`Q`^)w1!XSrns0L$@UdPy;h`*2|~K{V*$99)(>!un)Yay z8)g4Srq9*N>LMMNZFnisCZ&9{pKcVY1?8MzGn*rcn}{5YhNujbFCvw@S|t*G1P*o#ocWYNqcq_y!i%KEOS{|zVM!i9^*V}P}ZINJ|O zH9DpDzm6zoNj&xo2Y`zp_xuy=r8AWzF^V)HT*fR}q`;hKRkiiF48~P*JdgOqF9tW> zT<^~Ez;G?-kK+A;ZOJM4-s~t^7a~O)kOCg_Wupr6*)mm~f;aT)yQo5xSbxSDwbEp}@Woyod7 z0vzDJ=C?b)lQKSzj84f^C6981OuV5+pgyd#Y~MiUmRGOc{lQJ{qnm7;G~DXdM2 z!!y`!)8?WCx{-dw;RZ!|jA>_|pK*G6I+3p>J4sX*C7_&(IKZ34_y7c*#C5fa{mnTO zkHNg@-{qRYp`wm~_lkVufA1|ur9YGohijTe`gG*T&Tat4U!&QJE0`Kbq&Av{{GtGo zabLmPOkv}yZp#S^VtJclrY23YAO$|n^aLdd!T?x8dXQCqQtEf-8e*STh+cr+479>J zEPHdpYOfTuh2FbN4iRzF$BQ_4YWX5smvs}REb^aaL=ve61E^QKbG7|^Pp6GIy}@B% z1-LuWMuF~%Lb+|~WGx%;~#10tD)>4`pU-8_N}1d z+RA?XZgldqyJGASW-~!}f?_SHi#~_V=`{*YiG79wSkIFiUbeJ&^&^oPiBZYDL6q6B z-81tXGv7<`#Ga24_BGlMLDCf;hXl?~+EF+_)tnyP8}WcWltP_^f(-wx_Lw0Sa;)|{ z;A*Y*3*`3IecRiEx>=>qr8lwQAg{apzL{aVqF4XxoI$phoUV!4E7$qNX{Sd|)q;fF#4*xD1wrzd( zoi6t{DseUjRxCA|XsnP|=hL&n8d@vGa{12ofVH+V!&ZZ*#-G2yzD)J!22`o)6(!_0 znNo5zdH5?0w*RDdocGbl10KtU0*hc{fhU5+*j@H6xp*x!#WJQ4`$9mGU9G4uOVdA^1Di8Wvol&h}n1E9rCG6IAAqf7eMrcp3;GN^rIF1zP`~yb&-`! zOPDld3{QPpW`smr!TQzbK#ygdCKf%t0ln4RV8|qbH~ozVin2)_5HODsQxUM|^0ixj z@zj{o+^w`3SZM*zzxGf#yfUz$7C^u@Nu%p#)*~m-@x6NE*8>ZWXD(ei7a|dCr4mM&j zgOyM+&iZa}gc9d0PFn{3!ofEwcgVv-4(I!JN?OVNKHoKt2(Dc2-hsj%*-T-_P~w! z*=POw%<17IS)-TDwP9QFD)Ku5SBE}x;K*4~4y|;S%{N|4)T6SBS)@C1D&t8!2IJo2 z)BrLxhEc-iPxJVOd#ercxjI;ngp_PyV@FFTu|^@y$VVqsSYq zc5pu`UKiZiHDTmq{catY9E1|1LsFnGxPGkg;|_t&`=xnH1K1Ibj)qWDW>Wb@neS-n zDxY00X7Tv;!R{nw>Mb$w*wk`L1j7u~LO_okIsM?qr2i16|J8sCL!vBRC6v|bCYqF< z3l;mv+1ePbHYn0ECgMb580v;D*ep^qk^SFQOpALD7uByP0G&GnS%5Zqwep!GjxAhv zk$1WLwtpyieC_LE{+O&uzq_Bu7-M=SOS8<~KfYQ7FT7a9$lXN#x?}&wk~Yyt+Gqd{ zd`7~x9;7R6H!PP2SD2(@D3&4&JkX*8HWC@Dq`pV{Mb1@O}3Wmazlj_%8#RAl-;orihi zWtvew50et*??9W|chH6pjdg%Px`H9>Jibps+YR8gg7O8+h-#!0!@ALd_)4xQP7<#> z{d%ej&!Osu;3ox{Hl15>(Mox+!ZNuAqA={3!U#k%H|Ob38B?pk0O9(>Lq9`&p=8ei zMmt6XkxwxjrOldzI*zq@2`>c#*flk0TN%))LaMepHdm%>x3seo>wLt`IsRx3(gLAW zBgMxs_%Q=nVp5ZMpk@S9HqTG#FPwH&-rPd0MX=t;U~i-k2@eCizq{ga*l*_$11#O_ z@vJUVJ^aq`cG2^`xkh7KQT*2N)2S86J9jbsX0L;0;`JOWP$)&;NOLt;R6Ylt-YA6S z9PPCw36IQUzeDFQH(a*r;kSVYye4#v1(4eB3PA-Lyr`2B1K+3={CSV~=0;Dl7hSUA z-XH2ThMbvPSN|6pO2bxPf&udqJ2BLsCd5B!rYR$I0>S^PZ~`wbBv;b#<_YDRpyJlw zYwe(zUq-!to^T@yQ-l2S$nfO!1jGz4;(>&_N**FCU>X0nQPX=bj4=U;G|vjjev@#F zafLG#50NR!cgX%XtB^RbRav1X>d%vS@o`8MM*-J7dF;oAQgDP`aW0aA6=bopc*lH|*&rhEGf7AcP848xKK zJkinrmwtJ&Qzun^SB|Va)x@kBNSmjeJrDRhnyNyEKXi}jyB$JHSQL=oh;|_AtH?D; z8Ei_*aYwg`zqH&Qjd1=KYBvAwYzRQ(Fo6U!E)%G;w+V`$ad?Fu zx#nH_WDCs}2SwGKh^EraCfX{!n9=Fsq$?#V<$W+>BCS;8F~fClEpff?8kyHt{kpIZ z-Ii^5rkK;viH(k;kv&9Cg7V=svsl>jXOD$wePT)gS0vXUIE?fK@$ye-08Z@t70^?JvICe50zGe)h=Vyb}}#Yi}&Z@;p1G^ zl^0U#RGiJ)dcXg1eAat66+{v*fIUk$lTT|A0R^cxiuj1v2I8N!{-Xc}nr+2Doo#As z%x6(p(pj66oKLT@L1P&3hX7qi6cUSZSI41GNt!hvZt7+I%MU$ zL$U%zmCo||o$GFjf@WfwRca{+WEA25f!}BSq&mNcRE|c!PC+%6*GEN>75+gC zc6z>gPmRb~0G@}7&&wyVC{8CuTIxFl$_9I5$Bg#osz@KqFxu&!clBH2^ah=5UQix?C7WqE7f5VoFWQ078-DD<_Cx!FbkE&ZOFMOdi<5z~ra zgZzl;IYz8$%FO?JPOS^_16xr;==7f~#gou!%)KpPA+1@CPEj5uC|k0+mRa0xCynSm z8bBmkwD=*2lA;n!goN*WnePx}b_v5GLBoiYQ@{p1SHnw2Wb1#IpvhHB%t|g$X^17U zb$GV|E<2EE%yY0ijuDuU?gOVJ~^-{Zy!aQIq*WQTb1wi z1(Gq=G19y|8OQ-e@&C=OlKC12kl2I%@a*bc6eH93eZqQ;v*)YC)(j5v;6)0LGtYbP zbbij0v(1E2;nb2}`*-kufyO)uuf9dXjlZ(4j%6but|W6nRR`qQ$(bUUy082AXSLt$ zO%eWZ<>?^~Kz`qrt#$ z+(L*}jK_VL0OGc8~g39!>3~r@z>YRlt>>xk-##xI5)Aj!Vc9Z}W5g1Of9ojjM zc#v?D2nW&1^1*0YZ-XZ)Wp!F@(I7Zgb*C{4UIJ!iHC8aXei#8X8{Edoj1oxS>%NvS zp{$k6-#0y0VT$FxP!H0*D({c5J5|e=9Ioeq>s2SMPiNzvksreTUo8yD=Fm(WcIkT3?h0%A*^~P3-EX(@ncbF3t zF_W(Kf7^a7vxEbQ=@hRck$`rCMU9sYGcqDsS6-ExQh}t|_`oL_?ae~Wt}t51+eks|<;EQysLu~L8yQ?3>(CQrv^|0W;p>d^f&y~ksfMcROVRb~ zfY?d_V^@}%qGffNj*$2n?pLgA0If!F=$U$Lc~5j_)hf^AJOcIB_4BRQ;@`^f@eTx& zHE=1SdB-Pbq~i%Vg~r3}dJpEk~FMd|6>FQiStB! zf9`gl{G4vyz665o{Fs{`94OL*a35hB@xPOgGqfcK(&;Dwi#fB?0XkI39M}C-`9L1C z+fD;$z>e4TzIarPP`*zyb=S>|pR=$o)tLz{2(d~>-!+qruNzo-O0nm4mAIFG%0dKx z*5KO5^(~Tya|ACP{KGyXZg^^qui!FK;!VO823Mt#e}H;@a`!6sI@~5W_io0PTz$Gm zVC62Lfik(3)oE%%{?OU>+;6CnL^M~+WqSx*b|T41G99ymx^?ugOZ8NF_d}8h^zxee z2e11Bo$qA}Ube3ZovkcIG3T06Sc<? zZLL0Q<;t@GI&*Jw8EO{W+KpO^;-x~>-;EX2gS#Fjs`RR7F@BIeymBqHJ7#C^wW$z{ zsm_T@H5()W7ThNfbp=K==Q+&&xBsWS?qg3 zg7E4s!@xCd5TdQP0Un1%MrFl3f$1GlcNAdDRE*xvj&hB7bkslhoL`OQ)ke-CKfaz$ zgxU2*L_%kjR{#IQfi@6!P|-jVX)-RcgToh6sv~r~gv5+Y(AnV}0ue+Q$6=}#Zxr0W z!Of$mUk6*)cW=8?6O;c2z+N;-wk@NN1`hGJ3Y~R}RE^xfqt9XU(r7vyqIS>ZrSsJl zua5`pR}Cu@-nTmB@9&N^-ZF!KLjQxB)@q14%RUCG57)8Z%j5t*gdGuPv#!fcw|z=j zO2asMwS0>vl=(o)i}}iPBPrX5NHE6i1us&W!>Zb^FvYTK+8U=46Zb)+RSxrmIhif~ z82U%NTFbM>6HM9PRRt$T!{dweM)1?xy3=cF*jzr5>I6MA1a2>Nk0(oq`ow`Jr z1dx)<4%}&PItGm7pryQ;Y8TreH%P4?Dy$h%AG1<|{SrEk6*kL}A@{7(Mo^1$9o8A7 z4MdJJ)xlf#+(vv7eD=9L(qdRQssFp?`h<8@S;}EAQBt7L3b?7J91cnFOfHB5loymG zjBbCk8}QEV`)r=`6$R@|hbTY1naXrZ84;DKfZlH(kfvu1jq5s|-^CTYf9Dsg(DO16 zKCsuj8x0C@;F{6<1?q9-e7=%K&x##Bman{g2yY9LvJVx+I!8%(JhS#A!J>qV z>eOjtPTH4u99%^>KvXbO_Uux3@D=m+hy|AKIpDQow`ByD04X@Spt46x((Pmm zkKg!75>swq&*vU=MUQLg4u8$wc%#Rs&n@bT688~FOW`G85V>G)Sy?0;f0Wp=|E%s1 z^n*FKAC^m)^lLjc70)C>hpC`e-?&MXxcL}K5>JMgImhAan;~9;cXOz6XDw+I816Yi zKMJoXr%!2icyw%ftwj$7G*nD_CLOdV=y#y`aq}TuvWwMR**u+VkGch@!F+>sv?(zZ z`_locMD#sDOzMXz4FE&%i&uiQtJo%5Ez!<1qP>DBd!s&ju%JGqna#9ZlV2AA&A@Nl zayd}`C)&T7IX2zB@wEHi&d@;JM7*!XFPy+e@Bi;FzpH_# zlFU_T9Or^vpTKpMC$qow8G?OKC2(l44r^Ee?(c-GL6Za^^Yh+yc@p0+V`Fafydh3? zID~n~;Qvft9Wx!G%ZLuS?Me#C%YHy}WEjD2xMF_KT$Fc))g!MlsYWpleHd>@J~&=> z1E3lby%~abv3*65JhzD>$3|lhNoItlQQ#}#HF~QbH97ijhYSRdMZ}Gp`>X~y-r&{P zczVPsjR{>_>LcDjuu#h(q4j0b$c=w36rY7Ds3s;>il<3;eMul*4+M$zyWKJ&*$kDn z0*2^al(O)ikx-IeLJ10XVCC>B(G!B8;G+JHV`-O32DQ0#<&SsGyAzp~JUezIgw&C* ztidoHs(>ik+5xd(m)K7%K&e2b5vOHMM&dUfv1qeqO(fZ0vN69KusKpG#6=r^0P~iw zBb1^<3d4OJCc$3b7g9j8D#$S^gLBW^eZ;!CdhJUQuVsxQc8k}t2`HOO?AHYAv_(=- zQfH|>$q5xemRkAwY7Bg`|6Y8-cuz|&;xD9*6?eQczylS;#fLp!b1O{Mc8zx)Cg6vE z{3*ZRLPPqh$=qf`eBduZ{ zdtPLQoj#*piTd(Or%5@Y{LP^}_1=xuFki*d*?(q$T>sh{)$z|^OLNk3SNE~H*}xu? zRw~1I5~n!yt1w2oR4w2Vj-NSYMcy?*4DY$qj3X%n+NS_c2xgW%N6Ttz#(;kTbypP9 zVat!YdnRaK`2>$fyDS=A@essRZgXF8QX-j9&qs*nNiyF(=_%~!}pje|$Rx4V4U1W$5;X$bddQaF9F1|u4xCixxLH(CPi}b_ zGZ-psS4oc$)j0~Z%ZI8cWS|}eH9V`2vOxzoNUL|x5{(DEkJ=D@d?7aF<_zBM#cyN& zGV~zPYc_hjk=A({g@lP2U!A^n{E|7gPS14X#y9+_zJ`sV)2Sys0~KzaZm+3KsvD_x zUa{a;as*N-V$vr#8+DlE*IT%pq-x4{1mSgSB5UphrFA?6;N4Hb8}jt~nw(U=p;?EEd#DGH^nbg1MA``DolFr3ZN+&zrIkhnY2A7hCZ1 z#3gu+-O`dTryELKliYOO@5t;xMM`^@Lv%IcTk5!!QJX~w5joy?D<z3Ha z)8nnzkk#EVAo_CeH;)^vBNlbA0zqrMj9U>#dY{KdV&wBQ9F(9$9jX zKL3Y&wbRGK>pyMI-nL2_A9+!w&k=nQJZyl6J{ASe%-J5r%WK<&tNAK&wZ!JhRGewE zu+F8CKnhpx5O}ROA*UqW2xf>AoS0e3y7J4-;4cKRg*^S>0Ve`E4+N-7!jB@5+VrXA z?nJ+0sWRa(Brt6nM_DnpomE`wyBm0|;aQptmaJHr+c)@t0jB#5S9AW$QMSZ33IU3U<*4+hFN`X zdZm{yQ>bxEV2?_~gF#$Kh&;d^o9Nxz04@qYfn|K`T4xP=gP2-~qV zmX^3Vzq{HaPvmGp6>J9v3e2fRpX;DGQ+N5OUKNS!r9uW9m2OB+4n%7kHt%`GQzk%v zK-oThzh^>oP$-|-Qo-}RlDD0O5wy8L0!WV9`Kq01kFb09g*z4ZM6tWq#pjow?(t)Wuc>d>yaZ!;I7{87Ad5p{f8B$rG^iyv-KnYC^Qj88bnRL>fZ zko~hm7?AcXLllO_X-q{Ja(EDLiq`IXWNw>7xlvFy0P#v&=M&e$TNM=Y>gGr-1(438NQo}ZmeTPk^mYLrT14+fFYWXenM%~sgLv(a7#0NfzGc<+ zX9kZS)!a?t5i~7q$t|%CscC682$4ucj5C<>EzjNt2wxxa5&aOw3zL&rOZ6NI0v;e<2b+p5)g9fI zy@yw4(J75E)Bs`yloETC6rEy)NJ9HgKL8H~^5A`Ca6exJ_)Fa94i)c3FyKuL*=xe-a@O<&Y-_HuH~cm_Vcfp^Xpx@^wYp) zkHc#Zfb-w2Z1<-gePw(+02T|r-$1~5c)18vcG2jyAX5oT(PqL`^1HxU*^|fxNnuZi)t(AJPB!V&qwT?VvTx^F0-{Zf8J6 zZ}vCroJJ`&K&bqjrjV&;(iy!u^(Ikf9u?fWxGp-YcZI4_U%6EbXIJ33S!>#9X!}LO zPdB#3G##K8d=@0v7R_CSy6G24ziS-OJ{>csM89rKdqLfT_DmG8tm+(Kn+!GUy$*l*DG(SJ0GA24)3dfy^)<6TxTszE9w&ju35l6kW zD6@HT1R^=BueVd`BJ~TTcfdE6j(!KedGKqo5%FN=d-Tn`xq#*irXebiK6sd5Dk`6t zYCA>0<0MSvNv>~0I5rCjBn!&WJ1*}3qyqsJu7$1K;$SG=HvmSGBg4nL~5RNu_&fIv`15=1he;x(i(v>#2QuGE~f!{ra$;->1RXb{TqD0 zpdJj5PvHE0W-{d=UlrzIWL1iJG03eTdu7OiuK8WBKPC#9eNNCKFA?Z-V8{lR3c%^g09hh)mE%) z=ebtS;{nM0kaxH}Gof|dSGbXlDkIab7Ae@;+YM>Q3CaR`@g?N5)s2(Y%=19ixB4U} z|JJ*nmP((}v$&fG%;(Aifg+1!wqAX3dQro}xbuwyCqfLVQDyXv3acT9^fjRyk$Fh7 zW3efdF#S70y_#E`q`?i#xpuMc!5G^jgk0XE=Scu?ZzWHKc94WHnX+Da6K0AWM#*gb z(>-c*6=h-gh_%^_+0!nZ;%P+S$$7%SG8s0~umu%ZpdOyFKabv5ve|cb@8v}ZjpPe$ z1*(_R?0-uIKf1fP9@>wZxG#jJ7*#s|5`HniYi?+iDk8R0CP}btc=vKw-M%N@8J-fr z_1SoVF-j3}->F^NfGda&Tjj28oZ=9IZ!vm&Se-}?NB><&EzH_I!;GRa_Eus5BXsSY z_TbvO>xP6XuEpHl+Rh18g>G97NVQuOun+~O&X|xpal4p9N{wq!^j5f0$ap*VdN=y@ z#plErsPUeWN1iI)MS-Fr%7aiC+_eL4PgjQWhw*V0EGM!X#D`8pWHKedXX_nrVTSIG zdgsbFrTq+{a$9k-E$z}os;HfRG^qJbyXMIEDfl8Mve-v`T`R?&ha=bd8V)n78gPMs zk;1N9!S_`NY$b}lEVl&@n?EP@3uqf&iBDOUDK=Ev+6sSft9YHhG>)E?;>7A^9O%Zr zS$>lpBra`~Gk^3-L0%(!LmSc^glm!2|B<2~kf3P!L}WYwx1_|KC4}3i z5?)8FI?DD(J7KhPXY^-{tSf>fqnO_kDf9j_YO3}#&r^Lok0ueQkxKC@j*E=W0nO79 zMYO2&mM(NR3${FfuVr(1>W$SEdRzyQ925JU_oHK4lZNSD=Xc7zAr`%QFLV0UKWj=- z(=l>9kTGFSvyHORB5aVa1;RweSYTI3krU{ zSpYZLE>60XD$k4G^EmM*Mx+lCwF5cC6@?|dY@X6_9*VqE&ETP^@Humg_Jz8<(O>*O z5xdr*w5=eb0&JRl>8q*NBGMLCS*4KX>>WMN!k3${-vXJ3`WZA&-{l04m`d3e82ffU zIEMlM&UlIIO{clNp11x1+^M|DVx(jxQ<8c;+C3P>=Z81|8XPPW`4YAVHu~WCA>_{Mud9 z%XasdSJAY>*Lp!fQPsBlMr$8^Ei-rC9xR%u{0Td~?mx%e@_dU`A|ji95}>cCFE`48hJ3f%~w27RG+&!ve-cm zsVj7G&?LnUK&LZaj#TXkytlb?B&sh)Y!hC@5z|n^rc==^u-s-0elBT>*M3Ep>$@{ox!N{EV^;Kte(gtDg9S+Bf|U zeul70Q~eCk6ydOg3${iGAo_=SL1*46b{W4C3 z44k>Q$`S_7)E~BSnJD#ps{*nF|NKGE(M$VbN}9M{a>GI=)d%$M^GwCyz?r5_f);J6 z+Tw`L88mUfhbOg`gpbTH{4p88e8_|O*MZgu@nsA7KoVM|uo91}`1rQR#WOlLPhsr)0F#r^uEg)QQ~06JQFfkhczlGi`E< z^lVvLg|lQ2h4mNpSJ~>}79EHnSV(k1%kKm^K3T(pS}&3gk`y%fw2vQvbLh>v%sN>@ z-(6=J`;B~j0av~Kd@@3V0s;UrX7oI zeKD}YxDcPPS%^$9y%J+45K}JywkdfT60H5UXJgIDKlDozwwRC^! za5O{o@X#YY|GT37AH_gd2BkL$u?qA1pGy$}z3sLoqsj-2`RfC0Yn?$$O?%=2ejBIP*{emnDX~ocRq_*)rXF$1e(JhYP#3HPD^zZ z5R~z|z0>!4Ubu|D!epuZft%2#J~ayZUuM79HM{zj`t6{61kHe#7U%*-g;?^w59FIc zY}Dp}VKe7=B93z>1?4cm z3sx{r+pnZylV^84ID8npPgX9ivCB-b+Y3!_C`Pt5ik9a!vh0{Z16ywvY4OD+ut9Ns zJW18VsZuU7=G9_(Dv(ZI$T+DUnb~R6$zx0^?WghnSZ30OT;XU#PnB$@g-k_U6sHM6 zLQ5E-Me%-VXiWJDH`#mG=#JMSVs_B6Pds8(d({``tw@E@pmw<52`$0S1_J~G1iv78 zMSvF(k&Yc~CKj_~4?)c3Tef&cwB?xBm|^Z~&InEhP0$-PR{koIS4g&{eMqn;8A_9d z1NV`y!uTbRzhf$(5>*3jfH^w3GPe@Q4hFFyr~Az5Z)fZA82Kgk%(PTn2^K;P+ga7iQ{)KnR47dKN|f)gZhqGqT2B4-dnH};QtTinwkaN2-?uN}s*XIuy#UHu1xCY$r68Gv&xk^iGfBH`XFM3RE+tE5(JjPUO9}DfTWQmiH42Dr`_*Z*J%r*Ao^DoTz_noD+r3lheVp6x5}qD{s(i=;&dJ z5PsMHq|(C{e55?VM?D4Kk`YHy5#&KE0aGvUtgQyc3#4%<_?Ozs?xNQG<^|cW7;z;W zGx_7+SO5zD*wY?Bu;M4CciKx;`lpk`ROG&^s@DJUo+|V!9IAXZYVvQ)q4BjOQBjrgiUD!!$<~^+GXRJ1%Q>BJlpZ^iO7~ z)`;W$8W;1<)ez?vlD?S?o|1%KM?&16ux;LSrXvQ9C;?53DWxT=En$*i^%jzR_I@o5 z9UD0|QgwqBZ4~O(=D|t{d>UC{r}^*rgCvC;$N?PaFb3yoHUP`PqD@P7Fu<>*o}A5R zj-FRxEmrRz?YJK#4-I>}t4V_Usp@h=Bo^K`pwdy zYkq|?6*qoi6|}h;6-H9iD43^cU3g}W0itMsX^ivYO(e68{&v5X!#xD03yaL}%{51J zOr5}DX)q_&tEiPi_6Kq1!RkLuYN5^|7W1F_=Pa5?F?W&b;{gikd7A$Z$0tLoMvb(u z1gAnV|H$L@XK-piT3BhJoi8?ic`t`(re)tcbky$%K-{=v_-n~whZr;-STTxKi^PRd zTC7sZ0D*jikMO~q$mdUb{p<61kILKjm*DO4EyB=+m8^+d^W!ovF|mkN zbFyuHayA-al{BbyFik*oQ}~Mi+o)d2CHMm>&t`>a-?AD%>PwRW5izmDIaR=)anBE(8$93yIwvGlVDK zuGt<&&~`2%ST+`S)hO$RU+iu>vHV9SOHWpVHMyvPNt6g4-VE?%cU{89#aWvPSJ2G( zD=$iql7f4dFxBu+hRi~@kbP=RxmK(N2L9yb2Vl-Eykx=(P(Oe7PHqSe^FUNlDMm^n z)TT|%EB%755>U9`!)FK|f1JV?L1?3wCiG{akOHZn6Flu9BEvX~SCrfMKU-O=V!wo6 zs4xY6xAt5HS9nhI@Jea^O?Im6PE3*C-eaZ=lv7V7y%oC%k#HUIo$9BK9hA~M<5(wD zEAYkct{F{uI0MB-xbD35cXQZ^PLD+Z59ymghiJ|js$ZWo*|2ne_1^6v#NGGkpbsez zbv@s()~k&0)&wPydH{#?wZh<-Y+2KJ{>Xw^HrH2HF_(u0yy2EIRY}n6@;UHn+_~y$ z8v?e^(m(=>VVQj5jxjogK)=b$6dazwG2)_&O^ASz@zP8PsbC>cYXZzNMB50Auy^j- z?kCONbV$?pYh-ujv%+#mRIND6pg-EUM}%=VQspchS8=d9!b3M4P?3&B=I|$-a?>O= z!!&O}S3C_H^Fh$evyL}FlWmjRU5=Py`3Z%k6Vf>}IN6!fnmTjd1Y19GVFUE-cXvB_ zGI!FI}@#&HrpOpw`XgWDX6Vd71`QEAAe?);#p$KCMj&9D~>@J=;q^ zHqxqg17acv3xnt`FzxGR3DqIUEe36Z-vcwPhuO(Ik<%19hH0fPw`O*HJtff40Hxd_ zOUsJ|{HTCv9MmKp2Z-%% zpe>FBs24@PwcE1yl#T1%mjR!ezGIX-CIiY9P}`(k zx#?h`M%a(Vz3uO~#&UCPK$B<($x&y8EQpq~mQ#c%y+6X4wIqIl@Z`rA`EZ3l_DOj8 z@X3i`?pdJ&Ea2qBQr+tEGVvS_1h=BYam7!OU16S2dpf}v@}|IBQCnQGx&`3EM%1Lr zNB@N;dU^lw9ije+0P8T-35T~S4iX4u;%B+B=p*Ru1jZv%n&f5wTZpL48R0-qP4wyS za9ub4i@H_a5uua6*plf!b}T&QqamZa3w5aO@zj5?pS2YD%wwa8X-^@Y5^Cg9lt0E4 zr4@=k)?Mr~3T(mMcHq-qy^+jnT`|Ui4oVU>63^uJNh%Os&f|GL)Dg5D9M`Dya(GW} zzCejXR=Cbv4I2JaPamvu~p+R`y*icyR+)d~N?0wE2`r!IS`^2NQuk2!^Q3 zAxs9P%B`T(P;EaOz~^BC0|}m7bgYNLKAi)Hkw~|Q4hd6E0LGR3bZO5{{GZ*_nr5#A zq&hpWv`Gs+18_hNiMh;osue6v0R14ji4kS5^F0(Cz)*GRd-da>g9Ak=KNYtzL$9l* zXz9u=i>Zc{eW2B+X=hD|NqftZX#7`g+~9f9+KJQgqyHMfqYo5xj}CeC_qKe= zA9R)&%@R_E^g6s@zMnjKB`wzoGITU1!@tTTJ;o~7ISGHb$r=}%@iYRuei)Mj@DEh@=sk5hGM-fo@dvlSr(oj% From fa44c551dd710bf212da7ade0b809ca4f4ba393b Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Tue, 30 Mar 2021 14:09:44 -0500 Subject: [PATCH 019/288] rename markdown --- base/static/reports/20210121/{pipelines.md => methods.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename base/static/reports/20210121/{pipelines.md => methods.md} (100%) diff --git a/base/static/reports/20210121/pipelines.md b/base/static/reports/20210121/methods.md similarity index 100% rename from base/static/reports/20210121/pipelines.md rename to base/static/reports/20210121/methods.md From 34d1286de68a8fe95b02ba4513eba4144a4923ab Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Tue, 30 Mar 2021 14:13:07 -0500 Subject: [PATCH 020/288] create dev datastore objects --- base/cloud_config.py | 3 ++- base/config.py | 2 +- base/models.py | 25 ++++++++++++++++--------- base/views/admin/data.py | 2 +- base/views/admin/users.py | 2 +- 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/base/cloud_config.py b/base/cloud_config.py index b5b7f89d..77c59689 100644 --- a/base/cloud_config.py +++ b/base/cloud_config.py @@ -22,7 +22,8 @@ class CloudConfig: {'dataset': '20170531', 'wormbase': 'WS258', 'version': 'v1'}, {'dataset': '20160408', 'wormbase': 'WS245', 'version': 'v1'}] } - def __init__(self, name, cc=default_cc, local=True): + def __init__(self, name, cc=default_cc, kind_prefix='', local=True): + self.kind = '{}{}'.format(kind_prefix, self.kind) self.name = name self.filename = f"{name}.txt" self.cc = cc diff --git a/base/config.py b/base/config.py index 9d401c53..fd8750e4 100644 --- a/base/config.py +++ b/base/config.py @@ -58,7 +58,7 @@ def get_config(APP_CONFIG): cc = None local = True if CLOUD_CONFIG == 1 else False # Add configuration variables from cloud - cc = CloudConfig(DEFAULT_CLOUD_CONFIG, local=local) + cc = CloudConfig(DEFAULT_CLOUD_CONFIG, kind_prefix=config['DS_PREFIX'], local=local) cc.load() cc.get_external_content() props = cc.get_properties() diff --git a/base/models.py b/base/models.py index f70365b5..9ec61bb3 100644 --- a/base/models.py +++ b/base/models.py @@ -12,6 +12,7 @@ from sqlalchemy import or_, func from werkzeug.security import safe_str_cmp +from base.config import config from base.constants import GOOGLE_CLOUD_BUCKET, STRAIN_PHOTO_PATH from base.extensions import sqlalchemy from base.utils.gcloud import get_item, store_item, query_item, get_cendr_bucket, check_blob @@ -85,6 +86,7 @@ class trait_ds(datastore_model): If a task is re-run the report will only display the latest version. """ kind = 'trait' + kind = '{}{}'.format(config['DS_PREFIX'], kind) def __init__(self, *args, **kwargs): """ @@ -278,6 +280,7 @@ class mapping_ds(datastore_model): The mapping/peak interval model """ kind = 'mapping' + kind = '{}{}'.format(config['DS_PREFIX'], kind) def __init__(self, *args, **kwargs): super(mapping_ds, self).__init__(*args, **kwargs) @@ -289,6 +292,7 @@ class user_ds(datastore_model): information on users. """ kind = 'user' + kind = '{}{}'.format(config['DS_PREFIX'], kind) def __init__(self, *args, **kwargs): super(user_ds, self).__init__(*args, **kwargs) @@ -316,7 +320,7 @@ def save(self, *args, **kwargs): def reports(self): filters = [('user_id', '=', self.name)] # Note this requires a composite index defined very precisely. - results = query_item('trait', filters=filters, order=['user_id', '-created_on']) + results = query_item(self.kind, filters=filters, order=['user_id', '-created_on']) results = sorted(results, key=lambda x: x['created_on'], reverse=True) results_out = defaultdict(list) for row in results: @@ -324,8 +328,8 @@ def reports(self): # Generate report objects return results_out - def get_all(keys_only=False): - results = query_item('user', keys_only=keys_only) + def get_all(self, keys_only=False): + results = query_item(self.kind, keys_only=keys_only) return results def set_password(self, password, salt): @@ -357,17 +361,18 @@ class markdown_ds(datastore_model): documents uploaded to the site """ kind = 'markdown' + kind = '{}{}'.format(config['DS_PREFIX'], kind) def __init__(self, *args, **kwargs): super(markdown_ds, self).__init__(*args, **kwargs) - def get_all(keys_only=False): - results = query_item('markdown', keys_only=keys_only) + def get_all(self, keys_only=False): + results = query_item(self.kind, keys_only=keys_only) return results - def query_by_type(type, keys_only=False): + def query_by_type(self, type, keys_only=False): filters = [('type', '=', type)] - results = query_item('markdown', filters=filters, keys_only=keys_only) + results = query_item(self.kind, filters=filters, keys_only=keys_only) return results def save(self, *args, **kwargs): @@ -384,6 +389,7 @@ class data_report_ds(datastore_model): releases of genomic data """ kind = 'data-report' + kind = '{}{}'.format(config['DS_PREFIX'], kind) def init(self): self.dataset = '' @@ -399,8 +405,8 @@ def init(self): def __init__(self, *args, **kwargs): super(data_report_ds, self).__init__(*args, **kwargs) - def get_all(keys_only=False): - results = query_item('data-report', keys_only=keys_only) + def get_all(self, keys_only=False): + results = query_item(self.kind, keys_only=keys_only) return results def list_bucket_dirs(): @@ -432,6 +438,7 @@ class config_ds(datastore_model): for the site's data sources """ kind = 'config' + kind = '{}{}'.format(config['DS_PREFIX'], kind) def __init__(self, *args, **kwargs): super(config_ds, self).__init__(*args, **kwargs) diff --git a/base/views/admin/data.py b/base/views/admin/data.py index 964f7965..3970b52c 100644 --- a/base/views/admin/data.py +++ b/base/views/admin/data.py @@ -32,7 +32,7 @@ def data_admin(id=None): if id is None: title = 'All' - items = data_report_ds.get_all() + items = data_report_ds().get_all() else: return redirect(url_for('data_admin.data_edit', id=id)) diff --git a/base/views/admin/users.py b/base/views/admin/users.py index 2014d8cb..ee42b832 100644 --- a/base/views/admin/users.py +++ b/base/views/admin/users.py @@ -26,7 +26,7 @@ def users(id=None): if id is None: title = 'All' - users = user_ds.get_all() + users = user_ds().get_all() return render_template('admin/users_list.html', **locals()) else: return redirect(url_for('users.users_edit'), id=id) From 5247341fd02032b8380bcc4560f2709d1910b129 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Tue, 30 Mar 2021 14:27:20 -0500 Subject: [PATCH 021/288] store client objects --- base/utils/gcloud.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/base/utils/gcloud.py b/base/utils/gcloud.py index 2651dbde..bb6c2123 100644 --- a/base/utils/gcloud.py +++ b/base/utils/gcloud.py @@ -18,12 +18,12 @@ def google_datastore(open=False): Args: open - Return the client without storing it in the g object. """ - client = datastore.Client(project=GOOGLE_CLOUD_PROJECT_ID) - if open: - return client - if not hasattr(g, 'ds'): - g.ds = client - return g.ds + if (g and open == False): + if not hasattr(g, 'ds'): + g.ds = datastore.Client(project=GOOGLE_CLOUD_PROJECT_ID) + return g.ds + + return datastore.Client(project=GOOGLE_CLOUD_PROJECT_ID) def delete_item(item): @@ -143,13 +143,13 @@ def google_storage(open=False): Args: open - Return the client without storing it in the g object. """ - client = storage.Client(project=GOOGLE_CLOUD_PROJECT_ID) - if open: - return client - if g and not hasattr(g, 'gs'): - g.gs = client + if (g and open == False): + if not hasattr(g, 'gs'): + g.gs = storage.Client(project=GOOGLE_CLOUD_PROJECT_ID) return g.gs - return client + + return storage.Client(project=GOOGLE_CLOUD_PROJECT_ID) + def get_cendr_bucket(): @@ -223,9 +223,8 @@ def google_analytics(): def generate_download_signed_url_v4(blob_path, expiration=datetime.timedelta(minutes=15)): """Generates a v4 signed URL for downloading a blob. """ - # blob_name = 'your-object-name' - storage_client = storage.Client.from_service_account_json('env_config/client-secret.json') - bucket = storage_client.bucket(GOOGLE_CLOUD_BUCKET) + client = google_storage() + bucket = client.bucket(GOOGLE_CLOUD_BUCKET) blob = bucket.blob(blob_path) url = blob.generate_signed_url( @@ -237,8 +236,8 @@ def generate_download_signed_url_v4(blob_path, expiration=datetime.timedelta(min def generate_upload_signed_url_v4(blob_name, content_type="application/octet-stream"): """Generates a v4 signed URL for uploading a blob using HTTP PUT. """ - storage_client = storage.Client.from_service_account_json('env_config/client-secret.json') - bucket = storage_client.bucket(GOOGLE_CLOUD_BUCKET) + client = google_storage() + bucket = client.bucket(GOOGLE_CLOUD_BUCKET) blob = bucket.blob(blob_name) url = blob.generate_signed_url( From c64399bea8c30c2d65dedba8eec6a5e70db2596c Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Thu, 1 Apr 2021 07:42:23 -0500 Subject: [PATCH 022/288] fix prefix, includes --- base/config.py | 3 +++ base/views/user.py | 7 +++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/base/config.py b/base/config.py index fd8750e4..c37e1cfd 100644 --- a/base/config.py +++ b/base/config.py @@ -55,6 +55,9 @@ def get_config(APP_CONFIG): "WORMBASE_VERSION": WORMBASE_VERSION, "RELEASES": RELEASES}) + config['DS_PREFIX'] = '' + if APP_CONFIG == 'development': + config['DS_PREFIX'] = 'DEV_' cc = None local = True if CLOUD_CONFIG == 1 else False # Add configuration variables from cloud diff --git a/base/views/user.py b/base/views/user.py index 4707c297..3e3b38df 100644 --- a/base/views/user.py +++ b/base/views/user.py @@ -7,14 +7,13 @@ """ from flask import request, render_template, Blueprint, redirect, url_for -from flask_jwt_extended import jwt_required, get_jwt -from flask_jwt_extended.utils import get_current_user from slugify import slugify -from base.utils.jwt import assign_access_refresh_tokens +from base.config import config from base.models import user_ds from base.forms import user_register_form, user_update_form -from base.config import config +from base.utils.jwt import jwt_required, get_jwt, get_current_user, assign_access_refresh_tokens + user_bp = Blueprint('user', __name__) From 761dce1cae3ec41a657113ae75262cc78473ea72 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Thu, 1 Apr 2021 11:03:55 -0500 Subject: [PATCH 023/288] cache bam_downloads --- base/views/data.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/base/views/data.py b/base/views/data.py index ead1dfcb..870a1aaf 100644 --- a/base/views/data.py +++ b/base/views/data.py @@ -7,6 +7,7 @@ from base.config import config from base.constants import GOOGLE_CLOUD_BUCKET +from base.extensions import cache from base.views.api.api_strain import get_isotypes, query_strains from base.models import Strain from base.utils.gcloud import list_release_files, generate_download_signed_url_v4 @@ -21,6 +22,7 @@ @data_bp.route('/release/latest') @data_bp.route('/release/') +@cache.memoize(50) def data(selected_release=None): """ Default data page - lists @@ -44,6 +46,7 @@ def data(selected_release=None): return render_template('data_v2.html', **locals()) +@cache.memoize(50) def data_v01(selected_release): # Legacy releases (Pre 20200101) title = "Genomic Data" @@ -64,11 +67,13 @@ def data_v01(selected_release): wormbase_genome_version = dict(config["RELEASES"])[selected_release] return render_template('data.html', **locals()) + # ======================= # # Alignment Data Page # # ======================= # @data_bp.route('/release/latest/alignment') @data_bp.route('/release//alignment') +@cache.memoize(50) def alignment_data(selected_release=None): """ Alignment data page @@ -92,6 +97,7 @@ def alignment_data(selected_release=None): # =========================== # @data_bp.route('/release/latest/strain_issues') @data_bp.route('/release//strain_issues') +@cache.memoize(50) def strain_issues(selected_release=None): """ Strain Issues page @@ -113,6 +119,7 @@ def strain_issues(selected_release=None): # Download Script # # =================== # @data_bp.route('/release//download/download_isotype_bams.sh') +@cache.cached(timeout=60*60*24) @jwt_required() def download_script(selected_release): script_content = generate_bam_download_script(release=selected_release) @@ -124,6 +131,7 @@ def download_script(selected_release): @data_bp.route('/release/latest/download/download_strain_bams.sh') @data_bp.route('/release//download/download_strain_bams.sh') +@cache.cached(timeout=60*60*24) @jwt_required() def download_script_strain_v2(selected_release=None): if selected_release is None: @@ -144,17 +152,20 @@ def download_bam_url(blob_name=''): return render_template('download.html', **locals()) +@cache.memoize(timeout=60*60*24) def generate_bam_download_script(release): ''' Generates signed downloads urls for every sequenced strain and creates a script to download them ''' script_content = '' expiration = timedelta(days=7) strain_listing = query_strains(release=release, is_sequenced=True) + for strain in strain_listing: bam_path = 'bam/{}.bam'.format(strain) bai_path = 'bam/{}.bam.bai'.format(strain) - script_content += '\n\n# Strain: {}'.format(strain) + script_content += f'\n\n# Strain: {strain}' script_content += '\nwget "{}"'.format(generate_download_signed_url_v4(bam_path, expiration=expiration)) script_content += '\nwget "{}"'.format(generate_download_signed_url_v4(bai_path, expiration=expiration)) + return script_content From 4a9639073b33e16feb019c3c724cb35ed6760bcd Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Thu, 1 Apr 2021 11:26:59 -0500 Subject: [PATCH 024/288] add dev cache --- base/utils/cache.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/base/utils/cache.py b/base/utils/cache.py index ee49fafc..7a7260b6 100644 --- a/base/utils/cache.py +++ b/base/utils/cache.py @@ -13,6 +13,7 @@ from time import time from base.config import config +kind = config['DS_PREFIX'] + 'cache' class DatastoreCache(BaseCache): def __init__(self, default_timeout=500): @@ -23,14 +24,14 @@ def set(self, key, value, timeout=None): expires = time() + timeout try: value = base64.b64encode(pickle.dumps(value)) - store_item('cache', self.key_prefix + "/" + key, value=value, expires=expires, exclude_from_indexes=['value']) + store_item(kind, self.key_prefix + "/" + key, value=value, expires=expires, exclude_from_indexes=['value']) return True except: return False def get(self, key): try: - item = get_item('cache', self.key_prefix + "/" + key) + item = get_item(kind, self.key_prefix + "/" + key) value = item.get('value') value = pickle.loads(base64.b64decode(value)) expires = item.get('expires') @@ -46,14 +47,14 @@ def get_dict(self, *keys): results = {} for key in keys: try: - results.update({key: get_item('cache', key)}) + results.update({key: get_item(kind, key)}) except AttributeError: pass return results def has(self, key): try: - item = get_item('cache', key) + item = get_item(kind, key) expires = item.get('expires') if expires == 0 or expires > time(): return True @@ -62,7 +63,7 @@ def has(self, key): def set_many(self, mapping, timeout): for k, v in mapping.items(): - store_item('cache', k, value=v) + store_item(kind, k, value=v) def datastore_cache(app, config, args, kwargs): @@ -72,5 +73,5 @@ def datastore_cache(app, config, args, kwargs): def delete_expired_cache(): epoch_time = int(time()) filters = [("expires", "<", epoch_time)] - num_deleted = delete_items_by_query('cache', filters=filters, projection=['expires']) + num_deleted = delete_items_by_query(kind, filters=filters, projection=['expires']) return num_deleted \ No newline at end of file From c060dc4d123ade245e14618573bf8ee4796a3a51 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 5 Apr 2021 16:02:16 -0500 Subject: [PATCH 025/288] h2calc, tasks, jwt/user updates --- base/constants.py | 1 + base/models.py | 27 + base/queue.yaml | 6 + base/templates/data_v2.html | 21 +- base/templates/tools/h2_result_list.html | 50 ++ .../tools/heritability_calculator.html | 279 +++++++--- .../templates/tools/heritability_results.html | 265 +++++----- base/templates/user/profile.html | 7 +- base/utils/gcloud.py | 94 +++- base/utils/{jwt.py => jwt_utils.py} | 0 base/views/about.py | 2 +- base/views/admin/admin.py | 2 +- base/views/admin/data.py | 2 +- base/views/admin/users.py | 2 +- base/views/auth/auth.py | 5 +- base/views/auth/oauth.py | 3 +- base/views/auth/saml.py | 32 +- base/views/data.py | 19 +- base/views/order.py | 2 +- base/views/tools/heritability.py | 266 +++++----- base/views/user.py | 3 +- cloud_buckets/cors.json | 8 + cloud_functions/heritability_run/.envrc | 2 + .../heritability_run/.gcloudignore | 2 + cloud_functions/heritability_run/Dockerfile | 41 ++ cloud_functions/heritability_run/H2_script.R | 476 ++++++++++++++++++ cloud_functions/heritability_run/README.md | 8 + .../heritability_run/cloudbuild.yaml | 6 + cloud_functions/heritability_run/invoke.go | 203 ++++++++ requirements.txt | 2 + 30 files changed, 1455 insertions(+), 381 deletions(-) create mode 100644 base/queue.yaml create mode 100644 base/templates/tools/h2_result_list.html rename base/utils/{jwt.py => jwt_utils.py} (100%) create mode 100644 cloud_buckets/cors.json create mode 100644 cloud_functions/heritability_run/.envrc create mode 100644 cloud_functions/heritability_run/.gcloudignore create mode 100644 cloud_functions/heritability_run/Dockerfile create mode 100644 cloud_functions/heritability_run/H2_script.R create mode 100644 cloud_functions/heritability_run/README.md create mode 100644 cloud_functions/heritability_run/cloudbuild.yaml create mode 100644 cloud_functions/heritability_run/invoke.go diff --git a/base/constants.py b/base/constants.py index ec717b89..95ed7a30 100644 --- a/base/constants.py +++ b/base/constants.py @@ -39,6 +39,7 @@ class PRICES: GOOGLE_CLOUD_BUCKET = 'elegansvariation.org' GOOGLE_CLOUD_PROJECT_ID = 'andersen-lab' +GOOGLE_CLOUD_LOCATION = 'us-central1' # WI Strain Info Dataset GOOGLE_SHEETS = {"orders": "1BCnmdJNRjQR3Bx8fMjD_IlTzmh3o7yj8ZQXTkk6tTXM", diff --git a/base/models.py b/base/models.py index 9ec61bb3..07935a6d 100644 --- a/base/models.py +++ b/base/models.py @@ -383,6 +383,33 @@ def save(self, *args, **kwargs): super(markdown_ds, self).save(*args, **kwargs) + +class h2calc_ds(datastore_model): + """ + The Heritability Calculation Task Model - for creating and retrieving + data and status information about a heritability calculation task + executed in Google Cloud Run + """ + kind = 'h2calc' + kind = '{}{}'.format(config['DS_PREFIX'], kind) + + + def __init__(self, *args, **kwargs): + super(h2calc_ds, self).__init__(*args, **kwargs) + + def query_by_username(self, username, keys_only=False): + filters = [('username', '=', username)] + results = query_item(self.kind, filters=filters, keys_only=keys_only) + return results + + def save(self, *args, **kwargs): + now = arrow.utcnow().datetime + self.modified_on = now + if not self._exists: + self.created_on = now + super(h2calc_ds, self).save(*args, **kwargs) + + class data_report_ds(datastore_model): """ The Data Report model - for creating and retrieving diff --git a/base/queue.yaml b/base/queue.yaml new file mode 100644 index 00000000..bfa0d41c --- /dev/null +++ b/base/queue.yaml @@ -0,0 +1,6 @@ +queue: +- name: heritability-calc + rate: 1/s + retry_parameters: + task_retry_limit: 2 + task_age_limit: 1d diff --git a/base/templates/data_v2.html b/base/templates/data_v2.html index 950abdc2..d286941a 100644 --- a/base/templates/data_v2.html +++ b/base/templates/data_v2.html @@ -1,19 +1,24 @@ -{% extends "_layouts/default.html" %} {% block custom_head %} +{% extends "_layouts/default.html" %} + +{% block custom_head %} + + {% endblock %} {% block content %} +
    {# /Download Tab #} - {% endblock %} + {% block script %} + + {% endblock %} diff --git a/base/templates/tools/h2_result_list.html b/base/templates/tools/h2_result_list.html new file mode 100644 index 00000000..dfe9b55f --- /dev/null +++ b/base/templates/tools/h2_result_list.html @@ -0,0 +1,50 @@ +{% extends "_layouts/default.html" %} + +{% block content %} + +
    + +
    +
    + + + + + + + + + + + + {% for item in items %} + + {% if item %} + + + + + + {% endif %} + + {% endfor %} + +
    Label Data Hash Status Created:
    {{ item.label }} {{ item.data_hash }} {{ item.status }} {{ item.created_on }} + + View + +
    +
    +
    + + +{% endblock %} diff --git a/base/templates/tools/heritability_calculator.html b/base/templates/tools/heritability_calculator.html index 12ad4bcf..7dde73a1 100644 --- a/base/templates/tools/heritability_calculator.html +++ b/base/templates/tools/heritability_calculator.html @@ -2,6 +2,7 @@ {% block custom_head %} +
    + +
    -

    This tool will calculate the broad-sense heritability for your trait of interest using a set of C. elegans wild isolates. The broad-sense heritability is the amount of trait variance that comes from genetic differences in the assayed group of strains. Generally, it is the ratio of genetic variance to total (genetic plus environmental) variance.

    -

    To obtain the best estimate of heritability, please measure a set of at least five wild strains in three independent assays. These assays should use different nematode growths, synchronizations, bacterial food preparations, and any other experimental condition. You should measure trait variance across as many different experimental conditions (in one block) as you would typically encounter in a large experiment.

    -

    Please organize your data in a long format, where each row is an independent observation of one strain in one trait. The columns of the data set should be:

    -
    1. AssayNumber - a numeric indicator of independent assays.
    2. Strain - one of the CeNDR isotype reference strain names.
    3. @@ -40,38 +40,94 @@
    4. Value - the measured output of the trait (e.g. 297 for BroodSize).
    -
    Use example data
    -

    NA values will not be used in broad-sense heritability calculations.

    - +

    NA values will not be used in broad-sense heritability calculations.

    + +
    {# /col-md-12 #} +
    {# /row #} + + +{% if hide_form == True %} + +
    +
    {# /col-md-3 #} + {# /col-md-3 #} + {# /col-md-3 #} +
    {# /col-md-3 #} -
    {# /col-md-8 #}
    {# /row #} +{% else %} +
    -
    + +
    +
    {#/ col-md-3 #} + +
    {# /col-md-3 #} + + {# /col-md-3 #} + + {# /col-md-3 #} -
    -
    - -
    -
    - - Prepare your data according to the column headers (described above). Data should be pasted in the table below. - -
    -
    -
    -
    - -
    {# /col-md-12 #} -
    {# /row #} - -
    - -
    {#/ col-md-12 #} +
    {# /row #} + +
    +
    +
    + + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + Prepare your data according to the column headers (described below). Data should be pasted in the table below. +
    +
    {# /col-md-12 #} +
    {# /row #} + +
    {# /col-md-12 #}
    {# /row #} + + + + +{% endif %} + + {% endblock %} {% block script %} @@ -82,28 +138,62 @@ ]; var noNAs = 0; function dataValidator(instance, td, row, col, prop, value, cellProperties) { - Handsontable.renderers.TextRenderer.apply(this, arguments); - if (row === 0) { - cellProperties.readOnly = true; + Handsontable.renderers.TextRenderer.apply(this, arguments); + if (row === 0) { + cellProperties.readOnly = true; } - if (row === 0) { - td.style.fontWeight = 'bold'; - td.style.backgroundColor = '#EAEAEA'; - } + if (row === 0) { + td.style.fontWeight = 'bold'; + td.style.backgroundColor = '#EAEAEA'; + } - if (['NA',].indexOf(String(value).trim()) >= 0) { - td.style.background = '#FC6666'; - td.style.fontWeight = 'bold'; - } + if (['NA',].indexOf(String(value).trim()) >= 0) { + td.style.background = '#FC6666'; + td.style.fontWeight = 'bold'; + } if (duplicate_rows.indexOf(row) > -1) { - td.style.background = '#0CEF13'; + td.style.background = 'RED'; } + + if (trait_names.includes(value)) { + td.style.background = 'RED'; + } + + if (unknown_strains.includes(value)) { + td.style.background = 'RED'; + } } -var isValid = false +{% autoescape off %} +var strain_list = {{ strain_list }}; +{% endautoescape %} + +var isValid = false; var duplicate_rows = []; +var trait_names = []; +var unknown_strains = []; + + +var trim_data = function(data) { + for (let i = 0; i < data.length; i++) { + for (let j = 0; j < data[i].length; j++) { + if (data[i][j]) { + data[i][j] = data[i][j].replace("\r\n", "").replace("\r", "").replace("\n", ""); + data[i][j] = data[i][j].trim() + } + } + var stringData = JSON.stringify(data[i]) + if(JSON.stringify(['','','','','']) == stringData || "[null,null,null,null,null]" == stringData) { + data.splice(i, 1); + i--; + } + } + return data; +} + + var validate_data = function(data){ // Performs error checks: // 1) Duplicates @@ -111,36 +201,67 @@ // 3) Trait Name pass = true + data = trim_data(data); // Check for duplicates - dup_search = _.map(data, (item, idx) => item.toString()); - dup_values = _.chain(dup_search).groupBy().filter(x => x.length > 1 && x[0] !== ",,,,").flatten().uniq().value() - duplicate_rows = _.map(dup_search, (item, idx) => _.indexOf(dup_values, item) > -1 ? idx : null).filter(x => x !== null) + occurences = [] + duplicate_rows = [] + for (let i = 0; i < data.length; i++) { + item = JSON.stringify(data[i]); + if (occurences[item]) { + duplicate_rows.push(i) + } + occurences[item] = true; + }; // if dups are present alert user $("#duplicate_error").text("") if (duplicate_rows.length > 0) { - $("#duplicate_error").text("Please check the data because duplicate rows are present. The duplicate rows are shown in green.") + $("#duplicate_error").text("Please check the data because duplicate rows are present. The duplicate rows are shown in red.") pass = false } - // Count number of strains - n_strains = _.uniq(_.pluck(data, 1).filter((x, idx) => idx > 0 && x !== null)).length + // Strain names + var strain_names = _.uniq(_.pluck(data, 1).filter((x, idx) => idx > 0 && x !== null && x.length > 0)); + unknown_strains = _.difference(strain_names, strain_list); + $("#strain_name_error").text(""); + if (unknown_strains.length > 0) { + $("#strain_name_error").text("Please check the data - Some of the strains were not recognized") + pass = false + } - $("#strain_count_error").text("") + // Count number of strains + n_strains = strain_names.length; + $("#strain_count_error").text(""); if (n_strains < 5) { $("#strain_count_error").text("Please check the data because fewer than five strains are present. Please measure trait values for at least five wild strains in at least three independent assays.") pass = false } // Trait Name - trait_count = _.uniq(_.pluck(data, 2).filter((x, idx) => idx > 0 && x !== null)).length + trait_names = _.uniq(_.pluck(data, 2).filter((x, idx) => idx > 0 && x !== null && x.length > 0)); + trait_count = trait_names.length; $("#trait_name_error").text("") - if (trait_count > 1) { + if (trait_count >= 2) { + pass = false $("#trait_name_error").text("Please check the data. The TraitName has multiple unique values. Only data for a single trait allowed.") + var most_common_trait = _.chain(_.pluck(data, 2)).countBy().pairs().max(_.last).head().value(); + const index = trait_names.indexOf(most_common_trait); + if (index !== -1) { + trait_names.splice(index, 1); + } + } else { + trait_names = [] + } + + // Data label + label_len = $("#calcLabel").val().length + $("#calc_label_error").text("") + if (label_len == 0) { + $("#calc_label_error").text("Please include a brief description of the data.") pass = false } - + return pass } @@ -171,40 +292,26 @@ } }); + +$("#calcLabel").on('change', function() { + onFormChange() +}) + hot.addHook("afterChange", function() { - isValid = validate_data(hot.getData()); + onFormChange() +}) + +function onFormChange() { + isValid = validate_data(hot.getData()); hot.render(); if (isValid) { document.getElementById('hcalc').disabled = false; - - // Fetch dataset statistics - $.ajax({type: "POST", - url: "{{ url_for('heritability.check_data') }}", - data: JSON.stringify(hot.getData()), - contentType: "application/json; charset=utf-8", - dataType: 'json', - success:function(result) { - $("#trait_summary").html(` -
    - Input data summary: -
      -
    • Minimum: ${result['minimum']}
    • -
    • Maximum: ${result['maximum']}
    • -
    • 25% Quartile: ${result['25']}
    • -
    • 50% Quartile: ${result['50']}
    • -
    • 75% Quartile: ${result['75']}
    • -
    • Variance: ${result['variance']}
    • - `) - } - }); - } else { $("#trait_summary").html("") document.getElementById('hcalc').disabled = true; } - -}) +} // Enable setting example data $("#set-example").on('click', function() { @@ -225,16 +332,24 @@ // submit result $("#hcalc").on("click", function(e) { + $("#hcalc").addClass("disabled") if (isValid) { + var data = new FormData($('form#form-submit')[0]); + data.append('table_data', JSON.stringify(hot.getData())); + data.set('label', $("#calcLabel").val()); $.ajax({ type: "POST", + processData: false, + contentType: false, + dataType: 'json', url: "{{ url_for('heritability.submit_h2') }}", - data: JSON.stringify(hot.getData()), - contentType: "application/json; charset=utf-8", - dataType: 'json', - success:function(result){ - window.location = `heritability/h2/${result.data_hash}` - } + data: data, + success:function(result) { + window.location = `../heritability/h2/${result.id}` + }, + error:function(error) { + $("#hcalc").removeClass("disabled") + } }) } }); diff --git a/base/templates/tools/heritability_results.html b/base/templates/tools/heritability_results.html index 5293c734..b3d9e4fc 100644 --- a/base/templates/tools/heritability_results.html +++ b/base/templates/tools/heritability_results.html @@ -1,8 +1,8 @@ {% extends "_layouts/default.html" %} {% block custom_head %} - + @@ -11,6 +11,7 @@ + - {% endblock %} {% block content %} -
      {% endif %}
      {# col-md-6 #} +
      - {% for i in isotype %} - {% if i.strain_photo_url() %} - - - - {% endif %} - {% endfor %} + {% if isotype_ref_strain.strain_photo_url() %} +

      Photo

      + + {% endif %}
      {# /col-md-6 #} -
      {# row #} +
    {# row #} +
    {# col-md-8 #}
    @@ -147,7 +139,7 @@

    Alternative Names

  • - Data in this table is for the isotype reference strain. + Data in this table is for the reference strain.
  • diff --git a/base/templates/strain/strain_list.html b/base/templates/strain/strain_list.html deleted file mode 100644 index 8db481d7..00000000 --- a/base/templates/strain/strain_list.html +++ /dev/null @@ -1,361 +0,0 @@ -{% extends "_layouts/default.html" %} - - -{% block content %} - -
    -
    - - - - - -
    - - -
    -
    -
    -
    - Hover over or click a pin to see information about a C. elegans wild isolate -
    {# /text-center #} -
    {# /col-md-8 #} - -
    -
    -
    - - Strain Information -
    -
      - -
    • - - Isotype - -
      -
      -
    • - -
    • - - Strain - -
      -
      -
    • - -
    • - - Reference Strain - -
      -
      -
    • - -
    • Release
    • -
    • Isolation Date
    • -
    • Latitude, Longitude
    • -
    • Elevation
    • -
    • Landscape
    • -
    • Substrate
    • -
    • Sampled By
    • -
    -
    {# /panel #} - Submit Strains

    -
    {# /col-md-4 #} -
    {# /tabpanel #} - - -
    -
    -
    -
    - -
    {# /col-md-4 #} -
    {# /row #} -
    - - - - - - - - - - - - - - {% for isotype, strains in strain_listing|groupby('isotype') %} - - - - - - {% if strains[0].previous_names %} - - {% else %} - - {% endif %} - - - {% endfor %} - -
    - # - - - Reference Strain - - - - Isotype - - - - Strains - - - - Alternative Names - - - - Release - -
    {{ loop.index }} - {% set isotype_loop_index = loop.index %} - - - {{ isotype }} - - {{ strains|join(", ") }} - {{ strains[0].previous_names }} - {{ strains[0]['release'] }} -
    -
    {# /row #} -
    {# /tabpanel #} - - -
    - {% include('releases/download_tab_strain_v2_issues.html') %} -
    {# /tabpanel #} - - - {# /tabpanel #} - - -
    {# /tab-content #} -
    {# /col-md-12 #} -
    {# /row #} - -{% endblock %} - - -{% block script %} - - - -{% endblock %} diff --git a/base/templates/strain_issues.html b/base/templates/strain_issues.html deleted file mode 100644 index 45cfb9bb..00000000 --- a/base/templates/strain_issues.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "_layouts/default.html" %} - -{% block content %} - -{% include('releases/download_tab_strain_v2_issues.html') %} - -{% endblock %} - -{% block script %} - - - -{% endblock %} diff --git a/base/templates/tools/h2_result_list.html b/base/templates/tools/h2_result_list.html deleted file mode 100644 index dfe9b55f..00000000 --- a/base/templates/tools/h2_result_list.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "_layouts/default.html" %} - -{% block content %} - - - -
    -
    - - - - - - - - - - - - {% for item in items %} - - {% if item %} - - - - - - {% endif %} - - {% endfor %} - -
    Label Data Hash Status Created:
    {{ item.label }} {{ item.data_hash }} {{ item.status }} {{ item.created_on }} - - View - -
    -
    -
    - - -{% endblock %} diff --git a/base/templates/tools/heritability_calculator.html b/base/templates/tools/heritability_calculator.html index 7dde73a1..12ad4bcf 100644 --- a/base/templates/tools/heritability_calculator.html +++ b/base/templates/tools/heritability_calculator.html @@ -2,7 +2,6 @@ {% block custom_head %} -
    - -
    +

    This tool will calculate the broad-sense heritability for your trait of interest using a set of C. elegans wild isolates. The broad-sense heritability is the amount of trait variance that comes from genetic differences in the assayed group of strains. Generally, it is the ratio of genetic variance to total (genetic plus environmental) variance.

    +

    To obtain the best estimate of heritability, please measure a set of at least five wild strains in three independent assays. These assays should use different nematode growths, synchronizations, bacterial food preparations, and any other experimental condition. You should measure trait variance across as many different experimental conditions (in one block) as you would typically encounter in a large experiment.

    +

    Please organize your data in a long format, where each row is an independent observation of one strain in one trait. The columns of the data set should be:

    +
    1. AssayNumber - a numeric indicator of independent assays.
    2. Strain - one of the CeNDR isotype reference strain names.
    3. @@ -40,94 +40,38 @@
    4. Value - the measured output of the trait (e.g. 297 for BroodSize).
    -

    NA values will not be used in broad-sense heritability calculations.

    - -
    {# /col-md-12 #} -
    {# /row #} - - -{% if hide_form == True %} - -
    -
    {# /col-md-3 #} - {# /col-md-3 #} - {# /col-md-3 #} -
    {# /col-md-3 #} +
    Use example data
    +

    NA values will not be used in broad-sense heritability calculations.

    + +
    {# /col-md-8 #}
    {# /row #} -{% else %} -
    - -
    -
    {#/ col-md-3 #} - -
    {# /col-md-3 #} - - {# /col-md-3 #} - - {# /col-md-3 #} +
    -
    {# /row #} - -
    -
    -
    - - -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    - Prepare your data according to the column headers (described below). Data should be pasted in the table below. -
    -
    {# /col-md-12 #} -
    {# /row #} - - +
    +
    + +
    +
    + + Prepare your data according to the column headers (described above). Data should be pasted in the table below. + +
    +
    +
    +
    + +
    {# /col-md-12 #} +
    {# /row #} + +
    + +
    {#/ col-md-12 #}
    {# /col-md-12 #}
    {# /row #} - - - - -{% endif %} - - {% endblock %} {% block script %} @@ -138,62 +82,28 @@ ]; var noNAs = 0; function dataValidator(instance, td, row, col, prop, value, cellProperties) { - Handsontable.renderers.TextRenderer.apply(this, arguments); - if (row === 0) { - cellProperties.readOnly = true; + Handsontable.renderers.TextRenderer.apply(this, arguments); + if (row === 0) { + cellProperties.readOnly = true; } - if (row === 0) { - td.style.fontWeight = 'bold'; - td.style.backgroundColor = '#EAEAEA'; - } + if (row === 0) { + td.style.fontWeight = 'bold'; + td.style.backgroundColor = '#EAEAEA'; + } - if (['NA',].indexOf(String(value).trim()) >= 0) { - td.style.background = '#FC6666'; - td.style.fontWeight = 'bold'; - } + if (['NA',].indexOf(String(value).trim()) >= 0) { + td.style.background = '#FC6666'; + td.style.fontWeight = 'bold'; + } if (duplicate_rows.indexOf(row) > -1) { - td.style.background = 'RED'; + td.style.background = '#0CEF13'; } - - if (trait_names.includes(value)) { - td.style.background = 'RED'; - } - - if (unknown_strains.includes(value)) { - td.style.background = 'RED'; - } } -{% autoescape off %} -var strain_list = {{ strain_list }}; -{% endautoescape %} - -var isValid = false; +var isValid = false var duplicate_rows = []; -var trait_names = []; -var unknown_strains = []; - - -var trim_data = function(data) { - for (let i = 0; i < data.length; i++) { - for (let j = 0; j < data[i].length; j++) { - if (data[i][j]) { - data[i][j] = data[i][j].replace("\r\n", "").replace("\r", "").replace("\n", ""); - data[i][j] = data[i][j].trim() - } - } - var stringData = JSON.stringify(data[i]) - if(JSON.stringify(['','','','','']) == stringData || "[null,null,null,null,null]" == stringData) { - data.splice(i, 1); - i--; - } - } - return data; -} - - var validate_data = function(data){ // Performs error checks: // 1) Duplicates @@ -201,67 +111,36 @@ // 3) Trait Name pass = true - data = trim_data(data); // Check for duplicates - occurences = [] - duplicate_rows = [] - for (let i = 0; i < data.length; i++) { - item = JSON.stringify(data[i]); - if (occurences[item]) { - duplicate_rows.push(i) - } - occurences[item] = true; - }; + dup_search = _.map(data, (item, idx) => item.toString()); + dup_values = _.chain(dup_search).groupBy().filter(x => x.length > 1 && x[0] !== ",,,,").flatten().uniq().value() + duplicate_rows = _.map(dup_search, (item, idx) => _.indexOf(dup_values, item) > -1 ? idx : null).filter(x => x !== null) // if dups are present alert user $("#duplicate_error").text("") if (duplicate_rows.length > 0) { - $("#duplicate_error").text("Please check the data because duplicate rows are present. The duplicate rows are shown in red.") - pass = false - } - - // Strain names - var strain_names = _.uniq(_.pluck(data, 1).filter((x, idx) => idx > 0 && x !== null && x.length > 0)); - unknown_strains = _.difference(strain_names, strain_list); - $("#strain_name_error").text(""); - if (unknown_strains.length > 0) { - $("#strain_name_error").text("Please check the data - Some of the strains were not recognized") + $("#duplicate_error").text("Please check the data because duplicate rows are present. The duplicate rows are shown in green.") pass = false } // Count number of strains - n_strains = strain_names.length; - $("#strain_count_error").text(""); + n_strains = _.uniq(_.pluck(data, 1).filter((x, idx) => idx > 0 && x !== null)).length + + $("#strain_count_error").text("") if (n_strains < 5) { $("#strain_count_error").text("Please check the data because fewer than five strains are present. Please measure trait values for at least five wild strains in at least three independent assays.") pass = false } // Trait Name - trait_names = _.uniq(_.pluck(data, 2).filter((x, idx) => idx > 0 && x !== null && x.length > 0)); - trait_count = trait_names.length; + trait_count = _.uniq(_.pluck(data, 2).filter((x, idx) => idx > 0 && x !== null)).length $("#trait_name_error").text("") - if (trait_count >= 2) { - pass = false + if (trait_count > 1) { $("#trait_name_error").text("Please check the data. The TraitName has multiple unique values. Only data for a single trait allowed.") - var most_common_trait = _.chain(_.pluck(data, 2)).countBy().pairs().max(_.last).head().value(); - const index = trait_names.indexOf(most_common_trait); - if (index !== -1) { - trait_names.splice(index, 1); - } - } else { - trait_names = [] - } - - // Data label - label_len = $("#calcLabel").val().length - $("#calc_label_error").text("") - if (label_len == 0) { - $("#calc_label_error").text("Please include a brief description of the data.") pass = false } - + return pass } @@ -292,26 +171,40 @@ } }); - -$("#calcLabel").on('change', function() { - onFormChange() -}) - hot.addHook("afterChange", function() { - onFormChange() -}) - -function onFormChange() { - isValid = validate_data(hot.getData()); + isValid = validate_data(hot.getData()); hot.render(); if (isValid) { document.getElementById('hcalc').disabled = false; + + // Fetch dataset statistics + $.ajax({type: "POST", + url: "{{ url_for('heritability.check_data') }}", + data: JSON.stringify(hot.getData()), + contentType: "application/json; charset=utf-8", + dataType: 'json', + success:function(result) { + $("#trait_summary").html(` +
    + Input data summary: +
      +
    • Minimum: ${result['minimum']}
    • +
    • Maximum: ${result['maximum']}
    • +
    • 25% Quartile: ${result['25']}
    • +
    • 50% Quartile: ${result['50']}
    • +
    • 75% Quartile: ${result['75']}
    • +
    • Variance: ${result['variance']}
    • + `) + } + }); + } else { $("#trait_summary").html("") document.getElementById('hcalc').disabled = true; } -} + +}) // Enable setting example data $("#set-example").on('click', function() { @@ -332,24 +225,16 @@ // submit result $("#hcalc").on("click", function(e) { - $("#hcalc").addClass("disabled") if (isValid) { - var data = new FormData($('form#form-submit')[0]); - data.append('table_data', JSON.stringify(hot.getData())); - data.set('label', $("#calcLabel").val()); $.ajax({ type: "POST", - processData: false, - contentType: false, - dataType: 'json', url: "{{ url_for('heritability.submit_h2') }}", - data: data, - success:function(result) { - window.location = `../heritability/h2/${result.id}` - }, - error:function(error) { - $("#hcalc").removeClass("disabled") - } + data: JSON.stringify(hot.getData()), + contentType: "application/json; charset=utf-8", + dataType: 'json', + success:function(result){ + window.location = `heritability/h2/${result.data_hash}` + } }) } }); diff --git a/base/templates/tools/heritability_results.html b/base/templates/tools/heritability_results.html index b3d9e4fc..5293c734 100644 --- a/base/templates/tools/heritability_results.html +++ b/base/templates/tools/heritability_results.html @@ -1,8 +1,8 @@ {% extends "_layouts/default.html" %} {% block custom_head %} + - @@ -11,7 +11,6 @@ - + {% endblock %} {% block content %} +
      {% endif %}
      {# col-md-6 #} -
      - {% if isotype_ref_strain.strain_photo_url() %} -

      Photo

      - - {% endif %} + {% for i in isotype %} + {% if i.strain_photo_url() %} + + + + {% endif %} + {% endfor %}
      {# /col-md-6 #} -
      {# row #} - +
    {# row #}
    {# col-md-8 #}
    @@ -139,7 +147,7 @@

    Photo

  • - Data in this table is for the reference strain. + Data in this table is for the isotype reference strain.
  • diff --git a/base/templates/strain/strain_list.html b/base/templates/strain/strain_list.html new file mode 100644 index 00000000..8db481d7 --- /dev/null +++ b/base/templates/strain/strain_list.html @@ -0,0 +1,361 @@ +{% extends "_layouts/default.html" %} + + +{% block content %} + +
    +
    + + + + + +
    + + +
    +
    +
    +
    + Hover over or click a pin to see information about a C. elegans wild isolate +
    {# /text-center #} +
    {# /col-md-8 #} + +
    +
    +
    + + Strain Information +
    +
      + +
    • + + Isotype + +
      +
      +
    • + +
    • + + Strain + +
      +
      +
    • + +
    • + + Reference Strain + +
      +
      +
    • + +
    • Release
    • +
    • Isolation Date
    • +
    • Latitude, Longitude
    • +
    • Elevation
    • +
    • Landscape
    • +
    • Substrate
    • +
    • Sampled By
    • +
    +
    {# /panel #} + Submit Strains

    +
    {# /col-md-4 #} +
    {# /tabpanel #} + + +
    +
    +
    +
    + +
    {# /col-md-4 #} +
    {# /row #} +
    + + + + + + + + + + + + + + {% for isotype, strains in strain_listing|groupby('isotype') %} + + + + + + {% if strains[0].previous_names %} + + {% else %} + + {% endif %} + + + {% endfor %} + +
    + # + + + Reference Strain + + + + Isotype + + + + Strains + + + + Alternative Names + + + + Release + +
    {{ loop.index }} + {% set isotype_loop_index = loop.index %} + + + {{ isotype }} + + {{ strains|join(", ") }} + {{ strains[0].previous_names }} + {{ strains[0]['release'] }} +
    +
    {# /row #} +
    {# /tabpanel #} + + +
    + {% include('releases/download_tab_strain_v2_issues.html') %} +
    {# /tabpanel #} + + + {# /tabpanel #} + + +
    {# /tab-content #} +
    {# /col-md-12 #} +
    {# /row #} + +{% endblock %} + + +{% block script %} + + + +{% endblock %} diff --git a/base/templates/strain_issues.html b/base/templates/strain_issues.html new file mode 100644 index 00000000..45cfb9bb --- /dev/null +++ b/base/templates/strain_issues.html @@ -0,0 +1,14 @@ +{% extends "_layouts/default.html" %} + +{% block content %} + +{% include('releases/download_tab_strain_v2_issues.html') %} + +{% endblock %} + +{% block script %} + + + +{% endblock %} diff --git a/base/templates/tools/h2_result_list.html b/base/templates/tools/h2_result_list.html new file mode 100644 index 00000000..dfe9b55f --- /dev/null +++ b/base/templates/tools/h2_result_list.html @@ -0,0 +1,50 @@ +{% extends "_layouts/default.html" %} + +{% block content %} + + + +
    +
    + + + + + + + + + + + + {% for item in items %} + + {% if item %} + + + + + + {% endif %} + + {% endfor %} + +
    Label Data Hash Status Created:
    {{ item.label }} {{ item.data_hash }} {{ item.status }} {{ item.created_on }} + + View + +
    +
    +
    + + +{% endblock %} diff --git a/base/templates/tools/heritability_calculator.html b/base/templates/tools/heritability_calculator.html index 12ad4bcf..7dde73a1 100644 --- a/base/templates/tools/heritability_calculator.html +++ b/base/templates/tools/heritability_calculator.html @@ -2,6 +2,7 @@ {% block custom_head %} +
    + +
    -

    This tool will calculate the broad-sense heritability for your trait of interest using a set of C. elegans wild isolates. The broad-sense heritability is the amount of trait variance that comes from genetic differences in the assayed group of strains. Generally, it is the ratio of genetic variance to total (genetic plus environmental) variance.

    -

    To obtain the best estimate of heritability, please measure a set of at least five wild strains in three independent assays. These assays should use different nematode growths, synchronizations, bacterial food preparations, and any other experimental condition. You should measure trait variance across as many different experimental conditions (in one block) as you would typically encounter in a large experiment.

    -

    Please organize your data in a long format, where each row is an independent observation of one strain in one trait. The columns of the data set should be:

    -
    1. AssayNumber - a numeric indicator of independent assays.
    2. Strain - one of the CeNDR isotype reference strain names.
    3. @@ -40,38 +40,94 @@
    4. Value - the measured output of the trait (e.g. 297 for BroodSize).
    -
    Use example data
    -

    NA values will not be used in broad-sense heritability calculations.

    - +

    NA values will not be used in broad-sense heritability calculations.

    + +
    {# /col-md-12 #} +
    {# /row #} + + +{% if hide_form == True %} + +
    +
    {# /col-md-3 #} + {# /col-md-3 #} + {# /col-md-3 #} +
    {# /col-md-3 #} -
    {# /col-md-8 #} {# /row #} +{% else %} +
    -
    + +
    +
    {#/ col-md-3 #} + +
    {# /col-md-3 #} + + {# /col-md-3 #} + + {# /col-md-3 #} -
    -
    - -
    -
    - - Prepare your data according to the column headers (described above). Data should be pasted in the table below. - -
    -
    -
    -
    - -
    {# /col-md-12 #} -
    {# /row #} - -
    - -
    {#/ col-md-12 #} +
    {# /row #} + +
    +
    +
    + + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + Prepare your data according to the column headers (described below). Data should be pasted in the table below. +
    +
    {# /col-md-12 #} +
    {# /row #} + +
    {# /col-md-12 #} {# /row #} + + + + +{% endif %} + + {% endblock %} {% block script %} @@ -82,28 +138,62 @@ ]; var noNAs = 0; function dataValidator(instance, td, row, col, prop, value, cellProperties) { - Handsontable.renderers.TextRenderer.apply(this, arguments); - if (row === 0) { - cellProperties.readOnly = true; + Handsontable.renderers.TextRenderer.apply(this, arguments); + if (row === 0) { + cellProperties.readOnly = true; } - if (row === 0) { - td.style.fontWeight = 'bold'; - td.style.backgroundColor = '#EAEAEA'; - } + if (row === 0) { + td.style.fontWeight = 'bold'; + td.style.backgroundColor = '#EAEAEA'; + } - if (['NA',].indexOf(String(value).trim()) >= 0) { - td.style.background = '#FC6666'; - td.style.fontWeight = 'bold'; - } + if (['NA',].indexOf(String(value).trim()) >= 0) { + td.style.background = '#FC6666'; + td.style.fontWeight = 'bold'; + } if (duplicate_rows.indexOf(row) > -1) { - td.style.background = '#0CEF13'; + td.style.background = 'RED'; } + + if (trait_names.includes(value)) { + td.style.background = 'RED'; + } + + if (unknown_strains.includes(value)) { + td.style.background = 'RED'; + } } -var isValid = false +{% autoescape off %} +var strain_list = {{ strain_list }}; +{% endautoescape %} + +var isValid = false; var duplicate_rows = []; +var trait_names = []; +var unknown_strains = []; + + +var trim_data = function(data) { + for (let i = 0; i < data.length; i++) { + for (let j = 0; j < data[i].length; j++) { + if (data[i][j]) { + data[i][j] = data[i][j].replace("\r\n", "").replace("\r", "").replace("\n", ""); + data[i][j] = data[i][j].trim() + } + } + var stringData = JSON.stringify(data[i]) + if(JSON.stringify(['','','','','']) == stringData || "[null,null,null,null,null]" == stringData) { + data.splice(i, 1); + i--; + } + } + return data; +} + + var validate_data = function(data){ // Performs error checks: // 1) Duplicates @@ -111,36 +201,67 @@ // 3) Trait Name pass = true + data = trim_data(data); // Check for duplicates - dup_search = _.map(data, (item, idx) => item.toString()); - dup_values = _.chain(dup_search).groupBy().filter(x => x.length > 1 && x[0] !== ",,,,").flatten().uniq().value() - duplicate_rows = _.map(dup_search, (item, idx) => _.indexOf(dup_values, item) > -1 ? idx : null).filter(x => x !== null) + occurences = [] + duplicate_rows = [] + for (let i = 0; i < data.length; i++) { + item = JSON.stringify(data[i]); + if (occurences[item]) { + duplicate_rows.push(i) + } + occurences[item] = true; + }; // if dups are present alert user $("#duplicate_error").text("") if (duplicate_rows.length > 0) { - $("#duplicate_error").text("Please check the data because duplicate rows are present. The duplicate rows are shown in green.") + $("#duplicate_error").text("Please check the data because duplicate rows are present. The duplicate rows are shown in red.") pass = false } - // Count number of strains - n_strains = _.uniq(_.pluck(data, 1).filter((x, idx) => idx > 0 && x !== null)).length + // Strain names + var strain_names = _.uniq(_.pluck(data, 1).filter((x, idx) => idx > 0 && x !== null && x.length > 0)); + unknown_strains = _.difference(strain_names, strain_list); + $("#strain_name_error").text(""); + if (unknown_strains.length > 0) { + $("#strain_name_error").text("Please check the data - Some of the strains were not recognized") + pass = false + } - $("#strain_count_error").text("") + // Count number of strains + n_strains = strain_names.length; + $("#strain_count_error").text(""); if (n_strains < 5) { $("#strain_count_error").text("Please check the data because fewer than five strains are present. Please measure trait values for at least five wild strains in at least three independent assays.") pass = false } // Trait Name - trait_count = _.uniq(_.pluck(data, 2).filter((x, idx) => idx > 0 && x !== null)).length + trait_names = _.uniq(_.pluck(data, 2).filter((x, idx) => idx > 0 && x !== null && x.length > 0)); + trait_count = trait_names.length; $("#trait_name_error").text("") - if (trait_count > 1) { + if (trait_count >= 2) { + pass = false $("#trait_name_error").text("Please check the data. The TraitName has multiple unique values. Only data for a single trait allowed.") + var most_common_trait = _.chain(_.pluck(data, 2)).countBy().pairs().max(_.last).head().value(); + const index = trait_names.indexOf(most_common_trait); + if (index !== -1) { + trait_names.splice(index, 1); + } + } else { + trait_names = [] + } + + // Data label + label_len = $("#calcLabel").val().length + $("#calc_label_error").text("") + if (label_len == 0) { + $("#calc_label_error").text("Please include a brief description of the data.") pass = false } - + return pass } @@ -171,40 +292,26 @@ } }); + +$("#calcLabel").on('change', function() { + onFormChange() +}) + hot.addHook("afterChange", function() { - isValid = validate_data(hot.getData()); + onFormChange() +}) + +function onFormChange() { + isValid = validate_data(hot.getData()); hot.render(); if (isValid) { document.getElementById('hcalc').disabled = false; - - // Fetch dataset statistics - $.ajax({type: "POST", - url: "{{ url_for('heritability.check_data') }}", - data: JSON.stringify(hot.getData()), - contentType: "application/json; charset=utf-8", - dataType: 'json', - success:function(result) { - $("#trait_summary").html(` -
    - Input data summary: -
      -
    • Minimum: ${result['minimum']}
    • -
    • Maximum: ${result['maximum']}
    • -
    • 25% Quartile: ${result['25']}
    • -
    • 50% Quartile: ${result['50']}
    • -
    • 75% Quartile: ${result['75']}
    • -
    • Variance: ${result['variance']}
    • - `) - } - }); - } else { $("#trait_summary").html("") document.getElementById('hcalc').disabled = true; } - -}) +} // Enable setting example data $("#set-example").on('click', function() { @@ -225,16 +332,24 @@ // submit result $("#hcalc").on("click", function(e) { + $("#hcalc").addClass("disabled") if (isValid) { + var data = new FormData($('form#form-submit')[0]); + data.append('table_data', JSON.stringify(hot.getData())); + data.set('label', $("#calcLabel").val()); $.ajax({ type: "POST", + processData: false, + contentType: false, + dataType: 'json', url: "{{ url_for('heritability.submit_h2') }}", - data: JSON.stringify(hot.getData()), - contentType: "application/json; charset=utf-8", - dataType: 'json', - success:function(result){ - window.location = `heritability/h2/${result.data_hash}` - } + data: data, + success:function(result) { + window.location = `../heritability/h2/${result.id}` + }, + error:function(error) { + $("#hcalc").removeClass("disabled") + } }) } }); diff --git a/base/templates/tools/heritability_results.html b/base/templates/tools/heritability_results.html index 5293c734..b3d9e4fc 100644 --- a/base/templates/tools/heritability_results.html +++ b/base/templates/tools/heritability_results.html @@ -1,8 +1,8 @@ {% extends "_layouts/default.html" %} {% block custom_head %} - + @@ -11,6 +11,7 @@ + -{% endblock %} +{% endblock %} + {% block content %}
      - -
      -
      -

      Strain Sets

      -
      -
      -
      - -
      -
      + +
      +
      +

      Strain Sets

      +
      +
      +
      +
      -
      -
      +
      +
      +
      +

      Pre-defined sets of strains can be ordered below.

      -

      The divergent set includes 12 genotypically different strains and can be used to determine broad-sense heritability after repeated independent measures of your favorite phenotype. We recommend starting with the divergent set.

      -

      Sets 1-8 each contain 46-48 strains that should be assayed for genetic mapping experiments. We recommend using a single set of 48 strains. After you have scored and optimized your quantitative trait, additional sets can be assayed to increase statistical power for association mapping.

      -
      -
      -
      -
      Strain Data
      -
      - Strain Data including isolation location, isotype information, and more is available for download.

      - Download Strain Data -
      -
      -
      -
      - -
      -
      - - - - - - - - - - - - - - - - - - {% for i in range(1,9) %} - - - - - - - {% endfor %} - -
      - - Set NameNumber of StrainsStrains
      - - {{ len(strain_sets["D"]) }} - - {% for strain in strain_sets["D"] %} - {{ strain }} - {% endfor %} - -
      - {# REVERT - enable mapping set 7 and 8 #} - = 7 %}disabled{% endif %} value="set_{{ i }}"/> - {{ len(strain_sets["{}".format(i)]) }} - View Strains -
      -
        - {% for strain in strain_sets["{}".format(i)] %} -
      • {{ strain }}
      • - {% endfor %} -
      -
      -
      +

      The divergent set includes 12 genotypically different strains and can be used to determine broad-sense heritability after repeated independent measures of your favorite phenotype. We recommend starting with the divergent set.

      +

      Sets 1-8 each contain 46-48 strains that should be assayed for genetic mapping experiments. We recommend using a single set of 48 strains. After you have scored and optimized your quantitative trait, additional sets can be assayed to increase statistical power for association mapping.

      +
      +
      +
      +
      Strain Data
      +
      + Strain Data including isolation location, isotype information, and more is available for download.

      + Download Strain Data
      +
      +
      -
      -
      -

      Individual Strains

      +
      +
      -

      Individual strains can be ordered below.

      -
      -
      - -
      -
      - -
      -
      -
      - +
      - - - - - - - + + + - - {% for isotype, strains in strain_listing|groupby('isotype') %} + + + + + + + + {% for i in range(1,9) %} - + + - - - {% if strains[0].previous_names %} - - {% else %} - - {% endif %} - {% endfor %} -
      # - - - - Reference Strain - - - - Isotype - - - - Strains - Alternative Names - + + ReleaseSet NameNumber of StrainsStrains
      + + + {{ len(strain_sets["D"]) }} + + {% for strain in strain_sets["D"] %} + {{ strain }} + {% endfor %} + +
      {{ loop.index }} - {% set isotype_loop_index = loop.index %} - + {# REVERT - enable mapping set 7 and 8 #} + = 7 %}disabled{% endif %} value="set_{{ i }}" class="styled-checkbox" /> + {{ len(strain_sets["{}".format(i)]) }} - + View Strains +
      +
        + {% for strain in strain_sets["{}".format(i)] %} +
      • {{ strain }}
      • + {% endfor %} +
      +
      {{ isotype }}{{ strains|join(", ") }}{{ strains[0].previous_names }} {{ strains[0]['release'] }}
      +
      +
      + +
      +
      +

      Individual Strains

      + +

      Individual strains can be ordered below.

      +
      +
      + +
      +
      + +
      +
      +
      + + + + + + + + + + + + + + {% for isotype, strains in strain_listing|groupby('isotype') %} + + + + + + + {% if strains[0].previous_names %} + + {% else %} + + {% endif %} + + + {% endfor %} + + +
      # + + + + Reference Strain + + + + Isotype + + + + Strains + Alternative Names + + Release
      {{ loop.index }} + {% set isotype_loop_index = loop.index %} + + + + + + {{ isotype }}{{ strains|join(", ") }}{{ strains[0].previous_names }} {{ strains[0]['release'] }}
      -
    + + + +
    +
    + LATEST UPDATE: New release of data for 2021 v1.6 (2021-03-29) read more
    +
    From eda9f87311df18fb9b97d627e7edb1b74b8a1bd6 Mon Sep 17 00:00:00 2001 From: Gerard Panganiban Date: Wed, 21 Apr 2021 12:20:05 -0500 Subject: [PATCH 080/288] re-adding the footer and fixed CeNDR logo aspect ratio on smaller screens --- base/static/css/custom-styles.css | 69 ++++++++++++------------ base/static/sass/custom-styles.scss | 1 + base/templates/_includes/footer.html | 80 ++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 34 deletions(-) diff --git a/base/static/css/custom-styles.css b/base/static/css/custom-styles.css index c7658e37..927e4413 100644 --- a/base/static/css/custom-styles.css +++ b/base/static/css/custom-styles.css @@ -3,7 +3,7 @@ * Import this file using the following HTML or equivalent: * */ @import url("https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"); -/* line 5, ../../../../../../../../Library/Ruby/Gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 5, ../../../../../../../../../Library/Ruby/Gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, @@ -25,45 +25,45 @@ time, mark, audio, video { vertical-align: baseline; } -/* line 22, ../../../../../../../../Library/Ruby/Gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 22, ../../../../../../../../../Library/Ruby/Gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ html { line-height: 1; } -/* line 24, ../../../../../../../../Library/Ruby/Gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 24, ../../../../../../../../../Library/Ruby/Gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ ol, ul { list-style: none; } -/* line 26, ../../../../../../../../Library/Ruby/Gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 26, ../../../../../../../../../Library/Ruby/Gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ table { border-collapse: collapse; border-spacing: 0; } -/* line 28, ../../../../../../../../Library/Ruby/Gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 28, ../../../../../../../../../Library/Ruby/Gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ caption, th, td { text-align: left; font-weight: normal; vertical-align: middle; } -/* line 30, ../../../../../../../../Library/Ruby/Gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 30, ../../../../../../../../../Library/Ruby/Gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ q, blockquote { quotes: none; } -/* line 103, ../../../../../../../../Library/Ruby/Gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 103, ../../../../../../../../../Library/Ruby/Gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ q:before, q:after, blockquote:before, blockquote:after { content: ""; content: none; } -/* line 32, ../../../../../../../../Library/Ruby/Gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 32, ../../../../../../../../../Library/Ruby/Gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ a img { border: none; } -/* line 116, ../../../../../../../../Library/Ruby/Gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 116, ../../../../../../../../../Library/Ruby/Gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary { display: block; } @@ -838,38 +838,39 @@ header #latest-update-top div .read-more-btn:hover { /* line 814, ../sass/custom-styles.scss */ .home-main-container .home-top-container .left-container .cendr-globe-logo img { width: 160px !important; + max-height: unset !important; } - /* line 819, ../sass/custom-styles.scss */ + /* line 820, ../sass/custom-styles.scss */ .home-main-container .home-top-container .left-container .welcome-message { width: 70%; float: left; margin: 80px 0 0 30px; } - /* line 826, ../sass/custom-styles.scss */ + /* line 827, ../sass/custom-styles.scss */ .home-main-container .home-top-container .youtube-video-homepage { width: 100%; text-align: center; } - /* line 834, ../sass/custom-styles.scss */ + /* line 835, ../sass/custom-styles.scss */ .home-main-container .homepage-major-services .services-container li { width: 38%; margin: 20px 0 40px; padding: 0 30px; } } -/* line 845, ../sass/custom-styles.scss */ +/* line 846, ../sass/custom-styles.scss */ .styled-checkbox { position: absolute; opacity: 0; } -/* line 849, ../sass/custom-styles.scss */ +/* line 850, ../sass/custom-styles.scss */ .styled-checkbox + label { position: relative; cursor: pointer; padding: 0; border-radius: 50%; } -/* line 857, ../sass/custom-styles.scss */ +/* line 858, ../sass/custom-styles.scss */ .styled-checkbox + label:before { content: ''; margin-right: 10px; @@ -881,31 +882,31 @@ header #latest-update-top div .read-more-btn:hover { border-radius: 50%; border: 1px solid #CCC; } -/* line 870, ../sass/custom-styles.scss */ +/* line 871, ../sass/custom-styles.scss */ .styled-checkbox:hover + label:before { background: #FFC400; border-radius: 50%; } -/* line 876, ../sass/custom-styles.scss */ +/* line 877, ../sass/custom-styles.scss */ .styled-checkbox:focus + label:before { box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.12); } -/* line 881, ../sass/custom-styles.scss */ +/* line 882, ../sass/custom-styles.scss */ .styled-checkbox:checked + label:before { background: #FFC400; } -/* line 886, ../sass/custom-styles.scss */ +/* line 887, ../sass/custom-styles.scss */ .styled-checkbox:disabled + label { color: #b8b8b8; cursor: auto; } -/* line 892, ../sass/custom-styles.scss */ +/* line 893, ../sass/custom-styles.scss */ .styled-checkbox:disabled + label:before { box-shadow: none; background: #f8f8f8; border: 1px solid #eee; } -/* line 899, ../sass/custom-styles.scss */ +/* line 900, ../sass/custom-styles.scss */ .styled-checkbox:checked + label:after { content: ''; position: absolute; @@ -919,71 +920,71 @@ header #latest-update-top div .read-more-btn:hover { } @media (max-width: 768px) { - /* line 924, ../sass/custom-styles.scss */ + /* line 925, ../sass/custom-styles.scss */ .home-main-container .home-top-container .left-container .cendr-globe-logo { width: 30%; display: inline-block; } - /* line 929, ../sass/custom-styles.scss */ + /* line 930, ../sass/custom-styles.scss */ .home-main-container .home-top-container .left-container .welcome-message { width: 60%; float: unset; display: inline-block; margin-top: 55px; } - /* line 935, ../sass/custom-styles.scss */ + /* line 936, ../sass/custom-styles.scss */ .home-main-container .home-top-container .left-container .welcome-message .visible-xs { text-align: left !important; font-size: 1.8em !important; } - /* line 944, ../sass/custom-styles.scss */ + /* line 945, ../sass/custom-styles.scss */ .home-main-container .homepage-major-services .services-container { padding: 0 30px; } - /* line 947, ../sass/custom-styles.scss */ + /* line 948, ../sass/custom-styles.scss */ .home-main-container .homepage-major-services .services-container li { width: 100%; margin: 0 0 40px; padding: 0 20%; } - /* line 952, ../sass/custom-styles.scss */ + /* line 953, ../sass/custom-styles.scss */ .home-main-container .homepage-major-services .services-container li h3 { margin-bottom: 0; font-size: 1.4em; } - /* line 961, ../sass/custom-styles.scss */ + /* line 962, ../sass/custom-styles.scss */ .home-main-container .mailing-list-container h4 { width: 100%; margin-bottom: 20px; } - /* line 966, ../sass/custom-styles.scss */ + /* line 967, ../sass/custom-styles.scss */ .home-main-container .mailing-list-container #mc_embed_signup { width: 100%; text-align: center; } - /* line 971, ../sass/custom-styles.scss */ + /* line 972, ../sass/custom-styles.scss */ .home-main-container .mailing-list-container #mc_embed_signup .form-group input[type=submit] { margin-top: -7px; } - /* line 979, ../sass/custom-styles.scss */ + /* line 980, ../sass/custom-styles.scss */ .home-main-container .news-homepage-container ul.news-listing { columns: 1; -webkit-columns: 1; -moz-columns: 1; } - /* line 984, ../sass/custom-styles.scss */ + /* line 985, ../sass/custom-styles.scss */ .home-main-container .news-homepage-container ul.news-listing li { width: 100%; } - /* line 994, ../sass/custom-styles.scss */ + /* line 995, ../sass/custom-styles.scss */ tbody tr td .view-strain-list { columns: 1 !important; -webkit-columns: 1 !important; -moz-columns: 1 !important; margin-top: 25px; } - /* line 1000, ../sass/custom-styles.scss */ + /* line 1001, ../sass/custom-styles.scss */ tbody tr td .view-strain-list li { margin-left: 0 !important; width: 100% !important; diff --git a/base/static/sass/custom-styles.scss b/base/static/sass/custom-styles.scss index 5cc38de5..919c5dd0 100644 --- a/base/static/sass/custom-styles.scss +++ b/base/static/sass/custom-styles.scss @@ -813,6 +813,7 @@ header { .cendr-globe-logo { img { width: 160px !important; + max-height: unset !important; } } diff --git a/base/templates/_includes/footer.html b/base/templates/_includes/footer.html index 624cae33..2446d107 100644 --- a/base/templates/_includes/footer.html +++ b/base/templates/_includes/footer.html @@ -22,6 +22,86 @@ + - -
    diff --git a/base/templates/tools/heritability_results.html b/base/templates/tools/heritability_results.html index b3d9e4fc..ae663b6c 100644 --- a/base/templates/tools/heritability_results.html +++ b/base/templates/tools/heritability_results.html @@ -2,7 +2,6 @@ {% block custom_head %} - @@ -11,7 +10,6 @@ - +{% endblock %} + {% block content %}
    - - - +
    - - - - - - - - + + + + + + @@ -24,15 +41,15 @@ {% for item in items %} {% if item %} - - - + + + {% if item.empty %} - + {% else %} - + {% endif %} - {% endif %} - + {% endfor %} @@ -54,3 +71,29 @@ {% endblock %} + +{% block script %} + + + + +{% endblock %} \ No newline at end of file From e5d195ed9d7f3ac1296ccee140e5e603ae7c1a83 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 3 May 2021 21:56:39 -0500 Subject: [PATCH 140/288] newline at eof --- base/static/css/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/static/css/styles.css b/base/static/css/styles.css index 6ed1cfeb..ff25e5ec 100644 --- a/base/static/css/styles.css +++ b/base/static/css/styles.css @@ -647,4 +647,4 @@ article { .dataTables_length { padding-top: 10px; padding-bottom: 10px; -} \ No newline at end of file +} From 5d657fc311e6187865738493b1e3b96b1f315a00 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 3 May 2021 22:47:41 -0500 Subject: [PATCH 141/288] fix bam tracks loading --- base/templates/browser.html | 91 +++++++++---------------------------- 1 file changed, 22 insertions(+), 69 deletions(-) diff --git a/base/templates/browser.html b/base/templates/browser.html index 5d4e2510..69e8af38 100644 --- a/base/templates/browser.html +++ b/base/templates/browser.html @@ -285,7 +285,7 @@

    Variants

    }, {{ strain }}_bam : { id: "{{ strain.strain }}_bam", - name: "{{ strain.strain }}", + name: "{{ strain.strain }}_bam", url: "//storage.googleapis.com/elegansvariation.org/bam/{{ strain }}.bam", indexURL: "//storage.googleapis.com/elegansvariation.org/bam/{{ strain }}.bam.bai", order: 100, @@ -293,38 +293,6 @@

    Variants

    searchable: false }, {% endfor %} - "LOW": { - name: "LOW", - url: "//storage.googleapis.com/elegansvariation.org/releases/{{ DATASET_RELEASE }}/tracks/{{ DATASET_RELEASE }}.LOW.bed.gz", - order: 3, - color: "#33cc33", - displayMode: "EXPANDED", - height: 35 - }, - "MODERATE": { - name: "MODERATE", - url: "//storage.googleapis.com/elegansvariation.org/releases/{{ DATASET_RELEASE }}/tracks/{{ DATASET_RELEASE }}.MODERATE.bed.gz", - order: 4, - color: "#ffc500", - displayMode: "EXPANDED", - height: 35 - }, - "HIGH": { - name: "HIGH", - url: "//storage.googleapis.com/elegansvariation.org/releases/{{ DATASET_RELEASE }}/tracks/{{ DATASET_RELEASE }}.HIGH.bed.gz", - order: 5, - color: "#ff0000", - displayMode: "EXPANDED", - height: 35 - }, - "MODIFIER": { - name: "MODIFIER", - url: "//storage.googleapis.com/elegansvariation.org/releases/{{ DATASET_RELEASE }}/tracks/{{ DATASET_RELEASE }}.MODIFIER.bed.gz", - order: 5, - color: "#999999", - displayMode: "EXPANDED", - height: 35 - }, "Variants": { name: "Variants", url: "//storage.googleapis.com/elegansvariation.org/releases/{{ DATASET_RELEASE }}/variation/WI.{{ DATASET_RELEASE }}.hard-filter.isotype.vcf.gz", @@ -397,6 +365,24 @@

    Variants

    } }; +function reload_tracks() { + $('.track-select').each(function(i, obj) { + const track_name = $(this).attr("value"); + if ($(this).prop("checked") == true){ + if (!tracks.includes(track_name)) { + igv.getBrowser().loadTrack(trackset[track_name]); + tracks.push(track_name); + } + } else { + igv.getBrowser().removeTrackByName(track_name); + const i = tracks.indexOf(track_name); + if (i !== -1) { + tracks.splice(i, 1); + } + } + }); +} + $(document).ready(function () { var div = $("#browser")[0], options = { @@ -412,10 +398,7 @@

    Variants

    fastaURL: "//storage.googleapis.com/elegansvariation.org/browser_tracks/c_elegans.PRJNA13758.WS245.genomic.fa", }, locus: "{{ region }}", - tracks: [ - trackset["Genes"], - trackset["Transcripts"] - ] + tracks: [] }; var browser = igv.createBrowser(div, options) .then(function(browser) { @@ -428,21 +411,7 @@

    Variants

    // Detect track changes $(".track-select").on("change", function() { - $('.track-select').each(function(i, obj) { - const track_name = $(this).attr("value"); - if ($(this).prop("checked") == true){ - if (!tracks.includes(track_name)) { - igv.getBrowser().loadTrack(trackset[track_name]); - tracks.push(track_name); - } - } else { - igv.getBrowser().removeTrackByName(track_name); - const i = tracks.indexOf(track_name); - if (i !== -1) { - tracks.splice(i, 1); - } - } - }); + reload_tracks(); }); }); @@ -522,16 +491,7 @@

    Variants

    end = region.split(":")[1].split("-")[1].replace(/,/g, ""); release = {{ DATASET_RELEASE }}; - sample_tracks = [] - $(".sample-track:checked").each(function() { sample_tracks.push(this.value) }) - - tracks = []; - $('.track-select').each(function(i, obj) { - if ($(this).prop("checked") == true) { - tracks.push($(this).attr("value")); - } - }); - + reload_tracks(); data = {'chrom': chrom, 'start': parseInt(start), 'end': parseInt(end), @@ -662,18 +622,11 @@
    Other
    typingTimer = setTimeout(process_gene_search, doneTypingInterval); }) -$(".checkbox").on("click", function() { - setTimeout(refresh_variants, 200); -}) $(".igvNavigationSearchInput").on("input paste", function() { refresh_variants(); }); -$(".track-select").on("change", function() { - setTimeout(refresh_variants, 200); -}); - $(".igvNavigationSearchInput").keypress(function(e) { if(e.which == 13) { setTimeout(refresh_variants, 200); From 599212787dcc8f6e32f2a16c36166db308a72b7e Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 3 May 2021 22:50:09 -0500 Subject: [PATCH 142/288] remove variants --- base/templates/browser.html | 8 -------- 1 file changed, 8 deletions(-) diff --git a/base/templates/browser.html b/base/templates/browser.html index 69e8af38..a702c914 100644 --- a/base/templates/browser.html +++ b/base/templates/browser.html @@ -190,16 +190,8 @@
    Isotype (reference strain) -
    -
    -

    Variants

    -
    {# /col-lg-12 #} -
    {# /row #} -
    -

    A Maximum of 1000 variants or 100kb will be queried and returned. Heterozygous calls are likely errors. When rows are yellow, it indicates the entire variant failed QC.

    - Hovering over a failing genotype will list the filter applied. Genotypes are shown as follows:
    {# /col-md-10 #}
    From 4c09231c9d47768aa7cdcbdf2f28009cb2d2bb58 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 3 May 2021 22:51:24 -0500 Subject: [PATCH 143/288] btn --- base/templates/browser.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/templates/browser.html b/base/templates/browser.html index a702c914..174a22c0 100644 --- a/base/templates/browser.html +++ b/base/templates/browser.html @@ -193,7 +193,7 @@
    Isotype (reference strain)
    {# /col-md-10 #} -
    +
    {# /col-md-10 #}
    {# /row #} From f3587285f09710bd0193391f068283574f5c30b1 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 3 May 2021 23:04:20 -0500 Subject: [PATCH 144/288] sample tracks --- base/templates/browser.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/base/templates/browser.html b/base/templates/browser.html index 174a22c0..760f303f 100644 --- a/base/templates/browser.html +++ b/base/templates/browser.html @@ -482,8 +482,11 @@
    Isotype (reference strain) Date: Tue, 4 May 2021 00:07:07 -0500 Subject: [PATCH 145/288] updates to igv browser --- base/templates/browser.html | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/base/templates/browser.html b/base/templates/browser.html index 760f303f..e444cb98 100644 --- a/base/templates/browser.html +++ b/base/templates/browser.html @@ -362,14 +362,15 @@
    Isotype (reference strain) Isotype (reference strain) Isotype (reference strain) Isotype (reference strain) Other
    $(".igvNavigationSearchInput").on("input paste", function() { - refresh_variants(); + setTimeout(refresh_variants, 100); }); $(".igvNavigationSearchInput").keypress(function(e) { if(e.which == 13) { - setTimeout(refresh_variants, 200); + setTimeout(refresh_variants, 100); } }); // Initial load +setTimeout(reload_tracks, 500); setTimeout(refresh_variants, 1000); }); From 2a7521eb8e51e7dd1a25fd1de6008ac1889901e7 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Tue, 4 May 2021 15:12:08 -0500 Subject: [PATCH 146/288] improve datatables --- base/static/css/styles.css | 4 ++ base/templates/macros.html | 32 ++++++++- base/templates/tools/h2_result_list.html | 76 +++++++++++++++++++--- base/templates/tools/ip_result_list.html | 21 ++++-- base/views/tools/heritability.py | 2 + cloud_functions/heritability_run/invoke.go | 1 + 6 files changed, 122 insertions(+), 14 deletions(-) diff --git a/base/static/css/styles.css b/base/static/css/styles.css index ff25e5ec..aa4246d9 100644 --- a/base/static/css/styles.css +++ b/base/static/css/styles.css @@ -648,3 +648,7 @@ article { padding-top: 10px; padding-bottom: 10px; } + +.dataTables-menu-top { + padding-bottom: 10px; +} \ No newline at end of file diff --git a/base/templates/macros.html b/base/templates/macros.html index 13ecc282..43b6ce94 100644 --- a/base/templates/macros.html +++ b/base/templates/macros.html @@ -54,4 +54,34 @@
    Site Strain 1 Strain 2 Empty Status Date Site Strain 1 Strain 2 Empty Status Date
    {{ item.site }} {{ item.strain1 }} {{ item.strain2 }} {{ item.site }} {{ item.strain1 }} {{ item.strain2 }} + {% if item.status == 'COMPLETE' %} {{ item.status }} @@ -42,7 +59,7 @@ {% endif %} {{ item.created_on|date_format }} {{ item.created_on|date_format }}
    -{% endmacro %} \ No newline at end of file +{% endmacro %} + + +{% macro render_dataTable_top_menu(pages=True, filter=True) %} + +
    +
    + {% if pages %} +
    +
    + + +
    +
    + {% endif %} +
    {# /col-md-2 #} +
    +
    + {% if filter %} + + {% endif %} +
    {# /col-md-2 #} +
    {# /row #} + + +{% endmacro %} diff --git a/base/templates/tools/h2_result_list.html b/base/templates/tools/h2_result_list.html index 31a72f07..d2acc22b 100644 --- a/base/templates/tools/h2_result_list.html +++ b/base/templates/tools/h2_result_list.html @@ -1,17 +1,40 @@ {% extends "_layouts/default.html" %} +{% block custom_head %} + + +{% endblock %} + +{% block style %} + +{% endblock %} + {% block content %} +{% from "macros.html" import render_dataTable_top_menu %} +{{ render_dataTable_top_menu() }} +
    - - - +
    - - - + + + + @@ -19,8 +42,9 @@ {% for item in items %} {% if item %} - - + + {% endif %} - + {% endfor %} @@ -42,3 +66,37 @@ {% endblock %} + +{% block script %} + + + + +{% endblock %} \ No newline at end of file diff --git a/base/templates/tools/ip_result_list.html b/base/templates/tools/ip_result_list.html index b4fce769..4b2a02f1 100644 --- a/base/templates/tools/ip_result_list.html +++ b/base/templates/tools/ip_result_list.html @@ -23,6 +23,9 @@ {% block content %} +{% from "macros.html" import render_dataTable_top_menu %} +{{ render_dataTable_top_menu() }} +
    Label Status Created Label Trait Status Date
    {{ item.label }} + {{ item.label }} {{ item.trait }} {% if item.status == 'COMPLETE' %} {{ item.status }} @@ -30,7 +54,7 @@ {% endif %} {{ item.created_on|date_format }} {{ item.created_on|date_format }}
    @@ -59,7 +62,7 @@ {% endif %} {% endif %} - + {% endfor %} @@ -78,7 +81,7 @@ $(document).ready(function(){ - $('#ip-table').DataTable( { + dTable = $('#ip-table').DataTable( { paging: true, pageLength: 25, aaSorting: [ [5,'desc'] ], @@ -89,8 +92,18 @@ null, null, null - ] - } ); + ], + dom:"tipr" + }); + + $('#filter').keyup(function(){ + dTable.search($(this).val()).draw(); + }); + + $('#page-length').change(function(){ + dTable.page.len($(this).val()).draw(); + }); + }); diff --git a/base/views/tools/heritability.py b/base/views/tools/heritability.py index 7f335316..e3f6a62e 100644 --- a/base/views/tools/heritability.py +++ b/base/views/tools/heritability.py @@ -92,6 +92,7 @@ def submit_h2(): data = [x for x in data[1:] if x[0] is not None] header = ["AssayNumber", "Strain", "TraitName", "Replicate", "Value"] data = pd.DataFrame(data, columns=header) + trait = data.values[0][2] data = data.to_csv(index=False, sep="\t") # Generate an ID for the data based on its hash @@ -105,6 +106,7 @@ def submit_h2(): hr.data_hash = data_hash hr.username = user.name hr.status = 'NEW' + hr.trait = trait hr.save() # Check whether analysis has previously been run and if so - skip diff --git a/cloud_functions/heritability_run/invoke.go b/cloud_functions/heritability_run/invoke.go index a9a8823f..37ad0351 100644 --- a/cloud_functions/heritability_run/invoke.go +++ b/cloud_functions/heritability_run/invoke.go @@ -41,6 +41,7 @@ type dsInfo struct { type dsEntry struct { Username string `datastore:"username"` Label string `datastore:"label"` + Trait string `datastore:"trait"` Data_hash string `datastore:"data_hash"` Status string `datastore:"status"` Status_msg string `datastore:"status_msg,noindex"` From 00c35f707f8ae192194c7d0d55490ef1d01df9b3 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Tue, 4 May 2021 15:14:27 -0500 Subject: [PATCH 147/288] eof newline --- base/static/css/styles.css | 2 +- base/templates/tools/h2_result_list.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/base/static/css/styles.css b/base/static/css/styles.css index aa4246d9..b4a91515 100644 --- a/base/static/css/styles.css +++ b/base/static/css/styles.css @@ -651,4 +651,4 @@ article { .dataTables-menu-top { padding-bottom: 10px; -} \ No newline at end of file +} diff --git a/base/templates/tools/h2_result_list.html b/base/templates/tools/h2_result_list.html index d2acc22b..0544dd53 100644 --- a/base/templates/tools/h2_result_list.html +++ b/base/templates/tools/h2_result_list.html @@ -99,4 +99,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} From fe794d9ed876249234d1887988634bcc5429907d Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Wed, 5 May 2021 00:01:38 -0500 Subject: [PATCH 148/288] initial commit --- .gcloudignore | 1 + H2_script.R | 476 ++++++++++++++++++++++++++++++++++++++++++++++++ cloudbuild.yaml | 6 + go.mod | 8 + go.sum | 447 +++++++++++++++++++++++++++++++++++++++++++++ main.go | 263 ++++++++++++++++++++++++++ 6 files changed, 1201 insertions(+) create mode 100644 .gcloudignore create mode 100644 H2_script.R create mode 100644 cloudbuild.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.gcloudignore b/.gcloudignore new file mode 100644 index 00000000..13cd1fa7 --- /dev/null +++ b/.gcloudignore @@ -0,0 +1 @@ +server \ No newline at end of file diff --git a/H2_script.R b/H2_script.R new file mode 100644 index 00000000..9d6b2bc3 --- /dev/null +++ b/H2_script.R @@ -0,0 +1,476 @@ +#!/usr/bin/env Rscript H2_script.R +library(boot) +library(lme4) +library(dplyr) +library(futile.logger) +library(tidyr) +library(glue) +library(data.table) + +######################## +### define functions ### +######################## +# Heritability +# data is data frame that contains strain and Value column +# indicies are used by the boot function to sample from the 'data' data.frame +H2.test.boot <- function(data, indicies){ + + d <- data[indicies,] + + pheno <- as.data.frame(dplyr::select(d, Value))[,1] + + Strain <- as.factor(d$Strain) + + reffMod <- lme4::lmer(pheno ~ 1 + (1|Strain)) + + Variances <- as.data.frame(lme4::VarCorr(reffMod, comp = "Variance")) + + Vg <- Variances$vcov[1] + Ve <- Variances$vcov[2] + H2 <- Vg/(Vg+Ve) + + # errors <- sqrt(diag(lme4::VarCorr(reffMod, comp = "Variance")$strain)) + + return(H2) +} + +# data is data frame that contains strain and Value column +H2.test <- function(data){ + + pheno <- as.data.frame(dplyr::select(data, Value))[,1] + Strain <- as.factor(data$Strain) + + reffMod <- lme4::lmer(pheno ~ 1 + (1|Strain)) + + Variances <- as.data.frame(lme4::VarCorr(reffMod, comp = "Variance")) + + Vg <- Variances$vcov[1] + Ve <- Variances$vcov[2] + H2 <- Vg/(Vg+Ve) + + # errors <- sqrt(diag(lme4::VarCorr(reffMod, comp = "Variance")$strain)) + + return(H2) +} + +# df is data frame that contains strain and Value column +H2.calc <- function(data, boot = TRUE, type = "broad", reps = 10000){ + df <- dplyr::select(data, Strain, Value) + + flog.info("Running bootstrapping") + if(boot == TRUE){ + # bootstrapping with 10000 replications + # can reduce value to save time (500 is reasonable most of the time). + # if you Error in bca.ci(boot.out, conf, index[1L], L = L, t = t.o, t0 = t0.o, : estimated adjustment 'a' is NA, then you need to increase R value. + if(type == "broad") { + results <- boot(data = df, statistic = H2.test.boot, R = reps) + } else { + results <- boot(data = df, statistic = narrowh2.boot, R = reps) + } + + # get 95% confidence interval + ci <- boot.ci(results, type="bca") + + H2_errors <- data.frame(H2 = ci$t0, + ci_l = ci$bca[4], + ci_r = ci$bca[5]) + + return(H2_errors) + + } else { + if(type == "broad") { + H2 <- data.frame(H2 = H2.test(data = df), ci_l = NA, ci_r = NA) + } else { + H2 <- data.frame(H2 = narrowh2(df_h = df), ci_l = NA, ci_r = NA) + } + return(H2) + } + +} + +# extract strain, isotype dataframe from database +generate_isotype_lookup <- function(species = "ce") { + + if ( species == "ce" ) { + isotype_lookup <- strain_data %>% + dplyr::mutate(strain_names = ifelse(!is.na( previous_names ), + paste( strain, previous_names, sep = "|" ), + strain )) %>% + tidyr::separate_rows(strain_names, sep = "\\|") %>% + dplyr::select(strain, previous_name = strain_names, isotype) %>% + dplyr::distinct() + } + + return( isotype_lookup ) +} + +# Resolve strain names to isotypes +resolve_isotypes <- function(...) { + + isotype_lookup = generate_isotype_lookup() + strains <- unlist(list(...)) + + purrr::map_chr(strains, function(x) { + isotype <- isotype_lookup %>% + dplyr::filter( + (x == strain) | + (x == isotype) | + (x == previous_name) + ) %>% + dplyr::pull(isotype) %>% + unique() + + if (length(isotype) == 0) { + message(glue::glue("{x} is not a known strain. Isotype set to NA; Please check CeNDR")) + } else if (length(isotype) > 1) { + message(glue::glue("{x} resolves to multiple isotypes. Isotype set to NA; Please check CeNDR")) + } + if (length(isotype) != 1) { + isotype <- NA + } + isotype + }) + +} + +# prune outliers +# data = strain, trait, phenotype +BAMF_prune <- function(data, remove_outliers = TRUE ){ + + categorize1 <- function(data) { + with(data, + ( (sixhs >= 1 & ( (s6h + s5h + s4h ) / numst) <= .05)) + | ( (sixls >= 1 & ( (s6l + s5l + s4l) / numst) <= .05)) + ) + } + # If the 5 innermost bins are discontinuous by more than a 1 bin gap, the + # observation is in the fifth bin (between 7 and 10x IQR outside the + # distribution), and the four outermost bins make up less than 5% of the + # population, mark the observation an outlier + categorize2 <- function(data) { + with(data, + ( (fivehs >= 1 & ( (s6h + s5h + s4h + s3h) / numst) <= .05)) + | ( (fivels >= 1 & ( (s6l + s5l + s4l + s3l) / numst) <= .05)) + ) + } + # If the 4 innermost bins are discontinuous by more than a 1 bin gap, the + # observation is in the fourth bin (between 5 and 7x IQR outside the + # distribution), and the four outermost bins make up less than 5% of the + # population, mark the observation an outlier + categorize3 <- function(data) { + with(data, + ( (fourhs >= 1 & (s5h + s4h + s3h + s2h) / numst <= .05)) + | ( (fourls >= 1 & (s5l + s4l + s3l + s2l) / numst <= .05)) + ) + } + napheno <- data[is.na(data$phenotype), ] %>% + dplyr::mutate(bamfoutlier1 = NA, bamfoutlier2 = NA, bamfoutlier3 = NA) + + datawithoutliers <- data %>% + # Filter out all of the wash and/or empty wells + dplyr::filter(!is.na(strain)) %>% + # Group by trait, the, calculate the first and third + # quartiles for each of the traits + dplyr::group_by(trait) %>% + dplyr::summarise(iqr = IQR(phenotype, na.rm = TRUE), + q1 = quantile(phenotype, probs = .25, na.rm = TRUE), + q3 = quantile(phenotype, probs = .75, + na.rm = TRUE)) %>% + # Add a column for the boundaries of each of the bins + dplyr::mutate(cut1h = q3 + (iqr * 2), + cut1l = q1 - (iqr * 2), + cut2h = q3 + (iqr * 3), + cut2l = q1 - (iqr * 3), + cut3h = q3 + (iqr * 4), + cut3l = q1 - (iqr * 4), + cut4h = q3 + (iqr * 5), + cut4l = q1 - (iqr * 5), + cut5l = q1 - (iqr * 7), + cut5h = q3 + (iqr * 7), + cut6l = q1 - (iqr * 10), + cut6h = q3 + (iqr * 10)) %>% + # Join the bin boundaries back to the original data frame + dplyr::left_join(data, ., by = c("trait")) %>% + dplyr::ungroup() %>% + dplyr::rowwise() %>% + # Add columns tallying the total number of points in each of the bins + dplyr::mutate(onehs = ifelse(cut2h > phenotype & phenotype >= cut1h, + 1, 0), + onels = ifelse(cut2l < phenotype & phenotype <= cut1l, + 1, 0), + twohs = ifelse(cut3h > phenotype & phenotype >= cut2h, + 1, 0), + twols = ifelse(cut3l < phenotype & phenotype <= cut2l, + 1, 0), + threehs = ifelse(cut4h > phenotype & phenotype >= cut3h, + 1, 0), + threels = ifelse(cut4l < phenotype & phenotype <= cut3l, + 1, 0), + fourhs = ifelse(cut5h > phenotype & phenotype >= cut4h, + 1, 0), + fourls = ifelse(cut5l < phenotype & phenotype <= cut4l, + 1, 0), + fivehs = ifelse(cut6h > phenotype & phenotype >= cut5h, + 1, 0), + fivels = ifelse(cut6l < phenotype & phenotype <= cut5l, + 1, 0), + sixhs = ifelse(phenotype >= cut6h, 1, 0), + sixls = ifelse(phenotype <= cut6l, 1, 0)) %>% + # Group on condition and trait, then sum the total number of data points + # in each of the IQR multiple bins + dplyr::group_by(trait) %>% + dplyr::mutate(s1h = sum(onehs, na.rm = TRUE), + s2h = sum(twohs, na.rm = TRUE), + s3h = sum(threehs, na.rm = TRUE), + s4h = sum(fourhs, na.rm = TRUE), + s5h = sum(fivehs, na.rm = TRUE), + s1l = sum(onels, na.rm = TRUE), + s2l = sum(twols, na.rm = TRUE), + s3l = sum(threels, na.rm = TRUE), + s4l = sum(fourls, na.rm = TRUE), + s5l = sum(fivels, na.rm = TRUE), + s6h = sum(sixhs, na.rm = TRUE), + s6l = sum(sixls, na.rm = TRUE)) %>% + # Group on condition and trait, then check to see if the number of + # points in each bin is more than 5% of the total number of data points + dplyr::group_by(trait) %>% + dplyr::mutate(p1h = ifelse(sum(onehs, na.rm = TRUE) / n() >= .05, 1, 0), + p2h = ifelse(sum(twohs, na.rm = TRUE) / n() >= .05, 1, 0), + p3h = ifelse(sum(threehs, na.rm = TRUE) / n() >= .05, 1, 0), + p4h = ifelse(sum(fourhs, na.rm = TRUE) / n() >= .05 ,1, 0), + p5h = ifelse(sum(fivehs, na.rm = TRUE) / n() >= .05 ,1 ,0), + p6h = ifelse(sum(sixhs, na.rm = TRUE) / n() >= .05 ,1, 0), + p1l = ifelse(sum(onels, na.rm = TRUE) / n() >= .05 ,1, 0), + p2l = ifelse(sum(twols, na.rm = TRUE) / n() >= .05 ,1, 0), + p3l = ifelse(sum(threels, na.rm = TRUE) / n() >= .05 ,1,0), + p4l = ifelse(sum(fourls, na.rm = TRUE) / n() >= .05 , 1, 0), + p5l = ifelse(sum(fivels, na.rm = TRUE) / n() >= .05, 1, 0), + p6l = ifelse(sum(sixls, + na.rm = TRUE) / n() >= .05, 1, 0)) %>% + # Count the number of observations in each condition/trait combination + dplyr::mutate(numst = n()) %>% + # Group on trait, then filter out NAs in any of the added + # columns + dplyr::group_by(trait) %>% + dplyr::filter(!is.na(trait), !is.na(phenotype), !is.na(iqr), !is.na(q1), + !is.na(q3), !is.na(cut1h), !is.na(cut1l), !is.na(cut2h), + !is.na(cut2l), !is.na(cut3h), !is.na(cut3l), + !is.na(cut4h), !is.na(cut4l), !is.na(cut5l), + !is.na(cut5h), !is.na(cut6l), !is.na(cut6h), + !is.na(onehs), !is.na(onels), !is.na(twohs), + !is.na(twols), !is.na(threehs), !is.na(threels), + !is.na(fourhs), !is.na(fourls), !is.na(fivehs), + !is.na(fivels), !is.na(sixhs), !is.na(sixls), + !is.na(s1h), !is.na(s2h), !is.na(s3h), !is.na(s4h), + !is.na(s5h), !is.na(s1l), !is.na(s2l), !is.na(s3l), + !is.na(s4l), !is.na(s5l), !is.na(s6h), !is.na(s6l), + !is.na(p1h), !is.na(p2h), !is.na(p3h), !is.na(p4h), + !is.na(p5h), !is.na(p6h), !is.na(p1l), !is.na(p2l), + !is.na(p3l), !is.na(p4l), !is.na(p5l), !is.na(p6l), + !is.na(numst)) %>% + # Add three columns stating whether the observation is an outlier + # based the three outlier detection functions below + dplyr::ungroup() %>% + dplyr::mutate(cuts = categorize1(.), + cuts1 = categorize2(.), + cuts2 = categorize3(.)) + + if ( remove_outliers == T){ + outliers_removed <- datawithoutliers %>% + dplyr::rename(bamfoutlier1 = cuts, + bamfoutlier2 = cuts1, + bamfoutlier3 = cuts2) %>% + dplyr::filter(!bamfoutlier1 & !bamfoutlier2 & !bamfoutlier3) %>% + dplyr::select(trait, strain, phenotype) + + return(outliers_removed) + } else { + with_outliers <- datawithoutliers %>% + dplyr::rename(bamfoutlier1 = cuts, + bamfoutlier2 = cuts1, + bamfoutlier3 = cuts2) %>% + dplyr::select(trait, strain, phenotype, bamfoutlier1, bamfoutlier2, bamfoutlier3) %>% + dplyr::mutate(outlier = ifelse( bamfoutlier1 | bamfoutlier2 | bamfoutlier3, TRUE, FALSE)) %>% + dplyr::select(strain, trait, phenotype, outlier) + + return(with_outliers) + } +} + +# Process phenotypes for mapping and heritability +process_phenotypes <- function(df, + summarize_replicates = "mean", + prune_method = "BAMF", + remove_outliers = TRUE, + threshold = 2){ + + if ( sum(grepl(colnames(df)[1], "Strain", ignore.case = T)) == 0 ) { + message(glue::glue("Check input data format, strain should be the first column.")) + } + + # ~ ~ ~ # resolve strain isotypes # ~ ~ ~ # + # get strain isotypes + strain_isotypes_db <- generate_isotype_lookup() + # identify strains that were phenotyped, but are not part of an isotype + non_isotype_strains <- dplyr::filter(df, + !(strain %in% strain_isotypes_db$strain), + !(strain %in% strain_isotypes_db$isotype)) + # remove any strains identified to not fall into an isotype + if ( nrow(non_isotype_strains) > 0 ) { + + strains_to_remove <- unique(non_isotype_strains$strain) + + message(glue::glue("Removing strain(s) {strains_to_remove} because they do not fall into a defined isotype.")) + + df_non_isotypes_removed <- dplyr::filter( df, !( strain %in% strains_to_remove) ) + } else { + df_non_isotypes_removed <- df + } + + # ~ ~ ~ ## ~ ~ ~ ## ~ ~ ~ ## ~ ~ ~ # Resolve Isotypes # ~ ~ ~ ## ~ ~ ~ ## ~ ~ ~ ## ~ ~ ~ # + df_isotypes_resolved <- df_non_isotypes_removed %>% + dplyr::group_by(strain) %>% + dplyr::mutate(isotype = resolve_isotypes(strain)) %>% + dplyr::ungroup() %>% + tidyr::gather(trait, phenotype, -strain, -isotype) %>% + dplyr::filter(!is.na(phenotype)) + + # deal with multiple strains per isotype group + test <- df_isotypes_resolved %>% + dplyr::group_by(isotype) %>% + dplyr::mutate(num = length(unique(strain))) %>% + dplyr::mutate(ref_strain = strain == isotype) + no_issues <- test %>% + dplyr::filter(num == 1 & ref_strain == T) + issues <- test %>% + dplyr::filter(num > 1 | ref_strain == F) + + fixed_issues <- no_issues %>% + dplyr::select(-num, -ref_strain) + + # go through each isotype issue and resolve it + for(i in unique(issues$isotype)) { + df <- issues %>% + dplyr::filter(isotype == i) + + # if only one strain is phenotyped, just rename strain to isotype ref strain and flag + if(nrow(df) == 1) { + fix <- df %>% + dplyr::mutate(strain = isotype) + message(glue::glue("Non-isotype reference strain {df$strain[1]} renamed to isotype {i}.")) + } else { + # remove non-isotype strains + fix <- df %>% + dplyr::filter(ref_strain) %>% + dplyr::select(-ref_strain, -num) + + # warn the user + if(sum(df$ref_strain) > 0) { + message(glue::glue("Non-isotype reference strain(s) {paste(df %>% dplyr::filter(!ref_strain) %>% dplyr::pull(strain), collapse = ', ')} from isotype group {i} removed.")) + } + else { + message(glue::glue("Non-isotype reference strain(s) {paste(df %>% dplyr::filter(!ref_strain) %>% dplyr::pull(strain), collapse = ', ')} from isotype group {i} removed. + To include this isotype in the analysis, you can (1) phenotype {i} or (2) evaluate the similarity of these strains and choose one representative for the group.")) + } + } + # add to data + fixed_issues <- rbind(fixed_issues, fix) + } + + # ~ ~ ~ ## ~ ~ ~ ## ~ ~ ~ ## ~ ~ ~ # Summarize Replicate Data # ~ ~ ~ ## ~ ~ ~ ## ~ ~ ~ ## ~ ~ ~ # + df_replicates_summarized <- fixed_issues %>% + dplyr::group_by(isotype, trait) %>% { + if (summarize_replicates == "mean") dplyr::summarise(., phenotype = mean( phenotype, na.rm = T ) ) + else if (summarize_replicates == "median") dplyr::summarise(., phenotype = median( phenotype, na.rm = T ) ) + else if (summarize_replicates == "none") dplyr::mutate(., phenotype = phenotype) + else message(glue::glue("Please choose mean, median, or none as options for summarizeing replicate data.")) } %>% + dplyr::select(strain = isotype, trait, phenotype) %>% + dplyr::ungroup() + + # ~ ~ ~ ## ~ ~ ~ ## ~ ~ ~ ## ~ ~ ~ # Outlier Functions # ~ ~ ~ ## ~ ~ ~ ## ~ ~ ~ ## ~ ~ ~ # + is_out_tukey <- function(x, k = threshold, na.rm = TRUE) { + quar <- quantile(x, probs = c(0.25, 0.75), na.rm = na.rm) + iqr <- diff(quar) + + ( !(quar[1] - k * iqr <= x) ) | ( !(x <= quar[2] + k * iqr) ) + } + is_out_z <- function(x, thres = threshold, na.rm = TRUE) { + ( !abs(x - mean(x, na.rm = na.rm)) <= thres * sd(x, na.rm = na.rm) ) + } + is_out_mad <- function(x, thres = threshold, na.rm = TRUE) { + ( !abs(x - median(x, na.rm = na.rm)) <= thres * mad(x, na.rm = na.rm) ) + } + + # ~ ~ ~ ## ~ ~ ~ ## ~ ~ ~ ## ~ ~ ~ # Perform outlier removal # ~ ~ ~ ## ~ ~ ~ ## ~ ~ ~ ## ~ ~ ~ # + if ( prune_method == "BAMF" ) { + df_outliers <- BAMF_prune(df_replicates_summarized, remove_outliers = FALSE) + } else { + df_outliers <- dplyr::ungroup(df_replicates_summarized) %>% + dplyr::group_by(trait) %>% { + if (prune_method == "MAD") dplyr::transmute_if(., is.numeric, dplyr::funs( outlier = is_out_mad ) ) + else if (prune_method == "TUKEY") dplyr::transmute_if(., is.numeric, dplyr::funs( outlier = is_out_tukey ) ) + else if (prune_method == "Z") dplyr::transmute_if(., is.numeric, dplyr::funs( outlier = is_out_z ) ) + else message(glue::glue("Please choose BAMF, MAD, TUKEY, or Z as options for summarizeing replicate data.")) } %>% + dplyr::ungroup() %>% + dplyr::bind_cols(., dplyr::ungroup(df_replicates_summarized)) %>% + dplyr::select(strain, trait, phenotype, outlier) + } + + if (remove_outliers == TRUE ) { + processed_phenotypes_output <- df_outliers %>% + dplyr::filter( !outlier ) %>% + dplyr::select( -outlier ) + # tidyr::spread( trait, phenotype) + } else { + processed_phenotypes_output <- df_outliers + # tidyr::spread( trait, phenotype) + } + + return(processed_phenotypes_output) +} + + + +# RUN +args = commandArgs(trailingOnly=TRUE) + +# update to include geno matrix +usage_cmd = "USAGE: Rscript H2_script.R input_file output_file" + +if (length(args) < 3){ + print(usage_cmd) +} + +### ARGS ### +# 1 - input_file +# 2 - output_file +# 3 - hash_file +# 4 - version +# 5 - strain_data + + +# Read in data +data <- data.table::fread(args[1]) %>% + tidyr::spread(TraitName, Value) %>% + dplyr::select(-Replicate, -AssayNumber) %>% + dplyr::rename(strain = Strain) +output_fname = args[2] +heritability_version = args[4] +hash <- readLines(args[3]) +strain_data <- data.table::fread(args[5]) + +# process phenotypes +processed_data <- process_phenotypes(data, summarize_replicates = "none") %>% + dplyr::rename(Strain = strain, Value = phenotype, TraitName = trait) + +# Run H2 calculation +result <- H2.calc(processed_data, boot = T, type = "broad") + +# add timepoint data +result$hash <- hash +result$trait_name <- data$TraitName[1] +result$date <- Sys.time() +result$heritability_version <- heritability_version + +# Write the result +data.table::fwrite(result, output_fname, sep = '\t') diff --git a/cloudbuild.yaml b/cloudbuild.yaml new file mode 100644 index 00000000..4d59170f --- /dev/null +++ b/cloudbuild.yaml @@ -0,0 +1,6 @@ +steps: +- name: 'gcr.io/kaniko-project/executor:latest' + args: + - --destination=gcr.io/andersen-lab/h2-1 + - --cache=true + - --cache-ttl=12h diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..c6fdfeef --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/andersenlab/h2calc + +go 1.13 + +require ( + cloud.google.com/go/datastore v1.4.0 + cloud.google.com/go/storage v1.14.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..c237106e --- /dev/null +++ b/go.sum @@ -0,0 +1,447 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0 h1:XgtDnVJRCPEUG21gjFiRPz4zI1Mjg16R+NYQjfmU4XY= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/datastore v1.4.0 h1:CFDJm15RpYXeEblQ0TMDUrYtqmBmbAWTy536nA8JIc8= +cloud.google.com/go/datastore v1.4.0/go.mod h1:d18825/a9bICdAIJy2EkHs9joU4RlIZ1t6l8WDdbdY0= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0 h1:6RRlFMv1omScs6iq2hfE3IvgE+l6RfJPampq8UZc5TU= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0 h1:wCKgOCHuUEVfsaQLpPSJb7VdYCdTVZQAuOdYm1yc/60= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1 h1:Kvvh58BN8Y9/lBi7hTekvtMpm07eUZ0ck5pRHpsMWrY= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210113160501-8b1d76fa0423/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99 h1:5vD4XjIc0X5+kHZjx4UecYdjA6mJo+XXNoaW0EjU5Os= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073 h1:8qxJSnu+7dRq6upnbntrmriWByIakBuct5OM/MdQC1M= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0 h1:uWrpz12dpVPn7cojP82mk02XDgTJLDPc2KbVTxrWb4A= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210113195801-ae06605f4595/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705 h1:PYBmACG+YEv8uQPW0r1kJj8tR+gkF0UWq7iFdUezwEw= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.34.1/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/main.go b/main.go new file mode 100644 index 00000000..37ad0351 --- /dev/null +++ b/main.go @@ -0,0 +1,263 @@ +package main + +// VERSION v2 + +import ( + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + "time" + + "cloud.google.com/go/datastore" + "cloud.google.com/go/storage" +) + +// change to v2 20210121 +const heritabilityVersion = "v2" +const datasetName = "data.tsv" +const resultName = "result.tsv" +const bucketName = "elegansvariation.org" +const projectID = "andersen-lab" + +type Payload struct { + Hash string + Ds_id string + Ds_kind string +} + +type dsInfo struct { + Kind string + Id string + Msg string + Data_hash string +} + +type dsEntry struct { + Username string `datastore:"username"` + Label string `datastore:"label"` + Trait string `datastore:"trait"` + Data_hash string `datastore:"data_hash"` + Status string `datastore:"status"` + Status_msg string `datastore:"status_msg,noindex"` + Modified_on time.Time `datastore:"modified_on"` + Created_on time.Time `datastore:"created_on"` + K *datastore.Key `datastore:"__key__"` +} + +func check(e error, i dsInfo) { + if e != nil { + msg := e.Error() + "\n" + i.Msg + setDatastoreStatus(i.Kind, i.Id, "ERROR", msg) + panic(e) + } +} + +func setDatastoreStatus(kind string, id string, status string, msg string) { + ctx := context.Background() + dsClient, err := datastore.NewClient(ctx, projectID) + if err != nil { + log.Fatal(err) + } + defer dsClient.Close() + + k := datastore.NameKey(kind, id, nil) + e := new(dsEntry) + if err := dsClient.Get(ctx, k, e); err != nil { + log.Fatal(err) + } + + e.Status = status + e.Status_msg = msg + + if _, err := dsClient.Put(ctx, k, e); err != nil { + log.Fatal(err) + } + + fmt.Printf("Updated status: %q\n", e.Status) + fmt.Printf("Updated status msg: %q\n", e.Status_msg) + +} + +func downloadFile(w io.Writer, object string) ([]byte, error) { + // object := "object-name" + ctx := context.Background() + client, err := storage.NewClient(ctx) + if err != nil { + return nil, fmt.Errorf("storage.NewClient: %v", err) + } + defer client.Close() + + ctx, cancel := context.WithTimeout(ctx, time.Second*50) + defer cancel() + + rc, err := client.Bucket(bucketName).Object(object).NewReader(ctx) + if err != nil { + return nil, fmt.Errorf("Object(%q).NewReader: %v", object, err) + } + defer rc.Close() + + data, err := ioutil.ReadAll(rc) + if err != nil { + return nil, fmt.Errorf("ioutil.ReadAll: %v", err) + } + fmt.Fprintf(w, "Blob %v downloaded.\n", object) + return data, nil +} + +func copyBlob(bucket string, source string, blob string, i dsInfo) { + log.Printf("%s --> %s", source, blob) + ctx := context.Background() + client, err := storage.NewClient(ctx) + check(err, i) + + // source + f, err := os.Open(source) + if err != nil { + log.Fatal(err) + } + defer f.Close() + + wc := client.Bucket(bucket).Object(blob).NewWriter(ctx) + _, err = io.Copy(wc, f) + check(err, i) + err = wc.Close() + check(err, i) +} + +func fileExists(filename string) bool { + info, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} + +func runTask(dataHash string, queueName string, taskName string, i dsInfo) { + // Run the heritability analysis. This is used to run it in the background. + // Execute R script + cmd := exec.Command("Rscript", "H2_script.R", datasetName, resultName, "hash.txt", heritabilityVersion, "strain_data.tsv") + cmd.Stderr = os.Stderr + o, err := cmd.Output() + + i.Msg = string(o) + i.Data_hash = dataHash + check(err, i) + + // Copy results to google storage. + resultBlob := fmt.Sprintf("reports/heritability/%s/%s", dataHash, resultName) + copyBlob(bucketName, resultName, resultBlob, i) + + // Log & output details of the task. + output := fmt.Sprintf("Completed task: task queue(%s), task name(%s), output(%s)", + queueName, + taskName, + o, + ) + log.Println(output) +} + +func h2Handler(w http.ResponseWriter, r *http.Request) { + /* + Handler for google cloud task which triggers a heritability calculation + */ + // Check header to verify the request is from a Cloud Task + taskName := r.Header.Get("X-Cloudtasks-Taskname") + if taskName == "" { + log.Println("Invalid Task: No X-Appengine-Taskname request header found") + http.Error(w, "Bad Request - Invalid Task", http.StatusBadRequest) + return + } + + // Pull useful headers from Task request. + queueName := r.Header.Get("X-Cloudtasks-Queuename") + + // Extract the request body for further task details. + body, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Printf("ReadAll: %v", err) + http.Error(w, "Internal Error", http.StatusInternalServerError) + return + } + + // Get POST data parse the body + var p Payload + err1 := json.Unmarshal([]byte(string(body)), &p) + if err1 != nil { + log.Printf("Error parsing payload JSON: %v", err1) + http.Error(w, err1.Error(), http.StatusBadRequest) + return + } + log.Printf("payload: %+v", p) + i := dsInfo{Kind: p.Ds_kind, Id: p.Ds_id} + + // Download the dataset + dataBlob := fmt.Sprintf("reports/heritability/%s/%s", p.Hash, datasetName) + dataset, err2 := downloadFile(os.Stdout, dataBlob) + check(err2, i) + + // Store it locally + f, err3 := os.Create(datasetName) + check(err3, i) + f.Write(dataset) + f.Close() + + // Write the hash + h, err4 := os.Create("hash.txt") + check(err4, i) + h.WriteString(p.Hash + "\n") + f.Close() + + // Log & output details of the task. + output := fmt.Sprintf("Started task: task queue(%s), task name(%s), payload(%s)", + queueName, + taskName, + string(body), + ) + log.Println(output) + + // Update datastore + setDatastoreStatus(p.Ds_kind, p.Ds_id, "RUNNING", "") + + // Execute the R script + runTask(p.Hash, queueName, taskName, i) + + setDatastoreStatus(p.Ds_kind, p.Ds_id, "COMPLETE", "") + + if err5 := json.NewEncoder(w).Encode("submitted h2"); err5 != nil { + log.Printf("Error sending response: %v", err5) + } + + // Set a non-2xx status code to indicate a failure in task processing that should be retried. + // For example, http.Error(w, "Internal Server Error: Task Processing", http.StatusInternalServerError) + + fmt.Fprintln(w, "200 OK") + +} + +// indexHandler responds to requests with our greeting. +func indexHandler(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + fmt.Fprint(w, "200 OK") +} + +func main() { + http.HandleFunc("/", indexHandler) + http.HandleFunc("/h2", h2Handler) + + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + log.Printf("listening on %s", port) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil)) +} From f89cd484b661538c4119f6117a40d4ffbcb1d58a Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Wed, 5 May 2021 00:15:40 -0500 Subject: [PATCH 149/288] fix task queue properties --- queue.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/queue.yaml b/queue.yaml index 12f33f3b..61a5dc8b 100644 --- a/queue.yaml +++ b/queue.yaml @@ -3,11 +3,9 @@ queue: max_concurrent_requests: 1 rate: 1/s retry_parameters: - task_retry_limit: 2 - task_age_limit: 1d + task_retry_limit: 3 - name: ipcalc max_concurrent_requests: 1 rate: 1/s retry_parameters: - task_retry_limit: 2 - task_age_limit: 1d + task_retry_limit: 3 From 96f9823c7516800f2f32ec74fd79a77cd254722f Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Wed, 5 May 2021 00:38:19 -0500 Subject: [PATCH 150/288] update retry logic for task queues --- queue.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/queue.yaml b/queue.yaml index 61a5dc8b..f41fdb47 100644 --- a/queue.yaml +++ b/queue.yaml @@ -4,8 +4,15 @@ queue: rate: 1/s retry_parameters: task_retry_limit: 3 + min_backoff_seconds: 60 + max_backoff_seconds: 600 + max_doublings: 3 + - name: ipcalc max_concurrent_requests: 1 rate: 1/s retry_parameters: task_retry_limit: 3 + min_backoff_seconds: 60 + max_backoff_seconds: 600 + max_doublings: 3 \ No newline at end of file From 1315d927b5725581805bceec4413e7267a12d4d6 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Wed, 5 May 2021 01:21:27 -0500 Subject: [PATCH 151/288] add dockerfile --- Dockerfile | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 7 +++++++ 2 files changed, 61 insertions(+) create mode 100644 Dockerfile create mode 100644 README.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..2635b4a8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,54 @@ +# Use the offical golang image to create a binary. +# This is based on Debian and sets the GOPATH to /go. +# https://hub.docker.com/_/golang +FROM golang:1.16-buster as builder + +# Create and change to the app directory. +WORKDIR /app + +# Retrieve application dependencies. +# This allows the container build to reuse cached dependencies. +# Expecting to copy go.mod and if present go.sum. +COPY H2_script.R ./ +COPY strain_data.tsv ./ +COPY go.* ./ +RUN go mod download + +# Copy local code to the container image. +COPY . ./ + +# Build the binary. +RUN go build -v -o server + + +# build the deployed image +FROM continuumio/miniconda3 + +RUN apt-get update && apt-get install -y procps && \ + apt-get clean +RUN conda config --add channels defaults && \ + conda config --add channels bioconda && \ + conda config --add channels conda-forge +RUN conda create -n heritability \ + conda-forge::go=1.16.3 \ + r=3.6.0 \ + r-lme4 \ + r-dplyr \ + r-tidyr \ + r-glue \ + r-boot \ + r-data.table \ + r-futile.logger \ + && conda clean -a + +LABEL Name="heritability" Author="Daniel Cook" +ENV PATH /opt/conda/envs/heritability/bin:$PATH +WORKDIR /app + +# Copy local code to the container image. +COPY --from=builder /app/H2_script.R /app/H2_script.R +COPY --from=builder /app/strain_data.tsv /app/strain_data.tsv +COPY --from=builder /app/server /app/server + +# Run the web service on container startup. +CMD ["/app/server"] diff --git a/README.md b/README.md new file mode 100644 index 00000000..7bad9d5d --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# This directory contains the code to start the Google Cloud Run Microservice for the Heritability Tool + +Build using: + +gcloud config set project andersen-lab +gcloud builds submit --config cloudbuild.yaml . --timeout=3h +gcloud run deploy --image gcr.io/andersen-lab/h2-1 --memory 1024Mi --platform managed h2-1 --timeout=15m From 6642327320470ea95713fb6df17bb7279f880ae6 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Wed, 5 May 2021 01:25:26 -0500 Subject: [PATCH 152/288] replace csi links with tbi --- base/templates/data_v2.html | 30 +++++++++++++++--------------- base/views/data.py | 10 +++++----- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/base/templates/data_v2.html b/base/templates/data_v2.html index a7b370a6..dd174e18 100644 --- a/base/templates/data_v2.html +++ b/base/templates/data_v2.html @@ -110,10 +110,10 @@

    Datasets

    WI.{{ selected_release }}.soft-filter.vcf.gz is not included in this release {% endif %}
    - {% if f.get('soft_filter_vcf_gz_csi') %} - WI.{{ selected_release }}.soft-filter.vcf.gz.csi + {% if f.get('soft_filter_vcf_gz_tbi') %} + WI.{{ selected_release }}.soft-filter.vcf.gz.tbi {% else %} - WI.{{ selected_release }}.soft-filter.vcf.gz.csi is not included in this release + WI.{{ selected_release }}.soft-filter.vcf.gz.tbi is not included in this release {% endif %}

    @@ -125,10 +125,10 @@

    Datasets

    WI.{{ selected_release }}.soft-filter.isotype.vcf.gz is not included in this release {% endif %}
    - {% if f.get('soft_filter_isotype_vcf_gz_csi') %} - WI.{{ selected_release }}.soft-filter.isotype.vcf.gz.csi + {% if f.get('soft_filter_isotype_vcf_gz_tbi') %} + WI.{{ selected_release }}.soft-filter.isotype.vcf.gz.tbi {% else %} - WI.{{ selected_release }}.soft-filter.isotype.vcf.gz.csi is not included in this release + WI.{{ selected_release }}.soft-filter.isotype.vcf.gz.tbi is not included in this release {% endif %}
    @@ -146,10 +146,10 @@

    Datasets

    WI.{{ selected_release }}.hard-filter.vcf.gz is not included in this release {% endif %}
    - {% if f.get('hard_filter_vcf_gz_csi') %} - WI.{{ selected_release }}.hard-filter.vcf.gz.csi + {% if f.get('hard_filter_vcf_gz_tbi') %} + WI.{{ selected_release }}.hard-filter.vcf.gz.tbi {% else %} - WI.{{ selected_release }}.hard-filter.vcf.gz.csi is not included in this release + WI.{{ selected_release }}.hard-filter.vcf.gz.tbi is not included in this release {% endif %}

    @@ -161,10 +161,10 @@

    Datasets

    WI.{{ selected_release }}.hard-filter.isotype.vcf.gz is not included in this release {% endif %}
    - {% if f.get('hard_filter_isotype_vcf_gz_csi') %} - WI.{{ selected_release }}.hard-filter.isotype.vcf.gz.csi + {% if f.get('hard_filter_isotype_vcf_gz_tbi') %} + WI.{{ selected_release }}.hard-filter.isotype.vcf.gz.tbi {% else %} - WI.{{ selected_release }}.hard-filter.isotype.vcf.gz.csi is not included in this release + WI.{{ selected_release }}.hard-filter.isotype.vcf.gz.tbi is not included in this release {% endif %}
    @@ -182,10 +182,10 @@

    Datasets

    WI.{{ selected_release }}.impute.isotype.vcf.gz is not included in this release {% endif %}
    - {% if f.get('impute_isotype_vcf_gz_csi') %} - WI.{{ selected_release }}.impute.isotype.vcf.gz.csi + {% if f.get('impute_isotype_vcf_gz_tbi') %} + WI.{{ selected_release }}.impute.isotype.vcf.gz.tbi {% else %} - WI.{{ selected_release }}.impute.isotype.vcf.gz.csi is not included in this release + WI.{{ selected_release }}.impute.isotype.vcf.gz.tbi is not included in this release {% endif %}
    diff --git a/base/views/data.py b/base/views/data.py index 62baf685..e8785f7b 100644 --- a/base/views/data.py +++ b/base/views/data.py @@ -31,15 +31,15 @@ def generate_v2_file_list(selected_release): f = dict() f['soft_filter_vcf_gz'] = f'{prefix}/variation/WI.{selected_release}.soft-filter.vcf.gz' - f['soft_filter_vcf_gz_csi'] = f'{prefix}/variation/WI.{selected_release}.soft-filter.vcf.gz.csi' + f['soft_filter_vcf_gz_tbi'] = f'{prefix}/variation/WI.{selected_release}.soft-filter.vcf.gz.tbi' f['soft_filter_isotype_vcf_gz'] = f'{prefix}/variation/WI.{selected_release}.soft-filter.isotype.vcf.gz' - f['soft_filter_isotype_vcf_gz_csi'] = f'{prefix}/variation/WI.{selected_release}.soft-filter.isotype.vcf.gz.csi' + f['soft_filter_isotype_vcf_gz_tbi'] = f'{prefix}/variation/WI.{selected_release}.soft-filter.isotype.vcf.gz.tbi' f['hard_filter_vcf_gz'] = f'{prefix}/variation/WI.{selected_release}.hard-filter.vcf.gz' - f['hard_filter_vcf_gz_csi'] = f'{prefix}/variation/WI.{selected_release}.hard-filter.vcf.gz.csi' + f['hard_filter_vcf_gz_tbi'] = f'{prefix}/variation/WI.{selected_release}.hard-filter.vcf.gz.tbi' f['hard_filter_isotype_vcf_gz'] = f'{prefix}/variation/WI.{selected_release}.hard-filter.isotype.vcf.gz' - f['hard_filter_isotype_vcf_gz_csi'] = f'{prefix}/variation/WI.{selected_release}.hard-filter.isotype.vcf.gz.csi' + f['hard_filter_isotype_vcf_gz_tbi'] = f'{prefix}/variation/WI.{selected_release}.hard-filter.isotype.vcf.gz.tbi' f['impute_isotype_vcf_gz'] = f'{prefix}/variation/WI.{selected_release}.impute.isotype.vcf.gz' - f['impute_isotype_vcf_gz_csi'] = f'{prefix}/variation/WI.{selected_release}.impute.isotype.vcf.gz.csi' + f['impute_isotype_vcf_gz_tbi'] = f'{prefix}/variation/WI.{selected_release}.impute.isotype.vcf.gz.tbi' f['hard_filter_min4_tree'] = f'{prefix}/tree/WI.{selected_release}.hard-filter.min4.tree' f['hard_filter_min4_tree_pdf'] = f'{prefix}/tree/WI.{selected_release}.hard-filter.min4.tree.pdf' From bc2e0930fa068631321dee792e03eb8a5085938d Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Wed, 5 May 2021 01:25:53 -0500 Subject: [PATCH 153/288] update queue logic --- queue.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/queue.yaml b/queue.yaml index f41fdb47..404fc1c6 100644 --- a/queue.yaml +++ b/queue.yaml @@ -3,16 +3,16 @@ queue: max_concurrent_requests: 1 rate: 1/s retry_parameters: - task_retry_limit: 3 - min_backoff_seconds: 60 - max_backoff_seconds: 600 - max_doublings: 3 + task_retry_limit: 2 + min_backoff_seconds: 10 + max_backoff_seconds: 60 + max_doublings: 2 - name: ipcalc max_concurrent_requests: 1 rate: 1/s retry_parameters: - task_retry_limit: 3 - min_backoff_seconds: 60 - max_backoff_seconds: 600 - max_doublings: 3 \ No newline at end of file + task_retry_limit: 2 + min_backoff_seconds: 10 + max_backoff_seconds: 60 + max_doublings: 2 \ No newline at end of file From 251f273f37de1bd24fa9fa48444c734342f18cdb Mon Sep 17 00:00:00 2001 From: samwachspress <75639405+samwachspress@users.noreply.github.com> Date: Wed, 5 May 2021 12:10:09 -0500 Subject: [PATCH 154/288] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7bad9d5d..0ddf2581 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ -# This directory contains the code to start the Google Cloud Run Microservice for the Heritability Tool +### GOLANG CloudRun for H2 Heritability calculations. ### + +Server listens for CloudTask queue requests, downloads the input data from CloudStorage, executes R script, then uploads the result to CloudStorage. At each stage, the DataStore status is updated. Build using: -gcloud config set project andersen-lab gcloud builds submit --config cloudbuild.yaml . --timeout=3h gcloud run deploy --image gcr.io/andersen-lab/h2-1 --memory 1024Mi --platform managed h2-1 --timeout=15m From a316f93c583ce0c033b1cf62e3119e3d057cb857 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 10 May 2021 09:57:30 -0500 Subject: [PATCH 155/288] update gene browser, relocate --- .../templates/{browser.html => gbrowser.html} | 213 +----------------- 1 file changed, 1 insertion(+), 212 deletions(-) rename base/templates/{browser.html => gbrowser.html} (64%) diff --git a/base/templates/browser.html b/base/templates/gbrowser.html similarity index 64% rename from base/templates/browser.html rename to base/templates/gbrowser.html index e444cb98..ab3fc9be 100644 --- a/base/templates/browser.html +++ b/base/templates/gbrowser.html @@ -102,7 +102,7 @@
    Tracks {{ i }} {% endfor %} - {% for i in ["phyloP", "phastCons", "Variants", "Dust", "Repeat Masker", "Divergent Regions Summary" ] %} + {% for i in ["phyloP", "phastCons", "Dust", "Repeat Masker", "Divergent Regions Summary" ] %}
    @@ -141,10 +141,6 @@
    Gene Search
    Isotype (reference strain)
    -
    - -
    -
    @@ -190,51 +186,6 @@
    Isotype (reference strain) -
    -
    -
    {# /col-md-10 #} -
    - -
    {# /col-md-10 #} -
    {# /row #} - -
    {# /row #} - -
    -
    -
    {{ item.created_on|date_format }} {{ item.created_on|date_format }}
    - - - - - - - - - - - - - - - - -
    CHROM:POSREF / ALTFilterAFGene NameWormBase IDSequenceCoding ChangeBiotypeAnnotationImpact
    - -
    -
    {% endblock %} {% block script %} @@ -285,18 +236,6 @@
    Isotype (reference strain) Isotype (reference strain) Isotype (reference strain) Isotype (reference strain) Isotype (reference strain) input").val(); - if (/[IVXMtDNA]+:[0-9,]+-[0-9,]+/g.exec(region) != null) { - chrom = region.split(":")[0]; - start = region.split(":")[1].split("-")[0].replace(/,/g, ""); - end = region.split(":")[1].split("-")[1].replace(/,/g, ""); - release = {{ DATASET_RELEASE }}; - - sample_tracks = []; - $(".sample-track:checked").each(function(i, e) { sample_tracks.push(e.value) }) - - variant_annotation = $("input[name='variant-annotation']:checked").val() - - data = {'chrom': chrom, - 'start': parseInt(start), - 'end': parseInt(end), - 'release': release, - 'variant_impact': ['ALL'], - 'sample_tracks': sample_tracks.join('_'), - 'list-all-strains': $('.list-all-strains').prop('checked'), - 'variant-annotation': variant_annotation - } - - if (download == true) { - data['output'] = 'tsv'; - url = "{{ url_for('api_variant.variant_query') }}?" + $.param(data); - window.open(url, sprintf("%s_%s_%s.csv", chrom, start, end)); - } else { - - $("#variants > tbody").html(""); - data['output'] = 'json'; - data = $.param(data); - - if(xhr) { - xhr.abort(); - } - - // Check that strains are selected otherwise abort - - xhr = $.ajax({ - url: "{{ url_for('api_variant.variant_query') }}", - data: data, - method: "GET", - contentType: "application/json", - success: function(msg) { - console.log(msg) - $.each(msg, function(k, rec) { - var is_first = true; - var td_rowspan = 1; - if (rec["ANN"].length == 0) { - rec["ANN"] = [{ effect: "", gene_id: null}] - } - $.each(rec['ANN'], function(annk, ann) { - if (ann["gene_id"] != null) - { - ann["gene_id"] = `${ann["gene_id"]}`; - } - impact = "" - - items = [ann.gene_name, - ann.gene_id, - ann.feature_id, - ann.aa_change, - ann.transcript_biotype, - ann.effect.split("&").join(", "), - impact]; - - if (is_first) { - td_rowspan = rec['ANN'].length; - variant_items = [`${rec["CHROM"]}:${Number(rec["POS"]).toLocaleString('en')}`, - `${rec["REF"]} / ${rec["ALT"].join(",")}`, - rec["FILTER"], - rec["AF"]] - is_first = false; - rowspan_td = ``; - variant_items = rowspan_td + variant_items.join(`${rowspan_td}`) + "" - } else { - variant_items = "" - } - - additional_fields = [ann.gene_name + ": " , ann.filter] - if (rec['FILTER'] != "PASS") { - fail_variant_class = 'class="warning"' - } else { - fail_variant_class = "" - } - $("#variants > tbody").append(`${variant_items}${items.join("")}`); - }); - - if ($(".list-all-strains").prop('checked') == true) { - gt_panel = $(".isotype-item").map(function () {return this.value;}).get(); - } else { - gt_panel = $(".isotype-item:checked").map(function () {return this.value;}).get(); - } - - if (gt_panel.length > 0) { - gts = rec["GT"].filter(function(i) { return gt_panel.includes(i["SAMPLE"]) }); - // Append genotypes - $("#variants > tbody").append( - ` -
    -
    -
    Reference
    - ${draw_gt_set(gts, 0)} -
    Alternative
    - ${draw_gt_set(gts, 2)} -
    Other
    - ${draw_gt_set(gts, 1)} - ${draw_gt_set(gts, 3)} -
    -
    - `); - // enable tooltips - $('.ttop').tooltip(); - } - - }); - - // Enable links - $("#variants a").on("click", function() { - window.location = $(this).attr('href'); - }) - - } - }) {# /ajax #} - - } // End if else logic for download - } // Test for region -} - -$("#download").click(function() { - refresh_variants(download=true); -}); var typingTimer; //timer identifier var doneTypingInterval = 1000; //time in ms (5 seconds) @@ -622,21 +422,10 @@
    Other
    }) -$(".igvNavigationSearchInput").on("input paste", function() { - setTimeout(refresh_variants, 100); -}); - -$(".igvNavigationSearchInput").keypress(function(e) { - if(e.which == 13) { - setTimeout(refresh_variants, 100); - } -}); - // Initial load setTimeout(reload_tracks, 500); -setTimeout(refresh_variants, 1000); }); From cdf662efd791e7b04bd57832f07b545c6170819a Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 10 May 2021 10:17:21 -0500 Subject: [PATCH 156/288] cleanup wb versions, gbrowser, add vbrowser --- base/database/__init__.py | 2 +- base/forms.py | 4 +- base/models.py | 65 +++++ base/static/css/styles.css | 45 ++++ base/templates/_includes/navbar.html | 3 +- base/templates/gbrowser.html | 6 +- base/templates/gene/gene.html | 2 +- base/templates/tools/indel_primer.html | 4 +- base/templates/vbrowser.html | 345 +++++++++++++++++++++++++ base/views/data.py | 59 ++++- scripts/setup_browser_tracks.sh | 10 +- 11 files changed, 523 insertions(+), 22 deletions(-) create mode 100644 base/templates/vbrowser.html diff --git a/base/database/__init__.py b/base/database/__init__.py index 3f4370ae..b043b0e2 100644 --- a/base/database/__init__.py +++ b/base/database/__init__.py @@ -35,7 +35,7 @@ def initialize_sqlite_database(sel_wormbase_version, strain_only=False): """Create a static sqlite database Args: - sel_wormbase_version - e.g. WS245 + sel_wormbase_version - e.g. WS276 Generate an sqlite database """ diff --git a/base/forms.py b/base/forms.py index 33b6653e..2b2390a1 100644 --- a/base/forms.py +++ b/base/forms.py @@ -166,8 +166,10 @@ class heritability_form(Form): # -# Perform Mapping Form +# Variant Browser Forms # +class vbrowser_form(Form): + pass class TraitData(HiddenField): diff --git a/base/models.py b/base/models.py index 370e7062..08718bac 100644 --- a/base/models.py +++ b/base/models.py @@ -767,3 +767,68 @@ def unnest(self): def __repr__(self): return f"homolog: {self.gene_name} -- {self.homolog_gene}" + + +class StrainAnnotatedVariants(DictSerializable, db.Model): + """ + The Strain Annotated Variant table combines several features linked to variants: + Genetic location, base pairs affected, consequences of reading, gene information, + strains affected, and severity of impact + + """ + __tablename__ = 'v3' + idx = db.Column(db.String(), index=True, primary_key=True) + chrom = db.Column(db.String(), index=True) + pos = db.Column(db.Integer(), index=True) + ref_seq = db.Column(db.String(), index=True) + alt_seq =db.Column(db.String(), index=True) + consequence = db.Column(db.String(), index=True) + wormbase_id = db.Column(db.String(), index=True) + transcript = db.Column(db.String(), index=True) + biotype = db.Column(db.String(), index=True) + strand = db.Column(db.String(), index=True) + amino_acid_change = db.Column(db.String(), index=True) + dna_change = db.Column(db.String(), index=True) + strains = db.Column(db.String(), index=True) + blosum = db.Column(db.String(), index=True) + grantham = db.Column(db.String(), index=True) + percent_protein = db.Column(db.Float(), index=True) + gene = db.Column(db.String(), index=True) + variant_impact = db.Column(db.String(), index=True) + divergent = db.Column(db.String(), index=True) + + @classmethod + def generate_interval_sql(cls, type, interval): + if type == 'interval': + interval = interval.replace(',','') + chrom = interval.split(':')[0] + range = interval.split(':')[1] + start = int(range.split('-')[0]) + stop = int(range.split('-')[1]) + + q = f'SELECT * FROM {cls.__tablename__} WHERE chrom="{chrom}" AND pos > {start} AND pos < {stop};' + return q + + + ''' TODO: implement input checks here and in the browser form''' + @classmethod + def verify_query(cls, type, query, strains): + return True + + @classmethod + def run_query(cls, type, q, strains): + result = {} + # todo handle errors/no results better + if type == 'interval': + query = cls.generate_interval_sql(type, q) + df = pd.read_sql_query(query, db.engine) + + + result = df[['idx', 'chrom', 'pos', 'ref_seq', 'alt_seq', 'consequence', 'wormbase_id', 'transcript', 'biotype', 'strand', \ + 'amino_acid_change', 'dna_change', 'strains', 'blosum', 'grantham', 'percent_protein', 'gene', \ + 'variant_impact', 'divergent']].dropna(how='any') \ + .agg(list) \ + .to_dict() + return result + + diff --git a/base/static/css/styles.css b/base/static/css/styles.css index b4a91515..d81745e0 100644 --- a/base/static/css/styles.css +++ b/base/static/css/styles.css @@ -652,3 +652,48 @@ article { .dataTables-menu-top { padding-bottom: 10px; } + + +.strain-vb { + font-family: 'Roboto', sans-serif; + padding-top:20px; +} + +.strain-checkbox { + padding: 6px; + height: 500px; +} + +.strain-btn { + padding-top:10px; +} + +.strain-list { + display: inline-block; + font-weight: 400; + border-radius: 4px; + box-shadow: inset 0 1px 1px rgb(0 0 0 / 8%); + width: 100%; + text-align:left; + border: 1px solid rgba(0,0,0,.2); +} + +.strain-filter { + width: 100%; + font-weight: 400; + border-top:0px; + border-left:0px; + border-right:0px; + padding-top: 10px; + padding-bottom: 10px; + padding-left: 5px; + padding-right: 5px; + border-bottom: 1px solid rgba(0,0,0,.2); +} + +.variant-browser-navtab { + font-family: 'Roboto', sans-serif; + font-size: 25px; + padding-top: 20px; + padding-bottom: 20px; +} diff --git a/base/templates/_includes/navbar.html b/base/templates/_includes/navbar.html index 8efe6ed2..56bd8b61 100644 --- a/base/templates/_includes/navbar.html +++ b/base/templates/_includes/navbar.html @@ -78,7 +78,8 @@ diff --git a/base/templates/gbrowser.html b/base/templates/gbrowser.html index ab3fc9be..30700878 100644 --- a/base/templates/gbrowser.html +++ b/base/templates/gbrowser.html @@ -206,7 +206,7 @@
    Isotype (reference strain) Isotype (reference strain) Location
    - {{ gene_record.interval }} diff --git a/base/templates/tools/indel_primer.html b/base/templates/tools/indel_primer.html index a2b447c7..360a4a2c 100644 --- a/base/templates/tools/indel_primer.html +++ b/base/templates/tools/indel_primer.html @@ -75,8 +75,8 @@ showNavigation: true, showKaryo: false, reference: { - id: "WS245", - fastaURL: "//storage.googleapis.com/elegansvariation.org/browser_tracks/c_elegans.PRJNA13758.WS245.genomic.fa", + id: "WS276", + fastaURL: "//storage.googleapis.com/elegansvariation.org/browser_tracks/c_elegans.PRJNA13758.WS276.genomic.fa", }, }; diff --git a/base/templates/vbrowser.html b/base/templates/vbrowser.html new file mode 100644 index 00000000..e3b4c513 --- /dev/null +++ b/base/templates/vbrowser.html @@ -0,0 +1,345 @@ +{% extends "_layouts/default.html" %} + + +{% block custom_head %} + + +{% endblock %} + + +{% block content %} + +
    + + {# nav tabs #} + + +
    + +{# strain select list #} +
    +
    +
    + +

    Available Strains

    + +
    + + + + +
    +
    +
    + + +
    +
    + {% for strain in strain_listing %} +
    + + +
    + {% endfor %} +
    +
    + +
    + +
    + +
    + +
    {# / strain-vb #} +
    {# /col-md-2 #} +
    + + + {#- Tab panes -#} + +
    +
    + +
    + +

    + Search by WBGeneID, alphanumeric name (F37A4.8), or gene name (isw-1) +

    + +
    +
    + +
    +
    + +
    + +
    + +
    + +
    + +

    + Search using the format [chromosome:START-STOP] +

    + + +
    +
    + +
    +
    + + +
    + +
    + +
    + +
    {# /tabcontent #} +
    {# /col-md-4 #} + + +{# strain deselect list #} +
    +
    +
    +

    Selected Strains

    + +
    + + + +
    +
    +
    +
    + + +
    +
    + {% for strain in strain_listing %} + + {% endfor %} +
    +
    + +
    + +
    + +
    + +
    + +
    {# / strain-select-vb #} +
    {# /col-md-2 #} +
    + + +
    {# /row #} + +
    + + + + + {% for colname in column_names %} + + {% endfor %} + + + + + + {% for colname in column_names %} + + {% endfor %} + + +
    {{colname}}
    + +
    + + + + + + +{% endblock %} + +{% block script %} + +{% endblock %} + diff --git a/base/views/data.py b/base/views/data.py index 62baf685..be6d5057 100644 --- a/base/views/data.py +++ b/base/views/data.py @@ -1,3 +1,5 @@ +import json +from flask import request, jsonify import requests import os @@ -8,7 +10,8 @@ from base.constants import BAM_BAI_DOWNLOAD_SCRIPT_NAME, GOOGLE_CLOUD_BUCKET from base.config import config from base.extensions import cache -from base.models import Strain +from base.forms import vbrowser_form +from base.models import Strain, StrainAnnotatedVariants from base.utils.gcloud import list_release_files, generate_download_signed_url_v4, download_file from base.utils.jwt_utils import jwt_required from base.views.api.api_strain import get_isotypes, query_strains @@ -192,18 +195,58 @@ def download_bam_url(blob_name=''): # ============= # -# Browser # +# GBrowser # # ============= # -@data_bp.route('/browser') -@data_bp.route('/browser/') -@data_bp.route('/browser//') -@data_bp.route('/browser///') -def browser(release=config["DATASET_RELEASE"], region="III:11746923-11750250", query=None): +@data_bp.route('/gbrowser') +@data_bp.route('/gbrowser/') +@data_bp.route('/gbrowser//') +@data_bp.route('/gbrowser///') +def gbrowser(release=config["DATASET_RELEASE"], region="III:11746923-11750250", query=None): VARS = {'title': "Genome Browser", 'DATASET_RELEASE': int(release), 'strain_listing': get_isotypes(), 'region': region, 'query': query, 'fluid_container': False} - return render_template('browser.html', **VARS) + return render_template('gbrowser.html', **VARS) + + +# ============= # +# VBrowser # +# ============= # + + +@data_bp.route('/vbrowser') +def vbrowser(): + title = 'Variant Browser' + form = vbrowser_form() + selected_release = config['DATASET_RELEASE'] + strain_listing = query_strains(release=selected_release) + column_names = ['chrom', 'pos', 'ref_seq', 'alt_seq', 'consequence', 'wormbase_id', 'transcript',\ + 'biotype', 'strand', 'amino_acid_change', 'dna_change', 'strains', 'blosum', \ + 'grantham', 'percent_protein', 'gene', 'variant_impact', 'divergent']; + return render_template('vbrowser.html', **locals()) + + +@data_bp.route('/vbrowser/query', methods=['POST']) +def vbrowser_query(): + title = 'Variant Browser' + selected_release = config['DATASET_RELEASE'] + payload = json.loads(request.data) + + query_type = payload.get('query_type') + query = payload.get('query') + strains = payload.get('strains') + + is_valid = StrainAnnotatedVariants.verify_query(type=query_type, query=query, strains=strains) + if is_valid: + data = StrainAnnotatedVariants.run_query(type=query_type, q=query, strains=strains) + return jsonify(data) + + return jsonify({}) + + + + + diff --git a/scripts/setup_browser_tracks.sh b/scripts/setup_browser_tracks.sh index 23acb2c9..d8e1fa80 100644 --- a/scripts/setup_browser_tracks.sh +++ b/scripts/setup_browser_tracks.sh @@ -14,11 +14,11 @@ function zip_index { # one called elegans_genes on wormbase. # Add parenthetical gene name for transcripts. mkdir -p browser -curl ftp://ftp.wormbase.org/pub/wormbase/releases/WS253/MULTI_SPECIES/hub/elegans/elegans_genes_WS253.bb > elegans_genes_WS253.bb -BigBedToBed elegans_genes_WS253.bb tmp.bed -sortBed -i tmp.bed > browser/elegans_transcripts_WS253.bed -bgzip -f browser/elegans_transcripts_WS253.bed -tabix browser/elegans_transcripts_WS253.bed.gz +curl ftp://ftp.wormbase.org/pub/wormbase/releases/WS276/MULTI_SPECIES/hub/elegans/elegans_genes_WS276.bb > elegans_genes_WS276.bb +BigBedToBed elegans_genes_WS276.bb tmp.bed +sortBed -i tmp.bed > browser/elegans_transcripts_WS276.bed +bgzip -f browser/elegans_transcripts_WS276.bed +tabix browser/elegans_transcripts_WS276.bed.gz rm tmp.bed # Generate Gene Track BED File From 1c9335357a293736965f59b4dd7a134cc3235e42 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Wed, 12 May 2021 01:17:02 -0500 Subject: [PATCH 157/288] construct variant db schema and etl fn --- base/constants.py | 3 + base/database/__init__.py | 290 +++++++++++++++-------------- base/database/etl_variant_annot.py | 52 ++++++ base/models.py | 48 +++-- 4 files changed, 228 insertions(+), 165 deletions(-) create mode 100644 base/database/etl_variant_annot.py diff --git a/base/constants.py b/base/constants.py index 2beee99f..4e0478ad 100644 --- a/base/constants.py +++ b/base/constants.py @@ -64,6 +64,9 @@ class URLS: # BAM_URL_PREFIX = f"https://storage.googleapis.com/{GOOGLE_CLOUD_BUCKET}/bam" + # Variant Annotation CSV + STRAIN_VARIANT_ANNOTATION_URL = "https://storage.googleapis.com/elegansvariation.org/db/WI.20210121.strain-annotation.bcsq.20210401.csv" + """ Wormbase URLs """ diff --git a/base/database/__init__.py b/base/database/__init__.py index b043b0e2..3c0da032 100644 --- a/base/database/__init__.py +++ b/base/database/__init__.py @@ -9,7 +9,7 @@ from base.config import config from base.utils.data_utils import download from base.utils.gcloud import upload_file -from base.models import (db, +from base.models import (StrainAnnotatedVariants, db, Strain, Homologs, Metadata, @@ -22,154 +22,164 @@ from base.database.etl_wormbase import (fetch_gene_gff_summary, fetch_gene_gtf, fetch_orthologs) +from base.database.etl_variant_annot import fetch_strain_variant_annotation_data console = Console() DOWNLOAD_PATH = ".download" def download_fname(download_path: str, download_url: str): - return os.path.join(download_path, - download_url.split("/")[-1]) + return os.path.join(download_path, + download_url.split("/")[-1]) def initialize_sqlite_database(sel_wormbase_version, strain_only=False): - """Create a static sqlite database - Args: - sel_wormbase_version - e.g. WS276 - - Generate an sqlite database - """ - start = arrow.utcnow() - console.log("Initializing Database") - - DATASET_RELEASE = config['DATASET_RELEASE'] - SQLITE_PATH = f"base/cendr.{DATASET_RELEASE}.{sel_wormbase_version}.db" - SQLITE_BASENAME = os.path.basename(SQLITE_PATH) - - # Download wormbase files - if strain_only is False: - if os.path.exists(SQLITE_PATH): - os.remove(SQLITE_PATH) - - if not os.path.exists(DOWNLOAD_PATH): - os.makedirs(DOWNLOAD_PATH) - - # Parallel URL download - console.log("Downloading Wormbase Data") - GENE_GFF_URL = URLS.GENE_GFF_URL.format(WB=sel_wormbase_version) - GENE_GTF_URL = URLS.GENE_GTF_URL.format(WB=sel_wormbase_version) - download([GENE_GFF_URL, - GENE_GTF_URL, - URLS.GENE_IDS_URL, - URLS.HOMOLOGENE_URL, - URLS.ORTHOLOG_URL, - URLS.TAXON_ID_URL], - DOWNLOAD_PATH) - - gff_fname = download_fname(DOWNLOAD_PATH, GENE_GFF_URL) - gtf_fname = download_fname(DOWNLOAD_PATH, GENE_GTF_URL) - gene_ids_fname = download_fname(DOWNLOAD_PATH, URLS.GENE_IDS_URL) - homologene_fname = download_fname(DOWNLOAD_PATH, URLS.HOMOLOGENE_URL) - ortholog_fname = download_fname(DOWNLOAD_PATH, URLS.ORTHOLOG_URL) - - from base.application import create_app - app = create_app() - app.app_context().push() - app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{SQLITE_BASENAME}" - - if strain_only is True: - db.metadata.drop_all(bind=db.engine, checkfirst=True, tables=[Strain.__table__]) - db.metadata.create_all(bind=db.engine, tables=[Strain.__table__]) - else: - db.create_all(app=app) - db.session.commit() - - console.log(f"Created {SQLITE_PATH}") - - ################ - # Load Strains # - ################ - console.log('Loading strains...') - db.session.bulk_insert_mappings(Strain, fetch_andersen_strains()) - db.session.commit() - console.log(f"Inserted {Strain.query.count()} strains") - - if strain_only is True: - console.log('Finished loading strains') - return - - ################ - # Set metadata # - ################ - console.log('Inserting metadata') - metadata = {} - metadata.update(vars(constants)) - metadata.update({"CENDR_VERSION": config['CENDR_VERSION'], - "APP_CONFIG": config['APP_CONFIG'], - "DATASET_RELEASE": config['DATASET_RELEASE'], - "WORMBASE_VERSION": sel_wormbase_version, - "RELEASES": config['RELEASES'], - "DATE": arrow.utcnow()}) - for k, v in metadata.items(): - if not k.startswith("_"): - # For nested constants: - if type(v) == type: - for name in [x for x in dir(v) if not x.startswith("_")]: - key_val = Metadata(key="{}/{}".format(k, name), - value=getattr(v, name)) - db.session.add(key_val) - else: - key_val = Metadata(key=k, value=str(v)) - db.session.add(key_val) - - db.session.commit() - - ############## - # Load Genes # - ############## - console.log('Loading summary gene table') - genes = fetch_gene_gff_summary(gff_fname) - db.session.bulk_insert_mappings(WormbaseGeneSummary, genes) - db.session.commit() - - console.log('Loading gene table') - db.session.bulk_insert_mappings(WormbaseGene, fetch_gene_gtf(gtf_fname, gene_ids_fname)) - gene_summary = db.session.query(WormbaseGene.feature, - db.func.count(WormbaseGene.feature)) \ - .group_by(WormbaseGene.feature) \ - .all() - gene_summary = '\n'.join([f"{k}: {v}" for k, v in gene_summary]) - console.log(f"============\nGene Summary\n------------\n{gene_summary}\n============") - - ############################### - # Load homologs and orthologs # - ############################### - console.log('Loading homologs from homologene') - db.session.bulk_insert_mappings(Homologs, fetch_homologene(homologene_fname)) - db.session.commit() - - console.log('Loading orthologs from WormBase') - db.session.bulk_insert_mappings(Homologs, fetch_orthologs(ortholog_fname)) - db.session.commit() - - ############# - # Upload DB # - ############# - - # Upload the file using todays date for archiving purposes - console.log(f"Uploading Database ({SQLITE_BASENAME})") - upload_file(f"db/{SQLITE_BASENAME}", SQLITE_PATH) - - diff = int((arrow.utcnow() - start).total_seconds()) - console.log(f"{diff} seconds") - - # =========================== # - # Generate gene id dict # - # =========================== # - # Create a gene dictionary to match wormbase IDs to either the locus name - # or a sequence id - gene_dict = {x.gene_id: x.locus or x.sequence_name for x in WormbaseGeneSummary.query.all()} - pickle.dump(gene_dict, open("base/static/data/gene_dict.pkl", 'wb')) + """Create a static sqlite database + Args: + sel_wormbase_version - e.g. WS276 + + Generate an sqlite database + """ + start = arrow.utcnow() + console.log("Initializing Database") + + DATASET_RELEASE = config['DATASET_RELEASE'] + SQLITE_PATH = f"base/cendr.{DATASET_RELEASE}.{sel_wormbase_version}.db" + SQLITE_BASENAME = os.path.basename(SQLITE_PATH) + + # Download wormbase files + if strain_only is False: + if os.path.exists(SQLITE_PATH): + os.remove(SQLITE_PATH) + + if not os.path.exists(DOWNLOAD_PATH): + os.makedirs(DOWNLOAD_PATH) + + # Parallel URL download + console.log("Downloading Wormbase Data") + GENE_GFF_URL = URLS.GENE_GFF_URL.format(WB=sel_wormbase_version) + GENE_GTF_URL = URLS.GENE_GTF_URL.format(WB=sel_wormbase_version) + download([URLS.STRAIN_VARIANT_ANNOTATION_URL, + GENE_GFF_URL, + GENE_GTF_URL, + URLS.GENE_IDS_URL, + URLS.HOMOLOGENE_URL, + URLS.ORTHOLOG_URL, + URLS.TAXON_ID_URL], + DOWNLOAD_PATH) + + sva_fname = download_fname(DOWNLOAD_PATH,URLS.STRAIN_VARIANT_ANNOTATION_URL) + gff_fname = download_fname(DOWNLOAD_PATH, GENE_GFF_URL) + gtf_fname = download_fname(DOWNLOAD_PATH, GENE_GTF_URL) + gene_ids_fname = download_fname(DOWNLOAD_PATH, URLS.GENE_IDS_URL) + homologene_fname = download_fname(DOWNLOAD_PATH, URLS.HOMOLOGENE_URL) + ortholog_fname = download_fname(DOWNLOAD_PATH, URLS.ORTHOLOG_URL) + + from base.application import create_app + app = create_app() + app.app_context().push() + app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{SQLITE_BASENAME}" + + if strain_only is True: + db.metadata.drop_all(bind=db.engine, checkfirst=True, tables=[Strain.__table__]) + db.metadata.create_all(bind=db.engine, tables=[Strain.__table__]) + else: + db.create_all(app=app) + db.session.commit() + + console.log(f"Created {SQLITE_PATH}") + + ################ + # Load Strains # + ################ + console.log('Loading strains...') + db.session.bulk_insert_mappings(Strain, fetch_andersen_strains()) + db.session.commit() + console.log(f"Inserted {Strain.query.count()} strains") + + if strain_only is True: + console.log('Finished loading strains') + return + + ################ + # Set metadata # + ################ + console.log('Inserting metadata') + metadata = {} + metadata.update(vars(constants)) + metadata.update({"CENDR_VERSION": config['CENDR_VERSION'], + "APP_CONFIG": config['APP_CONFIG'], + "DATASET_RELEASE": config['DATASET_RELEASE'], + "WORMBASE_VERSION": sel_wormbase_version, + "RELEASES": config['RELEASES'], + "DATE": arrow.utcnow()}) + for k, v in metadata.items(): + if not k.startswith("_"): + # For nested constants: + if type(v) == type: + for name in [x for x in dir(v) if not x.startswith("_")]: + key_val = Metadata(key="{}/{}".format(k, name), + value=getattr(v, name)) + db.session.add(key_val) + else: + key_val = Metadata(key=k, value=str(v)) + db.session.add(key_val) + + db.session.commit() + + ############## + # Load Genes # + ############## + console.log('Loading summary gene table') + genes = fetch_gene_gff_summary(gff_fname) + db.session.bulk_insert_mappings(WormbaseGeneSummary, genes) + db.session.commit() + + console.log('Loading gene table') + db.session.bulk_insert_mappings(WormbaseGene, fetch_gene_gtf(gtf_fname, gene_ids_fname)) + gene_summary = db.session.query(WormbaseGene.feature, db.func.count(WormbaseGene.feature)) \ + .group_by(WormbaseGene.feature) \ + .all() + gene_summary = '\n'.join([f"{k}: {v}" for k, v in gene_summary]) + console.log(f"============\nGene Summary\n------------\n{gene_summary}\n============") + + ###################################### + # Load Strain Variant Annotated Data # + ###################################### + console.log('\nLoading strain variant annotated csv') + sva_data = fetch_strain_variant_annotation_data(sva_fname) + db.session.bulk_insert_mappings(StrainAnnotatedVariants, sva_data) + db.session.commit() + + ############################### + # Load homologs and orthologs # + ############################### + console.log('Loading homologs from homologene') + db.session.bulk_insert_mappings(Homologs, fetch_homologene(homologene_fname)) + db.session.commit() + + console.log('Loading orthologs from WormBase') + db.session.bulk_insert_mappings(Homologs, fetch_orthologs(ortholog_fname)) + db.session.commit() + + ############# + # Upload DB # + ############# + + # Upload the file using todays date for archiving purposes + #console.log(f"Uploading Database ({SQLITE_BASENAME})") + #upload_file(f"db/{SQLITE_BASENAME}", SQLITE_PATH) + + diff = int((arrow.utcnow() - start).total_seconds()) + console.log(f"{diff} seconds") + + # =========================== # + # Generate gene id dict # + # =========================== # + # Create a gene dictionary to match wormbase IDs to either the locus name + # or a sequence id + gene_dict = {x.gene_id: x.locus or x.sequence_name for x in WormbaseGeneSummary.query.all()} + pickle.dump(gene_dict, open("base/static/data/gene_dict.pkl", 'wb')) def download_sqlite_database(): diff --git a/base/database/etl_variant_annot.py b/base/database/etl_variant_annot.py new file mode 100644 index 00000000..9e1c1e91 --- /dev/null +++ b/base/database/etl_variant_annot.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +""" +Loads the Strain Variant Annotated CSV into the SQLite DB + +Author: Sam Wachspress +""" +import csv + +from sqlalchemy.sql.expression import null + +def fetch_strain_variant_annotation_data(sva_fname: str): + """ + Load strain variant annotation table data: + + CHROM,POS,REF,ALT,CONSEQUENCE,WORMBASE_ID,TRANSCRIPT,BIOTYPE, + STRAND,AMINO_ACID_CHANGE,DNA_CHANGE,Strains,BLOSUM,Grantham, + Percent_Protein,GENE,VARIANT_IMPACT,DIVERGENT + + """ + with open(sva_fname) as csv_file: + csv_reader = csv.reader(csv_file, delimiter=',') + + line_count = -1 + for row in csv_reader: + if line_count == -1: + print(f'Column names are {", ".join(row)}') + line_count += 1 + else: + line_count += 1 + yield { + 'id': line_count, + 'chrom': row[0], + 'pos': int(row[1]), + 'ref_seq': row[2] if row[2] else None, + 'alt_seq': row[3] if row[3] else None, + 'consequence': row[4] if row[4] else None, + 'gene_id': row[5] if row[6] else None, + 'transcript': row[6] if row[6] else None, + 'biotype': row[7] if row[7] else None, + 'strand': row[8] if row[8] else None, + 'amino_acid_change': row[9] if row[9] else None, + 'dna_change': row[10] if row[10] else None, + 'strains': row[11] if row[11] else None, + 'blosum': int(row[12]) if row[12] else None, + 'grantham': int(row[13]) if row[13] else None, + 'percent_protein': float(row[14]) if row[14] else None, + 'gene': row[15] if row[15] else None, + 'variant_impact': row[16] if row[16] else None, + 'divergent': 1 if row[17] == 'D' else None, + } + + print(f'Processed {line_count} lines.') diff --git a/base/models.py b/base/models.py index 08718bac..1853bed7 100644 --- a/base/models.py +++ b/base/models.py @@ -776,26 +776,26 @@ class StrainAnnotatedVariants(DictSerializable, db.Model): strains affected, and severity of impact """ - __tablename__ = 'v3' - idx = db.Column(db.String(), index=True, primary_key=True) - chrom = db.Column(db.String(), index=True) + __tablename__ = 'variant_annotation' + id = db.Column(db.Integer, primary_key=True) + chrom = db.Column(db.String(7), index=True) pos = db.Column(db.Integer(), index=True) - ref_seq = db.Column(db.String(), index=True) - alt_seq =db.Column(db.String(), index=True) - consequence = db.Column(db.String(), index=True) - wormbase_id = db.Column(db.String(), index=True) - transcript = db.Column(db.String(), index=True) - biotype = db.Column(db.String(), index=True) - strand = db.Column(db.String(), index=True) - amino_acid_change = db.Column(db.String(), index=True) - dna_change = db.Column(db.String(), index=True) - strains = db.Column(db.String(), index=True) - blosum = db.Column(db.String(), index=True) - grantham = db.Column(db.String(), index=True) - percent_protein = db.Column(db.Float(), index=True) - gene = db.Column(db.String(), index=True) - variant_impact = db.Column(db.String(), index=True) - divergent = db.Column(db.String(), index=True) + ref_seq = db.Column(db.String(), nullable=True) + alt_seq =db.Column(db.String(), nullable=True) + consequence = db.Column(db.String(), nullable=True) + gene_id = db.Column(db.String(), index=True, nullable=True) + transcript = db.Column(db.String(), index=True, nullable=True) + biotype = db.Column(db.String(), nullable=True) + strand = db.Column(db.String(1), nullable=True) + amino_acid_change = db.Column(db.String(), nullable=True) + dna_change = db.Column(db.String(), nullable=True) + strains = db.Column(db.String(), nullable=True) + blosum = db.Column(db.Integer(), nullable=True) + grantham = db.Column(db.Integer(), nullable=True) + percent_protein = db.Column(db.Float(), nullable=True) + gene = db.Column(db.String(), index=True, nullable=True) + variant_impact = db.Column(db.String(), nullable=True) + divergent = db.Column(db.Integer(), nullable=True) @classmethod def generate_interval_sql(cls, type, interval): @@ -812,21 +812,19 @@ def generate_interval_sql(cls, type, interval): ''' TODO: implement input checks here and in the browser form''' @classmethod - def verify_query(cls, type, query, strains): + def verify_query(cls, type, query): return True @classmethod - def run_query(cls, type, q, strains): + def run_query(cls, type, q): result = {} # todo handle errors/no results better if type == 'interval': query = cls.generate_interval_sql(type, q) df = pd.read_sql_query(query, db.engine) - - result = df[['idx', 'chrom', 'pos', 'ref_seq', 'alt_seq', 'consequence', 'wormbase_id', 'transcript', 'biotype', 'strand', \ - 'amino_acid_change', 'dna_change', 'strains', 'blosum', 'grantham', 'percent_protein', 'gene', \ - 'variant_impact', 'divergent']].dropna(how='any') \ + result = df[['id', 'chrom', 'pos', 'ref_seq', 'alt_seq', 'consequence', 'gene_id', 'transcript', 'biotype', 'strand', 'amino_acid_change', 'dna_change', 'strains', 'blosum', 'grantham', 'percent_protein', 'gene', 'variant_impact', 'divergent']].dropna(how='all') \ + .fillna(value="") \ .agg(list) \ .to_dict() return result From 0d2ddda45479745240112146a67f46fe7b178a45 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Wed, 12 May 2021 01:17:42 -0500 Subject: [PATCH 158/288] update browsers --- base/static/css/styles.css | 15 +- base/templates/gbrowser.html | 46 ++- base/templates/vbrowser.html | 583 +++++++++++++++++++++-------------- base/views/data.py | 30 +- base/views/maintenance.py | 2 +- 5 files changed, 404 insertions(+), 272 deletions(-) diff --git a/base/static/css/styles.css b/base/static/css/styles.css index d81745e0..15bc532a 100644 --- a/base/static/css/styles.css +++ b/base/static/css/styles.css @@ -659,18 +659,18 @@ article { padding-top:20px; } -.strain-checkbox { +.select-checkbox { padding: 6px; - height: 500px; } .strain-btn { padding-top:10px; } -.strain-list { +.chk-select-list { display: inline-block; font-weight: 400; + font-size:0.8em; border-radius: 4px; box-shadow: inset 0 1px 1px rgb(0 0 0 / 8%); width: 100%; @@ -678,7 +678,7 @@ article { border: 1px solid rgba(0,0,0,.2); } -.strain-filter { +.chk-select-filter { width: 100%; font-weight: 400; border-top:0px; @@ -690,10 +690,3 @@ article { padding-right: 5px; border-bottom: 1px solid rgba(0,0,0,.2); } - -.variant-browser-navtab { - font-family: 'Roboto', sans-serif; - font-size: 25px; - padding-top: 20px; - padding-bottom: 20px; -} diff --git a/base/templates/gbrowser.html b/base/templates/gbrowser.html index 30700878..e66a058b 100644 --- a/base/templates/gbrowser.html +++ b/base/templates/gbrowser.html @@ -119,21 +119,27 @@
    Tracks -
    Gene Search
    +
    Gene Search
    - - - - - - - - - - - - - + + + + + + + + + + + + +
    @@ -142,17 +148,13 @@
    Gene Search
    Isotype (reference strain)
    - -
    - - {% for strain in strain_listing %} - + {% for strain in strain_listing %}
    {{ strain.isotype }} ({{ strain.strain }}) @@ -168,9 +170,7 @@
    Isotype (reference strain) +{% block style %} + +{% endblock %} - {#- Tab panes -#} +{% block content %} -
    -
    +
    +
    -
    + {# search boxes row #} +
    -

    - Search by WBGeneID, alphanumeric name (F37A4.8), or gene name (isw-1) -

    - -
    -
    - -
    -
    + {# Gene Search Block #} +
    -
    - +

    +
    +
    +
    +
    + Search by WBGeneID, alphanumeric name (F37A4.8), or gene name (isw-1) + + + + + + + + + + + + +
    {# /col-md-8 #} + + {# Interval Search Block #} +
    +

    +
    +
    + +
    +
    + Search using the format [chromosome:START-STOP] +
    + +
    +
    {# /col-md-4 #} + +
    {# /row #} +
    {# /col-md-8 #} + + {# Column select list block #} +
    +
    Columns
    + +
    +
    +
    +
    +
    + + +
    +
    + {% for col in columns %} +
    + + +
    + {% endfor %} +
    +
    +
    {# /col-list #} +
    {# /col-md-2 #} -
    + {# strain select list block #} +
    +
    Strains
    -

    - Search using the format [chromosome:START-STOP] -

    +
    + -
    -
    - + +
    +
    +
    + + +
    +
    + {% for strain in strain_listing %} +
    + + +
    + {% endfor %}
    - - - -
    -
    + +
    {# /strain-list #} +
    {# /col-md-2 #} -
    -
    {# /tabcontent #} -
    {# /col-md-4 #} +
    {# /row #} + +
    +
    + + + + +
    {# /col-md-12 #} +
    {# /row #} -{# strain deselect list #} -
    -
    -
    -

    Selected Strains

    - -
    - - - -
    -
    -
    -
    - - -
    -
    - {% for strain in strain_listing %} - - {% endfor %} -
    -
    -
    +{% endblock %} -
    +{% block script %} + + + {% endblock %} @@ -22,6 +24,7 @@ mark { background-color: inherit; + font-size: 8px; } .hl-mark { @@ -75,16 +78,16 @@

    - + +
    + Search using the format [chromosome:START-STOP] +
    +
    - Search using the format [chromosome:START-STOP] -
    - -
    {# /col-md-4 #}
    {# /row #} @@ -157,7 +160,7 @@
    Strains
    {# / col-md-2 #} + + + + +
    {# /row #} + + + + +
    {# /container-fluid #} - + @@ -14,7 +14,13 @@ - + + + + + + + {# Hands on Table #} diff --git a/base/templates/_includes/navbar.html b/base/templates/_includes/navbar.html index 36bd4e38..ac31a533 100644 --- a/base/templates/_includes/navbar.html +++ b/base/templates/_includes/navbar.html @@ -1,116 +1,107 @@
    -
    - CeNDR - -
    +
    + {# /col-md-2 #} +
    {# /row #} + +
    diff --git a/base/templates/_layouts/default.html b/base/templates/_layouts/default.html index e78b1ec6..a9049e7e 100644 --- a/base/templates/_layouts/default.html +++ b/base/templates/_layouts/default.html @@ -29,27 +29,25 @@

    {{ title }} {% if subtitle %} {{ subtitle }}
  • Home
  • - {% if title %} - {% if title.lower() != request.blueprint.lower() %} + + {% if request.blueprint != "primary" %} + {% if disable_parent_breadcrumb %} +
  • {{ request.blueprint|title }}
  • + {% else %} +
  • {{ request.blueprint|title }}
  • + {% endif %} {% endif %} - {% if request.blueprint == 'user' and is_logged_in%} - {# Direct to user profile #} -
  • Profile
  • - {% else %} - {% if request.blueprint != "primary" %} -
  • {{ request.blueprint|title }}
  • - {% endif %} - {% endif %} + {% if title %} +
  • {{ title }}
  • {% endif %} {% if subtitle %} -
  • {{ title }}
  • -
  • {{ subtitle }}
  • - {% else %} -
  • {{ title }}
  • +
  • {{ subtitle }}
  • {% endif %} + {% endif %}

    @@ -74,10 +72,10 @@

    {{ title }} {% if subtitle %} {{ subtitle }} - {% include "_includes/footer.html" %} {% if config.DEBUG %}{{ session }}{% endif %} + {% include "_includes/footer.html" %} \ No newline at end of file diff --git a/base/templates/about/about.html b/base/templates/about/about.html index a256179c..ec8722c8 100644 --- a/base/templates/about/about.html +++ b/base/templates/about/about.html @@ -1,84 +1,110 @@ {% extends "_layouts/default.html" %} -{% block content %} -
    -
    - - -
    -Movie of C. elegans taken by the Goldstein Lab -
    - -
    -
    - -{% filter markdown %} - -### What is _C. elegans_? - -_Caenorhabditis elegans_ is a non-parasitic nematode roundworm that lives in rotting material and eats bacteria and fungi. Because this species grows easily and quickly in the laboratory, it is a powerful model to learn about human development, complex behaviors, and evolutionary processes. For more information about _C. elegans_, [see this wikipedia page](https://en.wikipedia.org/wiki/Caenorhabditis_elegans) or learn about its history at [wormclassroom.org](http://wormclassroom.org/short-history-c-elegans-research). - -{% endfilter %} -
    -
    -
    - -
    - - - -

    Global Distribution of wild isolates

    -

    Most research groups that study C. elegans focus on the laboratory-adapted strain (called N2) isolated in Bristol, England in the 1950s. We have learned a great deal about basic biological processes from studies of this one strain.

    -

    -However, this species is found worldwide, and wild strains are as different from one another as humans are different from one another. These strains are isolated from a variety of environments in nature when researchers collect rotting materials, including fruits, flowers, nuts, berries, stems, leaves, and compost. We can use the natural diversity of these strains to learn about how populations of individuals are genetically different from another and how those differences might impact disease.

    - -
    - - -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    - +{% block custom_head %} -
    + + -

    CeNDR Goals

    -

    To facilitate the study of natural diversity by C. elegans research groups, we created the C. elegans Natural Diversity Resource (CeNDR). We have three major goals:

    +{% endblock %} -
      -
    1. To accept, organize, and distribute wild strains to research groups that want to investigate their favorite trait(s) across natural C. elegans strains. See Strains.
    2. -
    3. To sequence the whole genomes of wild C. elegans strains, provide the aligned sequence data, and facilitate discovery of genetic variation across the entire species. See Data.
    4. -
    5. To perform genome-wide association mappings to correlate genotype with phenotype and identify genetic variation underlying quantitative traits.
    6. -
    +{% block content %} -

    Please let us know what we can do to facilitate your discoveries! We are interested in adding new resources and tools.

    +
    + +
    +
    + +
    Movie of C. elegans taken by the Goldstein Lab
    +
    +
    {# /col-md-6 #} + +
    +

    What is C. elegans?

    +

    + Caenorhabditis elegans is a non-parasitic nematode roundworm that lives in rotting material and eats bacteria and fungi. + Because this species grows easily and quickly in the laboratory, it is a powerful model to learn about human development, complex behaviors, + and evolutionary processes. For more information about C. elegans + see this wikipedia page or learn about its history at + wormclassroom.org +

    +
    {# /col-md-6 #} + +
    {# /row #} + +
    + +
    +
    + +
    +
    {# /col-md-6 #} + +
    +

    Global Distribution of wild isolates

    +

    + Most research groups that study C. elegans focus on the laboratory-adapted strain (called N2) isolated in + Bristol, England in the 1950s. We have learned a great deal about basic biological processes from studies of this one strain. +

    +

    + However, this species is found worldwide, and wild strains are as different from one another as humans are different from one another. + These strains are isolated from a variety of environments in nature when researchers collect rotting materials, including fruits, + flowers, nuts, berries, stems, leaves, and compost. We can use the natural diversity of these strains to learn about how populations + of individuals are genetically different from another and how those differences might impact disease. +

    +
    {# /col-md-6 #} + +
    {# /row #} + +
    + +
    + +
    {# /col-md-3 #} + +
    + +
    {# /col-md-3 #} + +
    +

    CeNDR Goals

    +

    + To facilitate the study of natural diversity by C. elegans research groups, we created the C. elegans + Natural Diversity Resource (CeNDR). We have three major goals: +

    + +
      +
    1. + To accept, organize, and distribute wild strains to research groups that want to investigate their favorite trait(s) + across natural C. elegans strains. See Strains. +
    2. +
    3. + To sequence the whole genomes of wild C. elegans strains, provide the aligned sequence data, and facilitate discovery + of genetic variation across the entire species. See Data. +
    4. +
    5. + To perform genome-wide association mappings to correlate genotype with phenotype and identify genetic variation underlying quantitative traits. +
    6. +
    + +

    + Please let us know what we can do to facilitate your discoveries! We are interested in + adding new resources and tools. +

    + +
    {# /col-md-6 #} + +
    {# /row #} -
    -
    {% endblock %} @@ -86,45 +112,50 @@

    CeNDR Goals

    {% block script %} {% endblock %} \ No newline at end of file diff --git a/base/templates/about/donate.html b/base/templates/about/donate.html index 2bea82de..6c9aa147 100644 --- a/base/templates/about/donate.html +++ b/base/templates/about/donate.html @@ -3,9 +3,11 @@ {% from "macros.html" import render_field %}
    +
    {{ render_markdown("donate.md") }} -
    +
    {# /col-md-6 #} +
    Donate
    @@ -19,9 +21,9 @@ {{ form.recaptcha }}
    - + +
    - - - + {# /col-md-6 #} + {# /row #} {% endblock %} \ No newline at end of file diff --git a/base/templates/about/funding.html b/base/templates/about/funding.html index 5c9eea9f..99f3379e 100644 --- a/base/templates/about/funding.html +++ b/base/templates/about/funding.html @@ -1,12 +1,16 @@ {% extends "_layouts/default.html" %} {% block content %} +
    {% for funder in funding_set %} -
    - - {{ funder.name }} -
    - {{ funder.description }} -
    + {% endfor %} +
    {# /row #} {% endblock %} \ No newline at end of file diff --git a/base/templates/about/statistics.html b/base/templates/about/statistics.html index fa3fcebb..26af98b3 100644 --- a/base/templates/about/statistics.html +++ b/base/templates/about/statistics.html @@ -7,45 +7,42 @@ {% block content %}
    -
    -

    Strains collected over time

    - {{ strain_collection_plot|safe }} -
    -
    -
    -
    -

    Numbers

    -
    - +
    +

    Strains collected over time

    + {{ strain_collection_plot|safe }} +
    {# /col-md-6 #} + +
    +
    +
    - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + -
    Strains{{ n_strains }}
    Isotypes{{ n_isotypes }}
    Unique Mapping Pipeline Users{{ n_users }}
    Reports{{ n_reports }}
    Traits{{ n_traits }}
    Strains{{ n_strains }}
    Isotypes{{ n_isotypes }}
    Unique Mapping Pipeline Users{{ n_users }}
    Reports{{ n_reports }}
    Traits{{ n_traits }}
    -
    - +

    +
    {# /col-md-6 #} -
    +
    {# /row #}
    diff --git a/base/templates/contact.html b/base/templates/contact.html index fda3af95..310ea6df 100644 --- a/base/templates/contact.html +++ b/base/templates/contact.html @@ -1,49 +1,83 @@ {% extends "_layouts/default.html" %} -{% block title_right %} -{% endblock %} {% block content %}
    -
    -
    -
    Associate Professor Erik Andersen
    -
    - - Erik.Andersen@northwestern.edu
    - phone - - -
    -
    - - -
    Lab Phone
    - phone - - -

    - - -
    {#/ col-xs-12 #} - -
    -

    Administrative Address

    -
    - Northwestern University
    Department of Molecular Biosciences

    - 2205 Tech Drive, Hogan 2-100
    - Evanston, IL. 60208-3500
    -
    - -

    Shipping Address

    -
    - Erik Andersen
    Northwestern University
    Department of Molecular Biosciences

    - 2205 Tech Dr, Hogan 1-500
    - Evanston, IL. 60208-3500
    -
    -
    -
    +
    +
    + +
    + +
    +
    Associate Professor Erik Andersen
    +
    + + + Erik.Andersen@northwestern.edu + +
    + phone + + +
    +
    +
    {# /col-md-12#} + + +
    + +
    +
    Lab Phone
    +
    + phone + + +
    +
    +
    {#/ col-xs-12 #} + + + + + + +
    {# /row #} +
    {# /col-lg-4 #} + +
    + +
    +
    Administrative Address
    +
    +
    + Northwestern University
    Department of Molecular Biosciences

    + 2205 Tech Drive, Hogan 2-100
    + Evanston, IL. 60208-3500
    +
    +
    +
    +
    {#/ col-md-12 #} + + +
    + +
    +
    Shipping Address
    +
    +
    + Erik Andersen
    Northwestern University
    Department of Molecular Biosciences

    + 2205 Tech Dr, Hogan 1-500
    + Evanston, IL. 60208-3500
    +
    +
    +
    +
    {#/ col-xs-12 #} + + + +
    {# /row #} +
    - +
    +
    {% endblock %} diff --git a/base/templates/strain/strains_list.html b/base/templates/strain/strains_list.html index c278d5e6..486aaac5 100644 --- a/base/templates/strain/strains_list.html +++ b/base/templates/strain/strains_list.html @@ -1,52 +1,39 @@ {% extends "_layouts/default.html" %} {% block content %} -
    -
    -
    -
    - -
    {# /col-md-4 #} + +
    +
    +
    + +
    {# /col-md-4 #}
    {# /row #} +
    - +
    - - - + - + - - + + + {% for isotype, strains in strain_listing|groupby('isotype') %} - - {% if strains[0].previous_names %} - - {% else %} - - {% endif %} + {% if strains[0].previous_names %} + + {% else %} + + {% endif %} {% endfor %}
    - # - - + Reference Strain - - - + Isotype - - - + Strains - - - - Alternative Names - - - + Release - - + Alternative Names +
    {{ loop.index }} {% set isotype_loop_index = loop.index %} @@ -57,20 +44,19 @@ {{ strains|join(", ") }} {{ strains[0].previous_names }} {{ strains[0]['release'] }} {{ strains[0].previous_names.replace(',', '|').split('|') | join(', ') }}
    {# /row #} -
    {# /tabpanel #} {% endblock %} diff --git a/base/templates/tools/h2_result_list.html b/base/templates/tools/h2_result_list.html index 0544dd53..32e70dae 100644 --- a/base/templates/tools/h2_result_list.html +++ b/base/templates/tools/h2_result_list.html @@ -27,8 +27,11 @@ {{ render_dataTable_top_menu() }}
    +
    - + +
    + @@ -61,8 +64,8 @@
    Label
    -
    -
    +
    {# /col #} +
    {# /row #} {% endblock %} @@ -76,12 +79,13 @@ const dTable = $('#h2-table').DataTable( { paging: true, pageLength: 25, + autoWidth: true, aaSorting: [ [3,'desc'] ], aoColumns: [ - null, - null, - null, - null + {"width": "25%"}, + {"width": "25%"}, + {"width": "25%"}, + {"width": "25%"} ], dom:"tipr" }); diff --git a/base/templates/tools/heritability_calculator.html b/base/templates/tools/heritability_calculator.html index 983a80db..bb311a71 100644 --- a/base/templates/tools/heritability_calculator.html +++ b/base/templates/tools/heritability_calculator.html @@ -6,77 +6,73 @@ {% block content %} - -
    -
    -

    - This tool will calculate the broad-sense heritability for your trait of interest using a set of C. elegans wild - isolates. The broad-sense heritability is the amount of trait variance that comes from genetic differences in the - assayed group of strains. Generally, it is the ratio of genetic variance to total (genetic plus environmental) - variance. -

    -

    - To obtain the best estimate of heritability, please measure a set of at least five wild strains in three - independent assays. These assays should use different nematode growths, synchronizations, bacterial food - preparations, and any other experimental condition. You should measure trait variance across as many - different experimental conditions (in one block) as you would typically encounter in a large experiment. -

    -

    - Please organize your data in a long format, where each row is an independent observation of one strain in one - trait. The columns of the data set should be: -

    -
      -
    1. AssayNumber - a numeric indicator of independent assays.
    2. -
    3. Strain - one of the CeNDR isotype reference strain names.
    4. -
    5. TraitName - a user-supplied name of a trait with no spaces (e.g. BroodSize).
    6. -
    7. Replicate - independent measures of a trait within one independent assay. You - can think of this column as a numerical value for a technical replicate.
    8. -
    9. Value - the measured output of the trait (e.g. 297 for BroodSize).
    10. -
    - -

    NA values will not be used in broad-sense heritability calculations.

    - -
    {# /col-md-12 #} -
    {# /row #} +
    + +
    +
    +

    + This tool will calculate the broad-sense heritability for your trait of interest using a set of C. elegans wild + isolates. The broad-sense heritability is the amount of trait variance that comes from genetic differences in the + assayed group of strains. Generally, it is the ratio of genetic variance to total (genetic plus environmental) variance. +

    +

    + To obtain the best estimate of heritability, please measure a set of at least five wild strains in three + independent assays. These assays should use different nematode growths, synchronizations, bacterial food + preparations, and any other experimental condition. You should measure trait variance across as many + different experimental conditions (in one block) as you would typically encounter in a large experiment. +

    +

    + Please organize your data in a long format, where each row is an independent observation of one strain in one trait. The columns of the data set should be: +

    +
      +
    1. AssayNumber - a numeric indicator of independent assays.
    2. +
    3. Strain - one of the CeNDR isotype reference strain names.
    4. +
    5. TraitName - a user-supplied name of a trait with no spaces (e.g. BroodSize).
    6. +
    7. Replicate - independent measures of a trait within one independent assay. You + can think of this column as a numerical value for a technical replicate.
    8. +
    9. Value - the measured output of the trait (e.g. 297 for BroodSize).
    10. +
    + +

    NA values will not be used in broad-sense heritability calculations.

    + +
    {# /col-md-12 #} +
    {# /row #} {% if hide_form == True %}
    -
    {# /col-md-3 #} -
    + {# /col-md-3 #} -
    + {# /col-md-3 #} -
    {# /col-md-3 #}
    {# /row #} -{% else %} +
    {# /well #} -
    +{% else %} +
    {# /well #} -
    -
    {#/ col-md-3 #} -
    {# /col-md-3 #} +
    -
    + {# /col-md-3 #} -
    + {# /row #}
    +
    {% if data %}
    {% endif %} +
    {# /row #}
    @@ -88,21 +90,20 @@

    {% endif %}

    {# /row #} -
    -
    -
    +
    + -
    + -
    +
    Download PDF diff --git a/base/templates/tools/indel_primer.html b/base/templates/tools/indel_primer.html index 360a4a2c..9b3a99af 100644 --- a/base/templates/tools/indel_primer.html +++ b/base/templates/tools/indel_primer.html @@ -11,55 +11,76 @@ {% block content %} {% from "macros.html" import render_field %} -
    -
    -

    - This web-based tool is designed to compare any two wild C. elegans strains for insertion-deletion (indel) variants that can be genotyped using PCR. - These molecular markers can be used to follow each respective genetic background in crosses. -

    - -

    - Enter a specific genomic region. The browser will show indel variants between your two chosen strains. - The table will show regions to search for primers. Primers might not be found flanking some indel sites. - Click on additional indel sites to find one that has good quality primers. - For each of these indels, you can search for primers to genotype them between your two chosen strains using differential PCR product sizes. - Primers are designed to avoid natural variants in either strain to ensure that the PCR works in both genetic backgrounds. -

    - -

    - The browser also shows divergent regions, where the reference genome and some wild isolates have sequences with many variants. - These regions should be avoided because indel calls are less reliable and high levels of variation will make primer searches more error-prone. -

    -
    -
    -
    -
    {# /container #} + +
    + +
    {{ form.csrf_token }} -
    {{ render_field(form.strain_1) }}
    -
    {{ render_field(form.strain_2) }}
    -
    {{ render_field(form.chromosome) }}
    -
    {{ render_field(form.start, placeholder="2,028,824") }}
    -
    {{ render_field(form.stop, placeholder="2,029,217") }}
    -
    +
    {{ render_field(form.strain_1) }}
    +
    {{ render_field(form.strain_2) }}
    +
    {{ render_field(form.chromosome) }}
    +
    {{ render_field(form.start, placeholder="2,028,824") }}
    +
    {{ render_field(form.stop, placeholder="2,029,217") }}
    +

    -
    -
    -
    -
    -
    +
    {# /container #} + + + +
    + +
    +
    +
    +
    {# /col #} +
    {# /row #} + +
    +
    +
    +
    {# /col #} +
    {# /row #} + +
    {# /container-fluid #} + + {% endblock %} diff --git a/base/templates/tools/indel_primer_results.html b/base/templates/tools/indel_primer_results.html index 7bf7848d..00bc8687 100644 --- a/base/templates/tools/indel_primer_results.html +++ b/base/templates/tools/indel_primer_results.html @@ -69,12 +69,10 @@ {% if data and ready %} {% if empty %} -
    +
    -

    No Results

    -

    - Unfortunately, no primers could be found for this site. -

    +

    No Results

    + Unfortunately, no primers could be found for this site.
    {# /col-md-4 #}
    {# /row #} {% else %} diff --git a/base/templates/tools/ip_result_list.html b/base/templates/tools/ip_result_list.html index 4b2a02f1..ed2a75f6 100644 --- a/base/templates/tools/ip_result_list.html +++ b/base/templates/tools/ip_result_list.html @@ -28,15 +28,15 @@
    - +
    - - - - - - + + + + + + @@ -86,12 +86,12 @@ pageLength: 25, aaSorting: [ [5,'desc'] ], aoColumns: [ - null, - null, - null, - null, - null, - null + {"width":"25%"}, + {"width":"13%"}, + {"width":"13%"}, + {"width":"9%"}, + {"width":"15%"}, + {"width":"25%"} ], dom:"tipr" }); diff --git a/base/templates/vbrowser.html b/base/templates/vbrowser.html index f6ac42d7..86f8a848 100644 --- a/base/templates/vbrowser.html +++ b/base/templates/vbrowser.html @@ -31,19 +31,17 @@ background-color: yellow; } + {% endblock %} {% block content %} -
    -
    - - {# search boxes row #} -
    +
    +
    {# Gene Search Block #} -
    +

    @@ -52,7 +50,7 @@

    Search by WBGeneID, alphanumeric name (F37A4.8), or gene name (isw-1) -
    Site Strain 1 Strain 2 Empty Status Date Site Strain 1 Strain 2 Empty Status Date
    + @@ -71,31 +69,29 @@

    -
    {# /col-md-8 #} +
    {# /col-lg-4 #} {# Interval Search Block #} -
    +

    Search using the format [chromosome:START-STOP] -
    +
    -
    {# /col-md-4 #} +
    {# /col-lg-4 #} -
    {# /row #} -
    {# /col-md-8 #} {# Column select list block #} -
    -
    Columns
    +
    +

    Columns

    @@ -119,8 +115,8 @@
    Columns
    {# /col-md-2 #} {# strain select list block #} -
    -
    Strains
    +
    +

    Strains

    @@ -148,9 +144,9 @@
    Strains
    {# /row #} +
    {# /container #}
    -
    - -
    {# /col-md-12 #}
    {# /row #} @@ -209,7 +204,7 @@
    Strains
    $("#loading-search-table").fadeOut(); var gene = $('#gene-search').val(); if (gene.length == 0) { - $("#search-table").fadeOut(); + $("#v-search-table").fadeOut(); } else { $("#orthologs").html(""); $.ajax({ @@ -222,10 +217,10 @@
    Strains
    const position = row["chrom"] + ":" + row["start"] + "-" + row["end"]; const link = `${row['gene_id']}`; const result = [row['homolog_gene'] || row["gene_symbol"], row['sequence_name'], link] - const table_data = "" + result.join("") + ""; + const table_data = "" + result.join("").slice(0,-4) + ""; $("#orthologs").append(table_data); }); - $("#search-table").fadeIn(); + $("#v-search-table").fadeIn(); }); } } @@ -330,7 +325,6 @@
    Strains
    colReorder: true, destroy: true, paging: true, - scrollX: true, pageLength: 100, dom:"ltipr", columnDefs: [ diff --git a/base/views/about.py b/base/views/about.py index 14acc22c..2a3d6f75 100644 --- a/base/views/about.py +++ b/base/views/about.py @@ -32,7 +32,8 @@ def about(): """ About us Page - Gives an overview of CeNDR """ - title = "About" + title = "About CeNDR" + disable_parent_breadcrumb = True strain_listing = get_isotypes(known_origin=True) return render_template('about/about.html', **locals()) @@ -43,9 +44,10 @@ def getting_started(): Getting Started - provides information on how to get started with CeNDR """ - VARS = {"title": "Getting Started", - "strain_listing": get_isotypes(known_origin=True)} - return render_template('about/getting_started.html', **VARS) + title = "Getting Started" + strain_listing = get_isotypes(known_origin=True) + disable_parent_breadcrumb = True + return render_template('about/getting_started.html', **locals()) @about_bp.route('/committee/') @@ -54,12 +56,14 @@ def committee(): Scientific Panel Page """ title = "Scientific Advisory Committee" + disable_parent_breadcrumb = True committee_data = load_yaml("advisory-committee.yaml") return render_template('about/committee.html', **locals()) @about_bp.route('/collaborators/') def collaborators(): title = "Collaborators" + disable_parent_breadcrumb = True collaborator_data = load_yaml("collaborators.yaml") return render_template('about/collaborators.html', **locals()) @@ -70,6 +74,7 @@ def staff(): Staff Page """ title = "Staff" + disable_parent_breadcrumb = True staff_data = load_yaml("staff.yaml") return render_template('about/staff.html', **locals()) @@ -81,6 +86,7 @@ def donate(): Process donation """ title = "Donate" + disable_parent_breadcrumb = True form = donation_form(request.form) # Autofill email @@ -112,6 +118,7 @@ def donate(): @about_bp.route('/funding/') def funding(): title = "Funding" + disable_parent_breadcrumb = True funding_set = load_yaml('funding.yaml') return render_template('about/funding.html', **locals()) @@ -167,6 +174,7 @@ def statistics(): n_users = get_unique_users() VARS = {'title': title, + 'disable_parent_breadcrumb': True, 'strain_collection_plot': strain_collection_plot, 'report_summary_plot': report_summary_plot, 'weekly_visits_plot': weekly_visits_plot, @@ -185,6 +193,7 @@ def publications(): List of publications that have referenced CeNDR """ title = "Publications" + disable_parent_breadcrumb = True csv_prefix = config['GOOGLE_SHEET_PREFIX'] sheet_id = config['CENDR_PUBLICATIONS_STRAIN_SHEET'] csv_export_suffix = 'export?format=csv&id={}&gid=0'.format(sheet_id) diff --git a/base/views/auth/auth.py b/base/views/auth/auth.py index 1e31aea0..fb0d4d83 100644 --- a/base/views/auth/auth.py +++ b/base/views/auth/auth.py @@ -54,7 +54,8 @@ def choose_login(error=None): @auth_bp.route("/login/basic", methods=["GET", "POST"]) def basic_login(): - page_title = "Login" + title = "Login" + disable_parent_breadcrumb = True form = basic_login_form(request.form) if request.method == 'POST' and form.validate(): username = slugify(request.form.get("username")) diff --git a/base/views/data.py b/base/views/data.py index e2180d2e..81e7b2dd 100644 --- a/base/views/data.py +++ b/base/views/data.py @@ -208,7 +208,7 @@ def gbrowser(release=config["DATASET_RELEASE"], region="III:11746923-11750250", 'strain_listing': get_isotypes(), 'region': region, 'query': query, - 'fluid_container': False} + 'fluid_container': True} return render_template('gbrowser.html', **VARS) @@ -223,6 +223,7 @@ def vbrowser(): form = vbrowser_form() strain_listing = query_strains() columns = StrainAnnotatedVariants.column_details + fluid_container = True return render_template('vbrowser.html', **locals()) diff --git a/base/views/primary.py b/base/views/primary.py index 7ac29337..f8b27c18 100644 --- a/base/views/primary.py +++ b/base/views/primary.py @@ -31,6 +31,7 @@ def primary(): files = sorted_files("base/static/content/news/") VARS = {'page_title': page_title, 'files': files, + 'fluid_container': True, 'latest_mappings': get_latest_public_mappings()} return render_template('primary/home.html', **VARS) diff --git a/base/views/strains.py b/base/views/strains.py index 1dc66c63..39eeae9f 100644 --- a/base/views/strains.py +++ b/base/views/strains.py @@ -74,10 +74,8 @@ def external_links(): """ Strain issues shows latest data releases table of strain issues """ - VARS = { - 'title': 'External Links' - } - return render_template('strain/external_links.html', **VARS) + title = 'External Links' + return render_template('strain/external_links.html', **locals()) # @@ -143,7 +141,7 @@ def isotype_page(isotype_name, release=config['DATASET_RELEASE']): @strains_bp.route('/catalog', methods=['GET', 'POST']) @cache.memoize(50) def strains_catalog(): - flash(Markup("Strain mapping sets 7 and 8 will not be available until later this year."), category="warning") + flash(Markup("Strain mapping sets 9 and 10 will not be available until later this year."), category="warning") VARS = {"title": "Strain Catalog", "warning": request.args.get('warning'), "strain_listing": get_strains(), diff --git a/base/views/tools/indel_primer.py b/base/views/tools/indel_primer.py index c7096a5c..a5d4784a 100644 --- a/base/views/tools/indel_primer.py +++ b/base/views/tools/indel_primer.py @@ -125,6 +125,7 @@ def indel_primer(): VARS = {"title": "Pairwise Indel Finder", "strains": SV_STRAINS, "chroms": CHROM_NUMERIC.keys(), + "fluid_container": True, "form": form} return render_template('tools/indel_primer.html', **VARS) From d803a2fecef169ce4c8739f4d1aa374573467ed6 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Thu, 20 May 2021 15:21:41 -0500 Subject: [PATCH 191/288] fix buttons --- base/templates/primary/home.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/base/templates/primary/home.html b/base/templates/primary/home.html index b5cd55f3..27c27661 100644 --- a/base/templates/primary/home.html +++ b/base/templates/primary/home.html @@ -37,28 +37,28 @@

    The Caenorhabditis elegans Natural Diversity Resource
    pro

    Collection, maintenance, and distribution of wild strains

    - + Strains
    {# /col-lg-2 #}

    Organization and dissemination of whole-genome sequences and variant data

    - + Data
    {# /col-lg-2 #}

    Facilitation of genetic mappings to empower researchers in quantitative genetics

    - + Mapping
    {# /col-lg-2 #}

    Utilities to enable the study of natural variation from genotyping to heritability

    - + Tools
    {# /col-lg-2 #} From 55c8e19792b37da438519a4eaab0ddbc0aa727ce Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Thu, 20 May 2021 15:22:46 -0500 Subject: [PATCH 192/288] stylesheet --- base/static/css/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/static/css/styles.css b/base/static/css/styles.css index c6ed02c0..913d29d0 100644 --- a/base/static/css/styles.css +++ b/base/static/css/styles.css @@ -836,7 +836,7 @@ article { max-height: 150px; } -.welcome-col button { +.welcome-col-btn { width: 50%; margin: 1.0rem; } From b08388c018fbcedd69e7c1046ef2ac432c5075b0 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Thu, 20 May 2021 15:25:47 -0500 Subject: [PATCH 193/288] fix other btn --- base/templates/primary/home.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/templates/primary/home.html b/base/templates/primary/home.html index 27c27661..7228f916 100644 --- a/base/templates/primary/home.html +++ b/base/templates/primary/home.html @@ -9,7 +9,7 @@

    Welcome to the Caenorhabditis elegans Natural Diversity Resource

    - + Get Started

    {# /col-md-6 #} From f26fc7423287ffd7cfe6cdc5f3a889604196d4a4 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Thu, 20 May 2021 15:41:14 -0500 Subject: [PATCH 194/288] add styles update --- base/static/css/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/static/css/styles.css b/base/static/css/styles.css index 913d29d0..a2474eb9 100644 --- a/base/static/css/styles.css +++ b/base/static/css/styles.css @@ -982,4 +982,4 @@ article { .instruction-well { background-color: #B6ACD1; -} \ No newline at end of file +} From c9030482bd28a9355b242ca1cd0ae72e8a1327a9 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Thu, 20 May 2021 15:49:09 -0500 Subject: [PATCH 195/288] travis++ --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 996e93d1..3b91e55c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ env: install: - openssl aes-256-cbc -K $encrypted_86f5a1ab1ccf_key -iv $encrypted_86f5a1ab1ccf_iv -in env_config.zip.enc -out env_config.zip -d - unzip -qo env_config.zip -- export VERSION_NUM=9-9-9-1 +- export VERSION_NUM=9-9-9-2 - export APP_CONFIG=development - export CLOUD_CONFIG=1 - if [ "${TRAVIS_BRANCH}" != "master" ]; then export APP_CONFIG=development; fi; From a577943f17882c5f7bf8240abd05bfb93832efb8 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Thu, 27 May 2021 09:45:37 -0500 Subject: [PATCH 196/288] add nemascan task queue --- queue.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/queue.yaml b/queue.yaml index 404fc1c6..eb30f534 100644 --- a/queue.yaml +++ b/queue.yaml @@ -15,4 +15,10 @@ queue: task_retry_limit: 2 min_backoff_seconds: 10 max_backoff_seconds: 60 - max_doublings: 2 \ No newline at end of file + max_doublings: 2 + +- name: nscalc + max_concurrent_requests: 1 + rate: 1/s + retry_parameters: + task_retry_limit: 1 From c42c119ff4e09c710c558e58061dfcd8d08e2654 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Thu, 27 May 2021 09:46:27 -0500 Subject: [PATCH 197/288] remove logging --- base/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/config.py b/base/config.py index 1565dd76..97ba6ec7 100644 --- a/base/config.py +++ b/base/config.py @@ -49,7 +49,7 @@ def get_config(APP_CONFIG): DB_PASS = APP_CONFIG_VARS['PSQL_DB_PASSWORD'] CONNECTION = APP_CONFIG_VARS['PSQL_DB_CONNECTION_NAME'] DB = APP_CONFIG_VARS['PSQL_DB_NAME'] - logger.info('WHY IS THIS OUT OF DATE') + config.update(BASE_VARS) config.update(APP_CONFIG_VARS) From 187641a16deb5219f308fc0b53dc6ccc6748bb0a Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Thu, 27 May 2021 09:46:40 -0500 Subject: [PATCH 198/288] file upload form stub --- base/forms.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/base/forms.py b/base/forms.py index 2b2390a1..dd16581e 100644 --- a/base/forms.py +++ b/base/forms.py @@ -15,9 +15,16 @@ FieldList, HiddenField, RadioField) + from wtforms.fields.simple import PasswordField -from wtforms.validators import Required, Length, Email, DataRequired, EqualTo, Optional -from wtforms.validators import ValidationError +from wtforms.validators import (Required, + Length, + Email, + DataRequired, + EqualTo, + Optional, + ValidationError) + from wtforms.fields.html5 import EmailField from base.constants import PRICES, USER_ROLES, SHIPPING_OPTIONS, PAYMENT_OPTIONS @@ -35,6 +42,10 @@ class MultiCheckboxField(SelectMultipleField): option_widget = widgets.CheckboxInput() +class file_upload_form(FlaskForm): + pass + + class basic_login_form(FlaskForm): """ The simple username/password login form From 8361a21cb7ce551c817af15ca5c4de7c4347d1e4 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Thu, 27 May 2021 09:47:03 -0500 Subject: [PATCH 199/288] add nemascan task datastore kind --- base/models.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/base/models.py b/base/models.py index 86cdc191..e26d4aeb 100644 --- a/base/models.py +++ b/base/models.py @@ -384,6 +384,30 @@ def save(self, *args, **kwargs): super(markdown_ds, self).save(*args, **kwargs) +class ns_calc_ds(datastore_model): + """ + The NemaScan Task Model - metadata for NemaScan nextflow pipeline + execution tasks executed by Google Life Sciences + """ + kind = 'ns_calc' + kind = '{}{}'.format(config['DS_PREFIX'], kind) + + + def __init__(self, *args, **kwargs): + super(ns_calc_ds, self).__init__(*args, **kwargs) + + def query_by_username(self, username, keys_only=False): + filters = [('username', '=', username)] + results = query_item(self.kind, filters=filters, keys_only=keys_only) + return results + + def save(self, *args, **kwargs): + now = arrow.utcnow().datetime + self.modified_on = now + if not self._exists: + self.created_on = now + super(ns_calc_ds, self).save(*args, **kwargs) + class h2calc_ds(datastore_model): """ From 9b18416928a14f1473c5224baaf193a469259f4c Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Thu, 27 May 2021 09:48:00 -0500 Subject: [PATCH 200/288] add gcloud api to upload file objects as blobs --- base/utils/gcloud.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/base/utils/gcloud.py b/base/utils/gcloud.py index 174ab096..904de91a 100644 --- a/base/utils/gcloud.py +++ b/base/utils/gcloud.py @@ -162,21 +162,27 @@ def get_cendr_bucket(): return gs.get_bucket(GOOGLE_CLOUD_BUCKET) -def upload_file(blob, obj, as_string = False): +def upload_file(blob, obj, as_string = False, as_file_obj = False): """ - Upload a file to the CeNDR bucket + Upload a file to the CeNDR bucket - Args: - blob - The name of the blob (server-side) - fname - The filename to upload (client-side) + Args: + blob - The name of the blob (server-side) + fname - The filename to upload (client-side) """ logger.info(f"Uploading: {blob} --> {obj}") cendr_bucket = get_cendr_bucket() blob = cendr_bucket.blob(blob) + if as_string: - blob.upload_from_string(obj) - else: - blob.upload_from_filename(obj) + blob.upload_from_string(obj) + return blob + + if as_file_obj: + blob.upload_from_file(obj) + return blob + + blob.upload_from_filename(obj) return blob @@ -312,5 +318,7 @@ def add_task(queue, url, payload, delay_seconds=None, task_name=None): eType = str(type(e).__name__) if eType == 'AlreadyExists': response = 'SCHEDULED' + else: + response = None return response From ef48f089a16b16625945435cee4ea14131c06089 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Thu, 27 May 2021 09:49:12 -0500 Subject: [PATCH 201/288] create mapping file upload page --- base/static/css/styles.css | 9 ++ base/templates/mapping.html | 227 ++++++++++-------------------------- base/views/mapping.py | 153 +++++++++++++----------- 3 files changed, 160 insertions(+), 229 deletions(-) diff --git a/base/static/css/styles.css b/base/static/css/styles.css index a2474eb9..07d6a3c5 100644 --- a/base/static/css/styles.css +++ b/base/static/css/styles.css @@ -972,6 +972,11 @@ article { margin-bottom: 2.0rem; } +.input-margin { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + .row-margin-top { margin-top: 2.0rem; } @@ -983,3 +988,7 @@ article { .instruction-well { background-color: #B6ACD1; } + +.nu-alt-btn { + background-color: #FFA500; +} \ No newline at end of file diff --git a/base/templates/mapping.html b/base/templates/mapping.html index 849806dc..75c6e22e 100644 --- a/base/templates/mapping.html +++ b/base/templates/mapping.html @@ -1,183 +1,82 @@ {% extends "_layouts/default.html" %} {% block content %} -
    - Note
    - The genome-wide association mapping portal is temporarily closed as we update the underlying algorithms, - strain sets, and variant data. Please check out NemaScan as we work to improve C. elegans association mappings. -
    - {% if session.get('user') and False %} - -
    -
    - - {% from "macros.html" import render_field %} - - {{ form.csrf_token }} - - {{ render_field(form.report_name, autocomplete='off') }} - - {{ render_field(form.description, placeholder='In this experiment...') }} - - {{ render_field(form.is_public) }} - - {{ form.trait_data }} - -
    - Reports are now stored in your user profile. -
    - - - Browser Compatibility -

    This submission page may not work with every browser. We have tested this page and found it to work with:

    -
      -
    • Google Chrome
    • -
    • Firefox
    • -
    • Safari
    • -
    -
    - - -
    -
    -
    C. elegans association mapping
    -
    -

    Directions

    -
      -
    1. Enter the required information to the left.
    2. -
    3. Enter strain name and phenotype data below by dragging and dropping from your spreadsheet. Strain names must be approved names from this site. Names in red need to be edited to match strains in the collection.
    4. -
    5. Click submit to map your trait
    6. -
    7. Mappings can take up to 15 minutes. Please be patient. Up to five traits can be mapped in parallel at this time.
    8. -
    9. Contact us with problems, questions, or suggestions.
    10. -
    -
    -

    DISCLAIMER

    -

    Although strains from other sources might have the same name as strains used in this resource, genotypes might be confused or names altered. For best association mapping results, please use the strains created as a part of this resource. All sequenced strains are the same as those sent through this resource. Additionally, association mapping results need to be analyzed with care. Allele frequency skews, small sample sizes, or phenotypes similar to genomic positions can cause spurious mapping results, among other issues. Please be critical of mapping data.

    -
    {# / panel-body #} -
    {# /panel #} -
    {# /col-md-6 #} -
    {# /row #} - - -
    -
    - - -
    - Put strain names in the first column (Column A), and up to five traits in columns B-F.
    -
    - Copy and Paste your dataset into the table above. -
    -
    -
    {# /col-md-12 #} -
    {# /row #} - - -
    -
    - {% if form.trait_data.errors %} -

    ERRORS

    -

    Please fix the errors in your data before submitting again.

    -
      - {% for error in form.trait_data.errors %} -
    • {{ error|safe|e }}
    • - {% endfor %} -
    - {% endif %} -
    {# /col-md-12 #} -
    {# /row #} - -
    -
    -
    -
    {# /col-md-6 #} -
    {# /row #} - -
    -
    -
    -
    - If you are not redirected to a reports page after clicking the submit button, please notify us of the issue via the feedback form. -
    -
    - -
    {#/ col-md-12 #} -
    {#/ row #} - - {% else %} - {#Please login to perform a mapping.#} - {% endif %} -{% endblock %} -{% block script %} +
    -{% if session.get('user') %} - -{% endif %} {% endblock %} diff --git a/base/views/mapping.py b/base/views/mapping.py index 0a9ec672..a54b384d 100644 --- a/base/views/mapping.py +++ b/base/views/mapping.py @@ -5,19 +5,20 @@ import pandas as pd import simplejson as json -from base.constants import BIOTYPES, TABLE_COLORS -from base.models import trait_ds from datetime import date from flask import render_template, request, redirect, url_for, abort from slugify import slugify -from base.forms import mapping_submission_form from logzero import logger from flask import session, flash, Blueprint, g -from base.utils.data_utils import unique_id -from base.config import config -from base.utils.gcloud import query_item, delete_item +from base.constants import BIOTYPES, TABLE_COLORS +from base.config import config +from base.models import trait_ds, ns_calc_ds +from base.forms import file_upload_form +from base.utils.data_utils import unique_id, hash_it +from base.utils.gcloud import check_blob, query_item, delete_item, upload_file, add_task +from base.utils.jwt_utils import jwt_required, get_jwt, get_current_user from base.utils.plots import pxg_plot, plotly_distplot @@ -34,68 +35,90 @@ def default(self, o): return str(o) return super(CustomEncoder, self).default(o) +def create_ns_task(data_hash, ds_id, ds_kind): + """ + Creates a Cloud Task to schedule the pipeline for execution + by the NemaScan service + """ + ns = ns_calc_ds(ds_id) + + # schedule nemascan request + queue = config['NEMASCAN_PIPELINE_TASK_QUEUE'] + url = config['NEMASCAN_PIPELINE_URL'] + data = {'hash': data_hash, 'ds_id': ds_id, 'ds_kind': ds_kind} + result = add_task(queue, url, data, task_name=data_hash) + + # Update report status + ns.status = 'SCHEDULED' if result else 'FAILED' + ns.save() + + +@mapping_bp.route('/upload', methods = ['POST']) +@jwt_required() +def schedule_mapping(): + ''' + Uploads the users file and schedules the nemascan pipeline task + tracking metadata in an associated datastore entry + ''' + form = file_upload_form(request.form) + if not form.validate_on_submit(): + flash("You must include a description of your data and a TSV file to upload", "error") + return redirect(url_for('mapping.mapping')) + + # Store report metadata in datastore + user = get_current_user() + id = unique_id() + ns = ns_calc_ds(id) + ns.label = request.form.get('label') + ns.username = user.name + ns.status = 'NEW' + ns.save() + + # Upload file to cloud bucket + file = request.files['file'] + data_hash = hash_it(file, length=32) + data_blob = f"reports/nemascan/{data_hash}/data.tsv" + # if check_blob(data_blob): + # todo: handle file already existing + + result = upload_file(data_blob, file, as_file_obj=True) + if not result: + ns.status = 'ERROR UPLOADING' + ns.save() + flash("There was an error uploading your data") + return redirect(url_for('mapping.mapping')) + + # Update report status + ns.filename = file.filename + ns.data_hash = data_hash + ns.status = 'RECEIVED' + ns.save() + + # Schedule task + create_ns_task(data_hash, id, ns.kind) + + return redirect(url_for('mapping.mapping_status', id=id)) + + + +@mapping_bp.route('/mapping/status/', methods=['GET', 'POST']) +@jwt_required() +def mapping_status(id): + return "MAPPING STATUS" -@mapping_bp.route('/mapping/perform-mapping/', methods=['GET', 'POST']) -def mapping(): - """ - This is the mapping submission page. - """ - form = mapping_submission_form(request.form) - VARS = {'title': 'Perform Mapping', - 'form': form} - # todo: replace session user id and props with datastore user and props - user = session.get('user') - if form.validate_on_submit() and user: - transaction = g.ds.transaction() - transaction.begin() - - # Now generate and run trait tasks - report_name = form.report_name.data - report_slug = slugify(report_name) - trait_list = list(form.trait_data.processed_data.columns[2:]) - now = arrow.utcnow().datetime - trait_set = [] - secret_hash = unique_id()[0:8] - for trait_name in trait_list: - trait = trait_ds() - trait_data = form.trait_data.processed_data[['ISOTYPE', 'STRAIN', trait_name]].dropna(how='any') \ - .to_csv(index=False, - sep="\t", - na_rep="NA") - trait.__dict__.update({ - 'report_name': report_name, - 'report_slug': report_slug, - 'trait_name': trait_name, - 'trait_list': list(form.trait_data.processed_data.columns[2:]), - 'trait_data': trait_data, - 'n_strains': int(form.trait_data.processed_data.STRAIN.count()), - 'created_on': now, - 'status': 'queued', - 'is_public': form.is_public.data == 'true', - 'CENDR_VERSION': CENDR_VERSION, - 'REPORT_VERSION': REPORT_VERSION, - 'DATASET_RELEASE': DATASET_RELEASE, - 'WORMBASE_VERSION': WORMBASE_VERSION, - 'username': user['username'], - 'user_id': user['user_id'], - 'user_email': user['user_email'] - }) - if trait.is_public is False: - trait.secret_hash = secret_hash - trait.run_task() - trait_set.append(trait) - # Update the report to contain the set of the - # latest task runs - transaction.commit() - - flash("Successfully submitted mapping!", 'success') - return redirect(url_for('mapping.report_view', - report_slug=report_slug, - trait_name=trait_list[0])) - - return render_template('mapping.html', **VARS) +@mapping_bp.route('/mapping/perform-mapping/', methods=['GET', 'POST']) +@jwt_required() +def mapping(): + """ + This is the mapping submission page. + """ + title = 'Perform Mapping' + jwt_csrf_token = (get_jwt() or {}).get("csrf") + form = file_upload_form() + + return render_template('mapping.html', **locals()) @mapping_bp.route("/report//") From 734a59751d07ff3ab697913d1e84a2cbd27318d8 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Thu, 27 May 2021 09:59:26 -0500 Subject: [PATCH 202/288] index for h2 task should be uuid not data hash --- base/views/tools/heritability.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/views/tools/heritability.py b/base/views/tools/heritability.py index e3f6a62e..60bfe32a 100644 --- a/base/views/tools/heritability.py +++ b/base/views/tools/heritability.py @@ -36,7 +36,7 @@ def create_h2_task(data_hash, ds_id, ds_kind): This is designed to be run in the background on the server. It will run a heritability analysis on google cloud run """ - hr = h2calc_ds(data_hash) + hr = h2calc_ds(ds_id) # Perform h2 request queue = config['HERITABILITY_CALC_TASK_QUEUE'] From ef6155ea8903426b8241f39a5fc781bbf8f9b6e4 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 31 May 2021 23:13:22 -0500 Subject: [PATCH 203/288] Update DB creation and add variant annotation --- base/application.py | 6 +- base/database/__init__.py | 247 +++++++++++++++++------------ base/database/etl_homologene.py | 40 +++-- base/database/etl_variant_annot.py | 21 ++- base/database/etl_wormbase.py | 39 +++-- base/manage.py | 15 +- base/models.py | 4 + 7 files changed, 234 insertions(+), 138 deletions(-) diff --git a/base/application.py b/base/application.py index 4b7693f8..84db0220 100644 --- a/base/application.py +++ b/base/application.py @@ -16,8 +16,7 @@ from base.manage import (initdb, update_strains, update_credentials, - decrypt_credentials, - download_db) + decrypt_credentials) # --------- # # Routing # @@ -108,8 +107,7 @@ def register_commands(app): for command in [initdb, update_strains, update_credentials, - decrypt_credentials, - download_db]: + decrypt_credentials]: app.cli.add_command(command) diff --git a/base/database/__init__.py b/base/database/__init__.py index 3c0da032..e430cd5a 100644 --- a/base/database/__init__.py +++ b/base/database/__init__.py @@ -16,7 +16,7 @@ WormbaseGene, WormbaseGeneSummary) # ETL Pipelines - fetch and format data for -# input into the sqlite database +# input into the postgres database from base.database.etl_homologene import fetch_homologene from base.database.etl_strains import fetch_andersen_strains from base.database.etl_wormbase import (fetch_gene_gff_summary, @@ -32,78 +32,123 @@ def download_fname(download_path: str, download_url: str): download_url.split("/")[-1]) -def initialize_sqlite_database(sel_wormbase_version, +def initialize_postgres_database(sel_wormbase_version, strain_only=False): - """Create a static sqlite database + """Create a postgres database Args: sel_wormbase_version - e.g. WS276 - Generate an sqlite database + Generate a postgres database """ - start = arrow.utcnow() console.log("Initializing Database") - DATASET_RELEASE = config['DATASET_RELEASE'] - SQLITE_PATH = f"base/cendr.{DATASET_RELEASE}.{sel_wormbase_version}.db" - SQLITE_BASENAME = os.path.basename(SQLITE_PATH) # Download wormbase files if strain_only is False: - if os.path.exists(SQLITE_PATH): - os.remove(SQLITE_PATH) - - if not os.path.exists(DOWNLOAD_PATH): - os.makedirs(DOWNLOAD_PATH) - - # Parallel URL download - console.log("Downloading Wormbase Data") - GENE_GFF_URL = URLS.GENE_GFF_URL.format(WB=sel_wormbase_version) - GENE_GTF_URL = URLS.GENE_GTF_URL.format(WB=sel_wormbase_version) - download([URLS.STRAIN_VARIANT_ANNOTATION_URL, - GENE_GFF_URL, - GENE_GTF_URL, - URLS.GENE_IDS_URL, - URLS.HOMOLOGENE_URL, - URLS.ORTHOLOG_URL, - URLS.TAXON_ID_URL], - DOWNLOAD_PATH) - - sva_fname = download_fname(DOWNLOAD_PATH,URLS.STRAIN_VARIANT_ANNOTATION_URL) - gff_fname = download_fname(DOWNLOAD_PATH, GENE_GFF_URL) - gtf_fname = download_fname(DOWNLOAD_PATH, GENE_GTF_URL) - gene_ids_fname = download_fname(DOWNLOAD_PATH, URLS.GENE_IDS_URL) - homologene_fname = download_fname(DOWNLOAD_PATH, URLS.HOMOLOGENE_URL) - ortholog_fname = download_fname(DOWNLOAD_PATH, URLS.ORTHOLOG_URL) + f = download_external_data(sel_wormbase_version) from base.application import create_app app = create_app() app.app_context().push() - app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{SQLITE_BASENAME}" + app.config['SQLALCHEMY_DATABASE_URI'] = f'postgresql://admin:password@localhost/cendr' if strain_only is True: - db.metadata.drop_all(bind=db.engine, checkfirst=True, tables=[Strain.__table__]) - db.metadata.create_all(bind=db.engine, tables=[Strain.__table__]) + reset_tables(app, db, tables=[Strain.__table__]) else: + reset_tables(app, db) + + load_strains(db) + if strain_only is True: + console.log('Finished loading strains') + return + + load_metadata(db, sel_wormbase_version) + load_genes(db, f) + load_homologs(db, f) + load_orthologs(db, f) + load_variant_annotation(db, f) + generate_gene_dict() + + +################################# +# Print task execution duration # +# ############################### +def print_timer(start): + diff = int((arrow.utcnow() - start).total_seconds()) + console.log(f"{diff} seconds") + + +########################## +# Download external data # +########################## +def download_external_data(sel_wormbase_version): + if not os.path.exists(DOWNLOAD_PATH): + os.makedirs(DOWNLOAD_PATH) + + # Parallel URL download + console.log("Downloading Wormbase Data") + GENE_GFF_URL = URLS.GENE_GFF_URL.format(WB=sel_wormbase_version) + GENE_GTF_URL = URLS.GENE_GTF_URL.format(WB=sel_wormbase_version) + download([URLS.STRAIN_VARIANT_ANNOTATION_URL, + GENE_GFF_URL, + GENE_GTF_URL, + URLS.GENE_IDS_URL, + URLS.HOMOLOGENE_URL, + URLS.ORTHOLOG_URL, + URLS.TAXON_ID_URL], + DOWNLOAD_PATH) + + fnames = { + "sva": download_fname(DOWNLOAD_PATH,URLS.STRAIN_VARIANT_ANNOTATION_URL), + "gff": download_fname(DOWNLOAD_PATH, GENE_GFF_URL), + "gtf": download_fname(DOWNLOAD_PATH, GENE_GTF_URL), + "gene_ids": download_fname(DOWNLOAD_PATH, URLS.GENE_IDS_URL), + "homologene": download_fname(DOWNLOAD_PATH, URLS.HOMOLOGENE_URL), + "ortholog": download_fname(DOWNLOAD_PATH, URLS.ORTHOLOG_URL) + } + return fnames + + +################ +# Reset Tables # +################ +def reset_tables(app, db, tables = None): + start = arrow.utcnow() + if tables is None: + console.log('Dropping all tables...') + db.drop_all(app=app) + console.log('Creating all tables...') db.create_all(app=app) + else: + console.log(f'Dropping tables: ${tables}') + db.metadata.drop_all(bind=db.engine, checkfirst=True, tables=tables) + console.log(f'Creating tables: ${tables}') + db.metadata.create_all(bind=db.engine, tables=tables) + db.session.commit() + print_timer(start) - console.log(f"Created {SQLITE_PATH}") - ################ - # Load Strains # - ################ + +################ +# Load Strains # +################ +def load_strains(db): + start = arrow.utcnow() console.log('Loading strains...') - db.session.bulk_insert_mappings(Strain, fetch_andersen_strains()) + andersen_strains = fetch_andersen_strains() + db.session.bulk_insert_mappings(Strain, andersen_strains) db.session.commit() console.log(f"Inserted {Strain.query.count()} strains") + print_timer(start) + - if strain_only is True: - console.log('Finished loading strains') - return - ################ - # Set metadata # - ################ +################ +# Set metadata # +################ +def load_metadata(db, sel_wormbase_version): + start = arrow.utcnow() console.log('Inserting metadata') metadata = {} metadata.update(vars(constants)) @@ -126,71 +171,77 @@ def initialize_sqlite_database(sel_wormbase_version, db.session.add(key_val) db.session.commit() + print_timer(start) + - ############## - # Load Genes # - ############## +############## +# Load Genes # +############## +def load_genes(db, f): + start = arrow.utcnow() console.log('Loading summary gene table') - genes = fetch_gene_gff_summary(gff_fname) - db.session.bulk_insert_mappings(WormbaseGeneSummary, genes) + gene_summary = fetch_gene_gff_summary(f['gff']) + db.session.bulk_insert_mappings(WormbaseGeneSummary, gene_summary) db.session.commit() + print_timer(start) + start = arrow.utcnow() console.log('Loading gene table') - db.session.bulk_insert_mappings(WormbaseGene, fetch_gene_gtf(gtf_fname, gene_ids_fname)) - gene_summary = db.session.query(WormbaseGene.feature, db.func.count(WormbaseGene.feature)) \ + genes = fetch_gene_gtf(f['gtf'], f['gene_ids']) + db.session.bulk_insert_mappings(WormbaseGene, genes) + db.session.commit(); + + results = db.session.query(WormbaseGene.feature, db.func.count(WormbaseGene.feature)) \ .group_by(WormbaseGene.feature) \ .all() - gene_summary = '\n'.join([f"{k}: {v}" for k, v in gene_summary]) - console.log(f"============\nGene Summary\n------------\n{gene_summary}\n============") - - ###################################### - # Load Strain Variant Annotated Data # - ###################################### - console.log('\nLoading strain variant annotated csv') - sva_data = fetch_strain_variant_annotation_data(sva_fname) - db.session.bulk_insert_mappings(StrainAnnotatedVariants, sva_data) - db.session.commit() + result_summary = '\n'.join([f"{k}: {v}" for k, v in results]) + console.log(f"============\nGene Summary\n------------\n{result_summary}\n============\n") + print_timer(start) - ############################### - # Load homologs and orthologs # - ############################### + +############################### +# Load homologs # +############################### +def load_homologs(db, f): + start = arrow.utcnow() console.log('Loading homologs from homologene') - db.session.bulk_insert_mappings(Homologs, fetch_homologene(homologene_fname)) + homologene = fetch_homologene(f['homologene']) + db.session.bulk_insert_mappings(Homologs, homologene) db.session.commit() + print_timer(start) + +############################### +# Load Orthologs # +############################### +def load_orthologs(db, f): + start = arrow.utcnow() console.log('Loading orthologs from WormBase') - db.session.bulk_insert_mappings(Homologs, fetch_orthologs(ortholog_fname)) + orthologs = fetch_orthologs(f['ortholog']) + db.session.bulk_insert_mappings(Homologs, orthologs) db.session.commit() + print_timer(start) - ############# - # Upload DB # - ############# - # Upload the file using todays date for archiving purposes - #console.log(f"Uploading Database ({SQLITE_BASENAME})") - #upload_file(f"db/{SQLITE_BASENAME}", SQLITE_PATH) - - diff = int((arrow.utcnow() - start).total_seconds()) - console.log(f"{diff} seconds") - - # =========================== # - # Generate gene id dict # - # =========================== # - # Create a gene dictionary to match wormbase IDs to either the locus name - # or a sequence id +###################################### +# Load Strain Variant Annotated Data # +###################################### +def load_variant_annotation(db, f): + start = arrow.utcnow() + console.log('Loading strain variant annotated csv') + sva_data = fetch_strain_variant_annotation_data(f['sva']) + db.session.bulk_insert_mappings(StrainAnnotatedVariants, sva_data) + db.session.commit() + print_timer(start) + +# =========================== # +# Generate gene id dict # +# =========================== # +# Create a gene dictionary to match wormbase IDs to either the locus name +# or a sequence id +def generate_gene_dict(): + start = arrow.utcnow() + console.log('Generating gene_dict.pkl') gene_dict = {x.gene_id: x.locus or x.sequence_name for x in WormbaseGeneSummary.query.all()} pickle.dump(gene_dict, open("base/static/data/gene_dict.pkl", 'wb')) - - -def download_sqlite_database(): - DATASET_RELEASE = config['DATASET_RELEASE'] - WORMBASE_VERSION = config['WORMBASE_VERSION'] - SQLITE_FILE = f"cendr.{DATASET_RELEASE}.{WORMBASE_VERSION}.db" - blob_path = f"db/{SQLITE_FILE}" - file_path = f"base/{SQLITE_FILE}" - storage_client = storage.Client.from_service_account_json('env_config/client-secret.json') - bucket = storage_client.bucket(GOOGLE_CLOUD_BUCKET) - blob = bucket.blob(blob_path) - console.log(f"Downloading DB file STARTED: {SQLITE_FILE}") - blob.download_to_file(open(file_path, 'wb')) - console.log(f"Downloading DB file COMPLETE: {SQLITE_FILE}") + print_timer(start) diff --git a/base/database/etl_homologene.py b/base/database/etl_homologene.py index bb71d8bf..41daf5dc 100644 --- a/base/database/etl_homologene.py +++ b/base/database/etl_homologene.py @@ -8,6 +8,8 @@ import re import tarfile import csv + +from logzero import logger from urllib.request import urlretrieve from tempfile import NamedTemporaryFile from base.models import WormbaseGeneSummary @@ -59,17 +61,29 @@ def fetch_homologene(homologene_fname: str): # First, fetch records with a homolog ID that possesses a C. elegans gene. elegans_set = dict([[int(x[0]), x[3]] for x in response_csv if x[1] == '6239']) + # Remove CELE_ prefix from some gene names + for k, v in elegans_set.items(): + elegans_set[k] = v.replace('CELE_', '') + + idx = 0 + count = 0 for line in response_csv: - tax_id = int(line[1]) - homolog_id = int(line[0]) - if homolog_id in elegans_set.keys() and tax_id != 6239: - # Try to resolve the wormbase WB ID if possible. - gene_name = elegans_set[homolog_id] - gene_id = WormbaseGeneSummary.resolve_gene_id(gene_name) or line[2] - yield {'gene_id': gene_id, - 'gene_name': gene_name, - 'homolog_species': taxon_ids[tax_id], - 'homolog_taxon_id': tax_id, - 'homolog_gene': line[3], - 'homolog_source': "Homologene", - 'is_ortholog': False} + idx += 1 + tax_id = int(line[1]) + homolog_id = int(line[0]) + if homolog_id in elegans_set.keys() and tax_id != 6239: + # Try to resolve the wormbase WB ID if possible. + gene_name = elegans_set[homolog_id] + gene_id = WormbaseGeneSummary.resolve_gene_id(gene_name) + ref = WormbaseGeneSummary.query.filter(WormbaseGeneSummary.gene_id == gene_id).first() + if idx % 10000 == 0: + logger.info(f'Processed {idx} records yielding {count} inserts') + if ref: + count += 1 + yield {'gene_id': gene_id, + 'gene_name': gene_name, + 'homolog_species': taxon_ids[tax_id], + 'homolog_taxon_id': tax_id, + 'homolog_gene': line[3], + 'homolog_source': "Homologene", + 'is_ortholog': False } diff --git a/base/database/etl_variant_annot.py b/base/database/etl_variant_annot.py index 9e1c1e91..d87e7804 100644 --- a/base/database/etl_variant_annot.py +++ b/base/database/etl_variant_annot.py @@ -5,8 +5,11 @@ Author: Sam Wachspress """ import csv +import re +from logzero import logger from sqlalchemy.sql.expression import null +from base.models import StrainAnnotatedVariants def fetch_strain_variant_annotation_data(sva_fname: str): """ @@ -27,14 +30,26 @@ def fetch_strain_variant_annotation_data(sva_fname: str): line_count += 1 else: line_count += 1 + if line_count % 100000 == 0: + logger.info(f"Processed {line_count} lines;") + + target_consequence = None + consequence = row[4]if row[4] else None + pattern = '^@[0-9]*$' + alt_target = re.match(pattern, consequence) + if alt_target: + target_consequence = int(consequence[1:]) + consequence = None + yield { 'id': line_count, 'chrom': row[0], 'pos': int(row[1]), 'ref_seq': row[2] if row[2] else None, 'alt_seq': row[3] if row[3] else None, - 'consequence': row[4] if row[4] else None, - 'gene_id': row[5] if row[6] else None, + 'consequence': consequence, + 'target_consequence': target_consequence, + 'gene_id': row[5] if row[5] else None, 'transcript': row[6] if row[6] else None, 'biotype': row[7] if row[7] else None, 'strand': row[8] if row[8] else None, @@ -46,7 +61,7 @@ def fetch_strain_variant_annotation_data(sva_fname: str): 'percent_protein': float(row[14]) if row[14] else None, 'gene': row[15] if row[15] else None, 'variant_impact': row[16] if row[16] else None, - 'divergent': 1 if row[17] == 'D' else None, + 'divergent': True if row[17] == 'D' else False, } print(f'Processed {line_count} lines.') diff --git a/base/database/etl_wormbase.py b/base/database/etl_wormbase.py index 0cd02e02..d6648239 100644 --- a/base/database/etl_wormbase.py +++ b/base/database/etl_wormbase.py @@ -8,6 +8,7 @@ Author: Daniel E. Cook (danielecook@gmail.com) """ +from base.models import WormbaseGeneSummary import csv import gzip from logzero import logger @@ -46,7 +47,11 @@ def fetch_gene_gtf(gtf_fname: str, gene_ids_fname: str): gene_gtf.frame = gene_gtf.frame.apply(lambda x: x if x != "." else None) gene_gtf.exon_number = gene_gtf.exon_number.apply(lambda x: x if x != "" else None) gene_gtf['arm_or_center'] = gene_gtf.apply(lambda row: arm_or_center(row['chrom'], row['pos']), axis=1) + idx = 0 for row in gene_gtf.to_dict('records'): + idx += 1 + if idx % 100000 == 0: + logger.info(f"Processed {idx} lines") yield row @@ -98,17 +103,25 @@ def fetch_orthologs(orthologs_fname: str): """ csv_out = list(csv.reader(open(orthologs_fname, 'r'), delimiter='\t')) + idx = 0 + count = 0 for line in csv_out: - size_of_line = len(line) - if size_of_line < 2: - continue - elif size_of_line == 2: - wb_id, locus_name = line - else: - yield {'gene_id': wb_id, - 'gene_name': locus_name, - 'homolog_species': line[0], - 'homolog_taxon_id': None, - 'homolog_gene': line[2], - 'homolog_source': line[3], - 'is_ortholog': line[0] == 'Caenorhabditis elegans'} + idx += 1 + size_of_line = len(line) + if size_of_line < 2: + continue + elif size_of_line == 2: + wb_id, locus_name = line + else: + ref = WormbaseGeneSummary.query.filter(WormbaseGeneSummary.gene_id == wb_id).first() + if idx % 10000 == 0: + logger.info(f'Processed {idx} records yielding {count} inserts') + if ref: + count += 1 + yield {'gene_id': wb_id, + 'gene_name': locus_name, + 'homolog_species': line[0], + 'homolog_taxon_id': None, + 'homolog_gene': line[2], + 'homolog_source': line[3], + 'is_ortholog': line[0] == 'Caenorhabditis elegans'} diff --git a/base/manage.py b/base/manage.py index e08fc066..cb5b72f8 100644 --- a/base/manage.py +++ b/base/manage.py @@ -12,8 +12,7 @@ from click import secho from base.utils.gcloud import get_item from base.utils.data_utils import zipdir -from base.database import (initialize_sqlite_database, - download_sqlite_database) +from base.database import initialize_postgres_database from base import constants from subprocess import Popen, PIPE @@ -23,19 +22,21 @@ @click.command(help="Initialize the database") @click.argument("wormbase_version", default=constants.WORMBASE_VERSION) def initdb(wormbase_version=constants.WORMBASE_VERSION): - initialize_sqlite_database(wormbase_version) + initialize_postgres_database(wormbase_version) @click.command(help="Updates the strain table of the database") @click.argument("wormbase_version", default=constants.WORMBASE_VERSION) def update_strains(wormbase_version): - initialize_sqlite_database(wormbase_version, strain_only=True) + initialize_postgres_database(wormbase_version, strain_only=True) -@click.command(help="Download the database (used in docker container)") -def download_db(): +# Todo: allow downloading postgres dump/local db in docker container +# or just link to .sql in cloud storage (even better!) +#@click.command(help="Download the database (used in docker container)") +#def download_db(): # Downloads the latest SQLITE database - download_sqlite_database() + #download_sqlite_database() @click.command(help="Update credentials") diff --git a/base/models.py b/base/models.py index 86cdc191..00864f61 100644 --- a/base/models.py +++ b/base/models.py @@ -807,6 +807,7 @@ class StrainAnnotatedVariants(DictSerializable, db.Model): ref_seq = db.Column(db.String(), nullable=True) alt_seq = db.Column(db.String(), nullable=True) consequence = db.Column(db.String(), nullable=True) + target_consequence = db.Column(db.Integer(), nullable=True) gene_id = db.Column(db.ForeignKey('wormbase_gene_summary.gene_id'), index=True, nullable=True) transcript = db.Column(db.String(), index=True, nullable=True) biotype = db.Column(db.String(), nullable=True) @@ -821,6 +822,9 @@ class StrainAnnotatedVariants(DictSerializable, db.Model): variant_impact = db.Column(db.String(), nullable=True) divergent = db.Column(db.Boolean(), nullable=True) + __gene_summary__ = db.relationship("WormbaseGeneSummary", backref='variant_annotation', lazy='joined') + + column_details = [ {'id': 'chrom', 'name': 'Chromosome'}, {'id': 'pos', 'name': 'Position'}, From 3275934670ff8ba49af8ca86c147ce062120aff5 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 31 May 2021 23:13:35 -0500 Subject: [PATCH 204/288] update cloud sql db names --- env_config.zip.enc | Bin 27344 -> 27376 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/env_config.zip.enc b/env_config.zip.enc index 4834239724b5573ee8819612801d29dc581ccc1c..3a7b820f084bba550dc60af21370f6f16273f1e9 100644 GIT binary patch literal 27376 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 nl4>il^tm90_o5SMl0oNoR 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 HCovq%BMS@D From a0c0f021b81497095567ec5abd5c0b4a3ef26f61 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 31 May 2021 23:14:28 -0500 Subject: [PATCH 205/288] add readme --- base/database/readme.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 base/database/readme.md diff --git a/base/database/readme.md b/base/database/readme.md new file mode 100644 index 00000000..87d0c4eb --- /dev/null +++ b/base/database/readme.md @@ -0,0 +1,13 @@ +# CeNDR Database + +This directory contains the scripts to perform the 'initdb' flask action. +It requires a local PostgreSQL instance to be running. + +The table can then be dumped with + +''' +pg_dump -U admin --format=plain --no-owner --no-acl cendr > cendr.sql + +''' + +The .sql file can then be uploaded to Google Cloud Buckets and batch imported to the Cloud SQL instance From d8ef099df6e5d8f12f1854e5b88549e65d032b51 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Tue, 1 Jun 2021 09:58:40 -0500 Subject: [PATCH 206/288] missing space --- base/database/etl_variant_annot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/database/etl_variant_annot.py b/base/database/etl_variant_annot.py index d87e7804..9ba2a37c 100644 --- a/base/database/etl_variant_annot.py +++ b/base/database/etl_variant_annot.py @@ -34,7 +34,7 @@ def fetch_strain_variant_annotation_data(sva_fname: str): logger.info(f"Processed {line_count} lines;") target_consequence = None - consequence = row[4]if row[4] else None + consequence = row[4] if row[4] else None pattern = '^@[0-9]*$' alt_target = re.match(pattern, consequence) if alt_target: From 851a20b14a04db5cb57a2b43d29a0f17d62f2801 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Fri, 4 Jun 2021 15:02:18 -0500 Subject: [PATCH 207/288] create timing decorator --- base/config.py | 1 - base/database/__init__.py | 52 +++++++++++++++------------------------ base/utils/decorators.py | 13 ++++++++++ 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/base/config.py b/base/config.py index 1565dd76..0ce7e067 100644 --- a/base/config.py +++ b/base/config.py @@ -49,7 +49,6 @@ def get_config(APP_CONFIG): DB_PASS = APP_CONFIG_VARS['PSQL_DB_PASSWORD'] CONNECTION = APP_CONFIG_VARS['PSQL_DB_CONNECTION_NAME'] DB = APP_CONFIG_VARS['PSQL_DB_NAME'] - logger.info('WHY IS THIS OUT OF DATE') config.update(BASE_VARS) config.update(APP_CONFIG_VARS) diff --git a/base/database/__init__.py b/base/database/__init__.py index e430cd5a..bb04bfcb 100644 --- a/base/database/__init__.py +++ b/base/database/__init__.py @@ -2,13 +2,12 @@ import arrow import pickle from rich.console import Console -from google.cloud import storage from base import constants from base.constants import URLS, GOOGLE_CLOUD_BUCKET from base.config import config from base.utils.data_utils import download -from base.utils.gcloud import upload_file +from base.utils.decorators import timeit from base.models import (StrainAnnotatedVariants, db, Strain, Homologs, @@ -24,14 +23,14 @@ fetch_orthologs) from base.database.etl_variant_annot import fetch_strain_variant_annotation_data -console = Console() DOWNLOAD_PATH = ".download" +console = Console() def download_fname(download_path: str, download_url: str): return os.path.join(download_path, download_url.split("/")[-1]) - +@timeit def initialize_postgres_database(sel_wormbase_version, strain_only=False): """Create a postgres database @@ -63,25 +62,20 @@ def initialize_postgres_database(sel_wormbase_version, return load_metadata(db, sel_wormbase_version) - load_genes(db, f) + load_genes_summary(db, f) + load_genes_table(db, f) load_homologs(db, f) load_orthologs(db, f) load_variant_annotation(db, f) generate_gene_dict() -################################# -# Print task execution duration # -# ############################### -def print_timer(start): - diff = int((arrow.utcnow() - start).total_seconds()) - console.log(f"{diff} seconds") - - ########################## # Download external data # ########################## +@timeit def download_external_data(sel_wormbase_version): + console.log('Downloading External Data...') if not os.path.exists(DOWNLOAD_PATH): os.makedirs(DOWNLOAD_PATH) @@ -112,8 +106,8 @@ def download_external_data(sel_wormbase_version): ################ # Reset Tables # ################ +@timeit def reset_tables(app, db, tables = None): - start = arrow.utcnow() if tables is None: console.log('Dropping all tables...') db.drop_all(app=app) @@ -126,27 +120,25 @@ def reset_tables(app, db, tables = None): db.metadata.create_all(bind=db.engine, tables=tables) db.session.commit() - print_timer(start) ################ # Load Strains # ################ +@timeit def load_strains(db): - start = arrow.utcnow() console.log('Loading strains...') andersen_strains = fetch_andersen_strains() db.session.bulk_insert_mappings(Strain, andersen_strains) db.session.commit() console.log(f"Inserted {Strain.query.count()} strains") - print_timer(start) - ################ # Set metadata # ################ +@timeit def load_metadata(db, sel_wormbase_version): start = arrow.utcnow() console.log('Inserting metadata') @@ -158,6 +150,7 @@ def load_metadata(db, sel_wormbase_version): "WORMBASE_VERSION": sel_wormbase_version, "RELEASES": config['RELEASES'], "DATE": arrow.utcnow()}) + for k, v in metadata.items(): if not k.startswith("_"): # For nested constants: @@ -171,21 +164,21 @@ def load_metadata(db, sel_wormbase_version): db.session.add(key_val) db.session.commit() - print_timer(start) ############## # Load Genes # ############## -def load_genes(db, f): - start = arrow.utcnow() +@timeit +def load_genes_summary(db, f): console.log('Loading summary gene table') gene_summary = fetch_gene_gff_summary(f['gff']) db.session.bulk_insert_mappings(WormbaseGeneSummary, gene_summary) db.session.commit() - print_timer(start) - start = arrow.utcnow() + +@timeit +def load_genes_table(db, f): console.log('Loading gene table') genes = fetch_gene_gtf(f['gtf'], f['gene_ids']) db.session.bulk_insert_mappings(WormbaseGene, genes) @@ -196,52 +189,47 @@ def load_genes(db, f): .all() result_summary = '\n'.join([f"{k}: {v}" for k, v in results]) console.log(f"============\nGene Summary\n------------\n{result_summary}\n============\n") - print_timer(start) ############################### # Load homologs # ############################### +@timeit def load_homologs(db, f): - start = arrow.utcnow() console.log('Loading homologs from homologene') homologene = fetch_homologene(f['homologene']) db.session.bulk_insert_mappings(Homologs, homologene) db.session.commit() - print_timer(start) ############################### # Load Orthologs # ############################### +@timeit def load_orthologs(db, f): - start = arrow.utcnow() console.log('Loading orthologs from WormBase') orthologs = fetch_orthologs(f['ortholog']) db.session.bulk_insert_mappings(Homologs, orthologs) db.session.commit() - print_timer(start) ###################################### # Load Strain Variant Annotated Data # ###################################### +@timeit def load_variant_annotation(db, f): - start = arrow.utcnow() console.log('Loading strain variant annotated csv') sva_data = fetch_strain_variant_annotation_data(f['sva']) db.session.bulk_insert_mappings(StrainAnnotatedVariants, sva_data) db.session.commit() - print_timer(start) # =========================== # # Generate gene id dict # # =========================== # # Create a gene dictionary to match wormbase IDs to either the locus name # or a sequence id +@timeit def generate_gene_dict(): - start = arrow.utcnow() console.log('Generating gene_dict.pkl') gene_dict = {x.gene_id: x.locus or x.sequence_name for x in WormbaseGeneSummary.query.all()} pickle.dump(gene_dict, open("base/static/data/gene_dict.pkl", 'wb')) - print_timer(start) diff --git a/base/utils/decorators.py b/base/utils/decorators.py index 362db9de..5e86efa0 100644 --- a/base/utils/decorators.py +++ b/base/utils/decorators.py @@ -1,6 +1,9 @@ +import arrow +from rich.console import Console from functools import wraps from flask import request, jsonify +console = Console() def jsonify_request(func): """ @@ -21,3 +24,13 @@ def jsonify_the_request(*args, **kwargs): return jsonify(func(*args, **kwargs)) return func(*args, **kwargs) return jsonify_the_request + + +def timeit(method): + def timed(*args, **kw): + start = arrow.utcnow() + result = method(*args, **kw) + diff = int((arrow.utcnow() - start).total_seconds()) + console.log(f"{diff} seconds") + return result + return timed \ No newline at end of file From 03c2fda52805ecb31969dd405f9f98da4820e7b7 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Fri, 4 Jun 2021 15:16:25 -0500 Subject: [PATCH 208/288] move constants --- base/database/etl_homologene.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/base/database/etl_homologene.py b/base/database/etl_homologene.py index 41daf5dc..2798e527 100644 --- a/base/database/etl_homologene.py +++ b/base/database/etl_homologene.py @@ -15,6 +15,8 @@ from base.models import WormbaseGeneSummary from base.constants import URLS +C_ELEGANS_PREFIX = 'CELE_' +C_ELEGANS_HOMOLOG_ID = 6239 def fetch_taxon_ids(): """ @@ -59,11 +61,11 @@ def fetch_homologene(homologene_fname: str): taxon_ids = fetch_taxon_ids() # First, fetch records with a homolog ID that possesses a C. elegans gene. - elegans_set = dict([[int(x[0]), x[3]] for x in response_csv if x[1] == '6239']) + elegans_set = dict([[int(x[0]), x[3]] for x in response_csv if x[1] == str(C_ELEGANS_HOMOLOG_ID)]) # Remove CELE_ prefix from some gene names for k, v in elegans_set.items(): - elegans_set[k] = v.replace('CELE_', '') + elegans_set[k] = v.replace(C_ELEGANS_PREFIX, '') idx = 0 count = 0 @@ -71,7 +73,7 @@ def fetch_homologene(homologene_fname: str): idx += 1 tax_id = int(line[1]) homolog_id = int(line[0]) - if homolog_id in elegans_set.keys() and tax_id != 6239: + if homolog_id in elegans_set.keys() and tax_id != int(C_ELEGANS_HOMOLOG_ID): # Try to resolve the wormbase WB ID if possible. gene_name = elegans_set[homolog_id] gene_id = WormbaseGeneSummary.resolve_gene_id(gene_name) From a049155550d06343a63833eb2715447a4562a62c Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Fri, 11 Jun 2021 15:08:26 -0500 Subject: [PATCH 209/288] remove sqlite_path to surface any ref errors --- base/config.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/base/config.py b/base/config.py index 97ba6ec7..c9e82dac 100644 --- a/base/config.py +++ b/base/config.py @@ -27,9 +27,6 @@ # The most recent release DATASET_RELEASE, WORMBASE_VERSION = RELEASES[0] -# SQLITE DATABASE -SQLITE_PATH = f"base/cendr.{DATASET_RELEASE}.{WORMBASE_VERSION}.db" - def load_yaml(path): return yaml.load(open(path), Loader=yaml.SafeLoader) From e08e4ee3c6a6d0741fac0cd05427bba11987cadd Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Fri, 11 Jun 2021 15:09:34 -0500 Subject: [PATCH 210/288] whitespace --- base/database/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/base/database/__init__.py b/base/database/__init__.py index bb04bfcb..3ae82224 100644 --- a/base/database/__init__.py +++ b/base/database/__init__.py @@ -49,8 +49,10 @@ def initialize_postgres_database(sel_wormbase_version, from base.application import create_app app = create_app() app.app_context().push() + app.config['SQLALCHEMY_DATABASE_URI'] = f'postgresql://admin:password@localhost/cendr' + if strain_only is True: reset_tables(app, db, tables=[Strain.__table__]) else: @@ -223,6 +225,7 @@ def load_variant_annotation(db, f): db.session.bulk_insert_mappings(StrainAnnotatedVariants, sva_data) db.session.commit() + # =========================== # # Generate gene id dict # # =========================== # From d1070e55bb35ec272567aeca54f56a2b80c57259 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Fri, 11 Jun 2021 15:10:05 -0500 Subject: [PATCH 211/288] update mapping text prompt --- base/templates/mapping.html | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/base/templates/mapping.html b/base/templates/mapping.html index 75c6e22e..c861789d 100644 --- a/base/templates/mapping.html +++ b/base/templates/mapping.html @@ -12,13 +12,10 @@

    - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras faucibus id dui eu pharetra. Nunc mattis bibendum maximus. Pellentesque viverra molestie elementum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Praesent aliquet laoreet tellus, quis eleifend lacus fermentum pretium. Pellentesque ac semper lectus, vel elementum nunc. Aliquam et diam lacus. Aliquam vulputate velit quis orci venenatis luctus. Nunc in elit scelerisque libero venenatis dapibus. + This portal will use your data to perform a genome-wide association mapping. Please enter your data using the format shown in the example data file [LINK to example data] and upload the comma-separated file below. This process will take your quantitative trait data for the set of strains that you scored and test for correlations of genotype and phenotype at over 50,000 loci across the C. elegans genome. You will be linked to a report for the mapping experiment along with links to download the data. This mapping portal uses NemaScan [LINK to github project] and can only map one quantitative trait at a time.

    - Cras venenatis lectus at tellus luctus finibus. Fusce id interdum ex, ac tincidunt risus. Sed vehicula erat nulla, a cursus est cursus sit amet. Donec consequat ultricies velit, vitae feugiat lorem tincidunt eget. Mauris in sodales nunc, id interdum est. Aliquam sodales vel lorem non varius. Aenean feugiat tortor id accumsan facilisis. Morbi orci ex, molestie et dictum ac, consectetur a ante. Nullam gravida massa purus, elementum varius nisi fringilla in. Nulla elementum tincidunt elit, et condimentum urna varius sed. -

    -

    - Donec faucibus non mauris vel eleifend. Nulla ullamcorper erat magna, vel aliquet urna semper et. Cras vitae tempus ligula. Donec dignissim sem ut arcu ornare tempor. Phasellus magna ante, eleifend eu ex non, ultrices mollis magna. Maecenas rutrum dolor ut ligula vulputate, vitae blandit mi sodales. Aenean ex augue, faucibus quis quam a, vestibulum suscipit sapien. Proin ut sapien pretium ligula rutrum pharetra. Curabitur eu elit at quam egestas bibendum. Nulla ut massa auctor massa ornare sodales. Etiam a ante eget eros lacinia auctor vel sit amet sapien. + Good luck hunting!

    {# /col #} From 0a39303b7038b19c9e50540b55973c067aacd5eb Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Fri, 11 Jun 2021 15:30:01 -0500 Subject: [PATCH 212/288] add list_files to gcloud api --- base/utils/gcloud.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/base/utils/gcloud.py b/base/utils/gcloud.py index 904de91a..6b67de86 100644 --- a/base/utils/gcloud.py +++ b/base/utils/gcloud.py @@ -209,12 +209,19 @@ def check_blob(fname): return cendr_bucket.get_blob(fname) +def list_files(prefix): + """ + Lists files with a given prefix + """ + cendr_bucket = get_cendr_bucket() + return cendr_bucket.list_blobs(prefix=prefix) + + def list_release_files(prefix): """ Lists files with a given prefix from the current dataset release """ - cendr_bucket = get_cendr_bucket() items = cendr_bucket.list_blobs(prefix=prefix) return list([f"https://storage.googleapis.com/{GOOGLE_CLOUD_BUCKET}/{x.name}" for x in items]) From 529c5643f4d23944c094ea9efefb126efbd9d098 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Fri, 11 Jun 2021 15:30:20 -0500 Subject: [PATCH 213/288] initial commit for mapping result pages (needs work) --- base/templates/mapping_result.html | 28 +++++++ base/templates/mapping_result_list.html | 106 ++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 base/templates/mapping_result.html create mode 100644 base/templates/mapping_result_list.html diff --git a/base/templates/mapping_result.html b/base/templates/mapping_result.html new file mode 100644 index 00000000..8909317a --- /dev/null +++ b/base/templates/mapping_result.html @@ -0,0 +1,28 @@ +{% extends "_layouts/default.html" %} + +{% block custom_head %} + + {% if not result %} + + {% endif %} + +{% endblock %} + + +{% block content %} + +{% if data and not result %} + +
    +
    +
    +

    + + The genome-wide association mapping is currently being run. Please check back in a few minutes for results. + This page will reload automatically. + +

    +
    {# /col-md-8 #} +
    {# /row #} + +{% else %} diff --git a/base/templates/mapping_result_list.html b/base/templates/mapping_result_list.html new file mode 100644 index 00000000..f985b24d --- /dev/null +++ b/base/templates/mapping_result_list.html @@ -0,0 +1,106 @@ +{% extends "_layouts/default.html" %} + +{% block custom_head %} + + +{% endblock %} + +{% block style %} + +{% endblock %} + +{% block content %} + +{% from "macros.html" import render_dataTable_top_menu %} +{{ render_dataTable_top_menu() }} + +
    + +
    + + + + + + + + + + + + + + {% for item in items %} + + {% if item %} + + + + {% endif %} + + + {% endfor %} + + +
    Label Trait Status Date
    {{ item.label }} {{ item.trait }} + {% if item.status == 'COMPLETE' %} + + {{ item.status }} + + {% else %} + {{ item.status }} + {% endif %} + {{ item.created_on|date_format }}
    + +
    {# /col #} +
    {# /row #} + + +{% endblock %} + +{% block script %} + + + + +{% endblock %} From 4465f4e994331e7887f0a8d6785e9146b39819fa Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Fri, 11 Jun 2021 15:30:47 -0500 Subject: [PATCH 214/288] mapping view updates --- base/views/mapping.py | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/base/views/mapping.py b/base/views/mapping.py index a54b384d..b794fa35 100644 --- a/base/views/mapping.py +++ b/base/views/mapping.py @@ -17,7 +17,7 @@ from base.models import trait_ds, ns_calc_ds from base.forms import file_upload_form from base.utils.data_utils import unique_id, hash_it -from base.utils.gcloud import check_blob, query_item, delete_item, upload_file, add_task +from base.utils.gcloud import check_blob, list_files, query_item, delete_item, upload_file, add_task from base.utils.jwt_utils import jwt_required, get_jwt, get_current_user from base.utils.plots import pxg_plot, plotly_distplot @@ -78,9 +78,13 @@ def schedule_mapping(): file = request.files['file'] data_hash = hash_it(file, length=32) data_blob = f"reports/nemascan/{data_hash}/data.tsv" - # if check_blob(data_blob): - # todo: handle file already existing - + results_path = f"reports/nemascan/{data_hash}/results/" + results = list_files(results_path) + # if there is anything in the results directory, don't schedule the task + # (could be running, failed, etc.. need to check result directory in more detail to confirm state) + if len(results) > 0: + return redirect(url_for('mapping.mapping_result', id=id)) + result = upload_file(data_blob, file, as_file_obj=True) if not result: ns.status = 'ERROR UPLOADING' @@ -97,15 +101,28 @@ def schedule_mapping(): # Schedule task create_ns_task(data_hash, id, ns.kind) - return redirect(url_for('mapping.mapping_status', id=id)) + return redirect(url_for('mapping.mapping_report', id=id)) + +@mapping_bp.route('/mapping/report/all', methods=['GET', 'POST']) +@jwt_required() +def mapping_result_list(id): + title = 'Genetic Mapping Results' + user = get_current_user() + items = ns_calc_ds().query_by_username(user.name) + items = sorted(items, key=lambda x: x['created_on'], reverse=True) + return render_template('mapping_result.html', **locals()) -@mapping_bp.route('/mapping/status/', methods=['GET', 'POST']) + +@mapping_bp.route('/mapping/report/', methods=['GET']) @jwt_required() -def mapping_status(id): - return "MAPPING STATUS" +def mapping_report(id): + title = 'Genetic Mapping Report' + user = get_current_user() + ns = ns_calc_ds(id) + return render_template('mapping_result.html', **locals()) @mapping_bp.route('/mapping/perform-mapping/', methods=['GET', 'POST']) From 62213dde8c9f3f74050b7076908f5ccb0c8dc90b Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Fri, 11 Jun 2021 15:31:35 -0500 Subject: [PATCH 215/288] whitespace --- base/static/css/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/static/css/styles.css b/base/static/css/styles.css index 07d6a3c5..a822e06d 100644 --- a/base/static/css/styles.css +++ b/base/static/css/styles.css @@ -991,4 +991,4 @@ article { .nu-alt-btn { background-color: #FFA500; -} \ No newline at end of file +} From aabc987340703ef7dbefe6610bb82879f30fc8e2 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Fri, 11 Jun 2021 15:43:40 -0500 Subject: [PATCH 216/288] update env config --- env_config.zip.enc | Bin 27376 -> 27392 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/env_config.zip.enc b/env_config.zip.enc index 3a7b820f084bba550dc60af21370f6f16273f1e9..e62a921a3109962572d1d2744e70188bb0876124 100644 GIT binary patch literal 27392 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 D<@7Jn literal 27376 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 nl4>il^ Date: Fri, 11 Jun 2021 16:08:55 -0500 Subject: [PATCH 217/288] small fixes --- base/templates/mapping_result.html | 4 ++++ base/utils/gcloud.py | 4 +++- queue.yaml | 2 -- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/base/templates/mapping_result.html b/base/templates/mapping_result.html index 8909317a..756c05ac 100644 --- a/base/templates/mapping_result.html +++ b/base/templates/mapping_result.html @@ -26,3 +26,7 @@
    {# /row #} {% else %} + +{% endif %} + +{% endblock %} diff --git a/base/utils/gcloud.py b/base/utils/gcloud.py index 6b67de86..dd93cbf0 100644 --- a/base/utils/gcloud.py +++ b/base/utils/gcloud.py @@ -214,7 +214,9 @@ def list_files(prefix): Lists files with a given prefix """ cendr_bucket = get_cendr_bucket() - return cendr_bucket.list_blobs(prefix=prefix) + items = cendr_bucket.list_blobs(prefix=prefix) + return list(items) + def list_release_files(prefix): diff --git a/queue.yaml b/queue.yaml index eb30f534..f424a517 100644 --- a/queue.yaml +++ b/queue.yaml @@ -7,7 +7,6 @@ queue: min_backoff_seconds: 10 max_backoff_seconds: 60 max_doublings: 2 - - name: ipcalc max_concurrent_requests: 1 rate: 1/s @@ -16,7 +15,6 @@ queue: min_backoff_seconds: 10 max_backoff_seconds: 60 max_doublings: 2 - - name: nscalc max_concurrent_requests: 1 rate: 1/s From 8414287478c8a6ebebeaeaeb016484298da3dc32 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Fri, 11 Jun 2021 16:44:00 -0500 Subject: [PATCH 218/288] remove filename from schema since it is standardized --- base/views/mapping.py | 1 - 1 file changed, 1 deletion(-) diff --git a/base/views/mapping.py b/base/views/mapping.py index b794fa35..53c5183b 100644 --- a/base/views/mapping.py +++ b/base/views/mapping.py @@ -93,7 +93,6 @@ def schedule_mapping(): return redirect(url_for('mapping.mapping')) # Update report status - ns.filename = file.filename ns.data_hash = data_hash ns.status = 'RECEIVED' ns.save() From 6c66ac16ecccca92db22b25959e1bee3f92f3d63 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Fri, 11 Jun 2021 18:28:39 -0500 Subject: [PATCH 219/288] hash file contents instead of file object --- base/utils/data_utils.py | 6 ++++++ base/views/mapping.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/base/utils/data_utils.py b/base/utils/data_utils.py index d6c1d219..4ff1cdb5 100644 --- a/base/utils/data_utils.py +++ b/base/utils/data_utils.py @@ -100,6 +100,12 @@ def hash_it(object, length=10): return hashlib.sha1(str(object).encode('utf-8')).hexdigest()[0:length] +def hash_file_upload(file, length=10): + ''' Computes the sha1 hash of a file upload (FileStorage object) ''' + logger.debug(file) + return hashlib.sha1(file.read()).hexdigest() [0:length] + + def hash_password(password): h = hashlib.md5(password.encode()) return h.hexdigest() diff --git a/base/views/mapping.py b/base/views/mapping.py index 53c5183b..86fca9f1 100644 --- a/base/views/mapping.py +++ b/base/views/mapping.py @@ -16,7 +16,7 @@ from base.config import config from base.models import trait_ds, ns_calc_ds from base.forms import file_upload_form -from base.utils.data_utils import unique_id, hash_it +from base.utils.data_utils import unique_id, hash_file_upload from base.utils.gcloud import check_blob, list_files, query_item, delete_item, upload_file, add_task from base.utils.jwt_utils import jwt_required, get_jwt, get_current_user from base.utils.plots import pxg_plot, plotly_distplot @@ -76,7 +76,7 @@ def schedule_mapping(): # Upload file to cloud bucket file = request.files['file'] - data_hash = hash_it(file, length=32) + data_hash = hash_file_upload(file, length=32) data_blob = f"reports/nemascan/{data_hash}/data.tsv" results_path = f"reports/nemascan/{data_hash}/results/" results = list_files(results_path) From f8403e9127da34494fc05a88181b206862a87376 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Fri, 11 Jun 2021 21:50:14 -0500 Subject: [PATCH 220/288] stage uploaded file on server for hashing --- .gitignore | 2 ++ base/utils/data_utils.py | 2 +- base/views/mapping.py | 10 +++++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 0d8bc192..238f8f53 100644 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,5 @@ invoke .vscode/launch.json cloud_functions/heritability_run/strain_data.tsv base/bam_bai_signed_download_script.sh + +uploads/ \ No newline at end of file diff --git a/base/utils/data_utils.py b/base/utils/data_utils.py index 4ff1cdb5..cf6f6810 100644 --- a/base/utils/data_utils.py +++ b/base/utils/data_utils.py @@ -103,7 +103,7 @@ def hash_it(object, length=10): def hash_file_upload(file, length=10): ''' Computes the sha1 hash of a file upload (FileStorage object) ''' logger.debug(file) - return hashlib.sha1(file.read()).hexdigest() [0:length] + return hashlib.sha1(file.read()).hexdigest()[0:length] def hash_password(password): diff --git a/base/views/mapping.py b/base/views/mapping.py index 86fca9f1..68228199 100644 --- a/base/views/mapping.py +++ b/base/views/mapping.py @@ -4,6 +4,7 @@ import urllib import pandas as pd import simplejson as json +import os from datetime import date from flask import render_template, request, redirect, url_for, abort @@ -27,6 +28,10 @@ template_folder='mapping') +# Create a directory in a known location to save files to. +uploads_dir = os.path.join('./', 'uploads') +os.makedirs(uploads_dir, exist_ok=True) + class CustomEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, decimal.Decimal): @@ -76,6 +81,9 @@ def schedule_mapping(): # Upload file to cloud bucket file = request.files['file'] + local_path = os.path.join(uploads_dir, f'{id}.tsv') + file.save(local_path) + data_hash = hash_file_upload(file, length=32) data_blob = f"reports/nemascan/{data_hash}/data.tsv" results_path = f"reports/nemascan/{data_hash}/results/" @@ -85,7 +93,7 @@ def schedule_mapping(): if len(results) > 0: return redirect(url_for('mapping.mapping_result', id=id)) - result = upload_file(data_blob, file, as_file_obj=True) + result = upload_file(data_blob, local_path) if not result: ns.status = 'ERROR UPLOADING' ns.save() From 4151343ce390269a3dfc0276408990f249088389 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Fri, 11 Jun 2021 21:52:19 -0500 Subject: [PATCH 221/288] whitespace --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 238f8f53..7490bb8e 100644 --- a/.gitignore +++ b/.gitignore @@ -102,4 +102,4 @@ invoke cloud_functions/heritability_run/strain_data.tsv base/bam_bai_signed_download_script.sh -uploads/ \ No newline at end of file +uploads/ From d4bef489b7e5e441313b14db0193118f74496505 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Fri, 11 Jun 2021 22:28:33 -0500 Subject: [PATCH 222/288] fix file hashing --- base/utils/data_utils.py | 14 +++++++++++--- base/views/mapping.py | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/base/utils/data_utils.py b/base/utils/data_utils.py index cf6f6810..521e7192 100644 --- a/base/utils/data_utils.py +++ b/base/utils/data_utils.py @@ -100,10 +100,18 @@ def hash_it(object, length=10): return hashlib.sha1(str(object).encode('utf-8')).hexdigest()[0:length] -def hash_file_upload(file, length=10): +def hash_file_upload(filename, length=10): ''' Computes the sha1 hash of a file upload (FileStorage object) ''' - logger.debug(file) - return hashlib.sha1(file.read()).hexdigest()[0:length] + logger.debug(filename) + BLOCKSIZE = 65536 + hasher = hashlib.sha1() + with open(filename, 'rb') as afile: + buf = afile.read(BLOCKSIZE) + while len(buf) > 0: + hasher.update(buf) + buf = afile.read(BLOCKSIZE) + + return hasher.hexdigest()[0:length] def hash_password(password): diff --git a/base/views/mapping.py b/base/views/mapping.py index 68228199..be99a699 100644 --- a/base/views/mapping.py +++ b/base/views/mapping.py @@ -84,7 +84,7 @@ def schedule_mapping(): local_path = os.path.join(uploads_dir, f'{id}.tsv') file.save(local_path) - data_hash = hash_file_upload(file, length=32) + data_hash = hash_file_upload(local_path, length=32) data_blob = f"reports/nemascan/{data_hash}/data.tsv" results_path = f"reports/nemascan/{data_hash}/results/" results = list_files(results_path) From ebb6e689acd7498e4a59704a99e13746655ea0f9 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 14 Jun 2021 00:57:35 -0500 Subject: [PATCH 223/288] fix button colors, update queue --- base/templates/tools/heritability_calculator.html | 8 ++++---- base/templates/tools/heritability_results.html | 6 +++--- base/templates/tools/indel_primer.html | 7 ++----- base/views/tools/heritability.py | 4 ++-- queue.yaml | 6 ++++-- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/base/templates/tools/heritability_calculator.html b/base/templates/tools/heritability_calculator.html index bb311a71..4e07fb4d 100644 --- a/base/templates/tools/heritability_calculator.html +++ b/base/templates/tools/heritability_calculator.html @@ -43,13 +43,13 @@
    {# /col-md-3 #}
    - + My Reports @@ -66,14 +66,14 @@
    {# /col-md-3 #}
    - + Calculate Heritability diff --git a/base/templates/tools/heritability_results.html b/base/templates/tools/heritability_results.html index fcdfd1be..1f45c098 100644 --- a/base/templates/tools/heritability_results.html +++ b/base/templates/tools/heritability_results.html @@ -92,19 +92,19 @@

    - + Download PDF diff --git a/base/templates/tools/indel_primer.html b/base/templates/tools/indel_primer.html index 9b3a99af..440d6e45 100644 --- a/base/templates/tools/indel_primer.html +++ b/base/templates/tools/indel_primer.html @@ -34,16 +34,13 @@ {# /row #} -
    {# /container #} - -
    @@ -55,7 +52,7 @@
    {{ render_field(form.stop, placeholder="2,029,217") }}

    - +
    diff --git a/base/views/tools/heritability.py b/base/views/tools/heritability.py index 60bfe32a..188d80c3 100644 --- a/base/views/tools/heritability.py +++ b/base/views/tools/heritability.py @@ -159,8 +159,8 @@ def heritability_result(id): data = data.to_dict('records') trait = data[0]['TraitName'] # Get trait and set title - title = f"Heritability Results: {trait}" - + subtitle = trait + if result: hr.status = 'COMPLETE' hr.save() diff --git a/queue.yaml b/queue.yaml index f424a517..d656e834 100644 --- a/queue.yaml +++ b/queue.yaml @@ -17,6 +17,8 @@ queue: max_doublings: 2 - name: nscalc max_concurrent_requests: 1 - rate: 1/s + rate: 0.01/s retry_parameters: - task_retry_limit: 1 + task_retry_limit: 2 + min_backoff_seconds: 300 + max_backoff_seconds: 3000 From cb390e5fa8216091db36474237c48d7afdbc249c Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 14 Jun 2021 00:58:18 -0500 Subject: [PATCH 224/288] fix warning snackbar readability --- base/static/css/styles.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/base/static/css/styles.css b/base/static/css/styles.css index a822e06d..194da0f0 100644 --- a/base/static/css/styles.css +++ b/base/static/css/styles.css @@ -640,10 +640,12 @@ article { .alert-danger { background-color: #FFC520; + color: black; } .alert-warning { background-color: #FFC520; + color: black; } .page-title-txt { @@ -991,4 +993,5 @@ article { .nu-alt-btn { background-color: #FFA500; + color: #000000; } From c4659557f5b253f3c0b3c9651bb9b8209e6de204 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 14 Jun 2021 01:02:28 -0500 Subject: [PATCH 225/288] add mapping results pages --- base/templates/mapping.html | 11 ++- base/templates/mapping_result.html | 82 +++++++++++++++++++-- base/templates/mapping_result_files.html | 15 ++++ base/templates/mapping_result_list.html | 2 +- base/views/mapping.py | 92 +++++++++++++++++++----- 5 files changed, 175 insertions(+), 27 deletions(-) create mode 100644 base/templates/mapping_result_files.html diff --git a/base/templates/mapping.html b/base/templates/mapping.html index c861789d..8fb22bdd 100644 --- a/base/templates/mapping.html +++ b/base/templates/mapping.html @@ -47,12 +47,19 @@
    -
    +
    - + My Mapping Reports
    {# /col #} +
    + + + +
    {# /col #} + +
    {# /row #}
    {# /form-group #} diff --git a/base/templates/mapping_result.html b/base/templates/mapping_result.html index 756c05ac..3e8483b1 100644 --- a/base/templates/mapping_result.html +++ b/base/templates/mapping_result.html @@ -3,7 +3,7 @@ {% block custom_head %} {% if not result %} - + {% endif %} {% endblock %} @@ -11,22 +11,92 @@ {% block content %} -{% if data and not result %} +{% if not result %}
    -
    -
    +

    The genome-wide association mapping is currently being run. Please check back in a few minutes for results. - This page will reload automatically.

    -
    {# /col-md-8 #} +
    {# /col-md-12 #}
    {# /row #} {% else %} + {% if report_path %} + + {# /row #} + + +
    +
    +
    +
    +
    {# /row #} + + + {% endif %} + + {% endif %} {% endblock %} + +{% block script %} + + + + +{% endblock %} \ No newline at end of file diff --git a/base/templates/mapping_result_files.html b/base/templates/mapping_result_files.html new file mode 100644 index 00000000..ab3d025c --- /dev/null +++ b/base/templates/mapping_result_files.html @@ -0,0 +1,15 @@ +{% extends "_layouts/default.html" %} + + +{% block content %} + +{% for file in file_list %} + +

    + {{ file.name }} +

    + +{% endfor %} + + +{% endblock %} diff --git a/base/templates/mapping_result_list.html b/base/templates/mapping_result_list.html index f985b24d..9a19f863 100644 --- a/base/templates/mapping_result_list.html +++ b/base/templates/mapping_result_list.html @@ -49,7 +49,7 @@ {{ item.trait }} {% if item.status == 'COMPLETE' %} - + {{ item.status }} {% else %} diff --git a/base/views/mapping.py b/base/views/mapping.py index be99a699..cdc6448e 100644 --- a/base/views/mapping.py +++ b/base/views/mapping.py @@ -1,6 +1,6 @@ import decimal import re -import arrow +import csv import urllib import pandas as pd import simplejson as json @@ -13,7 +13,7 @@ from flask import session, flash, Blueprint, g -from base.constants import BIOTYPES, TABLE_COLORS +from base.constants import BIOTYPES, GOOGLE_CLOUD_BUCKET, TABLE_COLORS from base.config import config from base.models import trait_ds, ns_calc_ds from base.forms import file_upload_form @@ -58,7 +58,7 @@ def create_ns_task(data_hash, ds_id, ds_kind): ns.save() -@mapping_bp.route('/upload', methods = ['POST']) +@mapping_bp.route('/mapping/upload', methods = ['POST']) @jwt_required() def schedule_mapping(): ''' @@ -84,15 +84,33 @@ def schedule_mapping(): local_path = os.path.join(uploads_dir, f'{id}.tsv') file.save(local_path) + # Read trait from file (also verify first row) + with open(local_path, 'r') as f: + csv_reader = csv.reader(f, delimiter='\t') + csv_headings = next(csv_reader) + if csv_headings[0] != 'strain' or len(csv_headings) != 2 or len(csv_headings[1]) == 0: + flash("Please make sure that your data file exactly matches the sample format") + return redirect(url_for('mapping.mapping')) + + trait = csv_headings[1] data_hash = hash_file_upload(local_path, length=32) + + # Update report status + ns.data_hash = data_hash + ns.trait = trait + ns.status = 'RECEIVED' + ns.save() + + # Check if the file already exists in google storage (matching hash) data_blob = f"reports/nemascan/{data_hash}/data.tsv" - results_path = f"reports/nemascan/{data_hash}/results/" - results = list_files(results_path) + data_exists = list_files(data_blob) # if there is anything in the results directory, don't schedule the task # (could be running, failed, etc.. need to check result directory in more detail to confirm state) - if len(results) > 0: - return redirect(url_for('mapping.mapping_result', id=id)) + if len(data_exists) > 0: + flash('It looks like that data has already been uploaded - You will be redirected to the saved results', 'danger') + return redirect(url_for('mapping.mapping_report', id=id)) + # Upload file to google storage result = upload_file(data_blob, local_path) if not result: ns.status = 'ERROR UPLOADING' @@ -100,38 +118,76 @@ def schedule_mapping(): flash("There was an error uploading your data") return redirect(url_for('mapping.mapping')) - # Update report status - ns.data_hash = data_hash - ns.status = 'RECEIVED' - ns.save() - # Schedule task create_ns_task(data_hash, id, ns.kind) + + # Delete copy stored locally on server + os.remove(local_path) return redirect(url_for('mapping.mapping_report', id=id)) @mapping_bp.route('/mapping/report/all', methods=['GET', 'POST']) @jwt_required() -def mapping_result_list(id): - title = 'Genetic Mapping Results' +def mapping_result_list(): + title = 'Genetic Mapping' + subtitle = 'Report List' user = get_current_user() items = ns_calc_ds().query_by_username(user.name) items = sorted(items, key=lambda x: x['created_on'], reverse=True) - return render_template('mapping_result.html', **locals()) - + return render_template('mapping_result_list.html', **locals()) -@mapping_bp.route('/mapping/report/', methods=['GET']) +@mapping_bp.route('/mapping/report//', methods=['GET']) @jwt_required() def mapping_report(id): - title = 'Genetic Mapping Report' + title = 'Genetic Mapping' user = get_current_user() ns = ns_calc_ds(id) + fluid_container = True + subtitle = ns.label +': ' + ns.trait + + # check if DS entry has complete status + data_hash = ns.data_hash + if (ns.status == 'COMPLETE' and len(ns.report_path) > 0): + report_path = ns.report_path + result = True + else: + # check if there is a report on GS, just to be sure + data_blob = f"reports/nemascan/{data_hash}/results/Reports/NemaScan_Report_" + result = list_files(data_blob) + if len(result) > 0: + for x in result: + if x.name.endswith('.html'): + # Store report URL in DS + report_path = GOOGLE_CLOUD_BUCKET + '/' + x.name + ns.report_path = report_path + ns.status = 'COMPLETE' + result = True + ns.save() return render_template('mapping_result.html', **locals()) +@mapping_bp.route('/mapping/results//', methods=['GET']) +@jwt_required() +def mapping_results(id): + title = 'Genetic Mapping' + subtitle = 'Result Files' + user = get_current_user() + ns = ns_calc_ds(id) + + data_blob = f"reports/nemascan/{ns.data_hash}/results/" + blobs = list_files(data_blob) + file_list = [] + for blob in blobs: + file_list.append({ + "name": blob.name.rsplit('/', 2)[1] + '/' + blob.name.rsplit('/', 2)[2], + "url": blob.public_url + }) + return render_template('mapping_result_files.html', **locals()) + + @mapping_bp.route('/mapping/perform-mapping/', methods=['GET', 'POST']) @jwt_required() def mapping(): From e5f6edf8c5c92391ae56df806488badf661a700c Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 14 Jun 2021 01:02:41 -0500 Subject: [PATCH 226/288] link profile to mapping results page --- base/templates/user/profile.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/base/templates/user/profile.html b/base/templates/user/profile.html index 4a3eaf53..f2e5bcb2 100644 --- a/base/templates/user/profile.html +++ b/base/templates/user/profile.html @@ -15,6 +15,8 @@ + + From b7b4d36a97b3871c4323c85635d2a2cc56621d3f Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 14 Jun 2021 10:17:47 -0500 Subject: [PATCH 227/288] add link to partial result filelist --- base/templates/mapping_result_list.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/base/templates/mapping_result_list.html b/base/templates/mapping_result_list.html index 9a19f863..afa30a5a 100644 --- a/base/templates/mapping_result_list.html +++ b/base/templates/mapping_result_list.html @@ -53,7 +53,9 @@ {{ item.status }} {% else %} - {{ item.status }} + + {{ item.status }} + {% endif %} {% endif %} From 634330c4047b533a083a83eb578bb23a28f24418 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 14 Jun 2021 10:20:04 -0500 Subject: [PATCH 228/288] cleanup checks for existing data, results; switch task id from data_hash to uuid because it makes testing painful --- base/utils/data_utils.py | 4 +- base/views/mapping.py | 83 +++++++++++++++++++++++++++------------- 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/base/utils/data_utils.py b/base/utils/data_utils.py index 521e7192..6b103126 100644 --- a/base/utils/data_utils.py +++ b/base/utils/data_utils.py @@ -100,8 +100,8 @@ def hash_it(object, length=10): return hashlib.sha1(str(object).encode('utf-8')).hexdigest()[0:length] -def hash_file_upload(filename, length=10): - ''' Computes the sha1 hash of a file upload (FileStorage object) ''' +def hash_file_contents(filename, length=10): + ''' Computes the sha1 hash of a file's contents ''' logger.debug(filename) BLOCKSIZE = 65536 hasher = hashlib.sha1() diff --git a/base/views/mapping.py b/base/views/mapping.py index cdc6448e..34ba0737 100644 --- a/base/views/mapping.py +++ b/base/views/mapping.py @@ -17,7 +17,7 @@ from base.config import config from base.models import trait_ds, ns_calc_ds from base.forms import file_upload_form -from base.utils.data_utils import unique_id, hash_file_upload +from base.utils.data_utils import unique_id, hash_file_contents from base.utils.gcloud import check_blob, list_files, query_item, delete_item, upload_file, add_task from base.utils.jwt_utils import jwt_required, get_jwt, get_current_user from base.utils.plots import pxg_plot, plotly_distplot @@ -27,6 +27,10 @@ __name__, template_folder='mapping') +DATA_BLOB_PATH = 'reports/nemascan/{data_hash}/data.tsv' +REPORT_BLOB_PATH = 'reports/nemascan/{data_hash}/results/Reports/NemaScan_Report_' +RESULT_BLOB_PATH = 'reports/nemascan/{data_hash}/results/' + # Create a directory in a known location to save files to. uploads_dir = os.path.join('./', 'uploads') @@ -51,12 +55,42 @@ def create_ns_task(data_hash, ds_id, ds_kind): queue = config['NEMASCAN_PIPELINE_TASK_QUEUE'] url = config['NEMASCAN_PIPELINE_URL'] data = {'hash': data_hash, 'ds_id': ds_id, 'ds_kind': ds_kind} - result = add_task(queue, url, data, task_name=data_hash) + result = add_task(queue, url, data, task_name=ds_id) # Update report status - ns.status = 'SCHEDULED' if result else 'FAILED' - ns.save() + if result: + ns.status = 'SCHEDULED' + else: + ns.status = 'FAILED' + + return result + +def is_data_cached(data_hash): + # Check if the file already exists in google storage (matching hash) + data_blob = DATA_BLOB_PATH.format(data_hash=data_hash) + data_exists = list_files(data_blob) + if len(data_exists) > 0: + return True + return False + +def is_result_cached(ns): + if ns.status == 'COMPLETE' and len(ns.report_path) > 0: + return True + + # check if there is a report on GS, just to be sure + data_blob = REPORT_BLOB_PATH.format(data_hash=ns.data_hash) + result = list_files(data_blob) + if len(result) > 0: + for x in result: + if x.name.endswith('.html'): + report_path = GOOGLE_CLOUD_BUCKET + '/' + x.name + ns.report_path = report_path + ns.status = 'COMPLETE' + ns.save() + return True + else: + return False @mapping_bp.route('/mapping/upload', methods = ['POST']) @jwt_required() @@ -79,21 +113,24 @@ def schedule_mapping(): ns.status = 'NEW' ns.save() - # Upload file to cloud bucket + # Save uploaded file to server temporarily file = request.files['file'] local_path = os.path.join(uploads_dir, f'{id}.tsv') file.save(local_path) - # Read trait from file (also verify first row) + # Read first line from tsv with open(local_path, 'r') as f: csv_reader = csv.reader(f, delimiter='\t') csv_headings = next(csv_reader) + + # Check first line for column headers (strain, {TRAIT}) if csv_headings[0] != 'strain' or len(csv_headings) != 2 or len(csv_headings[1]) == 0: + os.remove(local_path) flash("Please make sure that your data file exactly matches the sample format") return redirect(url_for('mapping.mapping')) trait = csv_headings[1] - data_hash = hash_file_upload(local_path, length=32) + data_hash = hash_file_contents(local_path, length=32) # Update report status ns.data_hash = data_hash @@ -101,16 +138,12 @@ def schedule_mapping(): ns.status = 'RECEIVED' ns.save() - # Check if the file already exists in google storage (matching hash) - data_blob = f"reports/nemascan/{data_hash}/data.tsv" - data_exists = list_files(data_blob) - # if there is anything in the results directory, don't schedule the task - # (could be running, failed, etc.. need to check result directory in more detail to confirm state) - if len(data_exists) > 0: + if is_data_cached(data_hash): flash('It looks like that data has already been uploaded - You will be redirected to the saved results', 'danger') return redirect(url_for('mapping.mapping_report', id=id)) # Upload file to google storage + data_blob = DATA_BLOB_PATH.format(data_hash=data_hash) result = upload_file(data_blob, local_path) if not result: ns.status = 'ERROR UPLOADING' @@ -119,11 +152,15 @@ def schedule_mapping(): return redirect(url_for('mapping.mapping')) # Schedule task - create_ns_task(data_hash, id, ns.kind) + task_result = create_ns_task(data_hash, id, ns.kind) # Delete copy stored locally on server os.remove(local_path) + if not task_result: + flash("There was an error scheduling your calculations...") + redirect(url_for('mapping.mapping')) + return redirect(url_for('mapping.mapping_report', id=id)) @@ -154,17 +191,7 @@ def mapping_report(id): result = True else: # check if there is a report on GS, just to be sure - data_blob = f"reports/nemascan/{data_hash}/results/Reports/NemaScan_Report_" - result = list_files(data_blob) - if len(result) > 0: - for x in result: - if x.name.endswith('.html'): - # Store report URL in DS - report_path = GOOGLE_CLOUD_BUCKET + '/' + x.name - ns.report_path = report_path - ns.status = 'COMPLETE' - result = True - ns.save() + result = is_result_cached(ns) return render_template('mapping_result.html', **locals()) @@ -176,8 +203,9 @@ def mapping_results(id): subtitle = 'Result Files' user = get_current_user() ns = ns_calc_ds(id) - - data_blob = f"reports/nemascan/{ns.data_hash}/results/" + result = is_result_cached(ns) + + data_blob = RESULT_BLOB_PATH.format(data_hash=ns.data_hash) blobs = list_files(data_blob) file_list = [] for blob in blobs: @@ -185,6 +213,7 @@ def mapping_results(id): "name": blob.name.rsplit('/', 2)[1] + '/' + blob.name.rsplit('/', 2)[2], "url": blob.public_url }) + return render_template('mapping_result_files.html', **locals()) From c2a8ad9c624be35f4338b1231e4b6b4272baed1e Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 14 Jun 2021 10:24:26 -0500 Subject: [PATCH 229/288] consistency --- base/static/css/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/static/css/styles.css b/base/static/css/styles.css index 194da0f0..48c71f8b 100644 --- a/base/static/css/styles.css +++ b/base/static/css/styles.css @@ -871,7 +871,7 @@ article { .btn-alt { background-color:#FFC400; - color: #000000; + color:black; } .welcome-article-img { From 4dd6c58d222b1d81dabddb1cf19f54d31e435a68 Mon Sep 17 00:00:00 2001 From: Sam Wachspress Date: Mon, 14 Jun 2021 12:07:47 -0500 Subject: [PATCH 230/288] update mapping result views --- base/templates/mapping_result.html | 36 +++++++++++------------- base/templates/mapping_result_files.html | 14 +++++++++ base/templates/mapping_result_list.html | 2 +- 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/base/templates/mapping_result.html b/base/templates/mapping_result.html index 3e8483b1..d09b30de 100644 --- a/base/templates/mapping_result.html +++ b/base/templates/mapping_result.html @@ -11,22 +11,6 @@ {% block content %} -{% if not result %} - -
    -
    -

    - - The genome-wide association mapping is currently being run. Please check back in a few minutes for results. - -

    -
    {# /col-md-12 #} -
    {# /row #} - -{% else %} - - {% if report_path %} -
    + {% if report_path %} + {% else %} + + {% endif %} Download Report @@ -49,25 +37,35 @@
    {# /row #} +{% if result %} +
    {# /row #} +{% else %} - {% endif %} - +
    +
    +

    + + The genome-wide association mapping is currently being run - please check back in a few hours for results. + +

    +
    {# /col-md-12 #} +
    {# /row #} {% endif %} {% endblock %} + {% block script %}