diff --git a/CHANGELOG.md b/CHANGELOG.md index 1097f706..8c91b599 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,12 +16,14 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) - Shift - Click now Zoom in ### Changed - Refactored page definitions into blueprint module + - Removed entrypoint script ### Fixed - Navigation shortcuts does not trigger in text fields - Fixed crash when searching for only chromosome - Restored ability to search for transcripts by gene name - Fixed crash when Shift - Click in interactive canvas - Fixed checking of api return status in drawInteractiveContent + - Aligned highlight in interactive canvas ## [1.1.1] ### Fixed diff --git a/Dockerfile b/Dockerfile index a841976f..71542196 100644 --- a/Dockerfile +++ b/Dockerfile @@ -59,8 +59,7 @@ COPY --from=node-builder /usr/src/app/build/*/gens.min.* gens/blueprints/gens/st # Chown all the files to the app user COPY gens gens COPY utils utils -COPY entrypoint.sh ./ -RUN chown -R app:app /home/app/app +# make mountpoints and change ownership of app +RUN mkdir -p /media /access /fs1 && chown -R app:app /home/app/app /media /access /fs1 # Change the user to app USER app -ENTRYPOINT ["./entrypoint.sh"] diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 00000000..275bf9a3 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,31 @@ +# Instructions to release a new version of Gens + +1. Create a release branch with the release name, e.g. `release-1.1.1` and checkout the branch + + ```bash + git checkout -b release-1.1.1 + ``` + +2. Update version to, e.g. 1.1.1 + + - in `gens/__version__.py` + - in `package.json` + +3. Make sure `CHANGELOG.md`is up to date for the release + + +4. Commit changes, push to github and create a pull request + + ```bash + git add gens/__version__.py + git add package.json CHANGELOG.md + git commit -m "Release notes version 1.1.1" + git push -u origin release-1.1.1 + ``` + +5. On github click **create pull request**. + +6. After getting the pull request approved by a reviewer merge it to master. + +7. Draft a new release on GitHub, add some text - e.g. and abbreviated CHANGELOG - and release. +This adds a version tag, builds docker image. diff --git a/assets/css/gens.scss b/assets/css/gens.scss index 96d13ce2..1a53525a 100644 --- a/assets/css/gens.scss +++ b/assets/css/gens.scss @@ -280,9 +280,8 @@ html, body { } #region_field.error:disabled { - outline: 1px dotted red; - outline: 5px auto red; - background-color: $default-bg-color; + outline: 5px dotted red; + background-color: red; } #times { diff --git a/assets/js/interactive.js b/assets/js/interactive.js index 30a558be..d2a7fbb6 100644 --- a/assets/js/interactive.js +++ b/assets/js/interactive.js @@ -115,7 +115,7 @@ class InteractiveCanvas extends FrequencyTrack { // Initialize marker div this.markerElem = document.getElementById('interactive-marker'); this.markerElem.style.height = `${this.plotHeight * 2}px`; - this.markerElem.style.top = `${this.y + 58}px`; + this.markerElem.style.top = `${this.y + 82}px`; // State values const input = inputField.value.split(/:|-/); @@ -209,7 +209,13 @@ class InteractiveCanvas extends FrequencyTrack { // numerical sort const [start, end] = [this.start + Math.round((this.dragStart.x - this.x) / scale), this.start + Math.round((this.dragEnd.x - this.x) / scale)].sort((a, b) => a - b); - this.loadChromosome(this.chromosome, start, end) + // if shift - click, zoom in a region 10 + // fix for slowdown when shift clicking + if ( ( end - start ) < 10 ) { + this.zoomIn(); + } + // + this.loadChromosome(this.chromosome, start, end + 1); } // reload window when stop draging if (this.drag) { @@ -233,52 +239,55 @@ class InteractiveCanvas extends FrequencyTrack { const keystrokeDelay = 2000; document.addEventListener('keyevent', event => { const key = event.detail.key; - const excludeFileds = ['input', 'select', 'textarea']; - if ( key === 'Enter' ) { - // Enter was pressed, process previous key presses. - const recentKeys = this.keyLogger.recentKeys(keystrokeDelay); - recentKeys.pop(); // skip Enter key - const lastKey = recentKeys[recentKeys.length - 1]; - const numKeys = parseInt((recentKeys - .slice(lastKey.length - 2) - .filter(val => parseInt(val.key)) - .map(val => val.key) - .join(''))) - // process keys - if ( lastKey.key == 'x' || lastKey.key == 'y' ) { - this.loadChromosome(lastKey.key); - } else if ( numKeys && 0 < numKeys < 23 ) { - this.loadChromosome(numKeys); - } else { - return; + // dont act on key presses in input fields + const excludeFileds = ['input', 'select', 'textarea']; + if ( !excludeFileds.includes(event.detail.target.toLowerCase()) ) { + if ( key === 'Enter' ) { + // Enter was pressed, process previous key presses. + const recentKeys = this.keyLogger.recentKeys(keystrokeDelay); + recentKeys.pop(); // skip Enter key + const lastKey = recentKeys[recentKeys.length - 1]; + const numKeys = parseInt((recentKeys + .slice(lastKey.length - 2) + .filter(val => parseInt(val.key)) + .map(val => val.key) + .join(''))) + // process keys + if ( lastKey.key == 'x' || lastKey.key == 'y' ) { + this.loadChromosome(lastKey.key); + } else if ( numKeys && 0 < numKeys < 23 ) { + this.loadChromosome(numKeys); + } else { + return; + } + } + switch (key) { + case 'ArrowLeft': + this.nextChromosome() + break; + case 'ArrowRight': + this.previousChromosome() + break; + case 'a': + this.panTracksLeft(); + break; + case 'd': + this.panTracksRight(); + break; + case 'ArrowUp': + case 'w': + case '+': + this.zoomIn(); + break; + case 'ArrowDown': + case 's': + case '-': + this.zoomOut(); + break; + default: + return; } - } - switch (key) { - case 'ArrowLeft': - this.nextChromosome() - break; - case 'ArrowRight': - this.previousChromosome() - break; - case 'a': - this.panTracksLeft(); - break; - case 'd': - this.panTracksRight(); - break; - case 'ArrowUp': - case 'w': - case '+': - this.zoomIn(); - break; - case 'ArrowDown': - case 's': - case '-': - this.zoomOut(); - break; - default: - return; } }); }); @@ -346,6 +355,9 @@ class InteractiveCanvas extends FrequencyTrack { reduce_data: 1, }).then( (result) => { console.timeEnd('getcoverage'); + if ( result.status == "error" ) { + throw result; + } // Clear canvas this.contentCanvas.getContext('2d').clearRect(0, 0, this.contentCanvas.width, this.contentCanvas.height); diff --git a/assets/js/track.js b/assets/js/track.js index 9a075130..eba84ce5 100644 --- a/assets/js/track.js +++ b/assets/js/track.js @@ -60,14 +60,17 @@ class Track { const chromosomes = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', 'X', 'Y'] - const chromosome = regionString.split(':')[0]; - if ( !chromosomes.includes(chromosome) ) { - throw `${chromosome} is not a valid chromosome`; + if ( regionString.includes(':') ) { + const [chromosome, position] = regionString.split(':'); + // verify chromosome + if ( !chromosomes.includes(chromosome) ) { + throw `${chromosome} is not a valid chromosome`; + } + let [start, end] = position.split('-'); + start = parseInt(start); + end = parseInt(end); + return [chromosome, start, end]; } - let [start, end] = regionString.split(':')[1].split('-'); - start = parseInt(start); - end = parseInt(end); - return [chromosome, start, end]; } tracksYPos(heightOrder) { diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100755 index d08c2266..00000000 --- a/entrypoint.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -set -e - -# Setup production configuration -if [[ ! "${FLASK_ENV}" == development ]]; then - echo "Mount Lennart access drive" - mkdir -p "${HOME}/access" - sshfs \ - -o reconnect,transform_symlinks,idmap=user \ - -o ro,ServerAliveInterval=30,ServerAliveCountMax=5 \ - -o StrictHostKeyChecking=no \ - -p 22 \ - worker@lennart:/media/isilon/backup_hopper/results "${HOME}/access" -fi - -# run commands given to container -echo "Executing command: ${@}" -exec "${@}" diff --git a/gens/__init__.py b/gens/__init__.py index d1cdbff1..63aa19ad 100644 --- a/gens/__init__.py +++ b/gens/__init__.py @@ -1,2 +1,2 @@ -from .app import create_app from .__version__ import VERSION as version +from .app import create_app diff --git a/gens/api.py b/gens/api.py index cb06dcbd..a143841c 100644 --- a/gens/api.py +++ b/gens/api.py @@ -7,47 +7,19 @@ from typing import List import attr -from flask import abort, current_app, jsonify, request - import cattr import connexion +from flask import abort, current_app, jsonify, request + from gens.db import RecordType, VariantCategory, query_records_in_region, query_variants from gens.exceptions import RegionParserException from gens.graph import REQUEST, get_cov, overview_chrom_dimensions, parse_region_str +from .constants import CHROMOSOMES, HG_TYPE from .io import get_overview_json_path, get_tabix_files LOG = logging.getLogger(__name__) -CHROMOSOMES = [ - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "10", - "11", - "12", - "13", - "14", - "15", - "16", - "17", - "18", - "19", - "20", - "21", - "22", - "X", - "Y", -] - -HG_TYPE = (38, 19) - @attr.s(auto_attribs=True, frozen=True) class ChromosomePosition: @@ -173,9 +145,6 @@ def get_transcript_data(region, hg_type, collapsed): LOG.error("Could not find transcript in database") return abort(404) - with current_app.app_context(): - collection = current_app.config["GENS_DB"][f"transcripts{hg_type}"] - # Get transcripts within span [start_pos, end_pos] or transcripts that go over the span transcripts = list( query_records_in_region( @@ -201,6 +170,36 @@ def get_transcript_data(region, hg_type, collapsed): ) +def search_annotation(query: str, hg_type, annotation_type): + """Search for anntations of genes and return their position.""" + # Lookup queried element + collection = current_app.config["GENS_DB"][annotation_type] + db_query = {"gene_name": re.compile("^" + re.escape(query) + "$", re.IGNORECASE)} + + if hg_type and int(hg_type) in HG_TYPE: + db_query['hg_type'] = hg_type + + elements = collection.find(db_query, sort=[("start", 1), ("chrom", 1)]) + # if no results was found + if elements.count() == 0: + msg = f"Did not find gene name: {query}" + LOG.warning(msg) + data = {'message': msg} + response_code = 404 + else: + start_elem = elements.next() + end_elem = max(elements, key=lambda elem: elem.get('end')) + data = { + 'chromosome': start_elem.get('chrom'), + 'start_pos': start_elem.get('start'), + 'end_pos': end_elem.get('end'), + 'hg_type': start_elem.get('hg_type'), + } + response_code = 200 + + return jsonify({**data, 'status': response_code}) + + def get_variant_data(sample_id, variant_category, **optional_kwargs): """Search Scout database for variants associated with a case and return info in JSON format.""" default_height_order = 0 diff --git a/gens/app.py b/gens/app.py index bf505a45..1ae8beab 100644 --- a/gens/app.py +++ b/gens/app.py @@ -6,20 +6,19 @@ from datetime import date from logging.config import dictConfig +import connexion from flask import abort, render_template, request from flask_compress import Compress from flask_debugtoolbar import DebugToolbarExtension -import connexion - from .__version__ import VERSION as version -from .blueprints import gens_bp, about_bp +from .blueprints import about_bp, gens_bp from .cache import cache from .db import init_database +from .errors import generic_error, sample_not_found from .graph import parse_region_str from .io import BAF_SUFFIX, COV_SUFFIX, _get_filepath from .utils import get_hg_type -from .errors import generic_error, sample_not_found toolbar = DebugToolbarExtension() dictConfig( diff --git a/gens/blueprints/__init__.py b/gens/blueprints/__init__.py index 78765abc..20eb4ce2 100644 --- a/gens/blueprints/__init__.py +++ b/gens/blueprints/__init__.py @@ -1,2 +1,2 @@ -from .gens.views import gens_bp from .about.views import about_bp +from .gens.views import gens_bp diff --git a/gens/blueprints/about/views.py b/gens/blueprints/about/views.py index ed9ffdca..71088491 100644 --- a/gens/blueprints/about/views.py +++ b/gens/blueprints/about/views.py @@ -6,7 +6,6 @@ import gens - LOG = logging.getLogger(__name__) about_bp = Blueprint("about", __name__, template_folder="templates") diff --git a/gens/blueprints/gens/templates/gens.html b/gens/blueprints/gens/templates/gens.html index 53a66902..8f224a07 100644 --- a/gens/blueprints/gens/templates/gens.html +++ b/gens/blueprints/gens/templates/gens.html @@ -183,8 +183,25 @@ // Redraw when new region is requested document.getElementById('region_form').addEventListener('submit', function (event) { + const chromosomes = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', + '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', + '22', 'X', 'Y'] event.preventDefault(); - ic.redraw(inputField.value); + // if input contains both : and - + if (inputField.value.includes(':') && + inputField.value.includes(':')) { + ic.redraw(inputField.value); + } else if (chromosomes.includes(inputField.value)) { + ic.redraw(`${inputField.value}:0-None`); + } else { + get('search-annotation', {query: inputField.value, + hg_type: hgType}) + .then( (result) => { + if ( result.status == 200 ) { + ic.redraw(`${result.chromosome}:${result.start_pos}-${result.end_pos}`) + } + }); + } }); diff --git a/gens/blueprints/gens/views.py b/gens/blueprints/gens/views.py index 3d6d56d2..d202ea06 100644 --- a/gens/blueprints/gens/views.py +++ b/gens/blueprints/gens/views.py @@ -1,6 +1,7 @@ """Functions for rendering Gens""" import logging +from datetime import date from flask import Blueprint, abort, current_app, render_template, request @@ -9,8 +10,6 @@ from gens.graph import parse_region_str from gens.io import BAF_SUFFIX, COV_SUFFIX, _get_filepath from gens.utils import get_hg_type -from datetime import date - LOG = logging.getLogger(__name__) diff --git a/gens/constants.py b/gens/constants.py new file mode 100644 index 00000000..693b3557 --- /dev/null +++ b/gens/constants.py @@ -0,0 +1,30 @@ +"""Constant variables used throught the app.""" + +CHROMOSOMES = ( + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "X", + "Y", +) + +HG_TYPE = (38, 19) diff --git a/gens/errors.py b/gens/errors.py index 8fcfb0e5..ecdc9086 100644 --- a/gens/errors.py +++ b/gens/errors.py @@ -1,8 +1,9 @@ """Defenition of custom error pages""" -from flask import render_template import os +from flask import render_template + def sample_not_found(error): """Resource not found.""" diff --git a/gens/graph.py b/gens/graph.py index 92a651e6..7e9ed098 100644 --- a/gens/graph.py +++ b/gens/graph.py @@ -8,6 +8,7 @@ from flask import request from .cache import cache +from .constants import CHROMOSOMES from .db import RecordType from .exceptions import NoRecordsException, RegionParserException from .io import tabix_query @@ -35,33 +36,6 @@ ), ) -CHROMOSOMES = [ - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "10", - "11", - "12", - "13", - "14", - "15", - "16", - "17", - "18", - "19", - "20", - "21", - "22", - "X", - "Y", -] - @cache.memoize(0) def convert_data( diff --git a/gens/openapi/openapi.yaml b/gens/openapi/openapi.yaml index 8e3e4f58..9e1f14c9 100644 --- a/gens/openapi/openapi.yaml +++ b/gens/openapi/openapi.yaml @@ -358,6 +358,47 @@ paths: type: integer res: type: string + /search-annotation: + get: + summary: Search for an annotation element + description: Search for a region in the annotation data. + operationId: gens.api.search_annotation + parameters: + - name: query + in: query + description: Name of element of interest + required: true + schema: + type: string + - name: hg_type + in: query + required: false + schema: + $ref: '#/components/schemas/HgType' + - name: annotation_type + in: query + description: Type of annotation + schema: + type: string + default: transcripts + responses: + '200': + description: Annotation positional information + content: + application/json: + schema: + type: object + properties: + chromosome: + type: string + start_pos: + type: number + end_pos: + type: number + hg_type: + type: string + status: + type: number /get-annotation-sources: get: summary: Get source information for annotations