diff --git a/README.md b/README.md index 8910b439..101d0850 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,65 @@ or run a `uwsgi` process: uwsgi --ini server/uwsgi/local.ini --protocol http --socket 127.0.0.1:8000 --check-static _site ``` +## Optional JBrowse Setup + +Install JBrowse in $project_root/jbrowse + +```bash +curl -O http://jbrowse.org/releases/JBrowse-x.x.x.zip (ie 1.11.3) +unzip JBrowse-x.x.x.zip -d jbrowse +cd jbrowse +./setup.sh +``` + +Download data files (ie BAM, GFF, VCF) to /your/directory/of/files/ and add the following to nginx.conf + +```bash +location /files/ { + alias /your/directory/of/files/; + internal; +} +``` + +Load reference sequences and load features tracks (in that order) using JBrowse loading scripts + +```bash +sudo ./bin/prepare-refseqs.pl --fasta ../files/chr1.fa +... +sudo ./bin/prepare-refseqs.pl --fasta ../files/chrY.fa +sudo ./bin/flatfile-to-json.pl --gff ../files/ALL_COSMIC_POINT_MUTS_v65.gff3 --trackLabel Cosmic --trackType CanvasFeatures +... + +Note: Segment large Cosmic GFF files with synchronization marks, a line containing '###', to prevent (really) slow loading +``` + +Run bgzip and tabix (via samtools/htslib) on VCF files + +```bash +git clone git://github.com/samtools/samtools.git +bgzip my.vcf +tabix -p vcf my.vcf.gz +``` + +Make one edit to JBrowse source (BAM.js), regex change related to custom URL + +```bash +/\/([^/\#\?]+)($|[\#\?])/ + +to + +/.*filename=([^&]+)/ +``` + +Lastly, save and load filenames correctly! Currently, the sample section key values of the manifest are concatenated to build filenames in the JBrowse URL + +```bash +[sample] +batch = Pseq_batch9 +sample = P-Pseq_0019-P-A +version = 1 +``` + ## Makefile Commands - `build` - builds and initializes all submodules, compiles SCSS and optimizes JavaScript diff --git a/varify/conf/global_settings.py b/varify/conf/global_settings.py index 44f4dd69..337b9934 100644 --- a/varify/conf/global_settings.py +++ b/varify/conf/global_settings.py @@ -167,7 +167,6 @@ TEMPLATE_CONTEXT_PROCESSORS += ( 'django.core.context_processors.request', 'varify.context_processors.static', - 'varify.context_processors.alamut', ) @@ -187,7 +186,7 @@ LOGOUT_URL = '/logout/' LOGIN_REDIRECT_URL = '/workspace/' -ALAMUT_URL = 'http://localhost:10000' +JBROWSE_HOST = 'localhost' # For non-publicly accessible applications, the siteauth app can be used to # restrict access site-wide. diff --git a/varify/context_processors.py b/varify/context_processors.py index 5424b8c8..6c787e13 100644 --- a/varify/context_processors.py +++ b/varify/context_processors.py @@ -14,9 +14,3 @@ def static(request): 'IMAGES_URL': os.path.join(static_url, 'images'), 'JAVASCRIPT_URL': os.path.join(static_url, 'js', prefix), } - - -def alamut(request): - return { - 'ALAMUT_URL': settings.ALAMUT_URL, - } diff --git a/varify/samples/formatters.py b/varify/samples/formatters.py index 933e3bcd..7d9d7e79 100644 --- a/varify/samples/formatters.py +++ b/varify/samples/formatters.py @@ -8,26 +8,6 @@ from django.conf import settings -class AlamutFormatter(HTMLFormatter): - href = settings.ALAMUT_URL + '/show?request={0}' - request = 'chr{chr}:g.{pos}{ref}>{alt}' - - def to_html(self, values, **context): - request = self.request.format(**values) - href = self.href.format(request) - return '{label}'.format( - href=href, label=request) - to_html.process_multiple = True - - def to_excel(self, values, **context): - request = self.request.format(**values) - href = self.href.format(request) - - return '=HYPERLINK("{href}", "{label}")'.format( - href=href, label=request) - to_excel.process_multiple = True - - class SampleFormatter(HTMLFormatter): def to_html(self, values, **kwargs): return values['sample__label'] @@ -105,7 +85,6 @@ def to_excel(self, value, **context): to_csv = to_excel -formatters.register(AlamutFormatter, 'Alamut Query Link') formatters.register(SampleFormatter, 'Sample') formatters.register(ReadDepthFormatter, 'Read Depth') formatters.register(CohortsFormatter, 'Cohorts') diff --git a/varify/samples/resources.py b/varify/samples/resources.py index 8c01413a..29f32169 100644 --- a/varify/samples/resources.py +++ b/varify/samples/resources.py @@ -16,7 +16,7 @@ from varify.variants.resources import VariantResource from varify import api from vdw.assessments.models import Assessment -from vdw.samples.models import Sample, Result, ResultScore +from vdw.samples.models import Sample, SampleManifest, Result, ResultScore from restlib2.http import codes log = logging.getLogger(__name__) @@ -218,6 +218,34 @@ def _cache_data(self, request, pk, key): data['variant'] = VariantResource.get(request, data['variant_id']) data.pop('variant_id') + # Integrate the SampleManifest data + path = SampleManifest.objects.get(sample__id=data['sample']['id']).path + manifest = open(path, 'r') + found = False + filestem = '' + + # Add JBrowse data requirements, including sample_manifest which is + # constructed by appending batch, sample, and version + for line in manifest: + if '[sample]' in line: + found = True + + if found: + if 'batch' in line or 'sample' in line or 'version' in line: + chunks = line.split('=') + + if len(chunks) > 1: + if 'batch' in line: + filestem += chunks[1].strip() + else: + filestem += '_' + chunks[1].strip() + + if 'version' in line: + break + + data['sample_manifest'] = filestem + data['jbrowse_host'] = settings.JBROWSE_HOST + try: score = ResultScore.objects.get(result=result) data['score'] = { @@ -254,10 +282,15 @@ class ResultsResource(ThrottledResource): template = api.templates.SampleResultVariant def post(self, request): - if (not request.data.get('ids') or - not isinstance(request.data['ids'], list)): - return HttpResponse(status=codes.unprocessable_entity, - content='Array of "ids" is required') + ids_not_found = 'ids' not in request.data + not_a_list = not isinstance(request.data['ids'], list) + + if ids_not_found or not_a_list: + return self.render( + request, + {'message': 'An array of "ids" is required'}, + status=codes.unprocessable_entity + ) data = [] resource = SampleResultResource() @@ -342,6 +375,43 @@ def get(self, request, year, month, day, name): return response + +class JbrowseResource(ThrottledResource): + def get(self, request): + data = request.GET + + if 'id' not in data or 'filename' not in data: + return self.render( + request, + {'message': 'An "id" and "filename" are required'}, + status=codes.unprocessable_entity + ) + + try: + if Sample.objects.get(id=data['id']): + response = HttpResponse() + f = data['filename'] + + if '.bai' in f or '.tbi' in f: + response['Content-Type'] = 'application/octet-stream' + else: + response['Content-Type'] = 'text/plain' + + # Control access to files hosted by nginx + response['X-Accel-Redirect'] = '/files/' + f + # Control access to files hosted by Apache + response['X-Sendfile'] = '/files/' + f + + response['Content-Disposition'] = 'attachment;filename=' + f + except Exception: + return self.render( + request, + {'message': 'No sample found for "id"'}, + status=codes.unprocessable_entity + ) + + return response + sample_resource = never_cache(SampleResource()) samples_resource = never_cache(SamplesResource()) named_sample_resource = never_cache(NamedSampleResource()) @@ -350,6 +420,7 @@ def get(self, request, year, month, day, name): results_resource = never_cache(ResultsResource()) phenotype_resource = never_cache(PhenotypeResource()) pedigree_resource = never_cache(PedigreeResource()) +jbrowse_resource = never_cache(JbrowseResource()) urlpatterns = patterns( '', @@ -360,6 +431,7 @@ def get(self, request, year, month, day, name): url(r'^(?P\d+)/variants/$', sample_results_resource, name='variants'), url(r'^variants/(?P\d+)/$', sample_result_resource, name='variant'), url(r'^variants/$', results_resource, name='results_resource'), + url(r'^jbrowse/$', jbrowse_resource, name='jbrowse_resource'), url(r'^(?P.+)/phenotypes/$', phenotype_resource, name='phenotype'), url(r'^pedigrees/(?P\d+)/(?P\d+)/(?P\d+)/(?P.+)$', diff --git a/varify/static/js/src/ui/modals/result.js b/varify/static/js/src/ui/modals/result.js index f49a7b4c..64d19a2f 100644 --- a/varify/static/js/src/ui/modals/result.js +++ b/varify/static/js/src/ui/modals/result.js @@ -134,6 +134,41 @@ define([ return content.join(''); }, + jbrowseUrl: function(variantAttrs, resultAttrs) { + var host = resultAttrs.jbrowse_host; + var url = 'http://' + host + '/jbrowse/?loc=chr' + variantAttrs.chr + ':'; + + url += parseInt(variantAttrs.pos-25) + '..' + parseInt(variantAttrs.pos+25); + url += '&tracks=DNA,Cosmic,VCF,BAM&highlight=&addTracks='; + + var addTracks ='[{"label":"BAM","store":"testStore","type":'; + + addTracks += '"JBrowse/View/Track/Alignments2"},{"label":"VCF","store":'; + addTracks += '"testStoreTwo","type":"JBrowse/View/Track/CanvasVariants"}]'; + + url += encodeURIComponent(addTracks) + '&addStores='; + + var sm = resultAttrs.sample_manifest; + var sid = resultAttrs.sample.id; + var addStores = '{"testStore":{"baiUrlTemplate"'; + + addStores += ':"http://' + host + '/api/samples/jbrowse/?id='; + addStores += sid + "&filename=" + sm + '.sorted.mdup.bai","type"'; + addStores += ':"JBrowse/Store/SeqFeature/BAM","urlTemplate":'; + addStores += '"http://' + host + '/api/samples/jbrowse/?id='; + addStores += sid + "&filename=" + sm; + addStores += '.sorted.mdup.bam"},"testStoreTwo":{"tbiUrlTemplate"'; + addStores += ':"http://' + host + '/api/samples/jbrowse/?id='; + addStores += sid + "&filename=" + sm + '.var_raw.vcf.gz.tbi","type"'; + addStores += ':"JBrowse/Store/SeqFeature/VCFTabix","urlTemplate":'; + addStores += '"http://' + host + '/api/samples/jbrowse/?id='; + addStores += sid + "&filename=" + sm + '.var_raw.vcf.gz"}}'; + + url += encodeURIComponent(addStores); + + return url; + }, + renderSummary: function(resultAttrs, variantAttrs) { var bases, content, hgmdLinks, key, labelClass; @@ -200,10 +235,13 @@ define([ } content.push(''); - content.push('Query Alamut'); + + url = this.jbrowseUrl(variantAttrs, resultAttrs); + + html = 'View JBrowse'; + + content.push(html); return content.join(''); },