From a802bc36eef627c6ddd0468be7cf276443696632 Mon Sep 17 00:00:00 2001 From: TheSinnerAR Date: Wed, 17 Jul 2024 22:23:43 -0300 Subject: [PATCH 01/27] Add Bisecting K-Means Cluster Algorithm in Feature Selection (ClusteringAlgorithm in models.py) - [src/feature_selection/models.py] - [src/feature_selection/fs_models.py] - [src/feature_selection/views.py] - [src/frontend/static/frontend/src/components/biomarkers/labels/ClusteringAlgotithmLable.tsx] - [src/frontend/static/frontend/src/components/biomarkers/types.ts] - [src/frontend/static/frontend/src/components/biomarkers/utils.ts] --- src/feature_selection/fs_algorithms_spark.py | 2 ++ src/feature_selection/fs_models.py | 6 ++++-- src/feature_selection/models.py | 1 + src/feature_selection/views.py | 8 +++++++- .../biomarkers/labels/ClusteringAlgorithmLabel.tsx | 4 ++++ .../static/frontend/src/components/biomarkers/types.ts | 3 ++- .../static/frontend/src/components/biomarkers/utils.ts | 3 ++- 7 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/feature_selection/fs_algorithms_spark.py b/src/feature_selection/fs_algorithms_spark.py index bc2125bb..fcec625d 100644 --- a/src/feature_selection/fs_algorithms_spark.py +++ b/src/feature_selection/fs_algorithms_spark.py @@ -32,6 +32,8 @@ def __get_clustering_algorithm_value(cluster_algorithm: ClusteringAlgorithm) -> """Gets the corresponding string value for the parameter 'clustering-algorithm' of the EMR integration.""" if cluster_algorithm == ClusteringAlgorithm.SPECTRAL: return 'spectral' + if cluster_algorithm == ClusteringAlgorithm.BK_MEANS: + return 'bk_means' return 'k_means' # Default is kmeans diff --git a/src/feature_selection/fs_models.py b/src/feature_selection/fs_models.py index 4570b80f..a8de8e1d 100644 --- a/src/feature_selection/fs_models.py +++ b/src/feature_selection/fs_models.py @@ -1,6 +1,6 @@ from typing import Literal, Union, Optional from django.conf import settings -from sklearn.cluster import KMeans, SpectralClustering +from sklearn.cluster import KMeans, SpectralClustering, BisectingKMeans from sksurv.ensemble import RandomSurvivalForest from sksurv.svm import FastKernelSurvivalSVM from .models import ClusteringAlgorithm @@ -12,7 +12,7 @@ SVMOptimizerOptions = Literal["avltree", "rbtree"] # Available models for clustering -ClusteringModels = Union[KMeans, SpectralClustering] +ClusteringModels = Union[KMeans, SpectralClustering, BisectingKMeans] def get_clustering_model(clustering_algorithm: ClusteringAlgorithm, @@ -28,6 +28,8 @@ def get_clustering_model(clustering_algorithm: ClusteringAlgorithm, return KMeans(n_clusters=number_of_clusters, random_state=random_state, n_init='auto') elif clustering_algorithm == ClusteringAlgorithm.SPECTRAL: return SpectralClustering(n_clusters=number_of_clusters, random_state=random_state) + elif clustering_algorithm == ClusteringAlgorithm.BK_MEANS: + return BisectingKMeans(n_clusters=number_of_clusters, random_state=random_state) raise Exception(f'Invalid clustering_algorithm parameter: {clustering_algorithm}') diff --git a/src/feature_selection/models.py b/src/feature_selection/models.py index e6c921fc..d8f164ff 100644 --- a/src/feature_selection/models.py +++ b/src/feature_selection/models.py @@ -31,6 +31,7 @@ class ClusteringAlgorithm(models.IntegerChoices): """Clustering algorithm.""" K_MEANS = 1 SPECTRAL = 2 # TODO: implement in backend + BK_MEANS = 3 class ClusteringMetric(models.IntegerChoices): diff --git a/src/feature_selection/views.py b/src/feature_selection/views.py index 7565b171..c6a52903 100644 --- a/src/feature_selection/views.py +++ b/src/feature_selection/views.py @@ -210,7 +210,13 @@ def __get_clustering_parameters_columns(row: pd.Series) -> Tuple[int, Clustering parameters_desc = row['parameters'] params = parameters_desc.split('_') number_of_clusters, algorithm_description, scoring_method = params[0], params[2], params[4] - algorithm = ClusteringAlgorithm.K_MEANS if algorithm_description == 'k-means' else ClusteringAlgorithm.SPECTRAL + # algorithm = ClusteringAlgorithm.K_MEANS if algorithm_description == 'k-means' else ClusteringAlgorithm.SPECTRAL + if algorithm_description == 'k-means': + algorithm = ClusteringAlgorithm.K_MEANS + elif algorithm_description == 'spectral': + algorithm = ClusteringAlgorithm.SPECTRAL + else: + algorithm = ClusteringAlgorithm.BK_MEANS scoring = ClusteringScoringMethod.C_INDEX if scoring_method == 'concordance-index' \ else ClusteringScoringMethod.LOG_LIKELIHOOD return number_of_clusters, algorithm, scoring diff --git a/src/frontend/static/frontend/src/components/biomarkers/labels/ClusteringAlgorithmLabel.tsx b/src/frontend/static/frontend/src/components/biomarkers/labels/ClusteringAlgorithmLabel.tsx index 79c41539..5535b83a 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/labels/ClusteringAlgorithmLabel.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/labels/ClusteringAlgorithmLabel.tsx @@ -28,6 +28,10 @@ export const ClusteringAlgorithmLabel = (props: ClusteringAlgorithmLabelProps) = color = 'blue' description = 'Spectral' break + case ClusteringAlgorithm.BK_MEANS: + color = 'blue' + description = 'Bisecting KMeans' + break default: color = 'blue' description = '' diff --git a/src/frontend/static/frontend/src/components/biomarkers/types.ts b/src/frontend/static/frontend/src/components/biomarkers/types.ts index 67da6130..8a5c5ff3 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/types.ts +++ b/src/frontend/static/frontend/src/components/biomarkers/types.ts @@ -208,7 +208,8 @@ enum FitnessFunction { /** Clustering algorithm. */ enum ClusteringAlgorithm { K_MEANS = 1, - SPECTRAL = 2 + SPECTRAL = 2, + BK_MEANS = 3 } /** Clustering metric to optimize. */ diff --git a/src/frontend/static/frontend/src/components/biomarkers/utils.ts b/src/frontend/static/frontend/src/components/biomarkers/utils.ts index 971cd35a..a51237af 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/utils.ts +++ b/src/frontend/static/frontend/src/components/biomarkers/utils.ts @@ -36,7 +36,8 @@ const SVMKernelOptions: DropdownItemProps[] = [ /** Available options for a Clustering algorithm. */ const clusteringAlgorithmOptions: DropdownItemProps[] = [ { key: ClusteringAlgorithm.K_MEANS, text: 'K-Means', value: ClusteringAlgorithm.K_MEANS }, - { key: ClusteringAlgorithm.SPECTRAL, text: 'Spectral', value: ClusteringAlgorithm.SPECTRAL } + { key: ClusteringAlgorithm.SPECTRAL, text: 'Spectral', value: ClusteringAlgorithm.SPECTRAL }, + { key: ClusteringAlgorithm.BK_MEANS, text: 'BK-Means', value: ClusteringAlgorithm.BK_MEANS } ] /** Available options for a Clustering metric to optimize. */ From 2d487e68b19d20c09df64fe6058e87b107a73c85 Mon Sep 17 00:00:00 2001 From: Genaro Camele Date: Thu, 3 Oct 2024 22:48:46 -0300 Subject: [PATCH 02/27] Fixed DEPLOYING instructions to integrate with Modulector and BioAPI --- DEPLOYING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DEPLOYING.md b/DEPLOYING.md index 14f1405f..eda5dcaf 100644 --- a/DEPLOYING.md +++ b/DEPLOYING.md @@ -179,7 +179,7 @@ To integrate with [Modulector][modulector] and/or [BioAPI][bioapi] using `docker name: 'multiomix-network' ``` 3. The new versions of BioAPI and Modulector already come with service names suitable for integration with Multiomix. But **if you have any old version of those platforms**, change the Modulector and BioAPI configuration so that it does not conflict with the Multiomix configuration: - 1. Rename all the services in the Modulector and BioAPI `docker-compose.yml` files with the suffix `_modulector` and `_bioapi`. And rename `web` service to `modulector` or `bioapi` respectively. **NOTE:** do not forget to rename the `depends_on` parameters, and the database connection parameters to point to the new services names. + 1. Rename all the services in the Modulector and BioAPI `docker-compose.yml` files with the suffix `_modulector` and `_bioapi`. For example `mongo_bioapi`, `web_bioapi` and `nginx_bioapi` in the case of BioAPI. **NOTE:** do not forget to rename the `depends_on` parameters, and the database connection parameters to point to the new services names. 2. Change the following block in the NGINX configuration files. In Modulector it's `config/nginx/conf.d/modulector.conf`, in BioAPI it's `/nginx/conf.d/default.conf`: ``` # Old @@ -191,7 +191,7 @@ To integrate with [Modulector][modulector] and/or [BioAPI][bioapi] using `docker # New upstream web { ip_hash; - server modulector:8000; # Or bioapi, dependening on which config file you're + server web_modulector:8000; # Or web_bioapi, dependening on which config file you're editing } ``` 4. Set Multiomix parameters: From 5ae8d08d5c0865691f99289164c35a1ca170d36a Mon Sep 17 00:00:00 2001 From: Hernan Date: Fri, 11 Oct 2024 18:58:18 -0300 Subject: [PATCH 03/27] Fix External API Service --- src/api_service/mrna_service.py | 14 ++++++++++++-- src/multiomics_intermediate/settings.py | 6 ++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/api_service/mrna_service.py b/src/api_service/mrna_service.py index 16a59f3d..95280624 100644 --- a/src/api_service/mrna_service.py +++ b/src/api_service/mrna_service.py @@ -13,10 +13,20 @@ class MRNAService(object): def __init__(self): modulector_settings = settings.MODULECTOR_SETTINGS - self.url_modulector_prefix = f"http://{modulector_settings['host']}:{modulector_settings['port']}" + if modulector_settings['protocol'] == 'http' and modulector_settings['port'] == 80: + self.url_modulector_prefix = f"{modulector_settings['protocol']}://{modulector_settings['host']}" + elif modulector_settings['protocol'] == 'https' and modulector_settings['port'] == 443: + self.url_modulector_prefix = f"{modulector_settings['protocol']}://{modulector_settings['host']}" + else: + self.url_modulector_prefix = f"{modulector_settings['protocol']}://{modulector_settings['host']}:{modulector_settings['port']}" bioapi_settings = settings.BIOAPI_SETTINGS - self.url_bioapi_prefix = f"http://{bioapi_settings['host']}:{bioapi_settings['port']}" + if bioapi_settings['protocol'] == 'http' and bioapi_settings['port'] == 80: + self.url_bioapi_prefix = f"{bioapi_settings['protocol']}://{bioapi_settings['host']}" + elif bioapi_settings['protocol'] == 'https' and bioapi_settings['port'] == 443: + self.url_bioapi_prefix = f"{bioapi_settings['protocol']}://{bioapi_settings['host']}" + else: + self.url_bioapi_prefix = f"{bioapi_settings['protocol']}://{bioapi_settings['host']}:{bioapi_settings['port']}" @staticmethod def __generate_rest_query_params(get_request: QueryDict) -> str: diff --git a/src/multiomics_intermediate/settings.py b/src/multiomics_intermediate/settings.py index beaf5106..e2477aa4 100644 --- a/src/multiomics_intermediate/settings.py +++ b/src/multiomics_intermediate/settings.py @@ -300,13 +300,15 @@ # Modulector settings MODULECTOR_SETTINGS = { 'host': os.getenv('MODULECTOR_HOST', '127.0.0.1'), - 'port': os.getenv('MODULECTOR_PORT', '8001') + 'port': os.getenv('MODULECTOR_PORT', '8001'), + 'protocol': os.getenv('BIOAPI_PROTOCOL', 'http') } # BioAPI settings BIOAPI_SETTINGS = { 'host': os.getenv('BIOAPI_HOST', '127.0.0.1'), - 'port': os.getenv('BIOAPI_PORT', '8002') + 'port': os.getenv('BIOAPI_PORT', '8002'), + 'protocol': os.getenv('BIOAPI_PROTOCOL', 'http') } # Multiomix-aws-emr From 250bed4f66385d9e0fef49d93575e49ff47d0e19 Mon Sep 17 00:00:00 2001 From: Juan Nicolas Herrera Delvalle Date: Mon, 14 Oct 2024 23:31:08 -0300 Subject: [PATCH 04/27] fix unlink dataset if not cgds, and fixs --- .../ClinicalSourcePopup.tsx | 11 ++++---- .../survival-analysis/KaplanMeier.tsx | 26 ++++++++++++++----- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/frontend/static/frontend/src/components/pipeline/all-experiments-view/ClinicalSourcePopup.tsx b/src/frontend/static/frontend/src/components/pipeline/all-experiments-view/ClinicalSourcePopup.tsx index 59eb5a50..e0f8596b 100644 --- a/src/frontend/static/frontend/src/components/pipeline/all-experiments-view/ClinicalSourcePopup.tsx +++ b/src/frontend/static/frontend/src/components/pipeline/all-experiments-view/ClinicalSourcePopup.tsx @@ -72,7 +72,7 @@ interface ClinicalSourceState { /** Alert interface */ alert: CustomAlert, /** Posibles values for survival tuple */ - survivalTuplesPossiblesValues: string[], + survivalTuplesPossiblesValues: string[] | undefined, } /** @@ -137,8 +137,9 @@ export class ClinicalSourcePopup extends React.Component 0) { clinicalSource.type = SourceType.UPLOADED_DATASETS clinicalSource.selectedExistingFile = selectedFile - - const survivalColumns = selectedFile.survival_columns ?? [] - this.setState({ clinicalSource, survivalColumns }, this.updateSourceFilenames) + const survivalColumns = selectedFile.survival_columns || [] + const survivalTuplesPossiblesValues = undefined + this.setState({ clinicalSource, survivalColumns, survivalTuplesPossiblesValues }, this.updateSourceFilenames) } else { this.clinicalSourceVoid() } diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/gene-gem-details/survival-analysis/KaplanMeier.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/gene-gem-details/survival-analysis/KaplanMeier.tsx index d9c61383..19668101 100644 --- a/src/frontend/static/frontend/src/components/pipeline/experiment-result/gene-gem-details/survival-analysis/KaplanMeier.tsx +++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/gene-gem-details/survival-analysis/KaplanMeier.tsx @@ -65,6 +65,8 @@ interface KaplanMeierChartState { selectedSurvivalColumnId: Nullable, /** Flag of survival data request */ gettingSurvivalData: boolean, + /** Flag to check if source is being unlink */ + unlinkingSource: boolean, } /** @@ -83,7 +85,8 @@ class KaplanMeierChart extends React.Component { response.json().then((response: DjangoCommonResponse) => { if (response.status.code === DjangoResponseCode.SUCCESS) { - // Todo: callback para success + unlinkingSource = false + this.setState({ unlinkingSource }) } else { alertGeneralError() } @@ -324,15 +337,14 @@ class KaplanMeierChart extends React.Component -
+
From f1ae78c58ebffd06345f3c9db99f909894bff54b Mon Sep 17 00:00:00 2001 From: Juan Nicolas Herrera Delvalle Date: Mon, 14 Oct 2024 23:44:41 -0300 Subject: [PATCH 05/27] clean posibleTuples if switch selection type --- .../pipeline/all-experiments-view/ClinicalSourcePopup.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frontend/static/frontend/src/components/pipeline/all-experiments-view/ClinicalSourcePopup.tsx b/src/frontend/static/frontend/src/components/pipeline/all-experiments-view/ClinicalSourcePopup.tsx index e0f8596b..747a416f 100644 --- a/src/frontend/static/frontend/src/components/pipeline/all-experiments-view/ClinicalSourcePopup.tsx +++ b/src/frontend/static/frontend/src/components/pipeline/all-experiments-view/ClinicalSourcePopup.tsx @@ -138,8 +138,8 @@ export class ClinicalSourcePopup extends React.Component Date: Tue, 15 Oct 2024 22:03:06 -0300 Subject: [PATCH 06/27] doc for undefined tuple --- .../pipeline/all-experiments-view/ClinicalSourcePopup.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/frontend/static/frontend/src/components/pipeline/all-experiments-view/ClinicalSourcePopup.tsx b/src/frontend/static/frontend/src/components/pipeline/all-experiments-view/ClinicalSourcePopup.tsx index 747a416f..935e76f7 100644 --- a/src/frontend/static/frontend/src/components/pipeline/all-experiments-view/ClinicalSourcePopup.tsx +++ b/src/frontend/static/frontend/src/components/pipeline/all-experiments-view/ClinicalSourcePopup.tsx @@ -71,7 +71,7 @@ interface ClinicalSourceState { cgdsStudyName: Nullable, /** Alert interface */ alert: CustomAlert, - /** Posibles values for survival tuple */ + /** Posibles values for survival tuple, undefined prevents rendering an empty Select for CGDS Datasets or user Datasets */ survivalTuplesPossiblesValues: string[] | undefined, } @@ -137,7 +137,9 @@ export class ClinicalSourcePopup extends React.Component Date: Tue, 22 Oct 2024 09:23:30 -0300 Subject: [PATCH 07/27] fix kaplan Y axis, mobile responsive --- ...StatisticalValidationResultKaplanMeier.tsx | 13 +++--- .../StatisticalValidationResultModal.tsx | 40 +++++++++---------- .../survival-analysis/KaplanMeierUtils.tsx | 12 ++++-- .../survival-analysis/kaplanMeier.css | 3 ++ 4 files changed, 39 insertions(+), 29 deletions(-) create mode 100644 src/frontend/static/frontend/src/components/pipeline/experiment-result/gene-gem-details/survival-analysis/kaplanMeier.css diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/stat-validations/result/StatisticalValidationResultKaplanMeier.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/stat-validations/result/StatisticalValidationResultKaplanMeier.tsx index 0c6ec5fe..289845f7 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/stat-validations/result/StatisticalValidationResultKaplanMeier.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/stat-validations/result/StatisticalValidationResultKaplanMeier.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react' import ky from 'ky' -import { Button, Form, Grid, Header, Icon, Modal, Statistic } from 'semantic-ui-react' +import { Button, Form, Grid, Header, Icon, Modal, Statistic, GridColumn } from 'semantic-ui-react' import { alertGeneralError, listToDropdownOptions } from '../../../../../utils/util_functions' import { StatisticalValidationForTable, KaplanMeierResultData, FitnessFunction } from '../../../types' import { KaplanMeier } from '../../../../pipeline/experiment-result/gene-gem-details/survival-analysis/KaplanMeierUtils' @@ -188,10 +188,13 @@ export const StatisticalValidationResultKaplanMeier = (props: StatisticalValidat return ( - + {/* {getKaplanMeierPanel()} - - + */} + + {getKaplanMeierPanel()} + + {/* Clustering metrics. */} } - + ) diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/stat-validations/result/StatisticalValidationResultModal.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/stat-validations/result/StatisticalValidationResultModal.tsx index 3aab6102..60231a03 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/stat-validations/result/StatisticalValidationResultModal.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/stat-validations/result/StatisticalValidationResultModal.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react' import { ActiveStatValidationsItemMenu, StatisticalValidationForTable } from '../../../types' import { StatisticalValidationMenu } from '../StatisticalValidationMenu' import { StatisticalValidationResultMetrics } from './StatisticalValidationResultMetrics' -import { Grid, Segment } from 'semantic-ui-react' +import { Grid, Segment, GridColumn } from 'semantic-ui-react' import { StatisticalValidationResultBestFeatures } from './StatisticalValidationResultBestFeatures' import { StatisticalValidationResultHeatMap } from './StatisticalValidationResultHeatMap' import { StatisticalValidationResultKaplanMeier } from './StatisticalValidationResultKaplanMeier' @@ -41,27 +41,25 @@ export const StatisticalValidationResultModal = (props: StatisticalValidationRes return ( <> - - - - - - - - - - {/* Menu */} - + + + + + + + + + {/* Menu */} + - {/* Selected menu option */} - {getSelectedComponent()} - - - + {/* Selected menu option */} + {getSelectedComponent()} + + ) diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/gene-gem-details/survival-analysis/KaplanMeierUtils.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/gene-gem-details/survival-analysis/KaplanMeierUtils.tsx index 12616511..b8da3a0e 100644 --- a/src/frontend/static/frontend/src/components/pipeline/experiment-result/gene-gem-details/survival-analysis/KaplanMeierUtils.tsx +++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/gene-gem-details/survival-analysis/KaplanMeierUtils.tsx @@ -1,7 +1,7 @@ import React from 'react' import * as d3 from 'd3' import { Nullable } from '../../../../../utils/interfaces' - +import './kaplanMeier.css' /** Needed structure for KaplanMeier chart */ type KaplanMeierSample = { time: number, @@ -174,7 +174,13 @@ const YAxis = (props: AxisProps) => { className='axis axis-y' transform={`translate(${props.left}, ${props.top})`} > - + { + /** + * g tag generate a structure of ... + * the idea es usign the first g wrapp the inside g tag and text to move it a few px to prevents being overwrite by Y axis + */ + } + { const colors = d3.scaleOrdinal(d3.schemeCategory10).range() const yDomain = [1, 0] - const seriesWidth = this.props.width - 64 + const seriesWidth = this.props.width const seriesHeight = this.props.height - 64 return ( diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/gene-gem-details/survival-analysis/kaplanMeier.css b/src/frontend/static/frontend/src/components/pipeline/experiment-result/gene-gem-details/survival-analysis/kaplanMeier.css new file mode 100644 index 00000000..589bd9bc --- /dev/null +++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/gene-gem-details/survival-analysis/kaplanMeier.css @@ -0,0 +1,3 @@ +.g-wrapper> g > text { + transform: translateX(-20px); /* Prevents Y axis overwrite text */ +} \ No newline at end of file From 8ea943a2af1adb32e8c042a69d0ebeb06aa91944 Mon Sep 17 00:00:00 2001 From: Juan Nicolas Herrera Delvalle Date: Tue, 22 Oct 2024 09:41:10 -0300 Subject: [PATCH 08/27] comments doc --- .../result/StatisticalValidationResultKaplanMeier.tsx | 3 --- .../gene-gem-details/survival-analysis/KaplanMeierUtils.tsx | 5 +++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/stat-validations/result/StatisticalValidationResultKaplanMeier.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/stat-validations/result/StatisticalValidationResultKaplanMeier.tsx index 289845f7..3979f855 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/stat-validations/result/StatisticalValidationResultKaplanMeier.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/stat-validations/result/StatisticalValidationResultKaplanMeier.tsx @@ -188,9 +188,6 @@ export const StatisticalValidationResultKaplanMeier = (props: StatisticalValidat return ( - {/* - {getKaplanMeierPanel()} - */} {getKaplanMeierPanel()} diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/gene-gem-details/survival-analysis/KaplanMeierUtils.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/gene-gem-details/survival-analysis/KaplanMeierUtils.tsx index b8da3a0e..d334f403 100644 --- a/src/frontend/static/frontend/src/components/pipeline/experiment-result/gene-gem-details/survival-analysis/KaplanMeierUtils.tsx +++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/gene-gem-details/survival-analysis/KaplanMeierUtils.tsx @@ -176,8 +176,9 @@ const YAxis = (props: AxisProps) => { > { /** - * g tag generate a structure of ... - * the idea es usign the first g wrapp the inside g tag and text to move it a few px to prevents being overwrite by Y axis + * g tag generate the dom structure of ... + * The idea es usign the first g wrapp the inside g tag and text to move it a few px to prevents being overwrite by Y axis. + * If the Dom change in the future, open the navigator inspect then verify the Dom and fix the new structure. */ } From 6d2734f1a3398ae77e81fc422860cf957e58f2f0 Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 23 Oct 2024 13:09:16 -0300 Subject: [PATCH 09/27] Hotfix consumo de APIs externas y agregado de migrations --- .../0061_alter_experiment_shared_users.py | 20 +++++++++++++ src/api_service/mrna_service.py | 30 ++++++++++++------- ...clusteringparameters_algorithm_and_more.py | 23 ++++++++++++++ src/multiomics_intermediate/settings.py | 4 +-- 4 files changed, 64 insertions(+), 13 deletions(-) create mode 100644 src/api_service/migrations/0061_alter_experiment_shared_users.py create mode 100644 src/feature_selection/migrations/0056_alter_clusteringparameters_algorithm_and_more.py diff --git a/src/api_service/migrations/0061_alter_experiment_shared_users.py b/src/api_service/migrations/0061_alter_experiment_shared_users.py new file mode 100644 index 00000000..b7cfa5af --- /dev/null +++ b/src/api_service/migrations/0061_alter_experiment_shared_users.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.15 on 2024-10-23 13:51 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('api_service', '0060_experiment_shared_users'), + ] + + operations = [ + migrations.AlterField( + model_name='experiment', + name='shared_users', + field=models.ManyToManyField(blank=True, related_name='shared_users_correlation_analysis', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/src/api_service/mrna_service.py b/src/api_service/mrna_service.py index 95280624..d431f53e 100644 --- a/src/api_service/mrna_service.py +++ b/src/api_service/mrna_service.py @@ -13,20 +13,28 @@ class MRNAService(object): def __init__(self): modulector_settings = settings.MODULECTOR_SETTINGS - if modulector_settings['protocol'] == 'http' and modulector_settings['port'] == 80: - self.url_modulector_prefix = f"{modulector_settings['protocol']}://{modulector_settings['host']}" - elif modulector_settings['protocol'] == 'https' and modulector_settings['port'] == 443: - self.url_modulector_prefix = f"{modulector_settings['protocol']}://{modulector_settings['host']}" - else: - self.url_modulector_prefix = f"{modulector_settings['protocol']}://{modulector_settings['host']}:{modulector_settings['port']}" + self.url_modulector_prefix = self.__build_url(modulector_settings) bioapi_settings = settings.BIOAPI_SETTINGS - if bioapi_settings['protocol'] == 'http' and bioapi_settings['port'] == 80: - self.url_bioapi_prefix = f"{bioapi_settings['protocol']}://{bioapi_settings['host']}" - elif bioapi_settings['protocol'] == 'https' and bioapi_settings['port'] == 443: - self.url_bioapi_prefix = f"{bioapi_settings['protocol']}://{bioapi_settings['host']}" + self.url_bioapi_prefix = self.__build_url(bioapi_settings) + + @staticmethod + def __build_url(settings: Dict[str, Any]) -> str: + """ + Constructs the URL based on the settings provided. + If the port is the default for the protocol (80 for http, 443 for https), it is omitted. + Otherwise, the port is included in the URL. + @param settings: Dictionary containing protocol, host, and port information. + @return: Constructed URL as a string. + """ + protocol = settings['protocol'] + host = settings['host'] + port = settings['port'] + + if (protocol == 'http' and port == 80) or (protocol == 'https' and port == 443): + return f"{protocol}://{host}" else: - self.url_bioapi_prefix = f"{bioapi_settings['protocol']}://{bioapi_settings['host']}:{bioapi_settings['port']}" + return f"{protocol}://{host}:{port}" @staticmethod def __generate_rest_query_params(get_request: QueryDict) -> str: diff --git a/src/feature_selection/migrations/0056_alter_clusteringparameters_algorithm_and_more.py b/src/feature_selection/migrations/0056_alter_clusteringparameters_algorithm_and_more.py new file mode 100644 index 00000000..53051cd6 --- /dev/null +++ b/src/feature_selection/migrations/0056_alter_clusteringparameters_algorithm_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.15 on 2024-10-23 13:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('feature_selection', '0055_alter_fsexperiment_app_name_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='clusteringparameters', + name='algorithm', + field=models.IntegerField(choices=[(1, 'K Means'), (2, 'Spectral'), (3, 'Bk Means')], default=1), + ), + migrations.AlterField( + model_name='clusteringtimesrecord', + name='algorithm', + field=models.IntegerField(choices=[(1, 'K Means'), (2, 'Spectral'), (3, 'Bk Means')]), + ), + ] diff --git a/src/multiomics_intermediate/settings.py b/src/multiomics_intermediate/settings.py index e2477aa4..e05c7ed0 100644 --- a/src/multiomics_intermediate/settings.py +++ b/src/multiomics_intermediate/settings.py @@ -300,14 +300,14 @@ # Modulector settings MODULECTOR_SETTINGS = { 'host': os.getenv('MODULECTOR_HOST', '127.0.0.1'), - 'port': os.getenv('MODULECTOR_PORT', '8001'), + 'port': os.getenv('MODULECTOR_PORT', 8001), 'protocol': os.getenv('BIOAPI_PROTOCOL', 'http') } # BioAPI settings BIOAPI_SETTINGS = { 'host': os.getenv('BIOAPI_HOST', '127.0.0.1'), - 'port': os.getenv('BIOAPI_PORT', '8002'), + 'port': os.getenv('BIOAPI_PORT', 8002), 'protocol': os.getenv('BIOAPI_PROTOCOL', 'http') } From 7fde39a0857f26b1c372f71fc9de0f9d0705592c Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 23 Oct 2024 13:38:46 -0300 Subject: [PATCH 10/27] Update settings.py Se configura para dejar como default el consumo de Modulector y Bioapi de manera online --- src/multiomics_intermediate/settings.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/multiomics_intermediate/settings.py b/src/multiomics_intermediate/settings.py index e05c7ed0..3718507c 100644 --- a/src/multiomics_intermediate/settings.py +++ b/src/multiomics_intermediate/settings.py @@ -299,16 +299,16 @@ # Modulector settings MODULECTOR_SETTINGS = { - 'host': os.getenv('MODULECTOR_HOST', '127.0.0.1'), - 'port': os.getenv('MODULECTOR_PORT', 8001), - 'protocol': os.getenv('BIOAPI_PROTOCOL', 'http') + 'host': os.getenv('MODULECTOR_HOST', 'modulector.multiomix.org'), + 'port': os.getenv('MODULECTOR_PORT', 443), + 'protocol': os.getenv('BIOAPI_PROTOCOL', 'https') } # BioAPI settings BIOAPI_SETTINGS = { - 'host': os.getenv('BIOAPI_HOST', '127.0.0.1'), - 'port': os.getenv('BIOAPI_PORT', 8002), - 'protocol': os.getenv('BIOAPI_PROTOCOL', 'http') + 'host': os.getenv('BIOAPI_HOST', 'bioapi.multiomix.org'), + 'port': os.getenv('BIOAPI_PORT', 443), + 'protocol': os.getenv('BIOAPI_PROTOCOL', 'https') } # Multiomix-aws-emr From 871898f2fb19345f556200e8505058b399d432ee Mon Sep 17 00:00:00 2001 From: Juan Nicolas Herrera Delvalle Date: Tue, 12 Nov 2024 14:01:10 -0300 Subject: [PATCH 11/27] save --- .../frontend/src/components/MainNavbar.tsx | 13 ++ .../institutions/InstitutionForm.tsx | 75 +++++++ .../institutions/InstitutionModal.tsx | 92 ++++++++ .../institutions/InstitutionsPanel.tsx | 203 +++++++++--------- src/institutions/views.py | 15 +- 5 files changed, 298 insertions(+), 100 deletions(-) create mode 100644 src/frontend/static/frontend/src/components/institutions/InstitutionForm.tsx create mode 100644 src/frontend/static/frontend/src/components/institutions/InstitutionModal.tsx diff --git a/src/frontend/static/frontend/src/components/MainNavbar.tsx b/src/frontend/static/frontend/src/components/MainNavbar.tsx index 1d97cc26..aab4466c 100644 --- a/src/frontend/static/frontend/src/components/MainNavbar.tsx +++ b/src/frontend/static/frontend/src/components/MainNavbar.tsx @@ -99,6 +99,9 @@ const MainNavbar = (props: MainNavbarProps) => { + + + } {/* Analysis menu */} @@ -188,6 +191,16 @@ const MainNavbar = (props: MainNavbarProps) => { } + {/* Institutions */} + {currentUser && (currentUser.is_superuser || currentUser.is_institution_admin) && + + + Institutions + + + + } + {/* About us */} diff --git a/src/frontend/static/frontend/src/components/institutions/InstitutionForm.tsx b/src/frontend/static/frontend/src/components/institutions/InstitutionForm.tsx new file mode 100644 index 00000000..a695678f --- /dev/null +++ b/src/frontend/static/frontend/src/components/institutions/InstitutionForm.tsx @@ -0,0 +1,75 @@ +import React, { useState } from 'react' +import { Form, Button } from 'semantic-ui-react' + +const defaultForm = { + name: '', + location: '', + email: '', + phone: '', + isLoading: false +} + +const InstitutionForm = () => { + const [formData, setFormData] = useState(defaultForm) + + const handleChange = (e, { name, value }) => { + setFormData({ ...formData, [name]: value }) + } + + const handleSubmit = () => { + console.log('Form Data Submitted:', formData) + } + + return ( +
+ + + + + + + + ) +} + +export default InstitutionForm diff --git a/src/frontend/static/frontend/src/components/institutions/InstitutionModal.tsx b/src/frontend/static/frontend/src/components/institutions/InstitutionModal.tsx new file mode 100644 index 00000000..e754a9e0 --- /dev/null +++ b/src/frontend/static/frontend/src/components/institutions/InstitutionModal.tsx @@ -0,0 +1,92 @@ +import React from 'react' +import { + ModalHeader, + ModalContent, + Modal, + Icon, + Segment, + Table +} from 'semantic-ui-react' +import { DjangoInstitution, DjangoUserCandidates } from '../../utils/django_interfaces' +import { Nullable } from '../../utils/interfaces' +import { PaginatedTable } from '../common/PaginatedTable' +import { TableCellWithTitle } from '../common/TableCellWithTitle' + +declare const urlGetUsersCandidates: string + +export enum InstitutionModalActions { + READ, + EDIT, +} + +export interface InstitutionModalState { + isOpen: boolean, + action: InstitutionModalActions + institution: Nullable +} + +interface Props extends InstitutionModalState { + /* Close modal function */ + handleCloseModal: () => void; +} + +/** + * Institution modal + * @param {Props} props Props for component + * @returns Component + */ +export const InstitutionModal = (props: Props) => { + return ( + } + > + {props.institution?.name} + + + + headerTitle={props.institution?.name + ' candidates'} + headers={[ + { name: 'User name', serverCodeToSort: 'username', width: 3 }, + { name: 'Email', serverCodeToSort: 'email', width: 1 } + ]} + defaultSortProp={{ sortField: 'upload_date', sortOrderAscendant: false }} + showSearchInput + searchLabel='User name' + searchPlaceholder='Search by User name' + urlToRetrieveData={urlGetUsersCandidates} + updateWSKey='user_for_institution' + mapFunction={(userCandidate: DjangoUserCandidates) => { + return ( + + + + + {/* Details button */} + { }} + /> + + {/* Edit button */} + { }} + /> + + + ) + }} + /> + + + + ) +} diff --git a/src/frontend/static/frontend/src/components/institutions/InstitutionsPanel.tsx b/src/frontend/static/frontend/src/components/institutions/InstitutionsPanel.tsx index af67dc4c..28182ffd 100644 --- a/src/frontend/static/frontend/src/components/institutions/InstitutionsPanel.tsx +++ b/src/frontend/static/frontend/src/components/institutions/InstitutionsPanel.tsx @@ -1,18 +1,21 @@ import React from 'react' import { Base } from '../Base' -import { Grid, Icon, Segment, Header, Form, DropdownItemProps } from 'semantic-ui-react' +import { Grid, Icon, Segment, Header, Table } from 'semantic-ui-react' import { DjangoInstitution, DjangoUserCandidates, DjangoCommonResponse, DjangoAddRemoveUserToInstitutionInternalCode, DjangoResponseCode, DjangoUser } from '../../utils/django_interfaces' import ky from 'ky' import { alertGeneralError, getDjangoHeader } from '../../utils/util_functions' -import { InstitutionsList } from './InstitutionsList' -import { InstitutionUsersInfo } from './InstitutionUsersInfo' +/* import { InstitutionsList } from './InstitutionsList' +import { InstitutionUsersInfo } from './InstitutionUsersInfo' */ import { RemoveUserFromInstitutionModal } from './RemoveUserFromInstitutionModal' import { Nullable } from '../../utils/interfaces' import { InfoPopup } from '../pipeline/experiment-result/gene-gem-details/InfoPopup' +import { PaginatedTable } from '../common/PaginatedTable' +import { TableCellWithTitle } from '../common/TableCellWithTitle' +import InstitutionForm from './InstitutionForm' +import { InstitutionModal, InstitutionModalActions, InstitutionModalState } from './InstitutionModal' // URLs defined in files.html declare const urlUserInstitutionsAsAdmin: string -declare const urlGetUsersCandidates: string declare const urlAddRemoveUserToInstitution: string /** @@ -29,6 +32,7 @@ interface InstitutionsPanelState { removingUserFromInstitution: boolean, selectedUserToRemove: Nullable, showRemoveUserFromInstitutionModal: boolean, + modalState: InstitutionModalState, } /** @@ -52,7 +56,8 @@ export class InstitutionsPanel extends React.Component<{}, InstitutionsPanelStat addingRemovingUserToInstitution: false, removingUserFromInstitution: false, selectedUserToRemove: null, - showRemoveUserFromInstitutionModal: false + showRemoveUserFromInstitutionModal: false, + modalState: this.defaultModalState() } } @@ -66,7 +71,7 @@ export class InstitutionsPanel extends React.Component<{}, InstitutionsPanelStat * When the component has been mounted, It requests for * tags and files */ - componentDidMount () { this.getUserInstitutions() } + componentDidMount () { /* this.getUserInstitutions() */ } /** * Abort controller if component unmount @@ -76,6 +81,39 @@ export class InstitutionsPanel extends React.Component<{}, InstitutionsPanelStat this.abortController.abort() } + /** + * Default modal attributes + * @returns {InstitutionModalState} Default modal + */ + defaultModalState (): InstitutionModalState { + return { + isOpen: false, + action: InstitutionModalActions.READ, + institution: null + } + } + + /** + * Close modal + */ + handleCloseModal () { + this.setState({ modalState: this.defaultModalState() }) + } + + /** + * Open modal + * @param {InstitutionModalActions} action action type for modal. + * @param {DjangoInstitution} institution institution for modal. + */ + handleOpenModal (action: InstitutionModalActions, institution: DjangoInstitution) { + const modalState = { + isOpen: true, + action, + institution + } + this.setState({ modalState }) + } + /** * Fetches the Institutions which the current user belongs to */ @@ -101,42 +139,6 @@ export class InstitutionsPanel extends React.Component<{}, InstitutionsPanelStat }) } - /** - * Fetches the User's uploaded files - */ - searchUsers = () => { - this.setState({ isFetchingUsersCandidates: true }, () => { - const searchParams = { - querySearch: this.state.searchUserText - } - ky.get(urlGetUsersCandidates, { searchParams, signal: this.abortController.signal }).then((response) => { - this.setState({ isFetchingUsersCandidates: false }) - response.json().then((userCandidates: DjangoUserCandidates[]) => { - this.setState({ userCandidates }) - }).catch((err) => { - console.log('Error parsing JSON ->', err) - }) - }).catch((err) => { - if (!this.abortController.signal.aborted) { - this.setState({ isFetchingUsersCandidates: false }) - } - - console.log("Error getting user's datasets ->", err) - }) - }) - } - - /** - * Handles search user input changes - * @param value Value to assign to the specified field - */ - handleInputChange = (value: string) => { - this.setState({ searchUserText: value }, () => { - clearTimeout(this.filterTimeout) - this.filterTimeout = window.setTimeout(this.searchUsers, 300) - }) - } - /** * Set a selected Institution in state to show it users * @param selectedInstitution Selected Institution to show users @@ -244,16 +246,16 @@ export class InstitutionsPanel extends React.Component<{}, InstitutionsPanelStat formIsDisabled = (): boolean => !this.state.selectedInstitution || this.state.addingRemovingUserToInstitution render () { - const userOptions: DropdownItemProps[] = this.state.userCandidates.map((userCandidate) => { - return { - key: userCandidate.id, - text: `${userCandidate.username} (${userCandidate.email})`, - value: userCandidate.id - } - }) - - const formIsDisabled = this.formIsDisabled() - + /* const userOptions: DropdownItemProps[] = this.state.userCandidates.map((userCandidate) => { + return { + key: userCandidate.id, + text: `${userCandidate.username} (${userCandidate.email})`, + value: userCandidate.id + } + }) */ + + /* const formIsDisabled = this.formIsDisabled() + */ return ( {/* Modal to confirm User removal from an Institution */} @@ -277,65 +279,68 @@ export class InstitutionsPanel extends React.Component<{}, InstitutionsPanelStat
- - + {/* Todo: remove component + + /> */} {/* Files overview panel */} - - -
- - Manage access -
- -
- - {/* User Search */} - this.selectUser(value)} - onSearchChange={(_e, { searchQuery }) => this.handleInputChange(searchQuery)} - disabled={formIsDisabled} - loading={this.state.isFetchingUsersCandidates || this.state.addingRemovingUserToInstitution} - /> - - - Add user - - -
- -
- - {/* Institution's Users info */} - - -
+ + headerTitle='Institutions' + headers={[ + { name: 'Name', serverCodeToSort: 'name', width: 3 }, + { name: 'Location', serverCodeToSort: 'location', width: 1 }, + { name: 'Email', width: 1 }, + { name: 'Phone number', width: 1 }, + { name: 'Actions', width: 1 } + ]} + defaultSortProp={{ sortField: 'upload_date', sortOrderAscendant: false }} + showSearchInput + searchLabel='Name' + searchPlaceholder='Search by name' + urlToRetrieveData={urlUserInstitutionsAsAdmin} + updateWSKey='institutionsList' + mapFunction={(institution: DjangoInstitution) => { + return ( + + + + + + + {/* Details button */} + this.handleOpenModal(InstitutionModalActions.READ, institution)} + /> + + {/* Edit button */} + this.handleOpenModal(InstitutionModalActions.EDIT, institution)} + /> + + + ) + }} + />
+ this.handleCloseModal()} action={this.state.modalState.action} isOpen={this.state.modalState.isOpen} institution={this.state.modalState.institution} /> ) } diff --git a/src/institutions/views.py b/src/institutions/views.py index 70789e0c..32fdc640 100644 --- a/src/institutions/views.py +++ b/src/institutions/views.py @@ -1,6 +1,6 @@ from django.contrib.auth.decorators import login_required from django.shortcuts import render -from rest_framework import generics, permissions +from rest_framework import generics, permissions, filters import json from common.enums import ResponseCode from common.functions import encode_json_response_status @@ -9,6 +9,8 @@ from .models import Institution from .serializers import InstitutionSerializer, UserCandidateSerializer from django.contrib.auth.models import User +from common.pagination import StandardResultsSetPagination +from django_filters.rest_framework import DjangoFilterBackend class InstitutionAsAdminList(generics.ListAPIView): @@ -22,6 +24,11 @@ def get_queryset(self): serializer_class = InstitutionSerializer permission_classes = [permissions.IsAuthenticated] + pagination_class = StandardResultsSetPagination + filter_backends = [filters.OrderingFilter, filters.SearchFilter, DjangoFilterBackend] + filterset_fields = [] + search_fields = ['name'] + ordering_fields = ['name', 'location'] class InstitutionList(generics.ListAPIView): @@ -50,6 +57,12 @@ def get_queryset(self): serializer_class = UserCandidateSerializer permission_classes = [permissions.IsAuthenticated] + + pagination_class = StandardResultsSetPagination + filter_backends = [filters.OrderingFilter, filters.SearchFilter, DjangoFilterBackend] + filterset_fields = [] + search_fields = ['name'] + ordering_fields = ['name'] @login_required From 438e90d052c56d4ae0829660209fb58dc027554a Mon Sep 17 00:00:00 2001 From: Hernan Date: Thu, 28 Nov 2024 17:31:33 -0300 Subject: [PATCH 12/27] add Ward hierarchical clustering --- src/feature_selection/fs_algorithms_spark.py | 2 ++ src/feature_selection/fs_models.py | 6 ++++-- src/feature_selection/models.py | 1 + src/feature_selection/views.py | 2 ++ .../biomarkers/labels/ClusteringAlgorithmLabel.tsx | 4 ++++ .../frontend/src/components/biomarkers/types.ts | 3 ++- .../frontend/src/components/biomarkers/utils.ts | 3 ++- src/statistical_properties/stats_service.py | 13 ++++++++++--- src/statistical_properties/survival_functions.py | 6 +++++- 9 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/feature_selection/fs_algorithms_spark.py b/src/feature_selection/fs_algorithms_spark.py index fcec625d..6cff6ab2 100644 --- a/src/feature_selection/fs_algorithms_spark.py +++ b/src/feature_selection/fs_algorithms_spark.py @@ -34,6 +34,8 @@ def __get_clustering_algorithm_value(cluster_algorithm: ClusteringAlgorithm) -> return 'spectral' if cluster_algorithm == ClusteringAlgorithm.BK_MEANS: return 'bk_means' + if cluster_algorithm == ClusteringAlgorithm.WARD: + return 'ward' return 'k_means' # Default is kmeans diff --git a/src/feature_selection/fs_models.py b/src/feature_selection/fs_models.py index a8de8e1d..6ef092a5 100644 --- a/src/feature_selection/fs_models.py +++ b/src/feature_selection/fs_models.py @@ -1,6 +1,6 @@ from typing import Literal, Union, Optional from django.conf import settings -from sklearn.cluster import KMeans, SpectralClustering, BisectingKMeans +from sklearn.cluster import KMeans, SpectralClustering, BisectingKMeans, AgglomerativeClustering from sksurv.ensemble import RandomSurvivalForest from sksurv.svm import FastKernelSurvivalSVM from .models import ClusteringAlgorithm @@ -12,7 +12,7 @@ SVMOptimizerOptions = Literal["avltree", "rbtree"] # Available models for clustering -ClusteringModels = Union[KMeans, SpectralClustering, BisectingKMeans] +ClusteringModels = Union[KMeans, SpectralClustering, BisectingKMeans, AgglomerativeClustering] def get_clustering_model(clustering_algorithm: ClusteringAlgorithm, @@ -28,6 +28,8 @@ def get_clustering_model(clustering_algorithm: ClusteringAlgorithm, return KMeans(n_clusters=number_of_clusters, random_state=random_state, n_init='auto') elif clustering_algorithm == ClusteringAlgorithm.SPECTRAL: return SpectralClustering(n_clusters=number_of_clusters, random_state=random_state) + elif clustering_algorithm == ClusteringAlgorithm.WARD: + return AgglomerativeClustering(n_clusters=number_of_clusters, linkage='ward') elif clustering_algorithm == ClusteringAlgorithm.BK_MEANS: return BisectingKMeans(n_clusters=number_of_clusters, random_state=random_state) diff --git a/src/feature_selection/models.py b/src/feature_selection/models.py index 0ad5b360..3435913c 100644 --- a/src/feature_selection/models.py +++ b/src/feature_selection/models.py @@ -34,6 +34,7 @@ class ClusteringAlgorithm(models.IntegerChoices): K_MEANS = 1 SPECTRAL = 2 # TODO: implement in backend BK_MEANS = 3 + WARD = 4 class ClusteringMetric(models.IntegerChoices): diff --git a/src/feature_selection/views.py b/src/feature_selection/views.py index 6e93a058..aaa9e9bf 100644 --- a/src/feature_selection/views.py +++ b/src/feature_selection/views.py @@ -214,6 +214,8 @@ def __get_clustering_parameters_columns(row: pd.Series) -> Tuple[int, Clustering algorithm = ClusteringAlgorithm.K_MEANS elif algorithm_description == 'spectral': algorithm = ClusteringAlgorithm.SPECTRAL + elif algorithm_description == 'ward': + algorithm = ClusteringAlgorithm.WARD else: algorithm = ClusteringAlgorithm.BK_MEANS scoring = ClusteringScoringMethod.C_INDEX if scoring_method == 'concordance-index' \ diff --git a/src/frontend/static/frontend/src/components/biomarkers/labels/ClusteringAlgorithmLabel.tsx b/src/frontend/static/frontend/src/components/biomarkers/labels/ClusteringAlgorithmLabel.tsx index 5535b83a..1e7d3d50 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/labels/ClusteringAlgorithmLabel.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/labels/ClusteringAlgorithmLabel.tsx @@ -32,6 +32,10 @@ export const ClusteringAlgorithmLabel = (props: ClusteringAlgorithmLabelProps) = color = 'blue' description = 'Bisecting KMeans' break + case ClusteringAlgorithm.WARD: + color = 'blue' + description = 'Ward' + break default: color = 'blue' description = '' diff --git a/src/frontend/static/frontend/src/components/biomarkers/types.ts b/src/frontend/static/frontend/src/components/biomarkers/types.ts index 20b78bb5..9411a955 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/types.ts +++ b/src/frontend/static/frontend/src/components/biomarkers/types.ts @@ -209,7 +209,8 @@ enum FitnessFunction { enum ClusteringAlgorithm { K_MEANS = 1, SPECTRAL = 2, - BK_MEANS = 3 + BK_MEANS = 3, + WARD = 4 } /** Clustering metric to optimize. */ diff --git a/src/frontend/static/frontend/src/components/biomarkers/utils.ts b/src/frontend/static/frontend/src/components/biomarkers/utils.ts index a51237af..3e525cfd 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/utils.ts +++ b/src/frontend/static/frontend/src/components/biomarkers/utils.ts @@ -37,7 +37,8 @@ const SVMKernelOptions: DropdownItemProps[] = [ const clusteringAlgorithmOptions: DropdownItemProps[] = [ { key: ClusteringAlgorithm.K_MEANS, text: 'K-Means', value: ClusteringAlgorithm.K_MEANS }, { key: ClusteringAlgorithm.SPECTRAL, text: 'Spectral', value: ClusteringAlgorithm.SPECTRAL }, - { key: ClusteringAlgorithm.BK_MEANS, text: 'BK-Means', value: ClusteringAlgorithm.BK_MEANS } + { key: ClusteringAlgorithm.BK_MEANS, text: 'BK-Means', value: ClusteringAlgorithm.BK_MEANS }, + { key: ClusteringAlgorithm.WARD, text: 'Ward', value: ClusteringAlgorithm.WARD } ] /** Available options for a Clustering metric to optimize. */ diff --git a/src/statistical_properties/stats_service.py b/src/statistical_properties/stats_service.py index d27bc9d1..c82d8f71 100644 --- a/src/statistical_properties/stats_service.py +++ b/src/statistical_properties/stats_service.py @@ -3,8 +3,9 @@ import numpy as np import pandas as pd from lifelines import CoxPHFitter -from sklearn.metrics import mean_squared_error, r2_score +from sklearn.metrics import mean_squared_error, r2_score, silhouette_score from sklearn.model_selection import GridSearchCV, StratifiedKFold +from sklearn.cluster import AgglomerativeClustering from sksurv.metrics import concordance_index_censored from common.datasets_utils import get_common_samples, generate_molecules_file, format_data, \ generate_clinical_file, generate_molecules_dataframe, check_sample_classes, \ @@ -113,13 +114,19 @@ def __compute_stat_validation(stat_validation: StatisticalValidation, molecules_ # Makes predictions if is_regression: check_if_stopped(is_aborted, ExperimentStopped) - predictions = classifier.predict(molecules_df) + if isinstance(classifier, AgglomerativeClustering): + predictions = classifier.fit_predict(molecules_df) + else: + predictions = classifier.predict(molecules_df) # Gets all the metrics for the SVM or RF check_if_stopped(is_aborted, ExperimentStopped) y_true = clinical_data['time'] stat_validation.mean_squared_error = mean_squared_error(y_true, predictions) - stat_validation.c_index = classifier.score(molecules_df, clinical_data) + if isinstance(classifier, AgglomerativeClustering): + stat_validation.c_index = silhouette_score(molecules_df, predictions) + else: + stat_validation.c_index = classifier.score(molecules_df, clinical_data) stat_validation.r2_score = r2_score(y_true, predictions) # TODO: add here all the metrics for every Source type diff --git a/src/statistical_properties/survival_functions.py b/src/statistical_properties/survival_functions.py index bde6f60d..66e37760 100644 --- a/src/statistical_properties/survival_functions.py +++ b/src/statistical_properties/survival_functions.py @@ -5,6 +5,7 @@ from lifelines.statistics import logrank_test from common.utils import get_subset_of_features from feature_selection.fs_models import ClusteringModels +from sklearn.cluster import AgglomerativeClustering KaplanMeierSample = Tuple[ int, @@ -138,7 +139,10 @@ def generate_survival_groups_by_clustering( molecules_df = get_subset_of_features(molecules_df, molecules_df.index) # Gets the groups - clustering_result = classifier.predict(molecules_df.values) + if isinstance(classifier, AgglomerativeClustering): + clustering_result = classifier.fit_predict(molecules_df.values) + else: + clustering_result = classifier.predict(molecules_df.values) # Retrieves the data for every group and stores the survival function data: List[Dict[str, LabelOrKaplanMeierResult]] = [] From e6aca83326dc95aca3363b48249dd62ca989878a Mon Sep 17 00:00:00 2001 From: Gonza Date: Wed, 4 Dec 2024 17:18:53 -0300 Subject: [PATCH 13/27] -fixed font icons and biomarkers can be shared --- .../migrations/0019_biomarker_is_public.py | 18 ++++ .../0020_biomarker_shared_institutions.py | 19 ++++ src/biomarkers/models.py | 5 +- src/biomarkers/views.py | 9 +- .../img/genes-icons/ActiveDriverDb.webp | Bin 0 -> 602 bytes .../img/genes-icons/Alliance-Genome.webp | Bin 0 -> 1252 bytes .../frontend/img/genes-icons/AlphaFold.webp | Bin 0 -> 666 bytes .../frontend/img/genes-icons/AmiGO.webp | Bin 0 -> 1294 bytes .../frontend/img/genes-icons/BioGrid.webp | Bin 0 -> 720 bytes .../frontend/img/genes-icons/BioPlex.webp | Bin 0 -> 618 bytes .../frontend/img/genes-icons/ClinVar.webp | Bin 0 -> 516 bytes .../img/genes-icons/ComplexPortal.webp | Bin 0 -> 616 bytes .../frontend/img/genes-icons/DGIDB.webp | Bin 0 -> 256 bytes .../frontend/img/genes-icons/Ensembl.webp | Bin 0 -> 758 bytes .../img/genes-icons/ExpressionAtlas.webp | Bin 0 -> 532 bytes .../static/frontend/img/genes-icons/GDC.webp | Bin 0 -> 414 bytes .../static/frontend/img/genes-icons/GTEX.webp | Bin 0 -> 356 bytes .../img/genes-icons/GWAS-Catalog.webp | Bin 0 -> 1222 bytes .../frontend/img/genes-icons/GeneCards.webp | Bin 0 -> 552 bytes .../frontend/img/genes-icons/GeneMANIA.webp | Bin 0 -> 574 bytes .../frontend/img/genes-icons/Google.webp | Bin 0 -> 470 bytes .../static/frontend/img/genes-icons/HGNC.webp | Bin 0 -> 478 bytes .../frontend/img/genes-icons/HI-CBrowser.webp | Bin 0 -> 1958 bytes .../static/frontend/img/genes-icons/HPO.webp | Bin 0 -> 638 bytes .../img/genes-icons/Human-Protein-Atlas.webp | Bin 0 -> 608 bytes .../static/frontend/img/genes-icons/ICGC.webp | Bin 0 -> 1274 bytes .../frontend/img/genes-icons/IntAct.webp | Bin 0 -> 474 bytes .../frontend/img/genes-icons/InterPro.webp | Bin 0 -> 940 bytes .../img/genes-icons/Interactome-Atlas.webp | Bin 0 -> 774 bytes .../frontend/img/genes-icons/MalaCards.webp | Bin 0 -> 1834 bytes .../frontend/img/genes-icons/Monarch.webp | Bin 0 -> 394 bytes .../static/frontend/img/genes-icons/NCBI.webp | Bin 0 -> 404 bytes .../static/frontend/img/genes-icons/OMIM.webp | Bin 0 -> 450 bytes .../img/genes-icons/Open-Targets.webp | Bin 0 -> 522 bytes .../static/frontend/img/genes-icons/PDB.webp | Bin 0 -> 532 bytes .../frontend/img/genes-icons/PathCards.webp | Bin 0 -> 638 bytes .../img/genes-icons/PathwayCommons.webp | Bin 0 -> 1130 bytes .../frontend/img/genes-icons/Pharmgkb.webp | Bin 0 -> 994 bytes .../frontend/img/genes-icons/PubMed.webp | Bin 0 -> 744 bytes .../frontend/img/genes-icons/Reactome.webp | Bin 0 -> 520 bytes .../frontend/img/genes-icons/STRING.webp | Bin 0 -> 444 bytes .../frontend/img/genes-icons/UCSCBrowser.webp | Bin 0 -> 492 bytes .../frontend/img/genes-icons/Uniprot.webp | Bin 0 -> 1646 bytes .../frontend/img/genes-icons/cBioPortal.webp | Bin 0 -> 646 bytes .../static/frontend/img/genes-icons/dgv.webp | Bin 0 -> 602 bytes .../frontend/img/genes-icons/harvard.webp | Bin 0 -> 3082 bytes .../molecules/genes/GeneInformation.tsx | 82 +++++++++--------- 47 files changed, 88 insertions(+), 45 deletions(-) create mode 100644 src/biomarkers/migrations/0019_biomarker_is_public.py create mode 100644 src/biomarkers/migrations/0020_biomarker_shared_institutions.py create mode 100644 src/frontend/static/frontend/img/genes-icons/ActiveDriverDb.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/Alliance-Genome.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/AlphaFold.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/AmiGO.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/BioGrid.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/BioPlex.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/ClinVar.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/ComplexPortal.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/DGIDB.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/Ensembl.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/ExpressionAtlas.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/GDC.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/GTEX.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/GWAS-Catalog.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/GeneCards.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/GeneMANIA.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/Google.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/HGNC.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/HI-CBrowser.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/HPO.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/Human-Protein-Atlas.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/ICGC.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/IntAct.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/InterPro.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/Interactome-Atlas.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/MalaCards.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/Monarch.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/NCBI.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/OMIM.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/Open-Targets.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/PDB.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/PathCards.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/PathwayCommons.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/Pharmgkb.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/PubMed.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/Reactome.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/STRING.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/UCSCBrowser.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/Uniprot.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/cBioPortal.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/dgv.webp create mode 100644 src/frontend/static/frontend/img/genes-icons/harvard.webp diff --git a/src/biomarkers/migrations/0019_biomarker_is_public.py b/src/biomarkers/migrations/0019_biomarker_is_public.py new file mode 100644 index 00000000..3b43a55e --- /dev/null +++ b/src/biomarkers/migrations/0019_biomarker_is_public.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.15 on 2024-11-21 18:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('biomarkers', '0018_alter_biomarker_state'), + ] + + operations = [ + migrations.AddField( + model_name='biomarker', + name='is_public', + field=models.BooleanField(default=False), + ), + ] diff --git a/src/biomarkers/migrations/0020_biomarker_shared_institutions.py b/src/biomarkers/migrations/0020_biomarker_shared_institutions.py new file mode 100644 index 00000000..856bc683 --- /dev/null +++ b/src/biomarkers/migrations/0020_biomarker_shared_institutions.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.15 on 2024-11-21 18:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('institutions', '0004_auto_20220923_2322'), + ('biomarkers', '0019_biomarker_is_public'), + ] + + operations = [ + migrations.AddField( + model_name='biomarker', + name='shared_institutions', + field=models.ManyToManyField(blank=True, related_name='biomarkers', to='institutions.institution'), + ), + ] diff --git a/src/biomarkers/models.py b/src/biomarkers/models.py index 14857468..6528da5f 100644 --- a/src/biomarkers/models.py +++ b/src/biomarkers/models.py @@ -3,6 +3,8 @@ from django.db import models from django.db.models import QuerySet from queryset_sequence import QuerySetSequence + +from institutions.models import Institution from tags.models import Tag from api_service.websocket_functions import send_update_biomarkers_command from user_files.models_choices import MoleculeType @@ -60,7 +62,7 @@ class Biomarker(models.Model): cnas: QuerySet['CNAIdentifier'] mirnas: QuerySet['MiRNAIdentifier'] mrnas: QuerySet['MRNAIdentifier'] - + is_public = models.BooleanField(blank=False, null=False, default=False) name: str = models.CharField(max_length=300) description: Optional[str] = models.TextField(null=True, blank=True) tag: Optional[Tag] = models.ForeignKey(Tag, on_delete=models.SET_NULL, default=None, blank=True, null=True) @@ -68,6 +70,7 @@ class Biomarker(models.Model): origin: int = models.IntegerField(choices=BiomarkerOrigin.choices) state: int = models.IntegerField(choices=BiomarkerState.choices) user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) + shared_institutions = models.ManyToManyField(Institution, related_name='biomarkers', blank=True) def __str__(self) -> str: return self.name diff --git a/src/biomarkers/views.py b/src/biomarkers/views.py index 5e09b62d..f31a09b0 100644 --- a/src/biomarkers/views.py +++ b/src/biomarkers/views.py @@ -16,15 +16,18 @@ BiomarkerSimpleSerializer, BiomarkerSimpleUpdateSerializer from common.pagination import StandardResultsSetPagination from common.response import generate_json_response_or_404 -from django.db.models import QuerySet +from django.db.models import QuerySet, Q class BiomarkerList(generics.ListAPIView): """REST endpoint: list for Biomarker model""" def get_queryset(self): + user = self.request.user only_successful = self.request.GET.get('onlySuccessful') == 'true' - biomarkers = Biomarker.objects.filter(user=self.request.user) + biomarkers = Biomarker.objects.filter( + Q(is_public=True) | Q(user=user) | Q(shared_institutions__institutionadministration__user=user)).distinct() + if only_successful: # FIXME: this is VERY slow. Taking more than 20secs in production. Must parametrize the DB, maybe # FIXME: autovacuum settings could help @@ -112,7 +115,6 @@ def get(self, request: Request, pk: int): self.__copy_molecules_instances(biomarker_copy, biomarker.cnas.all()) self.__copy_molecules_instances(biomarker_copy, biomarker.methylations.all()) - return Response({'ok': True}) @@ -142,6 +144,7 @@ def get_gene_aliases(genes_ids: List[str]) -> Optional[Dict]: method='post' ) + def find_genes_from_request(request: Request) -> List[Dict]: """ Generates the structure for the frontend for a list of genes. The needed structure is a list of dicts with diff --git a/src/frontend/static/frontend/img/genes-icons/ActiveDriverDb.webp b/src/frontend/static/frontend/img/genes-icons/ActiveDriverDb.webp new file mode 100644 index 0000000000000000000000000000000000000000..8f77a1a4a550db687bc8f81b5f5f60ea6f91bce7 GIT binary patch literal 602 zcmV-g0;T;@Nk&Fe0ssJ4MM6+kP&iCR0ssInAHW9?bwG?HNob?|gYK2>|3e|1zy}eM z001l_$+m6Vwr$(^*|u4%ZQHhOoo%~&Vc@orqU`Q540PrZU5wDM06T^S%<1H25t{yB zkkmHgqY1?lPLP>rM%#4+QY8a}P}z(K;XmXYB~M92Tns2IFPtV|C41W~%%E}c6j1Qp zCwkr8NedYDy$yOkT@*G5ULJ>MF(3v$`NFmL5n2!YAh>|oWj=?i*Fj8C?}U#sPVZeI z{zF*+t->17m^TP}Hex6QDEs@uK)XO-G&bl4jGYF}^MfvbLtDX+*Gh23n6$~NL*uWm zM(KaKZ@h_H)1ZLU&xP)W+$fm^kn3^?B@~8kOd!Uq6=VZ$s87MKz$inz3BBTpDWtgB z5qzJZF3fa6s5LwA%1^KC7{pyzV)_eHS|K2N?gM5{8*wbS&yi4}#Iu>1@i=(};^zro$cRFaZ(PItD6fgbxrjp1 zmMh4O@<11lj9vyisjC_Oo=3W35}#fN_pc&y-H>^XbSaw9F3TCaFC1!%E*r6{UDwoQ z^)7I~0`z13)%8JLh=zGce4mVj-g6N=s;evo4A0&NX|MdCP}ik$%pDy^M!p;0Gr+}j oSl3`uC|4swKpA|F(&v-D;Ef685>Dl&eVEZ+^o7s*3d^dmG_lViVgLXD literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/img/genes-icons/Alliance-Genome.webp b/src/frontend/static/frontend/img/genes-icons/Alliance-Genome.webp new file mode 100644 index 0000000000000000000000000000000000000000..f8bef6f5fee5ff9eb3e79b38af36491db03a62e1 GIT binary patch literal 1252 zcmVI`sAM;R_&;wDG)u$0s@c4*}-^Yq{fp4G$ax7l1*owry+M ze3F?hQ_9TD%(QXWo>h;aTPKgjk(tSChsFiKI2zPe^vB`v>odSOq>EpZM~|a&p~# zZkM#YZ!piwn3W^(@&@0|%gQz!dm z*K1|4CNrcFf?@zhl{)TLbKGOdmCA0$R3c^L+B0YxS9cDvwzgKax_Y(z;?VE(Zdkk5?>9Qi|Q9k~$s4-lB)o;m;ku^*dp!$MTrk26t%8 z&-(Oa1~ub%S-a}i&!CeO2LUl-Y^A!#Gv|J-ejCTEe_s2fk?ST@;Xti1MiuM2@76|0 zEwYICJ8krB&3~lyxP;@T20i$3dGDVpB816qsQ+#%nPcmomnZFxvUFB}c2{@GD*?!u z#&+#43+pvEh1=X~1RnPX9#?S~pS?fnj@#<}c=e)pX7xie1y%WdxjGAlykzD@t!=E% z;U$Haa0xDU`8DiK{c75G94)PNpgH?!S@q&}I3_++b7-UMOstjaiL97}R4TsSt{q7@ z=Kl|y?X&kUvl&`#n7Dw@&Tgjj_a&E0$Ja!?2*Y-AC|lx5xxYSg|B>&`U0;%3P7}MY zyYg3bPeKY}%c<5?{nMN{Es;v)ZUjD+O+$)!2c%pOK*{5EPMercr7e^?YL$y{Ir)e|5T#sbJ2NL*w zB%iv5B6BV&)RH}lz(7%#($b2exj{<#Pls;9pHY_XUaq| zW=Yc1K>1$4|E!CFXThf2htst(M9l~2T@08kmg_#Hh|rDQq`dXt0Ud23xhHEi3pOi^ z5B)?dn;4jx_=mD^p@v!onQSu`$T&?I*H8DSO!3ocO<}Kqkwui)20H$>PL8Q^NN zcR;1I|8%gjfT17im(*wAM`Af?;{T4Aa`{R{okaUL(;nC<(%Hi6kF-ajp-9kuSC5iC z;i8X-GMf;r$GD+}4Z(R#a2@vac_dRrz zBH}ec`~{1B<;88zcH_1lKd>k|uUG?CP&gps0002+1^}G_Dj)zL06vjEoJl34BOxdX zsIU?VYyh1ygm5>2eoFF)ZWbUYz&8MHfPcm60O&LSJNsSmS>)CPk+*;V{_iPA-Ws(; z#RnJnC!y;qd+;m%zYFIEGseG-iQDI0LW5B A00000 literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/img/genes-icons/AmiGO.webp b/src/frontend/static/frontend/img/genes-icons/AmiGO.webp new file mode 100644 index 0000000000000000000000000000000000000000..7a0df318ba368d5acb0e72ba49cad9635bafda5e GIT binary patch literal 1294 zcmV+p1@Zb)Nk&En1pok7MM6+kP&il$0000G0000V0018V06|PpNUZ_@00EF?+qP*t zI2}@blUZ_lT8~i;rpFlsCDUeD`k?ZH90FFwo`%3_CfpUz>nJAO+eHGE=24m{A6r-jx z>+dWDmZv03jy&*WtwXpGNWfL0R{Pb#nr%a(S9TpV%9%=eReP~^Q~`-Z>@pWdadU0w1f4U z`srDRC+0iMW~=R#J1xGo9`QO5VD(Rf*<{xLTD|k1Nz@qYDg@21J@~#DJ^!|ggn5O! z>bm0PBp$D>HQs9*J-0@yN#e$`t!I!AIb_xK16o7__y5=GS&wesAORdV%Ds)p#j6d`7jlKABzx z4vX1lIpV^ccYf@8;0{3rVuly6#cZ1e;mdD7JJtu>?(TyQnL{C=!g3Q}n8mUjl>@Rm zgVV9czWC<}`60jovu&yh?}w+Hf#j4;aLR6Y(xc^it*$Co#k$mHNGIk^A)!(*>S{#I zuT>~d$eW7%+MFqL%5_*Rc3~UJlX5nq=(H|xlbidzi`VvaLD7Fh-bQC7n40=%5Nf@K zJxrT{`sSyz9xP}V;tDPy?HAn3I@^i{cR%nx_4VDeXYagDeSfeA|23ff_|-dhteMb0 z5VZhSP&gnm0ssJz4FH`1Dj)zL06rxSg+d{Q|5X460Ff<#6Q)p(pz)8xKgJGl&H*qB zlRY3m8Tt@z0O$edGyMb71K0!51N8&aSM^KaFZLVui{Kyj7ytW66`-I4+RkZxTfG1X z2ndt_V0-mROwcwd%0g+bEz=2c9HCDD0RI08rusI%AKlpBI*NDnzo(uj zdvsKX1T}u3DZj3of6JP9bFNB0O3_LMbAWv>>#&fpQr-?}%$9z+9iPzf z+IRKln_mI0u1RJgFCUyslB}&f1H2|Xy`R?_0x$hiNk&G<0ssJ4MM6+kP&gpG0ssKe4*;D3Dj)zL06r-Yg+d{r3_uf2(XbZe&c0e5TI?jVeM`mS-Ww!D@f-85Q%EOpy|hy0HI z&GCKy7vp!VAE3WmJ^=rce>eXp_T%51(C76ey7*ps-8*S-9t;wks)~snW z%bq{fclh3M)40;AScWK-dcOYMFrI=?nb-JW?uVZaNmwOc(4&_OJ8Py#2D=ac=#5f~ zOU++-DCVZGMY0V8rF^*GpM?2nG0prLvpDYwwd!9gDu2@PCvkILBFo<@yk?;Z6kSw~ zgD)R?$v883q5?6M6JgPcjWKsI7~=}y+g)@{jHrE)#sY$nh|%Ylg8BK}ufX6rRsB4E zsEhlR0+8UvrFhn5`8+XPM{o1V`DJAMhg*MQvkiO`+d2Oc z2KFv*EANbBT^WTY!*;^;6Ng!U%jIHoB4)7fNV<*&X1_20%;5i#%_jsk{Y`kQ?|P3*5BaLM5Kxuctge~2E#2#rN zX&SS*9jTDyMv>9vn{;qwwXaDMw_d?nyT(_si z2a4>z7$G^*M5f$lVSbtJW=Aj*OLteTbj$ujaw`=aX%6)p_Czmw%xNCYYM6wmWZ+_7 z`^CaMUac=s?_2t-SE!FZT?$cnmG=Dp&HW#|0|5 zJ^L{02(+=e|1UJnu6N}#v3wmpDH$QDhC7xGK=;@Qn~WkP2Og_*Cpe$dS|-IeueU=( z7MJ&je-IzfnrgQ1T?<&wbq|hgy$~$d{p?WxsXKGZI(Dwb{I4Q?pmNTy@qVooF0**4 z(28#HKO?{0_{0|KbtC-2xfYc;IEmdh+0`as7|rguzXb?m*n(kvi_G`G`JHJ_)eHat E01Dwaf&c&j literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/img/genes-icons/ClinVar.webp b/src/frontend/static/frontend/img/genes-icons/ClinVar.webp new file mode 100644 index 0000000000000000000000000000000000000000..0b0458a6378a07377b0c4b0819d1e5d91b492eab GIT binary patch literal 516 zcmV+f0{i_^Nk&He0RRA3MM6+kP&gp)0RRAy4*;D3Dj)zL06r-aheDyD3IeJO0YXp( z*?zE~Mu7DJ_ya@->$dm*oCmN6>o-mZu=nlvrOg-*!<>*$Z2VpPZqyt7H{{Pa3*aB- zbcVl`{u%47|4H`E?^~EBivP^}f`1AALH*P6)BBJA{#2jX0`}awDWdoiUg_iTIXZ$! z=%a=-0RI2}lqmaLg=kEy$8`P}_l?B%&W1a3KVHF2ox=XCKg5|nKEL8^DRHvq-=)7R z{b&9rNzeL*Y5|$?dOqcVf(*Jg|G(}Vsvqq!m|5o&tPT9SaqF2kC&-Y}VYvINV_64F z(JqeG@AujqelVb|9)LP*#P{%P{jdazm6jzAVOI|my!_5D!+SJ<^MC3^(f{cd{0|fz zvs5JIa+qDHY)(zatw}u`<&}R_P7}!PqRL;DpWcEgwyZyU%L8z)-} GFaQ8LY-Q`y#TzW2+Aa{&=C0YvC>e1+choAY5a58zvt8dq|4 zV7_O7P&gn&0RR9{2>_h|Dj)zL06vjA zmq(?eA|Wsc0I(7XYyi72QYP3;{L+3S?Ynq;a0l#G^v_W*pa+%juutq~?N_n6Gg41K z5(mI9qE{EvNBmWxuwTZT-2edo|DAFAnJ57x)OzOz%&eVi+kjgCtESS1^W#`8<^TEq zJyizmzp5Ym7$V4`V51(=3Z<+KG|cFJ@5bKg3gp;GiM!Bj>0HY zK=PSCX;;{*kXWXcD18^bC?v#Y7gKR~UYHTJf$k|hKb&HeVP?_8leYh&(=0osPU&;P{qYRQB`Zkzw5 GC;$L^fqIMp literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/img/genes-icons/Ensembl.webp b/src/frontend/static/frontend/img/genes-icons/Ensembl.webp new file mode 100644 index 0000000000000000000000000000000000000000..b38d069e160126a533234d7dd1f51f344cb7e54c GIT binary patch literal 758 zcmV@>K}{0w3(w{9{&mXcj5uW+4&jF4HZA^JsJPK@|E)s{6BCn;{VS7!Tw$U zcmMz4H_+SlnXZr!jeC1PcOoKBCT58^8#AuzB&j{iiXzRBvETpz{{R02L@9wkDflM_ z2mUhctIs7)D^y*tp{)sjwEKzB{#%sP)jYuV9 z2@0uIA1t!M8RZUb{Eeb;I8CRgB3;ILFjhhM`q!})d~MTiwHVXd zbu6+UIEmKSiGRwgzAb1QXGKT5Hyd3p%9@LGAQcx*EgHe_!gTb3?-~aih-m+Wk(QkoGGgfzkldH`< zgUb01!!(Dw6R%3}~k(3b8lXRIhws#CwzScSIcYt6QZ6KkPDNhQLXL__PwdM^{| o(%=tOV1zcbI}d;-9FzauoPCS%C-#r%5F+uKD27Tp3=F^k0FcOg6#xJL literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/img/genes-icons/ExpressionAtlas.webp b/src/frontend/static/frontend/img/genes-icons/ExpressionAtlas.webp new file mode 100644 index 0000000000000000000000000000000000000000..823176dddf6138907775c5c657a45279c2b4003c GIT binary patch literal 532 zcmV+v0_*)!Nk&Et0ssJ4MM6+kP&gm}0ssI|3jmz~Dj)zL06vX2mPe%`q9Hf+SOB0A ziEIF!GK6q9mHrv>$!>9jy+r7%lml=FKo9l%;~U1m;%ES{-2fq%`1ksMfXgNSnm>L< zcLc5OGngdOo0>ttVnm0RvL7fT_W%I?|91z|eV?im-_wXkFiH6PglEI{eo@R!WAna^ zqoA7Z&jsY`QE-*mf*m89rMRoK`&IS#&+p_;g@sCfl1<&l{r|-FQ5#Pc>F0UDI#uJ` ziUoOIYnS~j&;Q-$L%lX}_5r#yNz8fGWHb8r*<-W+9{>532#@{Gc*rjSK+jIwApxP-jZHG*=7D=jxCj6FIi78~H|AjN=zlS={OxIaI(Hk*b8qlk@*C9& zG>f;QNaWNzG!2HlG3iWO$j#xXJZt~09{n<*{MOG%Ype1R537ED3FCIJZ0L1SiozzW z{*lw$(`;9(PhOUlfV74kb|6BaW{9eCCwhwU+B=0%jT(Jn{ji}wE0++T(`;sR%&M8X z$F%%-9|Wp**}}#5FTN610pA$D2@3I#kqwjRLiX-X<23B3DLCwXt;Xc=iLzA~oR=#v zd^_-m{T!`E3Gyl@9*QudXUU*H{m?^L3g8epOIX=lN_&zTIl>(T3d9$gd|&9nd7x0= WH>tyIk7(YmqwaPXH$Mvn)ol2he7s^-#Q zdMrkXmYX_Db6#(;FFG#BRkG-ax~87*`!Pt>TFn-UkhdoSfnfkIAXdfB*pG CWTNQ+ literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/img/genes-icons/GWAS-Catalog.webp b/src/frontend/static/frontend/img/genes-icons/GWAS-Catalog.webp new file mode 100644 index 0000000000000000000000000000000000000000..1d281b878e9ae55159c4935748b8cc445d7e6139 GIT binary patch literal 1222 zcmWIYbaUIs!oU#j>J$(bU=hK^z`&pY#PVR|>FgXZfeFZEV49GVoLkIdz`&53S5g$@ z?xYYA83j@Z2Z_nW1&#p$K5zjH#j3B|wx*)SKr11-Ir=%7qGcYi60F_xJ7ZjBM z*$zOqUwKIZh&=(w7D>)XOaZc&0NFZ4KnDWZ7eL}k5cU%gI~~IQ0b&=01UUo!#sg$4 zq(j&`Aa)Xj4f2aZaz;@uP)!byt&*El3UmV)fPBJ}nim=jq-DUkm?4P4oxuqhd`S$Y z44Di$3?&S@KspZ?X4v%^f%W+@6Ugq&)U|@ax|Nmz#1_t)o3=DtU|NsBJ=>Pw}iy0W0Z!j<% zn1axsKLr?^?||YH5MnBG85sDR85p+hLx?3UWnkD23}cCV1&Kw8V0)N>BvV=%1H-4~ z3=F)H3=G1+u;z-UR2~xEK(j&SGc3N&zz|Z+z~FP1fnnw>1_m{tef%-Nl&Zi1OqU-x z8Rjx-DF6e=Zlcw6_oa&$DH?DvY!KqiVK`;f7{<6~reSmVdc}2%cj#Ra*|+~)Wy~+* z8|FX#zw!TA`A=in_aa82slkf7;`}OA<)1|HD}Q))E&hg#PXoih|K&`6=Xi}CtDbLL z6d%|g_96FX^t(rfo0*Tto~})$T?IR3mY zU8tUK&;ML0k!g?r?kZ{hS9yq|;FB2F6Z=z2`7dsTtyPbI8R>tO({8ozK$|=1irxH=cTJk7mM`Nn gcg5T=|ETJZPwnTl2fLRq;;uJsuv{AX(1d{j0NcTbYXATM literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/img/genes-icons/GeneCards.webp b/src/frontend/static/frontend/img/genes-icons/GeneCards.webp new file mode 100644 index 0000000000000000000000000000000000000000..510609c1d1cec086d493c99a202a449749955ea4 GIT binary patch literal 552 zcmV+@0@wXgNk&E>0ssJ4MM6+kP&gnI0ssJz3ILq}Dj)zL06uvzkw>JXA)z%SFi_3XYHTuhqQ5k37Yq6l%yD80ZMXc!Y<&X z)Xy{gz->NCOR6BB1(;J06<)F?r^ONp* z>PSz;WA3=$adKi~<{sdy#Jr8}?(!oK`=Zar(0jm6$(z0(E~s1n-tr2WF+vlihhex1 zTk=L&&MFy5gwt%yi7z^I#4`LK_4;{P=39D+7uUojONA<}Xi^)>`ecRA!p7{k2(fa8 zf*~qoU0HbZG8F@+Q&ou>@cDML?mpUvz{LT6=!7O6>#>g}o#c^i1Pig<q1}M6ZOA8T$8t8O#q^CL@vHUE8q6iF1!_{ z{}6M$tYV>(pTLa3q|MwgLQ^05$vQjp6CcN1FmbrQv4S6^9Z6`3790|(I%oBifAax+ zKh`}*`EeKH#}mUYKmRfUIBdjbrH)fcdusQ4o@`8d3$*j)d+=;iyi@*1P=PW@*h82@ zMH@6Y%5a5(o&m!qUF+N~vYHc-f>S-KNk&FC0ssJ4MM6+kP&il$0000G0000V0018V06|PpNa_Iq00EF~+qP*( zGfCODZQHhO*PCtIwr$(CZQHSPMq!^5en3P_fTGmQH9L22S~O-@xph6|&;KtM zH*pv=J6`Cz2SdQ&flcdoTl5OJpF1?&fHm17$7wRU6mVJx|tWZX8 zuG$rZ(HaCBr37iD+|Qt~=0I_qK9EnZc^=(uv3uaX#>(NvW0!2fjI&w8I~9bUTEPF! zGzLoP0Viz^az$+*XFPv`FZuE%R=NNtXgQx^n~ripogjTGu-0{l8hXff4Z%B=X*>5- z9qQ;doK0rYO&2(c-0 zFmAHeXQ-{~T$Jrh=%TXUF`kL(`k3#rlKU~kE~Ptu<&ax?1-MMvmTz;vg7YtF@=idm-P?)<+2>}Kx z?4!BEwTcSQwmVz7C?W$^P&gna0002c0sx%>Dj)zL06vjKol2#nqM@h=0I(7XX#fB{ Mqo4r(_|F=^0LGODZ2$lO literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/img/genes-icons/Google.webp b/src/frontend/static/frontend/img/genes-icons/Google.webp new file mode 100644 index 0000000000000000000000000000000000000000..fa34354113949990634a2fe664c91f823c41b6ba GIT binary patch literal 470 zcmV;{0V)1cNk&G_0RRA3MM6+kP&gpM0RRB72>_h|Dj)zL06vjEm`J6gq9HIC2mr7W z32XqJGUI+Xj{Y!mZ8&cN3nfH#0Qk$-dxON`(b`z=Ss-YQG}s!pM}ju>86}l@0+T)IaYr?oTn)%C{98(FM2m!xy-*nl1_dadT1kxkIW_kSW(p z1>7C5!@`*Z`i;k>a{A>w%8L^otjf8M(woAE{Ha+(b?3qEudX<}XBi#O7hxu<1UsmI z@bmsIYmPtZ|1}Poh~3I>3G`dwTVK33_y0aVKv+{%d1#fkmo$8nj;X)X*!L;i>FDtp zG_gzGjgBA%@3G7!Z~q@LCi&gJ@9NBAS|7`f(wj_anS;M#gmkTP?-SZ)mxa40O(cIo M8y_8-Wp^U50H9jo>Hq)$ literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/img/genes-icons/HGNC.webp b/src/frontend/static/frontend/img/genes-icons/HGNC.webp new file mode 100644 index 0000000000000000000000000000000000000000..77d598e937ee104fd546fbf5fc4d29dcd893366b GIT binary patch literal 478 zcmV<40U`cUNk&H20RRA3MM6+kP&iD=0RR9mAHW9yr{tiGBuDaI&^>_X4>SKy(O}rN zjU)QL?LTgf9Ym6(NIm<^%o5(eFs9wOB*7*~aY(5?m4uDdB7eY0 zHJb!}0|5Xw@fD>K2x?OWvxUA?sx-hVYBm1S2nq}Y|NN+_ETN>vTDG9ES$~S87!Xv$ z3erS?F)&F2u*f1)Nt(V)s-*D}8DHb;zF*C~ce@65Q=5Uq3_XXD)7s@C0kCe{Hr}l7 zOBq{rquREO)V6J>R%-s^=rxZK{ht8B|AJ2x5R;M`87LYMZ8ia@O$I|3is82O%$|N( z{FngpUP0{c%g*YoQ}UYxhH)la0ZR3WZ$qG&U}ON`&&W@;#gac`UB`dH)bt>_*|;sy z2l%?#*|}J)oC8dSIp&Imi1QLwnpO$;nFsdG;$5I}@93~>1N(j2TE%{#k0Nj!s2*Ja z?&ZR5sDC<9Ke&7(O5SV0bxn)6BUb8uwl*FEi!Zc$o2Al*S0~@Okgx>hp1Z5F49eyK UMpaaKZZA*&30kWqM6H%^FcJ6QPXGV_ literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/img/genes-icons/HI-CBrowser.webp b/src/frontend/static/frontend/img/genes-icons/HI-CBrowser.webp new file mode 100644 index 0000000000000000000000000000000000000000..56dc7f761603b11f728efb82ecd32d7cc30108de GIT binary patch literal 1958 zcmV;X2U+-1Nk&GV2LJ$9MM6+kP&iDI2LJ#sAHW9?Cd%3VDUzGNaG7DCi@U4E%+iUO znVFfHndzoPl%GbuJsZMb6wk&`G3dH+Xij?)I}e|olmqPhrebEMNG1eR48pq;*Z`aX z+jW*CNs=N-8kt#;Rn0xi%*@OuzER2iV87AtSa{~1w*tTsOnPsIjBmPKd zj&0kvZQHhOuk78-c2*(lwlzbNrFT`2ZQHhO+po55^U1bt&$LypN<@5LL}s9EFmT&Q zk(YT7UIFKJ)Ut5-d`)zsmM;Hal25-GGai|W zu$$rC(VpM8ydGJI7H-a{fw$AaVRgr%`Q_B#akEX!bB?-Q;OL+({pyn8^Yh6YTjL3t zHC&vB2lLtG)BBZ=x@~6n#%|mFZRRgd%_=^UnA|M|y!vX1*4zT~gmn(yBq+AQg}$mxA^>i&!HsC~or{gstd&z)^` zC(Qiysj=tBdpr7`-qPfbwHfR?n>TTtE2)#M7k!LSeGS(m&67(U;Tz&s;tAiwd&-l) zd8dqaOY6ZcJ=e2lV;GoFeBec4tPU74G8X;pZ5VUN=+M9+1EHWm*Z^brI#-MJVu;lSY<@Z(VJ!b{S61rmHA8sqa(hUKd`l6hEp5c})`+ZTc{ z9MCpA>1Y2DT+TH>c*m-R0?Z4CQ+v05Deo8blsC+e9>*%ZM2^6}3q#=VkX6ZfHq+<3 z#@v!DV2oIGRxcMY5pL+JMU=ZBBO;pJ~O7l*v!NoBx z&{~*SJRBL@ij3;{c3bWrhb@V4)9_ev;I%$y;t5?=N{FMBko=Tj-DOnrah*hvg=n|! zE0|tekMa;fg$RPH#C+oiMb-&EzLGKG481)rx?{KcceMXitbphSg zvaHR(yJ)rYlwM0|rU2?U$wZe8|0p&*{({4TBl2k!MPD;Gn^i8o%Y(wF2Yx}j$9zXy z$9zY-#~h+P?M}6ML!`55bQg`*+ANGli&%(P7~SQFdMXh2&8N}VslX7)7$W+b11&1= z>v*Hwt=>=bpY8Jso$B`n9dCEN%w1aA6CTZY5+@R}`GN0}v&eOgh4_>q`5e|?2Lppe z6g`ayMGlQOr#b%DJz{wi$uf($z@eCS1znbiJwI|HWzNbtFEWR_@E&O9h=BTHa5uQ{eZdrhg=4^SMOh9<>b zW_o#1j#NeJ3n5vQ!71IPupohXEVzuaK7OmBTATCc>PVPa4TOoshy9y1D_K}-jatv3 sPfjOH&uepw3#+s|XW4sd?s`glcyziBV!f?Z_ygXtYE6iU+4_+A7L?H9o&W#< literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/img/genes-icons/HPO.webp b/src/frontend/static/frontend/img/genes-icons/HPO.webp new file mode 100644 index 0000000000000000000000000000000000000000..f2b823b4fbd32a8307a5bd231fafe11e88bd4273 GIT binary patch literal 638 zcmV-^0)hQfNk&F?0ssJ4MM6+kP&iC#0ssInAHW9?wa|_v*|k+||BbHx*!!G&@u1LJ zLjg}lKmq`;Y?QNY+qP}nwte@vZQHhO+qP|AC%`))K#N2sxE&NXj1!OM@O?x?sN}6X zd7%t}hd@An*L?puRb&n4u8DJ4#VRrAz%}Y;|DsDA>s9O^cn2DMRg9rASZ@vvtrr#V zDBVDkpW+wa13Zub{)4AzX+uZ^apSCmQN2GOGV#LOlmO|HD3Y z7!5`h1^xy_#Q_w#L4~I(!kg_T(+H^IS0SeK7vrjE@ztI0P*r8R=n(SX^+Sb=?<$5# z{lZ8UAF!z(ir*1qRcxq(VWwgcVO7LfsTe(gzGS;FrnrbvQL5M(j+bxPt&mblEV5ZI z^i=XHv~-9wKz|5|dPS0oak0Q>A)s!yIR8h`7^w{o)fZgVD;1IAse)Yx^Rqtcmnt-s zj$%O_cm3w4;8j^FHuT2{S$)B0jH(D0@9Y&CDpkcVYzG(|dHlWocd!Q#N!}v+0E$kr zN$sE(sA5*psLLqumC!*^fm<=Hg2GWn&S3q@`ib0F8@$R~;RIs^uZn8bq-Dhe z4B>x7SRI7q0Th7>dYLbpRPn2@^*1XASY(Cs0aRLRvx-rLjw99>oP zs~MV9DtMK$TR(!-9c&AZv#W6IeVgHEtm);8x1Bj|Z_^dd6rHUXu YaQT>+Rbg~`Y(!27JPGHxNA9unfjgHjjQ{`u literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/img/genes-icons/Human-Protein-Atlas.webp b/src/frontend/static/frontend/img/genes-icons/Human-Protein-Atlas.webp new file mode 100644 index 0000000000000000000000000000000000000000..b71ff5313f8804fe83abf814396eab3776233069 GIT binary patch literal 608 zcmV-m0-ya-Nk&Fk0ssJ4MM6+kP&iCW0ssInAHW9yC*?YlWTzQCd@2BCs<81qcI1L^ z97&R+^zD7&O<)0rW8ocIY^NhhlH#TS>7@T3{l$GZOxOI^K^7purJ&@u5ke3Xq&a_! zLH%!lvrC9Oss)=#YF{02y@SFkm4Bt8DBrLW03yHuxSoaLd@e$H7a%`L`OAXHLZUFR z0ssRdKyYe4|4CK`f>r)uAX#kpKVg4N7R&Y{&D#$k0i=rQ|CbFSBESeiWWxv|QUD4fNTK2^D(g6@1|FwF zJ(}^bt4GtUO*NY7xT{C^yr^0?_1INQb3D(8%I8H+|SP&(fU#U?&w=rNZgYF#iH&zjiI|G#NnbMD@i z2;du#13*%wRE;d|4nIkQ{C{NO?l8CARgi4kZQFbRR2kY%_P?eqB189X10R3>0{~lL zfI*NTI88_D5M@I;B7MGniN(W7)HAFoTog&xAe(660vO~pRAZ2{5QLmbO=>m(21PU* zrwGk%5`>Kf5F`T91i@iwQY$WE2nc{!V2A~P*kCjZBw~u)EHIP9722O9ew$h$Tn9M` ze?XABAzreX{u?CXEHo(v^{a<7zdBlIk~wS;oWN)tKoSxQ3l68hf zNDT_udFJ(0%VD=X%zic2hmZT4zI^yzI~!+T-}&(SQC|K}-j?^~D7snAzrrEQNo-i5 zITFHYN-g?pFTc~xC$Dw~@cMr9$*YO4-+FgC+WTBN`dhtw(%#+mzt@MSg^yJeQG}+_ z+NHB-D$PQc9J9-Lqc-7j_fH|9oy?D5ItMdx`}Yd*4Z^4pfJxuz-T)(kb$ z?!Iyk4gfzNcW3);g&bCXX#BdD4x@%o{H9mSj}&C3#E0M5;^L*N9BiC?f9TCWS_F9I z5@MpGA|nuDlCw6sK6BJvNT79sL2ZXNZXWUYxOhlRK9QYYxE8cwrKrT&*szK*d>gUm zp_Wrc>BizB3gR3A@SIvmx6*z+gPBS7l&jZG-!8{{dTjCl@ad0sw*{5|UEmRf9v&P5tvh<|QgA$Vf|w3Ja3W?Sxs` znA?AS$8Qsic2=IN;b8!w09UU1czKgslgqR23Q8*P$Nr8rG8*LZMQTAVAY##x%8Hdq ze~SAskzSLWQ6 zU%tCT!|JiWpXE_KB{v5VjusrLtXP>0B#!7N#tJMG^zYyB`wwGJH?MG<(Z(y|#_rrlh zw)RfW?mhto0;iTvnLX04`rp%sz107&52mDJMkZwEQHo1v&Jln)3fh}IzgItk19gn< z*Y-uFB>-4lM9Ie)0s!adkGO~kwY2^I@smEm!z~;e2l0u?De2ji#WNcq>5?lGaB{bP zouGZ;=ao!|!RV+6#OQ=$WOLh#_mv-+zo}&my|I4mwZ4iKM{-UiM!J%%ZA~@Jbneg^ z|8?nWI-S1|9N_EiVo$bv>#U2$n?8Egzs9>d8mfr!$i~)ZTX1vH{atrcKQ;`!@jZ)Y kPNyE3BRjp|WdfU?nqS4W=`&>aW0Q_kg2-MvJpD+v4cg0R@&Et; literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/img/genes-icons/IntAct.webp b/src/frontend/static/frontend/img/genes-icons/IntAct.webp new file mode 100644 index 0000000000000000000000000000000000000000..a23f0d20068115d6c6f08bc6e81ca252f1531e7b GIT binary patch literal 474 zcmV<00VV!YNk&G}0RRA3MM6+kP&il$0000G0000V0018V06|PpNa_Fp00EF|+t#sX zPi)(^ZQD*RASvCNm7Pkhc>%UdcebrlywCaay!lZ_!~~Fkt4jIQLYpNICi?&YkCDuT zXMuX9%E}4QELN#l4J$vafR*Gn?bDbQ$)e>D=E(qWS92+Z{S?#~DG?qsV1n=}bpUq! zltTyMRqy3Sg`Y9VChT1co0nlrL=chGn(8nCYJodU1k_&v02q>{Bx;MOX|+WE086+>l2#4hf|#iesdcmPhCb+W_yq%0xin z`g?%mT!M>xMVOKwYKH|XkpDwgP&gpM0000`1pu7^Dj)zL06vjAmq(?eq9HH{0I(7X zY7M~U1wdM0H~?*^yCB{s|4tP8FT@wP+5iCl`{y73%0V>!SSY7|E2t5#@SMtknjsHb z|KzwJ?(aXp3KlY#q-Xq)i_?20;qiTDp~O4>#DDxPEk(gd1`)o4OVV6?!4YF=I4y32 z5YX?V(`qyHcMO(+V|a|GwVp3frap`lz3$~Sfhq#M&4<)qPOt6@2bR5!gSM*qwrTvT Qx;5{jM$y7%joAJ$(bU=hK^z`!5^#PVR|=o8@a2gqY=P`>uHUQ()2TT? z?!V}Q2)WJge*d4tseAsi6qbD6UHZQv&-cOv~DRsB4p123>5`4Mh1oePKLRR zS_%vb40eGEy)Md&6eU;~ELd5#G5F1Nvf6#-?crDKQ#cM(G?X-aX57X2_wWklivI8J z-%__arT=;pBEzupnQ&$GmMgjrCo7l88@zwn&cN{R*NWqO6-n=nUif%%2**dv?W=qv zzquxDA-Cy7|3hNg>fJGCtADJlU-0Xdj=6QIR4qeG>+dt=*R=bY8`hlO^lVjG#^U2X z^4}CHPP00zbSK)zs7zZGKG7x2F80aa%YXi#DfWHG%Ny};ZPlMtrjWFgb-fCAVkC_2 zx?Ei5U;lf$|D(QE!JBeDY^Ih@&c9piUDaPOJ5Tdl7pAggHk)UxgJItr&kUoSSk~EB zHu`d}pHryrd~eb$pKXTsUj|q^Ui$Xz?*+XDxsnZ?y+Vs>E9TzTusc50`&)D3%6VdU zznjfFs>8q#;Txdf3=BP+3ag6Tg51=SM1_jnoV;SI3R|Vbl>DSrDP?Wt5Z@Sn2DRmzV368|&p4rRy77 zT3YHG80i}s=>k>g7FXt#Bv$C=6)QswftllyTAW;zSx}OhpQivaH!&%{w8U0P31kr* zK-^i9nTD__uUHT4Xuah8Tz!y?zM-ChKHO}eRvVD0m48uYD$r(-`F4gjV3jChPA~w(YEJ+qS*6ZQEGcu0{VHC+moq0Q|k3LK8f-U!WJRy!rS@H}9Xv zPD?4}o8fMh;uH`T9_*TU~GQ1ZJX1z`+$N?qw!Ay@q_ zKZ;k_@X2(4+|=m}G{VVC1>lw^2wrQ!oj1=8^<`zdjO-SvB6Q%^UGvK8P@_1_O4n@H zq%!9`C$QYGx{j?Mq?GsD=y1vu1m}z~#<>v$#kmTYI$^anJzD`y_EZNP$oLU`_;3Crigl)O>=UEUW6DI_4MNCX7N@iCXX0}gVzkX#_3A|s=W z1(61|zN(C3t00P2qo||QQE3?!b?`~6<0w^8u|TCh3h5;R@`vNi-OT>>zHj&2{p}{i zMn^-V03anQQkSe#qUk9 zPPM9Q=i`!g^+U&o;0GssyuB^f>>XOW&z0os_rHvpJKD=@2m3y`p$dDsaSTVaU5nox zo_MDHP@Z~h$Fvt&n{|_VZYviatFD||yYtcQ4BxO#y<>xZ5G*;a%Qomz7kNAPm&ead zpFPKeaU|J)D#}+yopB^)l1J;9R*q{C#t0-e(|}DY4=uHhY0qda53^o2PTI9@Yy6J2 z*_y6vT*>3^C3lNOQv0&)rJL%K@si7#dCxCPemk6W!&aSef2M8El;VO^;!nHJk$DI3 z^2UU~=77Mmnj86Bb5hJeMto;}xnFcl;;h@d7%#cKvx0eWPf-J3to{DYvzA2{XhYQWJ=6S0 zBq>G=cK{2vMJgs=a0WO*%+GVh)nONkszz<~y|Mi6u7<$avz39BfrWu7>R;3z>Z8k# zsezz6XYYJ){>h*wuN|?AJL;OHlkUB$jXl3_MIq4l>RxEhaOb|FcoSV76|Hw}H`ETcj^##>KUY?$%RTGsVX@aV>=iaQr(f!^RjYHj1~t4-EE=Qx+H|-pOA%sjUl~x}RKuyk$#M45jT5YIE_2-YviUW$q{m7% z`BqaS+U}ndErl$Vc41*vTY+l9lji^SW>YmaIN$AQ++Kzt4hMw?X*m+$eK7u(OvV_ue75!c%icaNR88hFNXm@nA&E- zi7bi(^;9}-R6(y!pMyY}R6!}yI3&)bp)%;0d^44pzaWXo&mu4qQip?KHl>X*F%*Y` zHpXDIC~YbTCs{qEn6e=h!{f-*SHq=ezz++1Ork8l!Y+UCXQx}perJ-XRVwH;+1|VficC!4F)z^gzkcb1k1oS z+ytXS1ZEglU2e36OQ1f=_$1nrlxL#g1j@o%%>>26rw@?%-hDpN0FWdGaVD$T;I5M- zV9G!-l##RWqENRYUb!;fO!F6r8?-E8b^VRisvxVCCY4gH2vJ}n2_}w=)<&WzhDst7 zQVh|GQ7NL(C|t>00frKJkC5cKG6a=rC6V$7d88Ocqa=|sxl|*HK(uI-HcBgr8sNqn zEgWtnC|6P!m;T6=eU_`#m?@lN%}Fe47#I~@2FtOQ4Aul{H15#=XT;$IZFD(9T({Y; zmon2i6d7%18PI(lO8OHjB!mLjizQM4B1TDpTrWWcdW4h;Bm^oG>!lQ;5J{naJ~^ay zAI6#Y@RgVb1SwG&LQ-N37vOTeSb(CGM4%TDA^|1QqnJ1iPb2gc1oIL}Ldnsbf$E>$ z2m{w|8EDr8DGj(Woo|(Zq|$J!frG+zX1>5M*fp32n#a1W4>Wj4Ft>ZiBo83@^>CSk Sd&wULH=8o@>ul!t7Wf|kpv7_h|Dj)zL06vjAm`A0fqM9jOU$|BnP(8k&z<sIU?V zKoh29z*Zm!KOK^lU}WycNM9;PN9# zpQWgreV+4MmD_7W+FsLGNWtq0yZ=Hx@VR-8d-|Jz=B)jRI!2id{z|dgGXd^>ip90$ zk=vcZ?qYU=E_D{2q?8#QvqtfWzUou&b9>rwmKYv#@cLy4{R+RYszwln8^1(o}e^oiZ8+ur7CZ-+41frjw0oPT-76Dcm1U2Uz| y{nC>+inA1%=)ZsYj=!;vd@g_LC@q)?u{3E>$R?t!7oAx|X(|vnBqKyglmGzMgTtc$ literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/img/genes-icons/OMIM.webp b/src/frontend/static/frontend/img/genes-icons/OMIM.webp new file mode 100644 index 0000000000000000000000000000000000000000..8f2119c061cd7202d9b8ec585879d691b268a8a7 GIT binary patch literal 450 zcmV;z0X_awNk&Gx0RRA3MM6+kP&gp20RRB72>_h|Dj)zL06r-YhC(5s3<3ZQ0YXp( z*kG6WGduLV*1rq&*8gt&K=;J{tI6NWzv=%RzJY&^e@6cg`7Ql7kOTFYc58MTJk%El zWsEDve!J>&Kmh*#`1d-$Ip1YRQ@M^$g$pkZ!Qbb)KgMg)d(Zo~hXp49LIue+*1G#@P&AEK*9>?r|F5e5$(^J6$&>%Xm~eUCOeQaubNhea z|IR}nYxK-6_9T94i=~>j?sItz)zw$If(Wko*%8#L-YW*mrNxUh{!38GOtf&j4fo z*Fd=3*g+VQP6yzibk8D>@epqPY1bX8if6u?ZlSiwc{|v$f7-+LmVB}F$*9x%Ltoj4 sg`adDq^*3Kb=cn#?_<0jBf$eqqU-h;Nk&Ej0ssJ4MM6+kP&il$0000G0000V0018V06|PpNWcI900Dqx+ma$B zT8DzWyDkD|uI=tF2`gsE+%=HMsvkRuhzTIzLOQoXnx%K_m*F)9G}4$%$d;Q`tci!#uB+Q_E_7P1^>ThXmP&4qcu_qtWQe zC=pTUAR3KE&)f`={pg;d1KaQ;W1F@M*1A!f{M4et6Xwf+neRw*wOrXCAXZQ~ARGYz0B{EYodGH!03ZN9B@l)} zA)yQd01N>lS^zFMP+yY%09%1}1Mqa&0A+%5wQtsK#VYn7vp*Rem^)VY%enIoy=R{w z0092~@oX=@{6B0LdBoZHG5m$j$1kDbxo4}>rZ0#w9wGq&Bk?3(|g3uGBvDU z!+-zJF+D!{vC@q2ZM2&~mSu|M&jL3#o$a|L|Mu4ma_`&rH~JT}t~_?X&9BU8{iCPm zbEh1Fl>;13Cm6ixas&KkRD8O>t-<|v&LBNMmERwyM-Kn}(NV9*3wP(k|4Yg8f8bcQ M;n&afD}n$309(}ft^fc4 literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/img/genes-icons/PDB.webp b/src/frontend/static/frontend/img/genes-icons/PDB.webp new file mode 100644 index 0000000000000000000000000000000000000000..2b1466b0da216d233bd483db157e9abd9be92e28 GIT binary patch literal 532 zcmV+v0_*)!Nk&Et0ssJ4MM6+kP&il$0000G0000V0018V06|PpNaz3n00EGrZQHR& zPi)(^ZChQABj`=ijos_i&Jb3;9hO!3$6w)%#8OlBZb<@C!TfhwP z=JOrMA({lBI|(f1Re!>f>ooY!qO<{&5H$ht90vce_B8OAo*L&TNtOT#$nqL^k(&kD zRSjl=qXpoMcsUO%e}PX?v^s5(#Di zLLiF(-bw$MSb^=~n|`q%Lob?w`Q&R!eyw5s|ItJM{{Ej~|Ic)Mnkmxp#K6XoDA`9n zEG_)u4)+i=pHWr47%+d=*j|Zq|NFn`J@yey&qQlItlaDeZQD~o&VTrzp1`!TF`d?*st zyT7yMDF=~*`GVTug2R?n+flPgK57q`SG4C2AgBCUZo-0Dj)zL06r-Yhe9Et3IeJO0YX~< zCrqIpXXbx^9JNjxz?%Wm1MSOv2Ry%^4d4yp=f*|>*brb!oy70>4^n*M{VD7T`iuVW zuXmvb>vG)L4vS#eVh!WKF5yh-i!0&>?;0mE1ReRdkN^Pw|NjGFGgJW2C2M~e@NW8* zu6r-Z36NtcdCwVVsUtXCDW4KLOn!Ne-v%d>a}d^A&ZGYkt{XzMpfNUt(J33HODrFX ze|?9q>i78V_z`|P4l*TG3a{5A#B`M#EcgNF*%M%cY0KRJeF?Vt7cUG* ziHbW|i}3;%;k-%_R=Et$Zzd45iy||sj67j=8Xytrjw>-thh;&iGil9{*#Z6flFjPx z->%Q@2JinD%Vwdyi=+Q%hNZtgpk%1kST)z^aSInqAj^rP|FCW1ecPIgwf1UcXlaVx zfTPp@t3yWeiS_#7Wjp8pUMs&p@tC}Qz`iH%*J00M(nJi2f1D?ubnM*t_xwlgbR!A^ z-k|GtdKb9ZX4#(RtMKS9$w?V0?|_g0mMIkhe?;&aqnrc5({^e;_h+h~?y5KoWpZ2W*73$+%v<$bIT>DHi0B@}~&j0`b literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/img/genes-icons/PathwayCommons.webp b/src/frontend/static/frontend/img/genes-icons/PathwayCommons.webp new file mode 100644 index 0000000000000000000000000000000000000000..e6e77400ee3a0194849f40c4b10665061bd1f043 GIT binary patch literal 1130 zcmWIYbaP8$VPFV%bqWXzu!!JdU|=u+VtFv~baoC1VFGd)m_l-rbBkGkQn`60MM3UP z3K5Y}3hWOV*ck+YP?>=@F}b+FF(AMPq#8T;c8!4%By(98WEOTcI7PWrQj3!r7#KN# zD(sRAib{ZN4mY@H&Y1A*)-An_y!`vr)d4q^WRv5P{2 zoPmB50J0U*k=RK{?BtB1T%a0Ips3{LlmguV1|XmCq~?VN18F57mSHGn2x4$&aAHtk z@MLghaAq)LFl4XgX zVo@U49%dlPl$OT8@M$>%18*b)gYX3g2CisGm;vQL@(hcwGcbfy1EcCJ1H;T&3=C>O z_53l8J^>!mjKCP0kez*dqq~33?QN5dRIRP&=HB+R+FcZ+ntXfPZcCQed%M5?zW@FA z^)rkvM_4Y@hngE`nyOxL*m_`P_{KbK*Ui8;CKeI1>R#XezKDGYN&wG$TR6hB_mY0Xqgu2VE(xlctJMiW2dfDqT;yb0w`(~bZkDdSM&;g%M3s(L4 zxnXTwM^@=k#e;XhUZ1_9e6GFTvB&BQUvN0A=n@TcUF`gP!spv>EqnfVub97?Va>Ay GpdSFWq*`$R literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/img/genes-icons/Pharmgkb.webp b/src/frontend/static/frontend/img/genes-icons/Pharmgkb.webp new file mode 100644 index 0000000000000000000000000000000000000000..c826da96a194adeef17927d3ccfe9e02c224c375 GIT binary patch literal 994 zcmV<810DQQNk&H60{{S5MM6+kP&il$0000G0000V0018V06|PpNaFzj00EFJ$+l@* z?;C z3%LkNAvFL-OXGnHa%~Hcv=kU9?FTw4VU60gYu71ciWxU@FF;A@CQwG1{sL?>85c^r z1(a2$Kei1Xe4iCZ(h;Dm(!KKe1?x@UcWqtTCy(q?fvZx&gwI_C$YptemFc8su7Xmy z&<22zt2cka=KquY66WO&q3|=M8zq_%?hvyugDp3y5 z#+4E#NhJ62y?neMCX+^adk1b!AZ_vS_V)I9e{)$4gC9htRM9{1t&pU1n0q^YfS|;B z1hkT*6TsJ4TTFTv8V@X$sZQo9AAoA6x7Gi^!vrpJN{4_#rjRcXnp-Yf73~%UXJoSb zBy!{UB&7!|@aS_-PyUs1apl;RuRuE&F$;hzF7ikvqv5jY-*DXrx|s4w3V^q!&_t>X z_@y>_eM*3Ct6G|$Vj zEbHRtVFe@!09H^qAkYB-0FVp-odGH!03ZN9DG-N3A)yQg0sssFLR$bQOrabN<$r@* zfts`057{iLPALB`_~nQJ;sNXd{*U+vU<6WP?61I&?H}!rvRjqCxFYuq z74|i>J&t=zj>G1M@Pac;h%+`Qys!ZN^Eo4{Z}hq~Mj0Q+D2|E$NCweXXmpKS7gJtI ztqCf}VG|p9oAOy<0ZlZ!xFaPDXX@O>FaB~5aVvm}gaU2aUBx;ZUC2x2OdtQ4SeY;x zr{sA)*qIn3N=B02`hW7x*cfjBzp4|+98a~qlkoyNpuV1Wfx{G0fqBHrBdhB>Z=)S! z?Szo?jlT8b5wb$Sx*fo}S@m!1acFZ^v*+mk`FYSo4C3d4eT$5ucreXvGz54i%X0HI zima^kwK${qaSaLs07huRhp8q7>1lryksV-6~A@=BVU0u-% zLP!c9ge+%XBXAN9$LbMQ+=;bBV#UX3tn=QM+sZ7&ypYKV=-E_Glotm`#C=_T_e(zB Q|BhSUngL{!_%q-D098QDX#fBK literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/img/genes-icons/PubMed.webp b/src/frontend/static/frontend/img/genes-icons/PubMed.webp new file mode 100644 index 0000000000000000000000000000000000000000..f9225b6e69af1c5754c3352ae67c3c3f114daa79 GIT binary patch literal 744 zcmV-T%G7F?vJqfGhUbY>G@Tt zfBHYU`V!hec?HUBO7ul>`$@1lq7Vwg2? zL0u<-ozexueEG;-WguhFw13y|aN8R`O6ULp{{Q}#-RAFn9(ufkFyEVlTKa+KR8C|s zlY*vxq<7*3Kx8BpLt)o3P6%>~s?K)(i-UIZ^O&ujlnrMSDu{TUnO@^1Ajea+4A z*1YDxi+YA*e1$Ov7ECaXkb9WU=k%9oLGM8mtHmJ#z>;_T0Z+D}fzHb157nh3(W``% zZTENEq@Krt);*GB9EH^;w_la0hQEA=;d8zG7N?dII_apP-~vMZl4|eyub4p2)`LF^ zBmYq=5=PE%MHOu>%ivR3cK@j`(v?@8U2l2gV`#GQSVg`wA9lAmtPJSFoI|JAe`_Gpz9M}Fcc?@)Nh}*i{l$z$; z&D>9V>6;1Nm=a@28h6i{nyC zs%9YfqVDwRLU-Fx9Zhi)>@D0vM?HCoPY0<3C=G`ktoj3SD2EF&; zvqVj&Is8j!_ctg20RI2}&kO$@{-HVg9tR|OuoyavZS?o!Wxkca+=1V)!pVdqr@wA% zPVkT6z!fWd?7`D-gjIXGE1xV5R>gx}Bd!3k2i$q@V9A^8Kf~nLNfHD7Pd~(r@8l@l z4wNE4Yd-kSA&!(j_)!olsQzEsp)w|wLb+Ma{?PGUq zx^e%TyOoqLnhYmzKIwo3w49Kn^)epFx%WWk!D_4iiT>mw?|C9wg4zD3k1WgX{TEw) zQN@ArB5K)uKbM$3%9XvVgQbVtJ`MkCP>gS%wf+Zy<<2t0%#lmSf-4GITA5}>_Ezu~ zlwNSRe&`HKtg0YQ2P!PDRq`_(#Lw+{Q)flV#iP@|_bK?k0Y69T^V$q~z|_KMR0vd+ z2f1P{DjjzF29}v!tZvdiI446?UwYoDA^I2Eb3ApbJX%kV3B#;3z)$y6c K8l4s30000;Cjz?w literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/img/genes-icons/STRING.webp b/src/frontend/static/frontend/img/genes-icons/STRING.webp new file mode 100644 index 0000000000000000000000000000000000000000..8b15b246d8e7c7b10229e3c2f437808ec593a952 GIT binary patch literal 444 zcmV;t0Ym;$Nk&Gr0RRA3MM6+kP&go{0RR9H2>_h|Dj)zL06u{Lo|XaZLdlb)^flWBvOj zwb?`f0RI1WdH%%j`%4(pF@{!k)jp9-jcH^_J1u{NA0PFhOuLG)f1Qu;qIzV6 z=#BC)n?Z=m{yqDvu9c*}{LIxC%np&~zdb`(AgIO&&uFy#HpN#5`-|O=&QizozovH} zrTaTRx__LDhLwv()?-K9+R&cj5pEffpq?x7NV&p=r`HjGlf|d5pb1UbX5R+yw+wHX zK7W6VHw`g#JWKPVONPMtOlQiR_W|tFvca7gdw}L+?L)fFxBS7QW_yKLnxn-B4Dnxc z7q+2`jG%rcruyu&-oY;`IA*Qw+Sqb@vB7TKveF4<@ m<8+b_HL>Hts(ztD@yn?(<<$I$a(K>G09S_nl*v!_(EtEenBVjO literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/img/genes-icons/UCSCBrowser.webp b/src/frontend/static/frontend/img/genes-icons/UCSCBrowser.webp new file mode 100644 index 0000000000000000000000000000000000000000..7740a907f4bf8d5fcee0ab9aff1305b14b0b4e5d GIT binary patch literal 492 zcmVVMD8uL?>3+r`yYUYIQ;n)&2V`F&bI;r4N% z3&g*@^L7?Dl}MG?B2E{OYZZ_BmzRX~^k<|d2~AQ^Jb$N&I!(fAnv literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/img/genes-icons/Uniprot.webp b/src/frontend/static/frontend/img/genes-icons/Uniprot.webp new file mode 100644 index 0000000000000000000000000000000000000000..404fa98e58286866486633a203c0c43a0dcd0416 GIT binary patch literal 1646 zcmV-!29fzvNk&Fy1^@t8MM6+kP&iCl1^@srAHW9yCd&Q)pYz=P^*Z<&+qP}n zwr!g;JG)ivEjs2n6Z@89I!Frm;}i9Pqn#{%6{LmSa<)~D?Ht+3Vp>QgcBY6_VB0#r zPkE$(bgMvCfQE{uYTOd93yzV)fzpndThz!qq3#dmY`){QiR_IN{~1 z@`!4z7$XOiU;>0iH6xi3U*f?cj0ZV`d9kk9;kU1gRd;6F;o}ngB_o0nEs@57HyP0w z5Lh4wxI{9cvIC2=e*gG2`Q1mRN`YrzA|eA?O>=~i!d;RUKQbXH9+;tump92K#7z>q zqUj~Rs-zJEtNatkbvE@x>WD&}sXpAZ*%46X)kg-w#Xt1yoAwXW7`SZuC z-rV-HL`1s90}FLba3*n?04DEH1P{sCBn0$1xr?ALOZ3SAnEdubaXgUga7mh5zYULyD-cV~=Lxk(LV7v#i)eAVID10|v)#n7L84*=4ocsR|XtJVr zi7fF>`{x&3VzICx*UqRqwsMJX3&JzLeIs)XzcYdW&!j2jPg}y@WMvOXUc`Qj5iKvG z(*5!?zJ8B+^hlE1#sQXF^GD^ zk4B{A#q6m|y0&e88db+tePpibcScZP&z~5OJJc}w-8;$+DqKRE;)c1KN77gJoMb}K z9TCJ4Lc)GzspbD4NL;|0B+FH>O}h8VEs(6B?H1kYrUS1Wff|$R5oA_H)dNDChu*lL z%9zR}y48#x z6--8CTL_;0`xlvO`j!GIOFBF5na}+3GxF}e{J_!`n<(rIW7EHV0sYSGIZ4&6p(scF z&?-r0t+$S`Jw*{`ym&{`)qNmlGWW!BAaOn`CqR2J{%2h%px-NFNSw#oVfU=%x>2k+ z1B-fCvZA&FMA%D-a$6q<=KT2s+V7lR`xcO%lAISLqvF@+?F(DT$@POv>Rh3>H(nThS56GRTrN2`3prlrzAL$ z8Cd{}5EdxuG+iZk`4Pq3%_6*lz6=~DYgF|@-Y5s%(afvO=J7gWL3vt)fl&|cuWlkD zUDQaOIV}LaPwtNT%@&~Tc1)*y{1|cnmZy1C5sg*HSAi3g=CMWSR;%s~0A&W{WpqUJ zTe3oSc$XcLx2yJ+8;1apD3|q;R3)6b8in1FEaJyt@m^^FnEmTFVUA^8fR^QN8}rd_ z=hS7ljDPv^DnGRN>@DkF3Ybt5W?6st!pM6!dw?wq+?yz`#YeS?Rflfxb872#LXe1z zNREJR*zF5^_rG{>1o2h$cI}x>0K;xvok2X9te|7kn|Gk|zJ+8&A^Pz@N06@X*&P;r zapGL|XE)uOzLKZf_-e7PX~h9Kz>g8OV0?I{Dn7FyGROe~+F+7Aj-fZt@OQ?K@6vq> zybdl2o^i%G0ir9`eG<|SvN(8|e#IHzzp$8tuAU0CNnR+)eA#0L1S10J21EuRD_n7- sg6&d+ub#04r+)hEynk7`f8k$OEIZ6weX5=eq0?6$EryvRrPIVuv)Qdnx&9N${2aKrEkaAKp?OyA>*( zyl9_}a)Nn0lQvAo?+kCd zoYsbmROhk(?rN&8hp(z%w?b_hWQc6XWbi0Tjf5JEHRKv%?ESP6L_R=}oG&)uVcOGY zI`YNRjuX9dKawbm=wO#HP5FJVzy3I$5L;-arDvfEwa4!Wf0MfZ-|qf?myP{?hJXM701*sEq5uE@ literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/img/genes-icons/dgv.webp b/src/frontend/static/frontend/img/genes-icons/dgv.webp new file mode 100644 index 0000000000000000000000000000000000000000..a482700515ee517a469218044242f66d992e38c0 GIT binary patch literal 602 zcmV-g0;T;@Nk&Fe0ssJ4MM6+kP&gn)0ssJT4*;D3Dj)zL06rxShC(5R0e}Voku87| zrcjIl#S6GDpg&Ca%KE@P06$8U_F;DiFwfv0{WNZzI|MBKj1V?zbduI z|9JFp{a491vp(Qo#ea=|eEiw|`SMrmC(r}+bvd@=lTWH~XZ(=fbjhaw1;^vIB6fo# z7ytkO{{Q}zDF5q}uUwP!zQi;T*|4|YsQEmKbSQ3w_TBP7kqcuao``s1Cj9ZBPc``c z!eF*zt7rV?^#;BR;Icm#`JNq;MU0mLZ?~fr|3+5#|Gz4X44n?n<*TPPcyOKnOA3Ea z`LPUPbfFRY(?Hk?2DG-it9pM_3H+Je$LPL};jH0$RbPp#{Tu6L7;ERDmgC}h>pDvQ z`K3Qc6W)PaTEhLMB5Kzx0vMnsXYcye&;sX3VWu?te0mrv+{9ixEE#gZcS;#@QFn<5g>XfBa&bhk$UFU-Y2oox$Q#`>u?uxmE#1Zx~mjE zzTxdRTf<(-{a@U^jse z5o3|VzslgA07=8-{HmYdJs3jtvAdK;N;ouvS`j8QlS7hBm<&fm6l51<)rtZh z6}POn#kJspSJf!HDu)$uMKH3URlyS~BBCOl08+b;c71l9nLPjT{@?e#-}nAs<^=}? zSP!RAqWyg%q9TL~9Vrxw3;B8Uz2r*h30n$<>0z>`7F@nnb|tqkC+`;`IxRL)V}7tv zYcA7%9dG_meVNuA=5eESEKyh;vJ)}yjk%&Ex5#&UDf#OJzs-4yOm5LE+N8atH3Zd6 z@L3Hwob77u!Sq4z{`@Bam14cF-mF`2;P~dCF+ou!Pc?Dovg>;)onQ7!)8$+CGJH$# zGqZTT{$F=Cg}bMnT~}K3v+a*v%3^s+YF10ftPSses5PAsy=F%^Ax&h|6i#aY|?Q%O@;j_y&a{{p}s5ZnW!Yx0xIeC%5 zW~T2?vHlI^P9qMTTVO04o#s{inuu!YK6p20-x7~8Tr;)dK5;wWv%TiYE5AlFhPo>k zJX3bMD-t(&x}117?Y^jF#$(TAku5iD549bQ)SI(4zR$`ZoDEatI`ET|k8Ewt{NfSq z=Ca4@4cD_hhc1eX#=1@&f0Mj3w(j^!mtzaZyFC(z_oQyaPCwQS8?9_+w>#F)(mSSD zY3#~44^M06J!~TT4?SW1Rl3wPH_luVx}96I=WmA>tcp*2KI7E!4(p3n(=({eTzy#F=#8Gx`sT$q zjjB=xQd{2aU!kByr=Kw;HD7gzbKT-FDsY=~*tWTT9ez$D(_-AH!!&jm(yyDwPpUdl z)D@<0s53soI#P;zk`rVrh9JO0YHq&EaLt|8k_U5hMme!$g+pdjg0nNOde=q)mAT#X z11~&malHB>;Tau?zT&~zItK^~tut(sUs%6(Se{W*NBK(=V@Pjf`_#Otjg5K*v&^R^vncC3c`$VkaM>q6Gtx=oBuna@bm?ujJp1qVg+61# zt*Sbmk5Lv~+-P@T`g5$HEqHTdX(d`JPa1t|!KOsppSEB4VeG4fjFBVmiLz}9C@(vM zZGEfL?N4ql5N7+;SeO38jz~;MYlLgPT=VIYyK8Qb+w_&}o0nBbFK!5lUV6A77QF7g z8TxBxi!s91x*9nWbSc-yMHK1SaBlS40H-C=2K&OXCn$w3LYvE`i5aWTPFphkUg|Nh z!phMtfLA*Yua0a^6~!$t>bC1iewuWfuEd?&hrG>@m|TU{njt#MQpxLsJ-(}xrZi10 z-+=G5wo)9KcwqIOLg4E7D&|RTv9SkV5XUUBUt_n|Mvx&j@lEI6?5gVtjWK4dxa90w z|NKjSct+d(eD(Q=l@;j8#r4dV^fTThCY90>Q1b1cFd(W$lF;$rE{*w82&ag=@QUY@oIkeeT(uOm9cuQDR z!a?d9>KYb$COyx4t0`8Q(J2|j48eP2AL`B&%K($y_)n^eIFw3Vv9->8Mb6Hv8t3ec@w^|EAaen!KTMq z#@YG_~OxEpW>8%1PTESyfqgFH*>W%VNj)cWV_*TWq47 zi|T#$2HS{ALNhmX?3X8w9(pmbO}B1Y$&2T%nJ+y&yS|Uo$!_- zuk6Xhh0+LMiXDaGIVVLA!SN^o$Wf(Q=S6EfeU1jG5ieRaPYO!)eyBZq0FAtN! z%j01YLYp%i@Jtb>X!Tl@fPfUOMrRPGc+nsPlcQpCY*}W~fIbKj??v;mYycLAei)7d zA_j{NgCGkKkRvXiEfk3a0>LbR4YD~*kk4fE=xmOdClIr^z`#QzJ>rN$JWmoZ;E$Yn z(NqMX7c-g3$;pgl4glvltQptXCX1;gCnQ}B2X_HSvM{nrhrz+wf4Yax=dS_>&r zre3F{0fUJY`(YXkCmoZ~ar&Z=O2xrC0|Dt^G+5$ABUv-lYD5h3SR4){5Ya_2n@{KR zAURzm6msY=A`);A2vL9v{`>P13{JGv)^fgI>IeptHh%M}5GpuA5scE=To#!Q9v7j@ zK>?c%bGUpV$de0M0^WOT;kcT-ZII?etCpw`(js5MlC!u99vu;|MV6?zbU7p^ryNiY z${~m&Km`^(_en$?q&ASnGY;I4`DntxNTUY&R3V07i`uLK|3Cec2vu7u$_~W!9N64N1DdTcK<8qal#z{ literal 0 HcmV?d00001 diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneInformation.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneInformation.tsx index a0836d44..bf652a04 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneInformation.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneInformation.tsx @@ -66,247 +66,247 @@ export const GeneInformation = (props: GeneInformationProps) => { source: 'PubMed', rel: 'PubMed', url: `https://pubmed.ncbi.nlm.nih.gov/?term=${moleculeKey}+`, - icon: 'https://www.gene-list.com/build/images/pubmed.svg' + icon: '/static/frontend/img/genes-icons/PubMed.webp' }, { source: 'Google', rel: 'Google', url: `https://scholar.google.ca/scholar?hl=en&as_sdt=0%2C5&q=${moleculeKey}+`, - icon: 'https://www.gene-list.com/build/images/google.svg' + icon: '/static/frontend/img/genes-icons/Google.webp' }, { source: 'GeneCards', rel: 'GeneCards', url: `https://www.genecards.org/cgi-bin/carddisp.pl?gene=${moleculeKey}`, - icon: 'https://www.gene-list.com/build/images/gene_cards.svg' + icon: '/static/frontend/img/genes-icons/GeneCards.webp' }, { source: 'MalaCards', rel: 'MalaCards', url: `https://www.malacards.org/search/results?query=${moleculeKey}`, - icon: 'https://www.gene-list.com/build/images/malacards.svg' + icon: '/static/frontend/img/genes-icons/MalaCards.webp' }, { source: 'PathCards', rel: 'PathCards', url: `https://pathcards.genecards.org/Search/Results?query=${moleculeKey}`, - icon: 'https://www.gene-list.com/build/images/pathcards.svg' + icon: '/static/frontend/img/genes-icons/PathCards.webp' }, { source: 'Reactome', rel: 'Reactome', url: `https://reactome.org/content/query?q=${moleculeKey}&species=Homo+sapiens&species=Entries+without+species&cluster=true`, - icon: 'https://www.gene-list.com/build/images/reactome.svg' + icon: '/static/frontend/img/genes-icons/Reactome.webp' }, { source: 'GeneMANIA', rel: 'GeneMANIA', url: `https://genemania.org/search/homo-sapiens/${moleculeKey}`, - icon: 'https://www.gene-list.com/build/images/genemania.svg' + icon: '/static/frontend/img/genes-icons/GeneMANIA.webp' }, { source: 'Pathway Commons', rel: 'Pathway Commons', url: `https://apps.pathwaycommons.org/search?type=Pathway&q=${moleculeKey}`, - icon: 'https://www.gene-list.com/build/images/pathway_commons.svg' + icon: '/static/frontend/img/genes-icons/PathwayCommons.webp' }, { source: 'STRING', rel: 'STRING', url: `https://string-db.org/newstring_cgi/show_network_section.pl?identifiers=${moleculeKey}`, - icon: 'https://www.gene-list.com/build/images/string.svg' + icon: '/static/frontend/img/genes-icons/STRING.webp' }, { source: 'IntAct', rel: 'IntAct', url: `https://www.ebi.ac.uk/intact/search?query=${moleculeKey}`, - icon: 'https://www.gene-list.com/build/images/intact.svg' + icon: '/static/frontend/img/genes-icons/IntAct.webp' }, { source: 'Interactome Atlas', rel: 'Interactome Atlas', url: `http://www.interactome-atlas.org/search/${moleculeKey}`, - icon: 'https://www.gene-list.com/build/images/interactomeatlas.svg' + icon: '/static/frontend/img/genes-icons/Interactome-Atlas.webp' }, { source: 'Complex Portal', rel: 'Complex Portal', url: `https://www.ebi.ac.uk/complexportal/complex/search?query=${moleculeKey}`, - icon: 'https://www.gene-list.com/build/images/complex_portal.svg' + icon: '/static/frontend/img/genes-icons/ComplexPortal.webp' }, { source: 'BioGrid', rel: 'BioGrid', url: `https://thebiogrid.org//summary/homo-sapiens/${moleculeKey}.html`, - icon: 'https://www.gene-list.com/build/images/biogrid.svg' + icon: '/static/frontend/img/genes-icons/BioGrid.webp' }, { source: 'DGV', rel: 'DGV', url: `http://dgv.tcag.ca/gb2/gbrowse/dgv2_hg19/?name=${moleculeKey};search=Search`, - icon: 'https://www.gene-list.com/build/images/dgv.svg' + icon: '/static/frontend/img/genes-icons/DGV.webp' }, { source: 'ClinVar', rel: 'ClinVar', url: `https://www.ncbi.nlm.nih.gov/clinvar/?term=${moleculeKey}%5Bgene%5D&redir=gene`, - icon: 'https://www.gene-list.com/build/images/ncbi.svg' + icon: '/static/frontend/img/genes-icons/ClinVar.webp' }, { source: 'DGIDB', rel: 'DGIDB', url: `https://www.dgidb.org/genes/${moleculeKey}#_interactions`, - icon: 'https://www.gene-list.com/build/images/dgibd.svg' + icon: '/static/frontend/img/genes-icons/dgidb.webp' }, { source: 'NCBI', rel: 'NCBI', url: `https://www.ncbi.nlm.nih.gov/gene/${jsonResponse.data[moleculeKey].entrez_id}`, - icon: 'https://www.gene-list.com/build/images/ncbi.svg' + icon: '/static/frontend/img/genes-icons/NCBI.webp' }, { source: 'Ensembl', rel: 'Ensembl', url: `http://www.ensembl.org/id/${jsonResponse.data[moleculeKey].ensembl_gene_id}`, - icon: 'https://www.gene-list.com/build/images/ensembl.svg' + icon: '/static/frontend/img/genes-icons/Ensembl.webp' }, { source: 'UCSC Browser', rel: 'UCSC Browser', url: `http://genome.ucsc.edu/cgi-bin/hgTracks?db=hg38&singleSearch=knownCanonical&position=${moleculeKey}`, - icon: 'https://www.gene-list.com/build/images/ucsc.svg' + icon: '/static/frontend/img/genes-icons/UCSCBrowser.webp' }, { source: 'Uniprot', rel: 'Uniprot', url: `http://www.uniprot.org/uniprot/${jsonResponse.data[moleculeKey].uniprot_ids}`, - icon: 'https://www.gene-list.com/build/images/uniprot.svg' + icon: '/static/frontend/img/genes-icons/Uniprot.webp' }, { source: 'Human Protein Atlas', rel: 'Human Protein Atlas', url: `https://www.proteinatlas.org/${jsonResponse.data[moleculeKey].uniprot_ids}`, - icon: 'https://www.gene-list.com/build/images/hpa.svg' + icon: '/static/frontend/img/genes-icons/Human-Protein-Atlas.webp' }, { source: 'Alliance Genome', rel: 'Alliance Genome', url: `https://www.proteinatlas.org/${jsonResponse.data[moleculeKey].hgnc_id}`, - icon: 'https://www.gene-list.com/build/images/alliance_genome_human.svg' + icon: '/static/frontend/img/genes-icons/Alliance-Genome.webp' }, { source: 'HGNC', rel: 'HGNC', url: `https://www.genenames.org/data/gene-symbol-report/#!/hgnc_id/${jsonResponse.data[moleculeKey].hgnc_id}`, - icon: 'https://www.gene-list.com/build/images/hgnc.svg' + icon: '/static/frontend/img/genes-icons/HGNC.webp' }, { source: 'Monarch', rel: 'Monarch', url: `https://monarchinitiative.org/gene/${jsonResponse.data[moleculeKey].hgnc_id}`, - icon: 'https://www.gene-list.com/build/images/monarch.svg' + icon: '/static/frontend/img/genes-icons/Monarch.webp' }, { source: 'HI-C Browser', rel: 'HI-C Browser', url: `http://3dgenome.fsm.northwestern.edu/view.php?method=Hi-C&species=human&assembly=hg38&source=inside&tissue=GM12878&type=Rao_2014-raw&c_url=&transfer=&gene=${jsonResponse.data[moleculeKey].uniprot_ids}&sessionID=&browser=none`, - icon: 'https://www.gene-list.com/build/images/hi_c_browser.svg' + icon: '/static/frontend/img/genes-icons/HI-CBrowser.webp' }, { source: 'InterPro', rel: 'InterPro', url: `https://www.ebi.ac.uk/interpro/protein/UniProt/${jsonResponse.data[moleculeKey].hgnc_id}`, - icon: 'https://www.gene-list.com/build/images/interpro.svg' + icon: '/static/frontend/img/genes-icons/InterPro.webp' }, { source: 'PDB', rel: 'PDB', url: `https://www.rcsb.org/search?request=%7B%22query%22%3A%7B%22type%22%3A%22group%22%2C%22logical_operator%22%3A%22and%22%2C%22nodes%22%3A%5B%7B%22type%22%3A%22group%22%2C%22logical_operator%22%3A%22and%22%2C%22nodes%22%3A%5B%7B%22type%22%3A%22group%22%2C%22nodes%22%3A%5B%7B%22type%22%3A%22terminal%22%2C%22service%22%3A%22full_text%22%2C%22parameters%22%3A%7B%22value%22%3A%22${moleculeKey}%22%7D%7D%5D%2C%22logical_operator%22%3A%22and%22%7D%5D%2C%22label%22%3A%22full_text%22%7D%5D%7D%2C%22return_type%22%3A%22entry%22%2C%22request_options%22%3A%7B%22pager%22%3A%7B%22start%22%3A0%2C%22rows%22%3A25%7D%2C%22scoring_strategy%22%3A%22combined%22%2C%22sort%22%3A%5B%7B%22sort_by%22%3A%22score%22%2C%22direction%22%3A%22desc%22%7D%5D%7D%2C%22request_info%22%3A%7B%22query_id%22%3A%225a4bea3ded0ab9c9fd19c0405097e1b2%22%7D%7D`, - icon: 'https://www.gene-list.com/build/images/pdb.svg' + icon: '/static/frontend/img/genes-icons/PDB.webp' }, { source: 'AlphaFold', rel: 'AlphaFold', url: `https://alphafold.ebi.ac.uk/entry/${jsonResponse.data[moleculeKey].uniprot_ids}`, - icon: 'https://www.gene-list.com/build/images/alphafold.svg' + icon: '/static/frontend/img/genes-icons/AlphaFold.webp' }, { source: 'Amigo', rel: 'Amigo', url: `http://amigo.geneontology.org/amigo/gene_product/UniProtKB:${jsonResponse.data[moleculeKey].uniprot_ids}`, - icon: 'https://www.gene-list.com/build/images/amigo.svg' + icon: '/static/frontend/img/genes-icons/Amigo.webp' }, { source: 'BioPlex', rel: 'BioPlex', url: `https://bioplex.hms.harvard.edu/explorer/externalQuery.php?geneQuery=${jsonResponse.data[moleculeKey].entrez_id}`, - icon: 'https://www.gene-list.com/build/images/bioplex.svg' + icon: '/static/frontend/img/genes-icons/BioPlex.webp' }, { source: 'GTEX', rel: 'GTEX', url: `https://www.gtexportal.org/home/gene/${jsonResponse.data[moleculeKey].ensembl_gene_id}`, - icon: 'https://www.gene-list.com/build/images/gtex.svg' + icon: '/static/frontend/img/genes-icons/GTEX.webp' }, { source: 'GDC', rel: 'GDC', url: `https://portal.gdc.cancer.gov/genes/${jsonResponse.data[moleculeKey].ensembl_gene_id}`, - icon: 'https://www.gene-list.com/build/images/tcga.svg' + icon: '/static/frontend/img/genes-icons/GDC.webp' }, { source: 'ICGC', rel: 'ICGC', url: `https://dcc.icgc.org/genes/${jsonResponse.data[moleculeKey].ensembl_gene_id}`, - icon: 'https://www.gene-list.com/build/images/pcawg.svg' + icon: '/static/frontend/img/genes-icons/ICGC.webp' }, { source: 'cBioPortal', rel: 'cBioPortal', url: `http://www.cbioportal.org/results/cancerTypesSummary?cancer_study_list=laml_tcga_pan_can_atlas_2018%2Cacc_tcga_pan_can_atlas_2018%2Cblca_tcga_pan_can_atlas_2018%2Clgg_tcga_pan_can_atlas_2018%2Cbrca_tcga_pan_can_atlas_2018%2Ccesc_tcga_pan_can_atlas_2018%2Cchol_tcga_pan_can_atlas_2018%2Ccoadread_tcga_pan_can_atlas_2018%2Cdlbc_tcga_pan_can_atlas_2018%2Cesca_tcga_pan_can_atlas_2018%2Cgbm_tcga_pan_can_atlas_2018%2Chnsc_tcga_pan_can_atlas_2018%2Ckich_tcga_pan_can_atlas_2018%2Ckirc_tcga_pan_can_atlas_2018%2Ckirp_tcga_pan_can_atlas_2018%2Clihc_tcga_pan_can_atlas_2018%2Cluad_tcga_pan_can_atlas_2018%2Clusc_tcga_pan_can_atlas_2018%2Cmeso_tcga_pan_can_atlas_2018%2Cov_tcga_pan_can_atlas_2018%2Cpaad_tcga_pan_can_atlas_2018%2Cpcpg_tcga_pan_can_atlas_2018%2Cprad_tcga_pan_can_atlas_2018%2Csarc_tcga_pan_can_atlas_2018%2Cskcm_tcga_pan_can_atlas_2018%2Cstad_tcga_pan_can_atlas_2018%2Ctgct_tcga_pan_can_atlas_2018%2Cthym_tcga_pan_can_atlas_2018%2Cthca_tcga_pan_can_atlas_2018%2Cucs_tcga_pan_can_atlas_2018%2Cucec_tcga_pan_can_atlas_2018%2Cuvm_tcga_pan_can_atlas_2018&Z_SCORE_THRESHOLD=2.0&RPPA_SCORE_THRESHOLD=2.0&profileFilter=mutations%2Cfusion%2Cgistic&case_set_id=all&gene_list=${moleculeKey}&geneset_list=%20&tab_index=tab_visualize&Action=Submit`, - icon: 'https://www.gene-list.com/build/images/cbioportal.svg' + icon: '/static/frontend/img/genes-icons/cBioPortal.webp' }, { source: 'OMIM', rel: 'OMIM', url: `https://omim.org/entry/616125${jsonResponse.data[moleculeKey].omim_id}`, - icon: 'https://www.gene-list.com/build/images/omim.svg' + icon: '/static/frontend/img/genes-icons/OMIM.webp' }, { source: 'HPO', rel: 'HPO', url: `https://hpo.jax.org/app/browse/gene/${jsonResponse.data[moleculeKey].entrez_id}`, - icon: 'https://www.gene-list.com/build/images/hpo.svg' + icon: '/static/frontend/img/genes-icons/HPO.webp' }, { source: 'Open Targets', rel: 'Open Targets', url: `https://genetics.opentargets.org/gene/ENSG00000164169${jsonResponse.data[moleculeKey].ensembl_gene_id}`, - icon: 'https://www.gene-list.com/build/images/open_targets.svg' + icon: '/static/frontend/img/genes-icons/Open-Targets.webp' }, { source: 'Expression Atlas', rel: 'Expression Atlas', url: `https://www.ebac.uk/gxa/search?geneQuery=%5B%7B%22value%22%3A%22PRMT9%22%7D%5D&species=&conditionQuery=%5B%5D&bs=%7B%22homo%20sapiens%22%3A%5B%22ORGANISM_PART%22%5D%2C%22chlorocebus%20sabaeus%22%3A%5B%22ORGANISM_PART%22%5D%2C%22danio%20rerio%22%3A%5B%22DEVELOPMENTAL_STAGE%22%5D%2C%22equus%20caballus%22%3A%5B%22ORGANISM_PART%22%5D%2C%22gallus%20gallus%22%3A%5B%22ORGANISM_PART%22%5D%2C%22mus%20musculus%22%3A%5B%22ORGANISM_PART%22%5D%2C%22papio%20anubis%22%3A%5B%22ORGANISM_PART%22%5D%2C%22rattus%20norvegicus%22%3A%5B%22ORGANISM_PART%22%5D%7D&ds=%7B%22kingdom%22%3A%5B%22animals%22%5D%7D#baseline${jsonResponse.data[moleculeKey].entrez_id}`, - icon: 'https://www.gene-list.com/build/images/expression_atlas.svg' + icon: '/static/frontend/img/genes-icons/ExpressionAtlas.webp' }, { source: 'GWAS Catalog', rel: 'GWAS Catalog', url: `https://www.ebi.ac.uk/gwas/search?query=${moleculeKey}`, - icon: 'https://www.gene-list.com/build/images/gwas.svg' + icon: '/static/frontend/img/genes-icons/GWAS-Catalog.webp' }, { source: 'ActiveDriverDb', rel: 'ActiveDriverDb', url: `https://activedriverdb.org/gene/show/${moleculeKey}`, - icon: 'https://www.gene-list.com/build/images/activedriverdb.svg' + icon: '/static/frontend/img/genes-icons/ActiveDriverDb.webp' }, { source: 'Pharmgkb', rel: 'Pharmgkb', url: `https://www.pharmgkb.org/search?query=${moleculeKey}`, - icon: 'https://www.gene-list.com/build/images/pharmgkb.svg' + icon: '/static/frontend/img/genes-icons/Pharmgkb.webp' } ] setLinksData(linksData) From a8c6ecd25142aacc7e37aa18aeccb15c62c08c7a Mon Sep 17 00:00:00 2001 From: Hernan Date: Thu, 19 Dec 2024 12:48:59 -0300 Subject: [PATCH 14/27] add help info in frontend --- .../trained-models/NewRFModelForm.tsx | 52 +++++++++++++++++-- .../trained-models/NewSVMModelForm.tsx | 45 ++++++++++++++-- .../trained-models/NewTrainedModelModal.tsx | 14 ++++- .../src/components/biomarkers/utils.ts | 22 ++++---- 4 files changed, 111 insertions(+), 22 deletions(-) diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/trained-models/NewRFModelForm.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/trained-models/NewRFModelForm.tsx index f77a3136..db9b737d 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/trained-models/NewRFModelForm.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/trained-models/NewRFModelForm.tsx @@ -1,6 +1,8 @@ import React from 'react' import { Form, InputOnChangeData } from 'semantic-ui-react' import { RFParameters } from '../../types' +import { InfoPopup } from '../../../pipeline/experiment-result/gene-gem-details/InfoPopup' +import { InputLabel } from '../../../common/InputLabel' interface NewSVMModelFormProps { /** Getter of the selected params to handle in the form. */ @@ -16,18 +18,38 @@ export const NewRFModelForm = (props: NewSVMModelFormProps) => { const lookForOptimalNEstimators = props.parameters.lookForOptimalNEstimators return ( <> - {/* TODO: add InfoPopup */} + { props.handleChangeOptimalNEstimators(checked ?? false) }} - label='Search for the optimal number of trees' + label={ + + + + } /> {!lookForOptimalNEstimators && + + + } type='number' min={10} max={20} @@ -40,7 +62,17 @@ export const NewRFModelForm = (props: NewSVMModelFormProps) => { + + + } placeholder='An integer number' type='number' min={3} @@ -52,7 +84,17 @@ export const NewRFModelForm = (props: NewSVMModelFormProps) => { + + + } placeholder='An integer number' type='number' step={1} diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/trained-models/NewSVMModelForm.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/trained-models/NewSVMModelForm.tsx index 2451fce9..07de421d 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/trained-models/NewSVMModelForm.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/trained-models/NewSVMModelForm.tsx @@ -2,6 +2,8 @@ import React from 'react' import { Form, InputOnChangeData } from 'semantic-ui-react' import { SVMKernelOptions } from '../../utils' import { SVMParameters } from '../../types' +import { InfoPopup } from '../../../pipeline/experiment-result/gene-gem-details/InfoPopup' +import { InputLabel } from '../../../common/InputLabel' interface NewSVMModelFormProps { /** Getter of the selected params to handle in the form. */ @@ -10,15 +12,28 @@ interface NewSVMModelFormProps { handleChangeParams: (event: React.ChangeEvent, data: InputOnChangeData) => void } +const SVMKernel = SVMKernelOptions.map(({ description, ...rest }) => rest) + export const NewSVMModelForm = (props: NewSVMModelFormProps) => { - // TODO: add an InfoPopup for all the inputs + const selectedKernel = SVMKernelOptions.find(option => option.value === props.parameters.kernel) + const kernelDescription = selectedKernel ? selectedKernel.description : 'No description available' return ( <> + + + } + options={SVMKernel} placeholder='Select a kernel' name='kernel' value={props.parameters.kernel} @@ -28,7 +43,17 @@ export const NewSVMModelForm = (props: NewSVMModelFormProps) => { + + + } placeholder='100-2000' name='maxIterations' value={props.parameters.maxIterations ?? ''} @@ -37,7 +62,17 @@ export const NewSVMModelForm = (props: NewSVMModelFormProps) => { + + + } placeholder='An integer number' type='number' step={1} diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/trained-models/NewTrainedModelModal.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/trained-models/NewTrainedModelModal.tsx index e4d4fb5d..3c4e01de 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/trained-models/NewTrainedModelModal.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/trained-models/NewTrainedModelModal.tsx @@ -10,6 +10,8 @@ import { DjangoCGDSStudy, DjangoUserFile } from '../../../../utils/django_interf import ky from 'ky' import { NewClusteringModelForm } from './NewClusteringModelForm' import { NewRFModelForm } from './NewRFModelForm' +import { InfoPopup } from '../../../pipeline/experiment-result/gene-gem-details/InfoPopup' +import { InputLabel } from '../../../common/InputLabel' declare const urlNewTrainedModel: string @@ -437,7 +439,17 @@ export const NewTrainedModelModal = (props: NewTrainedModelModalProps) => { + + + } placeholder='An integer number' type='number' step={1} diff --git a/src/frontend/static/frontend/src/components/biomarkers/utils.ts b/src/frontend/static/frontend/src/components/biomarkers/utils.ts index 3e525cfd..00dcc169 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/utils.ts +++ b/src/frontend/static/frontend/src/components/biomarkers/utils.ts @@ -28,29 +28,29 @@ const fitnessFunctionsOptions: DropdownItemProps[] = [ /** Available options for a SVM kernel. */ const SVMKernelOptions: DropdownItemProps[] = [ - { key: SVMKernel.LINEAR, text: 'Linear', value: SVMKernel.LINEAR }, - { key: SVMKernel.POLYNOMIAL, text: 'Polynomial', value: SVMKernel.POLYNOMIAL }, - { key: SVMKernel.RBF, text: 'RBF', value: SVMKernel.RBF } + { key: SVMKernel.LINEAR, text: 'Linear', value: SVMKernel.LINEAR, description: 'Linear Kernel: Best for linearly separable data; commonly used for simple genomic or clinical feature classification.' }, + { key: SVMKernel.POLYNOMIAL, text: 'Polynomial', value: SVMKernel.POLYNOMIAL, description: 'Polynomial Kernel: Captures non-linear patterns; effective for complex relationships in multi-omics data.' }, + { key: SVMKernel.RBF, text: 'RBF', value: SVMKernel.RBF, description: 'RBF Kernel: Maps data to a higher-dimensional space; ideal for handling non-linear separations in RNA and methylation analyses.' } ] /** Available options for a Clustering algorithm. */ const clusteringAlgorithmOptions: DropdownItemProps[] = [ - { key: ClusteringAlgorithm.K_MEANS, text: 'K-Means', value: ClusteringAlgorithm.K_MEANS }, - { key: ClusteringAlgorithm.SPECTRAL, text: 'Spectral', value: ClusteringAlgorithm.SPECTRAL }, - { key: ClusteringAlgorithm.BK_MEANS, text: 'BK-Means', value: ClusteringAlgorithm.BK_MEANS }, - { key: ClusteringAlgorithm.WARD, text: 'Ward', value: ClusteringAlgorithm.WARD } + { key: ClusteringAlgorithm.K_MEANS, text: 'K-Means', value: ClusteringAlgorithm.K_MEANS, description: 'K-Means: Groups data by minimizing intra-cluster variance; effective for clustering RNA and miRNA expression profiles.' }, + { key: ClusteringAlgorithm.SPECTRAL, text: 'Spectral', value: ClusteringAlgorithm.SPECTRAL, description: 'Spectral Clustering: Uses graph-based similarity to identify complex patterns; ideal for integrating methylation and CNA data.' }, + { key: ClusteringAlgorithm.BK_MEANS, text: 'BK-Means', value: ClusteringAlgorithm.BK_MEANS, description: 'BK-Means: A hierarchical variation of K-Means, suitable for layered clustering of clinical and multi-omics datasets.' }, + { key: ClusteringAlgorithm.WARD, text: 'Ward', value: ClusteringAlgorithm.WARD, description: 'Ward’s Method: Minimizes variance in hierarchical clustering; well-suited for combining RNA and methylation data in integrated analyses.' } ] /** Available options for a Clustering metric to optimize. */ const clusteringMetricOptions: DropdownItemProps[] = [ - { key: ClusteringMetric.COX_REGRESSION, text: 'Cox-Regression', value: ClusteringMetric.COX_REGRESSION }, - { key: ClusteringMetric.LOG_RANK_TEST, text: 'Log-Rank test', value: ClusteringMetric.LOG_RANK_TEST, disabled: true } // TODO: implement in backend + { key: ClusteringMetric.COX_REGRESSION, text: 'Cox-Regression', value: ClusteringMetric.COX_REGRESSION, description: 'Cox Regression: A proportional hazards model to identify associations between multi-omics features (RNA, miRNA, methylation) and clinical outcomes over time.' }, + { key: ClusteringMetric.LOG_RANK_TEST, text: 'Log-Rank test', value: ClusteringMetric.LOG_RANK_TEST, disabled: true, description: 'Log-Rank Test: A statistical test to compare survival distributions; ideal for evaluating the impact of CNA and methylation patterns on patient prognosis.' } ] /** Available options for a Clustering scoring method for Cox-Regression. */ const clusteringScoringMethodOptions: DropdownItemProps[] = [ - { key: ClusteringScoringMethod.C_INDEX, text: 'C-Index', value: ClusteringScoringMethod.C_INDEX }, - { key: ClusteringScoringMethod.LOG_LIKELIHOOD, text: 'Log likelihood', value: ClusteringScoringMethod.LOG_LIKELIHOOD } + { key: ClusteringScoringMethod.C_INDEX, text: 'C-Index', value: ClusteringScoringMethod.C_INDEX, description: 'C-Index: A measure of concordance between predicted and observed survival outcomes; higher values indicate better model performance.' }, + { key: ClusteringScoringMethod.LOG_LIKELIHOOD, text: 'Log likelihood', value: ClusteringScoringMethod.LOG_LIKELIHOOD, description: 'Log Likelihood: A measure of model fit; lower values indicate better agreement between predicted and observed survival outcomes.' } ] /** Available options for a Biomarker's state. */ From 764ca302a65d88ce83816deb0260bac74604d1a0 Mon Sep 17 00:00:00 2001 From: Hernan Date: Fri, 20 Dec 2024 17:09:42 -0300 Subject: [PATCH 15/27] fix InfoPopup Clustering and SVM Form Model --- .../trained-models/NewClusteringModelForm.tsx | 79 +++++++++++++++++-- .../trained-models/NewSVMModelForm.tsx | 14 ++-- .../src/components/biomarkers/utils.ts | 22 +++--- 3 files changed, 91 insertions(+), 24 deletions(-) diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/trained-models/NewClusteringModelForm.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/trained-models/NewClusteringModelForm.tsx index 54faf816..d4cd21a2 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/trained-models/NewClusteringModelForm.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/trained-models/NewClusteringModelForm.tsx @@ -15,13 +15,29 @@ interface NewClusteringModelFormProps { } export const NewClusteringModelForm = (props: NewClusteringModelFormProps) => { - // TODO: add an InfoPopup for all the inputs return ( <> + +

K-Means: Groups data by minimizing intra-cluster variance; effective for clustering RNA and miRNA expression profiles.

+

Spectral Clustering: Uses graph-based similarity to identify complex patterns; ideal for integrating methylation and CNA data.

+

BK-Means: A hierarchical variation of K-Means, suitable for layered clustering of clinical and multi-omics datasets.

+

Ward’s Method: Minimizes variance in hierarchical clustering; well-suited for combining RNA and methylation data in integrated analyses.

+ + } + onTop={false} + onEvent='hover' + noBorder + extraClassName='pull-right' + /> + + } options={clusteringAlgorithmOptions} placeholder='Select an algorithm' name='algorithm' @@ -29,7 +45,6 @@ export const NewClusteringModelForm = (props: NewClusteringModelFormProps) => { onChange={props.handleChangeParams} /> - {/* TODO: add InfoPopup */} { props.handleChangeOptimalNClusters(checked ?? false) }} @@ -39,7 +54,17 @@ export const NewClusteringModelForm = (props: NewClusteringModelFormProps) => { {!props.parameters.lookForOptimalNClusters && + + + } name='nClusters' min={2} max={10} @@ -51,7 +76,22 @@ export const NewClusteringModelForm = (props: NewClusteringModelFormProps) => { + +

Cox Regression: A proportional hazards model to identify associations between multi-omics features (RNA, miRNA, methylation) and clinical outcomes over time.

+

Log-Rank Test: A non-parametric test to compare the survival distributions of two or more groups; currently not available.

+ + } + onTop={false} + onEvent='hover' + noBorder + extraClassName='pull-right' + /> + + } options={clusteringMetricOptions} placeholder='Select a metric' name='metric' @@ -64,7 +104,22 @@ export const NewClusteringModelForm = (props: NewClusteringModelFormProps) => { + +

C-Index: A measure of concordance between predicted and observed survival outcomes; higher values indicate better model performance.

+

Log Likelihood: The probability of observing the data given the model; lower values indicate better model performance.

+ + } + onTop={false} + onEvent='hover' + noBorder + extraClassName='pull-right' + /> + + } options={clusteringScoringMethodOptions} placeholder='Select a method' name='scoringMethod' @@ -75,7 +130,17 @@ export const NewClusteringModelForm = (props: NewClusteringModelFormProps) => { + + + } placeholder='An integer number' type='number' step={1} diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/trained-models/NewSVMModelForm.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/trained-models/NewSVMModelForm.tsx index 07de421d..ed0e07b2 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/trained-models/NewSVMModelForm.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/trained-models/NewSVMModelForm.tsx @@ -12,11 +12,7 @@ interface NewSVMModelFormProps { handleChangeParams: (event: React.ChangeEvent, data: InputOnChangeData) => void } -const SVMKernel = SVMKernelOptions.map(({ description, ...rest }) => rest) - export const NewSVMModelForm = (props: NewSVMModelFormProps) => { - const selectedKernel = SVMKernelOptions.find(option => option.value === props.parameters.kernel) - const kernelDescription = selectedKernel ? selectedKernel.description : 'No description available' return ( <> { label={ +

Linear Kernel: Best for linearly separable data; commonly used for simple genomic or clinical feature classification.

+

Polynomial Kernel: Captures non-linear patterns; effective for complex relationships in multi-omics data.

+

RBF Kernel: Maps data to a higher-dimensional space; ideal for handling non-linear separations in RNA and methylation analyses.

+ + } onTop={false} onEvent='hover' noBorder @@ -33,7 +35,7 @@ export const NewSVMModelForm = (props: NewSVMModelFormProps) => { />
} - options={SVMKernel} + options={SVMKernelOptions} placeholder='Select a kernel' name='kernel' value={props.parameters.kernel} diff --git a/src/frontend/static/frontend/src/components/biomarkers/utils.ts b/src/frontend/static/frontend/src/components/biomarkers/utils.ts index 00dcc169..cbc13eca 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/utils.ts +++ b/src/frontend/static/frontend/src/components/biomarkers/utils.ts @@ -28,29 +28,29 @@ const fitnessFunctionsOptions: DropdownItemProps[] = [ /** Available options for a SVM kernel. */ const SVMKernelOptions: DropdownItemProps[] = [ - { key: SVMKernel.LINEAR, text: 'Linear', value: SVMKernel.LINEAR, description: 'Linear Kernel: Best for linearly separable data; commonly used for simple genomic or clinical feature classification.' }, - { key: SVMKernel.POLYNOMIAL, text: 'Polynomial', value: SVMKernel.POLYNOMIAL, description: 'Polynomial Kernel: Captures non-linear patterns; effective for complex relationships in multi-omics data.' }, - { key: SVMKernel.RBF, text: 'RBF', value: SVMKernel.RBF, description: 'RBF Kernel: Maps data to a higher-dimensional space; ideal for handling non-linear separations in RNA and methylation analyses.' } + { key: SVMKernel.LINEAR, text: 'Linear', value: SVMKernel.LINEAR }, + { key: SVMKernel.POLYNOMIAL, text: 'Polynomial', value: SVMKernel.POLYNOMIAL }, + { key: SVMKernel.RBF, text: 'RBF', value: SVMKernel.RBF } ] /** Available options for a Clustering algorithm. */ const clusteringAlgorithmOptions: DropdownItemProps[] = [ - { key: ClusteringAlgorithm.K_MEANS, text: 'K-Means', value: ClusteringAlgorithm.K_MEANS, description: 'K-Means: Groups data by minimizing intra-cluster variance; effective for clustering RNA and miRNA expression profiles.' }, - { key: ClusteringAlgorithm.SPECTRAL, text: 'Spectral', value: ClusteringAlgorithm.SPECTRAL, description: 'Spectral Clustering: Uses graph-based similarity to identify complex patterns; ideal for integrating methylation and CNA data.' }, - { key: ClusteringAlgorithm.BK_MEANS, text: 'BK-Means', value: ClusteringAlgorithm.BK_MEANS, description: 'BK-Means: A hierarchical variation of K-Means, suitable for layered clustering of clinical and multi-omics datasets.' }, - { key: ClusteringAlgorithm.WARD, text: 'Ward', value: ClusteringAlgorithm.WARD, description: 'Ward’s Method: Minimizes variance in hierarchical clustering; well-suited for combining RNA and methylation data in integrated analyses.' } + { key: ClusteringAlgorithm.K_MEANS, text: 'K-Means', value: ClusteringAlgorithm.K_MEANS }, + { key: ClusteringAlgorithm.SPECTRAL, text: 'Spectral', value: ClusteringAlgorithm.SPECTRAL }, + { key: ClusteringAlgorithm.BK_MEANS, text: 'BK-Means', value: ClusteringAlgorithm.BK_MEANS }, + { key: ClusteringAlgorithm.WARD, text: 'Ward', value: ClusteringAlgorithm.WARD } ] /** Available options for a Clustering metric to optimize. */ const clusteringMetricOptions: DropdownItemProps[] = [ - { key: ClusteringMetric.COX_REGRESSION, text: 'Cox-Regression', value: ClusteringMetric.COX_REGRESSION, description: 'Cox Regression: A proportional hazards model to identify associations between multi-omics features (RNA, miRNA, methylation) and clinical outcomes over time.' }, - { key: ClusteringMetric.LOG_RANK_TEST, text: 'Log-Rank test', value: ClusteringMetric.LOG_RANK_TEST, disabled: true, description: 'Log-Rank Test: A statistical test to compare survival distributions; ideal for evaluating the impact of CNA and methylation patterns on patient prognosis.' } + { key: ClusteringMetric.COX_REGRESSION, text: 'Cox-Regression', value: ClusteringMetric.COX_REGRESSION }, + { key: ClusteringMetric.LOG_RANK_TEST, text: 'Log-Rank test', value: ClusteringMetric.LOG_RANK_TEST, disabled: true } ] /** Available options for a Clustering scoring method for Cox-Regression. */ const clusteringScoringMethodOptions: DropdownItemProps[] = [ - { key: ClusteringScoringMethod.C_INDEX, text: 'C-Index', value: ClusteringScoringMethod.C_INDEX, description: 'C-Index: A measure of concordance between predicted and observed survival outcomes; higher values indicate better model performance.' }, - { key: ClusteringScoringMethod.LOG_LIKELIHOOD, text: 'Log likelihood', value: ClusteringScoringMethod.LOG_LIKELIHOOD, description: 'Log Likelihood: A measure of model fit; lower values indicate better agreement between predicted and observed survival outcomes.' } + { key: ClusteringScoringMethod.C_INDEX, text: 'C-Index', value: ClusteringScoringMethod.C_INDEX }, + { key: ClusteringScoringMethod.LOG_LIKELIHOOD, text: 'Log likelihood', value: ClusteringScoringMethod.LOG_LIKELIHOOD } ] /** Available options for a Biomarker's state. */ From 8c187897600091124df877b5985c2b64264580e2 Mon Sep 17 00:00:00 2001 From: Hernan Date: Thu, 2 Jan 2025 17:38:12 -0300 Subject: [PATCH 16/27] Add Django Migraritions DB --- ...clusteringparameters_algorithm_and_more.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/feature_selection/migrations/0057_alter_clusteringparameters_algorithm_and_more.py diff --git a/src/feature_selection/migrations/0057_alter_clusteringparameters_algorithm_and_more.py b/src/feature_selection/migrations/0057_alter_clusteringparameters_algorithm_and_more.py new file mode 100644 index 00000000..514406b9 --- /dev/null +++ b/src/feature_selection/migrations/0057_alter_clusteringparameters_algorithm_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.15 on 2025-01-02 20:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('feature_selection', '0056_alter_clusteringparameters_algorithm_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='clusteringparameters', + name='algorithm', + field=models.IntegerField(choices=[(1, 'K Means'), (2, 'Spectral'), (3, 'Bk Means'), (4, 'Ward')], default=1), + ), + migrations.AlterField( + model_name='clusteringtimesrecord', + name='algorithm', + field=models.IntegerField(choices=[(1, 'K Means'), (2, 'Spectral'), (3, 'Bk Means'), (4, 'Ward')]), + ), + ] From 3ea55cbd7328b648da72892e0cad2b03ebfc4260 Mon Sep 17 00:00:00 2001 From: Genaro Camele Date: Fri, 3 Jan 2025 15:09:00 -0300 Subject: [PATCH 17/27] Added missing type --- src/biomarkers/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/biomarkers/models.py b/src/biomarkers/models.py index 14857468..faf254b3 100644 --- a/src/biomarkers/models.py +++ b/src/biomarkers/models.py @@ -57,6 +57,7 @@ class Biomarker(models.Model): statistical_validations: QuerySet['statistical_properties.StatisticalValidation'] inference_experiments: QuerySet['inferences.InferenceExperiment'] methylations: QuerySet['MethylationIdentifier'] + trained_models: QuerySet['trained_models.TrainedModel'] cnas: QuerySet['CNAIdentifier'] mirnas: QuerySet['MiRNAIdentifier'] mrnas: QuerySet['MRNAIdentifier'] From 04e2473c43afe42f31344eff01f35b948f97d170 Mon Sep 17 00:00:00 2001 From: Juan Date: Mon, 6 Jan 2025 07:49:03 -0300 Subject: [PATCH 18/27] save institution, switch role --- src/api_service/websocket_functions.py | 22 ++ .../components/biomarkers/BiomarkersPanel.tsx | 8 +- .../src/components/biomarkers/types.ts | 8 - .../institutions/InstitutionForm.tsx | 189 +++++++--- .../institutions/InstitutionModal.tsx | 144 +++++--- .../institutions/InstitutionUsersInfo.tsx | 65 ---- .../institutions/InstitutionsList.tsx | 51 --- .../institutions/InstitutionsPanel.tsx | 345 +++++++++--------- .../frontend/src/utils/django_interfaces.ts | 17 +- .../static/frontend/src/utils/interfaces.ts | 11 +- .../src/websockets/WebsocketClient.ts | 7 +- .../templates/frontend/institutions.html | 6 +- src/institutions/models.py | 40 ++ src/institutions/serializers.py | 32 +- src/institutions/urls.py | 12 +- src/institutions/views.py | 177 +++++++-- src/statistical_properties/views.py | 2 + 17 files changed, 709 insertions(+), 427 deletions(-) delete mode 100644 src/frontend/static/frontend/src/components/institutions/InstitutionUsersInfo.tsx delete mode 100644 src/frontend/static/frontend/src/components/institutions/InstitutionsList.tsx diff --git a/src/api_service/websocket_functions.py b/src/api_service/websocket_functions.py index fec39e9f..237e34b9 100644 --- a/src/api_service/websocket_functions.py +++ b/src/api_service/websocket_functions.py @@ -106,3 +106,25 @@ def send_update_cluster_label_set_command(user_id: int): 'command': 'update_cluster_labels_sets' } send_message(user_group_name, message) + +def send_update_institutions_command(user_id: int): + """ + Sends a message indicating that a Institution state update has occurred + @param user_id: Institution's user's id to send the WS message + """ + user_group_name = f'notifications_{user_id}' + message = { + 'command': 'update_institutions' + } + send_message(user_group_name, message) + +def send_update_user_for_institution_command(user_id: int): + """ + Sends a message indicating that a Institution_user state update has occurred + @param user_id: Institution's user's id to send the WS message + """ + user_group_name = f'notifications_{user_id}' + message = { + 'command': 'update_user_for_institution' + } + send_message(user_group_name, message) \ No newline at end of file diff --git a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx index a3b58c97..8b0cd8cf 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx @@ -4,8 +4,8 @@ import { Header, Button, Modal, Table, DropdownItemProps, Icon, Confirm, Form } import { DjangoCGDSStudy, DjangoSurvivalColumnsTupleSimple, DjangoTag, DjangoUserFile, TagType } from '../../utils/django_interfaces' import ky, { Options } from 'ky' import { getDjangoHeader, alertGeneralError, formatDateLocale, cleanRef, getFilenameFromSource, makeSourceAndAppend, getDefaultSource } from '../../utils/util_functions' -import { NameOfCGDSDataset, Nullable, CustomAlert, CustomAlertTypes, SourceType, OkResponse } from '../../utils/interfaces' -import { Biomarker, BiomarkerType, BiomarkerOrigin, ConfirmModal, FormBiomarkerData, MoleculesSectionData, MoleculesTypeOfSelection, SaveBiomarkerStructure, SaveMoleculeStructure, FeatureSelectionPanelData, SourceStateBiomarker, FeatureSelectionAlgorithm, FitnessFunction, FitnessFunctionParameters, BiomarkerState, AdvancedAlgorithm as AdvancedAlgorithmParameters, BBHAVersion, BiomarkerSimple, CrossValidationParameters } from './types' +import { NameOfCGDSDataset, Nullable, CustomAlert, CustomAlertTypes, SourceType, OkResponse, ConfirmModal } from '../../utils/interfaces' +import { Biomarker, BiomarkerType, BiomarkerOrigin, FormBiomarkerData, MoleculesSectionData, MoleculesTypeOfSelection, SaveBiomarkerStructure, SaveMoleculeStructure, FeatureSelectionPanelData, SourceStateBiomarker, FeatureSelectionAlgorithm, FitnessFunction, FitnessFunctionParameters, BiomarkerState, AdvancedAlgorithm as AdvancedAlgorithmParameters, BBHAVersion, BiomarkerSimple, CrossValidationParameters } from './types' import { ManualForm } from './modalContentBiomarker/manualForm/ManualForm' import { PaginatedTable, PaginationCustomFilter } from '../common/PaginatedTable' import { TableCellWithTitle } from '../common/TableCellWithTitle' @@ -1704,7 +1704,7 @@ export class BiomarkersPanel extends React.Component<{}, BiomarkersPanelState> { {/* Stop button */} {isInProcess && this.setState({ biomarkerToStop: biomarker })} /> } @@ -1712,7 +1712,7 @@ export class BiomarkersPanel extends React.Component<{}, BiomarkersPanelState> { {/* Delete button */} {!isInProcess && this.confirmBiomarkerDeletion(biomarker)} /> diff --git a/src/frontend/static/frontend/src/components/biomarkers/types.ts b/src/frontend/static/frontend/src/components/biomarkers/types.ts index 20b78bb5..10e75d13 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/types.ts +++ b/src/frontend/static/frontend/src/components/biomarkers/types.ts @@ -99,13 +99,6 @@ interface Biomarker extends BiomarkerSimple { mrnas: SaveMoleculeStructure[] } -interface ConfirmModal { - confirmModal: boolean, - headerText: string, - contentText: string, - onConfirm: Function, -} - /** Represents a molecule info to show in molecules Dropdown. */ type MoleculeSymbol = { key: string, @@ -629,7 +622,6 @@ export { MoleculesMultipleSelection, MoleculesSectionData, MoleculeSectionItem, - ConfirmModal, MoleculeSymbol, MoleculesSymbolFinder, ClusteringScoringMethod, diff --git a/src/frontend/static/frontend/src/components/institutions/InstitutionForm.tsx b/src/frontend/static/frontend/src/components/institutions/InstitutionForm.tsx index a695678f..728870ee 100644 --- a/src/frontend/static/frontend/src/components/institutions/InstitutionForm.tsx +++ b/src/frontend/static/frontend/src/components/institutions/InstitutionForm.tsx @@ -1,74 +1,173 @@ -import React, { useState } from 'react' +import ky from 'ky' +import React, { useEffect, useState } from 'react' import { Form, Button } from 'semantic-ui-react' +import { getDjangoHeader } from '../../utils/util_functions' +import { CustomAlertTypes, Nullable } from '../../utils/interfaces' +import { DjangoInstitution } from '../../utils/django_interfaces' -const defaultForm = { +const defaultForm: { + id: undefined | number; + name: string; + location: string; + email: string; + telephone_number: string; + isLoading: boolean; +} = { + id: undefined, name: '', location: '', email: '', - phone: '', + telephone_number: '', isLoading: false } -const InstitutionForm = () => { +declare const urlCreateInstitution: string +declare const urlEditInstitution: string + + +interface Props { + institutionToEdit: Nullable, + handleResetInstitutionToEdit: (callbackToCancel: () => void) => void, + handleUpdateAlert(isOpen: boolean, type: CustomAlertTypes, message: string, callback: Nullable, isEdit?: boolean): void, +} + +const InstitutionForm = (props: Props) => { const [formData, setFormData] = useState(defaultForm) + /** + * Handle form state data + * @param e event + * @param param1 key value to edit field in form + */ const handleChange = (e, { name, value }) => { setFormData({ ...formData, [name]: value }) } + /** + * Handle user form to edit or create + */ const handleSubmit = () => { - console.log('Form Data Submitted:', formData) + const myHeaders = getDjangoHeader() + if (props.institutionToEdit?.id) { + const jsonParams = { + "id": formData.id, + "name": formData.name, + "location": formData.location, + "email": formData.email, + "telephone_number": formData.telephone_number + } + const editUrl = `${urlEditInstitution}/${formData.id}/` + + ky.patch(editUrl, { headers: myHeaders, json: jsonParams }).then((response) => { + setFormData(prevState => ({ ...prevState, isLoading: true })) + response.json().then((jsonResponse: DjangoInstitution) => { + props.handleUpdateAlert(true, CustomAlertTypes.SUCCESS, `Institution ${jsonResponse.name} Updated!`, () => setFormData(defaultForm),true) + }).catch((err) => { + setFormData(prevState => ({ ...prevState, isLoading: false })) + props.handleUpdateAlert(true, CustomAlertTypes.ERROR, 'Error creating an institution!', () => setFormData(prevState => ({ ...prevState, isLoading: false }))) + console.error('Error parsing JSON ->', err) + }) + }).catch((err) => { + props.handleUpdateAlert(true, CustomAlertTypes.ERROR, 'Error creating an institution!', () => setFormData(prevState => ({ ...prevState, isLoading: false }))) + console.error('Error adding new Institution ->', err) + }) + } else { + const jsonParams = { + "name": formData.name, + "location": formData.location, + "email": formData.email, + "telephone_number": formData.telephone_number + } + ky.post(urlCreateInstitution, { headers: myHeaders, json: jsonParams }).then((response) => { + setFormData(prevState => ({ ...prevState, isLoading: true })) + response.json().then((jsonResponse: DjangoInstitution) => { + //setFormData(defaultForm) + props.handleUpdateAlert(true, CustomAlertTypes.SUCCESS, `Institution ${jsonResponse.name} created!`, () => setFormData(defaultForm)) + }).catch((err) => { + props.handleUpdateAlert(true, CustomAlertTypes.ERROR, 'Error creating an institution!', () => setFormData(prevState => ({ ...prevState, isLoading: false }))) + setFormData(prevState => ({ ...prevState, isLoading: false })) + console.error('Error parsing JSON ->', err) + }) + }).catch((err) => { + props.handleUpdateAlert(true, CustomAlertTypes.ERROR, 'Error creating an institution!', () => setFormData(prevState => ({ ...prevState, isLoading: false }))) + console.error('Error adding new Institution ->', err) + }) + } } + /** + * Handle if user Reset or cancel edit + */ + const handleCancelForm = () => { + if (props.institutionToEdit) { + props.handleResetInstitutionToEdit(() => setFormData(defaultForm)) + } else { + setFormData(defaultForm) + } + } + /** + * use effect to handle if a institution for edit is sent + */ + useEffect(() => { + if (props.institutionToEdit) { + setFormData({ + ...props.institutionToEdit, + id: props.institutionToEdit?.id, + isLoading: false, + }) + } + }, [props.institutionToEdit]) return ( -
- - - - - + <> + + + + + + + - + ) } diff --git a/src/frontend/static/frontend/src/components/institutions/InstitutionModal.tsx b/src/frontend/static/frontend/src/components/institutions/InstitutionModal.tsx index e754a9e0..81b5badc 100644 --- a/src/frontend/static/frontend/src/components/institutions/InstitutionModal.tsx +++ b/src/frontend/static/frontend/src/components/institutions/InstitutionModal.tsx @@ -1,33 +1,35 @@ -import React from 'react' +import React, { useState } from 'react' import { ModalHeader, ModalContent, Modal, Icon, Segment, - Table + Table, + Input, + Button } from 'semantic-ui-react' -import { DjangoInstitution, DjangoUserCandidates } from '../../utils/django_interfaces' -import { Nullable } from '../../utils/interfaces' +import { DjangoInstitutionUser, DjangoUserCandidates } from '../../utils/django_interfaces' +import { CustomAlertTypes, Nullable } from '../../utils/interfaces' import { PaginatedTable } from '../common/PaginatedTable' import { TableCellWithTitle } from '../common/TableCellWithTitle' +import { InstitutionTableData } from './InstitutionsPanel' +import ky from 'ky' +import { getDjangoHeader } from '../../utils/util_functions' declare const urlGetUsersCandidates: string - -export enum InstitutionModalActions { - READ, - EDIT, -} +declare const urlEditInstitutionAdmin: string export interface InstitutionModalState { - isOpen: boolean, - action: InstitutionModalActions - institution: Nullable + isOpen: boolean, + institution: Nullable } interface Props extends InstitutionModalState { - /* Close modal function */ - handleCloseModal: () => void; + /* Close modal function */ + handleCloseModal: () => void, + handleChangeConfirmModalState: (setOption: boolean, headerText: string, contentText: string, onConfirm: Function) => void, + handleUpdateAlert(isOpen: boolean, type: CustomAlertTypes, message: string, callback: Nullable): void, } /** @@ -36,6 +38,32 @@ interface Props extends InstitutionModalState { * @returns Component */ export const InstitutionModal = (props: Props) => { + const [userInput, setUserInput] = useState(''); + + const handleAddUser = () => { + + } + const handleSwitchUserAdmin = (adminSwitched: boolean, id: number) => { + + const myHeaders = getDjangoHeader() + + const editUrl = `${urlEditInstitutionAdmin}/${id}/` + + + ky.patch(editUrl, { headers: myHeaders }).then((response) => { + response.json().then((jsonResponse: any) => { + props.handleUpdateAlert(true, CustomAlertTypes.SUCCESS, adminSwitched ? 'User is admin!' : 'User is not admin!', null) + }).catch((err) => { + props.handleUpdateAlert(true, CustomAlertTypes.ERROR, 'Error for switch role!', null) + console.error('Error parsing JSON ->', err) + }) + }).catch((err) => { + props.handleUpdateAlert(true, CustomAlertTypes.ERROR, 'Error for switch role!', null) + console.error('Error adding new Institution ->', err) + }) + + } + return ( { {props.institution?.name} - - headerTitle={props.institution?.name + ' candidates'} - headers={[ - { name: 'User name', serverCodeToSort: 'username', width: 3 }, - { name: 'Email', serverCodeToSort: 'email', width: 1 } + setUserInput(value)} placeholder='Username' /> + + + headerTitle={props.institution?.name + ' users'} + headers={props.institution?.is_user_admin ? [ + { name: 'User name', serverCodeToSort: 'user__username' as any, width: 3 }, + { name: 'Admin', width: 1 }, + { name: 'Actions', width: 1 } + ] : [ + { name: 'User name', serverCodeToSort: 'user__username' as any, width: 3 }, + { name: 'Admin', width: 1 }, ]} - defaultSortProp={{ sortField: 'upload_date', sortOrderAscendant: false }} showSearchInput searchLabel='User name' searchPlaceholder='Search by User name' - urlToRetrieveData={urlGetUsersCandidates} - updateWSKey='user_for_institution' - mapFunction={(userCandidate: DjangoUserCandidates) => { + urlToRetrieveData={urlGetUsersCandidates + '/' + props.institution?.id + '/'} + updateWSKey='update_user_for_institution' + mapFunction={(userCandidate: DjangoInstitutionUser) => { return ( - - - - - {/* Details button */} - { }} - /> - - {/* Edit button */} - { }} - /> + + + + { + userCandidate.is_institution_admin ? + + : + + } + {props.institution?.is_user_admin && + + {/* Edit button */} + { + userCandidate.is_institution_admin ? + handleSwitchUserAdmin(false, userCandidate.id)} + //onClick={() => props.handleChangeConfirmModalState(true, 'Manage admin', 'Are you sure to make user admin?', () => handleSwitchUserAdmin(false, 0))} + /> + : + handleSwitchUserAdmin(true, userCandidate.id)} + // onClick={() => props.handleChangeConfirmModalState(true, 'Manage admin', 'Are you sure to make user admin?', () => handleSwitchUserAdmin(true, 0))} + /> + } + + } ) }} diff --git a/src/frontend/static/frontend/src/components/institutions/InstitutionUsersInfo.tsx b/src/frontend/static/frontend/src/components/institutions/InstitutionUsersInfo.tsx deleted file mode 100644 index 6c9b9a3c..00000000 --- a/src/frontend/static/frontend/src/components/institutions/InstitutionUsersInfo.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, { useContext } from 'react' -import { List, Icon, Header, Segment, ListContent } from 'semantic-ui-react' -import { DjangoInstitution, DjangoUser } from '../../utils/django_interfaces' -import { Nullable } from '../../utils/interfaces' -import { CurrentUserContext } from '../Base' -import Avatar from 'react-avatar' - -/** - * Component's props - */ -interface InstitutionUsersInfoProps { - /** Selected Institution to show its users */ - selectedInstitution: Nullable, - /** Callback to show a modal to remove an User from a the selected Institution */ - confirmFileDeletion: (institutionUser: DjangoUser) => void -} - -/** - * Renders an Institution's Users info and some extra functionality like - * add a User to the Institution - * @param props Component's props - * @returns Component - */ -export const InstitutionUsersInfo = (props: InstitutionUsersInfoProps) => { - const currentUser = useContext(CurrentUserContext) - const selectedInstitution = props.selectedInstitution - - if (!selectedInstitution) { - return null - } - - return ( -
-
- - Users in {selectedInstitution.name} -
- - - - {selectedInstitution.users.map((institutionUser) => ( - - - {/* Remove from Institution button */} - {institutionUser.id !== currentUser?.id && - { props.confirmFileDeletion(institutionUser) }} - /> - } - - - - {institutionUser.username} - - - ))} - - -
- ) -} diff --git a/src/frontend/static/frontend/src/components/institutions/InstitutionsList.tsx b/src/frontend/static/frontend/src/components/institutions/InstitutionsList.tsx deleted file mode 100644 index 23a5727b..00000000 --- a/src/frontend/static/frontend/src/components/institutions/InstitutionsList.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react' -import { List, Icon } from 'semantic-ui-react' -import { DjangoInstitution } from '../../utils/django_interfaces' -import { Nullable } from '../../utils/interfaces' -import '../../css/institutions.css' - -/** - * Component's props - */ -interface InstitutionsListProps { - /** List of institutions */ - institutions: DjangoInstitution[], - /** Callback of 'Show users' button */ - showUsers: (institution: DjangoInstitution) => void - /** Selected institution to highlight */ - selectedInstitution: Nullable -} - -/** - * Renders a list of Institutions the current User belongs to - * @param props Component's props - * @returns Component - */ -export const InstitutionsList = (props: InstitutionsListProps) => { - return ( - - {props.institutions.map((institution) => ( - - - {/* Edit button */} - props.showUsers(institution)} - /> - - - {institution.name} - - {institution.location} - - {institution.email} {institution.telephone_number} - - - - ))} - - ) -} diff --git a/src/frontend/static/frontend/src/components/institutions/InstitutionsPanel.tsx b/src/frontend/static/frontend/src/components/institutions/InstitutionsPanel.tsx index 28182ffd..cb59190a 100644 --- a/src/frontend/static/frontend/src/components/institutions/InstitutionsPanel.tsx +++ b/src/frontend/static/frontend/src/components/institutions/InstitutionsPanel.tsx @@ -1,22 +1,21 @@ import React from 'react' import { Base } from '../Base' -import { Grid, Icon, Segment, Header, Table } from 'semantic-ui-react' +import { Grid, Icon, Segment, Header, Table, Confirm } from 'semantic-ui-react' import { DjangoInstitution, DjangoUserCandidates, DjangoCommonResponse, DjangoAddRemoveUserToInstitutionInternalCode, DjangoResponseCode, DjangoUser } from '../../utils/django_interfaces' import ky from 'ky' import { alertGeneralError, getDjangoHeader } from '../../utils/util_functions' -/* import { InstitutionsList } from './InstitutionsList' -import { InstitutionUsersInfo } from './InstitutionUsersInfo' */ import { RemoveUserFromInstitutionModal } from './RemoveUserFromInstitutionModal' -import { Nullable } from '../../utils/interfaces' +import { ConfirmModal, CustomAlert, CustomAlertTypes, Nullable } from '../../utils/interfaces' import { InfoPopup } from '../pipeline/experiment-result/gene-gem-details/InfoPopup' import { PaginatedTable } from '../common/PaginatedTable' import { TableCellWithTitle } from '../common/TableCellWithTitle' import InstitutionForm from './InstitutionForm' -import { InstitutionModal, InstitutionModalActions, InstitutionModalState } from './InstitutionModal' +import { InstitutionModal, InstitutionModalState } from './InstitutionModal' +import { Alert } from '../common/Alert' // URLs defined in files.html -declare const urlUserInstitutionsAsAdmin: string -declare const urlAddRemoveUserToInstitution: string +declare const urlGetUserInstitutions: string +declare const urlDeleteInstitution: string /** * Component's state @@ -33,6 +32,14 @@ interface InstitutionsPanelState { selectedUserToRemove: Nullable, showRemoveUserFromInstitutionModal: boolean, modalState: InstitutionModalState, + institutionToEdit: Nullable, + alert: CustomAlert, + isDeletingInstitution: boolean, + confirmModal: ConfirmModal, +} + +export interface InstitutionTableData extends DjangoInstitution { + is_user_admin: boolean } /** @@ -43,7 +50,7 @@ export class InstitutionsPanel extends React.Component<{}, InstitutionsPanelStat filterTimeout: number | undefined abortController = new AbortController() - constructor (props) { + constructor(props) { super(props) this.state = { @@ -57,216 +64,189 @@ export class InstitutionsPanel extends React.Component<{}, InstitutionsPanelStat removingUserFromInstitution: false, selectedUserToRemove: null, showRemoveUserFromInstitutionModal: false, - modalState: this.defaultModalState() + modalState: this.defaultModalState(), + institutionToEdit: null, + alert: this.getDefaultAlertProps(), + isDeletingInstitution: false, + confirmModal: this.getDefaultConfirmModal() } } - selectedInstitutionChanged (institution: DjangoInstitution) { - this.setState({ - selectedInstitution: institution - }) - } - /** - * When the component has been mounted, It requests for - * tags and files - */ - componentDidMount () { /* this.getUserInstitutions() */ } /** * Abort controller if component unmount */ - componentWillUnmount () { + componentWillUnmount() { this.abortController.abort() } /** - * Default modal attributes - * @returns {InstitutionModalState} Default modal - */ - defaultModalState (): InstitutionModalState { + * Generates a default alert structure + * @returns Default the default Alert + */ + getDefaultAlertProps(): CustomAlert { return { + message: '', // This have to change during cycle of component isOpen: false, - action: InstitutionModalActions.READ, - institution: null + type: CustomAlertTypes.SUCCESS, + duration: 500 } } - - /** - * Close modal - */ - handleCloseModal () { - this.setState({ modalState: this.defaultModalState() }) - } - - /** - * Open modal - * @param {InstitutionModalActions} action action type for modal. - * @param {DjangoInstitution} institution institution for modal. - */ - handleOpenModal (action: InstitutionModalActions, institution: DjangoInstitution) { - const modalState = { - isOpen: true, - action, - institution + getDefaultConfirmModal = (): ConfirmModal => { + return { + confirmModal: false, + headerText: '', + contentText: '', + onConfirm: () => console.log('DefaultConfirmModalFunction, this should change during cycle of component') } - this.setState({ modalState }) } /** - * Fetches the Institutions which the current user belongs to + * Reset the confirm modal, to be used again */ - getUserInstitutions () { - ky.get(urlUserInstitutionsAsAdmin, { signal: this.abortController.signal }).then((response) => { - response.json().then((institutions: DjangoInstitution[]) => { - // If it's showing an institution, refresh it's state - // For example, in the case of adding or removing a user to/from an Institution - let newSelectedInstitution: Nullable = null - - if (this.state.selectedInstitution !== null) { - newSelectedInstitution = institutions.find((institution) => { - return institution.id === this.state.selectedInstitution?.id - }) ?? null - } - - this.setState({ institutions, selectedInstitution: newSelectedInstitution }) - }).catch((err) => { - console.log('Error parsing JSON ->', err) - }) - }).catch((err) => { - console.log("Error getting user's datasets ->", err) - }) + handleCancelConfirmModalState() { + this.setState({ confirmModal: this.getDefaultConfirmModal() }) } /** - * Set a selected Institution in state to show it users - * @param selectedInstitution Selected Institution to show users + * Changes confirm modal state + * @param setOption New state of option + * @param headerText Optional text of header in confirm modal, by default will be empty + * @param contentText optional text of content in confirm modal, by default will be empty + * @param onConfirm Modal onConfirm callback */ - showUsers = (selectedInstitution: DjangoInstitution) => { this.setState({ selectedInstitution }) } + handleChangeConfirmModalState = (setOption: boolean, headerText: string, contentText: string, onConfirm: Function) => { + const confirmModal = this.state.confirmModal + confirmModal.confirmModal = setOption + confirmModal.headerText = headerText + confirmModal.contentText = contentText + confirmModal.onConfirm = onConfirm + this.setState({ confirmModal }) + } /** - * Cleans the inputs for adding a user to an Institution + * Update Alert + * @param isOpen flag to open or close alert. + * @param type type of alert. + * @param message message of alert. + * @param callback Callback function if is needed. */ - cleanSearchAndCandidates () { - this.setState({ - userCandidates: [], - selectedUserIdToAdd: null, - searchUserText: '' - }) + handleUpdateAlert(isOpen: boolean, type: CustomAlertTypes, message: string, callback: Nullable, isEdit?: boolean) { + const alert = this.state.alert + alert.isOpen = isOpen + alert.type = type + alert.message = message + let institutionToEdit = this.state.institutionToEdit + if(isEdit){ + institutionToEdit = null + } + if (callback) { + callback() + this.setState({ alert, institutionToEdit }) + }else{ + this.setState({ alert, institutionToEdit }) + } } /** - * Sets as selected a new user to add him to an Institution, - * @param selectedUserId Id of the selected user in the Dropdown + * Reset the confirm modal, to be used again */ - selectUser (selectedUserId) { this.setState({ selectedUserIdToAdd: selectedUserId }) } + handleCloseAlert = () => { + const alert = this.state.alert + alert.isOpen = false + this.setState({ alert }) + } /** - * Makes a request to the server to add/remove a User to/from an Institution - * @param userId Id of the user to add or remove - * @param isAdding True if want to add, false if want to remove + * Default modal attributes + * @returns {InstitutionModalState} Default modal */ - addOrRemoveUserToInstitution = (userId: number, isAdding: boolean) => { - // Sets the Request's Headers - const myHeaders = getDjangoHeader() - - const params = { - userId, - institutionId: this.state.selectedInstitution?.id, - isAdding + defaultModalState(): InstitutionModalState { + return { + isOpen: false, + institution: null } - - const loadingFlag = isAdding ? 'addingRemovingUserToInstitution' : 'removingUserFromInstitution' - - this.setState({ [loadingFlag]: true }, () => { - ky.post(urlAddRemoveUserToInstitution, { headers: myHeaders, json: params }).then((response) => { - this.setState({ [loadingFlag]: false }) - response.json().then((jsonResponse: DjangoCommonResponse) => { - this.cleanSearchAndCandidates() - - if (jsonResponse.status.code === DjangoResponseCode.SUCCESS) { - this.getUserInstitutions() - this.handleClose() - } else { - if (jsonResponse.status.internal_code === DjangoAddRemoveUserToInstitutionInternalCode.CANNOT_REMOVE_YOURSELF) { - alert('You cannot remove yourself from the Institution!') - } else { - alertGeneralError() - } - } - }).catch((err) => { - console.log('Error parsing JSON ->', err) - }) - }).catch((err) => { - this.setState({ [loadingFlag]: false }) - console.log("Error getting user's datasets ->", err) - }) - }) } /** - * Makes a request to the server to add a User from an Institution + * Close modal */ - addUserToInstitution = () => { - if (this.state.selectedUserIdToAdd !== null) { - this.addOrRemoveUserToInstitution(this.state.selectedUserIdToAdd, true) - } + handleCloseModal() { + this.setState({ modalState: this.defaultModalState() }) } /** - * Makes a request to the server to remove a User from an Institution + * Open modal + * @param {InstitutionTableData} institution institution for modal. */ - removeUserFromInstitution = () => { - if (this.state.selectedUserToRemove?.id) { - this.addOrRemoveUserToInstitution(this.state.selectedUserToRemove.id, false) + handleOpenModal(institution: InstitutionTableData) { + const modalState = { + isOpen: true, + institution } + this.setState({ modalState }) } + /** - * Show a modal to confirm a removal of an User from an Institution - * @param institutionUser Selected User to remove + * Reset institution to edit */ - confirmFileDeletion = (institutionUser: DjangoUser) => { - this.setState({ - selectedUserToRemove: institutionUser, - showRemoveUserFromInstitutionModal: true - }) + handleResetInstitutionToEdit(callbackToCancel: () => void) { + this.setState({ institutionToEdit: null }) + callbackToCancel() } /** - * Closes the removal confirm modals + * Set form to edit + * @param {InstitutionTableData} institution institution for modal. */ - handleClose = () => { this.setState({ showRemoveUserFromInstitutionModal: false }) } + handleSetInstitutionToEdit(institution: InstitutionTableData) { + this.setState({ institutionToEdit: institution }) + } /** - * Checks if search input should be enabled or not - * @returns True if user can choose a user to add to an Institution, false otherwise + * + * @returns */ - formIsDisabled = (): boolean => !this.state.selectedInstitution || this.state.addingRemovingUserToInstitution - - render () { - /* const userOptions: DropdownItemProps[] = this.state.userCandidates.map((userCandidate) => { - return { - key: userCandidate.id, - text: `${userCandidate.username} (${userCandidate.email})`, - value: userCandidate.id - } - }) */ + handleDeleteInstitution(institutionId: number) { + const url = `${urlDeleteInstitution}/${institutionId}/` + const myHeaders = getDjangoHeader() + ky.delete(url, { headers: myHeaders }).then((response) => { + response.json().then((jsonResponse: DjangoInstitution) => { + this.handleUpdateAlert(true, CustomAlertTypes.SUCCESS, 'Institution deleted!', null) + }).catch((err) => { + this.handleUpdateAlert(true, CustomAlertTypes.ERROR, 'Error deleting an Institution!', null) + console.error('Error parsing JSON ->', err) + }) + .finally(() => { + const confirmModal = this.state.confirmModal + confirmModal.confirmModal = false + this.setState({ confirmModal }) + }) + }).catch((err) => { + this.handleUpdateAlert(true, CustomAlertTypes.ERROR, 'Error deleting an Institution!', null) + console.error('Error adding new Institution ->', err) + }).finally(() => { + const confirmModal = this.state.confirmModal + confirmModal.confirmModal = false + this.setState({ confirmModal }) + }) + } - /* const formIsDisabled = this.formIsDisabled() - */ + render() { return ( {/* Modal to confirm User removal from an Institution */} - + /> */} {/* List of institutions */} @@ -279,7 +259,11 @@ export class InstitutionsPanel extends React.Component<{}, InstitutionsPanelStat
- + this.handleResetInstitutionToEdit(callback)} + handleUpdateAlert={(isOpen: boolean, type: CustomAlertTypes, message: string, callback: Nullable, isEdit?: boolean) => this.handleUpdateAlert(isOpen, type, message, callback, isEdit)} + /> {/* Todo: remove component - + headerTitle='Institutions' headers={[ { name: 'Name', serverCodeToSort: 'name', width: 3 }, { name: 'Location', serverCodeToSort: 'location', width: 1 }, - { name: 'Email', width: 1 }, - { name: 'Phone number', width: 1 }, + { name: 'Email', serverCodeToSort: 'email', width: 1 }, + { name: 'Phone number', serverCodeToSort: 'telephone_number', width: 1 }, { name: 'Actions', width: 1 } ]} defaultSortProp={{ sortField: 'upload_date', sortOrderAscendant: false }} showSearchInput searchLabel='Name' searchPlaceholder='Search by name' - urlToRetrieveData={urlUserInstitutionsAsAdmin} - updateWSKey='institutionsList' - mapFunction={(institution: DjangoInstitution) => { + urlToRetrieveData={urlGetUserInstitutions} + updateWSKey='update_institutions' + mapFunction={(institution: InstitutionTableData) => { return ( @@ -317,21 +301,32 @@ export class InstitutionsPanel extends React.Component<{}, InstitutionsPanelStat {/* Details button */} this.handleOpenModal(InstitutionModalActions.READ, institution)} + onClick={() => this.handleOpenModal(institution)} /> {/* Edit button */} - this.handleOpenModal(InstitutionModalActions.EDIT, institution)} - /> + onClick={() => this.handleSetInstitutionToEdit(institution)} + />} + {/*Delete button */} + {institution.is_user_admin && + this.handleChangeConfirmModalState(true, 'Delete institution', `Are you sure about deleting institution ${institution.name}`, () => this.handleDeleteInstitution(institution.id as number))} + /> + } ) @@ -340,7 +335,27 @@ export class InstitutionsPanel extends React.Component<{}, InstitutionsPanelStat
- this.handleCloseModal()} action={this.state.modalState.action} isOpen={this.state.modalState.isOpen} institution={this.state.modalState.institution} /> + , isEdit?: boolean) => this.handleUpdateAlert(isOpen, type, message, callback)} + handleChangeConfirmModalState={this.handleChangeConfirmModalState} + handleCloseModal={() => this.handleCloseModal()} + isOpen={this.state.modalState.isOpen} institution={this.state.modalState.institution} + /> + + this.handleCancelConfirmModalState()} + onConfirm={() => this.state.confirmModal.onConfirm()} + /> ) } diff --git a/src/frontend/static/frontend/src/utils/django_interfaces.ts b/src/frontend/static/frontend/src/utils/django_interfaces.ts index e731cd33..d991a2dc 100644 --- a/src/frontend/static/frontend/src/utils/django_interfaces.ts +++ b/src/frontend/static/frontend/src/utils/django_interfaces.ts @@ -446,7 +446,19 @@ interface DjangoInstitution { location: string, email: string, telephone_number: string, - users: DjangoUser[] + users?: DjangoUser[] +} + +/** + * Django Institution user + */ +interface DjangoInstitutionUser { + user: { + id: number, + username: string, + }, + id:number, + is_institution_admin: boolean, } /** @@ -683,5 +695,6 @@ export { DjangoSourceDataOutliersBasic, DjangoMonotonicityTest, DjangoSurvivalColumnsTupleSimple, - DjangoNumberSamplesInCommonClinicalValidationResult + DjangoNumberSamplesInCommonClinicalValidationResult, + DjangoInstitutionUser } diff --git a/src/frontend/static/frontend/src/utils/interfaces.ts b/src/frontend/static/frontend/src/utils/interfaces.ts index 9f96ddff..c03f925f 100644 --- a/src/frontend/static/frontend/src/utils/interfaces.ts +++ b/src/frontend/static/frontend/src/utils/interfaces.ts @@ -319,6 +319,14 @@ enum GenesColors { CLINICAL = 'teal' } +/** Confirm modal */ +interface ConfirmModal { + confirmModal: boolean, + headerText: string, + contentText: string, + onConfirm: Function, +} + export { GenesColors, CustomAlertTypes, @@ -355,5 +363,6 @@ export { ScoreClassData, BinData, DataUICategoricalBinnedDatumShape, - OkResponse + OkResponse, + ConfirmModal } diff --git a/src/frontend/static/frontend/src/websockets/WebsocketClient.ts b/src/frontend/static/frontend/src/websockets/WebsocketClient.ts index 0b9137e2..6d0fe1ae 100644 --- a/src/frontend/static/frontend/src/websockets/WebsocketClient.ts +++ b/src/frontend/static/frontend/src/websockets/WebsocketClient.ts @@ -22,8 +22,9 @@ class WebsocketClientCustom { this.websocket.onopen = function () { console.log('Websocket connection established') } - - this.websocket.onmessage = debounce(function (event) { + // Makes all the functions debounced to prevent multiple concatenated executions. + config.commandsToAttend = config.commandsToAttend.map(command => ({...command, functionToExecute: debounce(command.functionToExecute, 300)})) + this.websocket.onmessage = function (event) { try { const dataParsed: WebsocketMessage = JSON.parse(event.data) @@ -38,7 +39,7 @@ class WebsocketClientCustom { console.log('Data:', event.data) console.log('Exception:', ex) } - }, 300) + } this.websocket.onerror = function (event) { console.log('Error establishing websocket connection', event) diff --git a/src/frontend/templates/frontend/institutions.html b/src/frontend/templates/frontend/institutions.html index bb76824b..eced58d2 100644 --- a/src/frontend/templates/frontend/institutions.html +++ b/src/frontend/templates/frontend/institutions.html @@ -14,8 +14,12 @@ {% block js %} {% render_bundle 'institutions' 'js' %} diff --git a/src/institutions/models.py b/src/institutions/models.py index 31171db3..f4997e87 100644 --- a/src/institutions/models.py +++ b/src/institutions/models.py @@ -1,5 +1,6 @@ from django.contrib.auth import get_user_model from django.db import models +from api_service.websocket_functions import send_update_institutions_command, send_update_user_for_institution_command class Institution(models.Model): @@ -10,6 +11,24 @@ class Institution(models.Model): telephone_number = models.CharField(max_length=30, blank=True, null=True) users = models.ManyToManyField(get_user_model(), through='InstitutionAdministration') + class Meta: + ordering = ['-id'] + + def delete(self, *args, **kwargs) -> None: + """Deletes the instance and sends a websockets message to update state in the frontend """ + related_users = list(self.users.all()) + super().delete(*args, **kwargs) + # Sends a websockets message to all users for update the institutions state in the frontend + for user in related_users: + send_update_institutions_command(user.id) + + def save(self, *args, **kwargs) -> None: + """Everytime the institution status changes, uses websocket to update state in the frontend""" + super().save(*args, **kwargs) + # Sends a websockets message to all users for update the institutions state in the frontend + for user in self.users.all(): + send_update_institutions_command(user.id) + def __str__(self): return self.name @@ -20,6 +39,27 @@ class InstitutionAdministration(models.Model): institution = models.ForeignKey(Institution, on_delete=models.CASCADE) is_institution_admin = models.BooleanField(default=False) + class Meta: + ordering = ['-id'] + + def delete(self, *args, **kwargs) -> None: + """Deletes the instance and sends a websockets message to update state in the frontend """ + related_users = list(self.institution.users.all()) + super().delete(*args, **kwargs) + # Sends a websockets message to all users for update the institutions state in the frontend + for user in related_users: + send_update_institutions_command(user.id) + send_update_user_for_institution_command(user.id) + + def save(self, *args, **kwargs) -> None: + """Everytime the institution status changes, uses websocket to update state in the frontend""" + super().save(*args, **kwargs) + related_users = list(self.institution.users.all()) + # Sends a websockets message to all users for update the institutions state in the frontend + for user in related_users: + send_update_user_for_institution_command(user.id) + send_update_institutions_command(user.id) + def __str__(self): as_admin_label = '(as admin)' if self.is_institution_admin else '' return f'{self.user} -> {self.institution} {as_admin_label}' diff --git a/src/institutions/serializers.py b/src/institutions/serializers.py index a2ce6426..f722d838 100644 --- a/src/institutions/serializers.py +++ b/src/institutions/serializers.py @@ -1,7 +1,7 @@ -from django.contrib.auth import get_user_model from rest_framework import serializers -from .models import Institution +from .models import Institution, InstitutionAdministration from frontend.serializers import UserSerializer +from django.contrib.auth import get_user_model class InstitutionSerializer(serializers.ModelSerializer): @@ -19,9 +19,33 @@ class Meta: model = Institution fields = ['id', 'name'] +class InstitutionListSerializer(serializers.ModelSerializer): + """A simple serializer for get only the Institution fields""" + is_user_admin = serializers.SerializerMethodField(method_name='get_is_user_admin') + class Meta: + model = Institution + fields = ['id', 'name', 'location', 'email', 'telephone_number', 'is_user_admin'] + def get_is_user_admin(self, obj: Institution) -> bool: + user = self.context['request'].user + return obj.institutionadministration_set.filter(user=user, is_institution_admin=True).exists() + +class LimitedUserSerializer(serializers.ModelSerializer): + """A lightweight serializer for User model""" + class Meta: + model = get_user_model() + fields = ['id', 'username'] class UserCandidateSerializer(serializers.ModelSerializer): """Serializer useful to add a User to an Institution""" + user = LimitedUserSerializer() class Meta: - model = get_user_model() - fields = ['id', 'username', 'email'] + model = InstitutionAdministration + fields = ['id', 'user', 'is_institution_admin'] + +class InstitutionAdminUpdateSerializer(serializers.ModelSerializer): + """ + Serializer for updating 'is_institution_admin' in InstitutionAdministration + """ + class Meta: + model = InstitutionAdministration + fields = ['id', 'is_institution_admin'] diff --git a/src/institutions/urls.py b/src/institutions/urls.py index 24fe9831..0945ebea 100644 --- a/src/institutions/urls.py +++ b/src/institutions/urls.py @@ -4,9 +4,17 @@ urlpatterns = [ path('', views.institutions_action, name='institutions'), - path('my-institutions-as-admin', views.InstitutionAsAdminList.as_view(), name='user_institutions_as_admin'), - path('my-institutions', views.InstitutionList.as_view(), name='user_institutions'), + path('my-institutions', views.UserInstitution.as_view(), name='user_institutions'), + path('my-institutions-list', views.UserInstitutionList.as_view(), name='user_institutions_list'), path('user-candidates', views.UserCandidatesList.as_view(), name='user_candidates'), + path('user-candidates//', views.UserCandidatesList.as_view()), + path('create', views.CreateInstitutionView.as_view(), name='create_institution'), + path('edit', views.UpdateInstitutionView.as_view(), name='edit_institution'), + path('edit//', views.UpdateInstitutionView.as_view()), + path('delete', views.DeleteInstitutionView.as_view(), name='delete-institution'), + path('delete//', views.DeleteInstitutionView.as_view()), + path('admin-update', views.UpdateInstitutionAdminView.as_view(), name='update-institution-admin'), + path('admin-update//', views.UpdateInstitutionAdminView.as_view()), path( 'add-remove-user-to-institution', views.add_remove_user_to_institution_action, diff --git a/src/institutions/views.py b/src/institutions/views.py index 32fdc640..6e53a04c 100644 --- a/src/institutions/views.py +++ b/src/institutions/views.py @@ -6,64 +6,55 @@ from common.functions import encode_json_response_status from common.response import ResponseStatus from .enums import AddRemoveUserToInstitutionStatusErrorCode -from .models import Institution -from .serializers import InstitutionSerializer, UserCandidateSerializer +from .models import Institution, InstitutionAdministration +from .serializers import InstitutionSerializer, UserCandidateSerializer, InstitutionListSerializer, InstitutionAdminUpdateSerializer from django.contrib.auth.models import User from common.pagination import StandardResultsSetPagination from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.response import Response +from rest_framework.views import APIView +from django.shortcuts import get_object_or_404 +from rest_framework.exceptions import ValidationError -class InstitutionAsAdminList(generics.ListAPIView): - """REST endpoint: list for Institution model of which the current user is admin""" + +class UserInstitutionList(generics.ListAPIView): + """REST endpoint: list for Institution model of which the current user is part""" def get_queryset(self): return Institution.objects.filter( - institutionadministration__user=self.request.user, - institutionadministration__is_institution_admin=True - ) + institutionadministration__user=self.request.user + ).distinct() - serializer_class = InstitutionSerializer + serializer_class = InstitutionListSerializer permission_classes = [permissions.IsAuthenticated] pagination_class = StandardResultsSetPagination - filter_backends = [filters.OrderingFilter, filters.SearchFilter, DjangoFilterBackend] - filterset_fields = [] - search_fields = ['name'] - ordering_fields = ['name', 'location'] - - -class InstitutionList(generics.ListAPIView): + filter_backends = [filters.OrderingFilter, filters.SearchFilter] + search_fields = ['name', 'location'] + ordering_fields = ['name', 'location', 'email', 'telephone_number'] +class UserInstitution(generics.ListAPIView): """REST endpoint: list for Institution model of which the current user is part""" def get_queryset(self): return Institution.objects.filter(institutionadministration__user=self.request.user) - serializer_class = InstitutionSerializer permission_classes = [permissions.IsAuthenticated] - class UserCandidatesList(generics.ListAPIView): """REST endpoint: list for User model. Used to add to an Institution""" def get_queryset(self): - # Parses the request search param - query_search = self.request.GET.get('querySearch', '') - query_search = query_search.strip() - - if not query_search: - return User.objects.none() - - # Returns only 3 results - return User.objects.filter(username__icontains=query_search)[:3] - + institution_id = self.kwargs.get('institution_id') + return InstitutionAdministration.objects.filter( + institution_id=institution_id + ) serializer_class = UserCandidateSerializer permission_classes = [permissions.IsAuthenticated] pagination_class = StandardResultsSetPagination - filter_backends = [filters.OrderingFilter, filters.SearchFilter, DjangoFilterBackend] - filterset_fields = [] - search_fields = ['name'] - ordering_fields = ['name'] - + filter_backends = [filters.OrderingFilter, filters.SearchFilter] + search_fields = ['user__username'] + ordering_fields = ['user__username'] @login_required def add_remove_user_to_institution_action(request): @@ -132,6 +123,128 @@ def add_remove_user_to_institution_action(request): return encode_json_response_status(response) +class CreateInstitutionView(generics.CreateAPIView): + """ + REST endpoint: Create an Institution and assign the creator as admin + """ + serializer_class = InstitutionSerializer + permission_classes = [permissions.IsAuthenticated] + + def perform_create(self, serializer): + """ + Overrides the create method to add the creator as admin + """ + # Save the institution instance + institution = serializer.save() + # Create a relationship between the creator and the institution as admin + InstitutionAdministration.objects.create( + user=self.request.user, + institution=institution, + is_institution_admin=True + ) + + def create(self, request, *args, **kwargs): + """ + Handles the POST request to create an institution + """ + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + return Response( + serializer.data + ) + +class UpdateInstitutionView(generics.UpdateAPIView): + """ + REST endpoint: Update an Institution + Allows editing 'name', 'location', 'email', and 'telephone_number'. + """ + queryset = Institution.objects.all() + serializer_class = InstitutionListSerializer + permission_classes = [permissions.IsAuthenticated] + lookup_field = 'id' + + def get_queryset(self): + """ + Allow only admins of the institution to edit + """ + return Institution.objects.filter( + institutionadministration__user=self.request.user, + institutionadministration__is_institution_admin=True + ) + + def update(self, request, *args, **kwargs): + """ + Handle partial updates + """ + partial = kwargs.pop('partial', True) # Permite actualizaciones parciales + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data, partial=partial) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + + return Response(serializer.data) + +class DeleteInstitutionView(generics.DestroyAPIView): + """ + REST endpoint: Delete an Institution + Allows deletion only if the user is an admin of the institution. + """ + queryset = Institution.objects.all() + permission_classes = [permissions.IsAuthenticated] + lookup_field = 'id' + + def get_queryset(self): + """ + Allow only admins of the institution to delete it. + """ + return Institution.objects.filter( + institutionadministration__user=self.request.user, + institutionadministration__is_institution_admin=True + ) + + def delete(self, request, *args, **kwargs): + """ + Handle the delete operation with additional checks. + """ + try: + instance = self.get_object() + self.perform_destroy(instance) + response = { + 'status': ResponseStatus( + ResponseCode.ERROR, + message="Institution deleted successfully." + ) + } + return encode_json_response_status(response) + + except Institution.DoesNotExist: + response = { + 'status': ResponseStatus( + ResponseCode.ERROR, + message="Institution not found or you do not have permission to delete it." + ) + } + return encode_json_response_status(response) + + +class UpdateInstitutionAdminView(APIView): + """ + REST endpoint: Update the 'is_institution_admin' field in InstitutionAdministration + """ + queryset = InstitutionAdministration.objects.all() + serializer_class = InstitutionAdminUpdateSerializer + permission_classes = [permissions.IsAuthenticated] + def patch(self, request, id): + """ + Ensure that only an institution admin can update this relationship. + """ + relation = get_object_or_404(InstitutionAdministration, institution__institutionadministration__user=self.request.user, id=id) + if relation.user.id == self.request.user.id: + raise ValidationError('Can not change admin for current user.') + relation.is_institution_admin = not relation.is_institution_admin + relation.save(update_fields=['is_institution_admin']) + return Response({'ok': True}) @login_required def institutions_action(request): diff --git a/src/statistical_properties/views.py b/src/statistical_properties/views.py index ed9a7940..39c732f7 100644 --- a/src/statistical_properties/views.py +++ b/src/statistical_properties/views.py @@ -163,6 +163,8 @@ def get_queryset(self): ordering_fields = ['name', 'description', 'state', 'created'] + +# Todo: Verificar class StatisticalValidationDestroy(generics.DestroyAPIView): """REST endpoint: delete for StatisticalValidation model.""" From 9fe6a1ab7237997dd6b71224084c4fdade7b8c7a Mon Sep 17 00:00:00 2001 From: Gonza Date: Wed, 8 Jan 2025 20:16:44 -0300 Subject: [PATCH 19/27] edit profile implemented --- DEPLOYING.md | 1 + docker-compose.dev.yml | 2 +- src/frontend/serializers.py | 31 ---- src/frontend/urls.py | 11 +- src/frontend/views.py | 179 +--------------------- src/institutions/serializers.py | 2 +- src/multiomics_intermediate/settings.py | 6 +- src/multiomics_intermediate/urls.py | 1 + src/user_files/serializers.py | 8 +- src/user_files/views.py | 2 +- src/users/admin.py | 0 src/users/apps.py | 6 + src/users/migrations/__init__.py | 0 src/users/models.py | 3 + src/users/serializers.py | 67 +++++++++ src/users/urls.py | 16 ++ src/users/views.py | 188 ++++++++++++++++++++++++ 17 files changed, 295 insertions(+), 228 deletions(-) delete mode 100644 src/frontend/serializers.py create mode 100644 src/users/admin.py create mode 100644 src/users/apps.py create mode 100644 src/users/migrations/__init__.py create mode 100644 src/users/models.py create mode 100644 src/users/serializers.py create mode 100644 src/users/urls.py create mode 100644 src/users/views.py diff --git a/DEPLOYING.md b/DEPLOYING.md index eda5dcaf..47e6069b 100644 --- a/DEPLOYING.md +++ b/DEPLOYING.md @@ -45,6 +45,7 @@ The following are the steps to perform a deployment in production. In case you w - `CGDS_CHUNK_SIZE`: size **in bytes** of the chunk in which the files of a CGDS study are downloaded, the bigger it is, the faster the download is, but the more server memory it consumes. Default `2097152`, i.e. 2MB. - `THRESHOLD_ORDINAL`: number of different values for the GEM (CNA) information to be considered ordinal, if the number is <= to this value then it is considered categorical/ordinal and a boxplot is displayed, otherwise, it is considered continuous and the common correlation graph is displayed. Default `5`. - `THRESHOLD_GEM_SIZE_TO_COLLECT`: GEM file size threshold (in MB) for the GEM dataset to be available in memory. This has a HUGE impact on the performance of the analysis. If the size is less than or equal to this threshold, it is allocated in memory, otherwise, it will be read lazily from the disk. If None GGCA automatically allocates in memory when the GEM dataset size is small (<= 100MB). Therefore, if you want to force to always use RAM to improve performance you should set a very high threshold, on the contrary, if you want a minimum memory usage at the cost of poor performance, set it to `0`. Default `None`. + - `MIN_PASSWORD_LEN`: Defines the minimum required length for user passwords when updating their profile. If the provided password is shorter than this length, the update will be rejected. - PostgreSQL: - `POSTGRES_USERNAME`: PostgreSQL connection username. **Must be equal to** `POSTGRES_USER`. - `POSTGRES_PASSWORD`: PostgreSQL connection password. **Must be equal to** `POSTGRES_PASSWORD`. diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 84459560..45d20a66 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -5,7 +5,7 @@ services: restart: 'always' container_name: multiomics_dev_db ports: - - '5432:5432' + - '5433:5432' command: postgres -c 'config_file=/etc/postgresql/postgresql.conf' environment: POSTGRES_USER: root diff --git a/src/frontend/serializers.py b/src/frontend/serializers.py deleted file mode 100644 index a36cb4a0..00000000 --- a/src/frontend/serializers.py +++ /dev/null @@ -1,31 +0,0 @@ -from rest_framework import serializers -from django.contrib.auth import get_user_model -from institutions.models import Institution - - -class UserSerializer(serializers.ModelSerializer): - class Meta: - model = get_user_model() - fields = ['id', 'username', 'is_superuser'] - - def to_representation(self, instance): - # Adds custom fields - data = super(UserSerializer, self).to_representation(instance) - - # Check if current user has any institutions where he's admin - data['is_institution_admin'] = Institution.objects.filter( - institutionadministration__user=instance, - institutionadministration__is_institution_admin=True - ).exists() - - # User at this point is not anonymous - data['is_anonymous'] = False - - return data - - -class UserSimpleSerializer(serializers.ModelSerializer): - """User serializer with fewer data""" - class Meta: - model = get_user_model() - fields = ['id', 'username'] diff --git a/src/frontend/urls.py b/src/frontend/urls.py index 1aaa0b6e..256915fc 100644 --- a/src/frontend/urls.py +++ b/src/frontend/urls.py @@ -1,21 +1,12 @@ -from django.conf import settings from django.urls import path from . import views urlpatterns = [ path('', views.index_action, name='index'), path('gem', views.gem_action, name='gem'), - path('login', views.login_action, name='login'), path('my-datasets', views.datasets_action, name='datasets'), path('survival', views.survival_action, name='survival'), - path('authenticate', views.authenticate_action, name='authentication'), - path('create-user', views.create_user_action, name='create_user'), path('about-us', views.about_us_action, name='about_us'), - path('site-policy', views.terms_and_privacy_policy_action, name='site_policy'), - path('logout', views.logout_action, name='logout'), - path('user', views.CurrentUserView.as_view(), name='current_user') + path('site-policy', views.terms_and_privacy_policy_action, name='site_policy') ] -if settings.DEBUG: - urlpatterns.append(path('new-user-email-test', views.test_new_user_email)) - urlpatterns.append(path('confirmation-email-test', views.test_confirmation_email)) diff --git a/src/frontend/views.py b/src/frontend/views.py index f8fc9d65..68914f4c 100644 --- a/src/frontend/views.py +++ b/src/frontend/views.py @@ -1,17 +1,6 @@ -import logging -from typing import Optional, Union -from django.contrib.auth.base_user import AbstractBaseUser -from django.shortcuts import render, redirect -from django.contrib.auth import authenticate, logout, login -from django.contrib.auth.models import User +from django.shortcuts import render from django.contrib.auth.decorators import login_required -from django_email_verification import sendConfirm -from rest_framework import permissions -from rest_framework.response import Response -from rest_framework.views import APIView -from .serializers import UserSerializer from django.conf import settings -from django.db import transaction, InternalError def index_action(request): @@ -52,169 +41,3 @@ def datasets_action(request): def survival_action(request): """Survival Analysis view""" return render(request, "frontend/survival.html") - - -def login_action(request): - """Login view""" - if request.user.is_authenticated: - return redirect('index') - - return render(request, "frontend/login.html") - - -def view_bad_credentials(request): - """Returns login template with bad credential error in Its context""" - return render(request, "frontend/login.html", {'loginError': 'Your username or password is invalid'}) - - -def authenticate_action(request): - """Authentication view""" - username: Optional[str] = request.POST.get('username') - password: Optional[str] = request.POST.get('password') - if username is None or password is None: - return view_bad_credentials(request) - - user: Union[AbstractBaseUser, AbstractBaseUser, None] = authenticate(username=username, password=password) - - if user is not None: - login(request, user) - # If 'next' param is specified, it redirects to that URL - next_url = request.POST.get('next') - return redirect(next_url if next_url is not None else 'index') - else: - # Checks if User is active - try: - user: User = User.objects.get(username=username) - if user is not None and not user.is_active: - return render( - request, - "frontend/login.html", - { - 'loginWarning': 'Your account has not been validated yet. Please, check your email account for ' - 'our verification email' - } - ) - return view_bad_credentials(request) - except User.DoesNotExist: - return view_bad_credentials(request) - - -def is_sign_up_form_valid(username: str, email: str, password: str, password_repeated: str) -> bool: - """ - Checks if Sign Up form is valid - @param username: Username to check - @param email: Email to check - @param password: Password to check - @param password_repeated: Repeated password to check - @return: True if is all the field valid. False otherwise - """ - password_striped = password.strip() - password_repeated_striped = password_repeated.strip() - return len(username.strip()) > 0 \ - and len(email.strip()) > 0 \ - and len(password_striped) > 0 \ - and len(password_repeated_striped) > 0 \ - and password_striped == password_repeated_striped - - -def user_already_exists_username(username: str) -> bool: - """ - Check if exists any user in the DB with the same username passed by parameter - @param username: Username to check - @return: True if exists the user. False otherwise - """ - return User.objects.filter(username=username).exists() - - -def user_already_exists_email(email: str) -> bool: - """ - Check if exists any user in the DB with the same email passed by parameter - @param email: Email to check - @return: True if exists the user. False otherwise - """ - return User.objects.filter(email=email).exists() - - -@transaction.atomic -def create_user_action(request): - """User creation view""" - username: str = request.POST.get('newUsername', '') - email: str = request.POST.get('email', '') - password: str = request.POST.get('newPassword', '') - password_repeated: str = request.POST.get('newPasswordRepeated', '') - if not is_sign_up_form_valid(username, email, password, password_repeated): - return render(request, "frontend/login.html", {'loginError': 'Invalid fields'}) - - if user_already_exists_username(username): - return render(request, "frontend/login.html", {'loginError': 'There is already a user with that username'}) - - if user_already_exists_email(email): - return render(request, "frontend/login.html", {'loginError': 'There is already a user with that email'}) - - # If it's set to not send an email asking for confirmation just creates the user and redirect to login - if not settings.EMAIL_NEW_USER_CONFIRMATION_ENABLED: - User.objects.create_user(username, email, password) - return render(request, "frontend/login.html", {'loginSuccess': 'User created successfully'}) - - # Creates the user and sends email - # FIXME: library does not return exceptions thrown in sender thread. After a PR it should be tested - error_sending_email = False - try: - with transaction.atomic(): - user = User.objects.create_user(username, email, password) - try: - sendConfirm(user) - except Exception as ex: - logging.warning(f'Error sending email -> {ex}') - raise InternalError # Rollback - except InternalError: - error_sending_email = True - - if error_sending_email: - return render(request, "frontend/login.html", {'loginError': 'An error have occurred. Please, try again. If the' - ' problem persists contact us, please'}) - - # Hides email - idx_at = email.index('@') - idx_to_replace = idx_at - 2 - hidden_email = '*' * idx_to_replace + email[idx_to_replace:] - - return render( - request, - "frontend/login.html", - {'loginWarning': f'We have sent a confirmation email to {hidden_email}'} - ) - - -def test_new_user_email(request): - """ - To test new user email template in browser. Only accessible in DEBUG mode - @param request: Request object - @return: HTTP Response - """ - return render(request, settings.EMAIL_MAIL_HTML, {'link': '#'}) - - -def test_confirmation_email(request): - """ - To test confirmation email template in browser. Only accessible in DEBUG mode - @param request: Request object - @return: HTTP Response - """ - return render(request, settings.EMAIL_PAGE_TEMPLATE, {'success': True}) - - -def logout_action(request): - """Closes User session""" - logout(request) - return redirect('index') - - -class CurrentUserView(APIView): - """Gets current User info""" - permission_classes = [permissions.IsAuthenticated] - - @staticmethod - def get(request): - serializer = UserSerializer(request.user) - return Response(serializer.data) diff --git a/src/institutions/serializers.py b/src/institutions/serializers.py index a2ce6426..629cdc4a 100644 --- a/src/institutions/serializers.py +++ b/src/institutions/serializers.py @@ -1,7 +1,7 @@ from django.contrib.auth import get_user_model from rest_framework import serializers from .models import Institution -from frontend.serializers import UserSerializer +from users.serializers import UserSerializer class InstitutionSerializer(serializers.ModelSerializer): diff --git a/src/multiomics_intermediate/settings.py b/src/multiomics_intermediate/settings.py index 3718507c..52747f9f 100644 --- a/src/multiomics_intermediate/settings.py +++ b/src/multiomics_intermediate/settings.py @@ -62,6 +62,7 @@ 'inferences', 'molecules_details', 'chunked_upload', + 'users', ] MIDDLEWARE = [ @@ -128,7 +129,7 @@ 'USER': os.getenv('POSTGRES_USERNAME', 'root'), 'PASSWORD': os.getenv('POSTGRES_PASSWORD', 'example'), 'HOST': os.getenv('POSTGRES_HOST', '127.0.0.1'), - 'PORT': os.getenv('POSTGRES_PORT', 5432), + 'PORT': os.getenv('POSTGRES_PORT', 5433), 'NAME': os.getenv('POSTGRES_DB', 'multiomics') # Keep "multiomics" for backward compatibility } } @@ -151,6 +152,8 @@ }, ] +MIN_PASSWORD_LEN: int = 8 + # Internationalization # https://docs.djangoproject.com/en/3.0/topics/i18n/ @@ -296,7 +299,6 @@ EMAIL_PAGE_TEMPLATE = 'confirm_template.html' EMAIL_PAGE_DOMAIN = 'https://multiomix.org' - # Modulector settings MODULECTOR_SETTINGS = { 'host': os.getenv('MODULECTOR_HOST', 'modulector.multiomix.org'), diff --git a/src/multiomics_intermediate/urls.py b/src/multiomics_intermediate/urls.py index f277e55f..e5029a6b 100644 --- a/src/multiomics_intermediate/urls.py +++ b/src/multiomics_intermediate/urls.py @@ -36,4 +36,5 @@ path('molecules/', include('molecules_details.urls')), path('admin/', admin.site.urls), path('email/', include(mail_urls)), + path('users/', include('users.urls')), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/src/user_files/serializers.py b/src/user_files/serializers.py index f6b78b5d..b957cbe0 100644 --- a/src/user_files/serializers.py +++ b/src/user_files/serializers.py @@ -5,12 +5,12 @@ from rest_framework import serializers from common.functions import get_enum_from_value, create_survival_columns_from_json from datasets_synchronization.models import SurvivalColumnsTupleUserFile -from .models_choices import FileType -from .utils import has_uploaded_file_valid_format, get_invalid_format_response -from frontend.serializers import UserSimpleSerializer +from user_files.models_choices import FileType +from user_files.utils import has_uploaded_file_valid_format, get_invalid_format_response +from users.serializers import UserSimpleSerializer from institutions.serializers import InstitutionSimpleSerializer from tags.serializers import TagSerializer -from .models import UserFile +from user_files.models import UserFile class SurvivalColumnsTupleUserFileSimpleSerializer(serializers.ModelSerializer): diff --git a/src/user_files/views.py b/src/user_files/views.py index 7aeff2ef..e3b9cd19 100644 --- a/src/user_files/views.py +++ b/src/user_files/views.py @@ -11,7 +11,7 @@ from rest_framework.exceptions import ValidationError from rest_framework.views import APIView from common.pagination import StandardResultsSetPagination -from .serializers import UserFileSerializer, UserFileWithoutFileObjSerializer +from user_files.serializers import UserFileSerializer, UserFileWithoutFileObjSerializer from .models import UserFile from rest_framework.response import Response diff --git a/src/users/admin.py b/src/users/admin.py new file mode 100644 index 00000000..e69de29b diff --git a/src/users/apps.py b/src/users/apps.py new file mode 100644 index 00000000..72b14010 --- /dev/null +++ b/src/users/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UsersConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'users' diff --git a/src/users/migrations/__init__.py b/src/users/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/users/models.py b/src/users/models.py new file mode 100644 index 00000000..71a83623 --- /dev/null +++ b/src/users/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/src/users/serializers.py b/src/users/serializers.py new file mode 100644 index 00000000..d1edee17 --- /dev/null +++ b/src/users/serializers.py @@ -0,0 +1,67 @@ +from django.forms import ValidationError +from rest_framework import serializers +from django.contrib.auth import get_user_model +from institutions.models import Institution +from django.db import transaction +from django.contrib.auth.hashers import make_password + +from multiomics_intermediate import settings + + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = get_user_model() + fields = ['id', 'username', 'is_superuser', 'first_name', 'last_name'] + + def to_representation(self, instance): + # Adds custom fields + data = super(UserSerializer, self).to_representation(instance) + + # Check if current user has any institutions where he's admin + data['is_institution_admin'] = Institution.objects.filter( + institutionadministration__user=instance, + institutionadministration__is_institution_admin=True + ).exists() + + # User at this point is not anonymous + data['is_anonymous'] = False + + return data + + +class UserSimpleSerializer(serializers.ModelSerializer): + """User serializer with fewer data""" + class Meta: + model = get_user_model() + fields = ['id', 'username'] + +class UserUpdateSerializer(serializers.ModelSerializer): + """"fields that are received to update the user""" + first_name = serializers.CharField() + last_name = serializers.CharField() + password = serializers.CharField(write_only=True, required=False, min_length=8) + + class Meta: + model = get_user_model() + fields = ['first_name', 'last_name', 'password'] + + def update(self, instance, validated_data): + """Updates the users first_name, last_name, and password fields.""" + with transaction.atomic(): + instance.first_name = validated_data.get('first_name', instance.first_name) + instance.last_name = validated_data.get('last_name', instance.last_name) + + password = validated_data.pop('password', '') + + minimum_password_len = settings.MIN_PASSWORD_LEN + password_length = len(password) + if password_length != 0: + if password_length < minimum_password_len: + raise ValidationError(f'Password must be at least {minimum_password_len} chars long') + + instance.set_password(password) + + instance.save() + return instance + \ No newline at end of file diff --git a/src/users/urls.py b/src/users/urls.py new file mode 100644 index 00000000..ae789c04 --- /dev/null +++ b/src/users/urls.py @@ -0,0 +1,16 @@ +from django.conf import settings +from django.urls import path +from users import views + +urlpatterns = [ + path('login', views.login_action, name='login'), + path('authenticate', views.authenticate_action, name='authentication'), + path('create-user', views.create_user_action, name='create_user'), + path('logout', views.logout_action, name='logout'), + path('user', views.CurrentUserView.as_view(), name='current_user'), + path('edit-profile', views.UserRetrieveUpdateView.as_view(), name='update_user') +] + +if settings.DEBUG: + urlpatterns.append(path('new-user-email-test', views.test_new_user_email)) + urlpatterns.append(path('confirmation-email-test', views.test_confirmation_email)) \ No newline at end of file diff --git a/src/users/views.py b/src/users/views.py new file mode 100644 index 00000000..fdfbea18 --- /dev/null +++ b/src/users/views.py @@ -0,0 +1,188 @@ +import logging +from typing import Optional, Union +from django.contrib.auth.base_user import AbstractBaseUser +from django.shortcuts import render, redirect +from django.contrib.auth import authenticate, logout, login +from django.contrib.auth.models import User +from django_email_verification import sendConfirm +from rest_framework import permissions, generics +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated +from rest_framework.views import APIView +from users.serializers import UserSerializer, UserUpdateSerializer +from django.conf import settings +from django.db import transaction, InternalError + + +def login_action(request): + """Login view""" + if request.user.is_authenticated: + return redirect('index') + + return render(request, "frontend/login.html") + + +def view_bad_credentials(request): + """Returns login template with bad credential error in Its context""" + return render(request, "frontend/login.html", {'loginError': 'Your username or password is invalid'}) + + +def authenticate_action(request): + """Authentication view""" + username: Optional[str] = request.POST.get('username') + password: Optional[str] = request.POST.get('password') + if username is None or password is None: + return view_bad_credentials(request) + + user: Union[AbstractBaseUser, AbstractBaseUser, None] = authenticate(username=username, password=password) + + if user is not None: + login(request, user) + # If 'next' param is specified, it redirects to that URL + next_url = request.POST.get('next') + return redirect(next_url if next_url is not None else 'index') + else: + # Checks if User is active + try: + user: User = User.objects.get(username=username) + if user is not None and not user.is_active: + return render( + request, + "frontend/login.html", + { + 'loginWarning': 'Your account has not been validated yet. Please, check your email account for ' + 'our verification email' + } + ) + return view_bad_credentials(request) + except User.DoesNotExist: + return view_bad_credentials(request) + + +def is_sign_up_form_valid(username: str, email: str, password: str, password_repeated: str) -> bool: + """ + Checks if Sign Up form is valid + @param username: Username to check + @param email: Email to check + @param password: Password to check + @param password_repeated: Repeated password to check + @return: True if is all the field valid. False otherwise + """ + password_striped = password.strip() + password_repeated_striped = password_repeated.strip() + return len(username.strip()) > 0 \ + and len(email.strip()) > 0 \ + and len(password_striped) > 0 \ + and len(password_repeated_striped) > 0 \ + and password_striped == password_repeated_striped + + +def user_already_exists_username(username: str) -> bool: + """ + Check if exists any user in the DB with the same username passed by parameter + @param username: Username to check + @return: True if exists the user. False otherwise + """ + return User.objects.filter(username=username).exists() + + +def user_already_exists_email(email: str) -> bool: + """ + Check if exists any user in the DB with the same email passed by parameter + @param email: Email to check + @return: True if exists the user. False otherwise + """ + return User.objects.filter(email=email).exists() + + +@transaction.atomic +def create_user_action(request): + """User creation view""" + username: str = request.POST.get('newUsername', '') + email: str = request.POST.get('email', '') + password: str = request.POST.get('newPassword', '') + password_repeated: str = request.POST.get('newPasswordRepeated', '') + if not is_sign_up_form_valid(username, email, password, password_repeated): + return render(request, "frontend/login.html", {'loginError': 'Invalid fields'}) + + if user_already_exists_username(username): + return render(request, "frontend/login.html", {'loginError': 'There is already a user with that username'}) + + if user_already_exists_email(email): + return render(request, "frontend/login.html", {'loginError': 'There is already a user with that email'}) + + # If it's set to not send an email asking for confirmation just creates the user and redirect to login + if not settings.EMAIL_NEW_USER_CONFIRMATION_ENABLED: + User.objects.create_user(username, email, password) + return render(request, "frontend/login.html", {'loginSuccess': 'User created successfully'}) + + # Creates the user and sends email + # FIXME: library does not return exceptions thrown in sender thread. After a PR it should be tested + error_sending_email = False + try: + with transaction.atomic(): + user = User.objects.create_user(username, email, password) + try: + sendConfirm(user) + except Exception as ex: + logging.warning(f'Error sending email -> {ex}') + raise InternalError # Rollback + except InternalError: + error_sending_email = True + + if error_sending_email: + return render(request, "frontend/login.html", {'loginError': 'An error have occurred. Please, try again. If the' + ' problem persists contact us, please'}) + + # Hides email + idx_at = email.index('@') + idx_to_replace = idx_at - 2 + hidden_email = '*' * idx_to_replace + email[idx_to_replace:] + + return render( + request, + "frontend/login.html", + {'loginWarning': f'We have sent a confirmation email to {hidden_email}'} + ) + + +def test_new_user_email(request): + """ + To test new user email template in browser. Only accessible in DEBUG mode + @param request: Request object + @return: HTTP Response + """ + return render(request, settings.EMAIL_MAIL_HTML, {'link': '#'}) + + +def test_confirmation_email(request): + """ + To test confirmation email template in browser. Only accessible in DEBUG mode + @param request: Request object + @return: HTTP Response + """ + return render(request, settings.EMAIL_PAGE_TEMPLATE, {'success': True}) + + +def logout_action(request): + """Closes User session""" + logout(request) + return redirect('index') + + +class CurrentUserView(APIView): + """Gets current User info""" + permission_classes = [permissions.IsAuthenticated] + + @staticmethod + def get(request): + serializer = UserSerializer(request.user) + return Response(serializer.data) + + +class UserRetrieveUpdateView(generics.RetrieveUpdateAPIView): + serializer_class = UserUpdateSerializer + permission_classes = [IsAuthenticated] + + def get_object(self): + return self.request.user From 39077096b4965393399e7cc0a48d8fd9a2b68076 Mon Sep 17 00:00:00 2001 From: Gonza Date: Wed, 8 Jan 2025 20:21:14 -0300 Subject: [PATCH 20/27] - accidental port change fixed --- docker-compose.dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 45d20a66..84459560 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -5,7 +5,7 @@ services: restart: 'always' container_name: multiomics_dev_db ports: - - '5433:5432' + - '5432:5432' command: postgres -c 'config_file=/etc/postgresql/postgresql.conf' environment: POSTGRES_USER: root From b28387e6097e20683b0a7767a4100c8dffa036e9 Mon Sep 17 00:00:00 2001 From: Gonza Date: Thu, 9 Jan 2025 15:53:44 -0300 Subject: [PATCH 21/27] - Minimum password length fixed --- src/multiomics_intermediate/settings.py | 2 +- src/users/models.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/multiomics_intermediate/settings.py b/src/multiomics_intermediate/settings.py index 52747f9f..a2b18c56 100644 --- a/src/multiomics_intermediate/settings.py +++ b/src/multiomics_intermediate/settings.py @@ -152,7 +152,7 @@ }, ] -MIN_PASSWORD_LEN: int = 8 +MIN_PASSWORD_LEN: int = int(os.getenv('MIN_PASSWORD_LEN', 8)) # Internationalization # https://docs.djangoproject.com/en/3.0/topics/i18n/ diff --git a/src/users/models.py b/src/users/models.py index 71a83623..8b137891 100644 --- a/src/users/models.py +++ b/src/users/models.py @@ -1,3 +1 @@ -from django.db import models -# Create your models here. From ee07c1256faaa7f0f67ec85a504395f70399ed1e Mon Sep 17 00:00:00 2001 From: Gonza Date: Thu, 9 Jan 2025 15:56:54 -0300 Subject: [PATCH 22/27] Change in postgres port fixed --- src/multiomics_intermediate/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/multiomics_intermediate/settings.py b/src/multiomics_intermediate/settings.py index a2b18c56..182a0c45 100644 --- a/src/multiomics_intermediate/settings.py +++ b/src/multiomics_intermediate/settings.py @@ -129,7 +129,7 @@ 'USER': os.getenv('POSTGRES_USERNAME', 'root'), 'PASSWORD': os.getenv('POSTGRES_PASSWORD', 'example'), 'HOST': os.getenv('POSTGRES_HOST', '127.0.0.1'), - 'PORT': os.getenv('POSTGRES_PORT', 5433), + 'PORT': os.getenv('POSTGRES_PORT', 5432), 'NAME': os.getenv('POSTGRES_DB', 'multiomics') # Keep "multiomics" for backward compatibility } } From d7d8171d0bae8d1a1d22b50c6bd80f05d93fdf42 Mon Sep 17 00:00:00 2001 From: Gonza Date: Thu, 9 Jan 2025 16:49:09 -0300 Subject: [PATCH 23/27] Some bugs fixed --- DEPLOYING.md | 2 +- src/users/serializers.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DEPLOYING.md b/DEPLOYING.md index 47e6069b..144f7fcf 100644 --- a/DEPLOYING.md +++ b/DEPLOYING.md @@ -45,7 +45,7 @@ The following are the steps to perform a deployment in production. In case you w - `CGDS_CHUNK_SIZE`: size **in bytes** of the chunk in which the files of a CGDS study are downloaded, the bigger it is, the faster the download is, but the more server memory it consumes. Default `2097152`, i.e. 2MB. - `THRESHOLD_ORDINAL`: number of different values for the GEM (CNA) information to be considered ordinal, if the number is <= to this value then it is considered categorical/ordinal and a boxplot is displayed, otherwise, it is considered continuous and the common correlation graph is displayed. Default `5`. - `THRESHOLD_GEM_SIZE_TO_COLLECT`: GEM file size threshold (in MB) for the GEM dataset to be available in memory. This has a HUGE impact on the performance of the analysis. If the size is less than or equal to this threshold, it is allocated in memory, otherwise, it will be read lazily from the disk. If None GGCA automatically allocates in memory when the GEM dataset size is small (<= 100MB). Therefore, if you want to force to always use RAM to improve performance you should set a very high threshold, on the contrary, if you want a minimum memory usage at the cost of poor performance, set it to `0`. Default `None`. - - `MIN_PASSWORD_LEN`: Defines the minimum required length for user passwords when updating their profile. If the provided password is shorter than this length, the update will be rejected. + - `MIN_PASSWORD_LEN`: Defines the minimum required length for user passwords when updating their profile. If the provided password is shorter than this length, the update will be rejected. Default `8`. - PostgreSQL: - `POSTGRES_USERNAME`: PostgreSQL connection username. **Must be equal to** `POSTGRES_USER`. - `POSTGRES_PASSWORD`: PostgreSQL connection password. **Must be equal to** `POSTGRES_PASSWORD`. diff --git a/src/users/serializers.py b/src/users/serializers.py index d1edee17..d3ea7a20 100644 --- a/src/users/serializers.py +++ b/src/users/serializers.py @@ -53,15 +53,15 @@ def update(self, instance, validated_data): instance.last_name = validated_data.get('last_name', instance.last_name) password = validated_data.pop('password', '') - - minimum_password_len = settings.MIN_PASSWORD_LEN password_length = len(password) + if password_length != 0: + minimum_password_len = settings.MIN_PASSWORD_LEN if password_length < minimum_password_len: raise ValidationError(f'Password must be at least {minimum_password_len} chars long') - - instance.set_password(password) - + else: + instance.set_password(password) + instance.save() return instance \ No newline at end of file From fdb19c238c78fa8fb012247b8b2a3a10ebcc031e Mon Sep 17 00:00:00 2001 From: Juan Date: Thu, 9 Jan 2025 17:12:56 -0300 Subject: [PATCH 24/27] Institution panel --- .../frontend/src/components/MainNavbar.tsx | 12 +- .../institutions/InstitutionModal.tsx | 162 +++++++++++++++--- .../institutions/InstitutionsPanel.tsx | 21 +-- .../RemoveUserFromInstitutionModal.tsx | 50 ------ .../static/frontend/src/utils/interfaces.ts | 8 + .../templates/frontend/institutions.html | 1 + .../models.cpython-312.pyc.2847318479840 | Bin 0 -> 4600 bytes src/institutions/models.py | 2 +- src/institutions/urls.py | 2 + src/institutions/views.py | 29 +++- 10 files changed, 182 insertions(+), 105 deletions(-) delete mode 100644 src/frontend/static/frontend/src/components/institutions/RemoveUserFromInstitutionModal.tsx create mode 100644 src/institutions/__pycache__/models.cpython-312.pyc.2847318479840 diff --git a/src/frontend/static/frontend/src/components/MainNavbar.tsx b/src/frontend/static/frontend/src/components/MainNavbar.tsx index aab4466c..f4e4e5ad 100644 --- a/src/frontend/static/frontend/src/components/MainNavbar.tsx +++ b/src/frontend/static/frontend/src/components/MainNavbar.tsx @@ -176,23 +176,13 @@ const MainNavbar = (props: MainNavbarProps) => { /> } - - {/* Institutions panel (only for user who are admin of at least one institution) */} - {currentUser.is_institution_admin && - - } } {/* Institutions */} - {currentUser && (currentUser.is_superuser || currentUser.is_institution_admin) && + {currentUser && Institutions diff --git a/src/frontend/static/frontend/src/components/institutions/InstitutionModal.tsx b/src/frontend/static/frontend/src/components/institutions/InstitutionModal.tsx index 81b5badc..6e68c90f 100644 --- a/src/frontend/static/frontend/src/components/institutions/InstitutionModal.tsx +++ b/src/frontend/static/frontend/src/components/institutions/InstitutionModal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useContext, useEffect, useRef, useState } from 'react' import { ModalHeader, ModalContent, @@ -6,19 +6,22 @@ import { Icon, Segment, Table, - Input, - Button + Button, + Select } from 'semantic-ui-react' -import { DjangoInstitutionUser, DjangoUserCandidates } from '../../utils/django_interfaces' -import { CustomAlertTypes, Nullable } from '../../utils/interfaces' +import { DjangoInstitutionUser } from '../../utils/django_interfaces' +import { CustomAlertTypes, Nullable, SemanticListItem } from '../../utils/interfaces' import { PaginatedTable } from '../common/PaginatedTable' import { TableCellWithTitle } from '../common/TableCellWithTitle' import { InstitutionTableData } from './InstitutionsPanel' import ky from 'ky' import { getDjangoHeader } from '../../utils/util_functions' +import { CurrentUserContext } from '../Base' declare const urlGetUsersCandidates: string declare const urlEditInstitutionAdmin: string +declare const urlNonUserListInstitution: string +declare const urlAddRemoveUserToInstitution: string export interface InstitutionModalState { isOpen: boolean, @@ -38,16 +41,66 @@ interface Props extends InstitutionModalState { * @returns Component */ export const InstitutionModal = (props: Props) => { - const [userInput, setUserInput] = useState(''); + const [userIdToAdd, setUserIdToAdd] = useState(0); + const [userList, setUserList] = useState([]); + const abortController = useRef(new AbortController()) + const currentUser = useContext(CurrentUserContext) + /** + * Function to add user to institution. + */ const handleAddUser = () => { + if (userIdToAdd) { + const myHeaders = getDjangoHeader() + const body = { + userId: userIdToAdd, + isAdding: true, + institutionId: props.institution?.id + } + ky.post(urlAddRemoveUserToInstitution, { headers: myHeaders, signal: abortController.current.signal, json: body }).then((response) => { + response.json().then((jsonResponse: any) => { + usersListNonInInstitution() + }).catch((err) => { + console.error('Error parsing JSON ->', err) + }) + }).catch((err) => { + console.error('Error getting users ->', err) + }) + } + } + /** + * Function to remove user from institution. + * @param idUserCandidate user id to remove from institution. + */ + const handleRemoveUser = (idUserCandidate: number) => { + const myHeaders = getDjangoHeader() + const body = { + userId: idUserCandidate, + isAdding: false, + institutionId: props.institution?.id + } + ky.post(urlAddRemoveUserToInstitution, { headers: myHeaders, signal: abortController.current.signal, json: body }).then((response) => { + response.json().then((jsonResponse: any) => { + usersListNonInInstitution() + }).catch((err) => { + console.error('Error parsing JSON ->', err) + }) + }).catch((err) => { + console.error('Error getting users ->', err) + }) } - const handleSwitchUserAdmin = (adminSwitched: boolean, id: number) => { + + /** + * Function to manage admin. + * @param adminSwitched switch if true is going to be admin, false if going to be normal user + * @param idInstitution institution id. + */ + const handleSwitchUserAdmin = (adminSwitched: boolean, idInstitution: number) => { const myHeaders = getDjangoHeader() - const editUrl = `${urlEditInstitutionAdmin}/${id}/` + const editUrl = `${urlEditInstitutionAdmin}/${idInstitution}/` ky.patch(editUrl, { headers: myHeaders }).then((response) => { @@ -64,6 +117,36 @@ export const InstitutionModal = (props: Props) => { } + /** + * Function to search users that are not in institution. + */ + const usersListNonInInstitution = () => { + const myHeaders = getDjangoHeader() + + const editUrl = `${urlNonUserListInstitution}/${props.institution?.id}/` + + ky.get(editUrl, { headers: myHeaders, signal: abortController.current.signal }).then((response) => { + response.json().then((jsonResponse: { id: number, username: string }[]) => { + setUserList(jsonResponse.map(user => ({ key: user.id.toString(), value: user.id.toString(), text: user.username }))) + }).catch((err) => { + console.error('Error parsing JSON ->', err) + }) + }).catch((err) => { + console.error('Error getting users ->', err) + }) + + } + + useEffect(() => { + if (props.institution?.is_user_admin && props.institution?.id) { + usersListNonInInstitution() + } + return () => { + abortController.current.abort() + } + }, [props.institution?.id]) + + return ( { {props.institution?.name} - setUserInput(value)} placeholder='Username' /> - +