views.py 15.8 KB
Newer Older
1 2
from __future__ import print_function

Anton Petrov's avatar
Anton Petrov committed
3
"""
4
Copyright [2009-2017] EMBL-European Bioinformatics Institute
Anton Petrov's avatar
Anton Petrov committed
5 6 7 8 9 10 11 12 13 14 15
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
     http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

16
import json
Carlos Eduardo Ribas's avatar
Carlos Eduardo Ribas committed
17 18
import os
import random
19
import re
Anton Petrov's avatar
Anton Petrov committed
20
import requests
21
import six
22

23 24 25 26 27 28
if six.PY2:
    from urlparse import urlparse
elif six.PY3:
    from urllib.parse import urlparse

from django.http import Http404, HttpResponse, HttpResponseForbidden
Anton Petrov's avatar
Anton Petrov committed
29
from django.conf import settings
30 31
from django.shortcuts import render, render_to_response, redirect
from django.template import TemplateDoesNotExist
Anton Petrov's avatar
Anton Petrov committed
32
from django.template.loader import render_to_string
33
from django.views.decorators.cache import cache_page, never_cache
Anton Petrov's avatar
Anton Petrov committed
34
from django.views.generic.base import TemplateView
Anton Petrov's avatar
Anton Petrov committed
35
from django.views.generic.edit import FormView
36

37
from portal.config.expert_databases import expert_dbs
38
from portal.forms import ContactForm
39
from portal.models import Rna, Database, Xref, EnsemblAssembly
40
from portal.models.rna_precomputed import RnaPrecomputed
41
from portal.config.svg_images import examples
42
from portal.rna_summary import RnaSummary
43

44
CACHE_TIMEOUT = 60 * 60 * 24 * 1  # per-view cache timeout in seconds
45
XREF_PAGE_SIZE = 1000
Anton Petrov's avatar
Anton Petrov committed
46 47 48 49 50

########################
# Function-based views #
########################

51

Anton Petrov's avatar
Anton Petrov committed
52
@cache_page(CACHE_TIMEOUT)
53 54
def get_sequence_lineage(request, upi):
    """
55 56 57
    Internal API.
    Get the lineage for an RNA sequence based on the
    classifications from all database cross-references.
58 59
    """
    try:
60 61 62 63 64
        queryset = Xref.objects.filter(upi=upi).select_related('accession')
        results = queryset.filter(deleted='N')
        if not results.exists():
            results = queryset
        json_lineage_tree = _get_json_lineage_tree(results.iterator())
65 66
    except Rna.DoesNotExist:
        raise Http404
67 68 69
    return HttpResponse(json_lineage_tree, content_type="application/json")


70
@cache_page(1)
Anton Petrov's avatar
Anton Petrov committed
71
def homepage(request):
Boris A. Burkov's avatar
Boris A. Burkov committed
72
    """RNAcentral homepage."""
73
    random.shuffle(examples)
74
    context = {
Anton Petrov's avatar
Anton Petrov committed
75
        'databases': list(Database.objects.filter(alive='Y').order_by('?').all()),
Anton Petrov's avatar
Anton Petrov committed
76
        'blog_url': settings.RELEASE_ANNOUNCEMENT_URL,
77
        'svg_images': examples,
78
    }
79

80
    return render(request, 'portal/homepage.html', {'context': context})
Anton Petrov's avatar
Anton Petrov committed
81 82


83 84
@cache_page(CACHE_TIMEOUT)
def expert_databases_view(request):
Boris A. Burkov's avatar
Boris A. Burkov committed
85
    """List of RNAcentral expert databases."""
86 87
    expert_dbs.sort(key=lambda x: x['name'].lower())
    expert_dbs.sort(key=lambda x: x['imported'], reverse=True)
88
    context = {
89
        'expert_dbs': expert_dbs,
90 91
        'num_dbs': len(expert_dbs) - 1,  # Vega is archived
        'num_imported': len([x for x in expert_dbs if x['imported']]) - 1,  # Vega
92 93 94 95
    }
    return render(request, 'portal/expert-databases.html', {'context': context})


96 97
@cache_page(CACHE_TIMEOUT)
def rna_view_redirect(request, upi, taxid):
Boris A. Burkov's avatar
Boris A. Burkov committed
98
    """Redirect from urs_taxid to urs/taxid."""
99
    return redirect('unique-rna-sequence', upi=upi, taxid=taxid, permanent=True)
100 101


Anton Petrov's avatar
Anton Petrov committed
102
@cache_page(CACHE_TIMEOUT)
Anton Petrov's avatar
Anton Petrov committed
103
def rna_view(request, upi, taxid=None):
104 105
    """
    Unique RNAcentral Sequence view.
Anton Petrov's avatar
Anton Petrov committed
106
    Display all annotations or customize the page using the taxid (optional).
107
    """
108
    # get Rna or die
109
    upi = upi.upper()
Anton Petrov's avatar
Anton Petrov committed
110
    try:
111
        rna = Rna.objects.get(upi=upi)
Anton Petrov's avatar
Anton Petrov committed
112 113
    except Rna.DoesNotExist:
        raise Http404
Anton Petrov's avatar
Anton Petrov committed
114

115 116 117 118 119
    try:
        precomputed = RnaPrecomputed.objects.filter(upi=upi, taxid=taxid).get()
    except RnaPrecomputed.DoesNotExist:
        precomputed = None

120
    # if taxid is given, but the RNA does not have annotations for this taxid, redirect to an error page
121
    if taxid and not precomputed:
122 123 124 125
        response = redirect('unique-rna-sequence', upi=upi)
        response['Location'] += '?taxid-not-found={taxid}'.format(taxid=taxid)
        return response

126 127
    taxid_filtering = True if taxid else False

128 129 130
    symbol_counts = rna.count_symbols()
    non_canonical_base_counts = {key: symbol_counts[key] for key in symbol_counts if key not in ['A', 'U', 'G', 'C']}

131
    summary = RnaSummary(upi, taxid, settings.EBI_SEARCH_ENDPOINT)
Anton Petrov's avatar
Anton Petrov committed
132 133 134
    if taxid_filtering:
        summary_text = render_to_string('portal/summary.html', vars(summary))
        summary_text = re.sub(r'\s+', ' ', summary_text.strip())
135 136 137 138
        try:
            summary_so_terms = zip(summary.pretty_so_rna_type, summary.so_rna_type)
        except AttributeError:
            summary_so_terms = ''
Anton Petrov's avatar
Anton Petrov committed
139

140 141 142 143 144 145
    # Check if r2dt-web is installed
    path = os.path.join(
        settings.PROJECT_PATH, 'rnacentral', 'portal', 'static', 'r2dt-web', 'dist', 'r2dt-web.js'
    )
    plugin_installed = True if os.path.isfile(path) else False

Anton Petrov's avatar
Anton Petrov committed
146
    context = {
carlosribas's avatar
carlosribas committed
147
        'upi': upi,
148 149
        'symbol_counts': symbol_counts,
        'non_canonical_base_counts': non_canonical_base_counts,
Anton Petrov's avatar
Anton Petrov committed
150 151
        'taxid': taxid,
        'taxid_filtering': taxid_filtering,
152
        'taxid_not_found': request.GET.get('taxid-not-found', ''),
Anton Petrov's avatar
Anton Petrov committed
153
        'activeTab': 2 if request.GET.get('tab', '').lower() == '2d' else 0,
154 155
        'summary_text': summary_text if taxid_filtering else '',
        'summary': summary,
156
        'summary_so_terms': summary_so_terms if taxid_filtering else '',
157
        'precomputed': precomputed,
158
        'mirna_regulators': rna.get_mirna_regulators(taxid=taxid),
159
        'annotations_from_other_species': rna.get_annotations_from_other_species(taxid=taxid),
Anton Petrov's avatar
Anton Petrov committed
160
        'intact': rna.get_intact(taxid),
161
        'plugin_installed': plugin_installed,
Anton Petrov's avatar
Anton Petrov committed
162
    }
163
    response = render(request, 'portal/sequence.html', {'rna': rna, 'context': context})
Anton Petrov's avatar
Anton Petrov committed
164 165
    # define canonical URL for Google
    response['Link'] = '<{}>; rel="canonical"'.format(request.build_absolute_uri()).replace('http://', 'https://')
166 167 168
    # ask Google not to index non-species specific pages
    if not taxid:
        response['X-Robots-Tag'] = 'noindex'
169 170 171 172 173
    # if the request comes from the API URL, add the header to allow cross domain request
    try:
        response.headers.add('Access-Control-Allow-Origin', '*')
    except AttributeError:
        pass
174
    return response
Anton Petrov's avatar
Anton Petrov committed
175 176


Anton Petrov's avatar
Anton Petrov committed
177 178
@cache_page(CACHE_TIMEOUT)
def expert_database_view(request, expert_db_name):
Boris A. Burkov's avatar
Boris A. Burkov committed
179
    """Expert database view."""
180 181 182 183 184
    expert_db_name = expert_db_name.upper()
    expert_db = None
    for db in expert_dbs:
        if db['name'].upper() == expert_db_name and db['imported']:
            expert_db = db
Anton Petrov's avatar
Anton Petrov committed
185 186
        elif db['label'].upper() == expert_db_name.upper() and db['imported']:
            expert_db = db
187 188 189 190
        elif expert_db_name == 'TMRNA-WEBSITE' and db['label'].upper() == expert_db_name:
            expert_db = db

    if not expert_db:
Anton Petrov's avatar
Anton Petrov committed
191 192
        raise Http404()

193 194 195 196
    return render_to_response('portal/expert-database.html', {
        'expert_db': expert_db,
    })

Anton Petrov's avatar
Anton Petrov committed
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221

@never_cache
def website_status_view(request):
    """
    This view will be monitored by Nagios for the presence
    of string "All systems operational".
    """
    def _is_database_up():
        try:
            rna = Rna.objects.all()[0]
            return True
        except:
            return False

    def _is_api_up():
        return True

    def _is_search_up():
        return True

    context = dict()
    context['is_database_up'] = _is_database_up()
    context['is_api_up'] = _is_api_up()
    context['is_search_up'] = _is_search_up()
    context['overall_status'] = context['is_database_up'] and context['is_api_up'] and context['is_search_up']
Anton Petrov's avatar
Anton Petrov committed
222
    return render_to_response('portal/website-status.html', {'context': context})
Anton Petrov's avatar
Anton Petrov committed
223

Anton Petrov's avatar
Anton Petrov committed
224

225
@cache_page(CACHE_TIMEOUT)
226
def proxy(request):
Anton Petrov's avatar
Anton Petrov committed
227
    """
228 229
    Internal API. Used for:
     - EBeye search URL - bypasses EBeye same-origin policy.
Anton Petrov's avatar
Anton Petrov committed
230
     - Rfam and miRBase images - avoids mixed content warnings due to lack of https support in Rfam.
Anton Petrov's avatar
Anton Petrov committed
231 232
    """
    url = request.GET['url']
233 234 235

    # check domain for security - we don't want someone to abuse this endpoint
    domain = urlparse(url).netloc
Anton Petrov's avatar
Anton Petrov committed
236 237
    if domain != 'www.ebi.ac.uk' and domain != 'wwwdev.ebi.ac.uk' and domain != 'rfam.org' and domain != 'www.mirbase.org':
        return HttpResponseForbidden("This proxy is for www.ebi.ac.uk, wwwdev.ebi.ac.uk, mirbase.org or rfam.org only.")
238

Anton Petrov's avatar
Anton Petrov committed
239
    try:
240 241
        proxied_response = requests.get(url)
        if proxied_response.status_code == 200:
242 243
            if domain == 'rfam.org':  # for rfam images don't forget to set content-type header
                response = HttpResponse(proxied_response.text, content_type="image/svg+xml")
Anton Petrov's avatar
Anton Petrov committed
244 245
            elif 'mirbase.org' in domain:
                response = HttpResponse(proxied_response.content, content_type="image/png")
246 247
            else:
                response = HttpResponse(proxied_response.text)
Anton Petrov's avatar
Anton Petrov committed
248 249 250 251 252 253
            return response
        else:
            raise Http404
    except:
        raise Http404

254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275

def external_link(request, expert_db, external_id):
    """
    Provide a flexible way to link to RNAcentral by providing a database and external URL.
    """
    search_url = '{base_url}?query=expert_db:"{expert_db}" "{external_id}"&format=json'.format(
        base_url=settings.EBI_SEARCH_ENDPOINT,
        expert_db=expert_db,
        external_id=external_id)
    try:
        response = requests.get(search_url)
        if response.status_code == 200:
            data = response.json()
            if data['hitCount'] == 1:
                upi, taxid = data['entries'][0]['id'].split('_')
                return redirect('unique-rna-sequence', upi=upi, taxid=int(taxid))
            else:
                return redirect('/search?q=expert_db:"{}" "{}"'.format(expert_db, external_id))
    except:
        return redirect('/search?q=expert_db:"{}" "{}"'.format(expert_db, external_id))


Anton Petrov's avatar
Anton Petrov committed
276 277 278 279 280
#####################
# Class-based views #
#####################

class StaticView(TemplateView):
281
    """Render flat pages."""
Anton Petrov's avatar
Anton Petrov committed
282
    def get(self, request, page, *args, **kwargs):
Anton Petrov's avatar
Anton Petrov committed
283
        self.template_name = 'portal/' + page + '.html'
Anton Petrov's avatar
Anton Petrov committed
284 285 286 287 288 289 290
        response = super(StaticView, self).get(request, *args, **kwargs)
        try:
            return response.render()
        except TemplateDoesNotExist:
            raise Http404()


Boris A. Burkov's avatar
Boris A. Burkov committed
291
class GenomeBrowserView(TemplateView):
292
    """Render genome-browser, taking into account start/end locations."""
293
    def get(self, request, *args, **kwargs):
Boris A. Burkov's avatar
Boris A. Burkov committed
294 295
        self.template_name = 'portal/genome-browser.html'

296
        # if species is not defined - use homo_sapiens as default, if specified and wrong - 404
297
        if 'species' in request.GET:
298
            kwargs['genome'] = request.GET['species']
carlosribas's avatar
carlosribas committed
299 300 301
            try:
                ensembl_assembly = EnsemblAssembly.objects.filter(ensembl_url=kwargs['genome']).all()[0]
            except IndexError:
302 303 304
                raise Http404
        else:
            kwargs['genome'] = 'homo_sapiens'
Boris A. Burkov's avatar
Boris A. Burkov committed
305
            ensembl_assembly = EnsemblAssembly.objects.get(ensembl_url='homo_sapiens')
306

307 308 309 310 311 312
        # require chromosome, start and end in kwargs or use default location for this species
        if ('chromosome' in request.GET or 'chr' in request.GET) and 'start' in request.GET and 'end' in request.GET:
            if 'chromosome' in request.GET:
                kwargs['chromosome'] = request.GET['chromosome']
            elif 'chr' in request.GET:
                kwargs['chromosome'] = request.GET['chr']
Boris A. Burkov's avatar
Boris A. Burkov committed
313 314 315
            kwargs['start'] = request.GET['start']
            kwargs['end'] = request.GET['end']
        else:
316
            kwargs['chromosome'] = ensembl_assembly.example_chromosome
317 318
            kwargs['start'] = ensembl_assembly.example_start
            kwargs['end'] = ensembl_assembly.example_end
Boris A. Burkov's avatar
Boris A. Burkov committed
319 320

        response = super(GenomeBrowserView, self).get(request, *args, **kwargs)
321
        return response.render()
Boris A. Burkov's avatar
Boris A. Burkov committed
322

323

Anton Petrov's avatar
Anton Petrov committed
324
class ContactView(FormView):
325
    """Contact form view."""
Anton Petrov's avatar
Anton Petrov committed
326 327 328 329 330 331
    template_name = 'portal/contact.html'
    form_class = ContactForm

    def form_valid(self, form):
        # This method is called when valid form data has been POSTed.
        # It should return an HttpResponse.
332 333 334 335
        if form.send_email():
            return redirect('contact-us-success')
        else:
            return redirect('error')
Anton Petrov's avatar
Anton Petrov committed
336

337

Anton Petrov's avatar
Anton Petrov committed
338 339 340 341
####################
# Helper functions #
####################

342

343
def _get_json_lineage_tree(xrefs):
Anton Petrov's avatar
Anton Petrov committed
344
    """
345 346
    Combine lineages from multiple xrefs to produce a single species tree.
    The data are used by the d3 library.
Anton Petrov's avatar
Anton Petrov committed
347 348
    """

349
    def get_lineages_and_taxids():
350
        """Combine the lineages from all accessions in a single list."""
351
        if isinstance(xrefs, list):
352 353 354 355 356 357 358
            for xref in xrefs:
                lineages.add(xref[0])
                taxids[xref[0].split('; ')[-1]] = xref[1]
        else:
            for xref in xrefs:
                lineages.add(xref.accession.classification)
                taxids[xref.accession.classification.split('; ')[-1]] = xref.taxid
Anton Petrov's avatar
Anton Petrov committed
359 360

    def build_nested_dict_helper(path, text, container):
361
        """Recursive function that builds the nested dictionary."""
Anton Petrov's avatar
Anton Petrov committed
362 363 364 365 366
        segs = path.split('; ')
        head = segs[0]
        tail = segs[1:]
        if not tail:
            # store how many time the species is seen
367 368 369 370 371 372 373
            try:
                if head in container:
                    container[head] += 1
                else:
                    container[head] = 1
            except:
                container = {}
Anton Petrov's avatar
Anton Petrov committed
374 375
                container[head] = 1
        else:
376 377 378 379 380 381
            try:
                if head not in container:
                    container[head] = {}
            except:
                container = {}
                container[head] = 1
Anton Petrov's avatar
Anton Petrov committed
382 383 384 385
            build_nested_dict_helper('; '.join(tail), text, container[head])

    def get_nested_dict(lineages):
        """
386 387 388 389 390 391 392 393 394 395
        Transform a list like this:
            items = [
                'A; C; X; human',
                'A; C; X; human',
                'B; D; Y; mouse',
                'B; D; Z; rat',
                'B; D; Z; rat',
            ]
        into a nested dictionary like this:
            {'root': {'A': {'C': {'X': {'human': 2}}}, 'B': {'D': {'Y': {'mouse': 1}, 'Z': {'rat': 2}}}}}
Anton Petrov's avatar
Anton Petrov committed
396 397 398 399 400 401 402 403
        """
        container = {}
        for lineage in lineages:
            build_nested_dict_helper(lineage, lineage, container)
        return container

    def get_nested_tree(data, container):
        """
404 405 406 407
        Transform a nested dictionary like this:
            {'root': {'A': {'C': {'X': {'human': 2}}}, 'B': {'D': {'Y': {'mouse': 1}, 'Z': {'rat': 2}}}}}
        into a json file like this (fragment shown):
            {"name":"A","children":[{"name":"C","children":[{"name":"X","children":[{"name":"human","size":2}]}]}]}
Anton Petrov's avatar
Anton Petrov committed
408 409 410 411 412 413
        """
        if not container:
            container = {
                "name": 'All',
                "children": []
            }
414
        for name, children in six.iteritems(data):
Anton Petrov's avatar
Anton Petrov committed
415 416 417
            if isinstance(children, int):
                container['children'].append({
                    "name": name,
418 419
                    "size": children,
                    "taxid": taxids[name],
Anton Petrov's avatar
Anton Petrov committed
420 421 422 423 424 425 426 427 428
                })
            else:
                container['children'].append({
                    "name": name,
                    "children": []
                })
                get_nested_tree(children, container['children'][-1])
        return container

429
    lineages = set()
430 431
    taxids = dict()
    get_lineages_and_taxids()
Anton Petrov's avatar
Anton Petrov committed
432 433 434
    nodes = get_nested_dict(lineages)
    json_lineage_tree = get_nested_tree(nodes, {})
    return json.dumps(json_lineage_tree)
435 436 437 438 439 440 441 442 443 444 445 446 447


def handler500(request, *args, **argv):
    """
    Customized version of handler500 with status_code = 200 in order
    to make EBI load balancer to proxy pass to this view, instead of displaying 500.

    https://stackoverflow.com/questions/17662928/django-creating-a-custom-500-404-error-page
    """
    # warning: in django2 signature of this function has changed
    response = render_to_response("500.html", {})
    response.status_code = 200
    return response