From e00bfea00d1704498f7e92211d3f42e3352e5020 Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 2 Oct 2023 16:12:27 +0200 Subject: [PATCH 01/42] Testing some stuff about ES --- imports/api/publications.js | 33 +++++++++++++++++++++++++++--- imports/ui/genetable/GeneTable.jsx | 27 ++++++++++++++---------- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/imports/api/publications.js b/imports/api/publications.js index e82906bb..2fb221aa 100644 --- a/imports/api/publications.js +++ b/imports/api/publications.js @@ -1,6 +1,7 @@ import { Mongo } from 'meteor/mongo'; import { Meteor } from 'meteor/meteor'; import { Roles } from 'meteor/alanning:roles'; +import { fetch } from 'meteor/fetch'; // jobqueue import jobQueue from '/imports/api/jobqueue/jobqueue.js'; // genes @@ -76,9 +77,35 @@ Meteor.publish({ const queryGenomeIds = hasOwnProperty(query, 'genomeId') ? query.genomeId.$in.filter((genomeId) => genomeIds.includes(genomeId)) : genomeIds; - - const transformedQuery = { ...query, genomeId: { $in: queryGenomeIds } }; - + let transformedQuery; + + if (typeof Meteor.settings.customSearchOptions === "object" && Meteor.settings.customSearchOptions.url !== undefined){ + let url = Meteor.settings.customSearchOptions.url.replace(/,+$/, "") + "/"; + let paramsDict = {} + let geneField = Meteor.settings.customSearchOptions.gene_field !== undefined ? Meteor.settings.customSearchOptions.gene_field : "geneId" + if (Meteor.settings.customSearchOptions.query_param !== undefined){ + paramsDict[Meteor.settings.customSearchOptions.query_param] = query.query + } else { + url = url += query.query + } + if (Meteor.settings.customSearchOptions.field_param !== undefined){ + paramsDict[Meteor.settings.customSearchOptions.field_param] = geneField + } + + if (Meteor.settings.customSearchOptions.count_param !== undefined){ + paramsDict[Meteor.settings.customSearchOptions.count_param] = limit + } + url = url + new URLSearchParams(paramsDict) + let geneResults = [] + fetch(url).then((res) => { + if (res.status === 200){ + geneResults = res.json().data.map(result => result._source[geneField]) + } + }) + transformedQuery = {genomeId: { $in: queryGenomeIds }, ID: { $in: geneResults }} + } else { + transformedQuery = { ...query, genomeId: { $in: queryGenomeIds } }; + } return Genes.find(transformedQuery, { sort, limit }); }, singleGene({ geneId, transcriptId }) { diff --git a/imports/ui/genetable/GeneTable.jsx b/imports/ui/genetable/GeneTable.jsx index ef60c562..339750a8 100644 --- a/imports/ui/genetable/GeneTable.jsx +++ b/imports/ui/genetable/GeneTable.jsx @@ -62,20 +62,25 @@ function searchTracker({ const attributeString = queryString.get('attributes') || ''; const searchAttributes = attributeString.split(','); - const searchValue = queryString.get('search') || ''; - const searchQuery = { $or: [] }; - attributes - .filter(({ name }) => new RegExp(name).test(searchAttributes)) - .forEach((attribute) => { - searchQuery.$or.push({ - [attribute.query]: { - $regex: searchValue, - $options: 'i', - }, + let searchQuery + + if (Meteor.settings.public.customSearch === true){ + searchQuery = {query: searchValue} + } else { + searchQuery = { $or: [] }; + attributes + .filter(({ name }) => new RegExp(name).test(searchAttributes)) + .forEach((attribute) => { + searchQuery.$or.push({ + [attribute.query]: { + $regex: searchValue, + $options: 'i', + }, + }); }); - }); + } const selectedAttributes = attributes .filter( From 7f27fbb07fe072645316217d86746be34faae8bf Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 3 Oct 2023 15:29:41 +0000 Subject: [PATCH 02/42] testing some stuff --- config.json.template | 10 +++++++++- imports/api/publications.js | 22 ++++++++++++---------- imports/ui/genetable/GeneTable.jsx | 4 ++-- imports/ui/genetable/GeneTableBody.jsx | 2 +- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/config.json.template b/config.json.template index 85c3399e..cb954b9b 100644 --- a/config.json.template +++ b/config.json.template @@ -4,6 +4,14 @@ "disable_user_login": false, "disable_user_registration": false, "blast_link": "", - "expression_unit": "My unit" + "expression_unit": "My unit", + "customSearch": true + }, + "customSearchOptions": { + "url": "http://0.0.0.0:8888", + "gene_field": "gene_id", + "query_param": "q", + "field_param": "display_fields", + "count_param": "max_results" } } diff --git a/imports/api/publications.js b/imports/api/publications.js index 2fb221aa..05bb25b7 100644 --- a/imports/api/publications.js +++ b/imports/api/publications.js @@ -28,6 +28,7 @@ import { fileCollection } from '/imports/api/files/fileCollection.js'; import fetchDbxref from '/imports/api/methods/fetchDbxref.js'; // utilities import { DBXREF_REGEX } from '/imports/api/util/util.js'; +import logger from '/imports/api/util/logger.js' function availableGenomes({ userId }) { const roles = Roles.getRolesForUser(userId); @@ -77,9 +78,11 @@ Meteor.publish({ const queryGenomeIds = hasOwnProperty(query, 'genomeId') ? query.genomeId.$in.filter((genomeId) => genomeIds.includes(genomeId)) : genomeIds; - let transformedQuery; + let transformedQuery = {}; - if (typeof Meteor.settings.customSearchOptions === "object" && Meteor.settings.customSearchOptions.url !== undefined){ + logger.log(query) + + if ( query.query !== undefined && typeof Meteor.settings.customSearchOptions === "object" && Meteor.settings.customSearchOptions.url !== undefined){ let url = Meteor.settings.customSearchOptions.url.replace(/,+$/, "") + "/"; let paramsDict = {} let geneField = Meteor.settings.customSearchOptions.gene_field !== undefined ? Meteor.settings.customSearchOptions.gene_field : "geneId" @@ -94,15 +97,14 @@ Meteor.publish({ if (Meteor.settings.customSearchOptions.count_param !== undefined){ paramsDict[Meteor.settings.customSearchOptions.count_param] = limit + + } + url = url + "?" + new URLSearchParams(paramsDict) + const response = HTTP.get(url); + if (response.statusCode === 200){ + geneResults = response.data.data.map(result => result._source[geneField]) + transformedQuery = {genomeId: { $in: queryGenomeIds }, ID: { $in: geneResults }} } - url = url + new URLSearchParams(paramsDict) - let geneResults = [] - fetch(url).then((res) => { - if (res.status === 200){ - geneResults = res.json().data.map(result => result._source[geneField]) - } - }) - transformedQuery = {genomeId: { $in: queryGenomeIds }, ID: { $in: geneResults }} } else { transformedQuery = { ...query, genomeId: { $in: queryGenomeIds } }; } diff --git a/imports/ui/genetable/GeneTable.jsx b/imports/ui/genetable/GeneTable.jsx index 339750a8..5937a3d0 100644 --- a/imports/ui/genetable/GeneTable.jsx +++ b/imports/ui/genetable/GeneTable.jsx @@ -66,8 +66,8 @@ function searchTracker({ let searchQuery - if (Meteor.settings.public.customSearch === true){ - searchQuery = {query: searchValue} + if (searchValue !== "" && Meteor.settings.public.customSearch === true){ + searchQuery = {query: searchValue, $or: []} } else { searchQuery = { $or: [] }; attributes diff --git a/imports/ui/genetable/GeneTableBody.jsx b/imports/ui/genetable/GeneTableBody.jsx index 66fc60aa..dd3d8b1d 100644 --- a/imports/ui/genetable/GeneTableBody.jsx +++ b/imports/ui/genetable/GeneTableBody.jsx @@ -45,7 +45,7 @@ function dataTracker({ }) { const geneSub = Meteor.subscribe('genes', { query, sort, limit }); const loading = !geneSub.ready(); - const genes = Genes.find(query, { limit, sort }).fetch(); + const genes = Genes.find({}, { limit, sort }).fetch(); return { genes, From 859505d6b1cc197a591583b4d160b3acd8e28013 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 4 Oct 2023 15:04:26 +0200 Subject: [PATCH 03/42] Add optional direct redirection --- imports/api/publications.js | 27 ++++++++++++++------------- imports/ui/main/SearchBar.jsx | 13 ++++++++++--- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/imports/api/publications.js b/imports/api/publications.js index 05bb25b7..a071556b 100644 --- a/imports/api/publications.js +++ b/imports/api/publications.js @@ -80,31 +80,32 @@ Meteor.publish({ : genomeIds; let transformedQuery = {}; - logger.log(query) + let config = Meteor.settings - if ( query.query !== undefined && typeof Meteor.settings.customSearchOptions === "object" && Meteor.settings.customSearchOptions.url !== undefined){ - let url = Meteor.settings.customSearchOptions.url.replace(/,+$/, "") + "/"; + if ( query.query !== undefined && typeof config.customSearchOptions === "object" && config.customSearchOptions.url){ + let url = config.customSearchOptions.url.replace(/,+$/, "") + "/"; let paramsDict = {} - let geneField = Meteor.settings.customSearchOptions.gene_field !== undefined ? Meteor.settings.customSearchOptions.gene_field : "geneId" - if (Meteor.settings.customSearchOptions.query_param !== undefined){ - paramsDict[Meteor.settings.customSearchOptions.query_param] = query.query + let geneField = config.customSearchOptions.gene_field ? config.customSearchOptions.gene_field : "geneId" + if (config.customSearchOptions.query_param){ + paramsDict[config.customSearchOptions.query_param] = query.query } else { - url = url += query.query + url += query.query } - if (Meteor.settings.customSearchOptions.field_param !== undefined){ - paramsDict[Meteor.settings.customSearchOptions.field_param] = geneField + if (config.customSearchOptions.field_param){ + paramsDict[config.customSearchOptions.field_param] = geneField } - if (Meteor.settings.customSearchOptions.count_param !== undefined){ - paramsDict[Meteor.settings.customSearchOptions.count_param] = limit - + if (config.customSearchOptions.count_param){ + paramsDict[config.customSearchOptions.count_param] = limit } + + let geneResults = [] url = url + "?" + new URLSearchParams(paramsDict) const response = HTTP.get(url); if (response.statusCode === 200){ geneResults = response.data.data.map(result => result._source[geneField]) - transformedQuery = {genomeId: { $in: queryGenomeIds }, ID: { $in: geneResults }} } + transformedQuery = {genomeId: { $in: queryGenomeIds }, ID: { $in: geneResults }} } else { transformedQuery = { ...query, genomeId: { $in: queryGenomeIds } }; } diff --git a/imports/ui/main/SearchBar.jsx b/imports/ui/main/SearchBar.jsx index cfad6db7..cc3f508c 100644 --- a/imports/ui/main/SearchBar.jsx +++ b/imports/ui/main/SearchBar.jsx @@ -99,15 +99,22 @@ function SearchBar({ if (redirect) { const query = new URLSearchParams(); - query.set('attributes', [...selectedAttributes]); - query.set('search', searchString.trim()); + let searchUrl = "/genes" + if (Meteor.settings.public.redirectSearch){ + searchUrl = Meteor.settings.public.redirectSearch + const searchAttr = Meteor.settings.public.redirectSearchAttribute ? Meteor.settings.public.redirectSearchAttribute : 'query' + query.set(searchAttr, searchString.trim()); + } else { + query.set('attributes', [...selectedAttributes]); + query.set('search', searchString.trim()); + } const queryString = `?${query.toString()}`; return ( Date: Wed, 4 Oct 2023 14:46:04 +0000 Subject: [PATCH 04/42] Fix search --- config.json.template | 7 ++++--- imports/api/publications.js | 18 +++++++++--------- imports/ui/genetable/GeneTable.jsx | 2 +- imports/ui/main/SearchBar.jsx | 21 +++++++++++---------- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/config.json.template b/config.json.template index cb954b9b..6ab258e5 100644 --- a/config.json.template +++ b/config.json.template @@ -5,10 +5,11 @@ "disable_user_registration": false, "blast_link": "", "expression_unit": "My unit", - "customSearch": true + "externalSearch": false, + "redirectSearch": "" }, - "customSearchOptions": { - "url": "http://0.0.0.0:8888", + "externalSearchOptions": { + "url": "http://0.0.0.0:80", "gene_field": "gene_id", "query_param": "q", "field_param": "display_fields", diff --git a/imports/api/publications.js b/imports/api/publications.js index a071556b..cc594cdb 100644 --- a/imports/api/publications.js +++ b/imports/api/publications.js @@ -82,21 +82,21 @@ Meteor.publish({ let config = Meteor.settings - if ( query.query !== undefined && typeof config.customSearchOptions === "object" && config.customSearchOptions.url){ - let url = config.customSearchOptions.url.replace(/,+$/, "") + "/"; + if ( query.query !== undefined && config.public.externalSearch && typeof config.externalSearchOptions === "object" && config.externalSearchOptions.url){ + let url = config.externalSearchOptions.url.replace(/,+$/, "") + "/"; let paramsDict = {} - let geneField = config.customSearchOptions.gene_field ? config.customSearchOptions.gene_field : "geneId" - if (config.customSearchOptions.query_param){ - paramsDict[config.customSearchOptions.query_param] = query.query + let geneField = config.externalSearchOptions.gene_field ? config.externalSearchOptions.gene_field : "geneId" + if (config.externalSearchOptions.query_param){ + paramsDict[config.externalSearchOptions.query_param] = query.query } else { url += query.query } - if (config.customSearchOptions.field_param){ - paramsDict[config.customSearchOptions.field_param] = geneField + if (config.externalSearchOptions.field_param){ + paramsDict[config.externalSearchOptions.field_param] = geneField } - if (config.customSearchOptions.count_param){ - paramsDict[config.customSearchOptions.count_param] = limit + if (config.externalSearchOptions.count_param){ + paramsDict[config.externalSearchOptions.count_param] = limit } let geneResults = [] diff --git a/imports/ui/genetable/GeneTable.jsx b/imports/ui/genetable/GeneTable.jsx index 5937a3d0..9761d2f0 100644 --- a/imports/ui/genetable/GeneTable.jsx +++ b/imports/ui/genetable/GeneTable.jsx @@ -66,7 +66,7 @@ function searchTracker({ let searchQuery - if (searchValue !== "" && Meteor.settings.public.customSearch === true){ + if (searchValue !== "" && Meteor.settings.public.externalSearch === true){ searchQuery = {query: searchValue, $or: []} } else { searchQuery = { $or: [] }; diff --git a/imports/ui/main/SearchBar.jsx b/imports/ui/main/SearchBar.jsx index cc3f508c..21f565fb 100644 --- a/imports/ui/main/SearchBar.jsx +++ b/imports/ui/main/SearchBar.jsx @@ -5,7 +5,7 @@ import { Meteor } from 'meteor/meteor'; import { withTracker } from 'meteor/react-meteor-data'; import React, { useState, useEffect, useRef } from 'react'; -import { Redirect, withRouter } from 'react-router-dom'; +import { Redirect, withRouter, useHistory } from 'react-router-dom'; import { cloneDeep } from 'lodash'; import { attributeCollection } from '/imports/api/genes/attributeCollection.js'; @@ -57,7 +57,7 @@ function SearchBar({ const [selectedAttributes, setSelectedAttributes] = useState( new Set(['Gene ID', ...initialSelectedAttributes]), ); - + let history = useHistory() const inputRef = useRef(); useEffect(() => { if (highLightSearch) { @@ -90,6 +90,13 @@ function SearchBar({ function submit(event) { event.preventDefault(); + if (Meteor.settings.public.redirectSearch){ + const query = new URLSearchParams(); + const searchUrl = Meteor.settings.public.redirectSearch + const searchAttr = Meteor.settings.public.redirectSearchAttribute ? Meteor.settings.public.redirectSearchAttribute : 'query' + query.set(searchAttr, searchString.trim()); + location.href = searchUrl + `?${query.toString()}` + } setRedirect(true); } @@ -100,14 +107,8 @@ function SearchBar({ if (redirect) { const query = new URLSearchParams(); let searchUrl = "/genes" - if (Meteor.settings.public.redirectSearch){ - searchUrl = Meteor.settings.public.redirectSearch - const searchAttr = Meteor.settings.public.redirectSearchAttribute ? Meteor.settings.public.redirectSearchAttribute : 'query' - query.set(searchAttr, searchString.trim()); - } else { - query.set('attributes', [...selectedAttributes]); - query.set('search', searchString.trim()); - } + query.set('attributes', [...selectedAttributes]); + query.set('search', searchString.trim()); const queryString = `?${query.toString()}`; return ( From 816241622aa774491fc9813f2fd219fc00799dd8 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 4 Oct 2023 16:58:48 +0200 Subject: [PATCH 05/42] Changelog --- CHANGELOG.md | 15 ++++++++++++++- config.json.template | 3 ++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a750693..84cb52b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html) +## [0.4.8] Unreleased + +### Added + +- Option for redirecting the search to an external url ("public.redirectSearch" key in config) + - An additional key "public.redirectSearchAttribute", defaulting to 'query', will be used as the get parameter attribute. (ie: url + "?redirectSearchAttribute=query") +- Options for using a remote search ending, and merging the results with GNB internal search. + - The 'public.externalSearch' option need to be set to true, and an 'externalSearchOptions' dict need to be set. + - 'url' key is the remote endpoint where the query will be sent + - 'gene_field' is the remote field to get the gene IDs (default to geneId) + - 'query_param' : optional get parameter to use for the query + - 'field_param': optional get parameter to use to restrict the results to the gene_field value + - 'count_param': optional get parameter to restrict the number of results + ## [0.4.7] 2023-09-26 ### Fixed @@ -399,4 +413,3 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. [0.1.3]: https://github.com/genenotebook/genenotebook/compare/v0.1.2...v0.1.3 [0.1.2]: https://github.com/genenotebook/genenotebook/compare/v0.1.1...v0.1.2 [0.1.1]: https://github.com/genenotebook/genenotebook/compare/v0.1.0...v0.1.1 - diff --git a/config.json.template b/config.json.template index 6ab258e5..3e380878 100644 --- a/config.json.template +++ b/config.json.template @@ -6,7 +6,8 @@ "blast_link": "", "expression_unit": "My unit", "externalSearch": false, - "redirectSearch": "" + "redirectSearch": "", + "redirectSearchAttribute": "" }, "externalSearchOptions": { "url": "http://0.0.0.0:80", From 26809cede6c463efaa11a908d5d4c1d312294c53 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 4 Oct 2023 15:22:29 +0000 Subject: [PATCH 06/42] hide attribute search if redirect --- imports/ui/main/SearchBar.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/imports/ui/main/SearchBar.jsx b/imports/ui/main/SearchBar.jsx index 21f565fb..7aeba315 100644 --- a/imports/ui/main/SearchBar.jsx +++ b/imports/ui/main/SearchBar.jsx @@ -125,6 +125,9 @@ function SearchBar({ ); } + let label = Meteor.settings.public.externalSearch ? "Select attributes to display" : "Select attributes to search" + let display_attr = Meteor.settings.public.redirectSearch ? false : true + return (
+ {display_attr &&
@@ -143,7 +147,7 @@ function SearchBar({
+ }
Date: Thu, 5 Oct 2023 14:02:19 +0200 Subject: [PATCH 07/42] Test add annotation name --- cli/genoboo.js | 6 ++++++ imports/api/genes/geneCollection.js | 16 +++++++++++----- imports/api/genomes/annotation/addAnnotation.js | 11 +++++++++-- .../api/genomes/annotation/addAnnotation.test.js | 6 ++++++ .../annotation/parser/annotationParserGff3.js | 8 +++++--- imports/api/genomes/genomeCollection.js | 15 +++++++++------ imports/api/jobqueue/process-annotation.js | 5 ++++- imports/startup/server/fixtures/addTestData.js | 2 ++ 8 files changed, 52 insertions(+), 17 deletions(-) diff --git a/cli/genoboo.js b/cli/genoboo.js index bbf85cc3..8b28df82 100755 --- a/cli/genoboo.js +++ b/cli/genoboo.js @@ -322,6 +322,10 @@ addAnnotation '-n, --name ', 'Reference genome name to which the annotation should be added.', ) + .option( + '--annot ', + 'Annotation name', + ) .option( '-r, --re_protein ', 'Replacement string for the protein name using capturing groups defined by --re_protein_capture. Make sure to use JS-style groups ($1 for group 1)', @@ -352,6 +356,7 @@ addAnnotation password, port = 3000, name, + annot, re_protein, re_protein_capture, attr_protein, @@ -389,6 +394,7 @@ addAnnotation { fileName, genomeName: name, + annotationName: annot, re_protein, re_protein_capture: correctProteinCapture, attr_protein, diff --git a/imports/api/genes/geneCollection.js b/imports/api/genes/geneCollection.js index cc093e3e..bd13b122 100644 --- a/imports/api/genes/geneCollection.js +++ b/imports/api/genes/geneCollection.js @@ -73,9 +73,7 @@ const SubfeatureSchema = new SimpleSchema( ID: { type: String, index: true, - unique: true, - // denyUpdate: true, - label: 'Unique subfeature ID', + label: 'Subfeature ID', }, protein_id: { type: String, @@ -129,9 +127,8 @@ const GeneSchema = new SimpleSchema( { ID: { type: String, - unique: true, index: true, - label: 'Unique gene ID', + label: 'Gene ID', }, editing: { type: String, @@ -161,6 +158,11 @@ const GeneSchema = new SimpleSchema( index: true, label: 'Reference genome DB identifier (_id in genome collection)', }, + annotationName: { + type: String, + index: true, + label: 'Annotation name', + }, orthogroup: { type: OrthogroupSchema, index: true, @@ -207,8 +209,12 @@ const GeneSchema = new SimpleSchema( // Extend the gene schema with base features. GeneSchema.extend(IntervalBaseSchema); + Genes.attachSchema(GeneSchema); +Genes.createIndex({ID: 'text', 'annotationName': 'text'}, {name: 'Id and annotation index', unique: true}) +Genes.createIndex({'subfeatures.$.ID': 'text', 'annotationName': 'text'}, {name: 'SubId and annotation index', unique: true}) + export { Genes, GeneSchema, diff --git a/imports/api/genomes/annotation/addAnnotation.js b/imports/api/genomes/annotation/addAnnotation.js index b3ef47f9..55de26a3 100644 --- a/imports/api/genomes/annotation/addAnnotation.js +++ b/imports/api/genomes/annotation/addAnnotation.js @@ -15,6 +15,9 @@ const addAnnotation = new ValidatedMethod({ genomeName: { type: String, }, + annotationName: { + type: String, + }, re_protein: { type: String, optional: true, @@ -36,7 +39,7 @@ const addAnnotation = new ValidatedMethod({ noRetry: true, }, run({ - fileName, genomeName, re_protein, re_protein_capture, attr_protein, verbose, + fileName, genomeName, annotationName, re_protein, re_protein_capture, attr_protein, verbose, }) { if (!this.userId || !Roles.userIsInRole(this.userId, 'admin')) { throw new Meteor.Error('not-authorized'); @@ -46,8 +49,11 @@ const addAnnotation = new ValidatedMethod({ if (!existingGenome) { throw new Meteor.Error(`Unknown genome name: ${genomeName}`); } + if (typeof existingGenome.annotationTrack !== 'undefined') { - throw new Meteor.Error(`Genome ${genomeName} already has an annotation track`); + if (existingGenome.annotationTrack.some(annot => annot.name === annotationName)){ + throw new Meteor.Error(`Genome ${genomeName} already has an annotation track with the name ${annotationName}`); + } } const genomeId = existingGenome._id; @@ -58,6 +64,7 @@ const addAnnotation = new ValidatedMethod({ { fileName, genomeName, + annotationName, genomeId, re_protein, re_protein_capture, diff --git a/imports/api/genomes/annotation/addAnnotation.test.js b/imports/api/genomes/annotation/addAnnotation.test.js index 900b4914..68286cb1 100644 --- a/imports/api/genomes/annotation/addAnnotation.test.js +++ b/imports/api/genomes/annotation/addAnnotation.test.js @@ -31,6 +31,7 @@ describe('AddAnnotation', function testAnnotation() { const toAnnot = { fileName: 'assets/app/data/Bnigra.gff3', genomeName: 'Test Genome', + annotationName: 'Test annotation', verbose: false, }; @@ -54,6 +55,7 @@ describe('AddAnnotation', function testAnnotation() { const gene = genes[0]; chai.assert.equal(gene.ID, 'BniB01g000010.2N'); + chai.assert.equal(gene.annotationName, 'Test annotation'); chai.assert.equal(gene.seqid, 'B1'); chai.assert.equal(gene.source, 'AAFC_GIFS'); chai.assert.equal(gene.strand, '-'); @@ -73,6 +75,7 @@ describe('AddAnnotation', function testAnnotation() { const toAnnot = { fileName: 'assets/app/data/Bnigra_min.gff3', genomeName: 'Test Genome', + annotationName: 'Test annotation', verbose: false, }; @@ -94,6 +97,7 @@ describe('AddAnnotation', function testAnnotation() { const toAnnot = { fileName: 'assets/app/data/Bnigra_min.gff3', genomeName: 'Test Genome', + annotationName: 'Test annotation', verbose: false, re_protein_capture: '^Bni(.*?)$', re_protein: 'testprot-$1' @@ -117,6 +121,7 @@ describe('AddAnnotation', function testAnnotation() { const toAnnot = { fileName: 'assets/app/data/Bnigra_min.gff3', genomeName: 'Test Genome', + annotationName: 'Test annotation', verbose: false, attr_protein: 'protid' }; @@ -138,6 +143,7 @@ describe('AddAnnotation', function testAnnotation() { const toAnnot = { fileName: 'assets/app/data/Bnigra_min.gff3', genomeName: 'Test Genome', + annotationName: 'Test annotation', verbose: false, attr_protein: 'protid2' }; diff --git a/imports/api/genomes/annotation/parser/annotationParserGff3.js b/imports/api/genomes/annotation/parser/annotationParserGff3.js index 1984140b..8964cb47 100644 --- a/imports/api/genomes/annotation/parser/annotationParserGff3.js +++ b/imports/api/genomes/annotation/parser/annotationParserGff3.js @@ -17,10 +17,11 @@ import { Genes, GeneSchema } from '../../../genes/geneCollection'; * @param {Boolean} verbose - View more details. */ class AnnotationProcessor { - constructor(filename, genomeID, re_protein=undefined, re_protein_capture="^(.*?)$", attr_protein=undefined, verbose = true) { + constructor(filename, annotationName, genomeID, re_protein=undefined, re_protein_capture="^(.*?)$", attr_protein=undefined, verbose = true) { this.filename = filename; this.genomeID = genomeID; this.verbose = verbose; + this.annotationName = annotationName // Protein ID stuff // Regex options . @@ -501,6 +502,7 @@ class AnnotationProcessor { const features = { ID: identifier, genomeId: this.genomeID, + annotationName: this.annotationName, seqid: seqidGff, source: sourceGff, type: typeGff, @@ -594,9 +596,9 @@ class AnnotationProcessor { genomeCollection.update({ _id: this.genomeID, }, { - $set: { + $push: { annotationTrack: { - name: this.filename.split('/').pop(), + name: this.annotationName, }, }, }); diff --git a/imports/api/genomes/genomeCollection.js b/imports/api/genomes/genomeCollection.js index 7ead8563..c8b62309 100644 --- a/imports/api/genomes/genomeCollection.js +++ b/imports/api/genomes/genomeCollection.js @@ -70,24 +70,27 @@ const genomeSchema = new SimpleSchema({ label: 'Organism name', }, annotationTrack: { - type: Object, + type: Array, optional: true, - label: 'Genome annotation', + label: 'Genome annotations', + }, + 'annotationTrack.$': { + type: Object }, - 'annotationTrack.name': { + 'annotationTrack.$.name': { type: String, label: 'Annotation track name', }, - 'annotationTrack.blastDb': { + 'annotationTrack.$.blastDb': { type: Object, optional: true, label: 'Annotation track BLAST database identifiers', }, - 'annotationTrack.blastDb.nucl': { + 'annotationTrack.$.blastDb.nucl': { type: String, label: 'Nucleotide BLAST database', }, - 'annotationTrack.blastDb.prot': { + 'annotationTrack.$.blastDb.prot': { type: String, label: 'Protein BLAST database', }, diff --git a/imports/api/jobqueue/process-annotation.js b/imports/api/jobqueue/process-annotation.js index 028a6591..f7522e1a 100644 --- a/imports/api/jobqueue/process-annotation.js +++ b/imports/api/jobqueue/process-annotation.js @@ -14,17 +14,19 @@ jobQueue.processJobs( const { fileName, genomeName, + annotationName, genomeId, re_protein, re_protein_capture, attr_protein, verbose, } = job.data; - logger.log(`Adding annotation file "${fileName}" to genome "${genomeName}"`); + logger.log(`Adding annotation file "${fileName}" with name "${annotationName}" to genome "${genomeName}"`); if(verbose){ logger.log('file :', fileName); logger.log('name :', genomeName); + logger.log('annotation name : ', annotationName) logger.log('re_protein :', re_protein); logger.log('re_protein_capture', re_protein_capture); logger.log('attr_protein', attr_protein); @@ -33,6 +35,7 @@ jobQueue.processJobs( const lineProcessor = new AnnotationProcessor( fileName, + annotationName, genomeId, re_protein, re_protein_capture, diff --git a/imports/startup/server/fixtures/addTestData.js b/imports/startup/server/fixtures/addTestData.js index 150cc595..a2eb5695 100644 --- a/imports/startup/server/fixtures/addTestData.js +++ b/imports/startup/server/fixtures/addTestData.js @@ -68,6 +68,7 @@ export function addTestGenome(annot=false) { start: 0, end: 23320, genomeId: genomeId, + annotationName: "Annotation name", permission: 'admin', isPublic: false }) @@ -87,6 +88,7 @@ export function addTestGenome(annot=false) { start: 13640, end: 15401, genomeId: genomeId, + annotationName: "Annotation name", score: '.', subfeatures: [subfeature, cds], seq: 'AAAA', From 977863e6b9d005b81da64abeba0e76b07be1dde5 Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 5 Oct 2023 14:19:16 +0200 Subject: [PATCH 08/42] Fix index maybe --- imports/api/genes/geneCollection.js | 2 +- imports/startup/server/fixtures/addDefaultAttributes.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/imports/api/genes/geneCollection.js b/imports/api/genes/geneCollection.js index bd13b122..c2698501 100644 --- a/imports/api/genes/geneCollection.js +++ b/imports/api/genes/geneCollection.js @@ -213,7 +213,7 @@ GeneSchema.extend(IntervalBaseSchema); Genes.attachSchema(GeneSchema); Genes.createIndex({ID: 'text', 'annotationName': 'text'}, {name: 'Id and annotation index', unique: true}) -Genes.createIndex({'subfeatures.$.ID': 'text', 'annotationName': 'text'}, {name: 'SubId and annotation index', unique: true}) +Genes.createIndex({'subfeatures.ID': 'text', 'annotationName': 'text'}, {name: 'SubId and annotation index', unique: true}) export { Genes, diff --git a/imports/startup/server/fixtures/addDefaultAttributes.js b/imports/startup/server/fixtures/addDefaultAttributes.js index 07f0c459..04c59d6b 100644 --- a/imports/startup/server/fixtures/addDefaultAttributes.js +++ b/imports/startup/server/fixtures/addDefaultAttributes.js @@ -31,6 +31,10 @@ const PERMANENT_ATTRIBUTES = [ name: 'Genome', query: 'genomeId', }, + { + name: 'Annotation', + query: 'annotationName', + }, ]; export default function addDefaultAttributes() { From 5f50090af4ff177aa96cc5e70e9b561e4d7f1618 Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 5 Oct 2023 14:28:22 +0200 Subject: [PATCH 09/42] Test --- imports/api/genes/geneCollection.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/imports/api/genes/geneCollection.js b/imports/api/genes/geneCollection.js index c2698501..0b352739 100644 --- a/imports/api/genes/geneCollection.js +++ b/imports/api/genes/geneCollection.js @@ -212,8 +212,7 @@ GeneSchema.extend(IntervalBaseSchema); Genes.attachSchema(GeneSchema); -Genes.createIndex({ID: 'text', 'annotationName': 'text'}, {name: 'Id and annotation index', unique: true}) -Genes.createIndex({'subfeatures.ID': 'text', 'annotationName': 'text'}, {name: 'SubId and annotation index', unique: true}) +Genes.createIndex({ID: 'text', 'annotationName': 'text', 'subfeatures.ID': 'text'}, {name: 'Id and annotation index', unique: true}) export { Genes, From f48dafff8f5b99ff1d7dab285a09951c46e8736c Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 9 Oct 2023 15:31:08 +0000 Subject: [PATCH 10/42] fix test --- imports/api/genes/geneCollection.js | 5 ++- .../genomes/annotation/addAnnotation.test.js | 41 ++++++++++++++++++- .../startup/server/fixtures/addTestData.js | 3 +- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/imports/api/genes/geneCollection.js b/imports/api/genes/geneCollection.js index 0b352739..578adb8a 100644 --- a/imports/api/genes/geneCollection.js +++ b/imports/api/genes/geneCollection.js @@ -1,5 +1,6 @@ import SimpleSchema from 'simpl-schema'; import { Mongo } from 'meteor/mongo'; +import logger from '/imports/api/util/logger.js'; const VALID_SUBFEATURE_TYPES = [ 'transcript', @@ -209,10 +210,10 @@ const GeneSchema = new SimpleSchema( // Extend the gene schema with base features. GeneSchema.extend(IntervalBaseSchema); - Genes.attachSchema(GeneSchema); -Genes.createIndex({ID: 'text', 'annotationName': 'text', 'subfeatures.ID': 'text'}, {name: 'Id and annotation index', unique: true}) +Genes.createIndex({ID: 1, annotationName: 1}, {name: 'Id and annotation index', unique: true}) +Genes.createIndex({'subfeatures.ID': 1, annotationName: 1}, {name: 'SubId and annotation index', unique: true}) export { Genes, diff --git a/imports/api/genomes/annotation/addAnnotation.test.js b/imports/api/genomes/annotation/addAnnotation.test.js index 68286cb1..66c14718 100644 --- a/imports/api/genomes/annotation/addAnnotation.test.js +++ b/imports/api/genomes/annotation/addAnnotation.test.js @@ -28,7 +28,7 @@ describe('AddAnnotation', function testAnnotation() { this.timeout(10000); const { genomeId, genomeSeqId } = addTestGenome(); - const toAnnot = { + let toAnnot = { fileName: 'assets/app/data/Bnigra.gff3', genomeName: 'Test Genome', annotationName: 'Test annotation', @@ -66,6 +66,45 @@ describe('AddAnnotation', function testAnnotation() { chai.assert.lengthOf(gene.subfeatures, 13, 'Number of subfeatures is not 13'); }); + it('Should add multiple copies of genes with different annotation names', function addAnnotationGff3() { + // Increase timeout + this.timeout(10000); + + const { genomeId, genomeSeqId } = addTestGenome(); + let toAnnot = { + fileName: 'assets/app/data/Bnigra.gff3', + genomeName: 'Test Genome', + annotationName: 'Test annotation', + verbose: false, + }; + + // Should fail for non-logged in + chai.expect(() => { + addAnnotation._execute({}, toAnnot); + }).to.throw('[not-authorized]'); + + // Should fail for non admin user + chai.expect(() => { + addAnnotation._execute(userContext, toAnnot); + }).to.throw('[not-authorized]'); + + // Add annotation. + addAnnotation._execute(adminContext, toAnnot); + + + toAnnot = { + fileName: 'assets/app/data/Bnigra.gff3', + genomeName: 'Test Genome', + annotationName: 'Test annotation2', + verbose: false, + }; + + addAnnotation._execute(adminContext, toAnnot); + + const genes = Genes.find({ genomeId: genomeId }).fetch(); + + chai.assert.lengthOf(genes, 10, 'Number of created genes is not 10'); + }); it('Should add a default -protein label to the mRNA protein_id', function addAnnotationGff3() { // Increase timeout diff --git a/imports/startup/server/fixtures/addTestData.js b/imports/startup/server/fixtures/addTestData.js index a2eb5695..70924a1d 100644 --- a/imports/startup/server/fixtures/addTestData.js +++ b/imports/startup/server/fixtures/addTestData.js @@ -51,7 +51,7 @@ export function addTestUsers() { export function addTestGenome(annot=false) { - const annotObj = annot ? { name: 'myfilename.gff'} : undefined + const annotObj = annot ? [{ name: 'myfilename.gff'}] : undefined const genomeId = genomeCollection.insert({ name: "Test Genome", @@ -68,7 +68,6 @@ export function addTestGenome(annot=false) { start: 0, end: 23320, genomeId: genomeId, - annotationName: "Annotation name", permission: 'admin', isPublic: false }) From 8beb9c66b76716d48b17ee91e6a83ab64d6f6435 Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 10 Oct 2023 16:48:25 +0200 Subject: [PATCH 11/42] Test UI --- imports/ui/singleGenePage/SingleGenePage.jsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/imports/ui/singleGenePage/SingleGenePage.jsx b/imports/ui/singleGenePage/SingleGenePage.jsx index 983cce10..99394ead 100644 --- a/imports/ui/singleGenePage/SingleGenePage.jsx +++ b/imports/ui/singleGenePage/SingleGenePage.jsx @@ -40,10 +40,19 @@ function isNotFound({ gene }) { return typeof gene === 'undefined'; } -function geneDataTracker({ match, genomeDataCache }) { +function isMultiple({ gene }) { + return gene.length > 1; +} + +function Multiple( {gene} ){ + return
Multiple annotation
; +} + +function geneDataTracker({ match, genomeDataCache, location }) { const { geneId } = match.params; + const { annotation } = new URLSearchParams(location.search); const geneSub = Meteor.subscribe('singleGene', { geneId }); - const gene = Genes.findOne({ ID: geneId }); + const gene = Genes.find({ ID: geneId, annotationName: annotation }).fetch(); const loading = !geneSub.ready(); return { loading, @@ -83,6 +92,7 @@ function SingleGenePage({ gene, genome = {} }) {

{`${gene.ID} `} {genome.name} + {gene.annotationName}

    From 0becbda5000d43520322e14b984249ac7fa1c048 Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 10 Oct 2023 15:47:43 +0000 Subject: [PATCH 12/42] Fix ui --- imports/api/genes/geneCollection.js | 7 +++++-- imports/ui/singleGenePage/SingleGenePage.jsx | 11 ++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/imports/api/genes/geneCollection.js b/imports/api/genes/geneCollection.js index 578adb8a..03595b8b 100644 --- a/imports/api/genes/geneCollection.js +++ b/imports/api/genes/geneCollection.js @@ -212,8 +212,11 @@ GeneSchema.extend(IntervalBaseSchema); Genes.attachSchema(GeneSchema); -Genes.createIndex({ID: 1, annotationName: 1}, {name: 'Id and annotation index', unique: true}) -Genes.createIndex({'subfeatures.ID': 1, annotationName: 1}, {name: 'SubId and annotation index', unique: true}) +if (Meteor.isServer) { + Genes.createIndex({ID: 1, annotationName: 1}, {name: 'Id and annotation index', unique: true}) + Genes.createIndex({'subfeatures.ID': 1, annotationName: 1}, {name: 'SubId and annotation index', unique: true}) +} + export { Genes, diff --git a/imports/ui/singleGenePage/SingleGenePage.jsx b/imports/ui/singleGenePage/SingleGenePage.jsx index 99394ead..042d1f40 100644 --- a/imports/ui/singleGenePage/SingleGenePage.jsx +++ b/imports/ui/singleGenePage/SingleGenePage.jsx @@ -50,9 +50,14 @@ function Multiple( {gene} ){ function geneDataTracker({ match, genomeDataCache, location }) { const { geneId } = match.params; - const { annotation } = new URLSearchParams(location.search); + const annotation = new URLSearchParams(location.search).get("annotation"); const geneSub = Meteor.subscribe('singleGene', { geneId }); - const gene = Genes.find({ ID: geneId, annotationName: annotation }).fetch(); + let gene + if (annotation) { + gene = Genes.findOne({ ID: geneId, annotationName: annotation }); + } else { + gene = Genes.findOne({ ID: geneId }); + } const loading = !geneSub.ready(); return { loading, @@ -91,7 +96,7 @@ function SingleGenePage({ gene, genome = {} }) {

    {`${gene.ID} `} - {genome.name} + {genome.name}  {gene.annotationName}

    From efa2da0dc87ebf770d20f21013ad75bb9fc4a38d Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 11 Oct 2023 11:56:08 +0200 Subject: [PATCH 13/42] Some more stuff --- cli/genoboo.js | 52 ++++- imports/api/genes/addInterproscan.js | 18 +- .../api/genes/alignment/addSimilarSequence.js | 7 +- imports/api/genes/alignment/alignment.test.js | 3 + .../genes/alignment/parser/pairwiseParser.js | 13 +- .../api/genes/alignment/parser/xmlParser.js | 16 +- .../alignment/similarSequenceCollection.js | 4 + imports/api/genes/eggnog/addEggnog.js | 32 ++- imports/api/genes/eggnog/eggnog.test.js | 1 + imports/api/genes/eggnog/eggnogCollection.js | 4 + imports/api/genes/hectar/addHectar.js | 33 ++- imports/api/genes/hectar/hectar.test.js | 1 + imports/api/genes/hectar/hectarCollection.js | 4 + imports/api/genes/interproscan.js | 219 ------------------ .../genes/interproscan/interproscan.test.js | 2 + .../interproscan/interproscanCollection.js | 6 +- imports/api/genes/parseGff3Interproscan.js | 7 +- imports/api/genes/parseTsvInterproscan.js | 7 +- imports/api/publications.js | 5 +- imports/ui/singleGenePage/ProteinDomains.jsx | 4 +- 20 files changed, 167 insertions(+), 271 deletions(-) delete mode 100644 imports/api/genes/interproscan.js diff --git a/cli/genoboo.js b/cli/genoboo.js index 8b28df82..36f307a3 100755 --- a/cli/genoboo.js +++ b/cli/genoboo.js @@ -318,11 +318,11 @@ addAnnotation .arguments('') .option('-u, --username ', 'GeneNoteBook admin username') .option('-p, --password ', 'GeneNoteBook admin password') - .option( + .requiredOption( '-n, --name ', 'Reference genome name to which the annotation should be added.', ) - .option( + .requiredOption( '--annot ', 'Annotation name', ) @@ -439,6 +439,10 @@ running GeneNoteBook server.` '--port [port]', 'Port on which GeneNoteBook is running. Default: 3000' ) + .option( + '--annot ', + 'Annotation name', + ) .option( '-fmt, --format [parser]', `Choose a parser for the diamond output format. Parses .xml, .txt @@ -461,7 +465,7 @@ running GeneNoteBook server.` .action( ( file, - { username, password, port = 3000, format, algorithm, matrix, database } + { username, password, port = 3000, annot, format, algorithm, matrix, database } ) => { if (typeof file !== 'string') addDiamond.help(); @@ -496,6 +500,7 @@ file extension is not "xml", "txt"`); fileName, parser: parserType, program: 'diamond', + annot: annot, algorithm: algorithm, matrix: matrix, database: database, @@ -531,6 +536,10 @@ running GeneNoteBook server.` '-p, --password ', 'GeneNoteBook admin password' ) + .option( + '--annot ', + 'Annotation name', + ) .option( '--port [port]', 'Port on which GeneNoteBook is running. Default: 3000' @@ -554,7 +563,7 @@ running GeneNoteBook server.` .action( ( file, - { username, password, port = 3000, format, algorithm, matrix, database } + { username, password, port = 3000, annot, format, algorithm, matrix, database } ) => { if (typeof file !== 'string') addBlast.help(); @@ -590,6 +599,7 @@ file extension is not "xml", "txt"`); parser: parserType, program: 'blast', algorithm: algorithm, + annot: annot, matrix: matrix, database: database, } @@ -625,6 +635,10 @@ addExpression '-d, --sample-description ', 'Description of the experiment' ) + .option( + '--annot ', + 'Annotation name', + ) .option( '-r, --replicas ', 'Comma-separated column positions, which are part of the same replica group. Can be set multiple times for multiple groups. The replica group name will be the first column, unless replica-names is set' @@ -645,7 +659,7 @@ addExpression const replicas = opts.replicas || []; const replicaNames = opts.replicaNames || []; const isPublic = opts.public; - + const annot = opts.annot if (!(fileName && username && password)) { program.help(); } @@ -654,6 +668,7 @@ addExpression { fileName, description, + annot, replicas, replicaNames, isPublic @@ -686,6 +701,10 @@ addExpression '-d, --sample-description ', 'Description of the experiment' ) + .option( + '--annot ', + 'Annotation name', + ) .option( '--public', 'Set the generated replica groups as public. Default: false', @@ -698,6 +717,7 @@ addExpression const replicaGroup = opts.replicaGroup || fileName; const description = opts.sampleDescription || 'description'; const isPublic = opts.public + const anot = opts.annot if (!(fileName && username && password)) { program.help(); @@ -707,6 +727,7 @@ addExpression { fileName, sampleName, + annot: annot, replicaGroup, description, isPublic @@ -734,12 +755,16 @@ addInterproscan '--port [port]', 'Port on which GeneNoteBook is running. Default: 3000' ) + .option( + '--annot ', + 'Annotation name', + ) .option( '--format [parser]', `Choose a parser for the interproscan output files. Parses .gff3 and .tsv extensions.` ) - .action((file, { username, password, port = 3000, format }) => { + .action((file, { username, password, port = 3000, format, annot }) => { if (typeof file !== 'string') addInterproscan.help(); const fileName = path.resolve(file); @@ -772,6 +797,7 @@ file extension is not "tsv", "gff3" or "xml".`); { fileName, parser: parserType, + annot: annot } ); }) @@ -800,11 +826,15 @@ addEggnog '-p, --password ', 'GeneNoteBook admin password' ) + .option( + '--annot ', + 'Annotation name', + ) .option( '--port [port]', 'Port on which GeneNoteBook is running. Default: 3000' ) - .action((file, { username, password, port = 3000 }) => { + .action((file, { username, password, port = 3000, annot }) => { if (typeof file !== 'string') addEggnog.help(); const fileName = path.resolve(file); @@ -814,6 +844,7 @@ addEggnog new GeneNoteBookConnection({ username, password, port }).call('addEggnog', { fileName, + annot: annot }); }) .on('--help', () => { @@ -839,11 +870,15 @@ addHectar '-p, --password ', 'GeneNoteBook admin password' ) + .option( + '--annot ', + 'Annotation name', + ) .option( '--port [port]', 'Port on which GeneNoteBook is running. Default: 3000' ) - .action((file, { username, password, port = 3000 }) => { + .action((file, { username, password, port = 3000, annot }) => { if (typeof file !== 'string') addHectar.help(); const fileName = path.resolve(file); @@ -853,6 +888,7 @@ addHectar new GeneNoteBookConnection({ username, password, port }).call('addHectar', { fileName, + annot: annot }); }) .on('--help', () => { diff --git a/imports/api/genes/addInterproscan.js b/imports/api/genes/addInterproscan.js index ad762466..fa53caa4 100644 --- a/imports/api/genes/addInterproscan.js +++ b/imports/api/genes/addInterproscan.js @@ -12,14 +12,16 @@ import { Meteor } from 'meteor/meteor'; * @method finalize */ class InterproscanProcessor { - constructor() { + constructor(annot) { this.bulkOp = interproscanCollection.rawCollection().initializeUnorderedBulkOp(); this.geneBulkOp = Genes.rawCollection().initializeUnorderedBulkOp(); this.currentProt = "" this.currentGene = "" this.currentContent = [] this.currentDB = [] - this.currentOnto = [] + this.currentOnto = [], + this.currentAnnotationName = "", + this.annot = annot } finalize = () => { @@ -47,11 +49,13 @@ class InterproscanProcessor { this.bulkOp.find({ gene_id: this.currentGene, protein_id: this.currentProt, + annotationName: this.currentAnnotationName }).upsert().update( { $set: { gene_id: this.currentGene, protein_id: this.currentProt, + annotationName: this.currentAnnotationName, protein_domains: this.currentContent }, }, @@ -63,7 +67,7 @@ class InterproscanProcessor { } if (this.currentDB != [] || this.currentOnto != []){ - this.geneBulkOp.find({ID: this.currentGene}).update({ + this.geneBulkOp.find({ID: this.currentGene, annotationName: this.currentAnnotationName}).update({ $addToSet: { 'attributes.Ontology_term': { $each: this.currentOnto }, 'attributes.Dbxref': { $each: this.currentDB } @@ -80,6 +84,10 @@ const addInterproscan = new ValidatedMethod({ name: 'addInterproscan', validate: new SimpleSchema({ fileName: { type: String }, + annot: { + type: String, + optional: true, + }, parser: { type: String, allowedValues: ['tsv', 'gff3'], @@ -88,7 +96,7 @@ const addInterproscan = new ValidatedMethod({ applyOptions: { noRetry: true, }, - run({ fileName, parser }) { + run({ fileName, annot, parser }) { if (!this.userId) { throw new Meteor.Error('not-authorized'); } @@ -96,7 +104,7 @@ const addInterproscan = new ValidatedMethod({ throw new Meteor.Error('not-authorized'); } - const job = new Job(jobQueue, 'addInterproscan', { fileName, parser }); + const job = new Job(jobQueue, 'addInterproscan', { fileName, annot, parser }); const jobId = job.priority('high').save(); // Continue with synchronous processing diff --git a/imports/api/genes/alignment/addSimilarSequence.js b/imports/api/genes/alignment/addSimilarSequence.js index abceca55..aee77c88 100644 --- a/imports/api/genes/alignment/addSimilarSequence.js +++ b/imports/api/genes/alignment/addSimilarSequence.js @@ -19,6 +19,10 @@ const addSimilarSequence = new ValidatedMethod({ optional: true, allowedValues: ['blast', 'diamond'], }, + annot: { + type: String, + optional: true, + }, algorithm: { type: String, optional: true, @@ -44,7 +48,7 @@ const addSimilarSequence = new ValidatedMethod({ applyOptions: { noRetry: true, }, - run({ fileName, parser, program, algorithm, matrix, database }) { + run({ fileName, annot, parser, program, algorithm, matrix, database }) { if (!this.userId) { throw new Meteor.Error('not-authorized'); } @@ -57,6 +61,7 @@ const addSimilarSequence = new ValidatedMethod({ 'addDiamond', { fileName, + annot, parser, program, algorithm, diff --git a/imports/api/genes/alignment/alignment.test.js b/imports/api/genes/alignment/alignment.test.js index 70c0ff96..6b2fa963 100644 --- a/imports/api/genes/alignment/alignment.test.js +++ b/imports/api/genes/alignment/alignment.test.js @@ -45,6 +45,7 @@ describe('alignment', function testAlignment() { algorithm: 'blastx', matrix: 'blosum62', database: 'nr', + annot: "Annotation name" }; // Should fail for non-logged in @@ -85,6 +86,7 @@ describe('alignment', function testAlignment() { algorithm: 'blastx', matrix: 'BLOSUM90', database: 'nr', + annot: "Annotation name" }; // Should fail for non-logged in @@ -126,6 +128,7 @@ describe('alignment', function testAlignment() { algorithm: 'blastx', matrix: 'BLOSUM90', database: 'nr', + annot: "Annotation name" }; // Should fail for non-logged in diff --git a/imports/api/genes/alignment/parser/pairwiseParser.js b/imports/api/genes/alignment/parser/pairwiseParser.js index 66037ba0..7fe08ade 100644 --- a/imports/api/genes/alignment/parser/pairwiseParser.js +++ b/imports/api/genes/alignment/parser/pairwiseParser.js @@ -7,7 +7,7 @@ class Pairwise { constructor({ iteration_query, query_length, - position_query, + position_query }){ this.iteration_query = iteration_query; this.query_length = query_length; @@ -28,7 +28,7 @@ class Pairwise { * @param {string} database - The reference database (Non-redundant protein sequences (nr)). */ class PairwiseProcessor { - constructor(program, algorithm, matrix, database) { + constructor(program, algorithm, matrix, database, annot) { this.genesDb = Genes.rawCollection(); this.pairWise = new Pairwise({}); this.program = program; @@ -36,6 +36,7 @@ class PairwiseProcessor { this.matrix = matrix; this.database = database; this.similarSeqBulkOp = similarSequencesCollection.rawCollection().initializeUnorderedBulkOp(); + this.annot = annot; } /** @@ -68,7 +69,8 @@ class PairwiseProcessor { /** Update or insert pairwise. */ this.similarSeqBulkOp.find({ iteration_query: this.pairWise.iteration_query, - protein_id: this.pairWise.iteration_query + protein_id: this.pairWise.iteration_query, + annotationName: this.annot, }).upsert().update( { $set: { @@ -77,6 +79,7 @@ class PairwiseProcessor { matrix_ref: this.matrix, database_ref: this.database, iteration_query: this.pairWise.iteration_query, + annotationName: this.annot, protein_id: this.pairWise.iteration_query, query_len: this.pairWise.query_length, iteration_hits: this.pairWise.iteration_hits, @@ -361,7 +364,8 @@ class PairwiseProcessor { this.similarSeqBulkOp.find({ iteration_query: this.pairWise.iteration_query, - protein_id: this.pairWise.iteration_query + protein_id: this.pairWise.iteration_query, + annotationName: this.annot, }).upsert().update( { $set: { @@ -370,6 +374,7 @@ class PairwiseProcessor { matrix_ref: this.matrix, database_ref: this.database, iteration_query: this.pairWise.iteration_query, + annotationName: this.annot, protein_id: this.pairWise.iteration_query, query_len: this.pairWise.query_length, iteration_hits: this.pairWise.iteration_hits, diff --git a/imports/api/genes/alignment/parser/xmlParser.js b/imports/api/genes/alignment/parser/xmlParser.js index cebe7c3c..c88147de 100644 --- a/imports/api/genes/alignment/parser/xmlParser.js +++ b/imports/api/genes/alignment/parser/xmlParser.js @@ -16,12 +16,13 @@ import logger from '/imports/api/util/logger.js'; */ class XmlProcessor { - constructor(program, algorithm, matrix, database) { + constructor(program, algorithm, matrix, database, annot) { this.genesDb = Genes.rawCollection(); this.program = program; this.algorithm = algorithm; this.matrix = matrix; this.database = database; + this.annot = annot; this.similarSeqBulkOp = similarSequencesCollection.rawCollection().initializeUnorderedBulkOp(); } @@ -101,9 +102,16 @@ class XmlProcessor { await Promise.all(splitIterationQuery.map(async (iter) => { /** Chek if the queries exist in the genes collection. */ - const subfeatureIsFound = await this.genesDb.findOne({ $or: [{'subfeatures.ID': iter}, {'subfeatures.protein_id': iter}] }); + + let geneQuery = { $or: [{'subfeatures.ID': iter}, {'subfeatures.protein_id': iter}] } + if (typeof this.annot !== "undefined"){ + geneQuery['annotationName'] = this.annot + } + + const subfeatureIsFound = await this.genesDb.findOne(geneQuery); if (typeof subfeatureIsFound !== 'undefined' && subfeatureIsFound !== null) { /** Get the total query sequence length. */ + const annotationName = subfeatureIsFound.annotationName const queryLen = blastIteration[i]['iteration_query-len'] /** Get the root tag of hit sequences. */ @@ -185,7 +193,8 @@ class XmlProcessor { /** Mongo bulk-operation. */ this.similarSeqBulkOp.find({ iteration_query: geneIdentifier, - protein_id: iter + protein_id: iter, + annotationName: annotationName }).upsert().update( { $set: { @@ -197,6 +206,7 @@ class XmlProcessor { protein_id: iter, query_len: queryLen, iteration_hits: iterations, + annotationName: annotationName }, }, { diff --git a/imports/api/genes/alignment/similarSequenceCollection.js b/imports/api/genes/alignment/similarSequenceCollection.js index d0e0c918..b902b752 100644 --- a/imports/api/genes/alignment/similarSequenceCollection.js +++ b/imports/api/genes/alignment/similarSequenceCollection.js @@ -26,6 +26,10 @@ const similarSequencesSchema = new SimpleSchema({ type: String, label: 'Protein_id', }, + annotationName: { + type: String, + label: 'Annotation name', + }, iteration_hits: { type: Array, label: 'List of iteration hits.', diff --git a/imports/api/genes/eggnog/addEggnog.js b/imports/api/genes/eggnog/addEggnog.js index 573eb5ff..4f00d17d 100644 --- a/imports/api/genes/eggnog/addEggnog.js +++ b/imports/api/genes/eggnog/addEggnog.js @@ -90,38 +90,42 @@ class EggnogProcessor { // If subfeatures is found in genes database (e.g: ID = // MMUCEDO_000002-T1). - const subfeatureIsFound = Genes.findOne({ + + let geneQuery = { $or: [ { 'subfeatures.ID': queryName }, { 'subfeatures.protein_id': queryName }, ], - }); + } + if (typeof this.annot !== "undefined"){ + geneQuery['annotationName'] = this.annot + } + + const subfeatureIsFound = Genes.findOne(geneQuery); if (typeof subfeatureIsFound !== 'undefined') { // Increment eggnog. this.nEggnog += 1; + let annotationName = subfeatureIsFound.annotationName + annotations['annotationName'] = annotationName // Update or insert if no matching documents were found. const documentEggnog = eggnogCollection.upsert( - { query_name: queryName }, // selector. + { query_name: queryName, annotationName }, // selector. annotations, // modifier. ); // Update eggnogId in genes database. if (typeof documentEggnog.insertedId !== 'undefined') { // Eggnog _id is created. - return this.genesDb.update({ - $or: [ - { 'subfeatures.ID': queryName }, - { 'subfeatures.protein_id': queryName }, - ]}, + return this.genesDb.update(geneQuery, { $set: { eggnogId: documentEggnog.insertedId } }, ); } else { // Eggnog already exists. - const eggnogIdentifiant = eggnogCollection.findOne({ query_name: queryName })._id; + const eggnogIdentifiant = eggnogCollection.findOne({ query_name: queryName, annotationName })._id; return this.genesDb.update( - { $or: [{'subfeatures.ID': queryName}, {'subfeatures.protein_id': queryName}] }, + geneQuery, { $set: { eggnogId: eggnogIdentifiant } }, ); } @@ -139,11 +143,15 @@ const addEggnog = new ValidatedMethod({ name: 'addEggnog', validate: new SimpleSchema({ fileName: { type: String }, + annot: { + type: String, + optional: true, + }, }).validator(), applyOptions: { noRetry: true, }, - run({ fileName }) { + run({ fileName, annot }) { if (!this.userId) { throw new Meteor.Error('not-authorized'); } @@ -152,7 +160,7 @@ const addEggnog = new ValidatedMethod({ } logger.log('file :', { fileName }); - const job = new Job(jobQueue, 'addEggnog', { fileName }); + const job = new Job(jobQueue, 'addEggnog', { fileName, annot }); const jobId = job.priority('high').save(); let { status } = job.doc; diff --git a/imports/api/genes/eggnog/eggnog.test.js b/imports/api/genes/eggnog/eggnog.test.js index cc0cd260..9a08f565 100644 --- a/imports/api/genes/eggnog/eggnog.test.js +++ b/imports/api/genes/eggnog/eggnog.test.js @@ -33,6 +33,7 @@ describe('eggnog', function testEggnog() { const eggNogParams = { fileName: 'assets/app/data/Bnigra_eggnog.tsv', + annot: "Annotation name" }; // Should fail for non-logged in diff --git a/imports/api/genes/eggnog/eggnogCollection.js b/imports/api/genes/eggnog/eggnogCollection.js index 2c6ef1a5..5c766caa 100644 --- a/imports/api/genes/eggnog/eggnogCollection.js +++ b/imports/api/genes/eggnog/eggnogCollection.js @@ -6,6 +6,10 @@ const eggnogSchema = new SimpleSchema({ type: String, label: 'Query sequence name.', }, + annotationName: { + type: String, + label: 'Annotation name', + }, seed_eggNOG_ortholog: { type: String, label: 'Best protein match in eggNOG.', diff --git a/imports/api/genes/hectar/addHectar.js b/imports/api/genes/hectar/addHectar.js index 7c8ebf6d..c3bd8a17 100644 --- a/imports/api/genes/hectar/addHectar.js +++ b/imports/api/genes/hectar/addHectar.js @@ -63,39 +63,44 @@ class HectarProcessor { } // If subfeatures is found in genes database (e.g: ID = // MMUCEDO_000002-T1). - const subfeatureIsFound = Genes.findOne({ + + let geneQuery = { $or: [ { 'subfeatures.ID': proteinId }, { 'subfeatures.protein_id': proteinId }, ], - }); + } + if (typeof this.annot !== "undefined"){ + geneQuery['annotationName'] = this.annot + } + + const subfeatureIsFound = Genes.findOne(geneQuery); if (typeof subfeatureIsFound !== 'undefined') { console.log("if loop" + typeof subfeatureIsFound); + let annotationName = subfeatureIsFound.annotationName // Increment hectar. this.nHectar += 1; + annotations['annotationName'] = annotationName + // Update or insert if no matching documents were found. const documentHectar = hectarCollection.upsert( - { protein_id: proteinId }, // selector. + { protein_id: proteinId, annotationName }, // selector. annotations, // modifier. ); // Update hectarId in genes database. if (typeof documentHectar.insertedId !== 'undefined') { // Hectar _id is created. - return this.genesDb.update({ - $or: [ - { 'subfeatures.ID': proteinId }, - { 'subfeatures.protein_id': proteinId }, - ]}, + return this.genesDb.update(geneQuery, { $set: { hectarId: documentHectar.insertedId } }, ); } else { // Hectar already exists. - const hectarIdentifiant = hectarCollection.findOne({ protein_id: proteinId })._id; + const hectarIdentifiant = hectarCollection.findOne({ protein_id: proteinId, annotationName })._id; return this.genesDb.update( - { $or: [{'subfeatures.ID': proteinId}, {'subfeatures.protein_id': proteinId}] }, + geneQuery, { $set: { hectarId: hectarIdentifiant } }, ); } @@ -113,11 +118,15 @@ const addHectar = new ValidatedMethod({ name: 'addHectar', validate: new SimpleSchema({ fileName: { type: String }, + annot: { + type: String, + optional: true, + }, }).validator(), applyOptions: { noRetry: true, }, - run({ fileName }) { + run({ fileName, annot }) { if (!this.userId) { throw new Meteor.Error('not-authorized'); } @@ -126,7 +135,7 @@ const addHectar = new ValidatedMethod({ } logger.log('file :', { fileName }); - const job = new Job(jobQueue, 'addHectar', { fileName }); + const job = new Job(jobQueue, 'addHectar', { fileName, annot }); const jobId = job.priority('high').save(); let { status } = job.doc; diff --git a/imports/api/genes/hectar/hectar.test.js b/imports/api/genes/hectar/hectar.test.js index dbc210f8..beb1c11f 100644 --- a/imports/api/genes/hectar/hectar.test.js +++ b/imports/api/genes/hectar/hectar.test.js @@ -33,6 +33,7 @@ describe('hectar', function testHectar() { const hectarParams = { fileName: 'assets/app/data/Bnigra_hectar.tab', + annot: "Annotation name" }; // Should fail for non-logged in diff --git a/imports/api/genes/hectar/hectarCollection.js b/imports/api/genes/hectar/hectarCollection.js index bf7dad3f..5033f203 100644 --- a/imports/api/genes/hectar/hectarCollection.js +++ b/imports/api/genes/hectar/hectarCollection.js @@ -6,6 +6,10 @@ const hectarSchema = new SimpleSchema({ type: String, label: 'Query sequence name and type.', }, + annotationName: { + type: String, + label: 'Annotation name', + }, predicted_targeting_category: { type: String, label: 'Predicted sub-cellular localization.', diff --git a/imports/api/genes/interproscan.js b/imports/api/genes/interproscan.js deleted file mode 100644 index 0df060b9..00000000 --- a/imports/api/genes/interproscan.js +++ /dev/null @@ -1,219 +0,0 @@ -/* import { Meteor } from 'meteor/meteor'; -import { Roles } from 'meteor/alanning:roles'; - -import request from 'request'; -import Future from 'fibers/future'; - -import { getGeneSequences } from '/imports/api/util/util.js'; - -import { Genes } from '/imports/api/genes/gene_collection.js'; - -const revcomp = (seq) => { - const comp = { - 'A':'T','a':'t', - 'T':'A','t':'a', - 'C':'G','c':'g', - 'G':'C','g':'c', - 'N':'N','n':'n' - } - const revSeqArray = seq.split('').reverse() - const revCompSeqArray = revSeqArray.map( (nuc) => { - return comp[nuc] - }) - const revCompSeq = revCompSeqArray.join('') - return revCompSeq -} - -const translate = (seq) => { - const trans = { - 'ACC': 'T', 'ACA': 'T', 'ACG': 'T', - 'AGG': 'R', 'AGC': 'S', 'GTA': 'V', - 'AGA': 'R', 'ACT': 'T', 'GTG': 'V', - 'AGT': 'S', 'CCA': 'P', 'CCC': 'P', - 'GGT': 'G', 'CGA': 'R', 'CGC': 'R', - 'TAT': 'Y', 'CGG': 'R', 'CCT': 'P', - 'GGG': 'G', 'GGA': 'G', 'GGC': 'G', - 'TAA': '*', 'TAC': 'Y', 'CGT': 'R', - 'TAG': '*', 'ATA': 'I', 'CTT': 'L', - 'ATG': 'M', 'CTG': 'L', 'ATT': 'I', - 'CTA': 'L', 'TTT': 'F', 'GAA': 'E', - 'TTG': 'L', 'TTA': 'L', 'TTC': 'F', - 'GTC': 'V', 'AAG': 'K', 'AAA': 'K', - 'AAC': 'N', 'ATC': 'I', 'CAT': 'H', - 'AAT': 'N', 'GTT': 'V', 'CAC': 'H', - 'CAA': 'Q', 'CAG': 'Q', 'CCG': 'P', - 'TCT': 'S', 'TGC': 'C', 'TGA': '*', - 'TGG': 'W', 'TCG': 'S', 'TCC': 'S', - 'TCA': 'S', 'GAG': 'E', 'GAC': 'D', - 'TGT': 'C', 'GCA': 'A', 'GCC': 'A', - 'GCG': 'A', 'GCT': 'A', 'CTC': 'L', - 'GAT': 'D'} - const codonArray = seq.match(/.{1,3}/g) - const pepArray = codonArray.map( (codon) => { - let aminoAcid = 'X' - if (codon.indexOf('N') < 0){ - aminoAcid = trans[codon] - } - return aminoAcid - }) - const pep = pepArray.join('') - return pep -} - -const makeFasta = (gene) => { - let transcripts = gene.subfeatures.filter( (subfeature) => { return subfeature.type === 'mRNA' }) - let sequences = transcripts.map( (transcript) => { - let transcriptSeq = `>${transcript.ID}\n`; - let transcriptPep = `>${transcript.ID}\n`; - let cdsArray = gene.subfeatures.filter( (sub) => { - return sub.parents.indexOf(transcript.ID) >= 0 && sub.type === 'CDS' - }).sort( (a,b) => { - return a.start - b.start - }) - - let refStart = 10e99; - //let referenceSubscription = Meteor.subscribe('references',gene.seqid) - - //find all reference fragments overlapping the mRNA feature - let referenceArray = References.find({ - header: gene.seqid, - $and: [ - { start: {$lte: gene.end} }, - { end: {$gte: gene.start} } - ] - }).fetch() - - if (referenceArray.length){ - let reference = referenceArray.sort( (a,b) => { - //sort on start coordinate - return a.start - b.start - }).map( (ref) => { - //find starting position of first reference fragment - refStart = Math.min(refStart,ref.start) - return ref.seq - }).join('') - - seq = cdsArray.map( (cds, index) => { - let start = cds.start - refStart - 1; - let end = cds.end - refStart; - return reference.slice(start,end) - }).join('') - - let phase; - if (this.strand === '-'){ - seq = revcomp(seq) - phase = cdsArray[cdsArray.length -1].phase - } else { - phase = cdsArray[0].phase - } - - if ([1,2].indexOf(phase) >= 0){ - seq = seq.slice(phase) - } - - let pep = translate(seq.toUpperCase()); - - transcriptSeq += seq; - - transcriptPep += pep; - transcriptPep = transcriptPep.split('*').join('X') - } - return {ID:transcript.ID, seq: transcriptSeq, pep: transcriptPep} - }) - return sequences -} - -function submitInterpro(sequenceId,peptide){ - const submitJob = new Future(); - - request.post({ - url: 'http://www.ebi.ac.uk/Tools/services/rest/iprscan5/run/', - form: { - email: 'rens.holmer@gmail.com', - title: `genebook protein ${sequenceId}`, - sequence: peptide - } - }, (error, response, jobId) => { - console.log('error:', error); // Print the error if one occurred - console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received - console.log('requestId:', jobId); - submitJob.return(jobId) - }) - - const jobId = submitJob.wait() - - return jobId -} - -function pollInterpro(jobId,cb){ - const statusRequest = new Future(); - const url = `http://www.ebi.ac.uk/Tools/services/rest/iprscan5/status/${jobId}`; - console.log(`Trying ${url}`) - request.get(url, (error,response,body) => { - console.log(error) - console.log(body) - statusRequest.return(body) - }) - const status = statusRequest.wait() - if (status === 'RUNNING'){ - //figure out a way to call the function with a parameter - Meteor.setTimeout(function(){return pollInterpro(jobId,cb)}, 100000) - } else { - cb(status) - } -} - -function getInterproResults(jobId){ - const future = new Future(); - const url = `http://www.ebi.ac.uk/Tools/services/rest/iprscan5/result/${jobId}/json` - console.log(`Trying ${url}`) - request.get(url, (error,response,body) => { - let interproAnnotation = JSON.parse(body) - future.return(interproAnnotation) - }) - const results = future.wait() - return results -} - -Meteor.methods({ - interproscan(geneId){ - if (! this.userId) { - throw new Meteor.Error('not-authorized'); - } - if (! Roles.userIsInRole(this.userId,'admin')){ - throw new Meteor.Error('not-authorized'); - } - - //this.unblock(); - const gene = Genes.findOne({ID: geneId}) - const sequences = getGeneSequences(gene) - const results = sequences.map((sequence) => { - - // interproscan does not like stop codons, just replace all with X - let pep = sequence.pep.split('*').join('X') - - const jobId = submitInterpro(sequence.ID, pep); - - const fut = new Future(); - pollInterpro(jobId, (status) => { - console.log(`pollInterpro: ${status}`) - fut.return(status) - }) - - const finished = fut.wait(); - - let results; - - if (finished === 'FINISHED'){ - results = getInterproResults(jobId) - console.log(results) - Genes.update({'subfeatures.ID':sequence.ID},{$set:{interproscan:results[0].matches}}) - } - - return results - }) - - return results - } -}) -*/ diff --git a/imports/api/genes/interproscan/interproscan.test.js b/imports/api/genes/interproscan/interproscan.test.js index 1bfbc627..6add689b 100644 --- a/imports/api/genes/interproscan/interproscan.test.js +++ b/imports/api/genes/interproscan/interproscan.test.js @@ -41,6 +41,7 @@ describe('interproscan', function testInterproscan() { const interproParams = { fileName: 'assets/app/data/Bnigra_interproscan.tsv', parser: "tsv", + annot: "Annotation name" }; // Should fail for non-logged in @@ -89,6 +90,7 @@ describe('interproscan', function testInterproscan() { const interproParams = { fileName: 'assets/app/data/Bnigra_interproscan.gff', parser: "gff3", + annot: "Annotation name" }; // Should fail for non-logged in diff --git a/imports/api/genes/interproscan/interproscanCollection.js b/imports/api/genes/interproscan/interproscanCollection.js index 65a2ede8..3d9f577c 100644 --- a/imports/api/genes/interproscan/interproscanCollection.js +++ b/imports/api/genes/interproscan/interproscanCollection.js @@ -11,6 +11,10 @@ const interproscanSchema = new SimpleSchema({ type: String, label: 'Linked protein ID', }, + annotationName: { + type: String, + label: 'Annotation name', + }, protein_domains: { type: Array, label: 'Interproscan protein domains', @@ -20,7 +24,7 @@ const interproscanSchema = new SimpleSchema({ type: Object, label: 'Interproscan protein domain', blackbox: true, - }, + } }); const interproscanCollection = new Mongo.Collection('interproscan'); diff --git a/imports/api/genes/parseGff3Interproscan.js b/imports/api/genes/parseGff3Interproscan.js index 5d4a11dd..c4b42914 100644 --- a/imports/api/genes/parseGff3Interproscan.js +++ b/imports/api/genes/parseGff3Interproscan.js @@ -57,9 +57,14 @@ class ParseGff3File extends InterproscanProcessor { this.currentProt = seqId this.currentGene = "" - let gene = Genes.findOne({ $or: [{'subfeatures.ID': seqId}, {'subfeatures.protein_id': seqId}] }); + let geneQuery = { $or: [{'subfeatures.ID': seqId}, {'subfeatures.protein_id': seqId}] } + if (typeof this.annot !== "undefined"){ + geneQuery['annotationName'] = this.annot + } + let gene = Genes.findOne(geneQuery); if (typeof gene !== "undefined"){ this.currentGene = gene.ID + this.currentAnnotationName = gene.annotationName } this.currentContent = [] diff --git a/imports/api/genes/parseTsvInterproscan.js b/imports/api/genes/parseTsvInterproscan.js index 8f26694e..e4737c43 100644 --- a/imports/api/genes/parseTsvInterproscan.js +++ b/imports/api/genes/parseTsvInterproscan.js @@ -30,9 +30,14 @@ class ParseTsvFile extends InterproscanProcessor { this.currentProt = seqId this.currentGene = "" - let gene = Genes.findOne({ $or: [{'subfeatures.ID': seqId}, {'subfeatures.protein_id': seqId}] }); + let geneQuery = { $or: [{'subfeatures.ID': seqId}, {'subfeatures.protein_id': seqId}] } + if (typeof this.annot !== "undefined"){ + geneQuery['annotationName'] = this.annot + } + let gene = Genes.findOne(geneQuery); if (typeof gene !== "undefined"){ this.currentGene = gene.ID + this.currentAnnotationName = gene.annotationName } this.currentContent = [] diff --git a/imports/api/publications.js b/imports/api/publications.js index cc594cdb..eed47fe7 100644 --- a/imports/api/publications.js +++ b/imports/api/publications.js @@ -217,6 +217,7 @@ Meteor.publish({ alignment(gene) { const diamond = similarSequencesCollection.find( { + annotationName: gene.annotationName, $or: [ { iteration_query: gene.ID }, { iteration_query: { $in: gene.children } }, @@ -225,8 +226,8 @@ Meteor.publish({ ); return diamond; }, - interpro(query){ - return interproscanCollection.find({gene_id: query}) + interpro(gene){ + return interproscanCollection.find({gene_id: gene.ID, annotationName: gene.annotationName}) }, orthogroups(ID) { return orthogroupCollection.find({ _id: ID }); diff --git a/imports/ui/singleGenePage/ProteinDomains.jsx b/imports/ui/singleGenePage/ProteinDomains.jsx index e8f8144a..00211331 100644 --- a/imports/ui/singleGenePage/ProteinDomains.jsx +++ b/imports/ui/singleGenePage/ProteinDomains.jsx @@ -295,7 +295,7 @@ function NoProteinDomains({ showHeader }) { } function InterproDataTracker({ gene }) { - const interproSub = Meteor.subscribe('interpro', gene.ID); + const interproSub = Meteor.subscribe('interpro', gene); const loading = !interproSub.ready(); const proteinDomains = interproscanCollection.find({}).fetch() @@ -372,7 +372,7 @@ function ProteinDomains({ ); }) - let axisTransform = `translate(0,${15 + currentTranslate})` + let axisTransform = `translate(0,${15 + currentTranslate})` let gTransform = `translate(0,${40 + currentTranslate})` let data = ( From 454a4a0e470d6327ebc5c2656ac4914a84be409b Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 11 Oct 2023 14:20:25 +0000 Subject: [PATCH 14/42] fixing ui --- imports/api/api.js | 1 - imports/api/genes/parseGff3Interproscan.js | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/imports/api/api.js b/imports/api/api.js index 1c889783..ddcecfb2 100644 --- a/imports/api/api.js +++ b/imports/api/api.js @@ -17,7 +17,6 @@ import './genomes/removeGenome.js'; import './genomes/annotation/removeAnnotationTrack.js'; import './genomes/annotation/addAnnotation.js'; -import './genes/interproscan.js'; import './genes/addInterproscan.js'; import './genes/eggnog/addEggnog.js'; import './genes/hectar/addHectar.js'; diff --git a/imports/api/genes/parseGff3Interproscan.js b/imports/api/genes/parseGff3Interproscan.js index c4b42914..a6a72f58 100644 --- a/imports/api/genes/parseGff3Interproscan.js +++ b/imports/api/genes/parseGff3Interproscan.js @@ -62,6 +62,8 @@ class ParseGff3File extends InterproscanProcessor { geneQuery['annotationName'] = this.annot } let gene = Genes.findOne(geneQuery); + logger.log(gene) + logger.log(geneQuery) if (typeof gene !== "undefined"){ this.currentGene = gene.ID this.currentAnnotationName = gene.annotationName From fcaeca96702b8066afb47e1110884f175758bede Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 11 Oct 2023 16:55:18 +0200 Subject: [PATCH 15/42] Test check gene in pairwise parser --- .../genes/alignment/parser/pairwiseParser.js | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/imports/api/genes/alignment/parser/pairwiseParser.js b/imports/api/genes/alignment/parser/pairwiseParser.js index 7fe08ade..37f3d489 100644 --- a/imports/api/genes/alignment/parser/pairwiseParser.js +++ b/imports/api/genes/alignment/parser/pairwiseParser.js @@ -31,6 +31,7 @@ class PairwiseProcessor { constructor(program, algorithm, matrix, database, annot) { this.genesDb = Genes.rawCollection(); this.pairWise = new Pairwise({}); + this.currentGene = "" this.program = program; this.algorithm = algorithm; this.matrix = matrix; @@ -63,6 +64,22 @@ class PairwiseProcessor { this.pairWise.iteration_query = queryClean; } + let geneQuery = { + $or: [ + { 'subfeatures.ID': this.pairWise.iteration_query }, + { 'subfeatures.protein_id': this.pairWise.iteration_query }, + ], + } + if (typeof this.annot !== "undefined"){ + geneQuery['annotationName'] = this.annot + } + + this.currentGene = Genes.findOne(geneQuery); + if (typeof this.currentGene === 'undefined'){ + logger.warn(`Warning ! No sub-feature was found for ${this.pairWise.iteration_query}.`); + return + } + /** In the case where a new pairwise must be created. */ if (typeof this.pairWise.iteration_query !== 'undefined' && this.pairWise.iteration_query !== queryClean) { @@ -70,7 +87,7 @@ class PairwiseProcessor { this.similarSeqBulkOp.find({ iteration_query: this.pairWise.iteration_query, protein_id: this.pairWise.iteration_query, - annotationName: this.annot, + annotationName: this.currentGene.annotationName, }).upsert().update( { $set: { @@ -79,7 +96,7 @@ class PairwiseProcessor { matrix_ref: this.matrix, database_ref: this.database, iteration_query: this.pairWise.iteration_query, - annotationName: this.annot, + annotationName: this.currentGene.annotationName, protein_id: this.pairWise.iteration_query, query_len: this.pairWise.query_length, iteration_hits: this.pairWise.iteration_hits, @@ -97,6 +114,11 @@ class PairwiseProcessor { this.pairWise.iteration_query = queryClean; this.pairWise.iteration_hits = []; } + + if (typeof this.currentGene === 'undefined'){ + return + } + if (/Length/.test(line)) { /** * Get and clean the length of the query sequence according to the program used. @@ -365,7 +387,7 @@ class PairwiseProcessor { this.similarSeqBulkOp.find({ iteration_query: this.pairWise.iteration_query, protein_id: this.pairWise.iteration_query, - annotationName: this.annot, + annotationName: this.currentGene.annotationName, }).upsert().update( { $set: { @@ -374,7 +396,7 @@ class PairwiseProcessor { matrix_ref: this.matrix, database_ref: this.database, iteration_query: this.pairWise.iteration_query, - annotationName: this.annot, + annotationName: this.currentGene.annotationName, protein_id: this.pairWise.iteration_query, query_len: this.pairWise.query_length, iteration_hits: this.pairWise.iteration_hits, From 943565f20cb59f61ffe77d618f1a3ac081d182fd Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 11 Oct 2023 16:07:34 +0000 Subject: [PATCH 16/42] fix tests --- imports/api/genes/alignment/alignment.test.js | 6 +++++- imports/api/genes/alignment/parser/pairwiseParser.js | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/imports/api/genes/alignment/alignment.test.js b/imports/api/genes/alignment/alignment.test.js index 6b2fa963..0d3f2d11 100644 --- a/imports/api/genes/alignment/alignment.test.js +++ b/imports/api/genes/alignment/alignment.test.js @@ -1,4 +1,4 @@ -7/* eslint-env mocha */ +/* eslint-env mocha */ import chai from 'chai'; import { Meteor } from 'meteor/meteor'; import logger from '/imports/api/util/logger.js'; @@ -79,6 +79,8 @@ describe('alignment', function testAlignment() { // Increase timeout this.timeout(20000); + addTestGenome(annot=true) + const diamondParams = { fileName: 'assets/app/data/Diamond_blastx_bnigra.txt', parser: 'txt', @@ -121,6 +123,8 @@ describe('alignment', function testAlignment() { // Increase timeout this.timeout(20000); + addTestGenome(annot=true) + const diamondParams = { fileName: 'assets/app/data/BLAST_blastx_bnigra.txt', parser: 'txt', diff --git a/imports/api/genes/alignment/parser/pairwiseParser.js b/imports/api/genes/alignment/parser/pairwiseParser.js index 37f3d489..a706034f 100644 --- a/imports/api/genes/alignment/parser/pairwiseParser.js +++ b/imports/api/genes/alignment/parser/pairwiseParser.js @@ -384,6 +384,10 @@ class PairwiseProcessor { */ lastPairwise = () => { + if (typeof this.annot !== "undefined") { + return + } + this.similarSeqBulkOp.find({ iteration_query: this.pairWise.iteration_query, protein_id: this.pairWise.iteration_query, From 16d10ad9989e86750d5cf2c6dee6af08c785e889 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 11 Oct 2023 17:02:16 +0000 Subject: [PATCH 17/42] add proper redirect to gene with annotation --- imports/ui/genetable/GeneTableBody.jsx | 6 +++--- imports/ui/genetable/columns/GeneLink.jsx | 10 ++++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/imports/ui/genetable/GeneTableBody.jsx b/imports/ui/genetable/GeneTableBody.jsx index dd3d8b1d..29258bc7 100644 --- a/imports/ui/genetable/GeneTableBody.jsx +++ b/imports/ui/genetable/GeneTableBody.jsx @@ -122,11 +122,11 @@ function Loading({ selectedColumns, ...props }) { } function AttributeColumn({ - attributeName, attributeValue, geneId, genomeDataCache, + attributeName, attributeValue, gene, genomeDataCache, }) { switch (attributeName) { case 'Gene ID': - return ; + return ; case 'Genome': return ( diff --git a/imports/ui/genetable/columns/GeneLink.jsx b/imports/ui/genetable/columns/GeneLink.jsx index e338df1e..05b199a8 100644 --- a/imports/ui/genetable/columns/GeneLink.jsx +++ b/imports/ui/genetable/columns/GeneLink.jsx @@ -3,9 +3,15 @@ import { Meteor } from 'meteor/meteor'; import React from 'react'; import { Link } from 'react-router-dom'; -export default function GeneLink({ geneId }) { +export default function GeneLink({ gene }) { + const geneId = gene.ID + + const query = new URLSearchParams(); + query.set("annotation", gene.annotationName); + const url = `/gene/${geneId}?${query.toString()}` + return ( - + { geneId } ); From b562bea7cc98f3416ed8dbae523968206a5212fa Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 11 Oct 2023 19:11:04 +0200 Subject: [PATCH 18/42] Fix interpro import + add warnings --- imports/api/genes/addInterproscan.js | 2 +- imports/api/genes/parseGff3Interproscan.js | 4 ++-- imports/api/genes/parseTsvInterproscan.js | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/imports/api/genes/addInterproscan.js b/imports/api/genes/addInterproscan.js index fa53caa4..777ad49f 100644 --- a/imports/api/genes/addInterproscan.js +++ b/imports/api/genes/addInterproscan.js @@ -33,7 +33,7 @@ class InterproscanProcessor { if (this.bulkOp.length > 0){ return this.bulkOp.execute(); } - return { nMatched: 0 } + return { nUpserted: 0 } } updateGenes = () => { diff --git a/imports/api/genes/parseGff3Interproscan.js b/imports/api/genes/parseGff3Interproscan.js index a6a72f58..4f3a23ee 100644 --- a/imports/api/genes/parseGff3Interproscan.js +++ b/imports/api/genes/parseGff3Interproscan.js @@ -62,11 +62,11 @@ class ParseGff3File extends InterproscanProcessor { geneQuery['annotationName'] = this.annot } let gene = Genes.findOne(geneQuery); - logger.log(gene) - logger.log(geneQuery) if (typeof gene !== "undefined"){ this.currentGene = gene.ID this.currentAnnotationName = gene.annotationName + } else { + logger.warn(`Warning ! No sub-feature was found for ${seqId}.`) } this.currentContent = [] diff --git a/imports/api/genes/parseTsvInterproscan.js b/imports/api/genes/parseTsvInterproscan.js index e4737c43..6043e037 100644 --- a/imports/api/genes/parseTsvInterproscan.js +++ b/imports/api/genes/parseTsvInterproscan.js @@ -38,6 +38,8 @@ class ParseTsvFile extends InterproscanProcessor { if (typeof gene !== "undefined"){ this.currentGene = gene.ID this.currentAnnotationName = gene.annotationName + } else { + logger.warn(logger.warn(`Warning ! No sub-feature was found for ${seqId}.`)) } this.currentContent = [] From 65ad83e41b40a72eaa046ad8838e682ebb8cbcf4 Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 12 Oct 2023 11:05:52 +0200 Subject: [PATCH 19/42] Actually add annot in methods --- imports/api/genes/eggnog/addEggnog.js | 3 ++- imports/api/genes/hectar/addHectar.js | 3 ++- imports/api/genes/interproscan/interproscanCollection.js | 1 + imports/api/jobqueue/process-eggnog.js | 4 ++-- imports/api/jobqueue/process-hectar.js | 4 ++-- imports/api/jobqueue/process-interproscan.js | 6 +++--- imports/api/jobqueue/process-similarsequences.js | 6 +++--- 7 files changed, 15 insertions(+), 12 deletions(-) diff --git a/imports/api/genes/eggnog/addEggnog.js b/imports/api/genes/eggnog/addEggnog.js index 4f00d17d..a6a2f1ab 100644 --- a/imports/api/genes/eggnog/addEggnog.js +++ b/imports/api/genes/eggnog/addEggnog.js @@ -8,10 +8,11 @@ import SimpleSchema from 'simpl-schema'; import { Meteor } from 'meteor/meteor'; class EggnogProcessor { - constructor() { + constructor(annot) { // Not a bulk mongo suite. this.genesDb = Genes.rawCollection(); this.nEggnog = 0; + this.annot = annot; } /** diff --git a/imports/api/genes/hectar/addHectar.js b/imports/api/genes/hectar/addHectar.js index c3bd8a17..9981c443 100644 --- a/imports/api/genes/hectar/addHectar.js +++ b/imports/api/genes/hectar/addHectar.js @@ -8,10 +8,11 @@ import SimpleSchema from 'simpl-schema'; import { Meteor } from 'meteor/meteor'; class HectarProcessor { - constructor() { + constructor(annot) { // Not a bulk mongo suite. this.genesDb = Genes.rawCollection(); this.nHectar = 0; + this.annot = annot; } /** diff --git a/imports/api/genes/interproscan/interproscanCollection.js b/imports/api/genes/interproscan/interproscanCollection.js index 3d9f577c..7628ad86 100644 --- a/imports/api/genes/interproscan/interproscanCollection.js +++ b/imports/api/genes/interproscan/interproscanCollection.js @@ -13,6 +13,7 @@ const interproscanSchema = new SimpleSchema({ }, annotationName: { type: String, + index: true, label: 'Annotation name', }, protein_domains: { diff --git a/imports/api/jobqueue/process-eggnog.js b/imports/api/jobqueue/process-eggnog.js index b139a9c3..787ca037 100644 --- a/imports/api/jobqueue/process-eggnog.js +++ b/imports/api/jobqueue/process-eggnog.js @@ -11,10 +11,10 @@ jobQueue.processJobs( payload: 1, }, async (job, callback) => { - const { fileName } = job.data; + const { fileName, annot } = job.data; logger.log(`Add ${fileName} eggnog file.`); - const lineProcessor = new EggnogProcessor(); + const lineProcessor = new EggnogProcessor(annot); const rl = readline.createInterface({ input: fs.createReadStream(fileName, 'utf8'), diff --git a/imports/api/jobqueue/process-hectar.js b/imports/api/jobqueue/process-hectar.js index 4338b288..c7ec44ab 100644 --- a/imports/api/jobqueue/process-hectar.js +++ b/imports/api/jobqueue/process-hectar.js @@ -11,10 +11,10 @@ jobQueue.processJobs( payload: 1, }, async (job, callback) => { - const { fileName } = job.data; + const { fileName, annot } = job.data; logger.log(`Add ${fileName} hectar file.`); - const lineProcessor = new HectarProcessor(); + const lineProcessor = new HectarProcessor(annot); const rl = readline.createInterface({ input: fs.createReadStream(fileName, 'utf8'), diff --git a/imports/api/jobqueue/process-interproscan.js b/imports/api/jobqueue/process-interproscan.js index 41ddbd5b..9f07ffdd 100644 --- a/imports/api/jobqueue/process-interproscan.js +++ b/imports/api/jobqueue/process-interproscan.js @@ -12,7 +12,7 @@ jobQueue.processJobs( payload: 1, }, async (job, callback) => { - const { fileName, parser } = job.data; + const { fileName, parser, annot } = job.data; logger.log(`Add ${fileName} interproscan file.`); const rl = readline.createInterface({ @@ -24,11 +24,11 @@ jobQueue.processJobs( switch (parser) { case 'tsv': logger.log('Format : .tsv'); - lineProcessor = new ParseTsvFile(); + lineProcessor = new ParseTsvFile(annot); break; case 'gff3': logger.log('Format : .gff3'); - lineProcessor = new ParseGff3File(); + lineProcessor = new ParseGff3File(annot); break; } diff --git a/imports/api/jobqueue/process-similarsequences.js b/imports/api/jobqueue/process-similarsequences.js index 7032911b..90b74ae7 100644 --- a/imports/api/jobqueue/process-similarsequences.js +++ b/imports/api/jobqueue/process-similarsequences.js @@ -13,14 +13,14 @@ jobQueue.processJobs( payload: 1, }, async (job, callback) => { - const { fileName, parser, program, algorithm, matrix, database } = job.data; + const { fileName, parser, program, algorithm, matrix, database, annot } = job.data; logger.log(`Add ${fileName} diamond file.`); // Different parser for the xml file. if (parser === 'xml') { const stream = fs.createReadStream(fileName); const xml = new XmlFlow(stream, { normalize: false }); - const lineProcessor = new XmlProcessor(program, algorithm, matrix, database); + const lineProcessor = new XmlProcessor(program, algorithm, matrix, database, annot); const tag = 'blastoutput'; xml.on(`tag:${tag}`, async (obj) => { @@ -48,7 +48,7 @@ jobQueue.processJobs( input: fs.createReadStream(fileName, 'utf8'), }); - const lineProcessor = new PairwiseProcessor(program, algorithm, matrix, database); + const lineProcessor = new PairwiseProcessor(program, algorithm, matrix, database, annot); for await (const line of lineReader) { try { From 322dc94517362a68243b4246019d0ca192e711de Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 13 Oct 2023 14:59:41 +0000 Subject: [PATCH 20/42] fix protein display --- imports/ui/singleGenePage/ProteinDomains.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imports/ui/singleGenePage/ProteinDomains.jsx b/imports/ui/singleGenePage/ProteinDomains.jsx index 00211331..27dec0c2 100644 --- a/imports/ui/singleGenePage/ProteinDomains.jsx +++ b/imports/ui/singleGenePage/ProteinDomains.jsx @@ -298,7 +298,7 @@ function InterproDataTracker({ gene }) { const interproSub = Meteor.subscribe('interpro', gene); const loading = !interproSub.ready(); - const proteinDomains = interproscanCollection.find({}).fetch() + const proteinDomains = interproscanCollection.find({gene_id: gene.ID, annotationName: gene.annotationName}).fetch() return { loading, From 393eebe353ab66986d5df0fab7fa521db1ed871a Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 13 Oct 2023 17:42:52 +0200 Subject: [PATCH 21/42] Add some tests & change default attributes --- imports/api/genes/alignment/alignment.test.js | 12 +++++------ .../genes/alignment/parser/pairwiseParser.js | 4 ++-- imports/api/genes/eggnog/eggnog.test.js | 13 ++++++++++-- imports/api/genes/hectar/hectar.test.js | 11 ++++++++-- .../genes/interproscan/interproscan.test.js | 8 ++++---- .../server/fixtures/addDefaultAttributes.js | 14 ++++++++++--- .../startup/server/fixtures/addTestData.js | 20 ++++++++++++++++++- 7 files changed, 62 insertions(+), 20 deletions(-) diff --git a/imports/api/genes/alignment/alignment.test.js b/imports/api/genes/alignment/alignment.test.js index 0d3f2d11..4420bb47 100644 --- a/imports/api/genes/alignment/alignment.test.js +++ b/imports/api/genes/alignment/alignment.test.js @@ -36,7 +36,7 @@ describe('alignment', function testAlignment() { // Increase timeout this.timeout(20000); - addTestGenome(annot=true) + addTestGenome(annot=true, multiple=true) const diamondParams = { fileName: 'assets/app/data/Diamond_blastp_bnigra.xml', @@ -60,7 +60,7 @@ describe('alignment', function testAlignment() { let result = addSimilarSequence._execute(adminContext, diamondParams); - const simSeq = similarSequencesCollection.find({iteration_query: "BniB01g000010.2N"}).fetch(); + const simSeq = similarSequencesCollection.find({iteration_query: "BniB01g000010.2N", annotationName: "Annotation name"}).fetch(); chai.assert.lengthOf(simSeq, 1, "No similar sequence found") const seq = simSeq[0] @@ -79,7 +79,7 @@ describe('alignment', function testAlignment() { // Increase timeout this.timeout(20000); - addTestGenome(annot=true) + addTestGenome(annot=true, multiple=true) const diamondParams = { fileName: 'assets/app/data/Diamond_blastx_bnigra.txt', @@ -105,7 +105,7 @@ describe('alignment', function testAlignment() { //Meteor._sleepForMs(10000); - const simSeq = similarSequencesCollection.find({iteration_query: "BniB01g000010.2N.1-P"}).fetch(); + const simSeq = similarSequencesCollection.find({iteration_query: "BniB01g000010.2N", annotationName: "Annotation name"}).fetch(); chai.assert.lengthOf(simSeq, 1, "No similar sequence found") @@ -123,7 +123,7 @@ describe('alignment', function testAlignment() { // Increase timeout this.timeout(20000); - addTestGenome(annot=true) + addTestGenome(annot=true, multiple=true) const diamondParams = { fileName: 'assets/app/data/BLAST_blastx_bnigra.txt', @@ -147,7 +147,7 @@ describe('alignment', function testAlignment() { let result = addSimilarSequence._execute(adminContext, diamondParams); - const simSeq = similarSequencesCollection.find({iteration_query: "BniB01g000010.2N.1-P"}).fetch(); + const simSeq = similarSequencesCollection.find({iteration_query: "BniB01g000010.2N", annotationName: "Annotation name"}).fetch(); chai.assert.lengthOf(simSeq, 1, "No similar sequence found") const seq = simSeq[0] diff --git a/imports/api/genes/alignment/parser/pairwiseParser.js b/imports/api/genes/alignment/parser/pairwiseParser.js index a706034f..c8fe618b 100644 --- a/imports/api/genes/alignment/parser/pairwiseParser.js +++ b/imports/api/genes/alignment/parser/pairwiseParser.js @@ -384,8 +384,8 @@ class PairwiseProcessor { */ lastPairwise = () => { - if (typeof this.annot !== "undefined") { - return + if (typeof this.currentGene !== "undefined") { + return { ok:"", writeErrors:"", nInserted:0, nUpserted: 0 } } this.similarSeqBulkOp.find({ diff --git a/imports/api/genes/eggnog/eggnog.test.js b/imports/api/genes/eggnog/eggnog.test.js index 9a08f565..ac47bd2d 100644 --- a/imports/api/genes/eggnog/eggnog.test.js +++ b/imports/api/genes/eggnog/eggnog.test.js @@ -6,6 +6,7 @@ import { eggnogCollection } from './eggnogCollection'; import addEggnog from './addEggnog'; import { addTestUsers, addTestGenome } from '../../../startup/server/fixtures/addTestData'; import '../../jobqueue/process-eggnog'; +import { Genes } from '/imports/api/genes/geneCollection.js'; describe('eggnog', function testEggnog() { let adminId; @@ -29,7 +30,7 @@ describe('eggnog', function testEggnog() { // Increase timeout this.timeout(20000); - addTestGenome(annot = true); + addTestGenome(annot = true, multiple = true); const eggNogParams = { fileName: 'assets/app/data/Bnigra_eggnog.tsv', @@ -50,7 +51,7 @@ describe('eggnog', function testEggnog() { chai.assert.equal(result.nInserted, 1) - const eggs = eggnogCollection.find({ query_name: 'BniB01g000010.2N.1-P' }).fetch(); + const eggs = eggnogCollection.find({ query_name: 'BniB01g000010.2N.1-P', annotationName: "Annotation name" }).fetch(); chai.assert.lengthOf(eggs, 1, 'No eggnog data found'); @@ -62,5 +63,13 @@ describe('eggnog', function testEggnog() { chai.assert.lengthOf(egg.eggNOG_OGs, 5); chai.assert.lengthOf(egg.GOs, 18); chai.assert.equal(egg.Description, 'UDP-glucuronic acid decarboxylase'); + + const gene1 = Genes.findOne({ID: 'BniB01g000010.2N.1', annotationName: 'Annotation name'}) + const gene2 = Genes.findOne({ID: 'BniB01g000010.2N.1', annotationName: 'Annotation name 2'}) + + chai.assert.isDefined(gene2.eggnogId, "eggNodeId is not defined for the correct annotation") + chai.assert.isUndefined(gene2.eggnogId, "eggNodeId is defined for the wrong annotation") + + }); }); diff --git a/imports/api/genes/hectar/hectar.test.js b/imports/api/genes/hectar/hectar.test.js index beb1c11f..9dbae5eb 100644 --- a/imports/api/genes/hectar/hectar.test.js +++ b/imports/api/genes/hectar/hectar.test.js @@ -6,6 +6,7 @@ import { hectarCollection } from './hectarCollection'; import addHectar from './addHectar'; import { addTestUsers, addTestGenome } from '../../../startup/server/fixtures/addTestData'; import '../../jobqueue/process-hectar'; +import { Genes } from '/imports/api/genes/geneCollection.js'; describe('hectar', function testHectar() { let adminId; @@ -29,7 +30,7 @@ describe('hectar', function testHectar() { // Increase timeout this.timeout(20000); - addTestGenome(annot = true); + addTestGenome(annot = true, multiple = true); const hectarParams = { fileName: 'assets/app/data/Bnigra_hectar.tab', @@ -50,7 +51,7 @@ describe('hectar', function testHectar() { chai.assert.equal(result.nInserted, 1) - const hecs = hectarCollection.find({ protein_id: 'BniB01g000010.2N.1' }).fetch(); + const hecs = hectarCollection.find({ protein_id: 'BniB01g000010.2N.1', annotationName: "Annotation name" }).fetch(); chai.assert.lengthOf(hecs, 1, 'No hectar data found'); @@ -61,5 +62,11 @@ describe('hectar', function testHectar() { chai.assert.equal(hec.typeII_signal_anchor_score, '0.0228'); chai.assert.equal(hec.mitochondrion_score, '0.1032'); chai.assert.equal(hec.other_score, '0.8968'); + + const gene1 = Genes.findOne({ID: 'BniB01g000010.2N.1', annotationName: 'Annotation name'}) + const gene2 = Genes.findOne({ID: 'BniB01g000010.2N.1', annotationName: 'Annotation name 2'}) + + chai.assert.isDefined(gene2.eggnogId, "eggNodeId is not defined for the correct annotation") + chai.assert.isUndefined(gene2.eggnogId, "eggNodeId is defined for the wrong annotation") }); }); diff --git a/imports/api/genes/interproscan/interproscan.test.js b/imports/api/genes/interproscan/interproscan.test.js index 6add689b..58fb26ab 100644 --- a/imports/api/genes/interproscan/interproscan.test.js +++ b/imports/api/genes/interproscan/interproscan.test.js @@ -36,7 +36,7 @@ describe('interproscan', function testInterproscan() { // Increase timeout this.timeout(20000); - addTestGenome(annot=true) + addTestGenome(annot=true, multiple = true) const interproParams = { fileName: 'assets/app/data/Bnigra_interproscan.tsv', @@ -63,7 +63,7 @@ describe('interproscan', function testInterproscan() { chai.assert.deepEqual(gene.attributes.Dbxref, [ 'InterPro:1236' ]) chai.assert.deepEqual(gene.attributes.Ontology_term, [ 'GO:1238' ]) - const interpros = interproscanCollection.find({gene_id: "BniB01g000010.2N"}).fetch(); + const interpros = interproscanCollection.find({gene_id: "BniB01g000010.2N", annotationName: "Annotation name"}).fetch(); chai.assert.lengthOf(interpros, 1, "No Interpro document found") const protein_domains = interpros[0].protein_domains @@ -85,7 +85,7 @@ describe('interproscan', function testInterproscan() { // Increase timeout this.timeout(20000); - addTestGenome(annot=true) + addTestGenome(annot=true, multiple = true) const interproParams = { fileName: 'assets/app/data/Bnigra_interproscan.gff', @@ -107,7 +107,7 @@ describe('interproscan', function testInterproscan() { let result = addInterproscan._execute(adminContext, interproParams); - const interpros = interproscanCollection.find({gene_id: "BniB01g000010.2N"}).fetch(); + const interpros = interproscanCollection.find({gene_id: "BniB01g000010.2N", annotationName: "Annotation name"}).fetch(); chai.assert.lengthOf(interpros, 1, "No Interpro document found") const protein_domains = interpros[0].protein_domains diff --git a/imports/startup/server/fixtures/addDefaultAttributes.js b/imports/startup/server/fixtures/addDefaultAttributes.js index 04c59d6b..bf2e3f72 100644 --- a/imports/startup/server/fixtures/addDefaultAttributes.js +++ b/imports/startup/server/fixtures/addDefaultAttributes.js @@ -6,40 +6,48 @@ const PERMANENT_ATTRIBUTES = [ { name: 'Note', query: 'attributes.Note', + display: false }, { name: 'Dbxref', query: 'attributes.Dbxref', + display: false }, { name: 'Ontology Term', query: 'attributes.Ontology_term', + display: false }, { name: 'Orthogroup', query: 'orthogroup.name', + display: false }, { name: 'Gene ID', query: 'ID', + display: true }, { name: 'Has changes', query: 'changed', + display: false }, { name: 'Genome', query: 'genomeId', + display: true }, { name: 'Annotation', query: 'annotationName', + display: true }, ]; export default function addDefaultAttributes() { // add some default attributes to filter on - PERMANENT_ATTRIBUTES.forEach(({ name, query }) => { + PERMANENT_ATTRIBUTES.forEach(({ name, query, display }) => { const existingAttribute = attributeCollection.findOne({ name }); if (typeof existingAttribute === 'undefined') { logger.log(`Adding default filter option: ${name}`); @@ -51,8 +59,8 @@ export default function addDefaultAttributes() { $setOnInsert: { name, query, - defaultShow: false, - defaultSearch: false, + defaultShow: display, + defaultSearch: display, allGenomes: true, }, }, diff --git a/imports/startup/server/fixtures/addTestData.js b/imports/startup/server/fixtures/addTestData.js index 70924a1d..2e7812d4 100644 --- a/imports/startup/server/fixtures/addTestData.js +++ b/imports/startup/server/fixtures/addTestData.js @@ -49,7 +49,7 @@ export function addTestUsers() { return { adminId, newUserId, curatorId } } -export function addTestGenome(annot=false) { +export function addTestGenome(annot=false, multiple=false) { const annotObj = annot ? [{ name: 'myfilename.gff'}] : undefined @@ -93,6 +93,24 @@ export function addTestGenome(annot=false) { seq: 'AAAA', attributes: {"myNewAttribute": 1} }) + + if (multiple){ + Genes.insert({ + ID: 'BniB01g000010.2N', + seqid: 'B1', + source: 'AAFC_GIFS', + strand: '-', + type: 'gene', + start: 13640, + end: 15401, + genomeId: genomeId, + annotationName: "Annotation name 2", + score: '.', + subfeatures: [subfeature, cds], + seq: 'AAAA', + attributes: {"myNewAttribute": 1} + }) + } } return { genomeId, genomeSeqId, geneId: "BniB01g000010.2N" } From d4c2a30e16a714fdd4c31618115b27dc6d87d1c1 Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 13 Oct 2023 16:36:42 +0000 Subject: [PATCH 22/42] actually fix tests --- imports/api/genes/alignment/parser/pairwiseParser.js | 10 +++++----- imports/api/genes/eggnog/eggnog.test.js | 6 +++--- imports/api/genes/hectar/hectar.test.js | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/imports/api/genes/alignment/parser/pairwiseParser.js b/imports/api/genes/alignment/parser/pairwiseParser.js index c8fe618b..b9d62c3f 100644 --- a/imports/api/genes/alignment/parser/pairwiseParser.js +++ b/imports/api/genes/alignment/parser/pairwiseParser.js @@ -85,7 +85,7 @@ class PairwiseProcessor { && this.pairWise.iteration_query !== queryClean) { /** Update or insert pairwise. */ this.similarSeqBulkOp.find({ - iteration_query: this.pairWise.iteration_query, + iteration_query: this.currentGene.ID, protein_id: this.pairWise.iteration_query, annotationName: this.currentGene.annotationName, }).upsert().update( @@ -95,7 +95,7 @@ class PairwiseProcessor { algorithm_ref: this.algorithm, matrix_ref: this.matrix, database_ref: this.database, - iteration_query: this.pairWise.iteration_query, + iteration_query: this.currentGene.ID, annotationName: this.currentGene.annotationName, protein_id: this.pairWise.iteration_query, query_len: this.pairWise.query_length, @@ -384,12 +384,12 @@ class PairwiseProcessor { */ lastPairwise = () => { - if (typeof this.currentGene !== "undefined") { + if (typeof this.currentGene === "undefined") { return { ok:"", writeErrors:"", nInserted:0, nUpserted: 0 } } this.similarSeqBulkOp.find({ - iteration_query: this.pairWise.iteration_query, + iteration_query: this.currentGene.ID, protein_id: this.pairWise.iteration_query, annotationName: this.currentGene.annotationName, }).upsert().update( @@ -399,7 +399,7 @@ class PairwiseProcessor { algorithm_ref: this.algorithm, matrix_ref: this.matrix, database_ref: this.database, - iteration_query: this.pairWise.iteration_query, + iteration_query: this.currentGene.ID, annotationName: this.currentGene.annotationName, protein_id: this.pairWise.iteration_query, query_len: this.pairWise.query_length, diff --git a/imports/api/genes/eggnog/eggnog.test.js b/imports/api/genes/eggnog/eggnog.test.js index ac47bd2d..29ec7d57 100644 --- a/imports/api/genes/eggnog/eggnog.test.js +++ b/imports/api/genes/eggnog/eggnog.test.js @@ -64,10 +64,10 @@ describe('eggnog', function testEggnog() { chai.assert.lengthOf(egg.GOs, 18); chai.assert.equal(egg.Description, 'UDP-glucuronic acid decarboxylase'); - const gene1 = Genes.findOne({ID: 'BniB01g000010.2N.1', annotationName: 'Annotation name'}) - const gene2 = Genes.findOne({ID: 'BniB01g000010.2N.1', annotationName: 'Annotation name 2'}) + const gene1 = Genes.findOne({ID: 'BniB01g000010.2N', annotationName: 'Annotation name'}) + const gene2 = Genes.findOne({ID: 'BniB01g000010.2N', annotationName: 'Annotation name 2'}) - chai.assert.isDefined(gene2.eggnogId, "eggNodeId is not defined for the correct annotation") + chai.assert.isDefined(gene1.eggnogId, "eggNodeId is not defined for the correct annotation") chai.assert.isUndefined(gene2.eggnogId, "eggNodeId is defined for the wrong annotation") diff --git a/imports/api/genes/hectar/hectar.test.js b/imports/api/genes/hectar/hectar.test.js index 9dbae5eb..b9a5312b 100644 --- a/imports/api/genes/hectar/hectar.test.js +++ b/imports/api/genes/hectar/hectar.test.js @@ -63,10 +63,10 @@ describe('hectar', function testHectar() { chai.assert.equal(hec.mitochondrion_score, '0.1032'); chai.assert.equal(hec.other_score, '0.8968'); - const gene1 = Genes.findOne({ID: 'BniB01g000010.2N.1', annotationName: 'Annotation name'}) - const gene2 = Genes.findOne({ID: 'BniB01g000010.2N.1', annotationName: 'Annotation name 2'}) + const gene1 = Genes.findOne({ID: 'BniB01g000010.2N', annotationName: 'Annotation name'}) + const gene2 = Genes.findOne({ID: 'BniB01g000010.2N', annotationName: 'Annotation name 2'}) - chai.assert.isDefined(gene2.eggnogId, "eggNodeId is not defined for the correct annotation") - chai.assert.isUndefined(gene2.eggnogId, "eggNodeId is defined for the wrong annotation") + chai.assert.isDefined(gene1.hectarId, "eggNodeId is not defined for the correct annotation") + chai.assert.isUndefined(gene2.hectarId, "eggNodeId is defined for the wrong annotation") }); }); From 947d12bbd20726163752a6539f49b10f8043b638 Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 13 Oct 2023 18:38:37 +0200 Subject: [PATCH 23/42] Index more stuff --- imports/api/genes/alignment/similarSequenceCollection.js | 3 +++ imports/api/genes/interproscan/interproscanCollection.js | 1 + 2 files changed, 4 insertions(+) diff --git a/imports/api/genes/alignment/similarSequenceCollection.js b/imports/api/genes/alignment/similarSequenceCollection.js index b902b752..a4e13d3f 100644 --- a/imports/api/genes/alignment/similarSequenceCollection.js +++ b/imports/api/genes/alignment/similarSequenceCollection.js @@ -20,14 +20,17 @@ const similarSequencesSchema = new SimpleSchema({ }, iteration_query: { type: String, + index: true, label: 'Query sequence name.', }, protein_id: { type: String, + index: true, label: 'Protein_id', }, annotationName: { type: String, + index: true, label: 'Annotation name', }, iteration_hits: { diff --git a/imports/api/genes/interproscan/interproscanCollection.js b/imports/api/genes/interproscan/interproscanCollection.js index 7628ad86..1a175aed 100644 --- a/imports/api/genes/interproscan/interproscanCollection.js +++ b/imports/api/genes/interproscan/interproscanCollection.js @@ -9,6 +9,7 @@ const interproscanSchema = new SimpleSchema({ }, protein_id: { type: String, + index: true, label: 'Linked protein ID', }, annotationName: { From 6efabdb954cdc1d21260ad0d1c5a814d71c73f49 Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 13 Oct 2023 18:41:06 +0200 Subject: [PATCH 24/42] Add annot name to UI --- imports/ui/singleGenePage/GeneralInfo.jsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/imports/ui/singleGenePage/GeneralInfo.jsx b/imports/ui/singleGenePage/GeneralInfo.jsx index 1982f7f1..bcda3cdf 100644 --- a/imports/ui/singleGenePage/GeneralInfo.jsx +++ b/imports/ui/singleGenePage/GeneralInfo.jsx @@ -364,6 +364,12 @@ function GeneInfo({ + + Annotation name + + {`${gene.annotationName} `} + + Genome coordinates From 0c0b56455eed1d2511173c8e0f353552352e0b51 Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 13 Oct 2023 17:06:07 +0000 Subject: [PATCH 25/42] fixes --- .../geneExpression/SampleSelection.jsx | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/imports/ui/singleGenePage/geneExpression/SampleSelection.jsx b/imports/ui/singleGenePage/geneExpression/SampleSelection.jsx index e6cbe4bf..bc3d4f8d 100644 --- a/imports/ui/singleGenePage/geneExpression/SampleSelection.jsx +++ b/imports/ui/singleGenePage/geneExpression/SampleSelection.jsx @@ -10,6 +10,10 @@ import { ExperimentInfo } from '/imports/api/transcriptomes/transcriptome_collec import { Dropdown, DropdownMenu, DropdownButton } from '/imports/ui/util/Dropdown.jsx'; +import { + branch, compose, round, /* ErrorBoundary, */ +} from '/imports/ui/util/uiUtil.jsx'; + import './sampleSelection.scss'; function dataTracker({ gene, showHeader, children }) { @@ -27,6 +31,23 @@ function dataTracker({ gene, showHeader, children }) { }; } + +function hasNoExpression({ experiments }) { + return experiments.length === 0; +} + +function NoExpression() { + return ( +
    +
    +
    +

    No expression data found

    +
    +
    +
    + ); +} + const customStyles = { control: (provided) => ({ ...provided, @@ -197,4 +218,7 @@ function SampleSelection({ ); } -export default withTracker(dataTracker)(SampleSelection); +export default compose( + withTracker(dataTracker), + branch(hasNoExpression, NoExpression), +)(SampleSelection); From a461e52c2f9dd0e43770aa6533058bcd6a665a47 Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 16 Oct 2023 09:59:41 +0200 Subject: [PATCH 26/42] Test gene selection --- imports/ui/singleGenePage/SingleGenePage.jsx | 38 +++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/imports/ui/singleGenePage/SingleGenePage.jsx b/imports/ui/singleGenePage/SingleGenePage.jsx index 042d1f40..73578b57 100644 --- a/imports/ui/singleGenePage/SingleGenePage.jsx +++ b/imports/ui/singleGenePage/SingleGenePage.jsx @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { withTracker } from 'meteor/react-meteor-data'; import React from 'react'; +import { Link } from 'react-router-dom'; import hash from 'object-hash'; import { Genes } from '/imports/api/genes/geneCollection.js'; @@ -36,16 +37,34 @@ function isLoading({ loading }) { return loading; } -function isNotFound({ gene }) { - return typeof gene === 'undefined'; +function isNotFound({ genes }) { + return gene.length === 0; } -function isMultiple({ gene }) { +function isMultiple({ genes }) { return gene.length > 1; } -function Multiple( {gene} ){ - return
    Multiple annotation
    ; +function Multiple( {genes} ){ + let content = genes.map(gene => { + const query = new URLSearchParams(); + query.set("annotation", gene.annotationName); + const url = `/gene/${gene.ID}?${query.toString()}` + return ( + + { geneId } + + ); + }) + + return ( +
    +

    This gene has several available versions. Please select one:

    +
    + {content} +
    +
    + ); } function geneDataTracker({ match, genomeDataCache, location }) { @@ -54,20 +73,21 @@ function geneDataTracker({ match, genomeDataCache, location }) { const geneSub = Meteor.subscribe('singleGene', { geneId }); let gene if (annotation) { - gene = Genes.findOne({ ID: geneId, annotationName: annotation }); + gene = Genes.find{ ID: geneId, annotationName: annotation }).fetch(); } else { - gene = Genes.findOne({ ID: geneId }); + gene = Genes.findOne({ ID: geneId }).fetch(); } const loading = !geneSub.ready(); return { loading, - gene, + genes, genomeDataCache, }; } -function genomeDataTracker({ gene, genomeDataCache }) { +function genomeDataTracker({ genes, genomeDataCache }) { // const genomeSub = Meteor.subscribe('genomes'); + let gene = genes[0] const { genomeId } = gene; let genome; let genomeSub; From c82c2e850fba886d099057f45dc4ace9099dd6ec Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 16 Oct 2023 14:49:41 +0000 Subject: [PATCH 27/42] fix ui --- imports/ui/singleGenePage/SingleGenePage.jsx | 37 +++++++++++++------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/imports/ui/singleGenePage/SingleGenePage.jsx b/imports/ui/singleGenePage/SingleGenePage.jsx index 73578b57..39e91f62 100644 --- a/imports/ui/singleGenePage/SingleGenePage.jsx +++ b/imports/ui/singleGenePage/SingleGenePage.jsx @@ -38,30 +38,41 @@ function isLoading({ loading }) { } function isNotFound({ genes }) { - return gene.length === 0; + return genes.length === 0; } function isMultiple({ genes }) { - return gene.length > 1; + return genes.length > 1; } function Multiple( {genes} ){ + let gene = genes[0] + let content = genes.map(gene => { const query = new URLSearchParams(); query.set("annotation", gene.annotationName); const url = `/gene/${gene.ID}?${query.toString()}` return ( - - { geneId } - +

    + { gene.annotationName } +

    ); }) return ( -
    -

    This gene has several available versions. Please select one:

    -
    - {content} +
    +
    +
    +

    + {`${gene.ID} `} +

    +
    +

    This gene is defined in several annotations. Please select one:

    +
    + {content} +
    +
    +
    ); @@ -71,12 +82,13 @@ function geneDataTracker({ match, genomeDataCache, location }) { const { geneId } = match.params; const annotation = new URLSearchParams(location.search).get("annotation"); const geneSub = Meteor.subscribe('singleGene', { geneId }); - let gene + let genes if (annotation) { - gene = Genes.find{ ID: geneId, annotationName: annotation }).fetch(); + genes = Genes.find({ ID: geneId, annotationName: annotation }).fetch(); } else { - gene = Genes.findOne({ ID: geneId }).fetch(); + genes = Genes.find({ ID: geneId }).fetch(); } + const loading = !geneSub.ready(); return { loading, @@ -211,6 +223,7 @@ export default compose( withTracker(geneDataTracker), branch(isLoading, Loading), branch(isNotFound, NotFound), + branch(isMultiple, Multiple), withTracker(genomeDataTracker), branch(isLoading, Loading), )(SingleGenePage); From f32d42763f06be075ba8ad1d01525043fb69207c Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 16 Oct 2023 17:12:23 +0200 Subject: [PATCH 28/42] Test annotation selector in UI --- .../genetable/filteroptions/GenomeSelect.jsx | 99 ++++++++++++++++++- 1 file changed, 95 insertions(+), 4 deletions(-) diff --git a/imports/ui/genetable/filteroptions/GenomeSelect.jsx b/imports/ui/genetable/filteroptions/GenomeSelect.jsx index 638598b3..9584256f 100644 --- a/imports/ui/genetable/filteroptions/GenomeSelect.jsx +++ b/imports/ui/genetable/filteroptions/GenomeSelect.jsx @@ -38,6 +38,16 @@ function GenomeSelect({ new Set(genomes.map((genome) => genome._id)), ); + const [selectedAnnotations, setselectedAnnotations] = useState( + new Set(genomes.map((genome) => { + genome.annotationTrack.map((annotation) => annotation.name) + })).flat()), + ); + + let annotations = new Set(genomes.map((genome) => { + genome.annotationTrack.map((annotation) => annotation.name) + })).flat()) + function toggleGenomeSelect(genomeId) { const newSelection = cloneDeep(selectedGenomes); const newQuery = cloneDeep(query); @@ -57,7 +67,26 @@ function GenomeSelect({ updateQuery(newQuery); } - function selectAll() { + function toggleAnnotationSelect(annotationName) { + const newSelection = cloneDeep(selectedAnnotations); + const newQuery = cloneDeep(query); + + if (newSelection.has(annotationName)) { + newSelection.delete(annotationName); + } else { + newSelection.add(annotationName); + } + setSelectedAnnotations(newSelection); + + if (newSelection.size < annotations.length) { + newQuery.annotationName = { $in: [...newSelection] }; + } else if (hasOwnProperty(query, 'annotationName')) { + delete newQuery.annotationName; + } + updateQuery(newQuery); + } + + function selectAllGenomes() { const newSelection = new Set(genomes.map((genome) => genome._id)); setSelectedGenomes(newSelection); @@ -66,7 +95,7 @@ function GenomeSelect({ updateQuery(newQuery); } - function unselectAll() { + function unselectAllGenomes() { const newSelection = new Set(); setSelectedGenomes(newSelection); @@ -75,6 +104,26 @@ function GenomeSelect({ updateQuery(newQuery); } + function selectAllAnnotations() { + const newSelection = new Set(genomes.map((genome) => { + genome.annotationTrack.map((annotation) => annotation.name) + })).flat()); + setSelectedAnnotations(newSelection); + + const newQuery = cloneDeep(query); + newQuery.annotationName = { $in: [...newSelection] }; + updateQuery(newQuery); + } + + function unselectAllAnnotations() { + const newSelection = new Set(); + setSelectedAnnotations(newSelection); + + const newQuery = cloneDeep(query); + newQuery.annotationName = { $in: [...newSelection] }; + updateQuery(newQuery); + } + return (
    @@ -108,10 +157,52 @@ function GenomeSelect({ ); })}
    - + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + Select annotations +
    + {annotations.map(({name}) => { + const checked = selectedAnnotations.has(name); + return ( +
    + +
    + ); + })} +
    + -
    From 3e3407e58b3c54ac240ebe21abd432d34487bef7 Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 16 Oct 2023 16:02:51 +0000 Subject: [PATCH 29/42] ui --- .../genetable/filteroptions/GenomeSelect.jsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/imports/ui/genetable/filteroptions/GenomeSelect.jsx b/imports/ui/genetable/filteroptions/GenomeSelect.jsx index 9584256f..a5be7727 100644 --- a/imports/ui/genetable/filteroptions/GenomeSelect.jsx +++ b/imports/ui/genetable/filteroptions/GenomeSelect.jsx @@ -24,6 +24,7 @@ function genomeDataTracker({ ...props }) { annotationTrack: { $exists: true }, }) .fetch(); + return { loading, genomes, @@ -38,15 +39,15 @@ function GenomeSelect({ new Set(genomes.map((genome) => genome._id)), ); - const [selectedAnnotations, setselectedAnnotations] = useState( - new Set(genomes.map((genome) => { - genome.annotationTrack.map((annotation) => annotation.name) - })).flat()), + const [selectedAnnotations, setSelectedAnnotations] = useState( + new Set(genomes.flatMap((genome) => { + return genome.annotationTrack.map((annotation) => annotation.name); + })), ); - let annotations = new Set(genomes.map((genome) => { - genome.annotationTrack.map((annotation) => annotation.name) - })).flat()) + let annotations = genomes.flatMap((genome) => { + return genome.annotationTrack.map((annotation) => annotation.name) + }) function toggleGenomeSelect(genomeId) { const newSelection = cloneDeep(selectedGenomes); @@ -105,9 +106,9 @@ function GenomeSelect({ } function selectAllAnnotations() { - const newSelection = new Set(genomes.map((genome) => { + const newSelection = new Set(genomes.flatMap((genome) => { genome.annotationTrack.map((annotation) => annotation.name) - })).flat()); + })); setSelectedAnnotations(newSelection); const newQuery = cloneDeep(query); @@ -125,6 +126,7 @@ function GenomeSelect({ } return ( + <>
    + ); } From 468afd2b9225ca23091d24d93b4424af13de1fe4 Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 17 Oct 2023 10:39:31 +0200 Subject: [PATCH 30/42] Add annot to transcriptome --- imports/api/publications.js | 3 +- imports/api/transcriptomes/addExpression.js | 37 ++++++++++++++----- .../addKallistoTranscriptome.js | 35 +++++++++++++----- .../transcriptome_collection.js | 4 ++ .../api/transcriptomes/transcriptomes.test.js | 24 ++++++++---- .../geneExpression/SampleSelection.jsx | 4 +- 6 files changed, 79 insertions(+), 28 deletions(-) diff --git a/imports/api/publications.js b/imports/api/publications.js index eed47fe7..16a3e58b 100644 --- a/imports/api/publications.js +++ b/imports/api/publications.js @@ -154,7 +154,7 @@ Meteor.publish({ $or: [{ genomes: { $in: genomeIds } }, { allGenomes: true }], }); }, - geneExpression(geneId) { + geneExpression(geneId, annotationName) { const publication = this; const roles = Roles.getRolesForUser(publication.userId); const permission = { $in: roles }; @@ -168,6 +168,7 @@ Meteor.publish({ return Transcriptomes.find({ geneId, + annotationName, experimentId: { $in: experimentIds, }, diff --git a/imports/api/transcriptomes/addExpression.js b/imports/api/transcriptomes/addExpression.js index e4ed1b3d..76e86c7e 100644 --- a/imports/api/transcriptomes/addExpression.js +++ b/imports/api/transcriptomes/addExpression.js @@ -12,15 +12,23 @@ import { } from '/imports/api/transcriptomes/transcriptome_collection.js'; import logger from '/imports/api/util/logger.js'; -const getGenomeId = (data) => { +const getGenomeId = (data, annot) => { const firstTranscripts = data.slice(0, 10).map((line) => line.gene); logger.debug(firstTranscripts); - const gene = Genes.findOne({ + + let geneQuery = { $or: [ { ID: { $in: firstTranscripts } }, { 'subfeatures.ID': { $in: firstTranscripts } }, ], - }); + } + + if (annot){ + geneQuery['annotationName'] = annot + } + + const gene = Genes.findOne(geneQuery); + if (typeof gene === "undefined"){ return undefined } @@ -29,7 +37,7 @@ const getGenomeId = (data) => { }; const parseExpressionTsv = ({ - fileName, description, replicas = [], replicaNames = [], permission = 'admin', isPublic = false, + fileName, description, annot, replicas = [], replicaNames = [], permission = 'admin', isPublic = false, }) => new Promise((resolve, reject) => { const fileHandle = fs.readFileSync(fileName, { encoding: 'binary' }); const bulkOp = Transcriptomes.rawCollection().initializeUnorderedBulkOp(); @@ -80,7 +88,7 @@ const parseExpressionTsv = ({ } let firstColumn = replicaGroups.shift(); - const genomeId = getGenomeId(data); + const genomeId = getGenomeId(data, annot); if (typeof genomeId === 'undefined') { reject(new Meteor.Error('Could not find genomeId for first transcript')); @@ -100,12 +108,18 @@ const parseExpressionTsv = ({ }); data.forEach((row) => { - const gene = Genes.findOne({ + let geneQuery = { $or: [ { ID: row[firstColumn] }, { 'subfeatures.ID': row[firstColumn] }, ], - }); + } + + if (annot){ + geneQuery['annotationName'] = annot + } + + const gene = Genes.findOne(geneQuery); if (typeof gene === 'undefined') { logger.warn(`${target_id} not found`); @@ -114,6 +128,7 @@ const parseExpressionTsv = ({ replicaGroups.forEach((replicaGroup) => { bulkOp.insert({ geneId: gene.ID, + annotationName: gene.annotationName, tpm: row[replicaGroup], experimentId: experiments[replicaGroup] }); @@ -136,6 +151,10 @@ const addExpression = new ValidatedMethod({ validate: new SimpleSchema({ fileName: String, description: String, + annot: { + type: String, + optional: true, + }, replicas: { type: Array, optional: true, @@ -158,7 +177,7 @@ const addExpression = new ValidatedMethod({ noRetry: true, }, run({ - fileName, description, replicas, replicaNames, isPublic + fileName, description, annot, replicas, replicaNames, isPublic }) { if (!this.userId) { throw new Meteor.Error('not-authorized'); @@ -167,7 +186,7 @@ const addExpression = new ValidatedMethod({ throw new Meteor.Error('not-authorized'); } return parseExpressionTsv({ - fileName, description, replicas, replicaNames, isPublic + fileName, description, annot, replicas, replicaNames, isPublic }) .catch((error) => { logger.warn(error); diff --git a/imports/api/transcriptomes/addKallistoTranscriptome.js b/imports/api/transcriptomes/addKallistoTranscriptome.js index d6a4ff62..2237910a 100644 --- a/imports/api/transcriptomes/addKallistoTranscriptome.js +++ b/imports/api/transcriptomes/addKallistoTranscriptome.js @@ -12,21 +12,28 @@ import { } from '/imports/api/transcriptomes/transcriptome_collection.js'; import logger from '/imports/api/util/logger.js'; -const getGenomeId = (data) => { +const getGenomeId = (data, annot) => { const firstTranscipts = data.slice(0, 10).map((line) => line.target_id); logger.debug(firstTranscipts); - const { genomeId } = Genes.findOne({ + + let geneQuery = { $or: [ { ID: { $in: firstTranscipts } }, { 'subfeatures.ID': { $in: firstTranscipts } }, ], - }); + } + + if (annot){ + geneQuery['annotationName'] = annot + } + + const { genomeId } = Genes.findOne(geneQuery); logger.debug(genomeId); return genomeId; }; const parseKallistoTsv = ({ - fileName, sampleName, replicaGroup, + fileName, annot, sampleName, replicaGroup, description, permission = 'admin', isPublic = false, }) => new Promise((resolve, reject) => { const fileHandle = fs.readFileSync(fileName, { encoding: 'binary' }); @@ -43,7 +50,7 @@ const parseKallistoTsv = ({ complete({ data }, _file) { let nInserted = 0; - const genomeId = getGenomeId(data); + const genomeId = getGenomeId(data, annot); if (typeof genomeId === 'undefined') { reject(new Meteor.Error('Could not find genomeId for first transcript')); @@ -62,12 +69,18 @@ const parseKallistoTsv = ({ }); data.forEach(({ target_id, tpm, est_counts }) => { - const gene = Genes.findOne({ + let geneQuery = { $or: [ { ID: target_id }, { 'subfeatures.ID': target_id }, ], - }); + } + + if (annot){ + geneQuery['annotationName'] = annot + } + + const gene = Genes.findOne(geneQuery); if (typeof gene === 'undefined') { logger.warn(`${target_id} not found`); @@ -96,6 +109,10 @@ const addKallistoTranscriptome = new ValidatedMethod({ name: 'addKallistoTranscriptome', validate: new SimpleSchema({ fileName: String, + annot: { + type: String, + optional: true, + }, sampleName: String, replicaGroup: String, description: String, @@ -105,7 +122,7 @@ const addKallistoTranscriptome = new ValidatedMethod({ noRetry: true, }, run({ - fileName, sampleName, replicaGroup, description, isPublic + fileName, annot, sampleName, replicaGroup, description, isPublic }) { if (!this.userId) { throw new Meteor.Error('not-authorized'); @@ -114,7 +131,7 @@ const addKallistoTranscriptome = new ValidatedMethod({ throw new Meteor.Error('not-authorized'); } return parseKallistoTsv({ - fileName, sampleName, replicaGroup, description, isPublic + fileName, annot, sampleName, replicaGroup, description, isPublic }) .catch((error) => { logger.warn(error); diff --git a/imports/api/transcriptomes/transcriptome_collection.js b/imports/api/transcriptomes/transcriptome_collection.js index 574256cd..d51527dc 100644 --- a/imports/api/transcriptomes/transcriptome_collection.js +++ b/imports/api/transcriptomes/transcriptome_collection.js @@ -40,6 +40,10 @@ const TranscriptomeSchema = new SimpleSchema({ label: 'Gene ID', index: true, }, + annotationName: { + type: String, + label: 'Annotation name', + }, experimentId: { type: String, label: 'Experiment ID', diff --git a/imports/api/transcriptomes/transcriptomes.test.js b/imports/api/transcriptomes/transcriptomes.test.js index a52a60d9..482535a5 100644 --- a/imports/api/transcriptomes/transcriptomes.test.js +++ b/imports/api/transcriptomes/transcriptomes.test.js @@ -37,10 +37,11 @@ describe('transcriptomes', function testTranscriptomes() { // Increase timeout this.timeout(20000); - const {genomeId, genomeSeqId} = addTestGenome(annot=true) + const {genomeId, genomeSeqId} = addTestGenome(annot=true, multiple=true) const transcriParams = { fileName: 'assets/app/data/Bnigra_kallisto_abundance.tsv', + annot: "Annotation name", sampleName: "mySample", replicaGroup: "replicaGroup", description: "A new description", @@ -77,6 +78,7 @@ describe('transcriptomes', function testTranscriptomes() { const transcriptome = transcriptomes[0] chai.assert.equal(transcriptome.geneId, 'BniB01g000010.2N') + chai.assert.equal(transcriptome.annotationName, "Annotation name") chai.assert.equal(transcriptome.tpm, '1.80368') chai.assert.equal(transcriptome.est_counts, '21') @@ -86,10 +88,11 @@ describe('transcriptomes', function testTranscriptomes() { // Increase timeout this.timeout(20000); - const {genomeId, genomeSeqId} = addTestGenome(annot=true) + const {genomeId, genomeSeqId} = addTestGenome(annot=true, multiple=true) const transcriParams = { fileName: 'assets/app/data/Bnigra_abundance.tsv', + annot: "Annotation name", description: "A new description", isPublic: false }; @@ -128,6 +131,7 @@ describe('transcriptomes', function testTranscriptomes() { const transcriptome = transcriptomes[0] chai.assert.equal(transcriptome.geneId, 'BniB01g000010.2N') + chai.assert.equal(transcriptome.annotationName, "Annotation name") chai.assert.equal(transcriptome.tpm, '40') chai.assert.isUndefined(transcriptome.est_counts) @@ -137,10 +141,11 @@ describe('transcriptomes', function testTranscriptomes() { // Increase timeout this.timeout(20000); - const {genomeId, genomeSeqId} = addTestGenome(annot=true) + const {genomeId, genomeSeqId} = addTestGenome(annot=true, multiple=true) const transcriParams = { fileName: 'assets/app/data/Bnigra_abundance.tsv', + annot: "Annotation name", description: "A new description", replicas: ["1,2"], isPublic: false @@ -184,6 +189,7 @@ describe('transcriptomes', function testTranscriptomes() { const transcriptome = transcriptomes[0] chai.assert.equal(transcriptome.geneId, 'BniB01g000010.2N') + chai.assert.equal(transcriptome.annotationName, "Annotation name") chai.assert.equal(transcriptome.tpm, '40') chai.assert.isUndefined(transcriptome.est_counts) @@ -193,10 +199,11 @@ describe('transcriptomes', function testTranscriptomes() { // Increase timeout this.timeout(20000); - const {genomeId, genomeSeqId} = addTestGenome(annot=true) + const {genomeId, genomeSeqId} = addTestGenome(annot=true, multiple=true) const transcriParams = { fileName: 'assets/app/data/Bnigra_abundance.tsv', + annot: "Annotation name", description: "A new description", replicas: ["1,2"], replicaNames: ["My replica group name", "Another group name"], @@ -245,6 +252,7 @@ describe('transcriptomes', function testTranscriptomes() { const transcriptome = transcriptomes[0] chai.assert.equal(transcriptome.geneId, 'BniB01g000010.2N') + chai.assert.equal(transcriptome.annotationName, "Annotation name") chai.assert.equal(transcriptome.tpm, '40') chai.assert.isUndefined(transcriptome.est_counts) @@ -254,10 +262,11 @@ describe('transcriptomes', function testTranscriptomes() { // Increase timeout this.timeout(20000); - const {genomeId, genomeSeqId} = addTestGenome(annot=true) + const {genomeId, genomeSeqId} = addTestGenome(annot=true, multiple=true) const transcriParams = { fileName: 'assets/app/data/Bnigra_abundance.tsv', + annot: "Annotation name", description: "A new description", replicaNames: ["TestReplica1", "TestReplica2"], isPublic: false @@ -301,6 +310,7 @@ describe('transcriptomes', function testTranscriptomes() { const transcriptome = transcriptomes[0] chai.assert.equal(transcriptome.geneId, 'BniB01g000010.2N') + chai.assert.equal(transcriptome.annotationName, "Annotation name") chai.assert.equal(transcriptome.tpm, '40') chai.assert.isUndefined(transcriptome.est_counts) @@ -311,7 +321,7 @@ describe('transcriptomes', function testTranscriptomes() { // Increase timeout this.timeout(20000); - const {genomeId, genomeSeqId, geneId} = addTestGenome(annot=true) + const {genomeId, genomeSeqId, geneId} = addTestGenome(annot=true, multiple=true) const {expId, transcriptomeId} = addTestTranscriptome(genomeId, geneId) const updateParams = { @@ -347,7 +357,7 @@ describe('transcriptomes', function testTranscriptomes() { // Increase timeout this.timeout(20000); - const {genomeId, genomeSeqId, geneId} = addTestGenome(annot=true) + const {genomeId, genomeSeqId, geneId} = addTestGenome(annot=true, multiple=true) const {expId, transcriptomeId} = addTestTranscriptome(genomeId, geneId) const updateParams = { diff --git a/imports/ui/singleGenePage/geneExpression/SampleSelection.jsx b/imports/ui/singleGenePage/geneExpression/SampleSelection.jsx index bc3d4f8d..b79a8272 100644 --- a/imports/ui/singleGenePage/geneExpression/SampleSelection.jsx +++ b/imports/ui/singleGenePage/geneExpression/SampleSelection.jsx @@ -17,10 +17,10 @@ import { import './sampleSelection.scss'; function dataTracker({ gene, showHeader, children }) { - const { genomeId } = gene; + const { genomeId, annotationName } = gene; const experimentSub = Meteor.subscribe('experimentInfo'); const loading = !experimentSub.ready(); - const experiments = ExperimentInfo.find({ genomeId }).fetch(); + const experiments = ExperimentInfo.find({ genomeId, annotationName }).fetch(); const replicaGroups = groupBy(experiments, 'replicaGroup'); return { showHeader, From 0e3b0607a2998e5a9a323e747bff1662bb08d460 Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 17 Oct 2023 14:30:47 +0200 Subject: [PATCH 31/42] Fix tests --- imports/api/transcriptomes/addKallistoTranscriptome.js | 1 + imports/startup/server/fixtures/addTestData.js | 1 + 2 files changed, 2 insertions(+) diff --git a/imports/api/transcriptomes/addKallistoTranscriptome.js b/imports/api/transcriptomes/addKallistoTranscriptome.js index 2237910a..08861bfc 100644 --- a/imports/api/transcriptomes/addKallistoTranscriptome.js +++ b/imports/api/transcriptomes/addKallistoTranscriptome.js @@ -88,6 +88,7 @@ const parseKallistoTsv = ({ nInserted += 1; bulkOp.insert({ geneId: gene.ID, + annotationName: gene.annotationName, tpm, est_counts, experimentId, diff --git a/imports/startup/server/fixtures/addTestData.js b/imports/startup/server/fixtures/addTestData.js index 2e7812d4..973e0e93 100644 --- a/imports/startup/server/fixtures/addTestData.js +++ b/imports/startup/server/fixtures/addTestData.js @@ -129,6 +129,7 @@ export function addTestTranscriptome(genomeId, geneId) { const transcriptomeId = Transcriptomes.insert({ geneId: geneId, + annotationName: "Annotation name", tpm: "60", est_counts: "1000", experimentId: expId From 311557c3cbb13b6dcf74495f8ef3d3af71d10c4b Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 18 Oct 2023 08:38:57 +0000 Subject: [PATCH 32/42] Fix --- imports/ui/singleGenePage/geneExpression/SampleSelection.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imports/ui/singleGenePage/geneExpression/SampleSelection.jsx b/imports/ui/singleGenePage/geneExpression/SampleSelection.jsx index b79a8272..bc3d4f8d 100644 --- a/imports/ui/singleGenePage/geneExpression/SampleSelection.jsx +++ b/imports/ui/singleGenePage/geneExpression/SampleSelection.jsx @@ -17,10 +17,10 @@ import { import './sampleSelection.scss'; function dataTracker({ gene, showHeader, children }) { - const { genomeId, annotationName } = gene; + const { genomeId } = gene; const experimentSub = Meteor.subscribe('experimentInfo'); const loading = !experimentSub.ready(); - const experiments = ExperimentInfo.find({ genomeId, annotationName }).fetch(); + const experiments = ExperimentInfo.find({ genomeId }).fetch(); const replicaGroups = groupBy(experiments, 'replicaGroup'); return { showHeader, From 7c18735b926f8c9f225dc9d5456061fe162c2672 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 18 Oct 2023 09:24:20 +0000 Subject: [PATCH 33/42] fix expression ui --- imports/ui/singleGenePage/geneExpression/ExpressionPlot.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/imports/ui/singleGenePage/geneExpression/ExpressionPlot.jsx b/imports/ui/singleGenePage/geneExpression/ExpressionPlot.jsx index 16db79ef..c84ae4d7 100644 --- a/imports/ui/singleGenePage/geneExpression/ExpressionPlot.jsx +++ b/imports/ui/singleGenePage/geneExpression/ExpressionPlot.jsx @@ -29,13 +29,14 @@ import './expressionPlot.scss'; function expressionDataTracker({ gene, samples, loading, }) { - const transcriptomeSub = Meteor.subscribe('geneExpression', gene.ID); + const transcriptomeSub = Meteor.subscribe('geneExpression', gene.ID, gene.annotationName); const sampleInfo = groupBy(samples, '_id'); const sampleIds = samples.map((sample) => sample._id); const values = Transcriptomes.find({ geneId: gene.ID, + annotationName: gene.annotationName, experimentId: { $in: sampleIds, }, From 16545e656d0b201c018a910157b7bf0d49e816af Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 18 Oct 2023 14:43:22 +0200 Subject: [PATCH 34/42] Add annotationName to experiment document to pre-filter in geneList --- imports/api/transcriptomes/addExpression.js | 7 +++--- .../addKallistoTranscriptome.js | 23 +++++++++++-------- .../transcriptome_collection.js | 4 ++++ .../api/transcriptomes/transcriptomes.test.js | 23 +++++++++++++++---- .../geneExpression/SampleSelection.jsx | 4 ++-- 5 files changed, 42 insertions(+), 19 deletions(-) diff --git a/imports/api/transcriptomes/addExpression.js b/imports/api/transcriptomes/addExpression.js index 76e86c7e..cf738578 100644 --- a/imports/api/transcriptomes/addExpression.js +++ b/imports/api/transcriptomes/addExpression.js @@ -33,7 +33,7 @@ const getGenomeId = (data, annot) => { return undefined } logger.debug(gene.genomeId); - return gene.genomeId + return {genomeId: gene.genomeId, annotationName: gene.annotationName} }; const parseExpressionTsv = ({ @@ -88,7 +88,7 @@ const parseExpressionTsv = ({ } let firstColumn = replicaGroups.shift(); - const genomeId = getGenomeId(data, annot); + const {genomeId, annotationName} = getGenomeId(data, annot); if (typeof genomeId === 'undefined') { reject(new Meteor.Error('Could not find genomeId for first transcript')); @@ -99,6 +99,7 @@ const parseExpressionTsv = ({ const replicaGroup = replicaIndex + 1 in replicaNamesDict ? replicaNamesDict[replicaIndex + 1] : sampleName experiments[sampleName] = ExperimentInfo.insert({ genomeId, + annotationName, sampleName, replicaGroup, description, @@ -128,7 +129,7 @@ const parseExpressionTsv = ({ replicaGroups.forEach((replicaGroup) => { bulkOp.insert({ geneId: gene.ID, - annotationName: gene.annotationName, + annotationName, tpm: row[replicaGroup], experimentId: experiments[replicaGroup] }); diff --git a/imports/api/transcriptomes/addKallistoTranscriptome.js b/imports/api/transcriptomes/addKallistoTranscriptome.js index 08861bfc..e79ab933 100644 --- a/imports/api/transcriptomes/addKallistoTranscriptome.js +++ b/imports/api/transcriptomes/addKallistoTranscriptome.js @@ -13,13 +13,13 @@ import { import logger from '/imports/api/util/logger.js'; const getGenomeId = (data, annot) => { - const firstTranscipts = data.slice(0, 10).map((line) => line.target_id); - logger.debug(firstTranscipts); + const firstTranscripts = data.slice(0, 10).map((line) => line.gene); + logger.debug(firstTranscripts); let geneQuery = { $or: [ - { ID: { $in: firstTranscipts } }, - { 'subfeatures.ID': { $in: firstTranscipts } }, + { ID: { $in: firstTranscripts } }, + { 'subfeatures.ID': { $in: firstTranscripts } }, ], } @@ -27,9 +27,13 @@ const getGenomeId = (data, annot) => { geneQuery['annotationName'] = annot } - const { genomeId } = Genes.findOne(geneQuery); - logger.debug(genomeId); - return genomeId; + const gene = Genes.findOne(geneQuery); + + if (typeof gene === "undefined"){ + return undefined + } + logger.debug(gene.genomeId); + return {genomeId: gene.genomeId, annotationName: gene.annotationName} }; const parseKallistoTsv = ({ @@ -50,7 +54,7 @@ const parseKallistoTsv = ({ complete({ data }, _file) { let nInserted = 0; - const genomeId = getGenomeId(data, annot); + const {genomeId, annotationName} = getGenomeId(data, annot); if (typeof genomeId === 'undefined') { reject(new Meteor.Error('Could not find genomeId for first transcript')); @@ -60,6 +64,7 @@ const parseKallistoTsv = ({ const experimentId = ExperimentInfo.insert({ genomeId, + annotationName, sampleName, replicaGroup, description, @@ -88,7 +93,7 @@ const parseKallistoTsv = ({ nInserted += 1; bulkOp.insert({ geneId: gene.ID, - annotationName: gene.annotationName, + annotationName, tpm, est_counts, experimentId, diff --git a/imports/api/transcriptomes/transcriptome_collection.js b/imports/api/transcriptomes/transcriptome_collection.js index d51527dc..3ce2fc15 100644 --- a/imports/api/transcriptomes/transcriptome_collection.js +++ b/imports/api/transcriptomes/transcriptome_collection.js @@ -8,6 +8,10 @@ const ExperimentInfoSchema = new SimpleSchema({ type: String, label: 'Genome ID', }, + annotationName: { + type: String, + label: 'Annotation name', + }, sampleName: { type: String, label: 'Short name for the sample', diff --git a/imports/api/transcriptomes/transcriptomes.test.js b/imports/api/transcriptomes/transcriptomes.test.js index 482535a5..72db2997 100644 --- a/imports/api/transcriptomes/transcriptomes.test.js +++ b/imports/api/transcriptomes/transcriptomes.test.js @@ -70,6 +70,7 @@ describe('transcriptomes', function testTranscriptomes() { chai.assert.equal(exp.sampleName, 'mySample') chai.assert.equal(exp.replicaGroup, 'replicaGroup') chai.assert.equal(exp.description, 'A new description') + chai.assert.equal(exp.annotationName, "Annotation name") const transcriptomes = Transcriptomes.find({experimentId: exp._id}).fetch() @@ -78,8 +79,8 @@ describe('transcriptomes', function testTranscriptomes() { const transcriptome = transcriptomes[0] chai.assert.equal(transcriptome.geneId, 'BniB01g000010.2N') - chai.assert.equal(transcriptome.annotationName, "Annotation name") chai.assert.equal(transcriptome.tpm, '1.80368') + chai.assert.equal(transcriptome.annotationName, "Annotation name") chai.assert.equal(transcriptome.est_counts, '21') }) @@ -119,10 +120,12 @@ describe('transcriptomes', function testTranscriptomes() { chai.assert.equal(exp.sampleName, 'sample1') chai.assert.equal(exp.replicaGroup, 'sample1') chai.assert.equal(exp.description, 'A new description') + chai.assert.equal(exp.annotationName, "Annotation name") chai.assert.equal(exps[1].sampleName, 'sample2') chai.assert.equal(exps[1].replicaGroup, 'sample2') chai.assert.equal(exps[1].description, 'A new description') + chai.assert.equal(exps[1].annotationName, "Annotation name") const transcriptomes = Transcriptomes.find({experimentId: exp._id}).fetch() @@ -131,8 +134,8 @@ describe('transcriptomes', function testTranscriptomes() { const transcriptome = transcriptomes[0] chai.assert.equal(transcriptome.geneId, 'BniB01g000010.2N') - chai.assert.equal(transcriptome.annotationName, "Annotation name") chai.assert.equal(transcriptome.tpm, '40') + chai.assert.equal(transcriptome.annotationName, "Annotation name") chai.assert.isUndefined(transcriptome.est_counts) }) @@ -173,14 +176,17 @@ describe('transcriptomes', function testTranscriptomes() { chai.assert.equal(exp.sampleName, 'sample1') chai.assert.equal(exp.replicaGroup, 'sample1') chai.assert.equal(exp.description, 'A new description') + chai.assert.equal(exp.annotationName, "Annotation name") chai.assert.equal(exps[1].sampleName, 'sample2') chai.assert.equal(exps[1].replicaGroup, 'sample1') chai.assert.equal(exps[1].description, 'A new description') + chai.assert.equal(exps[1].annotationName, "Annotation name") chai.assert.equal(exps[2].sampleName, 'sample3') chai.assert.equal(exps[2].replicaGroup, 'sample3') chai.assert.equal(exps[2].description, 'A new description') + chai.assert.equal(exps[2].annotationName, "Annotation name") const transcriptomes = Transcriptomes.find({experimentId: exp._id}).fetch() @@ -189,8 +195,8 @@ describe('transcriptomes', function testTranscriptomes() { const transcriptome = transcriptomes[0] chai.assert.equal(transcriptome.geneId, 'BniB01g000010.2N') - chai.assert.equal(transcriptome.annotationName, "Annotation name") chai.assert.equal(transcriptome.tpm, '40') + chai.assert.equal(transcriptome.annotationName, "Annotation name") chai.assert.isUndefined(transcriptome.est_counts) }) @@ -232,18 +238,22 @@ describe('transcriptomes', function testTranscriptomes() { chai.assert.equal(exp.sampleName, 'sample1') chai.assert.equal(exp.replicaGroup, 'My replica group name') chai.assert.equal(exp.description, 'A new description') + chai.assert.equal(exp.annotationName, "Annotation name") chai.assert.equal(exps[1].sampleName, 'sample2') chai.assert.equal(exps[1].replicaGroup, 'My replica group name') chai.assert.equal(exps[1].description, 'A new description') + chai.assert.equal(exps[1].annotationName, "Annotation name") chai.assert.equal(exps[2].sampleName, 'sample3') chai.assert.equal(exps[2].replicaGroup, 'Another group name') chai.assert.equal(exps[2].description, 'A new description') + chai.assert.equal(exps[2].annotationName, "Annotation name") chai.assert.equal(exps[3].sampleName, 'sample4') chai.assert.equal(exps[3].replicaGroup, 'sample4') chai.assert.equal(exps[3].description, 'A new description') + chai.assert.equal(exps[3].annotationName, "Annotation name") const transcriptomes = Transcriptomes.find({experimentId: exp._id}).fetch() @@ -252,9 +262,9 @@ describe('transcriptomes', function testTranscriptomes() { const transcriptome = transcriptomes[0] chai.assert.equal(transcriptome.geneId, 'BniB01g000010.2N') - chai.assert.equal(transcriptome.annotationName, "Annotation name") chai.assert.equal(transcriptome.tpm, '40') chai.assert.isUndefined(transcriptome.est_counts) + chai.assert.equal(transcriptome.annotationName, "Annotation name") }) @@ -294,14 +304,17 @@ describe('transcriptomes', function testTranscriptomes() { chai.assert.equal(exp.sampleName, 'sample1') chai.assert.equal(exp.replicaGroup, 'TestReplica1') chai.assert.equal(exp.description, 'A new description') + chai.assert.equal(exp.annotationName, "Annotation name") chai.assert.equal(exps[1].sampleName, 'sample2') chai.assert.equal(exps[1].replicaGroup, 'TestReplica2') chai.assert.equal(exps[1].description, 'A new description') + chai.assert.equal(exps[1].annotationName, "Annotation name") chai.assert.equal(exps[2].sampleName, 'sample3') chai.assert.equal(exps[2].replicaGroup, 'sample3') chai.assert.equal(exps[2].description, 'A new description') + chai.assert.equal(exps[2].annotationName, "Annotation name") const transcriptomes = Transcriptomes.find({experimentId: exp._id}).fetch() @@ -310,8 +323,8 @@ describe('transcriptomes', function testTranscriptomes() { const transcriptome = transcriptomes[0] chai.assert.equal(transcriptome.geneId, 'BniB01g000010.2N') - chai.assert.equal(transcriptome.annotationName, "Annotation name") chai.assert.equal(transcriptome.tpm, '40') + chai.assert.equal(transcriptome.annotationName, "Annotation name") chai.assert.isUndefined(transcriptome.est_counts) }) diff --git a/imports/ui/singleGenePage/geneExpression/SampleSelection.jsx b/imports/ui/singleGenePage/geneExpression/SampleSelection.jsx index bc3d4f8d..b79a8272 100644 --- a/imports/ui/singleGenePage/geneExpression/SampleSelection.jsx +++ b/imports/ui/singleGenePage/geneExpression/SampleSelection.jsx @@ -17,10 +17,10 @@ import { import './sampleSelection.scss'; function dataTracker({ gene, showHeader, children }) { - const { genomeId } = gene; + const { genomeId, annotationName } = gene; const experimentSub = Meteor.subscribe('experimentInfo'); const loading = !experimentSub.ready(); - const experiments = ExperimentInfo.find({ genomeId }).fetch(); + const experiments = ExperimentInfo.find({ genomeId, annotationName }).fetch(); const replicaGroups = groupBy(experiments, 'replicaGroup'); return { showHeader, From 47003c8fb9cf4493084f3ab95e8b60d32d50fa82 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 18 Oct 2023 15:12:28 +0000 Subject: [PATCH 35/42] fix test & ui --- imports/api/transcriptomes/addExpression.js | 8 ++++---- .../transcriptomes/addKallistoTranscriptome.js | 4 ++-- imports/startup/server/fixtures/addTestData.js | 1 + .../geneExpression/ExpressionPlot.jsx | 18 ++++++++++++++++++ .../geneExpression/SampleSelection.jsx | 7 +++++-- 5 files changed, 30 insertions(+), 8 deletions(-) diff --git a/imports/api/transcriptomes/addExpression.js b/imports/api/transcriptomes/addExpression.js index cf738578..75b0dabc 100644 --- a/imports/api/transcriptomes/addExpression.js +++ b/imports/api/transcriptomes/addExpression.js @@ -12,8 +12,8 @@ import { } from '/imports/api/transcriptomes/transcriptome_collection.js'; import logger from '/imports/api/util/logger.js'; -const getGenomeId = (data, annot) => { - const firstTranscripts = data.slice(0, 10).map((line) => line.gene); +const getGenomeId = (data, firstColumn, annot) => { + const firstTranscripts = data.slice(0, 10).map((line) => line[firstColumn]); logger.debug(firstTranscripts); let geneQuery = { @@ -30,7 +30,7 @@ const getGenomeId = (data, annot) => { const gene = Genes.findOne(geneQuery); if (typeof gene === "undefined"){ - return undefined + return {genomeId: undefined, annotationName: undefined} } logger.debug(gene.genomeId); return {genomeId: gene.genomeId, annotationName: gene.annotationName} @@ -88,7 +88,7 @@ const parseExpressionTsv = ({ } let firstColumn = replicaGroups.shift(); - const {genomeId, annotationName} = getGenomeId(data, annot); + const {genomeId, annotationName} = getGenomeId(data, firstColumn, annot); if (typeof genomeId === 'undefined') { reject(new Meteor.Error('Could not find genomeId for first transcript')); diff --git a/imports/api/transcriptomes/addKallistoTranscriptome.js b/imports/api/transcriptomes/addKallistoTranscriptome.js index e79ab933..64547317 100644 --- a/imports/api/transcriptomes/addKallistoTranscriptome.js +++ b/imports/api/transcriptomes/addKallistoTranscriptome.js @@ -13,7 +13,7 @@ import { import logger from '/imports/api/util/logger.js'; const getGenomeId = (data, annot) => { - const firstTranscripts = data.slice(0, 10).map((line) => line.gene); + const firstTranscripts = data.slice(0, 10).map((line) => line.target_id); logger.debug(firstTranscripts); let geneQuery = { @@ -30,7 +30,7 @@ const getGenomeId = (data, annot) => { const gene = Genes.findOne(geneQuery); if (typeof gene === "undefined"){ - return undefined + return {genomeId: undefined, annotationName: undefined} } logger.debug(gene.genomeId); return {genomeId: gene.genomeId, annotationName: gene.annotationName} diff --git a/imports/startup/server/fixtures/addTestData.js b/imports/startup/server/fixtures/addTestData.js index 973e0e93..9001129f 100644 --- a/imports/startup/server/fixtures/addTestData.js +++ b/imports/startup/server/fixtures/addTestData.js @@ -121,6 +121,7 @@ export function addTestTranscriptome(genomeId, geneId) { const expId = ExperimentInfo.insert({ genomeId: genomeId, sampleName: "sampleName", + annotationName: "Annotation name", replicaGroup: "replicaGroup", description: 'description', permission: 'admin', diff --git a/imports/ui/singleGenePage/geneExpression/ExpressionPlot.jsx b/imports/ui/singleGenePage/geneExpression/ExpressionPlot.jsx index c84ae4d7..6bddb97c 100644 --- a/imports/ui/singleGenePage/geneExpression/ExpressionPlot.jsx +++ b/imports/ui/singleGenePage/geneExpression/ExpressionPlot.jsx @@ -83,6 +83,23 @@ function Loading() { ); } +function hasNoExpression({ values }) { + return values.length == 0; +} + +function NoExpression() { + return( +
    +
    +
    +

    No expression data for the selected samples

    +
    +
    +
    + ) +} + + function YAxis({ scale, numTicks }) { const range = scale.range(); const [start, end] = scale.domain(); @@ -407,4 +424,5 @@ export default compose( branch(hasNoSamples, NoSamples), withTracker(expressionDataTracker), branch(isLoading, Loading), + branch(hasNoExpression, NoExpression) )(ExpressionPlot); diff --git a/imports/ui/singleGenePage/geneExpression/SampleSelection.jsx b/imports/ui/singleGenePage/geneExpression/SampleSelection.jsx index b79a8272..6fc4a990 100644 --- a/imports/ui/singleGenePage/geneExpression/SampleSelection.jsx +++ b/imports/ui/singleGenePage/geneExpression/SampleSelection.jsx @@ -41,7 +41,7 @@ function NoExpression() {
    -

    No expression data found

    +

    No samples found

    @@ -91,10 +91,13 @@ function SampleSelection({ })); } + let className = showHeader ? "is-pulled-right" : "" + let style = showHeader ? {} : {"text-align": "right"} + return ( <> { showHeader &&
    } -
    +