├── static ├── .gitkeep └── css │ └── main.css ├── gennotes_server ├── __init__.py ├── tests │ ├── __init__.py │ ├── expected_data │ │ ├── relation_create.json │ │ ├── relation_put.json │ │ ├── variant_create.json │ │ ├── relation.json │ │ ├── relation_patch.json │ │ ├── variant.json │ │ ├── variant_patch2.json │ │ ├── variant_put_patch1.json │ │ ├── specified_variant_list.json │ │ ├── relation_list.json │ │ └── variant_list.json │ ├── test_helpers.py │ ├── test_relation.py │ └── test_variant.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── add_clinvar_data.py ├── migrations │ ├── __init__.py │ ├── 0004_auto_20160318_1926.py │ ├── 0002_add_clinvar_bot_user.py │ ├── 0003_editingapplication.py │ └── 0001_initial.py ├── templates │ ├── account │ │ ├── login.html │ │ ├── signup.html │ │ ├── login-form.html │ │ └── signup-form.html │ ├── gennotes_server │ │ └── home.html │ ├── base.html │ └── api_guide │ │ └── guide.html ├── pagination.py ├── forms.py ├── wsgi.py ├── utils.py ├── permissions.py ├── urls.py ├── models.py ├── settings.py ├── serializers.py ├── views.py └── fixtures │ └── test-data.json ├── Procfile ├── .pep257 ├── bin └── post_compile ├── .gitignore ├── CONTRIBUTORS.txt ├── manage.py ├── requirements.txt ├── LICENSE ├── env.example ├── README.md └── .pylintrc /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gennotes_server/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gennotes_server/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gennotes_server/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gennotes_server/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gennotes_server/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/css/main.css: -------------------------------------------------------------------------------- 1 | /* Add CSS overrides here */ 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn gennotes_server.wsgi --log-file - 2 | -------------------------------------------------------------------------------- /.pep257: -------------------------------------------------------------------------------- 1 | [pep257] 2 | ignore = D100,D102,D200,D203,D204,D205,D400 3 | -------------------------------------------------------------------------------- /bin/post_compile: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ./manage.py syncdb --noinput 4 | ./manage.py migrate --noinput 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled python 2 | *.pyc 3 | 4 | # Local files 5 | .env 6 | db.sqlite3 7 | static-files/ 8 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Chris Ball / @cjb (http://printf.net) 2 | Matej Usaj / @usajusaj (https://github.com/usajusaj) -------------------------------------------------------------------------------- /gennotes_server/templates/account/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |

Log in

5 | {% include 'account/login-form.html' %} 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /gennotes_server/templates/account/signup.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |

Make an account

5 | {% include 'account/signup-form.html' %} 6 | 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /gennotes_server/pagination.py: -------------------------------------------------------------------------------- 1 | from rest_framework.pagination import PageNumberPagination 2 | 3 | 4 | class PageNumberPaginationUpTo1000(PageNumberPagination): 5 | page_size_query_param = 'page_size' 6 | max_page_size = 1000 7 | -------------------------------------------------------------------------------- /gennotes_server/tests/expected_data/relation_create.json: -------------------------------------------------------------------------------- 1 | { 2 | "current_version": "Unknown", 3 | "tags": { 4 | "other-test-tag": "some-value", 5 | "type": "test-relation" 6 | }, 7 | "url": "http://testserver/api/relation/11/", 8 | "variant": "http://testserver/api/variant/1/" 9 | } 10 | -------------------------------------------------------------------------------- /gennotes_server/tests/expected_data/relation_put.json: -------------------------------------------------------------------------------- 1 | { 2 | "current_version": "Unknown", 3 | "tags": { 4 | "comment": "All other tags deleted!", 5 | "type": "clinvar-rcva" 6 | }, 7 | "url": "http://testserver/api/relation/1/", 8 | "variant": "http://testserver/api/variant/10/" 9 | } 10 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gennotes_server.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /gennotes_server/tests/expected_data/variant_create.json: -------------------------------------------------------------------------------- 1 | { 2 | "b37_id": "b37-1-123456-A-G", 3 | "current_version": "Unknown", 4 | "url": "http://testserver/api/variant/11/", 5 | "relation_set": [], 6 | "tags": { 7 | "ref_allele_b37": "A", 8 | "pos_b37": "123456", 9 | "chrom_b37": "1", 10 | "var_allele_b37": "G" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /gennotes_server/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import EditingApplication 4 | 5 | 6 | class EditingAppRegistrationForm(forms.ModelForm): 7 | 8 | class Meta: 9 | model = EditingApplication 10 | fields = ('name', 'description', 'redirect_uris') 11 | widgets = { 12 | 'description': forms.Textarea(attrs={'cols': 80, 'rows': 8}), 13 | } 14 | -------------------------------------------------------------------------------- /gennotes_server/templates/gennotes_server/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | 4 | {% block content %} 5 |

GenNotes

6 | 7 | {% if user.is_authenticated %} 8 |

9 | Welcome! 10 |

11 | {% else %} 12 | Log in 13 | Create account 14 | {% endif %} 15 | 16 | {% endblock content %} 17 | -------------------------------------------------------------------------------- /gennotes_server/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for gennotes_server project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gennotes_server.settings") 13 | 14 | from django.core.wsgi import get_wsgi_application 15 | from whitenoise.django import DjangoWhiteNoise 16 | 17 | application = DjangoWhiteNoise(get_wsgi_application()) 18 | -------------------------------------------------------------------------------- /gennotes_server/migrations/0004_auto_20160318_1926.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.4 on 2016-03-18 19:26 3 | from __future__ import unicode_literals 4 | 5 | import django.contrib.postgres.fields.jsonb 6 | from django.db import migrations 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('gennotes_server', '0003_editingapplication'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='relation', 18 | name='tags', 19 | field=django.contrib.postgres.fields.jsonb.JSONField(), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /gennotes_server/migrations/0002_add_clinvar_bot_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.contrib.auth import get_user_model 5 | from django.db import migrations 6 | 7 | 8 | def add_clinvar_bot_user(apps, schema_editor): 9 | usernames = ['clinvar-data-importer'] 10 | for username in usernames: 11 | get_user_model().objects.get_or_create(username=username) 12 | 13 | 14 | class Migration(migrations.Migration): 15 | 16 | dependencies = [ 17 | ('gennotes_server', '0001_initial'), 18 | ] 19 | 20 | operations = [ 21 | migrations.RunPython(add_clinvar_bot_user), 22 | ] 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.9.4 2 | PyYAML==3.11 3 | Pygments==2.1.3 4 | argparse==1.2.1 5 | blessings==1.6 6 | bpython==0.15 7 | configparser==3.5.0b2 8 | curtsies==0.2.6 9 | dj-database-url==0.4.0 10 | dj-static==0.0.6 11 | django-allauth==0.25.2 12 | django-braces==1.8.1 13 | django-cors-headers==1.1.0 14 | django-extensions==1.6.1 15 | django-oauth-toolkit==0.10.0 16 | django-rest-swagger==0.3.5 17 | django-reversion==1.10.1 18 | django-sslify==0.2.7 19 | django-toolbelt==0.0.1 20 | djangorestframework==3.3.3 21 | env-tools==2.0.0 22 | greenlet==0.4.9 23 | gunicorn==19.4.5 24 | oauthlib==1.0.3 25 | psycopg2==2.6.1 26 | python-openid==2.2.5 27 | requests==2.9.1 28 | requests-oauthlib==0.6.1 29 | six==1.10.0 30 | static3==0.6.1 31 | tini==3.0.1 32 | wcwidth==0.1.6 33 | whitenoise==2.0.6 34 | wsgiref==0.1.2 35 | -------------------------------------------------------------------------------- /gennotes_server/tests/expected_data/relation.json: -------------------------------------------------------------------------------- 1 | { 2 | "current_version": 11, 3 | "tags": { 4 | "clinvar-rcva:accession": "RCV000116253", 5 | "clinvar-rcva:esp-allele-frequency": "0.014089016971", 6 | "clinvar-rcva:gene-name": "agrin", 7 | "clinvar-rcva:gene-symbol": "AGRN", 8 | "clinvar-rcva:num-submissions": "1", 9 | "clinvar-rcva:preferred-name": "NM_198576.3(AGRN):c.1058A>G (p.Gln353Arg)", 10 | "clinvar-rcva:record-status": "current", 11 | "clinvar-rcva:significance": "Likely benign", 12 | "clinvar-rcva:trait-name": "not specified", 13 | "clinvar-rcva:trait-type": "Disease", 14 | "clinvar-rcva:version": "2", 15 | "type": "clinvar-rcva" 16 | }, 17 | "url": "http://testserver/api/relation/1/", 18 | "variant": "http://testserver/api/variant/10/" 19 | } 20 | -------------------------------------------------------------------------------- /gennotes_server/tests/expected_data/relation_patch.json: -------------------------------------------------------------------------------- 1 | { 2 | "current_version": "Unknown", 3 | "tags": { 4 | "clinvar-rcva:accession": "RCV000116253", 5 | "clinvar-rcva:esp-allele-frequency": "0.014089016971", 6 | "clinvar-rcva:gene-name": "agrin", 7 | "clinvar-rcva:gene-symbol": "AGRN", 8 | "clinvar-rcva:num-submissions": "1", 9 | "clinvar-rcva:preferred-name": "NM_198576.3(AGRN):c.1058A>G (p.Gln353Arg)", 10 | "clinvar-rcva:record-status": "current", 11 | "clinvar-rcva:significance": "Likely benign", 12 | "clinvar-rcva:trait-name": "not specified", 13 | "clinvar-rcva:trait-type": "Disease", 14 | "clinvar-rcva:version": "2", 15 | "comment": "All other tags preserved.", 16 | "type": "clinvar-rcva" 17 | }, 18 | "url": "http://testserver/api/relation/1/", 19 | "variant": "http://testserver/api/variant/10/" 20 | } 21 | -------------------------------------------------------------------------------- /gennotes_server/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from distutils import util 4 | 5 | 6 | def to_bool(env, default='false'): 7 | """ 8 | Convert a string to a bool. 9 | """ 10 | return bool(util.strtobool(os.getenv(env, default))) 11 | 12 | 13 | def map_chrom_to_index(chrom_str): 14 | "Converts chromosome labels to standard GenNotes chromosome index." 15 | if chrom_str.startswith('chr' or 'Chr'): 16 | chrom_str = chrom_str[3:] 17 | if chrom_str.startswith('ch' or 'Ch'): 18 | chrom_str = chrom_str[2:] 19 | try: 20 | return str(int(chrom_str)) 21 | except ValueError: 22 | if chrom_str == 'X': 23 | return '23' 24 | elif chrom_str == 'Y': 25 | return '24' 26 | elif chrom_str in ['M', 'MT']: 27 | return '25' 28 | raise ValueError("Can't determine chromosome for {0}".format(chrom_str)) 29 | -------------------------------------------------------------------------------- /gennotes_server/tests/expected_data/variant.json: -------------------------------------------------------------------------------- 1 | { 2 | "b37_id": "b37-1-883516-G-A", 3 | "current_version": 1, 4 | "relation_set": [ 5 | { 6 | "current_version": 18, 7 | "tags": { 8 | "clinvar-rcva:accession": "RCV000064926", 9 | "clinvar-rcva:gene-name": "nucleolar complex associated 2 homolog (S. cerevisiae)", 10 | "clinvar-rcva:gene-symbol": "NOC2L", 11 | "clinvar-rcva:num-submissions": "1", 12 | "clinvar-rcva:preferred-name": "NM_015658.3(NOC2L):c.1654C>T (p.Leu552=)", 13 | "clinvar-rcva:record-status": "current", 14 | "clinvar-rcva:significance": "not provided", 15 | "clinvar-rcva:trait-name": "Malignant melanoma", 16 | "clinvar-rcva:trait-type": "Disease", 17 | "clinvar-rcva:version": "2", 18 | "type": "clinvar-rcva" 19 | }, 20 | "url": "http://testserver/api/relation/8/", 21 | "variant": "http://testserver/api/variant/1/" 22 | } 23 | ], 24 | "tags": { 25 | "chrom_b37": "1", 26 | "pos_b37": "883516", 27 | "ref_allele_b37": "G", 28 | "var_allele_b37": "A" 29 | }, 30 | "url": "http://testserver/api/variant/1/" 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License (Expat) 2 | 3 | Copyright (c) 2015 PersonalGenomes.org and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /gennotes_server/tests/expected_data/variant_patch2.json: -------------------------------------------------------------------------------- 1 | { 2 | "b37_id": "b37-1-883516-G-A", 3 | "current_version": "Unknown", 4 | "relation_set": [ 5 | { 6 | "current_version": "Unknown", 7 | "tags": { 8 | "clinvar-rcva:accession": "RCV000064926", 9 | "clinvar-rcva:gene-name": "nucleolar complex associated 2 homolog (S. cerevisiae)", 10 | "clinvar-rcva:gene-symbol": "NOC2L", 11 | "clinvar-rcva:num-submissions": "1", 12 | "clinvar-rcva:preferred-name": "NM_015658.3(NOC2L):c.1654C>T (p.Leu552=)", 13 | "clinvar-rcva:record-status": "current", 14 | "clinvar-rcva:significance": "not provided", 15 | "clinvar-rcva:trait-name": "Malignant melanoma", 16 | "clinvar-rcva:trait-type": "Disease", 17 | "clinvar-rcva:version": "2", 18 | "type": "clinvar-rcva" 19 | }, 20 | "url": "http://testserver/api/relation/8/", 21 | "variant": "http://testserver/api/variant/1/" 22 | } 23 | ], 24 | "tags": { 25 | "chrom_b37": "1", 26 | "pos_b37": "883516", 27 | "ref_allele_b37": "G", 28 | "test_tag": "test_value_2", 29 | "var_allele_b37": "A" 30 | }, 31 | "url": "http://testserver/api/variant/1/" 32 | } 33 | -------------------------------------------------------------------------------- /gennotes_server/tests/expected_data/variant_put_patch1.json: -------------------------------------------------------------------------------- 1 | { 2 | "b37_id": "b37-1-883516-G-A", 3 | "current_version": "Unknown", 4 | "relation_set": [ 5 | { 6 | "current_version": "Unknown", 7 | "tags": { 8 | "clinvar-rcva:accession": "RCV000064926", 9 | "clinvar-rcva:gene-name": "nucleolar complex associated 2 homolog (S. cerevisiae)", 10 | "clinvar-rcva:gene-symbol": "NOC2L", 11 | "clinvar-rcva:num-submissions": "1", 12 | "clinvar-rcva:preferred-name": "NM_015658.3(NOC2L):c.1654C>T (p.Leu552=)", 13 | "clinvar-rcva:record-status": "current", 14 | "clinvar-rcva:significance": "not provided", 15 | "clinvar-rcva:trait-name": "Malignant melanoma", 16 | "clinvar-rcva:trait-type": "Disease", 17 | "clinvar-rcva:version": "2", 18 | "type": "clinvar-rcva" 19 | }, 20 | "url": "http://testserver/api/relation/8/", 21 | "variant": "http://testserver/api/variant/1/" 22 | } 23 | ], 24 | "tags": { 25 | "chrom_b37": "1", 26 | "pos_b37": "883516", 27 | "ref_allele_b37": "G", 28 | "test_tag": "test_value", 29 | "var_allele_b37": "A" 30 | }, 31 | "url": "http://testserver/api/variant/1/" 32 | } 33 | -------------------------------------------------------------------------------- /gennotes_server/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | from oauth2_provider.ext.rest_framework.permissions import TokenHasScope 3 | from allauth.account.models import EmailAddress 4 | 5 | 6 | class EditAuthorizedOrReadOnly(TokenHasScope): 7 | """ 8 | Verify permissions for viewing and editing GenNotes Variants and Relations. 9 | - Read only methods: always have permission. (GET, HEAD, OPTIONS) 10 | - Other methods require authorization. 11 | - OAuth2 auth requires: valid token, valid scope, and verified 12 | email address for the target user. 13 | - Other auth require: verified email address for the request.user 14 | """ 15 | def has_permission(self, request, view): 16 | if request.method in permissions.SAFE_METHODS: 17 | return True 18 | else: 19 | if request.auth and hasattr(request.auth, 'scope'): 20 | required_scopes = self.get_scopes(request, view) 21 | token_valid = request.auth.is_valid(required_scopes) 22 | user_verified = EmailAddress.objects.get( 23 | user=request.user).verified 24 | return token_valid and user_verified 25 | if request.user and request.user.is_authenticated(): 26 | # Avoiding try/except; we think this will work for any user. 27 | return EmailAddress.objects.get(user=request.user).verified 28 | 29 | return False 30 | -------------------------------------------------------------------------------- /gennotes_server/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls import include, url 3 | from django.conf.urls.static import static 4 | from django.contrib import admin 5 | from django.views.generic import TemplateView 6 | 7 | from rest_framework import routers 8 | 9 | from .views import (CurrentUserView, 10 | EditingAppRegistration, 11 | EditingAppUpdate, 12 | RelationViewSet, 13 | VariantViewSet) 14 | 15 | router = routers.DefaultRouter() 16 | 17 | router.register(r'relation', RelationViewSet) 18 | router.register(r'variant', VariantViewSet) 19 | 20 | urlpatterns = [ 21 | url(r'^admin/', include(admin.site.urls)), 22 | 23 | url(r'^oauth2-app/applications/register', EditingAppRegistration.as_view(), 24 | name='oauth2_provider:register'), 25 | url(r'^oauth2-app/applications/(?P\d+)/update/$', 26 | EditingAppUpdate.as_view(), name="update"), 27 | url(r'^oauth2-app/', include('oauth2_provider.urls', 28 | namespace='oauth2_provider')), 29 | 30 | url(r'^api/', include(router.urls)), 31 | url(r'^api/me/$', CurrentUserView.as_view(), name='current-user'), 32 | 33 | url(r'^api-auth/', include('rest_framework.urls', 34 | namespace='rest_framework')), 35 | url(r'^api-docs/', include('rest_framework_swagger.urls')), 36 | url(r'^api-guide/?', 37 | TemplateView.as_view(template_name='api_guide/guide.html'), 38 | name='api-guide'), 39 | 40 | url(r'^$', 41 | TemplateView.as_view(template_name='gennotes_server/home.html'), 42 | name='home'), 43 | 44 | # django-allauth URLs 45 | url(r'^accounts/', include('allauth.urls')), 46 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 47 | -------------------------------------------------------------------------------- /gennotes_server/migrations/0003_editingapplication.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from django.conf import settings 6 | import oauth2_provider.validators 7 | import oauth2_provider.generators 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ('gennotes_server', '0002_add_clinvar_bot_user'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='EditingApplication', 20 | fields=[ 21 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 22 | ('client_id', models.CharField(default=oauth2_provider.generators.generate_client_id, unique=True, max_length=100, db_index=True)), 23 | ('redirect_uris', models.TextField(help_text='Allowed URIs list, space separated', blank=True, validators=[oauth2_provider.validators.validate_uris])), 24 | ('client_type', models.CharField(max_length=32, choices=[('confidential', 'Confidential'), ('public', 'Public')])), 25 | ('authorization_grant_type', models.CharField(max_length=32, choices=[('authorization-code', 'Authorization code'), ('implicit', 'Implicit'), ('password', 'Resource owner password-based'), ('client-credentials', 'Client credentials')])), 26 | ('client_secret', models.CharField(default=oauth2_provider.generators.generate_client_secret, max_length=255, db_index=True, blank=True)), 27 | ('name', models.CharField(max_length=255, blank=True)), 28 | ('skip_authorization', models.BooleanField(default=False)), 29 | ('description', models.CharField(max_length=300)), 30 | ('user', models.ForeignKey(related_name='gennotes_server_editingapplication', to=settings.AUTH_USER_MODEL)), 31 | ], 32 | options={ 33 | 'abstract': False, 34 | }, 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | # We use environment variables to define deployment specific settings and 2 | # secret keys (which should never be stored in git). 3 | # 4 | # In addition to setting environment variables for your system, you may 5 | # instead define them in a file named ".env". (This can be easier for some 6 | # setups, e.g. local development.) 7 | # 8 | # This is an example file. To set up a local version, copy this file to 9 | # ".env" (note the leading period!) and add the appropriate settings. 10 | 11 | ###################################################################### 12 | # Required settings 13 | ###################################################################### 14 | 15 | # A long, random string used for cryptographic functions within Django 16 | # SECRET_KEY="abcdefghijABCDEFGHIJ0123456789!@#$%^&*()" 17 | SECRET_KEY="" 18 | 19 | # The PostgreSQL database URL. 20 | # e.g. for local dev with a database 'mydb', user 'jdoe', password 'pa55wd': 21 | DATABASE_URL="postgres://jdoe:pa55wd@localhost/mydb" 22 | 23 | ###################################################################### 24 | # Other settings 25 | ###################################################################### 26 | 27 | # You probably want DEBUG on when doing local development. 28 | # This also turns off django-sslify; runserver doesn't provide SSL. 29 | DEBUG="True" 30 | 31 | # Email backend. For local development you can use console backend by 32 | # setting EMAIL_BACKEND to 'django.core.mail.backends.console.EmailBackend' 33 | # and leave the other settings unspecified. 34 | # EMAIL_BACKEND = '' 35 | # EMAIL_USE_TLS = '' 36 | # EMAIL_HOST = '' 37 | # EMAIL_HOST_USER = '' 38 | # EMAIL_HOST_PASSWORD = '' 39 | # EMAIL_PORT = '' 40 | 41 | # If you can grant superuser priveleges to your PostgreSQL database user, 42 | # the following is not needed. 43 | # But if you aren't able to grant superuser privileges, you'll have to 44 | # add the hstore field by running the PostgreSQL command on your database: 45 | # `CREATE EXTENSION IF NOT EXISTS "hstore";`. Then set PSQL_USER_IS_SUPERUSER 46 | # to "False" to skip attempted automatic creation of the field. 47 | # PSQL_USER_IS_SUPERUSER="False" 48 | -------------------------------------------------------------------------------- /gennotes_server/templates/account/login-form.html: -------------------------------------------------------------------------------- 1 |
3 | 4 | {% csrf_token %} 5 | 6 | {% if redirect_field_value %} 7 | 9 | {% endif %} 10 | 11 | {% if form.errors %} 12 |
13 |

14 | Error: The password didn't match the username or email 15 | you provided. Please try again. 16 |

17 |
18 | {% endif %} 19 | 20 |
21 | 23 | 24 |
25 | 28 | {% if form.login.errors %} 29 | {% for error in form.login.errors %} 30 | {{ error }} 31 | {% endfor %} 32 | {% endif %} 33 |
34 |
35 | 36 |
37 | 39 | 40 |
41 | 44 | {% if form.password.errors %} 45 | {% for error in form.password.errors %} 46 | {{ error }} 47 | {% endfor %} 48 | {% endif %} 49 |
50 |
51 | 52 |
53 |
54 |

55 | 56 |

57 |

58 | 59 | Forgot your password? 60 |

61 |

62 | Create an account? 63 |

64 |
65 |
66 | 67 |
68 | -------------------------------------------------------------------------------- /gennotes_server/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.conf import settings 5 | import django.contrib.postgres.fields.hstore 6 | from django.contrib.postgres.operations import HStoreExtension 7 | from django.db import models, migrations 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('reversion', '0002_auto_20141216_1509'), 14 | ] 15 | 16 | operations = [ 17 | x for x in [HStoreExtension()] if settings.PSQL_USER_IS_SUPERUSER] + [ 18 | migrations.CreateModel( 19 | name='CommitDeletion', 20 | fields=[ 21 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 22 | ('deletion', models.BooleanField(default=True)), 23 | ('revision', models.ForeignKey(to='reversion.Revision')), 24 | ], 25 | ), 26 | migrations.CreateModel( 27 | name='Relation', 28 | fields=[ 29 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 30 | ('tags', django.contrib.postgres.fields.hstore.HStoreField()), 31 | ], 32 | ), 33 | migrations.CreateModel( 34 | name='Variant', 35 | fields=[ 36 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 37 | ('tags', django.contrib.postgres.fields.hstore.HStoreField()), 38 | ], 39 | ), 40 | migrations.AddField( 41 | model_name='relation', 42 | name='variant', 43 | field=models.ForeignKey(to='gennotes_server.Variant'), 44 | ), 45 | migrations.RunSQL( 46 | 'CREATE INDEX "gennotes_server_variant_tags_chrom_b37_idx" on ' 47 | "gennotes_server_variant USING btree (( tags->'chrom-b37' )); " 48 | 'CREATE INDEX "gennotes_server_variant_tags_pos_b37_idx" on ' 49 | "gennotes_server_variant USING btree (( tags->'pos-b37' ));" 50 | 'CREATE INDEX "gennotes_server_variant_tags_ref_allele_b37_idx" on ' 51 | "gennotes_server_variant USING btree (( tags->'ref-allele-b37' ));" 52 | 'CREATE INDEX "gennotes_server_variant_tags_var_allele_b37_idx" on ' 53 | "gennotes_server_variant USING btree (( tags->'var-allele-b37' ));" 54 | 'CREATE INDEX "gennotes_server_relation_type_idx" on ' 55 | "gennotes_server_relation USING btree (( tags->'type' ));" 56 | ), 57 | ] 58 | -------------------------------------------------------------------------------- /gennotes_server/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | 3 | 4 | 5 | {% block head_title %}GenNotes{% endblock %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 55 | 56 | 57 |
58 | {% block content %} 59 | {% endblock content %} 60 |
61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /gennotes_server/tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | from rest_framework.test import APITestCase as BaseAPITestCase 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class APITestCase(BaseAPITestCase): 10 | """ 11 | A helper for writing tests. 12 | """ 13 | base_path = '' 14 | fixtures = ['gennotes_server/fixtures/test-data.json'] 15 | 16 | def verify_request(self, path, method='get', 17 | expected_status=200, expected_data=None, 18 | **kwargs): 19 | args = ['/api' + self.base_path + path] 20 | 21 | logger.debug('%s %s', method.upper(), args[0]) 22 | 23 | response = getattr(self.client, method)(*args, **kwargs) 24 | 25 | self.assertEqual(response.status_code, expected_status) 26 | 27 | if expected_data: 28 | if hasattr(response.data, 'keys'): 29 | compare_response_dict(response.data, expected_data) 30 | elif hasattr(response.data, '__iter__'): 31 | compare_response_list(response.data, expected_data) 32 | # Doing this too in case the above didn't catch a difference. 33 | self.assertEqual(response.data, expected_data) 34 | 35 | return response 36 | 37 | 38 | def compare_response_list(response, expected, path_pre=''): 39 | """ 40 | Compare lists, helps determine test failure in responses. 41 | """ 42 | if not hasattr(expected, '__iter__'): 43 | raise AssertionError( 44 | 'List in response not expected at path: {}'.format(path_pre)) 45 | for i in range(len(response)): 46 | my_path = path_pre + ' [{}] ->'.format(str(i)) 47 | if hasattr(response[i], 'keys'): 48 | compare_response_dict(response[i], expected[i], path_pre=my_path) 49 | elif hasattr(response[i], '__iter__'): 50 | compare_response_list(response[i], expected[i], path_pre=my_path) 51 | elif response[i] != expected[i]: 52 | raise AssertionError( 53 | 'Values in list not equal at path:{0}\n' 54 | 'response: {1}\nexpected: {2}'.format( 55 | my_path, response[i], expected[i])) 56 | if len(expected) > len(response): 57 | raise AssertionError( 58 | 'Expected list longer than response at:{0}\n' 59 | 'response length: {1}\nexpected length: {2}.'.format( 60 | path_pre, str(len(expected)), str(len(response)))) 61 | 62 | 63 | def compare_response_dict(response, expected, path_pre=''): 64 | """ 65 | Compare dicts, helps determine test failure in responses. 66 | """ 67 | if not hasattr(expected, 'keys'): 68 | raise AssertionError( 69 | 'Dict in response not expected at path: {}'.format(path_pre)) 70 | for k in response.keys(): 71 | my_path = path_pre + ' ["{}"] ->'.format(k) 72 | if k not in expected: 73 | raise AssertionError( 74 | 'Unexpected key at path:{}\n' 75 | '"{}" in response is not in expected data.'.format( 76 | my_path, k)) 77 | elif hasattr(response[k], 'keys'): 78 | compare_response_dict(response[k], expected[k], my_path) 79 | elif hasattr(response[k], '__iter__'): 80 | compare_response_list(response[k], expected[k], my_path) 81 | elif response[k] != expected[k]: 82 | raise AssertionError( 83 | 'Values not equal at path:{}\n' 84 | 'response: {}\nexpected: {}'.format( 85 | my_path, response[k], expected[k])) 86 | for k in expected.keys(): 87 | my_path = path_pre + ' ["{}"] ->'.format(k) 88 | if k not in response: 89 | raise AssertionError( 90 | 'Missing key at path:{}\n' 91 | '"{}" in expected data, but not in response.'.format( 92 | my_path, k)) 93 | -------------------------------------------------------------------------------- /gennotes_server/tests/expected_data/specified_variant_list.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": 3, 3 | "next": null, 4 | "previous": null, 5 | "results": [ 6 | { 7 | "b37_id": "b37-1-883516-G-A", 8 | "current_version": 1, 9 | "relation_set": [ 10 | { 11 | "current_version": 18, 12 | "tags": { 13 | "clinvar-rcva:accession": "RCV000064926", 14 | "clinvar-rcva:gene-name": "nucleolar complex associated 2 homolog (S. cerevisiae)", 15 | "clinvar-rcva:gene-symbol": "NOC2L", 16 | "clinvar-rcva:num-submissions": "1", 17 | "clinvar-rcva:preferred-name": "NM_015658.3(NOC2L):c.1654C>T (p.Leu552=)", 18 | "clinvar-rcva:record-status": "current", 19 | "clinvar-rcva:significance": "not provided", 20 | "clinvar-rcva:trait-name": "Malignant melanoma", 21 | "clinvar-rcva:trait-type": "Disease", 22 | "clinvar-rcva:version": "2", 23 | "type": "clinvar-rcva" 24 | }, 25 | "url": "http://testserver/api/relation/8/", 26 | "variant": "http://testserver/api/variant/1/" 27 | } 28 | ], 29 | "tags": { 30 | "chrom_b37": "1", 31 | "pos_b37": "883516", 32 | "ref_allele_b37": "G", 33 | "var_allele_b37": "A" 34 | }, 35 | "url": "http://testserver/api/variant/1/" 36 | }, 37 | { 38 | "b37_id": "b37-1-891344-G-A", 39 | "current_version": 2, 40 | "relation_set": [ 41 | { 42 | "current_version": 19, 43 | "tags": { 44 | "clinvar-rcva:accession": "RCV000064927", 45 | "clinvar-rcva:gene-name": "nucleolar complex associated 2 homolog (S. cerevisiae)", 46 | "clinvar-rcva:gene-symbol": "NOC2L", 47 | "clinvar-rcva:num-submissions": "1", 48 | "clinvar-rcva:preferred-name": "NM_015658.3(NOC2L):c.657C>T (p.Leu219=)", 49 | "clinvar-rcva:record-status": "current", 50 | "clinvar-rcva:significance": "not provided", 51 | "clinvar-rcva:trait-name": "Malignant melanoma", 52 | "clinvar-rcva:trait-type": "Disease", 53 | "clinvar-rcva:version": "2", 54 | "type": "clinvar-rcva" 55 | }, 56 | "url": "http://testserver/api/relation/9/", 57 | "variant": "http://testserver/api/variant/2/" 58 | } 59 | ], 60 | "tags": { 61 | "chrom_b37": "1", 62 | "pos_b37": "891344", 63 | "ref_allele_b37": "G", 64 | "var_allele_b37": "A" 65 | }, 66 | "url": "http://testserver/api/variant/2/" 67 | }, 68 | { 69 | "b37_id": "b37-1-906168-G-A", 70 | "current_version": 3, 71 | "relation_set": [ 72 | { 73 | "current_version": 20, 74 | "tags": { 75 | "clinvar-rcva:accession": "RCV000064940", 76 | "clinvar-rcva:gene-name": "pleckstrin homology domain containing, family N member 1", 77 | "clinvar-rcva:gene-symbol": "PLEKHN1", 78 | "clinvar-rcva:num-submissions": "1", 79 | "clinvar-rcva:preferred-name": "NM_001160184.1(PLEKHN1):c.484+30G>A", 80 | "clinvar-rcva:record-status": "current", 81 | "clinvar-rcva:significance": "not provided", 82 | "clinvar-rcva:trait-name": "Malignant melanoma", 83 | "clinvar-rcva:trait-type": "Disease", 84 | "clinvar-rcva:version": "2", 85 | "type": "clinvar-rcva" 86 | }, 87 | "url": "http://testserver/api/relation/10/", 88 | "variant": "http://testserver/api/variant/3/" 89 | } 90 | ], 91 | "tags": { 92 | "chrom_b37": "1", 93 | "pos_b37": "906168", 94 | "ref_allele_b37": "G", 95 | "var_allele_b37": "A" 96 | }, 97 | "url": "http://testserver/api/variant/3/" 98 | } 99 | ] 100 | } 101 | -------------------------------------------------------------------------------- /gennotes_server/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | Models for GenNotes Variant and Relation elements. 3 | 4 | ## About Tags 5 | Most data in GenNotes is stored as key/value tags related to these elements. 6 | Users aren't constrained by database design to particular keys or values, but 7 | for now, I recommend tag keys match this regex format (it may be important for 8 | later optimizing with db indexing and Django): `^[a-z][a-z0-9]*(_[a-z0-9]+)*`. 9 | -- Madeleine 10 | """ 11 | from django.contrib.postgres.fields import HStoreField, JSONField 12 | from django.db import models 13 | 14 | from oauth2_provider.models import AbstractApplication 15 | 16 | from reversion import revisions as reversion 17 | from reversion.models import Revision 18 | 19 | 20 | class Variant(models.Model): 21 | """ 22 | Gennotes Variant element model. 23 | 24 | ## Special Tags 25 | The following keys are currently special/protected. They're used to find a 26 | variant based on location and sequence and are indexed in our db to 27 | optimize searches: 28 | 'chrom_b37', 'pos_b37', 'ref_allele_b37', 'var_allele_b37' 29 | """ 30 | ALLOWED_CHROMS = [str(i) for i in range(1, 25)] 31 | tags = HStoreField() 32 | special_tags = ['chrom_b37', 'pos_b37', 'ref_allele_b37', 'var_allele_b37'] 33 | required_tags = special_tags 34 | 35 | def __unicode__(self): 36 | return u'; '.join([u'%s=%s' % (k, v) for k, v in self.tags.iteritems()]) 37 | 38 | 39 | class Relation(models.Model): 40 | """ 41 | Gennotes Relation element model. 42 | 43 | ## Special Tags 44 | The following key is special/protected; it's used to identify a relation 45 | and is indexed in our db to optimize searches: 46 | 'type' 47 | """ 48 | variant = models.ForeignKey(Variant) 49 | tags = JSONField() 50 | special_tags = ['type'] 51 | required_tags = ['type'] 52 | 53 | def __unicode__(self): 54 | return 'Relation: {}, Type: {}'.format(str(self.pk), self.tags['type']) 55 | 56 | 57 | class CommitDeletion(models.Model): 58 | revision = models.ForeignKey(Revision) 59 | deletion = models.BooleanField(default=True) 60 | 61 | 62 | reversion.register(Variant) 63 | reversion.register(Relation) 64 | 65 | 66 | class EditingApplication(AbstractApplication): 67 | """ 68 | OAuth2 provider application for submitting edits on behalf of users. 69 | 70 | (Putting notes here for now on how to get token...) 71 | 1) send user to: 72 | /oauth2-app/authorize?client_id=[client-id-here]&response_type=code 73 | 2) your default redirect_uri will receive the following: 74 | yourdomain.com/path/to/redirect_uri?code=[grant-code] 75 | 3) exchange that grant code for a token immediately. example w/ requests: 76 | Set this up with your client_id and client_secret: 77 | client_auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 78 | Set code: 79 | code = [the grant-code you just received] 80 | Set redirect_uri (required by our framework...): 81 | redirect_uri = [a redirect uri you registered] 82 | Set the GenNotes receiving uri... e.g. for local testing I use: 83 | token_uri = 'http://localhost:8000/oauth2-app/token/' 84 | POST to this: 85 | response_token = requests.post(token_uri, data={ 86 | 'grant_type': 'authorization_code', 87 | 'code': code, 'redirect_uri': redirect_uri}, auth=client_auth) 88 | The response should contain an access token, e.g.: 89 | {'access_token': '1hu94IRBX3da0euOiX0u3E9h', 90 | 'token_type': 'Bearer', 91 | 'expires_in': 36000, 92 | 'refresh_token': 'WSuwoeBO0e9JFHqY7TnpDi7jUjgAex', 93 | 'scope': 'commit-edit'} 94 | To use the refresh token to get new tokens: 95 | refresh_token = response_token.json()['refresh_token'] 96 | response_refresh = requests.post(token_uri, data={ 97 | 'grant_type': 'refresh_token', 98 | 'refresh_token': refresh_token}, auth=client_auth) 99 | """ 100 | description = models.CharField(max_length=300) 101 | -------------------------------------------------------------------------------- /gennotes_server/templates/account/signup-form.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | Use the form below to create a GenNotes. You are responsible for edits 5 | contributed to GenNotes from this account. 6 |

7 |

Contributions are CC0 by default. Unless otherwise specified 8 | within GenNotes, users of GenNotes agree that their contributions 9 | to GenNotes tags are released as public domain with a CC0 dedication. 10 |

11 |
12 |
13 | 14 |
16 | 17 | {% csrf_token %} 18 | 19 | {% if redirect_field_value %} 20 | 22 | {% endif %} 23 | 24 | {% if form.non_field_errors %} 25 |
26 | {% for error in form.non_field_errors %} 27 | {{ error }}
28 | {% endfor %} 29 |
30 | {% endif %} 31 | 32 |
33 | 34 | 35 |
36 | 39 | 40 | Your username will be public and associated with 41 | your GenNotes edits. 42 | {% if form.username.errors %} 43 | {% for error in form.username.errors %} 44 | {{ error }} 45 | {% endfor %} 46 | {% endif %} 47 |
48 |
49 | 50 |
51 | 52 | 53 |
54 | 56 | 57 | {% if form.email.errors %} 58 | {% for error in form.email.errors %} 59 | {{ error }} 60 | {% endfor %} 61 | {% endif %} 62 | 63 | Your email will be private, but we require a verified email address in 64 | case we need to contact you. 65 | 66 |
67 |
68 | 69 |
70 | 71 | 72 |
73 | 76 | 77 | {% if form.password1.errors %} 78 | {% for error in form.password1.errors %} 79 | {{ error }} 80 | {% endfor %} 81 | {% endif %} 82 |
83 |
84 | 85 |
86 | 89 | 90 |
91 | 94 | 95 | {% if form.password2.errors %} 96 | {% for error in form.password2.errors %} 97 | {{ error }} 98 | {% endfor %} 99 | {% endif %} 100 |
101 |
102 | 103 |
104 |
105 |

106 | 107 |

108 |

109 | 110 | Already have an account? Click here to log in. 111 |

112 |
113 |
114 | 115 |
116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GenNotes 2 | 3 | [![Join the chat at https://gitter.im/PersonalGenomesOrg/gennotes](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/PersonalGenomesOrg/gennotes?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | **GenNotes** and **[Genevieve](https://github.com/PersonalGenomesOrg/genevieve)** are a pair of projects designed to empower 6 | individuals to understand putative effects reported for variants in an 7 | individual genome. We worked on these during the [Mozilla Science 8 | Lab Global Sprint 2015](https://www.mozillascience.org/global-sprint-2015), and 9 | are continuing work on the project. 10 | 11 | **Chat via IRC or gitter:** 12 | * #gennotes on irc.freenode. If you're new to IRC, the easiest 13 | way to join is Freenode's webchat interface: 14 | http://webchat.freenode.net/?channels=gennotes 15 | * or, use your GitHub account to start a Gitter account and chat in our Gitter 16 | room: https://gitter.im/PersonalGenomesOrg/gennotes 17 | * These two rooms are synced by the GennoteGitterBot. 18 | 19 | **Languages**: Python (Django), JavaScript, CSS, HTML 20 | 21 | ## Motivation 22 | 23 | GenNotes and Genevieve are about accessing and improving genomic knowledge. We believe access and discussion of knowledge is a right and necessity for informed citizenry in the age of genomics. We want to facilitate access to published claims regarding the effects of variants in an individual genome, and enable discussion and consensus understanding of these claims. 24 | 25 | We’d like to start by building understanding around ClinVar, a public domain resource that aggregates and reports assertions of clinical impact of genetic variants. While invaluable, these assertions can be flawed – this is especially apparent when evaluating assertions made for an individual genome. Coupling "genome reports" and "consensus, structured note-taking" helps evaluate assertions efficiently, and improves the quality of reports generated for future genomes. 26 | 27 | To this end, we’re developing a set of related open source tools: 28 | - **GenNotes** 29 | - A web server storing publicly shared and flexibly structured tags associated with genetic variants. These tags are structured as key/value tags, in the manner of Open Street Map. These can be arbitrary keys, but initially we'd like to focus on ClinVar and Genevieve tags. 30 | - **Genevieve** 31 | - A client web app that can process individual genomes to find variants matching ClinVar positions. Using GenNotes, the client can retrieves and reports the latest ClinVar and Genevieve tags for this variant list. Genevieve users can then create and edit structured notes about ClinVar assertions. These notes are shared as a consensus understanding: they are stored by the GenNotes server and become visible and editable for all Genevieve users. 32 | 33 | ### Not for clinical use! 34 | 35 | The tools that we create for this project may involve claims related to health and disease, but primary literature is research: reports may be contradicted or challenged by later findings. Edits may come from any source and are not vetted. None of the resources produced in this project are for medical use and they should not be used as a substitute for professional medical care or advice. Users seeking information about a personal genetic disease, syndrome, or condition should consult with a qualified healthcare professional. 36 | 37 | ## TODO List 38 | 39 | The basic structure for these projects is now complete, but it's a "bare bones" implementation. There remain numerous issues and improvements: see [GenNotes issues](https://github.com/PersonalGenomesOrg/gennotes/issues) and [Genevieve issues](https://github.com/PersonalGenomesOrg/genevieve/issues) for our ongoing feature wishlists. 40 | 41 | ## Examples 42 | 43 | **Note both projects are in active development!** 44 | 45 | - An example query retrieving GenNotes data: http://gennotes.herokuapp.com/api/variant/b37-1-100672060-T-T/ 46 | - [Genevieve genome report screenshot](https://cloud.githubusercontent.com/assets/82631/8336384/13b34ae4-1a72-11e5-8e84-bc47a62ca060.png) 47 | 48 | ## Local development set-up 49 | 50 | We've set up a copy of GenNotes running on Heroku at 51 | [gennotes.herokuapp.com](http://gennotes.herokuapp.com/) (NOT REMOTELY STABLE). 52 | While users can run their own copy of 53 | [Genevieve](https://github.com/PersonalGenomesOrg/genevieve), the intention 54 | is to have a single GenNotes service that every copy of Genevieve talks to. 55 | The instructions are *only* intended to assist developers interested in 56 | contributing to the centrally-run website's features. 57 | 58 | * **Use pip and virtualenv. In the virtualenv install required packages with: 59 | `pip install -r requirements.txt`** 60 | * If pip and virtualenv are new to you, this repo may not be the best place 61 | to start learning! Maybe you'd be interested in contributing to [Genevieve](https://github.com/PersonalGenomesOrg/genevieve)? 62 | * **Set up PostgreSQL** 63 | * Ubuntu/Debian 64 | * For working with PostgreSQL: `sudo apt-get install libpq-dev python-dev` 65 | * Install PostgreSQL: `sudo apt-get install postgresql postgresql-contrib` 66 | * Become the postgres user: `sudo su - postgres`. 67 | * [As postgres] Create your databasae, e.g.: `createdb mydb` 68 | * [As postgres] Create your database user, e.g.: `createuser -P myuser`. Remember the password used, you'll need it in your configuration later! 69 | * [As postgres] Log in to PostgreSQL, e.g.: `psql mydb` 70 | * **RECOMMENDED WITH WARNING:** The following grants **superuser** status to your database user: 71 | * [As postgres, in psql] `ALTER ROLE myuser WITH SUPERUSER;` 72 | * **ALTERNATE APPROACH:** If superuser isn't possible, you won't be able to run `python manage.py tests` as the database user won't be able to automatically create the test database. You can still set your project up with the following three steps: 73 | * [As postgres, in psql] Add HStore extension to this database: `CREATE EXTENSION IF NOT EXISTS "hstore";` 74 | * [As postgres, in psql] Grant database privileges to your user, e.g.: `GRANT ALL PRIVILEGES ON DATABASE mydb TO myuser` 75 | * [Later] When you edit your `.env` (see below) make sure to specify `PSQL_USER_IS_SUPERUSER="False"` 76 | * Use `\q` to quit psql, and `exit` to log off as the postgres user. 77 | * *OSX instructions (Homebrew?) invited!* 78 | * **Copy `env.example` to `.env`** (note the leading dot!) 79 | * Set your `SECRET_KEY` with a random string. 80 | * Set the `DATABASE_URL` using appropriate PostgreSQL database name, user, and password. 81 | * The easiest mail set-up is probably to set `EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"`. Emails "sent" by the site will show up in console output. 82 | * **Initialize the database:** `python manage.py migrate` 83 | * **Run the site:** `python manage.py runserver` 84 | * You can now load GenNotes in your web browser by visiting `http://localhost:8000/`. 85 | * You can also interact directly with your site via shell_plus using `python manage.py shell_plus` 86 | 87 | ### Testing 88 | 89 | Test your code changes by running `python manage.py test`. 90 | -------------------------------------------------------------------------------- /gennotes_server/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for gennotes_server project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.8. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.8/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | import dj_database_url 16 | from django.conf import global_settings 17 | from env_tools import apply_env 18 | 19 | from .utils import to_bool 20 | 21 | 22 | # Apply the environment variables in the .env file. 23 | apply_env() 24 | 25 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 26 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 27 | 28 | # Development settings - unsuitable for production 29 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 30 | # SECURITY WARNING: keep the secret key used in production secret! 31 | SECRET_KEY = os.getenv('SECRET_KEY') 32 | # SECURITY WARNING: don't run with debug turned on in production! 33 | DEBUG = to_bool('DEBUG', 'False') 34 | USING_SSL = to_bool('USING_SSL', str(not DEBUG)) 35 | 36 | # Settings per Heroku instructions: 37 | # https://devcenter.heroku.com/articles/getting-started-with-django 38 | ALLOWED_HOSTS = ['*'] 39 | if USING_SSL: 40 | # Add settings for SSL in production on Heroku. 41 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 42 | SESSION_COOKIE_SECURE = True 43 | CSRF_COOKIE_SECURE = True 44 | else: 45 | # Turn off django-sslify. 46 | SSLIFY_DISABLE = True 47 | 48 | # Application definition 49 | 50 | INSTALLED_APPS = ( 51 | 'django.contrib.admin', 52 | 'django.contrib.auth', 53 | 'django.contrib.contenttypes', 54 | 'django.contrib.sessions', 55 | 'django.contrib.messages', 56 | 'django.contrib.postgres', 57 | 'django.contrib.staticfiles', 58 | # Required by django-allauth 59 | 'django.contrib.sites', 60 | 61 | # Main app for the site. 62 | 'gennotes_server', 63 | 64 | # Third party apps 65 | 'allauth', 66 | 'allauth.account', 67 | 'corsheaders', 68 | 'django_extensions', 69 | 'oauth2_provider', 70 | 'reversion', 71 | 'rest_framework', 72 | 'rest_framework_swagger' 73 | ) 74 | 75 | TEMPLATE_CONTEXT_PROCESSORS = [ 76 | # Required by 'allauth' template tags 77 | 'django.core.context_processors.request', 78 | 79 | # 'allauth' specific context processors 80 | 'allauth.account.context_processors.account', 81 | ] + global_settings.TEMPLATE_CONTEXT_PROCESSORS 82 | 83 | AUTHENTICATION_BACKENDS = [ 84 | 'oauth2_provider.backends.OAuth2Backend', 85 | 86 | # Needed to login by username in Django admin 87 | 'django.contrib.auth.backends.ModelBackend', 88 | 89 | # 'allauth' specific authentication methods, such as login by e-mail 90 | 'allauth.account.auth_backends.AuthenticationBackend', 91 | ] + global_settings.AUTHENTICATION_BACKENDS 92 | 93 | MIDDLEWARE_CLASSES = ( 94 | 'sslify.middleware.SSLifyMiddleware', 95 | 'corsheaders.middleware.CorsMiddleware', 96 | 'reversion.middleware.RevisionMiddleware', 97 | 'django.contrib.sessions.middleware.SessionMiddleware', 98 | 'django.middleware.common.CommonMiddleware', 99 | 'django.middleware.csrf.CsrfViewMiddleware', 100 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 101 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 102 | 'oauth2_provider.middleware.OAuth2TokenMiddleware', 103 | 'django.contrib.messages.middleware.MessageMiddleware', 104 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 105 | 'django.middleware.security.SecurityMiddleware', 106 | ) 107 | 108 | ROOT_URLCONF = 'gennotes_server.urls' 109 | 110 | TEMPLATES = [ 111 | { 112 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 113 | 'DIRS': [], 114 | 'APP_DIRS': True, 115 | 'OPTIONS': { 116 | 'context_processors': [ 117 | 'django.template.context_processors.debug', 118 | 'django.template.context_processors.request', 119 | 'django.contrib.auth.context_processors.auth', 120 | 'django.contrib.messages.context_processors.messages', 121 | ], 122 | }, 123 | }, 124 | ] 125 | 126 | WSGI_APPLICATION = 'gennotes_server.wsgi.application' 127 | 128 | # Use DATABASE_URL to do database setup, for a local Postgres database it would 129 | # look like: postgres://localhost/database_name 130 | DATABASES = {} 131 | 132 | if os.getenv('CI_NAME') == 'codeship': 133 | DATABASES['default'] = { 134 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 135 | 'NAME': 'test', 136 | 'USER': os.getenv('PG_USER'), 137 | 'PASSWORD': os.getenv('PG_PASSWORD'), 138 | 'HOST': '127.0.0.1', 139 | } 140 | # Only override the default if there's a database URL specified 141 | elif dj_database_url.config(): 142 | DATABASES['default'] = dj_database_url.config() 143 | 144 | PSQL_USER_IS_SUPERUSER = to_bool('PSQL_USER_IS_SUPERUSER', 'True') 145 | 146 | # Internationalization 147 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 148 | 149 | LANGUAGE_CODE = 'en-us' 150 | 151 | TIME_ZONE = 'UTC' 152 | 153 | USE_I18N = True 154 | USE_L10N = True 155 | USE_TZ = True 156 | 157 | SITE_ID = 1 158 | 159 | # Email set up. 160 | EMAIL_BACKEND = os.getenv('EMAIL_BACKEND', global_settings.EMAIL_BACKEND) 161 | EMAIL_USE_TLS = to_bool('EMAIL_USE_TLS', str(global_settings.EMAIL_USE_TLS)) 162 | EMAIL_HOST = os.getenv('EMAIL_HOST', global_settings.EMAIL_HOST) 163 | EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', global_settings.EMAIL_HOST_USER) 164 | EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', 165 | global_settings.EMAIL_HOST_PASSWORD) 166 | EMAIL_PORT = int(os.getenv('EMAIL_PORT', str(global_settings.EMAIL_PORT))) 167 | 168 | MEDIA_URL = '/media/' 169 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 170 | 171 | STATICFILES_STORAGE = 'whitenoise.django.GzipManifestStaticFilesStorage' 172 | 173 | STATIC_URL = '/static/' 174 | STATIC_ROOT = os.path.join(BASE_DIR, 'static-files') 175 | 176 | STATICFILES_DIRS = ( 177 | os.path.join(BASE_DIR, 'static'), 178 | ) 179 | 180 | # Settings for django-allauth and account interactions. 181 | # Signup and login take both email and username. 182 | # No email confirmation is implemented. 183 | ACCOUNT_EMAIL_REQUIRED = True 184 | ACCOUNT_USERNAME_REQUIRED = True 185 | ACCOUNT_AUTHENTICATION_METHOD = 'username_email' 186 | ACCOUNT_EMAIL_VERIFICATION = 'mandatory' 187 | 188 | LOGIN_REDIRECT_URL = 'home' 189 | 190 | # Settings for Django REST Framework 191 | REST_FRAMEWORK = { 192 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 193 | # OAuth2 authorization for API usage 194 | 'oauth2_provider.ext.rest_framework.OAuth2Authentication', 195 | # Other authorization methods 196 | 'rest_framework.authentication.SessionAuthentication', 197 | 'rest_framework.authentication.BasicAuthentication', 198 | ), 199 | 'DEFAULT_PAGINATION_CLASS': 'gennotes_server.pagination.PageNumberPaginationUpTo1000', 200 | 'PAGE_SIZE': 100, 201 | } 202 | 203 | # Settings for Django OAuth Toolkit 204 | OAUTH2_PROVIDER_APPLICATION_MODEL = 'gennotes_server.EditingApplication' 205 | 206 | OAUTH2_PROVIDER = { 207 | 'SCOPES': {'email': 'Can read your GenNotes email address', 208 | 'username': 'Can read your GenNotes user ID and username', 209 | 'commit-edit': ('Can commit changes to GenNotes Variants ' 210 | 'and Relations on your behalf.')}, 211 | 'REQUEST_APPROVAL_PROMPT': 'auto', 212 | } 213 | 214 | # Settings for CORS (in dev) 215 | CORS_ALLOW_CREDENTIALS = True 216 | CORS_ORIGIN_ALLOW_ALL = True 217 | CORS_URLS_REGEX = r'^/api/.*$' 218 | -------------------------------------------------------------------------------- /gennotes_server/tests/expected_data/relation_list.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": 10, 3 | "next": null, 4 | "previous": null, 5 | "results": [ 6 | { 7 | "current_version": 11, 8 | "tags": { 9 | "clinvar-rcva:accession": "RCV000116253", 10 | "clinvar-rcva:esp-allele-frequency": "0.014089016971", 11 | "clinvar-rcva:gene-name": "agrin", 12 | "clinvar-rcva:gene-symbol": "AGRN", 13 | "clinvar-rcva:num-submissions": "1", 14 | "clinvar-rcva:preferred-name": "NM_198576.3(AGRN):c.1058A>G (p.Gln353Arg)", 15 | "clinvar-rcva:record-status": "current", 16 | "clinvar-rcva:significance": "Likely benign", 17 | "clinvar-rcva:trait-name": "not specified", 18 | "clinvar-rcva:trait-type": "Disease", 19 | "clinvar-rcva:version": "2", 20 | "type": "clinvar-rcva" 21 | }, 22 | "url": "http://testserver/api/relation/1/", 23 | "variant": "http://testserver/api/variant/10/" 24 | }, 25 | { 26 | "current_version": 12, 27 | "tags": { 28 | "clinvar-rcva:accession": "RCV000116272", 29 | "clinvar-rcva:gene-name": "agrin", 30 | "clinvar-rcva:gene-symbol": "AGRN", 31 | "clinvar-rcva:num-submissions": "1", 32 | "clinvar-rcva:preferred-name": "NM_198576.3(AGRN):c.45G>T (p.Pro15=)", 33 | "clinvar-rcva:record-status": "current", 34 | "clinvar-rcva:significance": "Likely benign", 35 | "clinvar-rcva:trait-name": "not specified", 36 | "clinvar-rcva:trait-type": "Disease", 37 | "clinvar-rcva:version": "2", 38 | "type": "clinvar-rcva" 39 | }, 40 | "url": "http://testserver/api/relation/2/", 41 | "variant": "http://testserver/api/variant/7/" 42 | }, 43 | { 44 | "current_version": 13, 45 | "tags": { 46 | "clinvar-rcva:accession": "RCV000116258", 47 | "clinvar-rcva:esp-allele-frequency": "0.031754574812", 48 | "clinvar-rcva:gene-name": "agrin", 49 | "clinvar-rcva:gene-symbol": "AGRN", 50 | "clinvar-rcva:num-submissions": "1", 51 | "clinvar-rcva:preferred-name": "NM_198576.3(AGRN):c.261C>T (p.Asp87=)", 52 | "clinvar-rcva:record-status": "current", 53 | "clinvar-rcva:significance": "Likely benign", 54 | "clinvar-rcva:trait-name": "not specified", 55 | "clinvar-rcva:trait-type": "Disease", 56 | "clinvar-rcva:version": "2", 57 | "type": "clinvar-rcva" 58 | }, 59 | "url": "http://testserver/api/relation/3/", 60 | "variant": "http://testserver/api/variant/8/" 61 | }, 62 | { 63 | "current_version": 14, 64 | "tags": { 65 | "clinvar-rcva:accession": "RCV000116282", 66 | "clinvar-rcva:gene-name": "agrin", 67 | "clinvar-rcva:gene-symbol": "AGRN", 68 | "clinvar-rcva:num-submissions": "1", 69 | "clinvar-rcva:preferred-name": "NM_198576.3(AGRN):c.804C>T (p.Ala268=)", 70 | "clinvar-rcva:record-status": "current", 71 | "clinvar-rcva:significance": "Benign", 72 | "clinvar-rcva:trait-name": "not specified", 73 | "clinvar-rcva:trait-type": "Disease", 74 | "clinvar-rcva:version": "1", 75 | "type": "clinvar-rcva" 76 | }, 77 | "url": "http://testserver/api/relation/4/", 78 | "variant": "http://testserver/api/variant/9/" 79 | }, 80 | { 81 | "current_version": 15, 82 | "tags": { 83 | "clinvar-rcva:accession": "RCV000148988", 84 | "clinvar-rcva:gene-name": "ISG15 ubiquitin-like modifier", 85 | "clinvar-rcva:gene-symbol": "ISG15", 86 | "clinvar-rcva:num-submissions": "1", 87 | "clinvar-rcva:preferred-name": "NM_005101.3(ISG15):c.379G>T (p.Glu127Ter)", 88 | "clinvar-rcva:record-status": "current", 89 | "clinvar-rcva:significance": "Pathogenic", 90 | "clinvar-rcva:trait-name": "Immunodeficiency 38", 91 | "clinvar-rcva:trait-type": "Disease", 92 | "clinvar-rcva:version": "4", 93 | "type": "clinvar-rcva" 94 | }, 95 | "url": "http://testserver/api/relation/5/", 96 | "variant": "http://testserver/api/variant/6/" 97 | }, 98 | { 99 | "current_version": 16, 100 | "tags": { 101 | "clinvar-rcva:accession": "RCV000148989", 102 | "clinvar-rcva:citations": "PMID22859821", 103 | "clinvar-rcva:gene-name": "ISG15 ubiquitin-like modifier", 104 | "clinvar-rcva:gene-symbol": "ISG15", 105 | "clinvar-rcva:num-submissions": "1", 106 | "clinvar-rcva:preferred-name": "NM_005101.3(ISG15):c.339dupG (p.Leu114Alafs)", 107 | "clinvar-rcva:record-status": "current", 108 | "clinvar-rcva:significance": "Pathogenic", 109 | "clinvar-rcva:trait-name": "Immunodeficiency 38", 110 | "clinvar-rcva:trait-type": "Disease", 111 | "clinvar-rcva:version": "4", 112 | "type": "clinvar-rcva" 113 | }, 114 | "url": "http://testserver/api/relation/6/", 115 | "variant": "http://testserver/api/variant/5/" 116 | }, 117 | { 118 | "current_version": 17, 119 | "tags": { 120 | "clinvar-rcva:accession": "RCV000162196", 121 | "clinvar-rcva:gene-name": "ISG15 ubiquitin-like modifier", 122 | "clinvar-rcva:gene-symbol": "ISG15", 123 | "clinvar-rcva:num-submissions": "1", 124 | "clinvar-rcva:preferred-name": "NM_005101.3(ISG15):c.163C>T (p.Gln55Ter)", 125 | "clinvar-rcva:record-status": "current", 126 | "clinvar-rcva:significance": "Pathogenic", 127 | "clinvar-rcva:trait-name": "Immunodeficiency 38", 128 | "clinvar-rcva:trait-type": "Disease", 129 | "clinvar-rcva:version": "2", 130 | "type": "clinvar-rcva" 131 | }, 132 | "url": "http://testserver/api/relation/7/", 133 | "variant": "http://testserver/api/variant/4/" 134 | }, 135 | { 136 | "current_version": 18, 137 | "tags": { 138 | "clinvar-rcva:accession": "RCV000064926", 139 | "clinvar-rcva:gene-name": "nucleolar complex associated 2 homolog (S. cerevisiae)", 140 | "clinvar-rcva:gene-symbol": "NOC2L", 141 | "clinvar-rcva:num-submissions": "1", 142 | "clinvar-rcva:preferred-name": "NM_015658.3(NOC2L):c.1654C>T (p.Leu552=)", 143 | "clinvar-rcva:record-status": "current", 144 | "clinvar-rcva:significance": "not provided", 145 | "clinvar-rcva:trait-name": "Malignant melanoma", 146 | "clinvar-rcva:trait-type": "Disease", 147 | "clinvar-rcva:version": "2", 148 | "type": "clinvar-rcva" 149 | }, 150 | "url": "http://testserver/api/relation/8/", 151 | "variant": "http://testserver/api/variant/1/" 152 | }, 153 | { 154 | "current_version": 19, 155 | "tags": { 156 | "clinvar-rcva:accession": "RCV000064927", 157 | "clinvar-rcva:gene-name": "nucleolar complex associated 2 homolog (S. cerevisiae)", 158 | "clinvar-rcva:gene-symbol": "NOC2L", 159 | "clinvar-rcva:num-submissions": "1", 160 | "clinvar-rcva:preferred-name": "NM_015658.3(NOC2L):c.657C>T (p.Leu219=)", 161 | "clinvar-rcva:record-status": "current", 162 | "clinvar-rcva:significance": "not provided", 163 | "clinvar-rcva:trait-name": "Malignant melanoma", 164 | "clinvar-rcva:trait-type": "Disease", 165 | "clinvar-rcva:version": "2", 166 | "type": "clinvar-rcva" 167 | }, 168 | "url": "http://testserver/api/relation/9/", 169 | "variant": "http://testserver/api/variant/2/" 170 | }, 171 | { 172 | "current_version": 20, 173 | "tags": { 174 | "clinvar-rcva:accession": "RCV000064940", 175 | "clinvar-rcva:gene-name": "pleckstrin homology domain containing, family N member 1", 176 | "clinvar-rcva:gene-symbol": "PLEKHN1", 177 | "clinvar-rcva:num-submissions": "1", 178 | "clinvar-rcva:preferred-name": "NM_001160184.1(PLEKHN1):c.484+30G>A", 179 | "clinvar-rcva:record-status": "current", 180 | "clinvar-rcva:significance": "not provided", 181 | "clinvar-rcva:trait-name": "Malignant melanoma", 182 | "clinvar-rcva:trait-type": "Disease", 183 | "clinvar-rcva:version": "2", 184 | "type": "clinvar-rcva" 185 | }, 186 | "url": "http://testserver/api/relation/10/", 187 | "variant": "http://testserver/api/variant/3/" 188 | } 189 | ] 190 | } 191 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Profiled execution. 11 | profile=no 12 | 13 | # Add files or directories to the blacklist. They should be base names, not 14 | # paths. 15 | ignore=CVS 16 | 17 | # Pickle collected data for later comparisons. 18 | persistent=yes 19 | 20 | # List of plugins (as comma separated values of python modules names) to load, 21 | # usually to register additional checkers. 22 | load-plugins= 23 | 24 | 25 | [MESSAGES CONTROL] 26 | 27 | # Enable the message, report, category or checker with the given id(s). You can 28 | # either give multiple identifier separated by comma (,) or put this option 29 | # multiple time. See also the "--disable" option for examples. 30 | #enable= 31 | 32 | # Disable the message, report, category or checker with the given id(s). You 33 | # can either give multiple identifiers separated by comma (,) or put this 34 | # option multiple times (only on the command line, not in the configuration 35 | # file where it should appear only once).You can also use "--disable=all" to 36 | # disable everything first and then reenable specific checks. For example, if 37 | # you want to run only the similarities checker, you can use "--disable=all 38 | # --enable=similarities". If you want to run only the classes checker, but have 39 | # no Warning level messages displayed, use"--disable=all --enable=classes 40 | # --disable=W" 41 | disable=C0111,W0511,E1101,R0903,R0904,W0142,C1001,W0232,C0103,E1002,W0212,R0901,locally-disabled,locally-enabled 42 | 43 | [REPORTS] 44 | 45 | # Set the output format. Available formats are text, parseable, colorized, msvs 46 | # (visual studio) and html. You can also give a reporter class, eg 47 | # mypackage.mymodule.MyReporterClass. 48 | output-format=text 49 | 50 | # Put messages in a separate file for each module / package specified on the 51 | # command line instead of printing them on stdout. Reports (if any) will be 52 | # written in a file name "pylint_global.[txt|html]". 53 | files-output=no 54 | 55 | # Tells whether to display a full report or only the messages 56 | reports=yes 57 | 58 | # Python expression which should return a note less than 10 (10 is the highest 59 | # note). You have access to the variables errors warning, statement which 60 | # respectively contain the number of errors / warnings messages and the total 61 | # number of statements analyzed. This is used by the global evaluation report 62 | # (RP0004). 63 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 64 | 65 | # Add a comment according to your evaluation note. This is used by the global 66 | # evaluation report (RP0004). 67 | comment=no 68 | 69 | 70 | [BASIC] 71 | 72 | # Required attributes for module, separated by a comma 73 | required-attributes= 74 | 75 | # List of builtins function names that should not be used, separated by a comma 76 | bad-functions=map,filter,apply,input 77 | 78 | # Regular expression which should only match correct module names 79 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 80 | 81 | # Regular expression which should only match correct module level names 82 | const-rgx=[a-zA-Z_][a-zA-Z0-9_]{1,30}$ 83 | 84 | # Regular expression which should only match correct class names 85 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 86 | 87 | # Regular expression which should only match correct function names 88 | function-rgx=[a-z_][a-z0-9_]{1,30}$ 89 | 90 | # Regular expression which should only match correct method names 91 | method-rgx=[a-z_][a-z0-9_]{1,30}$ 92 | 93 | # Regular expression which should only match correct instance attribute names 94 | attr-rgx=[a-z_][a-z0-9_]{1,30}$ 95 | 96 | # Regular expression which should only match correct argument names 97 | argument-rgx=[a-z_][a-z0-9_]{1,30}$ 98 | 99 | # Regular expression which should only match correct variable names 100 | variable-rgx=[a-z_][a-z0-9_]{1,30}$ 101 | 102 | # Regular expression which should only match correct list comprehension / 103 | # generator expression variable names 104 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 105 | 106 | # Good variable names which should always be accepted, separated by a comma 107 | good-names=i,j,k,ex,Run,_ 108 | 109 | # Bad variable names which should always be refused, separated by a comma 110 | bad-names=foo,bar,baz,toto,tutu,tata 111 | 112 | # Regular expression which should only match functions or classes name which do 113 | # not require a docstring 114 | no-docstring-rgx=__.*__ 115 | 116 | 117 | [FORMAT] 118 | 119 | # Maximum number of characters on a single line. 120 | max-line-length=80 121 | 122 | # Maximum number of lines in a module 123 | max-module-lines=1000 124 | 125 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 126 | # tab). 127 | indent-string=' ' 128 | 129 | 130 | [MISCELLANEOUS] 131 | 132 | # List of note tags to take in consideration, separated by a comma. 133 | notes=FIXME,XXX,TODO 134 | 135 | 136 | [SIMILARITIES] 137 | 138 | # Minimum lines number of a similarity. 139 | min-similarity-lines=4 140 | 141 | # Ignore comments when computing similarities. 142 | ignore-comments=yes 143 | 144 | # Ignore docstrings when computing similarities. 145 | ignore-docstrings=yes 146 | 147 | # Ignore imports when computing similarities. 148 | ignore-imports=no 149 | 150 | 151 | [TYPECHECK] 152 | 153 | # Tells whether missing members accessed in mixin class should be ignored. A 154 | # mixin class is detected if its name ends with "mixin" (case insensitive). 155 | ignore-mixin-members=yes 156 | 157 | # List of classes names for which member attributes should not be checked 158 | # (useful for classes with attributes dynamically set). 159 | ignored-classes=SQLObject 160 | 161 | # When zope mode is activated, add a predefined set of Zope acquired attributes 162 | # to generated-members. 163 | zope=no 164 | 165 | # List of members which are set dynamically and missed by pylint inference 166 | # system, and so shouldn't trigger E0201 when accessed. Python regular 167 | # expressions are accepted. 168 | generated-members=REQUEST,acl_users,aq_parent 169 | 170 | 171 | [VARIABLES] 172 | 173 | # Tells whether we should check for unused import in __init__ files. 174 | init-import=no 175 | 176 | # A regular expression matching the beginning of the name of dummy variables 177 | # (i.e. not used). 178 | dummy-variables-rgx=_|dummy 179 | 180 | # List of additional names supposed to be defined in builtins. Remember that 181 | # you should avoid to define new builtins when possible. 182 | additional-builtins= 183 | 184 | 185 | [CLASSES] 186 | 187 | # List of interface methods to ignore, separated by a comma. This is used for 188 | # instance to not check methods defines in Zope's Interface base class. 189 | ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by 190 | 191 | # List of method names used to declare (i.e. assign) instance attributes. 192 | defining-attr-methods=__init__,__new__,setUp 193 | 194 | # List of valid names for the first argument in a class method. 195 | valid-classmethod-first-arg=cls 196 | 197 | # List of valid names for the first argument in a metaclass class method. 198 | valid-metaclass-classmethod-first-arg=mcs 199 | 200 | 201 | [DESIGN] 202 | 203 | # Maximum number of arguments for function / method 204 | max-args=8 205 | 206 | # Argument names that match this expression will be ignored. Default to name 207 | # with leading underscore 208 | ignored-argument-names=_.* 209 | 210 | # Maximum number of locals for function / method body 211 | max-locals=25 212 | 213 | # Maximum number of return / yield for function / method body 214 | max-returns=6 215 | 216 | # Maximum number of branch for function / method body 217 | max-branchs=15 218 | 219 | # Maximum number of statements in function / method body 220 | max-statements=50 221 | 222 | # Maximum number of parents for a class (see R0901). 223 | max-parents=7 224 | 225 | # Maximum number of attributes for a class (see R0902). 226 | max-attributes=7 227 | 228 | # Minimum number of public methods for a class (see R0903). 229 | min-public-methods=2 230 | 231 | # Maximum number of public methods for a class (see R0904). 232 | max-public-methods=20 233 | 234 | 235 | [IMPORTS] 236 | 237 | # Deprecated modules which should not be used, separated by a comma 238 | deprecated-modules=regsub,string,TERMIOS,Bastion,rexec 239 | 240 | # Create a graph of every (i.e. internal and external) dependencies in the 241 | # given file (report RP0402 must not be disabled) 242 | import-graph= 243 | 244 | # Create a graph of external dependencies in the given file (report RP0402 must 245 | # not be disabled) 246 | ext-import-graph= 247 | 248 | # Create a graph of internal dependencies in the given file (report RP0402 must 249 | # not be disabled) 250 | int-import-graph= 251 | 252 | 253 | [EXCEPTIONS] 254 | 255 | # Exceptions that will emit a warning when being caught. Defaults to 256 | # "Exception" 257 | overgeneral-exceptions=Exception 258 | -------------------------------------------------------------------------------- /gennotes_server/tests/test_relation.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | from test_helpers import APITestCase 5 | 6 | ERR_NOAUTH = {'detail': 'Authentication credentials were not provided.'} 7 | ERR_TYPE_CHNG = {'detail': "Updates (PUT or PATCH) must not attempt to change " 8 | "the values for special tags. Your request " 9 | "attempts to change the value for tag 'type' " 10 | "from 'clinvar-rcva' to 'test-type'"} 11 | ERR_VAR_INC = {'detail': "Edits must update the 'tags' field of a Variant or " 12 | "Relation, and no other object fields. Your request " 13 | "includes the following object fields: " 14 | "[u'variant', u'tags']"} 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | class RelationTests(APITestCase): 20 | """ 21 | Test the Relation API. 22 | """ 23 | base_path = '/relation' 24 | fixtures = ['gennotes_server/fixtures/test-data.json'] 25 | 26 | def test_get_relation(self): 27 | """ 28 | Test getting data for a single Relation. 29 | """ 30 | with open('gennotes_server/tests/expected_data/relation.json') as f: 31 | expected_data = json.load(f) 32 | 33 | self.verify_request(path='/1/', method='get', 34 | expected_data=expected_data, expected_status=200) 35 | 36 | def test_get_relation_list(self): 37 | """ 38 | Test getting full list of Relation data. 39 | """ 40 | with open('gennotes_server/tests/expected_data/' 41 | 'relation_list.json') as f: 42 | expected_data = json.load(f) 43 | 44 | self.verify_request(path='/', method='get', 45 | expected_data=expected_data, expected_status=200) 46 | 47 | def test_post_relation(self): 48 | """ 49 | Test creating a new Relation. 50 | """ 51 | good_data = {"tags": {"type": "test-relation", 52 | "other-test-tag": "some-value"}, 53 | "variant": "http://testserver/api/variant/1/"} 54 | bad_data_1 = {"tags": {"type": "test-relation", 55 | "other-test-tag": "some-value"}} 56 | err_1 = {'detail': "Create (POST) should include the 'tags' and " 57 | "'variant' fields. Your request contains the " 58 | "following fields: [u'tags']"} 59 | bad_data_2 = {"tags": {"test-tag": "some-value"}, 60 | "variant": "http://testserver/api/variant/1/"} 61 | err_2 = {"detail": "Create (POST) tag data must include all required " 62 | "tags: ['type']"} 63 | 64 | with open('gennotes_server/tests/expected_data/' 65 | 'relation_create.json') as f: 66 | expected_data = json.load(f) 67 | 68 | # Test unauthenticated. 69 | self.verify_request(path='/', method='post', 70 | expected_data=ERR_NOAUTH, expected_status=401, 71 | data=good_data, format='json') 72 | 73 | self.client.login(username='testuser', password='password') 74 | 75 | # Test missing variant data. 76 | self.verify_request(path='/', method='post', 77 | expected_data=err_1, expected_status=400, 78 | data=bad_data_1, format='json') 79 | 80 | # Test missing "type" tag. 81 | self.verify_request(path='/', method='post', 82 | expected_data=err_2, expected_status=400, 83 | data=bad_data_2, format='json') 84 | 85 | # Test good request. 86 | self.verify_request(path='/', method='post', 87 | expected_data=expected_data, expected_status=201, 88 | data=good_data, format='json') 89 | 90 | def test_delete_relation(self): 91 | """ 92 | Test deleting an existing Relation. 93 | """ 94 | # Test unauthenticated. 95 | self.verify_request(path='/1/', method='delete', 96 | expected_data=ERR_NOAUTH, expected_status=401) 97 | 98 | self.client.login(username='testuser', password='password') 99 | 100 | # Test good request. 101 | self.verify_request(path='/1/', method='delete', 102 | data={'edited_version': 11}, format='json', 103 | expected_status=204) 104 | 105 | def test_put_relation(self): 106 | """ 107 | Test editing an existing Relation via PUT. 108 | """ 109 | good_data = {"tags": {"type": "clinvar-rcva", 110 | "comment": "All other tags deleted!"}, 111 | "edited_version": 11} 112 | bad_data_1 = {"tags": {"type": "clinvar-rcva", 113 | "comment": "All other tags deleted!"}, 114 | "variant": "http://testserver/api/variant/10/", 115 | "edited_version": 11} 116 | err_1 = ERR_VAR_INC 117 | bad_data_2 = {"tags": {"type": "test-type", 118 | "comment": "All other tags deleted!"}, 119 | "edited_version": 11} 120 | err_2 = ERR_TYPE_CHNG 121 | bad_data_3 = {"tags": {"comment": "All other tags deleted!"}, 122 | "edited_version": 11} 123 | err_3 = {'detail': "PUT requests must retain all special tags. Your " 124 | "request is missing the tag: type"} 125 | 126 | with open('gennotes_server/tests/expected_data/' 127 | 'relation_put.json') as f: 128 | expected_data = json.load(f) 129 | 130 | # Test unauthenticated 131 | self.verify_request(path='/1/', method='put', 132 | expected_data=ERR_NOAUTH, expected_status=401, 133 | data=good_data, format='json') 134 | 135 | self.client.login(username='testuser', password='password') 136 | 137 | # Test bad request (attempting to edit variant field). 138 | self.verify_request(path='/1/', method='put', 139 | expected_data=err_1, expected_status=400, 140 | data=bad_data_1, format='json') 141 | 142 | # Test bad request (attempting to edit special 'type' tag). 143 | self.verify_request(path='/1/', method='put', 144 | expected_data=err_2, expected_status=400, 145 | data=bad_data_2, format='json') 146 | 147 | # Test bad request (failing to retain special 'type' tag). 148 | self.verify_request(path='/1/', method='put', 149 | expected_data=err_3, expected_status=400, 150 | data=bad_data_3, format='json') 151 | 152 | # Test good request. 153 | self.verify_request(path='/1/', method='put', 154 | expected_data=expected_data, expected_status=200, 155 | data=good_data, format='json') 156 | 157 | self.client.logout() 158 | 159 | def test_patch_relation(self): 160 | """ 161 | Test editing an existing Relation via PATCH. 162 | """ 163 | good_data_1 = {"tags": {"comment": "All other tags preserved."}, 164 | "edited_version": 11} 165 | good_data_2 = {"tags": {"type": "clinvar-rcva", 166 | "comment": "All other tags preserved."}, 167 | "edited_version": 22} 168 | bad_data_1 = {"tags": {"type": "clinvar-rcva", 169 | "comment": "All other tags preserved."}, 170 | "variant": "http://testserver/api/variant/10/", 171 | "edited_version": 11} 172 | err_1 = ERR_VAR_INC 173 | bad_data_2 = {"tags": {"type": "test-type", 174 | "comment": "All other tags preserved."}, 175 | "edited_version": 11} 176 | err_2 = ERR_TYPE_CHNG 177 | 178 | with open('gennotes_server/tests/expected_data/' 179 | 'relation_patch.json') as f: 180 | expected_data = json.load(f) 181 | 182 | # Test unauthenticated 183 | self.verify_request(path='/1/', method='patch', 184 | expected_data=ERR_NOAUTH, expected_status=401, 185 | data=good_data_1, format='json') 186 | 187 | self.client.login(username='testuser', password='password') 188 | 189 | # Test bad request (attempting to edit variant field). 190 | self.verify_request(path='/1/', method='patch', 191 | expected_data=err_1, expected_status=400, 192 | data=bad_data_1, format='json') 193 | 194 | # Test bad request (attempting to edit special 'type' tag). 195 | self.verify_request(path='/1/', method='patch', 196 | expected_data=err_2, expected_status=400, 197 | data=bad_data_2, format='json') 198 | 199 | # Test good request ('type' tag not included). 200 | self.verify_request(path='/1/', method='patch', 201 | expected_data=expected_data, expected_status=200, 202 | data=good_data_1, format='json') 203 | 204 | # Test good request ('type' tag included and unchanged). 205 | self.verify_request(path='/1/', method='patch', 206 | expected_data=expected_data, expected_status=200, 207 | data=good_data_2, format='json') 208 | 209 | self.client.logout() 210 | -------------------------------------------------------------------------------- /gennotes_server/serializers.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.contrib.auth import get_user_model 4 | from django.utils import timezone 5 | from rest_framework import permissions, serializers 6 | from reversion import revisions as reversion 7 | 8 | from .models import Relation, Variant 9 | 10 | 11 | class CurrentVersionMixin(object): 12 | 13 | def get_current_version(self, obj): 14 | """ 15 | Return current version ID for non-edit methods, otherwise 'Unknown'. 16 | 17 | When editing, a new version will be created by django-reversion. 18 | However, due to transaction timing the ID for this new Version hasn't 19 | yet been generated and stored by the time the response for the editing 20 | API call is generated. Rather than return the old, incorrect ID, we 21 | simply report 'Unknown' for editing API calls. 22 | 23 | An editing app will need to perform a new GET request to get the new 24 | version ID for the object. 25 | """ 26 | if self.context['request'].method in permissions.SAFE_METHODS: 27 | return reversion.get_for_date(obj, timezone.now()).id 28 | else: 29 | return 'Unknown' 30 | 31 | 32 | class SafeTagCurrentVersionUpdateMixin(object): 33 | 34 | def _check_current_version(self, instance): 35 | """ 36 | Check that the edited_version parameter matches the current version. 37 | 38 | If different, it indicates a probably "edit conflict": the submitted 39 | edit is being made to a stale version of the model. 40 | """ 41 | edited_version = self.context['request'].data['edited_version'] 42 | current_version = reversion.get_for_date( 43 | instance, timezone.now()).id 44 | if not current_version == edited_version: 45 | raise serializers.ValidationError(detail={ 46 | 'detail': 47 | 'Edit conflict error! The current version for this object ' 48 | 'does not match the reported version being edited.', 49 | 'current_version': current_version, 50 | 'submitted_data': self.context['request'].data, 51 | }) 52 | 53 | def _check_tag_data(self, instance, validated_data): 54 | tag_data = validated_data['tags'] 55 | # For PUT, check that special tags are retained and unchanged. 56 | if not self.partial: 57 | for tag in instance.special_tags: 58 | if tag in instance.tags and tag not in tag_data: 59 | raise serializers.ValidationError(detail={ 60 | 'detail': 'PUT requests must retain all special tags. ' 61 | 'Your request is missing the tag: {}'.format(tag)}) 62 | # Check that special tags are unchanged. 63 | for tag in instance.special_tags: 64 | if (tag in instance.tags and tag in tag_data and 65 | tag_data[tag] != instance.tags[tag]): 66 | raise serializers.ValidationError(detail={ 67 | 'detail': 'Updates (PUT or PATCH) must not attempt ' 68 | 'to change the values for special tags. Your request ' 69 | 'attempts to change the value for tag ' 70 | "'{}' from '{}' to '{}'".format( 71 | tag, instance.tags[tag], tag_data[tag])}) 72 | return tag_data 73 | 74 | def update(self, instance, validated_data): 75 | """ 76 | Update tags. Accept edit to current version, check protected tags. 77 | """ 78 | if 'edited_version' not in self.context['request'].data: 79 | raise serializers.ValidationError(detail={ 80 | 'detail': 81 | 'Edit submissions to the API must include a parameter ' 82 | "'edited_version' that reports the version ID of the item " 83 | 'being edited.' 84 | }) 85 | if ['tags'] != sorted(validated_data.keys()): 86 | raise serializers.ValidationError(detail={ 87 | 'detail': 88 | "Edits must update the 'tags' field of a Variant or " 89 | 'Relation, and no other object fields. Your request ' 90 | 'includes the following object fields: {}'.format( 91 | validated_data.keys()) 92 | }) 93 | 94 | self._check_current_version(instance) 95 | tag_data = self._check_tag_data(instance, validated_data) 96 | 97 | if self.partial: 98 | instance.tags.update(tag_data) 99 | else: 100 | instance.tags = tag_data 101 | instance.save() 102 | 103 | return instance 104 | 105 | 106 | class UserSerializer(serializers.HyperlinkedModelSerializer): 107 | """ 108 | Serialize a User object. 109 | """ 110 | 111 | class Meta: 112 | model = get_user_model() 113 | fields = ('id', 'username', 'email') 114 | 115 | 116 | class RelationSerializer(CurrentVersionMixin, 117 | SafeTagCurrentVersionUpdateMixin, 118 | serializers.HyperlinkedModelSerializer): 119 | """ 120 | Serialize a Relation object. 121 | 122 | API-mediated updates to Relations may only be performed for the tags field. 123 | 124 | POST create must include required tags (e.g. type). 125 | 126 | PUT update overwrites all tags with the tags data in the request. Any 127 | existing special tags data (e.g. type) must be retained and unchanged. 128 | 129 | PATCH update will update any tags included in the request tag data. If 130 | special tags are listed, their values must be unchanged. 131 | """ 132 | current_version = serializers.SerializerMethodField() 133 | variant = serializers.HyperlinkedRelatedField( 134 | queryset=Variant.objects.all(), view_name='variant-detail', 135 | required=False) 136 | 137 | class Meta: 138 | model = Relation 139 | 140 | def create(self, validated_data): 141 | """ 142 | Check that all required tags are included in tag data before creating. 143 | """ 144 | if ['tags', 'variant'] != sorted(validated_data.keys()): 145 | raise serializers.ValidationError(detail={ 146 | 'detail': "Create (POST) should include the 'tags' and " 147 | "'variant' fields. Your request contains the following " 148 | 'fields: {}'.format(str(validated_data.keys()))}) 149 | if 'tags' in validated_data: 150 | for tag in Relation.required_tags: 151 | if tag not in validated_data['tags']: 152 | raise serializers.ValidationError(detail={ 153 | 'detail': 'Create (POST) tag data must include all ' 154 | 'required tags: {}'.format(Relation.required_tags)}) 155 | return super(RelationSerializer, self).create(validated_data) 156 | 157 | 158 | class VariantSerializer(CurrentVersionMixin, 159 | SafeTagCurrentVersionUpdateMixin, 160 | serializers.HyperlinkedModelSerializer): 161 | """ 162 | Serialize a Variant object. 163 | 164 | API-mediated updates to Variants may only be performed for the tags field. 165 | 166 | PUT update overwrites all tags with the tags data in the request. Any 167 | existing special tags data (e.g. build 37 position) must be retained and 168 | unchanged. 169 | 170 | PATCH update will update any tags included in the request tag data. If 171 | special tags are listed, their values must be unchanged. 172 | """ 173 | b37_id = serializers.SerializerMethodField() 174 | current_version = serializers.SerializerMethodField() 175 | relation_set = RelationSerializer(many=True, required=False) 176 | 177 | class Meta: 178 | model = Variant 179 | 180 | @staticmethod 181 | def get_b37_id(obj): 182 | """ 183 | Return an ID like "b37-1-883516-G-A". 184 | """ 185 | print obj.tags 186 | return '-'.join([ 187 | 'b37', 188 | obj.tags['chrom_b37'], 189 | obj.tags['pos_b37'], 190 | obj.tags['ref_allele_b37'], 191 | obj.tags['var_allele_b37'], 192 | ]) 193 | 194 | def create(self, validated_data): 195 | """ 196 | Check that all required tags are included in tag data before creating. 197 | """ 198 | if ['tags'] != sorted(validated_data.keys()): 199 | raise serializers.ValidationError(detail={ 200 | 'detail': "Create (POST) should include the 'tags' field." 201 | "Your request contains the following " 202 | 'fields: {}'.format(str(validated_data.keys()))}) 203 | for tag in Variant.required_tags: 204 | if tag not in validated_data['tags']: 205 | raise serializers.ValidationError(detail={ 206 | 'detail': 'Create (POST) tag data must include all ' 207 | 'required tags: {}'.format(Relation.required_tags)}) 208 | if (tag == 'chrom_b37' and validated_data['tags'][tag] not 209 | in Variant.ALLOWED_CHROMS): 210 | raise serializers.ValidationError(detail={ 211 | 'detail': 'Chromosomes must be numbers: "1", "2", ' 212 | '"3"... and "23" for X, "24" for Y, and "25" for MT.'}) 213 | if Variant.objects.filter( 214 | tags__chrom_b37=validated_data['tags']['chrom_b37'], 215 | tags__pos_b37=validated_data['tags']['pos_b37'], 216 | tags__ref_allele_b37=validated_data['tags']['ref_allele_b37'], 217 | tags__var_allele_b37=validated_data['tags']['var_allele_b37']): 218 | raise serializers.ValidationError(detail={ 219 | 'detail': 'A variant for the following data already ' 220 | 'exists: {}'.format(validated_data['tags'])}) 221 | return super(VariantSerializer, self).create(validated_data) 222 | -------------------------------------------------------------------------------- /gennotes_server/views.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | 4 | from django.contrib.auth import get_user_model 5 | from django.db import transaction 6 | from django.db.models import Q 7 | from django.http import Http404 8 | from django.shortcuts import get_object_or_404 9 | 10 | from oauth2_provider.views import (ApplicationRegistration, 11 | ApplicationUpdate) 12 | from oauth2_provider.ext.rest_framework import TokenHasScope 13 | 14 | import rest_framework 15 | from rest_framework import viewsets as rest_framework_viewsets 16 | from rest_framework.generics import RetrieveAPIView 17 | from rest_framework.response import Response 18 | 19 | from reversion import revisions as reversion 20 | 21 | from .forms import EditingAppRegistrationForm 22 | from .models import CommitDeletion, Relation, Variant, EditingApplication 23 | from .permissions import EditAuthorizedOrReadOnly 24 | from .serializers import RelationSerializer, UserSerializer, VariantSerializer 25 | 26 | 27 | class VariantLookupMixin(object): 28 | """ 29 | Mixin method for looking up a variant according to b37 position. 30 | """ 31 | 32 | def _custom_variant_filter_kwargs(self, variant_lookup): 33 | """ 34 | For a variant lookup string, return the variant filter arguments. 35 | """ 36 | try: 37 | parts = variant_lookup.split('-') 38 | if parts[0] == 'b37': 39 | return { 40 | 'tags__chrom_b37': parts[1], 41 | 'tags__pos_b37': parts[2], 42 | 'tags__ref_allele_b37': parts[3], 43 | 'tags__var_allele_b37': parts[4], 44 | } 45 | except IndexError: 46 | return None 47 | return None 48 | 49 | 50 | class RevisionUpdateMixin(object): 51 | """ 52 | ViewSet mixin to record django-reversion revision, report current version. 53 | """ 54 | @transaction.atomic() 55 | @reversion.create_revision() 56 | def update(self, request, *args, **kwargs): 57 | """ 58 | Custom update method that records revisions and reports new version. 59 | """ 60 | partial = kwargs.pop('partial', False) 61 | instance = self.get_object() 62 | serializer = self.get_serializer(instance, 63 | data=request.data, 64 | partial=partial, 65 | context={'request': request}) 66 | serializer.is_valid(raise_exception=True) 67 | 68 | commit_comment = self.request.data.get('commit-comment', '') 69 | reversion.set_comment(comment=commit_comment) 70 | reversion.set_user(user=self.request.user) 71 | self.perform_update(serializer) 72 | 73 | return Response(serializer.data) 74 | 75 | 76 | class VariantViewSet(VariantLookupMixin, 77 | RevisionUpdateMixin, 78 | rest_framework.mixins.RetrieveModelMixin, 79 | rest_framework.mixins.ListModelMixin, 80 | rest_framework.mixins.CreateModelMixin, 81 | rest_framework.mixins.UpdateModelMixin, 82 | rest_framework_viewsets.GenericViewSet): 83 | """ 84 | A viewset for Variants, allowing id and position-based lookups. 85 | 86 | In addition to lookup by primary key, Variants may be referenced by 87 | build 37 information (e.g. 'b37-1-123456-C-T'). Bulk GET requests can be 88 | formed by specifying a list of variants as a parameter. 89 | 90 | Uses django-reversion to record the revision, user, and commit comment. 91 | 92 | See API Guide for more info. 93 | """ 94 | permission_classes = (EditAuthorizedOrReadOnly,) 95 | required_scopes = ['commit-edit'] 96 | queryset = Variant.objects.all() 97 | serializer_class = VariantSerializer 98 | 99 | def get_queryset(self, *args, **kwargs): 100 | """ 101 | Return all variant data, or a subset if a specific list is requested. 102 | """ 103 | queryset = super(VariantViewSet, self).get_queryset(*args, **kwargs) 104 | 105 | variant_list_json = self.request.query_params.get('variant_list', None) 106 | if not variant_list_json: 107 | return queryset 108 | variant_list = json.loads(variant_list_json) 109 | 110 | # Combine the variant list to make a single db query. 111 | Q_obj = None 112 | for variant_lookup in variant_list: 113 | if variant_lookup.isdigit(): 114 | filter_kwargs = {'id': variant_lookup} 115 | else: 116 | filter_kwargs = self._custom_variant_filter_kwargs(variant_lookup) 117 | if filter_kwargs: 118 | if not Q_obj: 119 | Q_obj = Q(**filter_kwargs) 120 | else: 121 | Q_obj = Q_obj | Q(**filter_kwargs) 122 | queryset = queryset.filter(Q_obj) 123 | return queryset 124 | 125 | def get_object(self): 126 | """ 127 | Primary key lookup if pk numeric, otherwise use custom filter kwargs. 128 | 129 | This allows us to also support build 37 lookup by chromosome, position, 130 | reference and variant. 131 | """ 132 | if self.kwargs['pk'].isdigit(): 133 | return super(VariantViewSet, self).get_object() 134 | 135 | queryset = self.filter_queryset(self.get_queryset()) 136 | 137 | filter_kwargs = self._custom_variant_filter_kwargs(self.kwargs['pk']) 138 | if not filter_kwargs: 139 | raise Http404('No {} matches the given query.'.format( 140 | queryset.model._meta.object_name)) 141 | 142 | obj = get_object_or_404(queryset, **filter_kwargs) 143 | self.check_object_permissions(self.request, obj) 144 | return obj 145 | 146 | @transaction.atomic() 147 | @reversion.create_revision() 148 | def create(self, request, *args, **kwargs): 149 | commit_comment = request.data.get('commit-comment', '') 150 | reversion.set_user(user=self.request.user) 151 | reversion.set_comment(comment=commit_comment) 152 | return super(VariantViewSet, self).create(request, *args, **kwargs) 153 | 154 | 155 | # http GET localhost:8000/api/relation/ # all relations 156 | # http GET localhost:8000/api/relation/2/ # relation with ID 2 157 | # http -a youruser:yourpass PATCH localhost:8000/api/relation/2/ \ 158 | # tags:='{"foo": "bar"}' # set tags to '{"foo": "bar"}' 159 | class RelationViewSet(RevisionUpdateMixin, 160 | rest_framework.viewsets.ModelViewSet): 161 | """ 162 | A viewset for Relations. 163 | 164 | Updating ('PUT', 'PATCH', 'POST', and "DELETE") uses django-reversion to record 165 | the revision, user, and commit comment. See API Guide for more info. 166 | """ 167 | permission_classes = (EditAuthorizedOrReadOnly,) 168 | required_scopes = ['commit-edit'] 169 | queryset = Relation.objects.all() 170 | serializer_class = RelationSerializer 171 | 172 | @transaction.atomic() 173 | @reversion.create_revision() 174 | def create(self, request, *args, **kwargs): 175 | commit_comment = request.data.get('commit-comment', '') 176 | reversion.set_user(user=self.request.user) 177 | reversion.set_comment(comment=commit_comment) 178 | return super(RelationViewSet, self).create(request, *args, **kwargs) 179 | 180 | @transaction.atomic() 181 | @reversion.create_revision() 182 | def record_destroy(self, request, instance): 183 | commit_comment = request.data.get('commit-comment', '') 184 | instance.save() 185 | reversion.set_user(user=self.request.user) 186 | reversion.set_comment(comment=commit_comment) 187 | reversion.add_meta(CommitDeletion) 188 | 189 | def destroy(self, request, *args, **kwargs): 190 | """ 191 | Check version is the latest. If so: record CommitDeletion, then delete. 192 | """ 193 | if 'edited_version' not in request.data: 194 | raise rest_framework.serializers.ValidationError(detail={ 195 | 'detail': 196 | 'Delete sumbissions to the API must include a parameter ' 197 | "'edited_version' that reports the version ID of the item " 198 | 'being deleted.' 199 | }) 200 | instance = self.get_object() 201 | current_version = reversion.get_for_date( 202 | instance, datetime.datetime.now()).id 203 | if not current_version == request.data['edited_version']: 204 | raise rest_framework.serializers.ValidationError(detail={ 205 | 'detail': 206 | 'Edit conflict error! The current version for this object ' 207 | 'does not match the reported version being deleted.', 208 | 'current_version': current_version, 209 | 'submitted_data': self.context['request'].data, 210 | }) 211 | self.record_destroy(request, instance) 212 | return super(RelationViewSet, self).destroy(request, *args, **kwargs) 213 | 214 | 215 | class CurrentUserView(RetrieveAPIView): 216 | """ 217 | A viewset that returns the current user id, username, and email. 218 | """ 219 | permission_classes = (TokenHasScope,) 220 | required_scopes = ['username', 'email'] 221 | 222 | model = get_user_model() 223 | serializer_class = UserSerializer 224 | 225 | def get_object(self): 226 | return self.request.user 227 | 228 | 229 | class EditingAppRegistration(ApplicationRegistration): 230 | form_class = EditingAppRegistrationForm 231 | 232 | def form_valid(self, form): 233 | form.instance.client_type = EditingApplication.CLIENT_CONFIDENTIAL 234 | form.instance.authorization_grant_type = EditingApplication.GRANT_AUTHORIZATION_CODE 235 | return super(EditingAppRegistration, self).form_valid(form) 236 | 237 | 238 | class EditingAppUpdate(ApplicationUpdate): 239 | fields = ['name', 'description', 'redirect_uris'] 240 | -------------------------------------------------------------------------------- /gennotes_server/tests/test_variant.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from test_helpers import APITestCase 4 | 5 | ERR_NO_DELETE = {'detail': 'Method "DELETE" not allowed.'} 6 | ERR_NO_POST = {'detail': 'Method "POST" not allowed.'} 7 | ERR_NOAUTH = {'detail': 'Authentication credentials were not provided.'} 8 | ERR_BAD_CHR = {'detail': 'Chromosomes must be numbers: "1", "2", "3"... and ' 9 | '"23" for X, "24" for Y, and "25" for MT.'} 10 | ERR_CHR_CHNG = {'detail': "Updates (PUT or PATCH) must not attempt to change " 11 | "the values for special tags. Your request " 12 | "attempts to change the value for tag 'chrom_b37' " 13 | "from '1' to '2'"} 14 | ERR_REL_INC = {'detail': "Edits must update the 'tags' field of a Variant or " 15 | "Relation, and no other object fields. Your request " 16 | "includes the following object fields: " 17 | "[u'relation_set', u'tags']"} 18 | 19 | 20 | class VariantTests(APITestCase): 21 | """ 22 | Test the Variant API. 23 | """ 24 | base_path = '/variant' 25 | fixtures = ['gennotes_server/fixtures/test-data.json'] 26 | 27 | def test_post_variant(self): 28 | """ 29 | Test Variant POST responses. 30 | """ 31 | bad_data = {'tags': {'chrom_b37': '1AF', 'pos_b37': '123456', 32 | 'ref_allele_b37': 'A', 'var_allele_b37': 'G'}} 33 | good_data = {'tags': {'chrom_b37': '1', 'pos_b37': '123456', 34 | 'ref_allele_b37': 'A', 'var_allele_b37': 'G'}} 35 | 36 | with open('gennotes_server/tests/expected_data/variant_create.json') as f: 37 | expected_data = json.load(f) 38 | 39 | # Unauthenticated 40 | self.verify_request(path='/', method='post', 41 | expected_data=ERR_NOAUTH, expected_status=401, 42 | data=good_data, format='json') 43 | # Authenticated 44 | self.client.login(username='testuser', password='password') 45 | self.verify_request(path='/', method='post', 46 | expected_data=ERR_BAD_CHR, 47 | expected_status=400, 48 | data=bad_data, format='json') 49 | # Authenticated 50 | self.client.login(username='testuser', password='password') 51 | self.verify_request(path='/', method='post', 52 | expected_data=expected_data, 53 | expected_status=201, 54 | data=good_data, format='json') 55 | self.client.logout() 56 | 57 | def test_delete_variant(self): 58 | """ 59 | Test Variant DELETE responses. 60 | """ 61 | # Unauthenticated 62 | self.verify_request(path='/b37-1-883516-G-A/', method='delete', 63 | expected_data=ERR_NOAUTH, expected_status=401) 64 | # Authenticated 65 | self.client.login(username='testuser', password='password') 66 | self.verify_request(path='/b37-1-883516-G-A/', method='delete', 67 | expected_data=ERR_NO_DELETE, expected_status=405) 68 | self.client.logout() 69 | 70 | def test_get_variant(self): 71 | """ 72 | Test getting data for a single Variant. 73 | """ 74 | with open('gennotes_server/tests/expected_data/variant.json') as f: 75 | expected_data = json.load(f) 76 | 77 | # Look up by primary key. 78 | self.verify_request(path='/1/', method='get', 79 | expected_data=expected_data, expected_status=200) 80 | 81 | # Look up by build 37 information. 82 | self.verify_request(path='/b37-1-883516-G-A/', method='get', 83 | expected_data=expected_data, expected_status=200) 84 | 85 | def test_get_specified_variant_list(self): 86 | """ 87 | Test getting data for a specified list of Variants. 88 | """ 89 | data = {'variant_list': json.dumps( 90 | ['b37-1-883516-G-A', 'b37-1-891344-G-A', '3'])} 91 | 92 | with open('gennotes_server/tests/expected_data/' 93 | 'specified_variant_list.json') as f: 94 | expected_data = json.load(f) 95 | 96 | self.verify_request(path='/', method='get', 97 | expected_data=expected_data, expected_status=200, 98 | data=data) 99 | 100 | def test_get_variant_list(self): 101 | """ 102 | Test getting full list of Variant data. 103 | """ 104 | with open('gennotes_server/tests/expected_data/' 105 | 'variant_list.json') as f: 106 | expected_data = json.load(f) 107 | 108 | self.verify_request(path='/', method='get', 109 | expected_data=expected_data, expected_status=200) 110 | 111 | def test_put_variant(self): 112 | """ 113 | Test Variant PUT responses. 114 | """ 115 | good_data = {"tags": {"chrom_b37": "1", "pos_b37": "883516", 116 | "ref_allele_b37": "G", "var_allele_b37": "A", 117 | "test_tag": "test_value"}, 118 | "edited_version": 1} 119 | with open('gennotes_server/tests/expected_data/' 120 | 'variant_put_patch1.json') as f: 121 | expected_data = json.load(f) 122 | 123 | # Test unauthorized. 124 | self.verify_request(path='/b37-1-883516-G-A/', method='put', 125 | expected_data=ERR_NOAUTH, expected_status=401, 126 | data=good_data, format='json') 127 | 128 | # Test bad data. 129 | # Should only have tags field, and all special tags included unchanged. 130 | bad_data_1 = {"tags": {"chrom_b37": "1", "pos_b37": "883516", 131 | "ref_allele_b37": "G", "var_allele_b37": "A", 132 | "test_tag": "test_value"}, 133 | "relation_set": [{ 134 | "tags": {"type": "test-relation"}, 135 | "variant": "http://testserver/api/variant/1/"}], 136 | "edited_version": 1} 137 | err_1 = ERR_REL_INC 138 | bad_data_2 = {"tags": {"chrom_b37": "2", "pos_b37": "883516", 139 | "ref_allele_b37": "G", "var_allele_b37": "A", 140 | "test_tag": "test_value"}, 141 | "edited_version": 1} 142 | err_2 = ERR_CHR_CHNG 143 | bad_data_3 = {"tags": {"chrom_b37": "1", "test_tag": "test_value"}, 144 | "edited_version": 1} 145 | err_3 = {'detail': "PUT requests must retain all special tags. Your " 146 | "request is missing the tag: pos_b37"} 147 | 148 | self.client.login(username='testuser', password='password') 149 | 150 | # Test disallowed field 'relation_set'. 151 | self.verify_request(path='/b37-1-883516-G-A/', method='put', 152 | expected_data=err_1, expected_status=400, 153 | data=bad_data_1, format='json') 154 | # Test changing special tag. 155 | self.verify_request(path='/b37-1-883516-G-A/', method='put', 156 | expected_data=err_2, expected_status=400, 157 | data=bad_data_2, format='json') 158 | # Test missing special tags. 159 | self.verify_request(path='/b37-1-883516-G-A/', method='put', 160 | expected_data=err_3, expected_status=400, 161 | data=bad_data_3, format='json') 162 | 163 | # Test good request. 164 | self.verify_request(path='/b37-1-883516-G-A/', method='put', 165 | expected_data=expected_data, expected_status=200, 166 | data=good_data, format='json') 167 | 168 | self.client.logout() 169 | 170 | def test_patch_variant(self): 171 | """ 172 | Test Variant PATCH responses. 173 | """ 174 | good_data_1 = {"tags": {"chrom_b37": "1", "test_tag": "test_value"}, 175 | "edited_version": 1} 176 | good_data_2 = {"tags": {"test_tag": "test_value_2"}, 177 | "edited_version": 21} 178 | with open('gennotes_server/tests/expected_data/' 179 | 'variant_put_patch1.json') as f: 180 | expected_data_1 = json.load(f) 181 | with open('gennotes_server/tests/expected_data/' 182 | 'variant_patch2.json') as f: 183 | expected_data_2 = json.load(f) 184 | 185 | # Test unauthorized. 186 | self.verify_request(path='/b37-1-883516-G-A/', method='patch', 187 | expected_data=ERR_NOAUTH, expected_status=401, 188 | data=good_data_1, format='json') 189 | 190 | # Test bad data. 191 | # Should only have tags field. If included, special tags unchanged. 192 | bad_data_1 = {"tags": {"chrom_b37": "1", "pos_b37": "883516", 193 | "ref_allele_b37": "G", "var_allele_b37": "A", 194 | "test_tag": "test_value"}, 195 | "relation_set": [{ 196 | "tags": {"type": "test-relation"}, 197 | "variant": "http://testserver/api/variant/1/"}], 198 | "edited_version": 1} 199 | err_1 = ERR_REL_INC 200 | bad_data_2 = {"tags": {"chrom_b37": "2", "test_tag": "test_value"}, 201 | "edited_version": 1} 202 | err_2 = ERR_CHR_CHNG 203 | 204 | self.client.login(username='testuser', password='password') 205 | 206 | # Test disallowed field 'relation_set'. 207 | self.verify_request(path='/b37-1-883516-G-A/', method='patch', 208 | expected_data=err_1, expected_status=400, 209 | data=bad_data_1, format='json') 210 | # Test changing special tag. 211 | self.verify_request(path='/b37-1-883516-G-A/', method='patch', 212 | expected_data=err_2, expected_status=400, 213 | data=bad_data_2, format='json') 214 | 215 | # Test good request #1. 216 | self.verify_request(path='/b37-1-883516-G-A/', method='patch', 217 | expected_data=expected_data_1, expected_status=200, 218 | data=good_data_1, format='json') 219 | 220 | # Test good request #2. 221 | self.verify_request(path='/b37-1-883516-G-A/', method='patch', 222 | expected_data=expected_data_2, expected_status=200, 223 | data=good_data_2, format='json') 224 | 225 | self.client.logout() 226 | -------------------------------------------------------------------------------- /gennotes_server/tests/expected_data/variant_list.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": 10, 3 | "next": null, 4 | "previous": null, 5 | "results": [ 6 | { 7 | "b37_id": "b37-1-883516-G-A", 8 | "current_version": 1, 9 | "relation_set": [ 10 | { 11 | "current_version": 18, 12 | "tags": { 13 | "clinvar-rcva:accession": "RCV000064926", 14 | "clinvar-rcva:gene-name": "nucleolar complex associated 2 homolog (S. cerevisiae)", 15 | "clinvar-rcva:gene-symbol": "NOC2L", 16 | "clinvar-rcva:num-submissions": "1", 17 | "clinvar-rcva:preferred-name": "NM_015658.3(NOC2L):c.1654C>T (p.Leu552=)", 18 | "clinvar-rcva:record-status": "current", 19 | "clinvar-rcva:significance": "not provided", 20 | "clinvar-rcva:trait-name": "Malignant melanoma", 21 | "clinvar-rcva:trait-type": "Disease", 22 | "clinvar-rcva:version": "2", 23 | "type": "clinvar-rcva" 24 | }, 25 | "url": "http://testserver/api/relation/8/", 26 | "variant": "http://testserver/api/variant/1/" 27 | } 28 | ], 29 | "tags": { 30 | "chrom_b37": "1", 31 | "pos_b37": "883516", 32 | "ref_allele_b37": "G", 33 | "var_allele_b37": "A" 34 | }, 35 | "url": "http://testserver/api/variant/1/" 36 | }, 37 | { 38 | "b37_id": "b37-1-891344-G-A", 39 | "current_version": 2, 40 | "relation_set": [ 41 | { 42 | "current_version": 19, 43 | "tags": { 44 | "clinvar-rcva:accession": "RCV000064927", 45 | "clinvar-rcva:gene-name": "nucleolar complex associated 2 homolog (S. cerevisiae)", 46 | "clinvar-rcva:gene-symbol": "NOC2L", 47 | "clinvar-rcva:num-submissions": "1", 48 | "clinvar-rcva:preferred-name": "NM_015658.3(NOC2L):c.657C>T (p.Leu219=)", 49 | "clinvar-rcva:record-status": "current", 50 | "clinvar-rcva:significance": "not provided", 51 | "clinvar-rcva:trait-name": "Malignant melanoma", 52 | "clinvar-rcva:trait-type": "Disease", 53 | "clinvar-rcva:version": "2", 54 | "type": "clinvar-rcva" 55 | }, 56 | "url": "http://testserver/api/relation/9/", 57 | "variant": "http://testserver/api/variant/2/" 58 | } 59 | ], 60 | "tags": { 61 | "chrom_b37": "1", 62 | "pos_b37": "891344", 63 | "ref_allele_b37": "G", 64 | "var_allele_b37": "A" 65 | }, 66 | "url": "http://testserver/api/variant/2/" 67 | }, 68 | { 69 | "b37_id": "b37-1-906168-G-A", 70 | "current_version": 3, 71 | "relation_set": [ 72 | { 73 | "current_version": 20, 74 | "tags": { 75 | "clinvar-rcva:accession": "RCV000064940", 76 | "clinvar-rcva:gene-name": "pleckstrin homology domain containing, family N member 1", 77 | "clinvar-rcva:gene-symbol": "PLEKHN1", 78 | "clinvar-rcva:num-submissions": "1", 79 | "clinvar-rcva:preferred-name": "NM_001160184.1(PLEKHN1):c.484+30G>A", 80 | "clinvar-rcva:record-status": "current", 81 | "clinvar-rcva:significance": "not provided", 82 | "clinvar-rcva:trait-name": "Malignant melanoma", 83 | "clinvar-rcva:trait-type": "Disease", 84 | "clinvar-rcva:version": "2", 85 | "type": "clinvar-rcva" 86 | }, 87 | "url": "http://testserver/api/relation/10/", 88 | "variant": "http://testserver/api/variant/3/" 89 | } 90 | ], 91 | "tags": { 92 | "chrom_b37": "1", 93 | "pos_b37": "906168", 94 | "ref_allele_b37": "G", 95 | "var_allele_b37": "A" 96 | }, 97 | "url": "http://testserver/api/variant/3/" 98 | }, 99 | { 100 | "b37_id": "b37-1-949523-C-T", 101 | "current_version": 4, 102 | "relation_set": [ 103 | { 104 | "current_version": 17, 105 | "tags": { 106 | "clinvar-rcva:accession": "RCV000162196", 107 | "clinvar-rcva:gene-name": "ISG15 ubiquitin-like modifier", 108 | "clinvar-rcva:gene-symbol": "ISG15", 109 | "clinvar-rcva:num-submissions": "1", 110 | "clinvar-rcva:preferred-name": "NM_005101.3(ISG15):c.163C>T (p.Gln55Ter)", 111 | "clinvar-rcva:record-status": "current", 112 | "clinvar-rcva:significance": "Pathogenic", 113 | "clinvar-rcva:trait-name": "Immunodeficiency 38", 114 | "clinvar-rcva:trait-type": "Disease", 115 | "clinvar-rcva:version": "2", 116 | "type": "clinvar-rcva" 117 | }, 118 | "url": "http://testserver/api/relation/7/", 119 | "variant": "http://testserver/api/variant/4/" 120 | } 121 | ], 122 | "tags": { 123 | "chrom_b37": "1", 124 | "pos_b37": "949523", 125 | "ref_allele_b37": "C", 126 | "var_allele_b37": "T" 127 | }, 128 | "url": "http://testserver/api/variant/4/" 129 | }, 130 | { 131 | "b37_id": "b37-1-949696-C-CG", 132 | "current_version": 5, 133 | "relation_set": [ 134 | { 135 | "current_version": 16, 136 | "tags": { 137 | "clinvar-rcva:accession": "RCV000148989", 138 | "clinvar-rcva:citations": "PMID22859821", 139 | "clinvar-rcva:gene-name": "ISG15 ubiquitin-like modifier", 140 | "clinvar-rcva:gene-symbol": "ISG15", 141 | "clinvar-rcva:num-submissions": "1", 142 | "clinvar-rcva:preferred-name": "NM_005101.3(ISG15):c.339dupG (p.Leu114Alafs)", 143 | "clinvar-rcva:record-status": "current", 144 | "clinvar-rcva:significance": "Pathogenic", 145 | "clinvar-rcva:trait-name": "Immunodeficiency 38", 146 | "clinvar-rcva:trait-type": "Disease", 147 | "clinvar-rcva:version": "4", 148 | "type": "clinvar-rcva" 149 | }, 150 | "url": "http://testserver/api/relation/6/", 151 | "variant": "http://testserver/api/variant/5/" 152 | } 153 | ], 154 | "tags": { 155 | "chrom_b37": "1", 156 | "pos_b37": "949696", 157 | "ref_allele_b37": "C", 158 | "var_allele_b37": "CG" 159 | }, 160 | "url": "http://testserver/api/variant/5/" 161 | }, 162 | { 163 | "b37_id": "b37-1-949739-G-T", 164 | "current_version": 6, 165 | "relation_set": [ 166 | { 167 | "current_version": 15, 168 | "tags": { 169 | "clinvar-rcva:accession": "RCV000148988", 170 | "clinvar-rcva:gene-name": "ISG15 ubiquitin-like modifier", 171 | "clinvar-rcva:gene-symbol": "ISG15", 172 | "clinvar-rcva:num-submissions": "1", 173 | "clinvar-rcva:preferred-name": "NM_005101.3(ISG15):c.379G>T (p.Glu127Ter)", 174 | "clinvar-rcva:record-status": "current", 175 | "clinvar-rcva:significance": "Pathogenic", 176 | "clinvar-rcva:trait-name": "Immunodeficiency 38", 177 | "clinvar-rcva:trait-type": "Disease", 178 | "clinvar-rcva:version": "4", 179 | "type": "clinvar-rcva" 180 | }, 181 | "url": "http://testserver/api/relation/5/", 182 | "variant": "http://testserver/api/variant/6/" 183 | } 184 | ], 185 | "tags": { 186 | "chrom_b37": "1", 187 | "pos_b37": "949739", 188 | "ref_allele_b37": "G", 189 | "var_allele_b37": "T" 190 | }, 191 | "url": "http://testserver/api/variant/6/" 192 | }, 193 | { 194 | "b37_id": "b37-1-955597-G-T", 195 | "current_version": 7, 196 | "relation_set": [ 197 | { 198 | "current_version": 12, 199 | "tags": { 200 | "clinvar-rcva:accession": "RCV000116272", 201 | "clinvar-rcva:gene-name": "agrin", 202 | "clinvar-rcva:gene-symbol": "AGRN", 203 | "clinvar-rcva:num-submissions": "1", 204 | "clinvar-rcva:preferred-name": "NM_198576.3(AGRN):c.45G>T (p.Pro15=)", 205 | "clinvar-rcva:record-status": "current", 206 | "clinvar-rcva:significance": "Likely benign", 207 | "clinvar-rcva:trait-name": "not specified", 208 | "clinvar-rcva:trait-type": "Disease", 209 | "clinvar-rcva:version": "2", 210 | "type": "clinvar-rcva" 211 | }, 212 | "url": "http://testserver/api/relation/2/", 213 | "variant": "http://testserver/api/variant/7/" 214 | } 215 | ], 216 | "tags": { 217 | "chrom_b37": "1", 218 | "pos_b37": "955597", 219 | "ref_allele_b37": "G", 220 | "var_allele_b37": "T" 221 | }, 222 | "url": "http://testserver/api/variant/7/" 223 | }, 224 | { 225 | "b37_id": "b37-1-957640-C-T", 226 | "current_version": 8, 227 | "relation_set": [ 228 | { 229 | "current_version": 13, 230 | "tags": { 231 | "clinvar-rcva:accession": "RCV000116258", 232 | "clinvar-rcva:esp-allele-frequency": "0.031754574812", 233 | "clinvar-rcva:gene-name": "agrin", 234 | "clinvar-rcva:gene-symbol": "AGRN", 235 | "clinvar-rcva:num-submissions": "1", 236 | "clinvar-rcva:preferred-name": "NM_198576.3(AGRN):c.261C>T (p.Asp87=)", 237 | "clinvar-rcva:record-status": "current", 238 | "clinvar-rcva:significance": "Likely benign", 239 | "clinvar-rcva:trait-name": "not specified", 240 | "clinvar-rcva:trait-type": "Disease", 241 | "clinvar-rcva:version": "2", 242 | "type": "clinvar-rcva" 243 | }, 244 | "url": "http://testserver/api/relation/3/", 245 | "variant": "http://testserver/api/variant/8/" 246 | } 247 | ], 248 | "tags": { 249 | "chrom_b37": "1", 250 | "pos_b37": "957640", 251 | "ref_allele_b37": "C", 252 | "var_allele_b37": "T" 253 | }, 254 | "url": "http://testserver/api/variant/8/" 255 | }, 256 | { 257 | "b37_id": "b37-1-976629-C-T", 258 | "current_version": 9, 259 | "relation_set": [ 260 | { 261 | "current_version": 14, 262 | "tags": { 263 | "clinvar-rcva:accession": "RCV000116282", 264 | "clinvar-rcva:gene-name": "agrin", 265 | "clinvar-rcva:gene-symbol": "AGRN", 266 | "clinvar-rcva:num-submissions": "1", 267 | "clinvar-rcva:preferred-name": "NM_198576.3(AGRN):c.804C>T (p.Ala268=)", 268 | "clinvar-rcva:record-status": "current", 269 | "clinvar-rcva:significance": "Benign", 270 | "clinvar-rcva:trait-name": "not specified", 271 | "clinvar-rcva:trait-type": "Disease", 272 | "clinvar-rcva:version": "1", 273 | "type": "clinvar-rcva" 274 | }, 275 | "url": "http://testserver/api/relation/4/", 276 | "variant": "http://testserver/api/variant/9/" 277 | } 278 | ], 279 | "tags": { 280 | "chrom_b37": "1", 281 | "pos_b37": "976629", 282 | "ref_allele_b37": "C", 283 | "var_allele_b37": "T" 284 | }, 285 | "url": "http://testserver/api/variant/9/" 286 | }, 287 | { 288 | "b37_id": "b37-1-976963-A-G", 289 | "current_version": 10, 290 | "relation_set": [ 291 | { 292 | "current_version": 11, 293 | "tags": { 294 | "clinvar-rcva:accession": "RCV000116253", 295 | "clinvar-rcva:esp-allele-frequency": "0.014089016971", 296 | "clinvar-rcva:gene-name": "agrin", 297 | "clinvar-rcva:gene-symbol": "AGRN", 298 | "clinvar-rcva:num-submissions": "1", 299 | "clinvar-rcva:preferred-name": "NM_198576.3(AGRN):c.1058A>G (p.Gln353Arg)", 300 | "clinvar-rcva:record-status": "current", 301 | "clinvar-rcva:significance": "Likely benign", 302 | "clinvar-rcva:trait-name": "not specified", 303 | "clinvar-rcva:trait-type": "Disease", 304 | "clinvar-rcva:version": "2", 305 | "type": "clinvar-rcva" 306 | }, 307 | "url": "http://testserver/api/relation/1/", 308 | "variant": "http://testserver/api/variant/10/" 309 | } 310 | ], 311 | "tags": { 312 | "chrom_b37": "1", 313 | "pos_b37": "976963", 314 | "ref_allele_b37": "A", 315 | "var_allele_b37": "G" 316 | }, 317 | "url": "http://testserver/api/variant/10/" 318 | } 319 | ] 320 | } 321 | -------------------------------------------------------------------------------- /gennotes_server/templates/api_guide/guide.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |

GenNotes API

5 | 6 |

7 | The GenNotes API allows other services to interact with GenNotes to retrieve, 8 | add, and edit the data in the GenNotes database. 9 |

10 | 11 | 34 | 35 |

1. Retrieving data

36 | 37 |

38 | Data can be retrieving using GET commands. No GenNotes account is needed 39 | to retrieve data. Data is returned in JSON format. 40 |

41 | 42 |

43 | Note: The API is intended for programmatic use, e.g. using the Python 44 | 'requests' module. Navigating to these pages in the browser will render 45 | the response within a page layout. To see raw JSON that is returned, you 46 | can access JSON content directly within the browser by adding '.json' to 47 | the end, e.g. 48 |

58 |

59 | 60 |

61 | 1.1 Get individual variant data

62 | 63 |

64 | An individual variant's data can be retrieved based on build 37 coordinates, 65 | or based on the GenNotes variant ID. 66 |

67 | 68 |

69 | Example GET commands: 70 |

71 | 77 | 78 |

79 | 1.2 Get multiple variant data

80 | 81 |

82 | Multiple variants can be retrieved at once by calling `/api/variant/` with 83 | the `variant_list` parameter set to a JSON-formatted list of variant IDs. 84 | (Results are only returned for valid variants; identifiers not matching 85 | variants in GenNotes will silently fail.) 86 |

87 | 88 |

89 | Example GET command: 90 |

91 | 95 | 96 |

1.3 Get individual relation data

97 | 98 |

99 | An individual relations's data can be retrieved based on the GenNotes 100 | relation ID. Currently querying multiple relations is not supported. 101 |

102 | 103 |

104 | Example GET commands: 105 |

106 | 110 | 111 |

2. Adding and editing data

112 | 113 |

2.1 Authentication

114 | 115 |

2.1.1 Using your own credentials

116 | 117 |

118 | Note: This is not the anticipated method of submitting edits. 119 |

120 |

121 | The anticipated method for submitting changes is through apps that you 122 | authorize to submit to GenNotes on your behalf (e.g. Genevieve). However 123 | it is possible to submit directly using your account credentials, using 124 | HTTP Basic Authorization. 125 |

126 |

127 | For example, using fake IDs and the Python requests module for a PATCH edit: 128 |

129 |
130 | requests.patch(
131 |     'http://localhost:8000/api/relation/123/',
132 |     data=json.dumps({
133 |         'tags': {'example-tag': 'example-value'},
134 |         'commit-comment': 'Adding an example tag using PATCH and HTTP Basic Authorization.'}),
135 |     headers={'Content-type': 'application/json'},
136 |     auth=('username', 'password'))
137 | 
138 | 139 |

2.1.2 Using behalf of another user, via OAuth2

140 | 141 |

142 | A GenNotes account is needed to register an app that will submit edits on 143 | behalf of other GenNotes users. Although you could use any account for this 144 | purpose, you may want to create a separate "non-personal" account for 145 | your OAuth2 app. You may want to do this if, for example, you're setting 146 | up a Genevieve client. 147 |

148 | 149 |

150 | Note: if you work with multiple accounts, be careful to check which 151 | account you're logged in to! When you're logged in, your username is 152 | displayed at the top left. 153 |

154 | 155 |

156 | Once you have an account, you can 157 | 158 | register a client application here. 159 |

160 | 161 |

Example process for getting authorization and access tokens:

162 | 163 |
    164 |
  1. Send user to: 165 | /oauth2-app/authorize?client_id=[client-id-here]&response_type=code 166 |
  2. 167 |
  3. Your default redirect_uri will receive the grant code, e.g.: 168 | yourdomain.com/path/to/redirect_uri?code=[grant-code] 169 |
  4. 170 |
  5. Exchange that grant code for a token immediately. For example, using 171 | Python with the requests module: 172 |
      173 |
    • Set this up with your client_id and client_secret: 174 | client_auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 175 |
    • 176 |
    • Set the code: 177 | code = [the grant-code you just received] 178 |
    • 179 |
    • 180 | Set redirect_uri (required by our framework...): 181 | redirect_uri = [a redirect uri you registered] 182 |
    • 183 |
    • 184 | Set the GenNotes receiving uri: 185 | token_uri = 'http://gennotes.herokuapp.com/oauth2-app/token/' 186 |
    • 187 |
    • 188 | POST to this: 189 |
      response_token = requests.post(token_uri, data={
      190 |     'grant_type': 'authorization_code',
      191 |     'code': code, 'redirect_uri': redirect_uri}, auth=client_auth)
      192 |
    • 193 |
    • 194 | The response should contain an access token, e.g.: 195 |
      {'access_token': '1hu94IRBX3da0euOiX0u3E9h',
      196 |  'token_type': 'Bearer',
      197 |  'expires_in': 36000,
      198 |  'refresh_token': 'WSuwoeBO0e9JFHqY7TnpDi7jUjgAex',
      199 |  'scope': 'commit-edit'}
      200 |
    • 201 |
    • 202 | To use the refresh token to get new tokens: 203 |
      refresh_token = response_token.json()['refresh_token']
      204 | response_refresh = requests.post(token_uri, data={
      205 |     'grant_type': 'refresh_token',
      206 |     'refresh_token': refresh_token}, auth=client_auth)
      207 |
    • 208 |
    209 |
  6. 210 |
211 | 212 |

213 | Once a user has authorized your client app to make edits on their behalf, 214 | you can use a valid access token to submit edits through the API. 215 |

216 | 217 |

218 | For example, using fake IDs and the Python requests module for a PATCH edit: 219 |

220 | 221 |
222 | requests.patch(
223 |     'http://localhost:8000/api/relation/123/',
224 |     data=json.dumps({
225 |         'tags': {'example-tag-key': 'example-tag-value'},
226 |         'commit-comment': 'Adding an example tag using PATCH and OAuth2 authorization.'}),
227 |     headers={'Content-type': 'application/json',
228 |              'Authorization': 'Bearer {}'.format(access_token)})
229 | 
230 | 231 |

2.2 PATCH: Partial edit

232 | 233 |

234 | When you submit an edit using PATCH, the tag keys you include will be 235 | created or replaced with the values you submit. Submitting edits using PATCH 236 | is preferred, as there is less danger of accidentally overwriting information. 237 |

238 | 239 |

240 | For a Variant, a PATCH specifies the following parameters: 241 | 'edited_version', 'tags', and 'commit-comment' (optional).
242 | For a Relation, a PATCH specifies the following parameters: 243 | 'edited_version', 'tags', 'variant' (optional), and 244 | commit-comment (optional). 245 |

246 | 247 |

248 | Returned: In response, you receive a copy of the updated data for the 249 | object. Unfortunately, the "current_version" will be "Unknown". Due to 250 | how edit versioning is being performed, we're unable to return the version ID 251 | for the updated object in this response. A separate GET will need to be 252 | performed to discover this. 253 |

254 | 255 |

256 | Example using fake IDs, the Python requests module and OAuth2 access 257 | token authorization: 258 |

259 | 260 |
261 | requests.patch(
262 |     'http://localhost:8000/api/relation/123/',
263 |     data=json.dumps({
264 |         'commit-comment': 'Adding an example tag using PATCH and OAuth2 authorization.'}),
265 |         'edited_version': 456,
266 |         'tags': {'example-tag': 'example-value'},
267 |     headers={'Content-type': 'application/json',
268 |              'Authorization': 'Bearer {}'.format(access_token)})
269 | 
270 | 271 |

2.3 PUT: Whole edit

272 | 273 |

274 | When you submit an edit using PUT, the object becomes redefined with the data 275 | you include. Submitting edits using PUT could be dangerous: by omitting 276 | existing data, you delete it. On the other hand, a PUT edit is the only way 277 | to delete existing tags. 278 |

279 | 280 |

281 | For a Variant, a PUT specifies the following parameters: 282 | 'edited_version', 'tags', and 'commit-comment' (optional).
283 | For a Relation, a PUT specifies the following parameters: 284 | 'edited_version', 'tags', 'variant', and 285 | commit-comment (optional). 286 |

287 | 288 |

289 | Returned: In response, you receive a copy of the updated data for the 290 | object. Unfortunately, the "current_version" will be "Unknown". Due to 291 | how edit versioning is being performed, we're unable to return the version ID 292 | for the updated object in this response. A separate GET will need to be 293 | performed to discover this. 294 |

295 | 296 |

297 | Example using fake IDs, the Python requests module and OAuth2 access 298 | token authorization: 299 |

300 | 301 |
302 | requests.put(
303 |     'http://localhost:8000/api/relation/123/',
304 |     data=json.dumps({
305 |         'commit-comment': 'Updating a Relation using PUT and OAuth2 authorization.'
306 |         'edited_version': 456,
307 |         'tags': {
308 |             'example-tag': 'A new example tag, with example value here.',
309 |             'genevieve:notes': 'Some updated notes might also be in here... or maybe we're retaining the original.',
310 |             'genevieve:inheritance': 'recessive',
311 |             'genevieve:trait-name': 'Hemochromatosis',
312 |             'genevieve:clinvar-rcva-list': '["RCV000000028"]',
313 |             'type': 'genevieve',
314 |         },
315 |         'variant": "http://testserver/api/variant/789/',
316 |     }),
317 |     headers={'Content-type': 'application/json',
318 |              'Authorization': 'Bearer {}'.format(access_token)})
319 | 
320 | 321 |

2.4 POST: New object

322 | 323 |

324 | The POST method is used to create a new object. This method is only allowed 325 | for Relations. (Currently, new Variants cannot be added to the database via 326 | the API.) 327 |

328 | 329 |

330 | For a Relation, a POST specifies the following parameters: 331 | 'edited_version', 'tags', 'variant', and 332 | commit-comment (optional). 333 |

334 | 335 |

336 | Returned: In response, you receive a copy of the data for the new 337 | object, including its ID. Unfortunately, the "current_version" will be 338 | "Unknown". Due to how edit versioning is being performed, we're unable to 339 | return the version ID for the updated object in this response. A separate GET 340 | will need to be performed to discover this. 341 |

342 | 343 |

344 | Example using fake IDs, the Python requests module and OAuth2 access 345 | token authorization: 346 |

347 | 348 |
349 | requests.post(
350 |     'http://localhost:8000/api/relation/',
351 |     data=json.dumps({
352 |         'commit-comment': 'Creating a Relation using POST and OAuth2 authorization.'
353 |         'tags': {
354 |             'genevieve:notes': 'Initial notes here.',
355 |             'genevieve:trait-name': 'Hemochromatosis',
356 |             'type': 'genevieve',
357 |         },
358 |         'variant": "http://testserver/api/variant/789/',
359 |     }),
360 |     headers={'Content-type': 'application/json',
361 |              'Authorization': 'Bearer {}'.format(access_token)})
362 | 
363 | 364 |

2.4 DELETE: Destroy object

365 | 366 |

367 | The DELETE method is used to destroy an object. This method is only allowed 368 | for Relations. (Currently, Variants cannot be removed from the database via 369 | the API.) 370 |

371 | 372 |

373 | For a Relation, a DELETE specifies the following parameters: 374 | 'edited_version', commit-comment (optional). 375 |

376 | 377 |

378 | Returned: A 204 status is sent in response to a successful DELETE 379 | API call. 380 |

381 | 382 |

383 | Example using fake IDs, the Python requests module and OAuth2 access 384 | token authorization: 385 |

386 | 387 |
388 | requests.delete(
389 |     'http://localhost:8000/api/relation/123/',
390 |     data=json.dumps({
391 |         'commit-comment': 'Removing a Relation using DELETE and OAuth2 authorization.',
392 |         'edited_version': 456,
393 |     }),
394 |     headers={'Content-type': 'application/json',
395 |              'Authorization': 'Bearer {}'.format(access_token)})
396 | 
397 | 398 |
399 | {% endblock content %} 400 | -------------------------------------------------------------------------------- /gennotes_server/management/commands/add_clinvar_data.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import fileinput 3 | from ftplib import FTP 4 | import gzip 5 | import json 6 | import logging 7 | import md5 8 | from optparse import make_option 9 | import os 10 | import re 11 | import shutil 12 | import tempfile 13 | 14 | from django.contrib.auth import get_user_model 15 | from django.core.management.base import BaseCommand, CommandError 16 | from django.db import transaction 17 | import reversion 18 | from vcf2clinvar.clinvar import ClinVarVCFLine 19 | 20 | from gennotes_server.models import Variant, Relation 21 | from gennotes_server.utils import map_chrom_to_index 22 | 23 | try: 24 | # faster implementation using bindings to libxml 25 | from lxml import etree as ET 26 | except ImportError: 27 | logging.info('Falling back to default ElementTree implementation') 28 | from xml.etree import ElementTree as ET 29 | 30 | CV_VCF_DIR = 'pub/clinvar/vcf_GRCh37' 31 | CV_XML_DIR = 'pub/clinvar/xml' 32 | 33 | CV_VCF_REGEX = r'^clinvar_[0-9]{8}.vcf.gz$' 34 | CV_XML_REGEX = r'^ClinVarFullRelease_[0-9]{4}-[0-9]{2}.xml.gz$' 35 | 36 | SPLITTER = re.compile('[,|]') 37 | 38 | # Keep track of tags used for ReferenceClinVarAssertion data. Tags are keys. 39 | # Values are tuples of (variable-type, function); during parsing the function 40 | # is applied to that variable type to retrieve corresponding data from the XML. 41 | RCVA_DATA = { 42 | 'type': 43 | (None, lambda: 'clinvar-rcva'), 44 | 'clinvar-rcva:accession': 45 | ('rcva', lambda rcva: rcva.find('ClinVarAccession').get('Acc')), 46 | 'clinvar-rcva:version': 47 | ('rcva', lambda rcva: rcva.find('ClinVarAccession').get('Version')), 48 | 'clinvar-rcva:trait-name': 49 | ('rcva', lambda rcva: rcva.findtext( 50 | 'TraitSet/Trait/Name/ElementValue[@Type="Preferred"]')), 51 | 'clinvar-rcva:trait-type': 52 | ('rcva', lambda rcva: rcva.find( 53 | 'TraitSet/Trait/Name/ElementValue[@Type="Preferred"]/../..' 54 | ).get('Type')), 55 | 'clinvar-rcva:significance': 56 | ('rcva', lambda rcva: rcva.findtext( 57 | 'ClinicalSignificance/Description')), 58 | 'clinvar-rcva:num-submissions': 59 | ('ele', lambda ele: str(len(ele.findall('ClinVarAssertion')))), 60 | 'clinvar-rcva:record-status': 61 | ('rcva', lambda rcva: rcva.findtext('RecordStatus')), 62 | 'clinvar-rcva:gene-name': 63 | ('rcva', lambda rcva: rcva.find( 64 | 'MeasureSet/Measure/MeasureRelationship' 65 | '[@Type="variant in gene"]').findtext('Name/ElementValue')), 66 | 'clinvar-rcva:gene-symbol': 67 | ('rcva', lambda rcva: rcva.find( 68 | 'MeasureSet/Measure/MeasureRelationship' 69 | '[@Type="variant in gene"]').findtext('Symbol/ElementValue')), 70 | 'clinvar-rcva:citations': 71 | ('rcva', lambda rcva: 72 | ';'.join(['PMID%s' % c.text for c in rcva.findall( 73 | 'MeasureSet/Measure/Citation/ID[@Source="PubMed"]')])), 74 | 'clinvar-rcva:esp-allele-frequency': 75 | ('rcva', lambda rcva: 76 | # Using list comprehension to enable conditional wo/ separate fxn 77 | [xref.findtext('../Attribute[@Type="AlleleFrequency"]') if xref is 78 | not None else None for xref in [ 79 | rcva.find('MeasureSet/Measure/AttributeSet/XRef[@DB=' + 80 | '"NHLBI GO Exome Sequencing Project (ESP)"]')]][0]), 81 | 'clinvar-rcva:preferred-name': 82 | ('rcva', lambda rcva: rcva.findtext( 83 | 'MeasureSet[@Type="Variant"]/Measure/Name/' + 84 | 'ElementValue[@Type="Preferred"]')), 85 | } 86 | 87 | 88 | class Command(BaseCommand): 89 | help = 'Download latest ClinVar VCF, import variants not already in db.' 90 | 91 | option_list = BaseCommand.option_list + ( 92 | make_option('-c', '--local-vcf', 93 | dest='local_vcf', 94 | help='Open local ClinVar VCF file'), 95 | make_option('-x', '--local-xml', 96 | dest='local_xml', 97 | help='Open local ClinVar XML file'), 98 | make_option('-n', '--num-vars', 99 | dest='max_num', 100 | help="Maximum number of variants to store in db.") 101 | ) 102 | 103 | def _hash_xml_dict(self, d): 104 | return md5.md5(json.dumps( 105 | [(k, d.get(k, '')) for k in RCVA_DATA.keys()])).hexdigest() 106 | 107 | def _get_elements(self, fp, tag): 108 | ''' 109 | Convenience and memory management function 110 | that iterates required tags 111 | ''' 112 | context = iter(ET.iterparse(fp, events=('start', 'end'))) 113 | _, root = next(context) # get root element 114 | for event, elem in context: 115 | if event == 'end' and elem.tag == tag: 116 | yield elem 117 | root.clear() # preserve memory 118 | 119 | def _open(self, fp): 120 | if 'xml' in fp: 121 | return fileinput.hook_compressed(fp, 'r') 122 | 123 | if fp.endswith('.gz'): 124 | reader = codecs.getreader("utf-8") 125 | return reader(gzip.open(fp)) 126 | return codecs.open(fp, encoding='utf-8', mode='r') 127 | 128 | def _download_latest_clinvar(self, dest_dir): 129 | ftp = FTP('ftp.ncbi.nlm.nih.gov') 130 | ftp.login() 131 | ftp.cwd(CV_VCF_DIR) 132 | cv_vcf_w_date = [f for f in ftp.nlst() if re.match(CV_VCF_REGEX, f)] 133 | if len(cv_vcf_w_date) != 1: 134 | raise CommandError('ClinVar reporting more than one VCF matching' + 135 | ' regex: \'{0}\' in directory {1}'.format( 136 | CV_VCF_REGEX, CV_VCF_DIR)) 137 | ftp_vcf_filename = cv_vcf_w_date[0] 138 | dest_filepath = os.path.join(dest_dir, ftp_vcf_filename) 139 | with open(dest_filepath, 'w') as fh: 140 | ftp.retrbinary('RETR {0}'.format(ftp_vcf_filename), fh.write) 141 | return dest_filepath, ftp_vcf_filename 142 | 143 | def _download_latest_clinvar_xml(self, dest_dir): 144 | ftp = FTP('ftp.ncbi.nlm.nih.gov') 145 | ftp.login() 146 | ftp.cwd(CV_XML_DIR) 147 | # sort just in case the ftp lists the files in random order 148 | cv_xml_w_date = sorted( 149 | [f for f in ftp.nlst() if re.match(CV_XML_REGEX, f)]) 150 | if len(cv_xml_w_date) == 0: 151 | raise CommandError('ClinVar reporting zero XML matching' + 152 | ' regex: \'{0}\' in directory {1}'.format( 153 | CV_XML_REGEX, CV_XML_DIR)) 154 | ftp_xml_filename = cv_xml_w_date[-1] 155 | dest_filepath = os.path.join(dest_dir, ftp_xml_filename) 156 | with open(dest_filepath, 'w') as fh: 157 | ftp.retrbinary('RETR {0}'.format(ftp_xml_filename), fh.write) 158 | return dest_filepath, ftp_xml_filename 159 | 160 | @transaction.atomic() 161 | @reversion.create_revision() 162 | def _save_as_revision(self, object_list, user, comment): 163 | for object in object_list: 164 | object.save() 165 | reversion.set_user(user=user) 166 | reversion.set_comment(comment=comment) 167 | 168 | def handle(self, local_vcf=None, local_xml=None, max_num=None, *args, **options): 169 | # The clinvar_user will be recorded as the editor by reversion. 170 | logging.basicConfig(level=logging.INFO, 171 | format='%(asctime)s - %(message)s') 172 | clinvar_user = get_user_model().objects.get( 173 | username='clinvar-data-importer') 174 | 175 | # Store objects that need to be saved (created or updated) to db. 176 | # These are saved in a separate function so they're represented as 177 | # different Revisions (sets of changes) in django-reversion. 178 | variants_new = [] 179 | relations_new = [] 180 | relations_updated = [] 181 | 182 | # Load ClinVar VCF. 183 | logging.info('Loading ClinVar VCF file...') 184 | if not (local_vcf and local_xml): 185 | tempdir = tempfile.mkdtemp() 186 | logging.info('Created tempdir {}'.format(tempdir)) 187 | if local_vcf: 188 | cv_fp, vcf_filename = local_vcf, os.path.split(local_vcf)[-1] 189 | else: 190 | cv_fp, vcf_filename = self._download_latest_clinvar(tempdir) 191 | logging.info('Loaded Clinvar VCF, stored at {}'.format(cv_fp)) 192 | 193 | logging.info('Caching existing variants with build 37 lookup info.') 194 | variant_map = {( 195 | v.tags['chrom-b37'], 196 | v.tags['pos-b37'], 197 | v.tags['ref-allele-b37'], 198 | v.tags['var-allele-b37']): v for v in Variant.objects.all() if 199 | 'chrom-b37' in v.tags and 200 | 'pos-b37' in v.tags and 201 | 'ref-allele-b37' in v.tags and 202 | 'var-allele-b37' in v.tags} 203 | 204 | # Dict to track ClinVar RCV records in VCF and corresponding Variants. 205 | # Key: RCV accession number. Value: set of tuples of values for Variant 206 | # tags: ('chrom-b37', 'pos-b37', 'ref-allele-b37', 'var-allele-b37') 207 | rcv_map = {} 208 | 209 | logging.info('Done caching variants. Reading VCF.') 210 | clinvar_vcf = self._open(cv_fp) 211 | 212 | # Add Variants if they have ClinVar data. Variants are initially added 213 | # only with the build 37 position information from the VCF. 214 | num_vars = 0 215 | for line in clinvar_vcf: 216 | if line.startswith('#'): 217 | continue 218 | 219 | # Useful for generating small dataset for our testing fixture. 220 | num_vars += 1 221 | if max_num and num_vars > int(max_num): 222 | break 223 | 224 | # Parse ClinVar information using vcf2clinvar. 225 | cvvl = ClinVarVCFLine(vcf_line=line).as_dict() 226 | data = line.rstrip('\n').split('\t') 227 | 228 | # Get build 37 position information. 229 | chrom = map_chrom_to_index(data[0]) 230 | pos = data[1] 231 | ref_allele = data[3] 232 | var_alleles = data[4].split(',') 233 | all_alleles = [ref_allele] + var_alleles 234 | info_dict = {v[0]: v[1] for v in 235 | [x.split('=') for x in data[7].split(';')] 236 | if len(v) == 2} 237 | 238 | for allele in info_dict['CLNALLE'].split(','): 239 | # Check if we already have this Variant. If not, add it. 240 | var_allele = all_alleles[int(allele)] 241 | var_key = (chrom, pos, ref_allele, var_allele) 242 | if var_key not in variant_map: 243 | # logging.info('Inserting new Variant' 244 | # '{}, {}, {}, {}'.format(*var_key)) 245 | variant = Variant(tags={ 246 | 'chrom-b37': chrom, 247 | # Check pos is a valid int before adding. 248 | 'pos-b37': str(int(pos)), 249 | 'ref-allele-b37': ref_allele, 250 | 'var-allele-b37': var_allele}) 251 | variants_new.append(variant) 252 | variant_map[var_key] = variant 253 | 254 | # Keep track of RCV Assertion IDs we encounter, we'll add later 255 | for record in cvvl['alleles'][int(allele)].get('records', []): 256 | rcv_acc, _ = record['acc'].split('.') 257 | rcv_map.setdefault( 258 | rcv_acc, set()).add(var_key) 259 | 260 | # Close VCF, save new variants to db bundled revisions of 10k or less. 261 | clinvar_vcf.close() 262 | logging.info('VCF closed. Now adding {} new variants to db.'.format( 263 | len(variants_new))) 264 | for i in range(1 + int(len(variants_new) / 10000)): 265 | variants_subset = variants_new[i * 10000: (i + 1) * 10000] 266 | if len(variants_subset) == 0: 267 | break 268 | logging.info('Adding {} through {} to db...'.format( 269 | 1 + i * 10000, i * 10000 + len(variants_subset))) 270 | self._save_as_revision( 271 | object_list=variants_subset, 272 | user=clinvar_user, 273 | comment='Variant added based on presence in ClinVar ' + 274 | 'VCF file: {}'.format(vcf_filename)) 275 | 276 | # print 'Multiple variants for single RCV Assertion ID:' 277 | # for k, v in rcv_map.iteritems(): 278 | # if len(v) > 1: 279 | # print '\t', k[0], v 280 | 281 | # Load ClinVar XML file. 282 | logging.info('Loading latest ClinVar XML...') 283 | if local_xml: 284 | cv_fp, xml_filename = local_xml, os.path.split(local_xml)[-1] 285 | else: 286 | cv_fp, xml_filename = self._download_latest_clinvar_xml(tempdir) 287 | logging.info('Loaded latest Clinvar XML, stored at {}'.format(cv_fp)) 288 | 289 | logging.info('Caching existing clinvar-rcva Relations by accession') 290 | rcv_hash_cache = { 291 | rel.tags['clinvar-rcva:accession']: 292 | (rel.id, self._hash_xml_dict(rel.tags)) for 293 | rel in Relation.objects.filter( 294 | **{'tags__type': RCVA_DATA['type'][1]()})} 295 | 296 | logging.info('Reading XML, parsing each ClinVarSet') 297 | clinvar_xml = self._open(cv_fp) 298 | 299 | for ele in self._get_elements(clinvar_xml, 'ClinVarSet'): 300 | # Retrieve Reference ClinVar Assertion (RCVA) data. 301 | # Each RCV Assertion is intended to represent a single phenotype, 302 | # and may contain multiple Submitter ClinVar Assertions. 303 | # Each Variant may have multiple associated RCVA accessions. 304 | # Discrepencies in phenotype labeling by SCVAs can lead to multiple 305 | # RCVAs which should theoretically be merged. 306 | rcva = ele.find('ReferenceClinVarAssertion') 307 | rcv_acc = rcva.find('ClinVarAccession').get('Acc') 308 | 309 | if rcv_acc not in rcv_map: 310 | # We do not have a record of this RCV from VCF, skip parsing... 311 | continue 312 | 313 | if len(rcv_map[rcv_acc]) != 1: 314 | # either no or too many variations for this RCV 315 | continue 316 | 317 | # Use the functions in RCVA_DATA to retrieve data for tags. 318 | val_store = dict() 319 | for rcva_key in RCVA_DATA: 320 | variable_type = RCVA_DATA[rcva_key][0] 321 | try: 322 | if not variable_type: 323 | value = RCVA_DATA[rcva_key][1]() 324 | elif variable_type == 'ele': 325 | value = RCVA_DATA[rcva_key][1](ele) 326 | elif variable_type == 'rcva': 327 | value = RCVA_DATA[rcva_key][1](rcva) 328 | except AttributeError: 329 | # Some retrieval functions are chained and the parent elem 330 | # isn't present. In that case, the data doesn't exist. 331 | value = None 332 | if value: 333 | val_store[rcva_key] = value 334 | 335 | # Get the hash of this data. 336 | xml_hash = self._hash_xml_dict(val_store) 337 | 338 | if rcv_acc not in rcv_hash_cache: 339 | # We got a brand new record 340 | rel = Relation(variant=variant_map[list(rcv_map[rcv_acc])[0]], 341 | tags=val_store) 342 | relations_new.append(rel) 343 | rcv_hash_cache[rcv_acc] = (rel.pk, xml_hash) 344 | 345 | # TODO: set HGVS tag in Variant 346 | 347 | # logging.info('Added new RCVA, accession: {}, {}'.format( 348 | # rcv_acc, str(val_store))) 349 | elif rcv_hash_cache[rcv_acc][1] != xml_hash: 350 | # XML parameters have changed, update required 351 | rel = Relation.objects.get(id=rcv_hash_cache[rcv_acc][0]) 352 | rel.tags.update(val_store) 353 | relations_updated.append(rel) 354 | # logging.info('Need to update accession {} with: {}'.format( 355 | # rcv_acc, str(val_store))) 356 | else: 357 | # nothing to do here, move along 358 | pass 359 | 360 | clinvar_xml.close() 361 | 362 | logging.info('ClinVar XML closed. Now adding {}'.format( 363 | str(len(relations_new))) + ' new clinvar-rcva Relations to db.') 364 | for i in range(1 + int(len(relations_new) / 10000)): 365 | relations_subset = relations_new[i * 10000: (i + 1) * 10000] 366 | if len(relations_subset) == 0: 367 | break 368 | logging.info('Adding {} through {} to db...'.format( 369 | 1 + i * 10000, i * 10000 + len(relations_subset))) 370 | self._save_as_revision( 371 | object_list=relations_subset, 372 | user=clinvar_user, 373 | comment='Relation added based on presence in ClinVar ' + 374 | 'XML file: {}'.format(xml_filename)) 375 | 376 | logging.info('Updating {} clinvar-rcva Relations in db.'.format( 377 | str(len(relations_updated)))) 378 | for i in range(1 + int(len(relations_updated) / 10000)): 379 | relations_subset = relations_updated[i * 10000: (i + 1) * 10000] 380 | if len(relations_subset) == 0: 381 | break 382 | logging.info('Adding {} through {} to db...'.format( 383 | 1 + i * 10000, i * 10000 + len(relations_subset))) 384 | self._save_as_revision( 385 | object_list=relations_subset, 386 | user=clinvar_user, 387 | comment='Relation updated based on updated data detected in ' + 388 | 'ClinVar XML file: {}'.format(xml_filename)) 389 | 390 | if not (local_vcf and local_xml): 391 | shutil.rmtree(tempdir) 392 | logging.info('Removed tempdir {}'.format(tempdir)) 393 | -------------------------------------------------------------------------------- /gennotes_server/fixtures/test-data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "contenttypes.contenttype", 4 | "fields": { 5 | "app_label": "admin", 6 | "model": "logentry" 7 | } 8 | }, 9 | { 10 | "model": "contenttypes.contenttype", 11 | "fields": { 12 | "app_label": "auth", 13 | "model": "permission" 14 | } 15 | }, 16 | { 17 | "model": "contenttypes.contenttype", 18 | "fields": { 19 | "app_label": "auth", 20 | "model": "group" 21 | } 22 | }, 23 | { 24 | "model": "contenttypes.contenttype", 25 | "fields": { 26 | "app_label": "auth", 27 | "model": "user" 28 | } 29 | }, 30 | { 31 | "model": "contenttypes.contenttype", 32 | "fields": { 33 | "app_label": "contenttypes", 34 | "model": "contenttype" 35 | } 36 | }, 37 | { 38 | "model": "contenttypes.contenttype", 39 | "fields": { 40 | "app_label": "sessions", 41 | "model": "session" 42 | } 43 | }, 44 | { 45 | "model": "contenttypes.contenttype", 46 | "fields": { 47 | "app_label": "sites", 48 | "model": "site" 49 | } 50 | }, 51 | { 52 | "model": "contenttypes.contenttype", 53 | "fields": { 54 | "app_label": "gennotes_server", 55 | "model": "variant" 56 | } 57 | }, 58 | { 59 | "model": "contenttypes.contenttype", 60 | "fields": { 61 | "app_label": "gennotes_server", 62 | "model": "relation" 63 | } 64 | }, 65 | { 66 | "model": "contenttypes.contenttype", 67 | "fields": { 68 | "app_label": "gennotes_server", 69 | "model": "commitdeletion" 70 | } 71 | }, 72 | { 73 | "model": "contenttypes.contenttype", 74 | "fields": { 75 | "app_label": "gennotes_server", 76 | "model": "editingapplication" 77 | } 78 | }, 79 | { 80 | "model": "contenttypes.contenttype", 81 | "fields": { 82 | "app_label": "account", 83 | "model": "emailaddress" 84 | } 85 | }, 86 | { 87 | "model": "contenttypes.contenttype", 88 | "fields": { 89 | "app_label": "account", 90 | "model": "emailconfirmation" 91 | } 92 | }, 93 | { 94 | "model": "contenttypes.contenttype", 95 | "fields": { 96 | "app_label": "corsheaders", 97 | "model": "corsmodel" 98 | } 99 | }, 100 | { 101 | "model": "contenttypes.contenttype", 102 | "fields": { 103 | "app_label": "oauth2_provider", 104 | "model": "grant" 105 | } 106 | }, 107 | { 108 | "model": "contenttypes.contenttype", 109 | "fields": { 110 | "app_label": "oauth2_provider", 111 | "model": "accesstoken" 112 | } 113 | }, 114 | { 115 | "model": "contenttypes.contenttype", 116 | "fields": { 117 | "app_label": "oauth2_provider", 118 | "model": "refreshtoken" 119 | } 120 | }, 121 | { 122 | "model": "contenttypes.contenttype", 123 | "fields": { 124 | "app_label": "reversion", 125 | "model": "revision" 126 | } 127 | }, 128 | { 129 | "model": "contenttypes.contenttype", 130 | "fields": { 131 | "app_label": "reversion", 132 | "model": "version" 133 | } 134 | }, 135 | { 136 | "model": "sessions.session", 137 | "pk": "evnty9e24hvj74yvanhgetaes4hre19h", 138 | "fields": { 139 | "session_data": "NDc1NTRkM2JmY2MxY2U0ZDM5Y2M0NTg0NGQ0MjM1ODQzZmY2ZGI5MDp7fQ==", 140 | "expire_date": "2015-07-18T20:53:27.085Z" 141 | } 142 | }, 143 | { 144 | "model": "sessions.session", 145 | "pk": "r13jvm8ize1bx44l9i3nkfqtb7i3ttnu", 146 | "fields": { 147 | "session_data": "OWEzMGY5OGJmZDI1OGFlYzBjMWQ3ZGZiZWE3MWIwMmE4ZDAyMmQ5ZTp7Il9hdXRoX3VzZXJfaGFzaCI6IjYzMzhmZDQ3YzM4ZjVlZTlkMTY3YjY3ZTIxZDFkYjgzMmE3NDdiNjQiLCJfYXV0aF91c2VyX2JhY2tlbmQiOiJhbGxhdXRoLmFjY291bnQuYXV0aF9iYWNrZW5kcy5BdXRoZW50aWNhdGlvbkJhY2tlbmQiLCJfYXV0aF91c2VyX2lkIjoiMiIsImFjY291bnRfdmVyaWZpZWRfZW1haWwiOm51bGx9", 148 | "expire_date": "2015-07-18T23:54:38.559Z" 149 | } 150 | }, 151 | { 152 | "model": "sites.site", 153 | "pk": 1, 154 | "fields": { 155 | "domain": "example.com", 156 | "name": "example.com" 157 | } 158 | }, 159 | { 160 | "model": "gennotes_server.variant", 161 | "pk": 1, 162 | "fields": { 163 | "tags": "{\"pos_b37\": \"883516\", \"var_allele_b37\": \"A\", \"ref_allele_b37\": \"G\", \"chrom_b37\": \"1\"}" 164 | } 165 | }, 166 | { 167 | "model": "gennotes_server.variant", 168 | "pk": 2, 169 | "fields": { 170 | "tags": "{\"pos_b37\": \"891344\", \"var_allele_b37\": \"A\", \"ref_allele_b37\": \"G\", \"chrom_b37\": \"1\"}" 171 | } 172 | }, 173 | { 174 | "model": "gennotes_server.variant", 175 | "pk": 3, 176 | "fields": { 177 | "tags": "{\"pos_b37\": \"906168\", \"var_allele_b37\": \"A\", \"ref_allele_b37\": \"G\", \"chrom_b37\": \"1\"}" 178 | } 179 | }, 180 | { 181 | "model": "gennotes_server.variant", 182 | "pk": 4, 183 | "fields": { 184 | "tags": "{\"pos_b37\": \"949523\", \"var_allele_b37\": \"T\", \"ref_allele_b37\": \"C\", \"chrom_b37\": \"1\"}" 185 | } 186 | }, 187 | { 188 | "model": "gennotes_server.variant", 189 | "pk": 5, 190 | "fields": { 191 | "tags": "{\"pos_b37\": \"949696\", \"var_allele_b37\": \"CG\", \"ref_allele_b37\": \"C\", \"chrom_b37\": \"1\"}" 192 | } 193 | }, 194 | { 195 | "model": "gennotes_server.variant", 196 | "pk": 6, 197 | "fields": { 198 | "tags": "{\"pos_b37\": \"949739\", \"var_allele_b37\": \"T\", \"ref_allele_b37\": \"G\", \"chrom_b37\": \"1\"}" 199 | } 200 | }, 201 | { 202 | "model": "gennotes_server.variant", 203 | "pk": 7, 204 | "fields": { 205 | "tags": "{\"pos_b37\": \"955597\", \"var_allele_b37\": \"T\", \"ref_allele_b37\": \"G\", \"chrom_b37\": \"1\"}" 206 | } 207 | }, 208 | { 209 | "model": "gennotes_server.variant", 210 | "pk": 8, 211 | "fields": { 212 | "tags": "{\"pos_b37\": \"957640\", \"var_allele_b37\": \"T\", \"ref_allele_b37\": \"C\", \"chrom_b37\": \"1\"}" 213 | } 214 | }, 215 | { 216 | "model": "gennotes_server.variant", 217 | "pk": 9, 218 | "fields": { 219 | "tags": "{\"pos_b37\": \"976629\", \"var_allele_b37\": \"T\", \"ref_allele_b37\": \"C\", \"chrom_b37\": \"1\"}" 220 | } 221 | }, 222 | { 223 | "model": "gennotes_server.variant", 224 | "pk": 10, 225 | "fields": { 226 | "tags": "{\"pos_b37\": \"976963\", \"var_allele_b37\": \"G\", \"ref_allele_b37\": \"A\", \"chrom_b37\": \"1\"}" 227 | } 228 | }, 229 | { 230 | "model": "gennotes_server.relation", 231 | "pk": 1, 232 | "fields": { 233 | "variant": 10, 234 | "tags": { 235 | "clinvar-rcva:record-status": "current", 236 | "clinvar-rcva:num-submissions": "1", 237 | "clinvar-rcva:trait-type": "Disease", 238 | "clinvar-rcva:preferred-name": "NM_198576.3(AGRN):c.1058A>G (p.Gln353Arg)", 239 | "clinvar-rcva:significance": "Likely benign", 240 | "clinvar-rcva:version": "2", 241 | "clinvar-rcva:gene-name": "agrin", 242 | "clinvar-rcva:accession": "RCV000116253", 243 | "clinvar-rcva:gene-symbol": "AGRN", 244 | "clinvar-rcva:esp-allele-frequency": "0.014089016971", 245 | "clinvar-rcva:trait-name": "not specified", 246 | "type": "clinvar-rcva" 247 | } 248 | } 249 | }, 250 | { 251 | "model": "gennotes_server.relation", 252 | "pk": 2, 253 | "fields": { 254 | "variant": 7, 255 | "tags": { 256 | "clinvar-rcva:record-status": "current", 257 | "clinvar-rcva:num-submissions": "1", 258 | "clinvar-rcva:trait-type": "Disease", 259 | "clinvar-rcva:preferred-name": "NM_198576.3(AGRN):c.45G>T (p.Pro15=)", 260 | "clinvar-rcva:significance": "Likely benign", 261 | "clinvar-rcva:version": "2", 262 | "clinvar-rcva:gene-name": "agrin", 263 | "clinvar-rcva:accession": "RCV000116272", 264 | "clinvar-rcva:gene-symbol": "AGRN", 265 | "clinvar-rcva:trait-name": "not specified", 266 | "type": "clinvar-rcva" 267 | } 268 | } 269 | }, 270 | { 271 | "model": "gennotes_server.relation", 272 | "pk": 3, 273 | "fields": { 274 | "variant": 8, 275 | "tags": { 276 | "clinvar-rcva:record-status": "current", 277 | "clinvar-rcva:num-submissions": "1", 278 | "clinvar-rcva:trait-type": "Disease", 279 | "clinvar-rcva:preferred-name": "NM_198576.3(AGRN):c.261C>T (p.Asp87=)", 280 | "clinvar-rcva:significance": "Likely benign", 281 | "clinvar-rcva:version": "2", 282 | "clinvar-rcva:gene-name": "agrin", 283 | "clinvar-rcva:accession": "RCV000116258", 284 | "clinvar-rcva:gene-symbol": "AGRN", 285 | "clinvar-rcva:esp-allele-frequency": "0.031754574812", 286 | "clinvar-rcva:trait-name": "not specified", 287 | "type": "clinvar-rcva" 288 | } 289 | } 290 | }, 291 | { 292 | "model": "gennotes_server.relation", 293 | "pk": 4, 294 | "fields": { 295 | "variant": 9, 296 | "tags": { 297 | "clinvar-rcva:record-status": "current", 298 | "clinvar-rcva:num-submissions": "1", 299 | "clinvar-rcva:trait-type": "Disease", 300 | "clinvar-rcva:preferred-name": "NM_198576.3(AGRN):c.804C>T (p.Ala268=)", 301 | "clinvar-rcva:significance": "Benign", 302 | "clinvar-rcva:version": "1", 303 | "clinvar-rcva:gene-name": "agrin", 304 | "clinvar-rcva:accession": "RCV000116282", 305 | "clinvar-rcva:gene-symbol": "AGRN", 306 | "clinvar-rcva:trait-name": "not specified", 307 | "type": "clinvar-rcva" 308 | } 309 | } 310 | }, 311 | { 312 | "model": "gennotes_server.relation", 313 | "pk": 5, 314 | "fields": { 315 | "variant": 6, 316 | "tags": { 317 | "clinvar-rcva:record-status": "current", 318 | "clinvar-rcva:num-submissions": "1", 319 | "clinvar-rcva:trait-type": "Disease", 320 | "clinvar-rcva:preferred-name": "NM_005101.3(ISG15):c.379G>T (p.Glu127Ter)", 321 | "clinvar-rcva:significance": "Pathogenic", 322 | "clinvar-rcva:version": "4", 323 | "clinvar-rcva:gene-name": "ISG15 ubiquitin-like modifier", 324 | "clinvar-rcva:accession": "RCV000148988", 325 | "clinvar-rcva:gene-symbol": "ISG15", 326 | "clinvar-rcva:trait-name": "Immunodeficiency 38", 327 | "type": "clinvar-rcva" 328 | } 329 | } 330 | }, 331 | { 332 | "model": "gennotes_server.relation", 333 | "pk": 6, 334 | "fields": { 335 | "variant": 5, 336 | "tags": { 337 | "clinvar-rcva:record-status": "current", 338 | "clinvar-rcva:num-submissions": "1", 339 | "clinvar-rcva:trait-type": "Disease", 340 | "clinvar-rcva:preferred-name": "NM_005101.3(ISG15):c.339dupG (p.Leu114Alafs)", 341 | "clinvar-rcva:significance": "Pathogenic", 342 | "clinvar-rcva:citations": "PMID22859821", 343 | "clinvar-rcva:version": "4", 344 | "clinvar-rcva:gene-name": "ISG15 ubiquitin-like modifier", 345 | "clinvar-rcva:accession": "RCV000148989", 346 | "clinvar-rcva:gene-symbol": "ISG15", 347 | "clinvar-rcva:trait-name": "Immunodeficiency 38", 348 | "type": "clinvar-rcva" 349 | } 350 | } 351 | }, 352 | { 353 | "model": "gennotes_server.relation", 354 | "pk": 7, 355 | "fields": { 356 | "variant": 4, 357 | "tags": { 358 | "clinvar-rcva:record-status": "current", 359 | "clinvar-rcva:num-submissions": "1", 360 | "clinvar-rcva:trait-type": "Disease", 361 | "clinvar-rcva:preferred-name": "NM_005101.3(ISG15):c.163C>T (p.Gln55Ter)", 362 | "clinvar-rcva:significance": "Pathogenic", 363 | "clinvar-rcva:version": "2", 364 | "clinvar-rcva:gene-name": "ISG15 ubiquitin-like modifier", 365 | "clinvar-rcva:accession": "RCV000162196", 366 | "clinvar-rcva:gene-symbol": "ISG15", 367 | "clinvar-rcva:trait-name": "Immunodeficiency 38", 368 | "type": "clinvar-rcva" 369 | } 370 | } 371 | }, 372 | { 373 | "model": "gennotes_server.relation", 374 | "pk": 8, 375 | "fields": { 376 | "variant": 1, 377 | "tags": { 378 | "clinvar-rcva:record-status": "current", 379 | "clinvar-rcva:num-submissions": "1", 380 | "clinvar-rcva:trait-type": "Disease", 381 | "clinvar-rcva:preferred-name": "NM_015658.3(NOC2L):c.1654C>T (p.Leu552=)", 382 | "clinvar-rcva:significance": "not provided", 383 | "clinvar-rcva:version": "2", 384 | "clinvar-rcva:gene-name": "nucleolar complex associated 2 homolog (S. cerevisiae)", 385 | "clinvar-rcva:accession": "RCV000064926", 386 | "clinvar-rcva:gene-symbol": "NOC2L", 387 | "clinvar-rcva:trait-name": "Malignant melanoma", 388 | "type": "clinvar-rcva" 389 | } 390 | } 391 | }, 392 | { 393 | "model": "gennotes_server.relation", 394 | "pk": 9, 395 | "fields": { 396 | "variant": 2, 397 | "tags": { 398 | "clinvar-rcva:record-status": "current", 399 | "clinvar-rcva:num-submissions": "1", 400 | "clinvar-rcva:trait-type": "Disease", 401 | "clinvar-rcva:preferred-name": "NM_015658.3(NOC2L):c.657C>T (p.Leu219=)", 402 | "clinvar-rcva:significance": "not provided", 403 | "clinvar-rcva:version": "2", 404 | "clinvar-rcva:gene-name": "nucleolar complex associated 2 homolog (S. cerevisiae)", 405 | "clinvar-rcva:accession": "RCV000064927", 406 | "clinvar-rcva:gene-symbol": "NOC2L", 407 | "clinvar-rcva:trait-name": "Malignant melanoma", 408 | "type": "clinvar-rcva" 409 | } 410 | } 411 | }, 412 | { 413 | "model": "gennotes_server.relation", 414 | "pk": 10, 415 | "fields": { 416 | "variant": 3, 417 | "tags": { 418 | "clinvar-rcva:record-status": "current", 419 | "clinvar-rcva:num-submissions": "1", 420 | "clinvar-rcva:trait-type": "Disease", 421 | "clinvar-rcva:preferred-name": "NM_001160184.1(PLEKHN1):c.484+30G>A", 422 | "clinvar-rcva:significance": "not provided", 423 | "clinvar-rcva:version": "2", 424 | "clinvar-rcva:gene-name": "pleckstrin homology domain containing, family N member 1", 425 | "clinvar-rcva:accession": "RCV000064940", 426 | "clinvar-rcva:gene-symbol": "PLEKHN1", 427 | "clinvar-rcva:trait-name": "Malignant melanoma", 428 | "type": "clinvar-rcva" 429 | } 430 | } 431 | }, 432 | { 433 | "model": "account.emailconfirmation", 434 | "pk": 1, 435 | "fields": { 436 | "email_address": 1, 437 | "created": "2015-07-04T23:54:24.325Z", 438 | "sent": "2015-07-04T23:54:24.342Z", 439 | "key": "da2dgg2qz00ftqnmo9pnl4ucqfeqfjw7qlolwddaphxxaisosxmpxz9yoiqbk3l1" 440 | } 441 | }, 442 | { 443 | "model": "reversion.version", 444 | "pk": 1, 445 | "fields": { 446 | "revision": 1, 447 | "object_id": "1", 448 | "object_id_int": 1, 449 | "content_type": [ 450 | "gennotes_server", 451 | "variant" 452 | ], 453 | "format": "json", 454 | "serialized_data": "[{\"fields\": {\"tags\": \"{\\\"pos_b37\\\": \\\"883516\\\", \\\"var_allele_b37\\\": \\\"A\\\", \\\"ref_allele_b37\\\": \\\"G\\\", \\\"chrom_b37\\\": \\\"1\\\"}\"}, \"model\": \"gennotes_server.variant\", \"pk\": 1}]", 455 | "object_repr": "pos_b37=883516; var_allele_b37=A; ref_allele_b37=G; chrom_b37=1" 456 | } 457 | }, 458 | { 459 | "model": "reversion.version", 460 | "pk": 2, 461 | "fields": { 462 | "revision": 1, 463 | "object_id": "2", 464 | "object_id_int": 2, 465 | "content_type": [ 466 | "gennotes_server", 467 | "variant" 468 | ], 469 | "format": "json", 470 | "serialized_data": "[{\"fields\": {\"tags\": \"{\\\"pos_b37\\\": \\\"891344\\\", \\\"var_allele_b37\\\": \\\"A\\\", \\\"ref_allele_b37\\\": \\\"G\\\", \\\"chrom_b37\\\": \\\"1\\\"}\"}, \"model\": \"gennotes_server.variant\", \"pk\": 2}]", 471 | "object_repr": "pos_b37=891344; var_allele_b37=A; ref_allele_b37=G; chrom_b37=1" 472 | } 473 | }, 474 | { 475 | "model": "reversion.version", 476 | "pk": 3, 477 | "fields": { 478 | "revision": 1, 479 | "object_id": "3", 480 | "object_id_int": 3, 481 | "content_type": [ 482 | "gennotes_server", 483 | "variant" 484 | ], 485 | "format": "json", 486 | "serialized_data": "[{\"fields\": {\"tags\": \"{\\\"pos_b37\\\": \\\"906168\\\", \\\"var_allele_b37\\\": \\\"A\\\", \\\"ref_allele_b37\\\": \\\"G\\\", \\\"chrom_b37\\\": \\\"1\\\"}\"}, \"model\": \"gennotes_server.variant\", \"pk\": 3}]", 487 | "object_repr": "pos_b37=906168; var_allele_b37=A; ref_allele_b37=G; chrom_b37=1" 488 | } 489 | }, 490 | { 491 | "model": "reversion.version", 492 | "pk": 4, 493 | "fields": { 494 | "revision": 1, 495 | "object_id": "4", 496 | "object_id_int": 4, 497 | "content_type": [ 498 | "gennotes_server", 499 | "variant" 500 | ], 501 | "format": "json", 502 | "serialized_data": "[{\"fields\": {\"tags\": \"{\\\"pos_b37\\\": \\\"949523\\\", \\\"var_allele_b37\\\": \\\"T\\\", \\\"ref_allele_b37\\\": \\\"C\\\", \\\"chrom_b37\\\": \\\"1\\\"}\"}, \"model\": \"gennotes_server.variant\", \"pk\": 4}]", 503 | "object_repr": "pos_b37=949523; var_allele_b37=T; ref_allele_b37=C; chrom_b37=1" 504 | } 505 | }, 506 | { 507 | "model": "reversion.version", 508 | "pk": 5, 509 | "fields": { 510 | "revision": 1, 511 | "object_id": "5", 512 | "object_id_int": 5, 513 | "content_type": [ 514 | "gennotes_server", 515 | "variant" 516 | ], 517 | "format": "json", 518 | "serialized_data": "[{\"fields\": {\"tags\": \"{\\\"pos_b37\\\": \\\"949696\\\", \\\"var_allele_b37\\\": \\\"CG\\\", \\\"ref_allele_b37\\\": \\\"C\\\", \\\"chrom_b37\\\": \\\"1\\\"}\"}, \"model\": \"gennotes_server.variant\", \"pk\": 5}]", 519 | "object_repr": "pos_b37=949696; var_allele_b37=CG; ref_allele_b37=C; chrom_b37=1" 520 | } 521 | }, 522 | { 523 | "model": "reversion.version", 524 | "pk": 6, 525 | "fields": { 526 | "revision": 1, 527 | "object_id": "6", 528 | "object_id_int": 6, 529 | "content_type": [ 530 | "gennotes_server", 531 | "variant" 532 | ], 533 | "format": "json", 534 | "serialized_data": "[{\"fields\": {\"tags\": \"{\\\"pos_b37\\\": \\\"949739\\\", \\\"var_allele_b37\\\": \\\"T\\\", \\\"ref_allele_b37\\\": \\\"G\\\", \\\"chrom_b37\\\": \\\"1\\\"}\"}, \"model\": \"gennotes_server.variant\", \"pk\": 6}]", 535 | "object_repr": "pos_b37=949739; var_allele_b37=T; ref_allele_b37=G; chrom_b37=1" 536 | } 537 | }, 538 | { 539 | "model": "reversion.version", 540 | "pk": 7, 541 | "fields": { 542 | "revision": 1, 543 | "object_id": "7", 544 | "object_id_int": 7, 545 | "content_type": [ 546 | "gennotes_server", 547 | "variant" 548 | ], 549 | "format": "json", 550 | "serialized_data": "[{\"fields\": {\"tags\": \"{\\\"pos_b37\\\": \\\"955597\\\", \\\"var_allele_b37\\\": \\\"T\\\", \\\"ref_allele_b37\\\": \\\"G\\\", \\\"chrom_b37\\\": \\\"1\\\"}\"}, \"model\": \"gennotes_server.variant\", \"pk\": 7}]", 551 | "object_repr": "pos_b37=955597; var_allele_b37=T; ref_allele_b37=G; chrom_b37=1" 552 | } 553 | }, 554 | { 555 | "model": "reversion.version", 556 | "pk": 8, 557 | "fields": { 558 | "revision": 1, 559 | "object_id": "8", 560 | "object_id_int": 8, 561 | "content_type": [ 562 | "gennotes_server", 563 | "variant" 564 | ], 565 | "format": "json", 566 | "serialized_data": "[{\"fields\": {\"tags\": \"{\\\"pos_b37\\\": \\\"957640\\\", \\\"var_allele_b37\\\": \\\"T\\\", \\\"ref_allele_b37\\\": \\\"C\\\", \\\"chrom_b37\\\": \\\"1\\\"}\"}, \"model\": \"gennotes_server.variant\", \"pk\": 8}]", 567 | "object_repr": "pos_b37=957640; var_allele_b37=T; ref_allele_b37=C; chrom_b37=1" 568 | } 569 | }, 570 | { 571 | "model": "reversion.version", 572 | "pk": 9, 573 | "fields": { 574 | "revision": 1, 575 | "object_id": "9", 576 | "object_id_int": 9, 577 | "content_type": [ 578 | "gennotes_server", 579 | "variant" 580 | ], 581 | "format": "json", 582 | "serialized_data": "[{\"fields\": {\"tags\": \"{\\\"pos_b37\\\": \\\"976629\\\", \\\"var_allele_b37\\\": \\\"T\\\", \\\"ref_allele_b37\\\": \\\"C\\\", \\\"chrom_b37\\\": \\\"1\\\"}\"}, \"model\": \"gennotes_server.variant\", \"pk\": 9}]", 583 | "object_repr": "pos_b37=976629; var_allele_b37=T; ref_allele_b37=C; chrom_b37=1" 584 | } 585 | }, 586 | { 587 | "model": "reversion.version", 588 | "pk": 10, 589 | "fields": { 590 | "revision": 1, 591 | "object_id": "10", 592 | "object_id_int": 10, 593 | "content_type": [ 594 | "gennotes_server", 595 | "variant" 596 | ], 597 | "format": "json", 598 | "serialized_data": "[{\"fields\": {\"tags\": \"{\\\"pos_b37\\\": \\\"976963\\\", \\\"var_allele_b37\\\": \\\"G\\\", \\\"ref_allele_b37\\\": \\\"A\\\", \\\"chrom_b37\\\": \\\"1\\\"}\"}, \"model\": \"gennotes_server.variant\", \"pk\": 10}]", 599 | "object_repr": "pos_b37=976963; var_allele_b37=G; ref_allele_b37=A; chrom_b37=1" 600 | } 601 | }, 602 | { 603 | "model": "reversion.version", 604 | "pk": 11, 605 | "fields": { 606 | "revision": 2, 607 | "object_id": "1", 608 | "object_id_int": 1, 609 | "content_type": [ 610 | "gennotes_server", 611 | "relation" 612 | ], 613 | "format": "json", 614 | "serialized_data": "[{\"fields\": {\"variant\": 10, \"tags\": \"{\\\"clinvar-rcva:record-status\\\": \\\"current\\\", \\\"clinvar-rcva:num-submissions\\\": \\\"1\\\", \\\"clinvar-rcva:trait-type\\\": \\\"Disease\\\", \\\"clinvar-rcva:significance\\\": \\\"Likely benign\\\", \\\"clinvar-rcva:preferred-name\\\": \\\"NM_198576.3(AGRN):c.1058A>G (p.Gln353Arg)\\\", \\\"clinvar-rcva:version\\\": \\\"2\\\", \\\"clinvar-rcva:gene-name\\\": \\\"agrin\\\", \\\"clinvar-rcva:accession\\\": \\\"RCV000116253\\\", \\\"clinvar-rcva:gene-symbol\\\": \\\"AGRN\\\", \\\"clinvar-rcva:esp-allele-frequency\\\": \\\"0.014089016971\\\", \\\"clinvar-rcva:trait-name\\\": \\\"not specified\\\", \\\"type\\\": \\\"clinvar-rcva\\\"}\"}, \"model\": \"gennotes_server.relation\", \"pk\": 1}]", 615 | "object_repr": "Relation: 1, Type: clinvar-rcva" 616 | } 617 | }, 618 | { 619 | "model": "reversion.version", 620 | "pk": 12, 621 | "fields": { 622 | "revision": 2, 623 | "object_id": "2", 624 | "object_id_int": 2, 625 | "content_type": [ 626 | "gennotes_server", 627 | "relation" 628 | ], 629 | "format": "json", 630 | "serialized_data": "[{\"fields\": {\"variant\": 7, \"tags\": \"{\\\"clinvar-rcva:record-status\\\": \\\"current\\\", \\\"clinvar-rcva:num-submissions\\\": \\\"1\\\", \\\"clinvar-rcva:trait-type\\\": \\\"Disease\\\", \\\"clinvar-rcva:significance\\\": \\\"Likely benign\\\", \\\"clinvar-rcva:preferred-name\\\": \\\"NM_198576.3(AGRN):c.45G>T (p.Pro15=)\\\", \\\"clinvar-rcva:version\\\": \\\"2\\\", \\\"clinvar-rcva:gene-name\\\": \\\"agrin\\\", \\\"clinvar-rcva:accession\\\": \\\"RCV000116272\\\", \\\"clinvar-rcva:gene-symbol\\\": \\\"AGRN\\\", \\\"clinvar-rcva:trait-name\\\": \\\"not specified\\\", \\\"type\\\": \\\"clinvar-rcva\\\"}\"}, \"model\": \"gennotes_server.relation\", \"pk\": 2}]", 631 | "object_repr": "Relation: 2, Type: clinvar-rcva" 632 | } 633 | }, 634 | { 635 | "model": "reversion.version", 636 | "pk": 13, 637 | "fields": { 638 | "revision": 2, 639 | "object_id": "3", 640 | "object_id_int": 3, 641 | "content_type": [ 642 | "gennotes_server", 643 | "relation" 644 | ], 645 | "format": "json", 646 | "serialized_data": "[{\"fields\": {\"variant\": 8, \"tags\": \"{\\\"clinvar-rcva:record-status\\\": \\\"current\\\", \\\"clinvar-rcva:num-submissions\\\": \\\"1\\\", \\\"clinvar-rcva:trait-type\\\": \\\"Disease\\\", \\\"clinvar-rcva:significance\\\": \\\"Likely benign\\\", \\\"clinvar-rcva:preferred-name\\\": \\\"NM_198576.3(AGRN):c.261C>T (p.Asp87=)\\\", \\\"clinvar-rcva:version\\\": \\\"2\\\", \\\"clinvar-rcva:gene-name\\\": \\\"agrin\\\", \\\"clinvar-rcva:accession\\\": \\\"RCV000116258\\\", \\\"clinvar-rcva:gene-symbol\\\": \\\"AGRN\\\", \\\"clinvar-rcva:esp-allele-frequency\\\": \\\"0.031754574812\\\", \\\"clinvar-rcva:trait-name\\\": \\\"not specified\\\", \\\"type\\\": \\\"clinvar-rcva\\\"}\"}, \"model\": \"gennotes_server.relation\", \"pk\": 3}]", 647 | "object_repr": "Relation: 3, Type: clinvar-rcva" 648 | } 649 | }, 650 | { 651 | "model": "reversion.version", 652 | "pk": 14, 653 | "fields": { 654 | "revision": 2, 655 | "object_id": "4", 656 | "object_id_int": 4, 657 | "content_type": [ 658 | "gennotes_server", 659 | "relation" 660 | ], 661 | "format": "json", 662 | "serialized_data": "[{\"fields\": {\"variant\": 9, \"tags\": \"{\\\"clinvar-rcva:record-status\\\": \\\"current\\\", \\\"clinvar-rcva:num-submissions\\\": \\\"1\\\", \\\"clinvar-rcva:trait-type\\\": \\\"Disease\\\", \\\"clinvar-rcva:significance\\\": \\\"Benign\\\", \\\"clinvar-rcva:preferred-name\\\": \\\"NM_198576.3(AGRN):c.804C>T (p.Ala268=)\\\", \\\"clinvar-rcva:version\\\": \\\"1\\\", \\\"clinvar-rcva:gene-name\\\": \\\"agrin\\\", \\\"clinvar-rcva:accession\\\": \\\"RCV000116282\\\", \\\"clinvar-rcva:gene-symbol\\\": \\\"AGRN\\\", \\\"clinvar-rcva:trait-name\\\": \\\"not specified\\\", \\\"type\\\": \\\"clinvar-rcva\\\"}\"}, \"model\": \"gennotes_server.relation\", \"pk\": 4}]", 663 | "object_repr": "Relation: 4, Type: clinvar-rcva" 664 | } 665 | }, 666 | { 667 | "model": "reversion.version", 668 | "pk": 15, 669 | "fields": { 670 | "revision": 2, 671 | "object_id": "5", 672 | "object_id_int": 5, 673 | "content_type": [ 674 | "gennotes_server", 675 | "relation" 676 | ], 677 | "format": "json", 678 | "serialized_data": "[{\"fields\": {\"variant\": 6, \"tags\": \"{\\\"clinvar-rcva:record-status\\\": \\\"current\\\", \\\"clinvar-rcva:num-submissions\\\": \\\"1\\\", \\\"clinvar-rcva:trait-type\\\": \\\"Disease\\\", \\\"clinvar-rcva:significance\\\": \\\"Pathogenic\\\", \\\"clinvar-rcva:preferred-name\\\": \\\"NM_005101.3(ISG15):c.379G>T (p.Glu127Ter)\\\", \\\"clinvar-rcva:version\\\": \\\"4\\\", \\\"clinvar-rcva:gene-name\\\": \\\"ISG15 ubiquitin-like modifier\\\", \\\"clinvar-rcva:accession\\\": \\\"RCV000148988\\\", \\\"clinvar-rcva:gene-symbol\\\": \\\"ISG15\\\", \\\"clinvar-rcva:trait-name\\\": \\\"Immunodeficiency 38\\\", \\\"type\\\": \\\"clinvar-rcva\\\"}\"}, \"model\": \"gennotes_server.relation\", \"pk\": 5}]", 679 | "object_repr": "Relation: 5, Type: clinvar-rcva" 680 | } 681 | }, 682 | { 683 | "model": "reversion.version", 684 | "pk": 16, 685 | "fields": { 686 | "revision": 2, 687 | "object_id": "6", 688 | "object_id_int": 6, 689 | "content_type": [ 690 | "gennotes_server", 691 | "relation" 692 | ], 693 | "format": "json", 694 | "serialized_data": "[{\"fields\": {\"variant\": 5, \"tags\": \"{\\\"clinvar-rcva:record-status\\\": \\\"current\\\", \\\"clinvar-rcva:num-submissions\\\": \\\"1\\\", \\\"clinvar-rcva:trait-type\\\": \\\"Disease\\\", \\\"clinvar-rcva:significance\\\": \\\"Pathogenic\\\", \\\"clinvar-rcva:preferred-name\\\": \\\"NM_005101.3(ISG15):c.339dupG (p.Leu114Alafs)\\\", \\\"clinvar-rcva:citations\\\": \\\"PMID22859821\\\", \\\"clinvar-rcva:version\\\": \\\"4\\\", \\\"clinvar-rcva:gene-name\\\": \\\"ISG15 ubiquitin-like modifier\\\", \\\"clinvar-rcva:accession\\\": \\\"RCV000148989\\\", \\\"clinvar-rcva:gene-symbol\\\": \\\"ISG15\\\", \\\"clinvar-rcva:trait-name\\\": \\\"Immunodeficiency 38\\\", \\\"type\\\": \\\"clinvar-rcva\\\"}\"}, \"model\": \"gennotes_server.relation\", \"pk\": 6}]", 695 | "object_repr": "Relation: 6, Type: clinvar-rcva" 696 | } 697 | }, 698 | { 699 | "model": "reversion.version", 700 | "pk": 17, 701 | "fields": { 702 | "revision": 2, 703 | "object_id": "7", 704 | "object_id_int": 7, 705 | "content_type": [ 706 | "gennotes_server", 707 | "relation" 708 | ], 709 | "format": "json", 710 | "serialized_data": "[{\"fields\": {\"variant\": 4, \"tags\": \"{\\\"clinvar-rcva:record-status\\\": \\\"current\\\", \\\"clinvar-rcva:num-submissions\\\": \\\"1\\\", \\\"clinvar-rcva:trait-type\\\": \\\"Disease\\\", \\\"clinvar-rcva:significance\\\": \\\"Pathogenic\\\", \\\"clinvar-rcva:preferred-name\\\": \\\"NM_005101.3(ISG15):c.163C>T (p.Gln55Ter)\\\", \\\"clinvar-rcva:version\\\": \\\"2\\\", \\\"clinvar-rcva:gene-name\\\": \\\"ISG15 ubiquitin-like modifier\\\", \\\"clinvar-rcva:accession\\\": \\\"RCV000162196\\\", \\\"clinvar-rcva:gene-symbol\\\": \\\"ISG15\\\", \\\"clinvar-rcva:trait-name\\\": \\\"Immunodeficiency 38\\\", \\\"type\\\": \\\"clinvar-rcva\\\"}\"}, \"model\": \"gennotes_server.relation\", \"pk\": 7}]", 711 | "object_repr": "Relation: 7, Type: clinvar-rcva" 712 | } 713 | }, 714 | { 715 | "model": "reversion.version", 716 | "pk": 18, 717 | "fields": { 718 | "revision": 2, 719 | "object_id": "8", 720 | "object_id_int": 8, 721 | "content_type": [ 722 | "gennotes_server", 723 | "relation" 724 | ], 725 | "format": "json", 726 | "serialized_data": "[{\"fields\": {\"variant\": 1, \"tags\": \"{\\\"clinvar-rcva:record-status\\\": \\\"current\\\", \\\"clinvar-rcva:num-submissions\\\": \\\"1\\\", \\\"clinvar-rcva:trait-type\\\": \\\"Disease\\\", \\\"clinvar-rcva:significance\\\": \\\"not provided\\\", \\\"clinvar-rcva:preferred-name\\\": \\\"NM_015658.3(NOC2L):c.1654C>T (p.Leu552=)\\\", \\\"clinvar-rcva:version\\\": \\\"2\\\", \\\"clinvar-rcva:gene-name\\\": \\\"nucleolar complex associated 2 homolog (S. cerevisiae)\\\", \\\"clinvar-rcva:accession\\\": \\\"RCV000064926\\\", \\\"clinvar-rcva:gene-symbol\\\": \\\"NOC2L\\\", \\\"clinvar-rcva:trait-name\\\": \\\"Malignant melanoma\\\", \\\"type\\\": \\\"clinvar-rcva\\\"}\"}, \"model\": \"gennotes_server.relation\", \"pk\": 8}]", 727 | "object_repr": "Relation: 8, Type: clinvar-rcva" 728 | } 729 | }, 730 | { 731 | "model": "reversion.version", 732 | "pk": 19, 733 | "fields": { 734 | "revision": 2, 735 | "object_id": "9", 736 | "object_id_int": 9, 737 | "content_type": [ 738 | "gennotes_server", 739 | "relation" 740 | ], 741 | "format": "json", 742 | "serialized_data": "[{\"fields\": {\"variant\": 2, \"tags\": \"{\\\"clinvar-rcva:record-status\\\": \\\"current\\\", \\\"clinvar-rcva:num-submissions\\\": \\\"1\\\", \\\"clinvar-rcva:trait-type\\\": \\\"Disease\\\", \\\"clinvar-rcva:significance\\\": \\\"not provided\\\", \\\"clinvar-rcva:preferred-name\\\": \\\"NM_015658.3(NOC2L):c.657C>T (p.Leu219=)\\\", \\\"clinvar-rcva:version\\\": \\\"2\\\", \\\"clinvar-rcva:gene-name\\\": \\\"nucleolar complex associated 2 homolog (S. cerevisiae)\\\", \\\"clinvar-rcva:accession\\\": \\\"RCV000064927\\\", \\\"clinvar-rcva:gene-symbol\\\": \\\"NOC2L\\\", \\\"clinvar-rcva:trait-name\\\": \\\"Malignant melanoma\\\", \\\"type\\\": \\\"clinvar-rcva\\\"}\"}, \"model\": \"gennotes_server.relation\", \"pk\": 9}]", 743 | "object_repr": "Relation: 9, Type: clinvar-rcva" 744 | } 745 | }, 746 | { 747 | "model": "reversion.version", 748 | "pk": 20, 749 | "fields": { 750 | "revision": 2, 751 | "object_id": "10", 752 | "object_id_int": 10, 753 | "content_type": [ 754 | "gennotes_server", 755 | "relation" 756 | ], 757 | "format": "json", 758 | "serialized_data": "[{\"fields\": {\"variant\": 3, \"tags\": \"{\\\"clinvar-rcva:record-status\\\": \\\"current\\\", \\\"clinvar-rcva:num-submissions\\\": \\\"1\\\", \\\"clinvar-rcva:trait-type\\\": \\\"Disease\\\", \\\"clinvar-rcva:significance\\\": \\\"not provided\\\", \\\"clinvar-rcva:preferred-name\\\": \\\"NM_001160184.1(PLEKHN1):c.484+30G>A\\\", \\\"clinvar-rcva:version\\\": \\\"2\\\", \\\"clinvar-rcva:gene-name\\\": \\\"pleckstrin homology domain containing, family N member 1\\\", \\\"clinvar-rcva:accession\\\": \\\"RCV000064940\\\", \\\"clinvar-rcva:gene-symbol\\\": \\\"PLEKHN1\\\", \\\"clinvar-rcva:trait-name\\\": \\\"Malignant melanoma\\\", \\\"type\\\": \\\"clinvar-rcva\\\"}\"}, \"model\": \"gennotes_server.relation\", \"pk\": 10}]", 759 | "object_repr": "Relation: 10, Type: clinvar-rcva" 760 | } 761 | }, 762 | { 763 | "model": "auth.permission", 764 | "fields": { 765 | "name": "Can add log entry", 766 | "content_type": [ 767 | "admin", 768 | "logentry" 769 | ], 770 | "codename": "add_logentry" 771 | } 772 | }, 773 | { 774 | "model": "auth.permission", 775 | "fields": { 776 | "name": "Can change log entry", 777 | "content_type": [ 778 | "admin", 779 | "logentry" 780 | ], 781 | "codename": "change_logentry" 782 | } 783 | }, 784 | { 785 | "model": "auth.permission", 786 | "fields": { 787 | "name": "Can delete log entry", 788 | "content_type": [ 789 | "admin", 790 | "logentry" 791 | ], 792 | "codename": "delete_logentry" 793 | } 794 | }, 795 | { 796 | "model": "auth.permission", 797 | "fields": { 798 | "name": "Can add permission", 799 | "content_type": [ 800 | "auth", 801 | "permission" 802 | ], 803 | "codename": "add_permission" 804 | } 805 | }, 806 | { 807 | "model": "auth.permission", 808 | "fields": { 809 | "name": "Can change permission", 810 | "content_type": [ 811 | "auth", 812 | "permission" 813 | ], 814 | "codename": "change_permission" 815 | } 816 | }, 817 | { 818 | "model": "auth.permission", 819 | "fields": { 820 | "name": "Can delete permission", 821 | "content_type": [ 822 | "auth", 823 | "permission" 824 | ], 825 | "codename": "delete_permission" 826 | } 827 | }, 828 | { 829 | "model": "auth.permission", 830 | "fields": { 831 | "name": "Can add group", 832 | "content_type": [ 833 | "auth", 834 | "group" 835 | ], 836 | "codename": "add_group" 837 | } 838 | }, 839 | { 840 | "model": "auth.permission", 841 | "fields": { 842 | "name": "Can change group", 843 | "content_type": [ 844 | "auth", 845 | "group" 846 | ], 847 | "codename": "change_group" 848 | } 849 | }, 850 | { 851 | "model": "auth.permission", 852 | "fields": { 853 | "name": "Can delete group", 854 | "content_type": [ 855 | "auth", 856 | "group" 857 | ], 858 | "codename": "delete_group" 859 | } 860 | }, 861 | { 862 | "model": "auth.permission", 863 | "fields": { 864 | "name": "Can add user", 865 | "content_type": [ 866 | "auth", 867 | "user" 868 | ], 869 | "codename": "add_user" 870 | } 871 | }, 872 | { 873 | "model": "auth.permission", 874 | "fields": { 875 | "name": "Can change user", 876 | "content_type": [ 877 | "auth", 878 | "user" 879 | ], 880 | "codename": "change_user" 881 | } 882 | }, 883 | { 884 | "model": "auth.permission", 885 | "fields": { 886 | "name": "Can delete user", 887 | "content_type": [ 888 | "auth", 889 | "user" 890 | ], 891 | "codename": "delete_user" 892 | } 893 | }, 894 | { 895 | "model": "auth.permission", 896 | "fields": { 897 | "name": "Can add content type", 898 | "content_type": [ 899 | "contenttypes", 900 | "contenttype" 901 | ], 902 | "codename": "add_contenttype" 903 | } 904 | }, 905 | { 906 | "model": "auth.permission", 907 | "fields": { 908 | "name": "Can change content type", 909 | "content_type": [ 910 | "contenttypes", 911 | "contenttype" 912 | ], 913 | "codename": "change_contenttype" 914 | } 915 | }, 916 | { 917 | "model": "auth.permission", 918 | "fields": { 919 | "name": "Can delete content type", 920 | "content_type": [ 921 | "contenttypes", 922 | "contenttype" 923 | ], 924 | "codename": "delete_contenttype" 925 | } 926 | }, 927 | { 928 | "model": "auth.permission", 929 | "fields": { 930 | "name": "Can add session", 931 | "content_type": [ 932 | "sessions", 933 | "session" 934 | ], 935 | "codename": "add_session" 936 | } 937 | }, 938 | { 939 | "model": "auth.permission", 940 | "fields": { 941 | "name": "Can change session", 942 | "content_type": [ 943 | "sessions", 944 | "session" 945 | ], 946 | "codename": "change_session" 947 | } 948 | }, 949 | { 950 | "model": "auth.permission", 951 | "fields": { 952 | "name": "Can delete session", 953 | "content_type": [ 954 | "sessions", 955 | "session" 956 | ], 957 | "codename": "delete_session" 958 | } 959 | }, 960 | { 961 | "model": "auth.permission", 962 | "fields": { 963 | "name": "Can add site", 964 | "content_type": [ 965 | "sites", 966 | "site" 967 | ], 968 | "codename": "add_site" 969 | } 970 | }, 971 | { 972 | "model": "auth.permission", 973 | "fields": { 974 | "name": "Can change site", 975 | "content_type": [ 976 | "sites", 977 | "site" 978 | ], 979 | "codename": "change_site" 980 | } 981 | }, 982 | { 983 | "model": "auth.permission", 984 | "fields": { 985 | "name": "Can delete site", 986 | "content_type": [ 987 | "sites", 988 | "site" 989 | ], 990 | "codename": "delete_site" 991 | } 992 | }, 993 | { 994 | "model": "auth.permission", 995 | "fields": { 996 | "name": "Can add variant", 997 | "content_type": [ 998 | "gennotes_server", 999 | "variant" 1000 | ], 1001 | "codename": "add_variant" 1002 | } 1003 | }, 1004 | { 1005 | "model": "auth.permission", 1006 | "fields": { 1007 | "name": "Can change variant", 1008 | "content_type": [ 1009 | "gennotes_server", 1010 | "variant" 1011 | ], 1012 | "codename": "change_variant" 1013 | } 1014 | }, 1015 | { 1016 | "model": "auth.permission", 1017 | "fields": { 1018 | "name": "Can delete variant", 1019 | "content_type": [ 1020 | "gennotes_server", 1021 | "variant" 1022 | ], 1023 | "codename": "delete_variant" 1024 | } 1025 | }, 1026 | { 1027 | "model": "auth.permission", 1028 | "fields": { 1029 | "name": "Can add relation", 1030 | "content_type": [ 1031 | "gennotes_server", 1032 | "relation" 1033 | ], 1034 | "codename": "add_relation" 1035 | } 1036 | }, 1037 | { 1038 | "model": "auth.permission", 1039 | "fields": { 1040 | "name": "Can change relation", 1041 | "content_type": [ 1042 | "gennotes_server", 1043 | "relation" 1044 | ], 1045 | "codename": "change_relation" 1046 | } 1047 | }, 1048 | { 1049 | "model": "auth.permission", 1050 | "fields": { 1051 | "name": "Can delete relation", 1052 | "content_type": [ 1053 | "gennotes_server", 1054 | "relation" 1055 | ], 1056 | "codename": "delete_relation" 1057 | } 1058 | }, 1059 | { 1060 | "model": "auth.permission", 1061 | "fields": { 1062 | "name": "Can add commit deletion", 1063 | "content_type": [ 1064 | "gennotes_server", 1065 | "commitdeletion" 1066 | ], 1067 | "codename": "add_commitdeletion" 1068 | } 1069 | }, 1070 | { 1071 | "model": "auth.permission", 1072 | "fields": { 1073 | "name": "Can change commit deletion", 1074 | "content_type": [ 1075 | "gennotes_server", 1076 | "commitdeletion" 1077 | ], 1078 | "codename": "change_commitdeletion" 1079 | } 1080 | }, 1081 | { 1082 | "model": "auth.permission", 1083 | "fields": { 1084 | "name": "Can delete commit deletion", 1085 | "content_type": [ 1086 | "gennotes_server", 1087 | "commitdeletion" 1088 | ], 1089 | "codename": "delete_commitdeletion" 1090 | } 1091 | }, 1092 | { 1093 | "model": "auth.permission", 1094 | "fields": { 1095 | "name": "Can add editing application", 1096 | "content_type": [ 1097 | "gennotes_server", 1098 | "editingapplication" 1099 | ], 1100 | "codename": "add_editingapplication" 1101 | } 1102 | }, 1103 | { 1104 | "model": "auth.permission", 1105 | "fields": { 1106 | "name": "Can change editing application", 1107 | "content_type": [ 1108 | "gennotes_server", 1109 | "editingapplication" 1110 | ], 1111 | "codename": "change_editingapplication" 1112 | } 1113 | }, 1114 | { 1115 | "model": "auth.permission", 1116 | "fields": { 1117 | "name": "Can delete editing application", 1118 | "content_type": [ 1119 | "gennotes_server", 1120 | "editingapplication" 1121 | ], 1122 | "codename": "delete_editingapplication" 1123 | } 1124 | }, 1125 | { 1126 | "model": "auth.permission", 1127 | "fields": { 1128 | "name": "Can add email address", 1129 | "content_type": [ 1130 | "account", 1131 | "emailaddress" 1132 | ], 1133 | "codename": "add_emailaddress" 1134 | } 1135 | }, 1136 | { 1137 | "model": "auth.permission", 1138 | "fields": { 1139 | "name": "Can change email address", 1140 | "content_type": [ 1141 | "account", 1142 | "emailaddress" 1143 | ], 1144 | "codename": "change_emailaddress" 1145 | } 1146 | }, 1147 | { 1148 | "model": "auth.permission", 1149 | "fields": { 1150 | "name": "Can delete email address", 1151 | "content_type": [ 1152 | "account", 1153 | "emailaddress" 1154 | ], 1155 | "codename": "delete_emailaddress" 1156 | } 1157 | }, 1158 | { 1159 | "model": "auth.permission", 1160 | "fields": { 1161 | "name": "Can add email confirmation", 1162 | "content_type": [ 1163 | "account", 1164 | "emailconfirmation" 1165 | ], 1166 | "codename": "add_emailconfirmation" 1167 | } 1168 | }, 1169 | { 1170 | "model": "auth.permission", 1171 | "fields": { 1172 | "name": "Can change email confirmation", 1173 | "content_type": [ 1174 | "account", 1175 | "emailconfirmation" 1176 | ], 1177 | "codename": "change_emailconfirmation" 1178 | } 1179 | }, 1180 | { 1181 | "model": "auth.permission", 1182 | "fields": { 1183 | "name": "Can delete email confirmation", 1184 | "content_type": [ 1185 | "account", 1186 | "emailconfirmation" 1187 | ], 1188 | "codename": "delete_emailconfirmation" 1189 | } 1190 | }, 1191 | { 1192 | "model": "auth.permission", 1193 | "fields": { 1194 | "name": "Can add cors model", 1195 | "content_type": [ 1196 | "corsheaders", 1197 | "corsmodel" 1198 | ], 1199 | "codename": "add_corsmodel" 1200 | } 1201 | }, 1202 | { 1203 | "model": "auth.permission", 1204 | "fields": { 1205 | "name": "Can change cors model", 1206 | "content_type": [ 1207 | "corsheaders", 1208 | "corsmodel" 1209 | ], 1210 | "codename": "change_corsmodel" 1211 | } 1212 | }, 1213 | { 1214 | "model": "auth.permission", 1215 | "fields": { 1216 | "name": "Can delete cors model", 1217 | "content_type": [ 1218 | "corsheaders", 1219 | "corsmodel" 1220 | ], 1221 | "codename": "delete_corsmodel" 1222 | } 1223 | }, 1224 | { 1225 | "model": "auth.permission", 1226 | "fields": { 1227 | "name": "Can add grant", 1228 | "content_type": [ 1229 | "oauth2_provider", 1230 | "grant" 1231 | ], 1232 | "codename": "add_grant" 1233 | } 1234 | }, 1235 | { 1236 | "model": "auth.permission", 1237 | "fields": { 1238 | "name": "Can change grant", 1239 | "content_type": [ 1240 | "oauth2_provider", 1241 | "grant" 1242 | ], 1243 | "codename": "change_grant" 1244 | } 1245 | }, 1246 | { 1247 | "model": "auth.permission", 1248 | "fields": { 1249 | "name": "Can delete grant", 1250 | "content_type": [ 1251 | "oauth2_provider", 1252 | "grant" 1253 | ], 1254 | "codename": "delete_grant" 1255 | } 1256 | }, 1257 | { 1258 | "model": "auth.permission", 1259 | "fields": { 1260 | "name": "Can add access token", 1261 | "content_type": [ 1262 | "oauth2_provider", 1263 | "accesstoken" 1264 | ], 1265 | "codename": "add_accesstoken" 1266 | } 1267 | }, 1268 | { 1269 | "model": "auth.permission", 1270 | "fields": { 1271 | "name": "Can change access token", 1272 | "content_type": [ 1273 | "oauth2_provider", 1274 | "accesstoken" 1275 | ], 1276 | "codename": "change_accesstoken" 1277 | } 1278 | }, 1279 | { 1280 | "model": "auth.permission", 1281 | "fields": { 1282 | "name": "Can delete access token", 1283 | "content_type": [ 1284 | "oauth2_provider", 1285 | "accesstoken" 1286 | ], 1287 | "codename": "delete_accesstoken" 1288 | } 1289 | }, 1290 | { 1291 | "model": "auth.permission", 1292 | "fields": { 1293 | "name": "Can add refresh token", 1294 | "content_type": [ 1295 | "oauth2_provider", 1296 | "refreshtoken" 1297 | ], 1298 | "codename": "add_refreshtoken" 1299 | } 1300 | }, 1301 | { 1302 | "model": "auth.permission", 1303 | "fields": { 1304 | "name": "Can change refresh token", 1305 | "content_type": [ 1306 | "oauth2_provider", 1307 | "refreshtoken" 1308 | ], 1309 | "codename": "change_refreshtoken" 1310 | } 1311 | }, 1312 | { 1313 | "model": "auth.permission", 1314 | "fields": { 1315 | "name": "Can delete refresh token", 1316 | "content_type": [ 1317 | "oauth2_provider", 1318 | "refreshtoken" 1319 | ], 1320 | "codename": "delete_refreshtoken" 1321 | } 1322 | }, 1323 | { 1324 | "model": "auth.permission", 1325 | "fields": { 1326 | "name": "Can add revision", 1327 | "content_type": [ 1328 | "reversion", 1329 | "revision" 1330 | ], 1331 | "codename": "add_revision" 1332 | } 1333 | }, 1334 | { 1335 | "model": "auth.permission", 1336 | "fields": { 1337 | "name": "Can change revision", 1338 | "content_type": [ 1339 | "reversion", 1340 | "revision" 1341 | ], 1342 | "codename": "change_revision" 1343 | } 1344 | }, 1345 | { 1346 | "model": "auth.permission", 1347 | "fields": { 1348 | "name": "Can delete revision", 1349 | "content_type": [ 1350 | "reversion", 1351 | "revision" 1352 | ], 1353 | "codename": "delete_revision" 1354 | } 1355 | }, 1356 | { 1357 | "model": "auth.permission", 1358 | "fields": { 1359 | "name": "Can add version", 1360 | "content_type": [ 1361 | "reversion", 1362 | "version" 1363 | ], 1364 | "codename": "add_version" 1365 | } 1366 | }, 1367 | { 1368 | "model": "auth.permission", 1369 | "fields": { 1370 | "name": "Can change version", 1371 | "content_type": [ 1372 | "reversion", 1373 | "version" 1374 | ], 1375 | "codename": "change_version" 1376 | } 1377 | }, 1378 | { 1379 | "model": "auth.permission", 1380 | "fields": { 1381 | "name": "Can delete version", 1382 | "content_type": [ 1383 | "reversion", 1384 | "version" 1385 | ], 1386 | "codename": "delete_version" 1387 | } 1388 | }, 1389 | { 1390 | "model": "auth.user", 1391 | "fields": { 1392 | "password": "", 1393 | "last_login": null, 1394 | "is_superuser": false, 1395 | "username": "clinvar-data-importer", 1396 | "first_name": "", 1397 | "last_name": "", 1398 | "email": "", 1399 | "is_staff": false, 1400 | "is_active": true, 1401 | "date_joined": "2015-07-04T20:38:04.414Z", 1402 | "groups": [], 1403 | "user_permissions": [] 1404 | } 1405 | }, 1406 | { 1407 | "model": "auth.user", 1408 | "fields": { 1409 | "password": "pbkdf2_sha256$20000$iLAzMU2UwJOd$Op5XrV6s/CG2EjbNa+BECyA3XDPDQj+G6zBks70X1wg=", 1410 | "last_login": "2015-07-04T23:54:38.553Z", 1411 | "is_superuser": false, 1412 | "username": "testuser", 1413 | "first_name": "", 1414 | "last_name": "", 1415 | "email": "mpball@gmail.com", 1416 | "is_staff": false, 1417 | "is_active": true, 1418 | "date_joined": "2015-07-04T23:54:24.227Z", 1419 | "groups": [], 1420 | "user_permissions": [] 1421 | } 1422 | }, 1423 | { 1424 | "model": "account.emailaddress", 1425 | "pk": 1, 1426 | "fields": { 1427 | "user": [ 1428 | "testuser" 1429 | ], 1430 | "email": "mpball@gmail.com", 1431 | "verified": true, 1432 | "primary": true 1433 | } 1434 | }, 1435 | { 1436 | "model": "reversion.revision", 1437 | "pk": 1, 1438 | "fields": { 1439 | "manager_slug": "default", 1440 | "date_created": "2015-07-04T20:40:04.302Z", 1441 | "user": [ 1442 | "clinvar-data-importer" 1443 | ], 1444 | "comment": "Variant added based on presence in ClinVar VCF file: clinvar_20150629.vcf.gz" 1445 | } 1446 | }, 1447 | { 1448 | "model": "reversion.revision", 1449 | "pk": 2, 1450 | "fields": { 1451 | "manager_slug": "default", 1452 | "date_created": "2015-07-04T20:43:18.524Z", 1453 | "user": [ 1454 | "clinvar-data-importer" 1455 | ], 1456 | "comment": "Relation added based on presence in ClinVar XML file: ClinVarFullRelease_2015-07.xml.gz" 1457 | } 1458 | } 1459 | ] 1460 | --------------------------------------------------------------------------------