Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions src/api_service/mrna_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
TermRelationType,
)
import modulector_sdk as modulector
from modulector_sdk import PaginatedResponse
from modulector_sdk import PaginatedResponse, SupportType


class MRNAService(object):
Expand Down Expand Up @@ -101,6 +101,29 @@ def get_mirna_target_interactions(
logging.exception(ex)
return self._paginated_to_dict(None)

def get_mirna_target_validations(
self,
mirna: Optional[str] = None,
target: Optional[str] = None,
support_type: Optional[str] = None,
experiment: Optional[str] = None,
ordering: Optional[str] = None,
page: Optional[int] = None,
page_size: Optional[int] = None,
) -> Dict:
"""Get experimentally validated miRNA-target interactions from miRTarBase."""
result = modulector.get_mirna_target_validations(
mirna=mirna,
target=target,
support_type=cast(Optional[SupportType], support_type),
experiment=experiment,
ordering=ordering,
page=page,
page_size=page_size,
base_url=self._modulector_base_url,
)
return self._paginated_to_dict(result)

def get_diseases(self, mirna: Optional[str] = None, page: Optional[int] = None,
page_size: Optional[int] = None) -> Dict:
"""Get miRNA disease associations from Modulector SDK."""
Expand Down Expand Up @@ -310,4 +333,4 @@ def get_drugs_regulating_gene(self, gene_id: str) -> Optional[Dict]:
return None


global_mrna_service = MRNAService()
global_mrna_service = MRNAService()
12 changes: 12 additions & 0 deletions src/api_service/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@
from django.contrib.auth import get_user_model


class MirnaTargetValidationSerializer(serializers.Serializer):
"""Read-only representation of one miRTarBase validation record."""
id = serializers.IntegerField(read_only=True)
mirtarbase_id = serializers.CharField(read_only=True)
mirna = serializers.CharField(read_only=True)
gene = serializers.CharField(read_only=True)
target_gene_entrez_id = serializers.JSONField(read_only=True, allow_null=True)
experiments = serializers.ListField(child=serializers.CharField(), read_only=True)
support_type = serializers.CharField(read_only=True)
pmid = serializers.JSONField(read_only=True, allow_null=True)


class GeneGEMCombinationSerializer(serializers.ModelSerializer):
"""GeneGEMCombination serializer"""
gene_extra_data = GeneForResultTableSerializer(source='gene', read_only=True)
Expand Down
149 changes: 149 additions & 0 deletions src/api_service/tests/test_mirna_target_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import json
from types import SimpleNamespace
from unittest.mock import patch

import requests
from django.test import RequestFactory, SimpleTestCase
from django.urls import reverse

from api_service.mrna_service import MRNAService
from api_service.serializers import MirnaTargetValidationSerializer
from api_service.views import get_mirna_target_validation_action


class MirnaTargetValidationTests(SimpleTestCase):
def setUp(self):
self.factory = RequestFactory()

def request(self, params):
request = self.factory.get(reverse('mirna_target_validation'), params)
request.user = SimpleNamespace(is_authenticated=True)
return request

def test_requires_mirna_or_target(self):
"""The endpoint rejects requests that cannot identify a miRNA-target pair."""
response = get_mirna_target_validation_action(self.request({}))
self.assertEqual(response.status_code, 400)

def test_only_accepts_get(self):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agregar docstrings explicando qué es lo que chequea cada test

"""The endpoint only permits GET requests."""
request = self.factory.post(reverse('mirna_target_validation'))
request.user = SimpleNamespace(is_authenticated=True)
response = get_mirna_target_validation_action(request)
self.assertEqual(response.status_code, 405)

def test_forwards_query_parameters_and_serializes_paginated_results(self):
"""The view forwards query parameters and returns read-only serialized records."""
result = {
'id': 1,
'mirtarbase_id': 'MIRT000001',
'mirna': 'hsa-miR-122-5p',
'gene': 'SLC7A1',
'target_gene_entrez_id': '10558',
'experiments': ['Western blot'],
'support_type': 'Functional MTI',
'pmid': '12345678',
}
page = {'count': 1, 'next': '', 'previous': '', 'results': [result]}
with patch('api_service.views.global_mrna_service.get_mirna_target_validations', return_value=page) as get_data:
response = get_mirna_target_validation_action(self.request({
'mirna': 'hsa-miR-122-5p',
'target': 'SLC7A1',
'support_type': 'Functional MTI',
'experiment': 'Western blot',
'page': '2',
'page_size': '20',
'ordering': '-gene',
}))

self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content), page)
get_data.assert_called_once_with(
mirna='hsa-miR-122-5p', target='SLC7A1',
support_type='Functional MTI', experiment='Western blot',
page='2', page_size='20', ordering='-gene',
)

def test_accepts_target_without_mirna(self):
"""A target-only query is forwarded to Modulector and returns its empty page."""
page = {'count': 0, 'next': '', 'previous': '', 'results': []}
with patch('api_service.views.global_mrna_service.get_mirna_target_validations', return_value=page) as get_data:
response = get_mirna_target_validation_action(self.request({'target': 'SLC7A1'}))

self.assertEqual(response.status_code, 200)
get_data.assert_called_once_with(
mirna=None, target='SLC7A1', support_type=None, experiment=None,
ordering=None, page=None, page_size=None,
)

def test_preserves_upstream_pagination_links(self):
"""Pagination metadata from Modulector is returned without rewriting its links."""
page = {
'count': 11,
'next': 'https://web/mirna-target-validation/?target=SLC7A1&page=2',
'previous': '',
'results': [],
}
with patch('api_service.views.global_mrna_service.get_mirna_target_validations', return_value=page):
response = get_mirna_target_validation_action(self.request({'target': 'SLC7A1'}))

self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content), page)

def test_accepts_multi_field_ordering(self):
"""The view forwards a multi-field ordering value to the SDK service."""
page = {'count': 0, 'next': '', 'previous': '', 'results': []}
with patch('api_service.views.global_mrna_service.get_mirna_target_validations', return_value=page) as get_data:
response = get_mirna_target_validation_action(self.request({
'target': 'SLC7A1', 'ordering': 'mirna, -gene'
}))
self.assertEqual(response.status_code, 200)
get_data.assert_called_once_with(
mirna=None, target='SLC7A1', support_type=None, experiment=None,
ordering='mirna, -gene', page=None, page_size=None,
)

def test_upstream_failure_is_not_reported_as_no_data(self):
"""An upstream service failure is reported as a gateway error, not an empty result."""
with patch('api_service.views.global_mrna_service.get_mirna_target_validations',
side_effect=requests.Timeout), \
patch('api_service.views.logging.exception'):
response = get_mirna_target_validation_action(self.request({'target': 'SLC7A1'}))
self.assertEqual(response.status_code, 502)

@patch('api_service.mrna_service.modulector.get_mirna_target_validations')
def test_service_calls_sdk(self, get_validations):
"""The service forwards all supported miRTarBase filters and pagination to the SDK."""
get_validations.return_value = SimpleNamespace(
count=1, next=None, previous=None, results=[{'gene': 'SLC7A1'}]
)
result = MRNAService().get_mirna_target_validations(
mirna='hsa-miR-122-5p', target='SLC7A1',
support_type='Functional MTI', experiment='Western blot',
ordering='-gene', page=1
)

self.assertEqual(result['results'], [{'gene': 'SLC7A1'}])
self.assertEqual(result['count'], 1)
self.assertEqual(get_validations.call_args.kwargs['target'], 'SLC7A1')
self.assertEqual(get_validations.call_args.kwargs['support_type'], 'Functional MTI')
self.assertEqual(get_validations.call_args.kwargs['experiment'], 'Western blot')
self.assertEqual(get_validations.call_args.kwargs['ordering'], '-gene')
self.assertEqual(get_validations.call_args.kwargs['page'], 1)

def test_result_serializer_is_read_only(self):
"""The result serializer represents Modulector records without accepting writes."""
record = {
'id': 1,
'mirtarbase_id': 'MIRT000001',
'mirna': 'hsa-miR-122-5p',
'gene': 'SLC7A1',
'target_gene_entrez_id': None,
'experiments': ['Western blot'],
'support_type': 'Functional MTI',
'pmid': None,
}
serializer = MirnaTargetValidationSerializer(data=record)

self.assertTrue(serializer.is_valid())
self.assertEqual(serializer.validated_data, {})
2 changes: 2 additions & 0 deletions src/api_service/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
# MiRNA Interaction
path('mirna-target-interaction', views.get_mirna_target_interaction_action,
name='mirna_target_interaction'),
path('mirna-target-validation', views.get_mirna_target_validation_action,
name='mirna_target_validation'),
# MiRNA Disease associated
path('mirna-disease', views.get_mirna_diseases_action, name='mirna_diseases'),
path('mirna-drugs', views.get_mirna_drugs_action, name='mirna_drugs'),
Expand Down
31 changes: 30 additions & 1 deletion src/api_service/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import logging
import requests
from typing import Optional, Dict, Tuple, List, Type, OrderedDict, Union, cast, Any

import numpy as np
Expand Down Expand Up @@ -53,7 +54,7 @@
from .permissions import ExperimentIsNotRunning
from .serializers import ExperimentSerializer, ExperimentSerializerDetail, \
GeneMiRNACombinationSerializer, GeneCNACombinationSerializer, GeneMethylationCombinationSerializer, \
ExperimentClinicalSourceSerializer, LimitedUserSerializer
ExperimentClinicalSourceSerializer, LimitedUserSerializer, MirnaTargetValidationSerializer
from .tasks import eval_mrna_gem_experiment
from .utils import get_experiment_source, file_type_to_experiment_type, get_cgds_dataset
from institutions.models import Institution
Expand Down Expand Up @@ -955,6 +956,34 @@ def get_mirna_target_interaction_action(request):
return JsonResponse(data)


@login_required
@require_http_methods(['GET'])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hacer igual que la vista de arriba. No debería ser más complejo

def get_mirna_target_validation_action(request):
"""Return experimentally validated miRNA-target interactions from miRTarBase."""
mirna = request.GET.get('mirna')
target = request.GET.get('target')

if not mirna and not target:
return JsonResponse(data={"error": "Param 'mirna' or 'target' are mandatory"}, status=400)

try:
data = global_mrna_service.get_mirna_target_validations(
mirna=mirna,
target=target,
support_type=request.GET.get('support_type'),
experiment=request.GET.get('experiment'),
ordering=request.GET.get('ordering'),
page=request.GET.get('page'),
page_size=request.GET.get('page_size'),
)
except (requests.RequestException, ValueError):
logging.exception('Modulector miRNA-target validation request failed')
return JsonResponse({'error': 'Modulector validation service is unavailable'}, status=502)

data['results'] = MirnaTargetValidationSerializer(instance=data['results'], many=True).data
return JsonResponse(data)


@login_required
def get_mirna_diseases_action(request):
"""Searches in papers miRNA associations with diseases"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { GeneGemModalMenu } from './GeneGemModalMenu'
import { NoClinicalData } from './survival-analysis/NoClinicalData'
import { KaplanMeierChart } from './survival-analysis/KaplanMeier'
import { MiRNATargetInteractionPanel } from './MiRNATargetInteractionPanel'
import { MiRNATargetValidationPanel } from './MiRNATargetValidationPanel'
import { TryAgainSegment } from '../../../common/TryAgainSegment'
import { COLOR_YELLOW_FILL, COLOR_YELLOW_STROKE } from '../../../../utils/constants'
import { useIntl, IntlShape } from 'react-intl'
Expand All @@ -38,6 +39,7 @@ enum ActiveItemMenu {
STATISTICAL_PROPERTIES,
ASSUMPTIONS,
MIRNA_TARGET_INTERACTION,
MIRNA_TARGET_VALIDATION,
MIRNA_INTERACTION,
DISEASES_ASSOCIATION,
DRUGS_ASSOCIATION,
Expand Down Expand Up @@ -245,6 +247,8 @@ class GeneGemDetailsModal extends React.Component<GeneGemDetailsModalProps, Gene
miRNA={gem}
/>
)
case ActiveItemMenu.MIRNA_TARGET_VALIDATION:
return <MiRNATargetValidationPanel gene={gene} miRNA={gem} />
case ActiveItemMenu.MIRNA_INTERACTION:
// NOTE: it's inside a React.Fragment to force componentDidMount execution
// in PaginatedTable component. Otherwise, React will see miRNA interaction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,23 @@ export const GeneGemModalMenu = (props: GeneGemModalMenuProps) => {
/>
</Menu.Item>

<Menu.Item
active={props.activeItem === ActiveItemMenu.MIRNA_TARGET_VALIDATION}
onClick={() => props.setActiveItem(ActiveItemMenu.MIRNA_TARGET_VALIDATION)}
>
{intl.formatMessage({ id: 'geneGemModalMenu.mirnaTargetValidation' })}

<InfoPopup
content={intl.formatMessage(
{ id: 'geneGemModalMenu.mirnaTargetValidation.info' },
{ gene: props.gene, gem: props.gem }
)}
onTop={false}
onEvent='hover'
extraClassName='margin-left-5'
/>
</Menu.Item>

<Menu.Item
active={props.activeItem === ActiveItemMenu.MIRNA_INTERACTION}
onClick={() => props.setActiveItem(ActiveItemMenu.MIRNA_INTERACTION)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React from 'react'
import { Table } from 'semantic-ui-react'
import { RowHeader } from '../../../../utils/django_interfaces'
import { PaginatedTable } from '../../../common/PaginatedTable'
import { useIntl } from 'react-intl'
import { PubmedButton } from './PubmedButton'

declare const urlMiRNATargetValidation: string

/** One experimentally validated miRNA-target record returned by Modulector. */
interface MiRNATargetValidation {
id: number,
mirtarbase_id: string,
mirna: string,
gene: string,
target_gene_entrez_id: string | number,
experiments: string[],
support_type: string,
pmid: string | number | null
}

interface MiRNATargetValidationPanelProps {
/** Gene selected in the current experiment result row. */
gene: string,
/** miRNA selected in the current experiment result row. */
miRNA: string
}

/**
* Shows miRTarBase experimental evidence for the selected miRNA-gene pair.
* @param props Selected miRNA and gene identifiers.
* @returns A paginated table of validation records.
*/
export const MiRNATargetValidationPanel = (props: MiRNATargetValidationPanelProps) => {
const intl = useIntl()
const headers: RowHeader<MiRNATargetValidation>[] = [
{ name: intl.formatMessage({ id: 'miRNATargetValidationPanel.mirtarbaseId' }) },
{ name: intl.formatMessage({ id: 'miRNATargetValidationPanel.supportType' }) },
{ name: intl.formatMessage({ id: 'miRNATargetValidationPanel.experiments' }) },
{ name: intl.formatMessage({ id: 'miRNATargetValidationPanel.targetGeneEntrezId' }) },
{ name: intl.formatMessage({ id: 'miRNATargetValidationPanel.pubmed' }), textAlign: 'center' }
]

return (
<PaginatedTable<MiRNATargetValidation>
headerTitle={intl.formatMessage(
{ id: 'miRNATargetValidationPanel.headerTitle' },
{ miRNA: props.miRNA, gene: props.gene }
)}
headers={headers}
urlToRetrieveData={urlMiRNATargetValidation}
queryParams={{ mirna: props.miRNA, target: props.gene }}
defaultSortProp={{ sortField: 'gene', sortOrderAscendant: true }}
mapFunction={(validation) => {
const pubmedUrl = validation.pmid
? `https://pubmed.ncbi.nlm.nih.gov/${encodeURIComponent(String(validation.pmid))}/`
: ''

return (
<Table.Row key={validation.id}>
<Table.Cell>{validation.mirtarbase_id || '-'}</Table.Cell>
<Table.Cell>{validation.support_type || '-'}</Table.Cell>
<Table.Cell>{validation.experiments?.join(', ') || '-'}</Table.Cell>
<Table.Cell>{validation.target_gene_entrez_id || '-'}</Table.Cell>
<Table.Cell textAlign='center'>
{pubmedUrl ? <PubmedButton pubmedURL={pubmedUrl} /> : '-'}
</Table.Cell>
</Table.Row>
)
}}
/>
)
}
Loading
Loading