diff --git a/resolwe_bio/tests/unit/test_api.py b/resolwe_bio/tests/unit/test_api.py index 52354e5b0..15384d5bb 100644 --- a/resolwe_bio/tests/unit/test_api.py +++ b/resolwe_bio/tests/unit/test_api.py @@ -2,7 +2,7 @@ from rest_framework import status from rest_framework.test import APIRequestFactory, force_authenticate -from resolwe.flow.models import Collection, Data, DescriptorSchema, Entity, Process +from resolwe.flow.models import Collection, Data, Entity, Process from resolwe.flow.views import CollectionViewSet, DataViewSet, EntityViewSet from resolwe.test import ProcessTestCase, TestCase @@ -50,41 +50,6 @@ def setUpTestData(cls): last_name="Miller", ) - cls.descriptor_schema = DescriptorSchema.objects.create( - slug="test-schema", - version="1.0.0", - contributor=cls.contributor, - schema=[ - { - "name": "general", - "group": [ - {"name": "species", "type": "basic:string:", "required": False}, - {"name": "organ", "type": "basic:string:", "required": False}, - { - "name": "biosample_treatment", - "type": "basic:string:", - "required": False, - }, - { - "name": "biosample_source", - "type": "basic:string:", - "required": False, - }, - ], - }, - { - "name": "response_and_survival_analysis", - "group": [ - { - "name": "confirmed_bor", - "type": "basic:string:", - "required": False, - } - ], - }, - ], - ) - cls.collections = [ Collection.objects.create( contributor=cls.contributor, @@ -101,58 +66,21 @@ def setUpTestData(cls): name="Test entity 0", collection=cls.collections[0], contributor=cls.contributor, - descriptor_schema=cls.descriptor_schema, - descriptor={ - "general": { - "species": "Homo sapiens", - "organ": "CRC", - "biosample_source": "CRC", - "biosample_treatment": "koh", - }, - "response_and_survival_analysis": { - "confirmed_bor": "pd", - }, - }, ), Entity.objects.create( name="Test entity 1", collection=cls.collections[1], contributor=cls.contributor, - descriptor_schema=cls.descriptor_schema, - descriptor={ - "general": {"species": "Homo sapiens", "organ": "CRC"}, - "response_and_survival_analysis": { - "confirmed_bor": "sd", - }, - }, ), Entity.objects.create( name="Test entity 2", collection=cls.collections[0], contributor=cls.contributor, - descriptor_schema=cls.descriptor_schema, - descriptor={ - "general": { - "species": "Mus musculus", - "organ": "CRC", - "biosample_treatment": "dmso", - }, - }, ), Entity.objects.create( name="Test entity 3", collection=cls.collections[2], contributor=cls.contributor, - descriptor_schema=cls.descriptor_schema, - descriptor={ - "general": { - "species": "Mus musculus", - "biosample_treatment": "dmso", - }, - "response_and_survival_analysis": { - "confirmed_bor": "pd", - }, - }, ), ] cls.collections[0].save() @@ -205,7 +133,6 @@ def setUp(self): version="1.0.0", contributor=self.contributor, entity_type="test-schema", - entity_descriptor_schema="test-schema", input_schema=[ {"name": "input_data", "type": "data:test:", "required": False} ], @@ -217,12 +144,6 @@ def setUp(self): ], ) - self.descriptor_schema = DescriptorSchema.objects.create( - slug="test-schema", - version="1.0.0", - contributor=self.contributor, - ) - self.data = [] for index in range(10): data = Data.objects.create( @@ -288,87 +209,22 @@ def setUp(self): } ) - clinical_schema = DescriptorSchema.objects.get(slug="general-clinical") - sample_schema = DescriptorSchema.objects.get(slug="sample") - self.entities = [ Entity.objects.create( name="Test entity 1", contributor=self.contributor, - descriptor_schema=clinical_schema, - descriptor={ - "general": {"species": "Homo sapiens"}, - "disease_information": { - "disease_type": "Colorectal cancer", - "disease_status": "Progresive", - }, - "subject_information": { - "batch": 1, - "group": "Pre", - "subject_id": "P-006", - "sample_label": "CRC", - }, - "immuno_oncology_treatment_type": { - "io_drug": "D-00A", - "io_treatment": "single", - }, - "response_and_survival_analysis": { - "pfs_event": "no", - "confirmed_bor": "pd", - }, - }, ), Entity.objects.create( name="Test entity 2", contributor=self.contributor, - descriptor_schema=clinical_schema, - descriptor={ - "general": {"species": "Homo sapiens"}, - "disease_information": { - "disease_type": "Mesothelioma", - "disease_status": "Regresive", - }, - "subject_information": { - "batch": 2, - "group": "Post", - "subject_id": "P-019", - "sample_label": "Meso", - }, - "immuno_oncology_treatment_type": { - "io_drug": "D-12A", - "io_treatment": "combo", - }, - "response_and_survival_analysis": { - "pfs_event": "yes", - "confirmed_bor": "sd", - }, - }, ), Entity.objects.create( name="Test entity 3", contributor=self.contributor, - descriptor_schema=sample_schema, - descriptor={ - "general": { - "species": "Mus musculus", - "description": "First sample", - "biosample_source": "lung", - "biosample_treatment": "dmso", - } - }, ), Entity.objects.create( name="Test entity 4", contributor=self.contributor, - descriptor_schema=sample_schema, - descriptor={ - "general": { - "species": "Mus musculus", - "description": "Second sample", - "biosample_source": "liver", - "biosample_treatment": "koh", - } - }, ), ] diff --git a/resolwe_bio/variants/__init__.py b/resolwe_bio/variants/__init__.py new file mode 100644 index 000000000..bbb1c004c --- /dev/null +++ b/resolwe_bio/variants/__init__.py @@ -0,0 +1,7 @@ +""".. Ignore pydocstyle D400. + +=================================== +Resolwe Bioinformatics Variants App +=================================== + +""" diff --git a/resolwe_bio/variants/apps.py b/resolwe_bio/variants/apps.py new file mode 100644 index 000000000..c4b967fec --- /dev/null +++ b/resolwe_bio/variants/apps.py @@ -0,0 +1,16 @@ +""".. Ignore pydocstyle D400. + +=============================== +Variants Base App Configuration +=============================== + +""" +from django.apps import AppConfig + + +class VariantsConfig(AppConfig): + """App configuration.""" + + name = "resolwe_bio.variants" + label = "resolwe_bio_variants" + verbose_name = "Resolwe Bioinformatics Variants Base" diff --git a/resolwe_bio/variants/listener_plugin.py b/resolwe_bio/variants/listener_plugin.py new file mode 100644 index 000000000..91b410f51 --- /dev/null +++ b/resolwe_bio/variants/listener_plugin.py @@ -0,0 +1,75 @@ +"""Handle variants related commands.""" + +import logging +from typing import TYPE_CHECKING + +from resolwe.flow.executors.socket_utils import Message, Response +from resolwe.flow.managers.listener.plugin import ( + ListenerPlugin, + listener_plugin_manager, +) + +from .models import Variant, VariantCall, VariantExperiment + +if TYPE_CHECKING: + from resolwe.flow.managers.listener.listener import Processor + +logger = logging.getLogger(__name__) + + +class VariantCommands(ListenerPlugin): + """Listener handlers related to the variants application.""" + + plugin_manager = listener_plugin_manager + + def add_variants( + self, data_id: int, message: Message[dict], manager: "Processor" + ) -> Response[int]: + """Handle connecting variants with the samples. + + If the reported variant does not exist in the file it is created. + """ + data = manager.data(data_id) + sample = data.entity + metadata, variants_data = message.message_data + species, genome_assembly = metadata["species"], metadata["genome_assembly"] + + variant_calls = list() + variant_cache = dict() + experiment = VariantExperiment.objects.create( + variant_data_source=metadata["variant_data_source"], + contributor=data.contributor, + ) + + # Bulk create variants. The consequesce of ignore_conflicts flag is that the + # database does not returt the ids of the created objects. So first create all + # the variants and then create the variant calls. + for variant_data in variants_data: + key = { + "species": species, + "genome_assembly": genome_assembly, + "chromosome": variant_data["chromosome"], + "position": variant_data["position"], + "reference": variant_data["reference"], + "alternative": variant_data["alternative"], + } + # To reduce the hits to the database use cache for variants. + key_tuple = tuple(key.values()) + if key_tuple not in variant_cache: + variant_cache[key_tuple] = Variant.objects.get_or_create(**key)[0] + variant = variant_cache[key_tuple] + + variant_calls.append( + VariantCall( + variant=variant, + data=data, + sample=sample, + quality=variant_data["quality"], + depth=variant_data["depth"], + genotype=variant_data["genotype"], + filter=variant_data["filter"], + experiment=experiment, + ) + ) + + VariantCall.objects.bulk_create(variant_calls) diff --git a/resolwe_bio/variants/migrations/0001_initial.py b/resolwe_bio/variants/migrations/0001_initial.py new file mode 100644 index 000000000..8463987fd --- /dev/null +++ b/resolwe_bio/variants/migrations/0001_initial.py @@ -0,0 +1,192 @@ +# Generated by Django 4.2.11 on 2024-03-25 10:07 + +from django.conf import settings +import django.contrib.postgres.fields +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("flow", "0021_annotationvalue_modified"), + ] + + operations = [ + migrations.CreateModel( + name="Variant", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("species", models.CharField(max_length=50)), + ("genome_assembly", models.CharField(max_length=20)), + ("chromosome", models.CharField(max_length=20)), + ("position", models.PositiveBigIntegerField()), + ("reference", models.CharField(max_length=100)), + ("alternative", models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name="VariantExperiment", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("variant_data_source", models.CharField(max_length=100)), + ("date", models.DateTimeField(auto_now_add=True, db_index=True)), + ( + "contributor", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="VariantCall", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("quality", models.FloatField()), + ("depth", models.PositiveIntegerField()), + ("filter", models.CharField(max_length=20)), + ("genotype", models.CharField(blank=True, max_length=100, null=True)), + ( + "data", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="variant_calls", + to="flow.data", + ), + ), + ( + "experiment", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="variant_calls", + to="resolwe_bio_variants.variantexperiment", + ), + ), + ( + "sample", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="variant_calls", + to="flow.entity", + ), + ), + ( + "variant", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="variant_calls", + to="resolwe_bio_variants.variant", + ), + ), + ], + ), + migrations.CreateModel( + name="VariantAnnotation", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("type", models.CharField(blank=True, max_length=100, null=True)), + ("annotation", models.CharField(max_length=200)), + ("annotation_impact", models.CharField(max_length=20)), + ("gene", models.CharField(max_length=100)), + ("protein_impact", models.CharField(max_length=100)), + ( + "feature_id", + django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=200), + default=list, + size=None, + ), + ), + ( + "clinical_diagnosis", + models.CharField(blank=True, max_length=200, null=True), + ), + ( + "clinical_significance", + models.CharField(blank=True, max_length=100, null=True), + ), + ("dbsnp_id", models.CharField(blank=True, max_length=20, null=True)), + ( + "clinical_var_id", + models.CharField(blank=True, max_length=20, null=True), + ), + ( + "data", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="variant_annotations", + to="flow.data", + ), + ), + ( + "variant", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="annotation", + to="resolwe_bio_variants.variant", + ), + ), + ], + ), + migrations.AddConstraint( + model_name="variant", + constraint=models.UniqueConstraint( + fields=( + "species", + "genome_assembly", + "chromosome", + "position", + "reference", + "alternative", + ), + name="uniq_composite_key_variants", + ), + ), + ] diff --git a/resolwe_bio/variants/migrations/__init__.py b/resolwe_bio/variants/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/resolwe_bio/variants/models.py b/resolwe_bio/variants/models.py new file mode 100644 index 000000000..0cabb2e12 --- /dev/null +++ b/resolwe_bio/variants/models.py @@ -0,0 +1,151 @@ +""".. Ignore pydocstyle D400. + +====== +Models +====== + +""" + +from django.conf import settings +from django.contrib.postgres.fields import ArrayField +from django.db import models + +from resolwe.flow.models import Data +from resolwe.flow.models import Entity as Sample + +# TODO: sync with the kb, at least the first entry. +SPECIES_MAX_LENGTH = 50 +GENOME_ASSEMBLY_MAX_LENGTH = 20 +CHROMOSOME_MAX_LENGTH = 20 +REFERENCE_MAX_LENGTH = 100 +ALTERNATIVE_MAX_LENGTH = 100 + +# Metadata +VARIANT_DATA_SOURCE_MAX_LENGTH = 100 +VARIANT_ANNOTATION_SOURCE_MAX_LENGTH = 100 +ANNOTATION_MAX_LENGTH = 200 +GENOTYPE_MAX_LENGTH = 100 +TYPE_MAX_LENGTH = 100 +CLINICAL_DIAGNOSIS_MAX_LENGTH = 200 +CLINICAL_SIGNIFICANCE_MAX_LENGTH = 100 +DBSNP_ID_MAX_LENGTH = 20 +CLINICAL_VAR_ID_MAX_LENGTH = 20 +ANNOTATION_IMPACT_MAX_LENGTH = 20 +GENE_MAX_LENGTH = 100 +PROTEIN_IMPACT_MAX_LENGTH = 100 +FEATURE_ID_MAX_LENGTH = 200 +FILTER_MAX_LENGTH = 20 + + +class Variant(models.Model): + """Describe a variant in the database.""" + + # Because Django ORM cannot handle composite primary keys, each feature is + # still assigned an internal numeric 'id' and the ('source', 'feature_id', + # 'species') combination is used to uniquely identify a feature. + species = models.CharField(max_length=SPECIES_MAX_LENGTH) + genome_assembly = models.CharField(max_length=GENOME_ASSEMBLY_MAX_LENGTH) + chromosome = models.CharField(max_length=CHROMOSOME_MAX_LENGTH) + position = models.PositiveBigIntegerField() + reference = models.CharField(max_length=REFERENCE_MAX_LENGTH) + alternative = models.CharField(max_length=ALTERNATIVE_MAX_LENGTH) + + class Meta: + """Add constraint for composite key.""" + + constraints = [ + models.UniqueConstraint( + fields=[ + "species", + "genome_assembly", + "chromosome", + "position", + "reference", + "alternative", + ], + name="uniq_composite_key_variants", + ), + ] + + +class VariantAnnotation(models.Model): + """Describes an annotation of a variant.""" + + variant = models.OneToOneField( + Variant, on_delete=models.CASCADE, related_name="annotation" + ) + type = models.CharField(max_length=TYPE_MAX_LENGTH, blank=True, null=True) + annotation = models.CharField(max_length=ANNOTATION_MAX_LENGTH) + annotation_impact = models.CharField(max_length=ANNOTATION_IMPACT_MAX_LENGTH) + gene = models.CharField(max_length=GENE_MAX_LENGTH) + protein_impact = models.CharField(max_length=PROTEIN_IMPACT_MAX_LENGTH) + feature_id = ArrayField( + models.CharField(max_length=FEATURE_ID_MAX_LENGTH), default=list + ) + clinical_diagnosis = models.CharField( + max_length=CLINICAL_DIAGNOSIS_MAX_LENGTH, blank=True, null=True + ) + clinical_significance = models.CharField( + max_length=CLINICAL_SIGNIFICANCE_MAX_LENGTH, blank=True, null=True + ) + # Optional references to the external databases. + dbsnp_id = models.CharField(max_length=DBSNP_ID_MAX_LENGTH, blank=True, null=True) + clinical_var_id = models.CharField( + max_length=CLINICAL_VAR_ID_MAX_LENGTH, blank=True, null=True + ) + data = models.ForeignKey( + Data, + on_delete=models.CASCADE, + related_name="variant_annotations", + null=True, + blank=True, + ) + + +class VariantExperiment(models.Model): + """Represents a single experiment.""" + + variant_data_source = models.CharField(max_length=VARIANT_DATA_SOURCE_MAX_LENGTH) + date = models.DateTimeField(auto_now_add=True, db_index=True) + contributor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) + + +class VariantCall(models.Model): + """VariantCall object.""" + + sample = models.ForeignKey( + Sample, + on_delete=models.CASCADE, + related_name="variant_calls", + null=True, + blank=True, + ) + + variant = models.ForeignKey( + Variant, + on_delete=models.CASCADE, + related_name="variant_calls", + null=True, + blank=True, + ) + + experiment = models.ForeignKey( + VariantExperiment, + on_delete=models.CASCADE, + related_name="variant_calls", + null=True, + blank=True, + ) + + # QC data. + quality = models.FloatField() + depth = models.PositiveIntegerField() + filter = models.CharField(max_length=FILTER_MAX_LENGTH) + genotype = models.CharField(max_length=GENOTYPE_MAX_LENGTH, blank=True, null=True) + data = models.ForeignKey( + Data, + on_delete=models.CASCADE, + related_name="variant_calls", + null=True, + blank=True, + ) diff --git a/resolwe_bio/variants/tests/__init__.py b/resolwe_bio/variants/tests/__init__.py new file mode 100644 index 000000000..d3bdcc6b9 --- /dev/null +++ b/resolwe_bio/variants/tests/__init__.py @@ -0,0 +1 @@ +# Make this folder a Python package. \ No newline at end of file diff --git a/resolwe_bio/variants/tests/test_variant.py b/resolwe_bio/variants/tests/test_variant.py new file mode 100644 index 000000000..94f536476 --- /dev/null +++ b/resolwe_bio/variants/tests/test_variant.py @@ -0,0 +1,955 @@ +from unittest.mock import Mock + +from django.contrib.auth import get_user_model +from django.test import TestCase +from rest_framework import status +from rest_framework.test import APIRequestFactory + +from resolwe.flow.executors.socket_utils import Message, MessageType +from resolwe.flow.models import Data, Entity, Process + +from resolwe_bio.variants.listener_plugin import VariantCommands +from resolwe_bio.variants.models import ( + Variant, + VariantAnnotation, + VariantCall, + VariantExperiment, +) +from resolwe_bio.variants.views import ( + VariantAnnotationSerializer, + VariantAnnotationViewSet, + VariantCallSerializer, + VariantCallViewSet, + VariantExperimentSerializer, + VariantExperimentViewSet, + VariantSerializer, + VariantViewSet, +) + + +class PrepareDataMixin: + """Prepare the data for all variant tests.""" + + @classmethod + def setUpTestData(cls): + """Set up the test data.""" + cls.contributor = get_user_model().objects.get_or_create( + username="contributor", email="contributor@genialis.com" + )[0] + cls.variants = Variant.objects.bulk_create( + [ + Variant( + species="Homo Sapiens", + genome_assembly="assembly 1", + chromosome="CHR1", + position=1234, + reference="ref1", + alternative="alt1", + ), + Variant( + species="Mus Musculus", + genome_assembly="assembly 2", + chromosome="CHR2", + position=67891234, + reference="ref2", + alternative="alt2", + ), + ] + ) + cls.annotations = VariantAnnotation.objects.bulk_create( + [ + VariantAnnotation( + variant=cls.variants[0], + type="annotation type 1", + annotation="annotation 1", + annotation_impact="impact 1", + gene="gene 1", + protein_impact="protein impact 1", + feature_id=["f1", "f2"], + clinical_diagnosis="clinical diagnosis 1", + clinical_significance="clinical significance 1", + dbsnp_id="dbsnp_id 1", + clinical_var_id="clinical_var_id 1", + ) + ] + ) + cls.experiments = VariantExperiment.objects.bulk_create( + [ + VariantExperiment( + variant_data_source="source 1", + contributor=cls.contributor, + ), + VariantExperiment( + variant_data_source="source 2", + contributor=cls.contributor, + ), + ] + ) + cls.calls = VariantCall.objects.bulk_create( + [ + VariantCall( + variant=cls.variants[0], + quality=0.7, + depth=15, + filter="filter 1", + genotype="genotype 1", + experiment=cls.experiments[0], + ), + VariantCall( + variant=cls.variants[1], + quality=0.2, + depth=5, + filter="filter 2", + genotype="genotype 2", + experiment=cls.experiments[1], + ), + ] + ) + + +class ListenerPluginTest(TestCase): + + def setUp(self): + """Prepare the test data.""" + contributor = get_user_model().objects.get_or_create( + username="contributor", email="contributor@genialis.com" + )[0] + self.data = Data.objects.create( + contributor=contributor, + entity=Entity.objects.create(contributor=contributor), + process=Process.objects.create(contributor=contributor), + status=Data.STATUS_PROCESSING, + ) + Variant.objects.create( + species="Homo Sapiens", + genome_assembly="ENSEMBL", + chromosome="chr1", + position=1, + reference="ref1", + alternative="alt1", + ) + return super().setUp() + + def test_add_variants(self): + """Test listener method.""" + + # The first variant already exists in the database and should be re-used. + # The second one is new and should be created. + metadata = { + "species": "Homo Sapiens", + "genome_assembly": "ENSEMBL", + "variant_data_source": "process", + } + variants_data = [ + { + "chromosome": "chr1", + "position": 1, + "reference": "ref1", + "alternative": "alt1", + "quality": 1, + "depth": 1, + "genotype": "1", + "filter": "1", + }, + { + "chromosome": "chr2", + "position": 2, + "reference": "ref2", + "alternative": "alt2", + "quality": 2, + "depth": 2, + "genotype": "2", + "filter": "2", + }, + ] + message = Message( + MessageType.COMMAND, "variants_test", [metadata, variants_data] + ) + manager_mock = Mock(data=Mock(return_value=self.data)) + VariantCommands().add_variants( + data_id=self.data.pk, + message=message, + manager=manager_mock, + ) + expected = [ + { + "species": "Homo Sapiens", + "genome_assembly": "ENSEMBL", + "chromosome": "chr1", + "position": 1, + "reference": "ref1", + "alternative": "alt1", + }, + { + "species": "Homo Sapiens", + "genome_assembly": "ENSEMBL", + "chromosome": "chr2", + "position": 2, + "reference": "ref2", + "alternative": "alt2", + }, + ] + self.assertCountEqual( + Variant.objects.all().values( + "species", + "genome_assembly", + "chromosome", + "position", + "reference", + "alternative", + ), + expected, + ) + # Exactly one experiment should be created. + experiment = VariantExperiment.objects.get() + self.assertEqual(experiment.contributor, self.data.contributor) + self.assertEqual(experiment.variant_data_source, "process") + expected_calls = [ + { + "sample_id": self.data.entity.pk, + "variant_id": 1, + "experiment_id": experiment.pk, + "quality": 1.0, + "depth": 1, + "filter": "1", + "genotype": "1", + "data_id": self.data.pk, + }, + { + "sample_id": self.data.entity.pk, + "variant_id": 2, + "experiment_id": experiment.pk, + "quality": 2.0, + "depth": 2, + "filter": "2", + "genotype": "2", + "data_id": self.data.pk, + }, + ] + self.assertCountEqual( + VariantCall.objects.all().values( + "sample_id", + "variant_id", + "experiment_id", + "quality", + "depth", + "filter", + "genotype", + "data_id", + ), + expected_calls, + ) + + +class VariantTest(PrepareDataMixin, TestCase): + def setUp(self) -> None: + self.view = VariantViewSet.as_view({"get": "list"}) + return super().setUp() + + def test_filter(self): + """Test the Variant filter.""" + request = APIRequestFactory().get("/variant") + # Basic get, no filter. + expected = VariantSerializer(self.variants, many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by id. + request = APIRequestFactory().get("/variant", {"id": self.variants[0].id}) + expected = VariantSerializer(self.variants[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by species. + request = APIRequestFactory().get("/variant", {"species": "Homo Sapiens"}) + expected = VariantSerializer(self.variants[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by genome_assembly. + request = APIRequestFactory().get( + "/variant", {"genome_assembly__icontains": "Embly 1"} + ) + expected = VariantSerializer(self.variants[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by chromosome. + request = APIRequestFactory().get("/variant", {"chromosome__iexact": "chr1"}) + expected = VariantSerializer(self.variants[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by position. + request = APIRequestFactory().get("/variant", {"position__lt": "12345"}) + expected = VariantSerializer(self.variants[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by reference. + request = APIRequestFactory().get("/variant", {"reference": "ref1"}) + expected = VariantSerializer(self.variants[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by alternative. + request = APIRequestFactory().get("/variant", {"alternative": "alt1"}) + expected = VariantSerializer(self.variants[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by annotation type. + request = APIRequestFactory().get( + "/variant", {"annotation__type__isnull": "true"} + ) + expected = VariantSerializer(self.variants[1:], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + request = APIRequestFactory().get( + "/variant", {"annotation__type__contains": "type 1"} + ) + expected = VariantSerializer(self.variants[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by annotation. + request = APIRequestFactory().get( + "/variant", {"annotation__annotation": "annotation 1"} + ) + expected = VariantSerializer(self.variants[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + request = APIRequestFactory().get( + "/variant", {"annotation__annotation__isnull": True} + ) + expected = VariantSerializer(self.variants[1:], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by annotation impact. + request = APIRequestFactory().get( + "/variant", {"annotation__annotation_impact__icontains": "MpAcT 1"} + ) + expected = VariantSerializer(self.variants[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + request = APIRequestFactory().get( + "/variant", {"annotation__annotation_impact__isnull": True} + ) + expected = VariantSerializer(self.variants[1:], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by gene. + request = APIRequestFactory().get("/variant", {"annotation__gene": "gene 1"}) + expected = VariantSerializer(self.variants[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + request = APIRequestFactory().get( + "/variant", {"annotation__gene__isnull": True} + ) + expected = VariantSerializer(self.variants[1:], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by protein impact. + request = APIRequestFactory().get( + "/variant", {"annotation__protein_impact__icontains": "protein"} + ) + expected = VariantSerializer(self.variants[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + request = APIRequestFactory().get( + "/variant", {"annotation__protein_impact__isnull": True} + ) + expected = VariantSerializer(self.variants[1:], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by clinical diagnosis. + request = APIRequestFactory().get( + "/variant", + {"annotation__clinical_diagnosis__icontains": "clinical diagnosis"}, + ) + expected = VariantSerializer(self.variants[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + request = APIRequestFactory().get( + "/variant", {"annotation__clinical_diagnosis__isnull": True} + ) + expected = VariantSerializer(self.variants[1:], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by clinical significance. + request = APIRequestFactory().get( + "/variant", + {"annotation__clinical_significance__contains": "significance 1"}, + ) + expected = VariantSerializer(self.variants[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + request = APIRequestFactory().get( + "/variant", {"annotation__clinical_significance__isnull": True} + ) + expected = VariantSerializer(self.variants[1:], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by quality. + request = APIRequestFactory().get( + "/variant", {"variant_calls__quality__isnull": "True"} + ) + expected = VariantSerializer([], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + request = APIRequestFactory().get( + "/variant", {"variant_calls__quality__gt": 0.5} + ) + expected = VariantSerializer(self.variants[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by depth. + request = APIRequestFactory().get( + "/variant", {"variant_calls__depth__isnull": "True"} + ) + expected = VariantSerializer([], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + request = APIRequestFactory().get("/variant", {"variant_calls__depth__gt": 10}) + expected = VariantSerializer(self.variants[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + def test_ordering(self): + """Test the Variant ordering.""" + # Order by species. + request = APIRequestFactory().get("/variant", {"ordering": "species"}) + expected = VariantSerializer(self.variants, many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + request = APIRequestFactory().get("/variant", {"ordering": "-species"}) + expected = VariantSerializer(self.variants[::-1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + + # Order by position. + request = APIRequestFactory().get("/variant", {"ordering": "position"}) + expected = VariantSerializer(self.variants, many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + request = APIRequestFactory().get("/variant", {"ordering": "-position"}) + expected = VariantSerializer(self.variants[::-1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + + # Order by genome_assembly. + request = APIRequestFactory().get("/variant", {"ordering": "genome_assembly"}) + expected = VariantSerializer(self.variants, many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + request = APIRequestFactory().get("/variant", {"ordering": "-genome_assembly"}) + expected = VariantSerializer(self.variants[::-1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + + # Order by chromosome. + request = APIRequestFactory().get("/variant", {"ordering": "chromosome"}) + expected = VariantSerializer(self.variants, many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + request = APIRequestFactory().get("/variant", {"ordering": "-chromosome"}) + expected = VariantSerializer(self.variants[::-1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + + +class VariantAnnotationTest(PrepareDataMixin, TestCase): + def setUp(self) -> None: + self.view = VariantAnnotationViewSet.as_view({"get": "list"}) + self.annotations.append( + VariantAnnotation.objects.create( + variant=self.variants[1], + type="annotation type 2", + annotation="annotation 2", + annotation_impact="impact 2", + gene="gene 2", + protein_impact="protein impact 2", + feature_id=["f1", "f2"], + clinical_diagnosis="clinical diagnosis 2", + clinical_significance="clinical significance 2", + dbsnp_id="dbsnp_id 2", + clinical_var_id="clinical_var_id 2", + ) + ) + return super().setUp() + + def test_filter(self): + # No filter. + request = APIRequestFactory().get("/variantannotation") + expected = VariantAnnotationSerializer(self.annotations, many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by id. + request = APIRequestFactory().get( + "/variantannotation", {"id": self.annotations[0].id} + ) + expected = VariantAnnotationSerializer(self.annotations[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by type. + request = APIRequestFactory().get( + "/variantannotation", {"type": "annotation type 1"} + ) + expected = VariantAnnotationSerializer(self.annotations[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by annotation. + request = APIRequestFactory().get( + "/variantannotation", {"annotation": "annotation 1"} + ) + expected = VariantAnnotationSerializer(self.annotations[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by annotation impact. + request = APIRequestFactory().get( + "/variantannotation", {"annotation_impact": "impact 1"} + ) + expected = VariantAnnotationSerializer(self.annotations[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by gene. + request = APIRequestFactory().get("/variantannotation", {"gene": "gene 1"}) + expected = VariantAnnotationSerializer(self.annotations[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by protein impact. + request = APIRequestFactory().get( + "/variantannotation", {"protein_impact": "protein impact 1"} + ) + expected = VariantAnnotationSerializer(self.annotations[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by clinical diagnosis. + request = APIRequestFactory().get( + "/variantannotation", {"clinical_diagnosis": "clinical diagnosis 1"} + ) + expected = VariantAnnotationSerializer(self.annotations[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by clinical significance. + request = APIRequestFactory().get( + "/variantannotation", {"clinical_significance": "clinical significance 1"} + ) + expected = VariantAnnotationSerializer(self.annotations[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by dbsnp_id. + request = APIRequestFactory().get( + "/variantannotation", {"dbsnp_id": "dbsnp_id 1"} + ) + expected = VariantAnnotationSerializer(self.annotations[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by clinical_var_id. + request = APIRequestFactory().get( + "/variantannotation", {"clinical_var_id": "clinical_var_id 1"} + ) + expected = VariantAnnotationSerializer(self.annotations[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + def test_ordering(self): + # Order by gene. + request = APIRequestFactory().get("/variantannotation", {"ordering": "gene"}) + expected = VariantAnnotationSerializer(self.annotations, many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + request = APIRequestFactory().get("/variantannotation", {"ordering": "-gene"}) + expected = VariantAnnotationSerializer(self.annotations[::-1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + + # Order by protein impact. + request = APIRequestFactory().get( + "/variantannotation", {"ordering": "protein_impact"} + ) + expected = VariantAnnotationSerializer(self.annotations, many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + request = APIRequestFactory().get( + "/variantannotation", {"ordering": "-protein_impact"} + ) + expected = VariantAnnotationSerializer(self.annotations[::-1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + + # Sort by annotation. + request = APIRequestFactory().get( + "/variantannotation", {"ordering": "annotation"} + ) + expected = VariantAnnotationSerializer(self.annotations, many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + request = APIRequestFactory().get( + "/variantannotation", {"ordering": "-annotation"} + ) + expected = VariantAnnotationSerializer(self.annotations[::-1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + + # Sort by clinical significance. + request = APIRequestFactory().get( + "/variantannotation", {"ordering": "clinical_significance"} + ) + expected = VariantAnnotationSerializer(self.annotations, many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + request = APIRequestFactory().get( + "/variantannotation", {"ordering": "-clinical_significance"} + ) + expected = VariantAnnotationSerializer(self.annotations[::-1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + + +class VariantCallTest(PrepareDataMixin, TestCase): + def setUp(self) -> None: + self.view = VariantCallViewSet.as_view({"get": "list"}) + return super().setUp() + + def test_filter(self): + # No filter. + request = APIRequestFactory().get("/variantcall") + expected = VariantCallSerializer(self.calls, many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by id. + request = APIRequestFactory().get("/variantcall", {"id": self.calls[0].id}) + expected = VariantCallSerializer(self.calls[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by quality. + request = APIRequestFactory().get("/variantcall", {"quality__gt": 0.5}) + expected = VariantCallSerializer(self.calls[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by depth. + request = APIRequestFactory().get("/variantcall", {"depth__gt": 10}) + expected = VariantCallSerializer(self.calls[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by variant id. + request = APIRequestFactory().get( + "/variantcall", {"variant__id": self.variants[0].id} + ) + expected = VariantCallSerializer(self.calls[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by variant species. + request = APIRequestFactory().get( + "/variantcall", {"variant__species": self.variants[0].species} + ) + expected = VariantCallSerializer(self.calls[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by variant genome_assembly. + request = APIRequestFactory().get( + "/variantcall", + {"variant__genome_assembly": self.variants[0].genome_assembly}, + ) + expected = VariantCallSerializer(self.calls[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by variant chromosome. + request = APIRequestFactory().get( + "/variantcall", {"variant__chromosome": self.variants[0].chromosome} + ) + expected = VariantCallSerializer(self.calls[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by variant position. + request = APIRequestFactory().get( + "/variantcall", + { + "variant__position__gte": self.variants[0].position, + "variant__position__lt": self.variants[1].position, + }, + ) + expected = VariantCallSerializer(self.calls[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by variant reference. + request = APIRequestFactory().get( + "/variantcall", {"variant__reference": self.variants[0].reference} + ) + expected = VariantCallSerializer(self.calls[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by variant alternative. + request = APIRequestFactory().get( + "/variantcall", {"variant__alternative": self.variants[0].alternative} + ) + expected = VariantCallSerializer(self.calls[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by variant annotation. + request = APIRequestFactory().get( + "/variantcall", + {"variant__annotation__annotation": self.annotations[0].annotation}, + ) + expected = VariantCallSerializer(self.calls[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by experiment id. + request = APIRequestFactory().get( + "/variantcall", {"experiment__id": self.experiments[0].id} + ) + expected = VariantCallSerializer(self.calls[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + def test_ordering(self): + # Order by id. + request = APIRequestFactory().get("/variantcall", {"ordering": "id"}) + expected = VariantCallSerializer(self.calls, many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + request = APIRequestFactory().get("/variantcall", {"ordering": "-id"}) + expected = VariantCallSerializer(self.calls[::-1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + + # Order by quality. + request = APIRequestFactory().get("/variantcall", {"ordering": "-quality"}) + expected = VariantCallSerializer(self.calls, many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + request = APIRequestFactory().get("/variantcall", {"ordering": "quality"}) + expected = VariantCallSerializer(self.calls[::-1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + + # Order by depth. + request = APIRequestFactory().get("/variantcall", {"ordering": "-depth"}) + expected = VariantCallSerializer(self.calls, many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + request = APIRequestFactory().get("/variantcall", {"ordering": "depth"}) + expected = VariantCallSerializer(self.calls[::-1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + + +class VariantExperimentTest(PrepareDataMixin, TestCase): + def setUp(self) -> None: + self.view = VariantExperimentViewSet.as_view({"get": "list"}) + return super().setUp() + + def test_filter(self): + # No filter. + request = APIRequestFactory().get("/variantexperiment") + expected = VariantExperimentSerializer(self.experiments, many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by id. + request = APIRequestFactory().get( + "/variantexperiment", {"id": self.experiments[0].id} + ) + expected = VariantExperimentSerializer(self.experiments[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by variant data source. + request = APIRequestFactory().get( + "/variantexperiment", {"variant_data_source": "source 1"} + ) + expected = VariantExperimentSerializer(self.experiments[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by contributor. + contributor2 = get_user_model().objects.create( + username="contributor2", email="contributor2@genialis.com" + ) + self.experiments[1].contributor = contributor2 + self.experiments[1].save() + request = APIRequestFactory().get( + "/variantexperiment", {"contributor__id": self.contributor.id} + ) + expected = VariantExperimentSerializer(self.experiments[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + request = APIRequestFactory().get( + "/variantexperiment", {"contributor__username": self.contributor.username} + ) + expected = VariantExperimentSerializer(self.experiments[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + request = APIRequestFactory().get( + "/variantexperiment", {"contributor__email": self.contributor.email} + ) + expected = VariantExperimentSerializer(self.experiments[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + # Filter by date. + request = APIRequestFactory().get( + "/variantexperiment", {"date__lt": self.experiments[1].date} + ) + expected = VariantExperimentSerializer(self.experiments[:1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + def test_ordering(self): + # Order by id. + request = APIRequestFactory().get("/variantexperiment", {"ordering": "id"}) + expected = VariantExperimentSerializer(self.experiments, many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + request = APIRequestFactory().get("/variantexperiment", {"ordering": "-id"}) + expected = VariantExperimentSerializer(self.experiments[::-1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + + # Order by date. + request = APIRequestFactory().get("/variantexperiment", {"ordering": "date"}) + expected = VariantExperimentSerializer(self.experiments, many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + request = APIRequestFactory().get("/variantexperiment", {"ordering": "-date"}) + expected = VariantExperimentSerializer(self.experiments[::-1], many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + + # Order by contributor. + contributor2 = get_user_model().objects.create( + username="contributor2", email="contributor2@genialis.com" + ) + self.experiments[1].contributor = contributor2 + self.experiments[1].save() + + request = APIRequestFactory().get( + "/variantexperiment", {"ordering": "-contributor__email"} + ) + expected = VariantExperimentSerializer(self.experiments, many=True).data + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + request = APIRequestFactory().get( + "/variantexperiment", {"ordering": "contributor__email"} + ) + response = self.view( + APIRequestFactory().get( + "/variantexperiment", {"ordering": "contributor__email"} + ) + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected[::-1]) diff --git a/resolwe_bio/variants/views.py b/resolwe_bio/variants/views.py new file mode 100644 index 000000000..492b9e355 --- /dev/null +++ b/resolwe_bio/variants/views.py @@ -0,0 +1,234 @@ +""".. Ignore pydocstyle D400. + +============================= +Expose Variants models on API +============================= + +""" + +import logging + +import django_filters as filters +from rest_framework import mixins, serializers, viewsets + +from resolwe.flow.filters import ( + DATE_LOOKUPS, + NUMBER_LOOKUPS, + TEXT_LOOKUPS, + CheckQueryParamsMixin, + OrderingFilter, +) +from resolwe.rest.serializers import SelectiveFieldMixin + +from .models import Variant, VariantAnnotation, VariantCall, VariantExperiment + +logger = logging.getLogger(__name__) + + +class VariantSerializer(SelectiveFieldMixin, serializers.ModelSerializer): + """Serializer for Variant objects.""" + + class Meta: + """Serializer configuration.""" + + model = Variant + fields = [ + "id", + "species", + "genome_assembly", + "chromosome", + "position", + "reference", + "alternative", + "annotation", + ] + + +class VariantFilter(CheckQueryParamsMixin, filters.FilterSet): + """Filter the Variant objects endpoint.""" + + class Meta: + """Filter configuration.""" + + model = Variant + fields = { + "id": NUMBER_LOOKUPS, + "species": TEXT_LOOKUPS, + "genome_assembly": TEXT_LOOKUPS, + "chromosome": TEXT_LOOKUPS, + "position": NUMBER_LOOKUPS, + "reference": TEXT_LOOKUPS, + "alternative": TEXT_LOOKUPS, + "annotation__type": TEXT_LOOKUPS, + "annotation__annotation": TEXT_LOOKUPS, + "annotation__annotation_impact": TEXT_LOOKUPS, + "annotation__gene": TEXT_LOOKUPS, + "annotation__protein_impact": TEXT_LOOKUPS, + "annotation__clinical_diagnosis": TEXT_LOOKUPS, + "annotation__clinical_significance": TEXT_LOOKUPS, + "variant_calls__quality": NUMBER_LOOKUPS, + "variant_calls__depth": NUMBER_LOOKUPS, + } + + +class VariantViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): + """Variant endpoint.""" + + queryset = Variant.objects.all() + serializer_class = VariantSerializer + filter_backends = [filters.rest_framework.DjangoFilterBackend, OrderingFilter] + + filterset_class = VariantFilter + ordering_fields = ("species", "genome_assembly", "position", "chromosome") + + +class VariantAnnotationSerializer(SelectiveFieldMixin, serializers.ModelSerializer): + """Serializer for VariantAnnotation objects.""" + + class Meta: + """Serializer configuration.""" + + model = VariantAnnotation + fields = [ + "id", + "variant_id", + "type", + "annotation", + "annotation_impact", + "gene", + "protein_impact", + "feature_id", + "clinical_diagnosis", + "clinical_significance", + "dbsnp_id", + "clinical_var_id", + "data_id", + ] + + +class VariantAnnotationFilter(CheckQueryParamsMixin, filters.FilterSet): + """Filter the VariantAnnotation objects endpoint.""" + + class Meta: + """Filter configuration.""" + + model = VariantAnnotation + fields = { + "id": NUMBER_LOOKUPS, + "type": TEXT_LOOKUPS, + "annotation": TEXT_LOOKUPS, + "annotation_impact": TEXT_LOOKUPS, + "gene": TEXT_LOOKUPS, + "protein_impact": TEXT_LOOKUPS, + "clinical_diagnosis": TEXT_LOOKUPS, + "clinical_significance": TEXT_LOOKUPS, + "dbsnp_id": TEXT_LOOKUPS, + "clinical_var_id": TEXT_LOOKUPS, + } + + +class VariantAnnotationViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): + """VariantAnnotation endpoint.""" + + queryset = VariantAnnotation.objects.all() + serializer_class = VariantAnnotationSerializer + filter_backends = [filters.rest_framework.DjangoFilterBackend, OrderingFilter] + + filterset_class = VariantAnnotationFilter + ordering_fields = ("gene", "protein_impact", "annotation", "clinical_significance") + + +class VariantCallSerializer(SelectiveFieldMixin, serializers.ModelSerializer): + """Serializer for VariantCall objects.""" + + class Meta: + """Serializer configuration.""" + + model = VariantCall + fields = [ + "id", + "sample_id", + "variant_id", + "experiment_id", + "quality", + "depth", + "filter", + "genotype", + "data_id", + ] + + +class VariantCallFilter(CheckQueryParamsMixin, filters.FilterSet): + """Filter the VariantCall objects endpoint.""" + + class Meta: + """Filter configuration.""" + + model = VariantCall + fields = { + "id": NUMBER_LOOKUPS, + "sample__slug": TEXT_LOOKUPS, + "sample__id": NUMBER_LOOKUPS, + "data__slug": TEXT_LOOKUPS, + "data__id": NUMBER_LOOKUPS, + "variant__id": NUMBER_LOOKUPS, + "variant__species": TEXT_LOOKUPS, + "variant__genome_assembly": TEXT_LOOKUPS, + "variant__chromosome": TEXT_LOOKUPS, + "variant__position": NUMBER_LOOKUPS, + "variant__reference": TEXT_LOOKUPS, + "variant__alternative": TEXT_LOOKUPS, + "variant__annotation__annotation": TEXT_LOOKUPS, + "experiment__id": NUMBER_LOOKUPS, + "quality": NUMBER_LOOKUPS, + "depth": NUMBER_LOOKUPS, + } + + +class VariantCallViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): + """VariantCall endpoint.""" + + queryset = VariantCall.objects.all() + serializer_class = VariantCallSerializer + filter_backends = [filters.rest_framework.DjangoFilterBackend, OrderingFilter] + + filterset_class = VariantCallFilter + ordering_fields = ("id", "quality", "depth") + + +class VariantExperimentSerializer(SelectiveFieldMixin, serializers.ModelSerializer): + """Serializer for VariantExperiment objects.""" + + class Meta: + """Serializer configuration.""" + + model = VariantExperiment + fields = ["id", "date", "contributor", "variant_data_source"] + + +class VariantExperimentFilter(CheckQueryParamsMixin, filters.FilterSet): + """Filter the VariantExperiment objects endpoint.""" + + class Meta: + """Filter configuration.""" + + model = VariantExperiment + fields = { + "id": NUMBER_LOOKUPS, + "contributor__username": TEXT_LOOKUPS, + "contributor__email": TEXT_LOOKUPS, + "contributor__id": NUMBER_LOOKUPS, + "date": DATE_LOOKUPS, + "variant_data_source": TEXT_LOOKUPS, + } + + +class VariantExperimentViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): + """VariantExperiment endpoint.""" + + queryset = VariantExperiment.objects.all() + serializer_class = VariantExperimentSerializer + filter_backends = [filters.rest_framework.DjangoFilterBackend, OrderingFilter] + + filterset_class = VariantExperimentFilter + ordering_fields = ("id", "date", "contributor__email") diff --git a/tests/settings.py b/tests/settings.py index 30187d629..3f79263b5 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -43,6 +43,7 @@ "resolwe.test_helpers", "resolwe_bio", "resolwe_bio.kb", + "resolwe_bio.variants", ) ROOT_URLCONF = "tests.urls" diff --git a/tests/urls.py b/tests/urls.py index 4f9544c20..a96e0231a 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,30 +1,38 @@ from django.urls import include, path - from rest_framework import routers from resolwe.api_urls import api_router as resolwe_router from resolwe.flow.views import EntityViewSet + from resolwe_bio.filters import BioEntityFilter -from resolwe_bio.kb.views import ( - FeatureViewSet, MappingSearchViewSet -) +from resolwe_bio.kb.views import FeatureViewSet, MappingSearchViewSet +from resolwe_bio.variants.views import VariantAnnotationViewSet, VariantViewSet from .routers import SearchRouter - EntityViewSet.filterset_class = BioEntityFilter api_router = routers.DefaultRouter(trailing_slash=False) -api_router.register(r'sample', EntityViewSet) +api_router.register(r"sample", EntityViewSet) +api_router.register(r"variant", VariantViewSet) +api_router.register(r"variant_annotations", VariantAnnotationViewSet) search_router = SearchRouter(trailing_slash=False) -search_router.register(r'kb/feature', FeatureViewSet, 'kb_feature') -search_router.register(r'kb/mapping/search', MappingSearchViewSet, 'kb_mapping_search') +search_router.register(r"kb/feature", FeatureViewSet, "kb_feature") +search_router.register(r"kb/mapping/search", MappingSearchViewSet, "kb_mapping_search") urlpatterns = [ - path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), + path("api-auth/", include("rest_framework.urls", namespace="rest_framework")), # XXX: Temporary fix to work with Resolwe 2.0.0, which requires 'resolwe-api' namespace to be available when # reporting errors when running processes. - path('api-resolwe/', include((resolwe_router.urls, 'resolwe-api'))), - path('api/', include((api_router.urls + search_router.urls + resolwe_router.urls, 'resolwebio-api'))), + path("api-resolwe/", include((resolwe_router.urls, "resolwe-api"))), + path( + "api/", + include( + ( + api_router.urls + search_router.urls + resolwe_router.urls, + "resolwebio-api", + ) + ), + ), ]