Commit 9f1dd1ac authored by Kenzo-Hugo Hillion's avatar Kenzo-Hugo Hillion
Browse files

Merge branch 'benchmark-frontend' into 'dev'

Add gene detail page on frontend

Closes #61 and #56

See merge request !17
parents e59f15d7 9ca50215
Pipeline #18076 passed with stages
in 2 minutes and 32 seconds
import pandas as pd
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import status
from rest_framework import filters, status
from rest_framework.decorators import action
from rest_framework.response import Response
......@@ -43,6 +43,8 @@ class DocGeneLength(object):
class GeneViewSet(BulkViewSet):
search_fields = ['gene_name']
filter_backends = (filters.SearchFilter,)
queryset = Gene.objects.select_related('taxonomy').prefetch_related('functions').all()
serializer_class = GeneSerializer
lookup_field = 'gene_id'
......
from collections import OrderedDict
from rest_framework import serializers
class AsymetricSlugRelatedField(serializers.SlugRelatedField):
def to_representation(self, value):
return self.serializer_class(value).data
# Get choices used by DRF autodoc and expect to_representation() to return an ID
# We overload to use item.pk instead of to_representation()
def get_choices(self, cutoff=None):
queryset = self.get_queryset()
if queryset is None:
return {}
if cutoff is not None:
queryset = queryset[:cutoff]
return OrderedDict([
(
item.pk,
self.display_value(item)
)
for item in queryset
])
# DRF skip validations when it only has id, we deactivate that
def use_pk_only_optimization(self):
return False
@classmethod
def from_serializer(cls, serializer, name=None, *args, **kwargs):
if name is None:
name = f"{serializer.__class__.__name__}AsymetricSlugAutoField"
return type(name, (cls,), {"serializer_class": serializer})(*args, **kwargs)
......@@ -5,7 +5,11 @@ from rest_framework import serializers
from rest_framework.utils import model_meta
from metagenedb.apps.catalog.models import Function, Gene, GeneFunction, Taxonomy
from .asymetricslugrelatedfield import AsymetricSlugRelatedField
from .bulk_list import BulkListSerializer
from .function import FunctionSerializer
from .taxonomy import SimpleTaxonomySerializer
_LOGGER = logging.getLogger(__name__)
......@@ -48,13 +52,15 @@ class GeneListSerializer(BulkListSerializer):
class GeneSerializer(serializers.ModelSerializer):
functions = serializers.SlugRelatedField(
functions = AsymetricSlugRelatedField.from_serializer(
FunctionSerializer,
queryset=Function.objects.all(),
slug_field='function_id',
many=True,
required=False,
)
taxonomy = serializers.SlugRelatedField(
taxonomy = AsymetricSlugRelatedField.from_serializer(
SimpleTaxonomySerializer,
queryset=Taxonomy.objects.all(),
slug_field='tax_id',
required=False,
......
from rest_framework import serializers
from metagenedb.apps.catalog.models import Taxonomy
from .asymetricslugrelatedfield import AsymetricSlugRelatedField
from .bulk_list import BulkListSerializer
class SimpleTaxonomySerializer(serializers.ModelSerializer):
class Meta:
model = Taxonomy
fields = ('tax_id', 'name')
class TaxonomyListSerializer(BulkListSerializer):
class Meta:
......@@ -12,48 +20,57 @@ class TaxonomyListSerializer(BulkListSerializer):
class TaxonomySerializer(serializers.ModelSerializer):
rank = serializers.CharField(required=False)
parent_tax_id = serializers.SlugRelatedField(
parent_tax_id = AsymetricSlugRelatedField.from_serializer(
SimpleTaxonomySerializer,
queryset=Taxonomy.objects.all(),
slug_field='tax_id',
source='parent',
required=False,
)
superkingdom = serializers.SlugRelatedField(
superkingdom = AsymetricSlugRelatedField.from_serializer(
SimpleTaxonomySerializer,
queryset=Taxonomy.objects.all(),
slug_field='tax_id',
required=False
)
kingdom = serializers.SlugRelatedField(
kingdom = AsymetricSlugRelatedField.from_serializer(
SimpleTaxonomySerializer,
queryset=Taxonomy.objects.all(),
slug_field='tax_id',
required=False
)
phylum = serializers.SlugRelatedField(
phylum = AsymetricSlugRelatedField.from_serializer(
SimpleTaxonomySerializer,
queryset=Taxonomy.objects.all(),
slug_field='tax_id',
required=False
)
class_rank = serializers.SlugRelatedField(
class_rank = AsymetricSlugRelatedField.from_serializer(
SimpleTaxonomySerializer,
queryset=Taxonomy.objects.all(),
slug_field='tax_id',
required=False
)
order = serializers.SlugRelatedField(
order = AsymetricSlugRelatedField.from_serializer(
SimpleTaxonomySerializer,
queryset=Taxonomy.objects.all(),
slug_field='tax_id',
required=False
)
family = serializers.SlugRelatedField(
family = AsymetricSlugRelatedField.from_serializer(
SimpleTaxonomySerializer,
queryset=Taxonomy.objects.all(),
slug_field='tax_id',
required=False
)
genus = serializers.SlugRelatedField(
genus = AsymetricSlugRelatedField.from_serializer(
SimpleTaxonomySerializer,
queryset=Taxonomy.objects.all(),
slug_field='tax_id',
required=False
)
species = serializers.SlugRelatedField(
species = AsymetricSlugRelatedField.from_serializer(
SimpleTaxonomySerializer,
queryset=Taxonomy.objects.all(),
slug_field='tax_id',
required=False
......
<template>
<v-flex xs12 md6 lg4>
<v-card>
<v-toolbar class="genefunction white--text" dense>
<v-icon class="white--text">dialpad</v-icon>
<v-toolbar-title class="genefunction white--text">
KEGG Function
</v-toolbar-title>
</v-toolbar>
<div class="text-xs-center" v-if="kegg_id == '' || request_done == false">
<v-progress-circular
indeterminate
color="genefunction"
></v-progress-circular>
</div>
<div v-else-if="kegg_details.length == 0" class="text-xs-center">
<v-btn block color="tertiary">
No annotation for this entry
</v-btn>
</div>
<v-list v-else>
<!-- Basic information about the KEGG KO-->
<template v-for="(item, index) in kegg_details">
<v-divider
v-if="index > 0"
:key="index + '_details'"
></v-divider>
<v-list-tile :key="item.title">
<v-list-tile-content>
<v-list-tile-title v-html="item.title"></v-list-tile-title>
<v-list-tile-sub-title v-html="item.content"></v-list-tile-sub-title>
</v-list-tile-content>
<v-list-tile-action v-if="item.url">
<v-tooltip right>
<template v-slot:activator="{ on }">
<v-btn :href="item.url" flat icon class="genefunction--text text--darken-1" target="_blank" v-on="on">
<v-icon>open_in_new</v-icon>
</v-btn>
</template>
<span>Open in KEGG</span>
</v-tooltip>
</v-list-tile-action>
</v-list-tile>
</template>
<!-- Expandable details (pathways and diseases) -->
<template v-for="(expanditem, expandindex) in kegg_expand_details">
<v-expansion-panel v-if="expanditem.content.length > 0" :key="expandindex">
<v-divider
:key="expanditem.title"
></v-divider>
<v-expansion-panel-content>
<template v-slot:header>
<div>{{ expanditem.title }}</div>
</template>
<v-card>
<v-list>
<template v-for="(item, index) in expanditem.content">
<v-divider
v-if="index > 0"
:key="index + '_expanded'"
></v-divider>
<v-list-tile :key="item.id" class="tertiary">
<v-list-tile-content>
<v-list-tile-title v-html="item.id"></v-list-tile-title>
<v-list-tile-sub-title v-html="item.name"></v-list-tile-sub-title>
</v-list-tile-content>
<v-list-tile-action>
<v-tooltip right>
<template v-slot:activator="{ on }">
<v-btn :href="item.url" flat icon class="genefunction--text text--darken-1" target="_blank" v-on="on">
<v-icon>open_in_new</v-icon>
</v-btn>
</template>
<span>Open in KEGG</span>
</v-tooltip>
</v-list-tile-action>
</v-list-tile>
</template>
</v-list>
</v-card>
</v-expansion-panel-content>
</v-expansion-panel>
</template>
<!-- References -->
<v-expansion-panel v-if="kegg_references.length > 0">
<v-divider/>
<v-expansion-panel-content>
<template v-slot:header>
<div>References</div>
</template>
<v-card>
<v-list>
<template v-for="(item, index) in kegg_references">
<v-divider
v-if="index > 0"
:key="index + '_ref'"
></v-divider>
<v-list-tile :key="item.id" class="tertiary">
<v-list-tile-content>
<v-list-tile-title v-html="item.title"></v-list-tile-title>
<v-list-tile-sub-title v-html="item.authors"></v-list-tile-sub-title>
</v-list-tile-content>
<v-list-tile-action>
<v-tooltip right>
<template v-slot:activator="{ on }">
<v-btn :href="item.url" flat icon class="genefunction--text text--darken-1" target="_blank" v-on="on">
<v-icon>open_in_new</v-icon>
</v-btn>
</template>
<span>Open in pubmed</span>
</v-tooltip>
</v-list-tile-action>
</v-list-tile>
</template>
</v-list>
</v-card>
</v-expansion-panel-content>
</v-expansion-panel>
</v-list>
</v-card>
</v-flex>
</template>
<script>
import axios from 'axios';
export default {
name: 'KeggCard',
props: {
kegg_id: String,
},
watch: {
kegg_id: function (val, oldVal) {
this.getKeggDetail();
}
},
data() {
return {
kegg_details: [],
kegg_expand_details: [],
kegg_references: [],
request_done: false,
};
},
methods: {
getKeggDetail() {
if (this.kegg_id == "no_kegg") {
this.request_done = true;
return;
}
axios.get('http://togows.org/entry/kegg-orthology/' + this.kegg_id + '.json', {
headers: {
Accept: 'application/json',
},
})
.then((response) => {
this.buildKeggDetails(response);
this.buildKeggExpandDetails(response);
this.buildReferences(response);
})
.catch((error) => {
console.log(error);
})
.finally(() => {
this.request_done = true
});
},
buildKeggDetails(response) {
this.kegg_details = [
{
title: 'ID',
content: response.data[0].entry_id,
url: 'https://www.genome.jp/dbget-bin/www_bget?ko:' + response.data[0].entry_id,
},
{
title: 'Name',
content: response.data[0].name,
},
{
title: 'Definition',
content: response.data[0].definition,
},
];
},
buildKeggExpandDetails(response) {
this.kegg_expand_details = [
this.buildPathways(response),
this.buildDiseases(response),
];
},
buildPathways(response) {
var pathways = {
title: 'Pathways',
content: [],
}
Object.entries(response.data[0].pathways).forEach(([key,value])=>{
pathways.content.push(
{
id: key,
name: value,
url: 'https://www.genome.jp/kegg-bin/show_pathway?' + key + '+' + this.kegg_id,
}
);
});
return pathways;
},
buildDiseases(response) {
var diseases = {
title: 'Diseases',
content: [],
}
Object.entries(response.data[0].diseases).forEach(([key,value])=>{
diseases.content.push(
{
id: key,
name: value,
url: 'https://www.genome.jp/dbget-bin/www_bget?ds:' + key,
}
);
});
return diseases;
},
buildReferences(response) {
for (var i=0; i < response.data[0].references.length; i++) {
this.kegg_references.push(
{
title: response.data[0].references[i].title,
authors: response.data[0].references[i].authors[0] + ' et al. ' + response.data[0].references[i].journal,
url: 'https://www.ncbi.nlm.nih.gov/pubmed/' + response.data[0].references[i].pubmed,
}
);
};
},
}
}
</script>
<template>
<v-flex xs12 md6 lg4>
<v-card>
<v-toolbar class="taxonomy white--text" dense>
<v-icon class="white--text">account_tree</v-icon>
<v-toolbar-title class="taxonomy white--text">
Taxonomy
</v-toolbar-title>
</v-toolbar>
<div class="text-xs-center" v-if="taxonomy_id == '' || request_done == false">
<v-progress-circular
indeterminate
color="taxonomy"
></v-progress-circular>
</div>
<div v-else-if="taxonomy_detail.length == 0" class="text-xs-center">
<v-btn block color="tertiary">
No annotation for this entry
</v-btn>
</div>
<v-list v-else>
<template v-for="(item, index) in taxonomy_detail">
<v-divider
v-if="index > 0"
:key="index"
></v-divider>
<v-list-tile v-if="item.content" :key="item.title">
<v-list-tile-content :key="item.title">
<v-list-tile-title v-html="item.title"></v-list-tile-title>
<v-list-tile-sub-title v-html="item.content.name"></v-list-tile-sub-title>
</v-list-tile-content>
</v-list-tile>
</template>
</v-list>
</v-card>
</v-flex>
</template>
<script>
import axios from 'axios';
export default {
name: 'TaxonomyCard',
props: {
taxonomy_id: String,
},
watch: {
taxonomy_id: function (val, oldVal) {
this.getTaxonomyDetail();
}
},
data() {
return {
taxonomy_detail: [],
request_done: false,
};
},
methods: {
getTaxonomyDetail() {
if (this.taxonomy_id == 'no_taxonomy') {
this.request_done = true;
return;
}
axios.get('/api/catalog/v1/taxonomy/' + this.taxonomy_id, {
headers: {
Accept: 'application/json',
},
})
.then((response) => {
this.taxonomy_detail = [
{
title: 'Superkingdom',
content: response.data.superkingdom,
},
{
title: 'Kingdom',
content: response.data.kingdom,
},
{
title: 'Phylum',
content: response.data.phylum,
},
{
title: 'Class',
content: response.data.class_rank,
},
{
title: 'Order',
content: response.data.order,
},
{
title: 'Family',
content: response.data.family,
},
{
title: 'Genus',
content: response.data.genus,
},
{
title: 'Species',
content: response.data.species,
},
];
})
.catch((error) => {
console.log(error);
})
.finally(() => {
this.request_done = true
});
}
}
}
</script>
......@@ -12,6 +12,9 @@ Vue.use(Vuetify, {
theme: {
primary: '#263238',
secondary: '#508991',
tertiary: '#f5edf0',
genefunction: '#65b891',
taxonomy: '#886f68',
},
});
......
......@@ -2,6 +2,7 @@ import Vue from 'vue';
import Router from 'vue-router';
import Home from '@/views/Home.vue';
import GeneDetail from '@/views/GeneDetail.vue';
import Genes from '@/views/Genes.vue';
import Catalogstats from '@/views/Stats.vue';
......@@ -26,6 +27,11 @@ export default new Router({
name: 'genes',
component: Genes,
},
{
path: '/gene-detail/:id',
name: 'gene-detail',
component: GeneDetail,
},
{
path: '/about',
name: 'about',
......
<template>
<div class="gene-detail">
<v-flex>
<v-toolbar class="secondary white--text" dense>
<v-btn dark icon>
<v-icon>list_alt</v-icon>
</v-btn>
<v-toolbar-title>Gene details: {{ gene_id }}</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn dark icon>
<v-icon>search</v-icon>
</v-btn>
</v-toolbar>
<v-card>
<v-container
fluid
grid-list-md
>
<v-layout row wrap>
<!-- General information -->
<v-flex xs12 md6 lg4>
<v-card>
<v-toolbar class="primary white--text" dense>
<v-icon class="white--text">format_list_bulleted</v-icon>
<v-toolbar-title>
Gene information
</v-toolbar-title>
</v-toolbar>
<v-list>
<template v-for="(item, index) in gene_detail">
<v-divider
v-if="index > 0"
:key="index"
></v-divider>
<v-list-tile :key="item.title">
<v-list-tile-content :key="item.content">
<v-list-tile-title v-html="item.title"></v-list-tile-title>
<v-list-tile-sub-title v-html="item.content"></v-list-tile-sub-title>
</v-list-tile-content>
</v-list-tile>
</template>
</v-list>
</v-card>
</v-flex>
<!-- KEGG Function -->
<KeggCard :kegg_id='kegg_id'></KeggCard>