From 90eaabb5a37c27b02a7bf0102c1ac5ed60f66b59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gregor=20Jer=C5=A1e?= Date: Tue, 26 Nov 2024 09:17:37 +0100 Subject: [PATCH 1/3] Fix remove the deprecated option --- resolwe_bio/processes/import_data/geneset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resolwe_bio/processes/import_data/geneset.py b/resolwe_bio/processes/import_data/geneset.py index d669fbba8..029a96fdc 100644 --- a/resolwe_bio/processes/import_data/geneset.py +++ b/resolwe_bio/processes/import_data/geneset.py @@ -18,7 +18,7 @@ def parse_geneset_file(geneset_file, warning): """Parse geneset file.""" - with open(geneset_file, "rU") as handle: + with open(geneset_file, "r") as handle: # skip empty lines genes = [str(line.strip()) for line in handle if line.strip()] geneset = sorted(set(genes)) From da0819913476a33f1da1e89c9c89f8db716e10f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gregor=20Jer=C5=A1e?= Date: Mon, 2 Dec 2024 15:47:12 +0100 Subject: [PATCH 2/3] Remove deprecated isort option --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index c33481804..2db842d1d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,6 @@ known_utils = utils sections=FUTURE,STDLIB,THIRDPARTY,RESOLWE,FIRSTPARTY,UTILS,LOCALFOLDER default_section = THIRDPARTY skip = migrations -not_skip = __init__.py [flake8] ; E,W - disable pycodestyle checks as they may conflict with black From eeb0a15ac6bf8383f97f00a50934dca01fed4216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gregor=20Jer=C5=A1e?= Date: Mon, 11 Nov 2024 11:08:41 +0100 Subject: [PATCH 3/3] Allow filtering variant experiment by sample and use generic filter for filtering by related models with respect to permissions --- docs/CHANGELOG.rst | 14 ++- resolwe_bio/variants/filters.py | 90 ++++++++-------- resolwe_bio/variants/tests/test_variant.py | 116 ++++++++++++++++++++- 3 files changed, 163 insertions(+), 57 deletions(-) diff --git a/docs/CHANGELOG.rst b/docs/CHANGELOG.rst index 5cd424165..f31dfb5cc 100644 --- a/docs/CHANGELOG.rst +++ b/docs/CHANGELOG.rst @@ -7,6 +7,16 @@ All notable changes to this project are documented in this file. This project adheres to `Semantic Versioning `_. +========== +Unreleased +========== + +Added +----- +- Add filtering ``Variant`` by ``VariantExperiment`` and ``VariantCall`` +- Use generic permission filters when filtering variants by related models + + =================== 61.0.0 - 2024-11-21 =================== @@ -20,10 +30,6 @@ Changed 60.0.0 - 2024-11-19 =================== -Added ------ -- Add filtering ``Variant`` by ``VariantExperiment`` and ``VariantCall`` - Changed ------- - **BACKWARD INCOMPATIBLE:** Require Resolwe 41.x diff --git a/resolwe_bio/variants/filters.py b/resolwe_bio/variants/filters.py index bbe41e313..fcfcebe55 100644 --- a/resolwe_bio/variants/filters.py +++ b/resolwe_bio/variants/filters.py @@ -6,14 +6,14 @@ """ -import django_filters as filters - from resolwe.flow.filters import ( DATETIME_LOOKUPS, NUMBER_LOOKUPS, RELATED_LOOKUPS, TEXT_LOOKUPS, - CheckQueryParamsMixin, + BaseResolweFilter, + DataFilter, + FilterRelatedWithPermissions, ) from resolwe_bio.variants.models import ( @@ -24,9 +24,42 @@ ) -class VariantFilter(CheckQueryParamsMixin, filters.FilterSet): +class VariantCallFilter(BaseResolweFilter): + """Filter the VariantCall objects endpoint.""" + + data = FilterRelatedWithPermissions(DataFilter) + + class Meta: + """Filter configuration.""" + + model = VariantCall + fields = { + "id": NUMBER_LOOKUPS, + "sample__slug": TEXT_LOOKUPS, + "sample": RELATED_LOOKUPS, + "variant": RELATED_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, + "experiment": RELATED_LOOKUPS, + "quality": NUMBER_LOOKUPS, + "depth": NUMBER_LOOKUPS, + "filter": TEXT_LOOKUPS, + "genotype": TEXT_LOOKUPS, + "genotype_quality": NUMBER_LOOKUPS, + "alternative_allele_depth": NUMBER_LOOKUPS, + "depth_norm_quality": NUMBER_LOOKUPS, + } + + +class VariantFilter(BaseResolweFilter): """Filter the Variant objects endpoint.""" + variant_calls = FilterRelatedWithPermissions(VariantCallFilter) + class Meta: """Filter configuration.""" @@ -51,18 +84,6 @@ class Meta: "annotation__clinical_significance": TEXT_LOOKUPS, "annotation__dbsnp_id": TEXT_LOOKUPS, "annotation__clinvar_id": TEXT_LOOKUPS, - "annotation__data": RELATED_LOOKUPS, - "variant_calls": RELATED_LOOKUPS, - "variant_calls__quality": NUMBER_LOOKUPS, - "variant_calls__depth": NUMBER_LOOKUPS, - "variant_calls__sample__slug": TEXT_LOOKUPS, - "variant_calls__sample": RELATED_LOOKUPS, - "variant_calls__experiment": RELATED_LOOKUPS, - "variant_calls__filter": TEXT_LOOKUPS, - "variant_calls__genotype": TEXT_LOOKUPS, - "variant_calls__genotype_quality": NUMBER_LOOKUPS, - "variant_calls__alternative_allele_depth": NUMBER_LOOKUPS, - "variant_calls__depth_norm_quality": NUMBER_LOOKUPS, } @property @@ -72,7 +93,7 @@ def qs(self): return parent.distinct() -class VariantAnnotationFilter(CheckQueryParamsMixin, filters.FilterSet): +class VariantAnnotationFilter(BaseResolweFilter): """Filter the VariantAnnotation objects endpoint.""" class Meta: @@ -94,40 +115,11 @@ class Meta: } -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": RELATED_LOOKUPS, - "data__slug": TEXT_LOOKUPS, - "data": RELATED_LOOKUPS, - "variant": RELATED_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, - "experiment": RELATED_LOOKUPS, - "quality": NUMBER_LOOKUPS, - "depth": NUMBER_LOOKUPS, - "filter": TEXT_LOOKUPS, - "genotype": TEXT_LOOKUPS, - "genotype_quality": NUMBER_LOOKUPS, - "alternative_allele_depth": NUMBER_LOOKUPS, - "depth_norm_quality": NUMBER_LOOKUPS, - } - - -class VariantExperimentFilter(CheckQueryParamsMixin, filters.FilterSet): +class VariantExperimentFilter(BaseResolweFilter): """Filter the VariantExperiment objects endpoint.""" + variant_calls = FilterRelatedWithPermissions(VariantCallFilter) + class Meta: """Filter configuration.""" diff --git a/resolwe_bio/variants/tests/test_variant.py b/resolwe_bio/variants/tests/test_variant.py index 3195d84f4..47a792e0b 100644 --- a/resolwe_bio/variants/tests/test_variant.py +++ b/resolwe_bio/variants/tests/test_variant.py @@ -12,7 +12,6 @@ from resolwe.flow.models import Data from resolwe.flow.models import Entity as Sample from resolwe.flow.models import Process -from resolwe.flow.serializers.entity import EntitySerializer from resolwe.permissions.models import Permission from resolwe.test import ProcessTestCase, tag_process @@ -60,7 +59,9 @@ def setUpTestData(cls): cls.contributor = User.objects.get_or_create( username="contributor", email="contributor@genialis.com" )[0] - cls.sample = Sample.objects.create(contributor=cls.contributor) + cls.sample = Sample.objects.create( + contributor=cls.contributor, name="Test sample" + ) cls.variants = Variant.objects.bulk_create( [ Variant( @@ -744,6 +745,7 @@ def test_filter(self): self.assertCountEqual(response.data, expected) # Filter by quality. + self.sample.set_permission(Permission.EDIT, self.contributor) request = APIRequestFactory().get( "/variant", {"variant_calls__quality__isnull": "True"} ) @@ -755,6 +757,7 @@ def test_filter(self): "/variant", {"variant_calls__quality__gt": 0.5} ) expected = VariantSerializer(self.variants[:1], many=True).data + force_authenticate(request, self.contributor) response = self.view(request) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertCountEqual(response.data, expected) @@ -763,11 +766,13 @@ def test_filter(self): request = APIRequestFactory().get( "/variant", {"variant_calls__depth__isnull": "True"} ) + force_authenticate(request, self.contributor) 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}) + force_authenticate(request, self.contributor) expected = VariantSerializer(self.variants[:1], many=True).data response = self.view(request) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -778,6 +783,7 @@ def test_filter(self): "/variant", {"variant_calls__sample__slug": "non_existing"} ) expected = [] + force_authenticate(request, self.contributor) response = self.view(request) self.assertContains(response, expected, status_code=status.HTTP_200_OK) @@ -785,6 +791,7 @@ def test_filter(self): request = APIRequestFactory().get( "/variant", {"variant_calls__sample__slug": self.sample.slug} ) + force_authenticate(request, self.contributor) response = self.view(request) expected = VariantSerializer(self.variants, many=True).data self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -792,6 +799,7 @@ def test_filter(self): # Filter by sample id. request = APIRequestFactory().get("/variant", {"variant_calls__sample": -1}) + force_authenticate(request, self.contributor) response = self.view(request) self.assertContains( response, @@ -803,17 +811,20 @@ def test_filter(self): request = APIRequestFactory().get( "/variant", {"variant_calls__sample__in": [self.sample.pk]} ) + force_authenticate(request, self.contributor) response = self.view(request) expected = VariantSerializer(self.variants, many=True).data self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertCountEqual(response.data, expected) sample = Sample.objects.create(contributor=self.contributor) + sample.set_permission(Permission.EDIT, self.contributor) self.calls[0].sample = sample self.calls[0].save() request = APIRequestFactory().get( "/variant", {"variant_calls__sample": self.sample.pk} ) + force_authenticate(request, self.contributor) response = self.view(request) expected = VariantSerializer(self.variants[1:], many=True).data self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -822,6 +833,7 @@ def test_filter(self): request = APIRequestFactory().get( "/variant", {"variant_calls__sample": sample.pk} ) + force_authenticate(request, self.contributor) response = self.view(request) expected = VariantSerializer(self.variants[:1], many=True).data self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -831,6 +843,7 @@ def test_filter(self): request = APIRequestFactory().get( "/variant", {"variant_calls": self.calls[0].pk} ) + force_authenticate(request, self.contributor) response = self.view(request) expected = VariantSerializer(self.variants[:1], many=True).data self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -840,11 +853,14 @@ def test_filter(self): request = APIRequestFactory().get( "/variant", {"variant_calls__experiment": self.experiments[0].pk} ) + force_authenticate(request, self.contributor) response = self.view(request) expected = VariantSerializer(self.variants[:1], many=True).data self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertCountEqual(response.data, expected) + self.sample.set_permission(Permission.NONE, self.contributor) + def test_filter_sample_by_variant(self): client = APIClient() path = reverse("resolwebio-api:entity-list") @@ -870,8 +886,8 @@ def test_filter_sample_by_variant(self): path, {"variant_calls__variant": self.variants[0].pk} ) self.assertEqual(response.status_code, status.HTTP_200_OK) - response.data[0].pop("current_user_permissions") - self.assertEqual(response.data, [EntitySerializer(self.sample).data]) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]["id"], self.sample.pk) def test_ordering(self): """Test the Variant ordering.""" @@ -1143,6 +1159,47 @@ def test_filter(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertCountEqual(response.data, expected) + # Filter by sample. + request = APIRequestFactory().get("/variantcall", {"sample": self.sample.id}) + force_authenticate(request, self.contributor) + 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 data. + proc = Process.objects.create( + type="data:test:process", + slug="test-process", + version="1.0.0", + contributor=self.contributor, + ) + + data = Data.objects.create(contributor=self.contributor, process=proc) + self.calls[0].data = data + self.calls[0].save(update_fields=["data"]) + data.set_permission(Permission.NONE, self.contributor) + request = APIRequestFactory().get("/variantcall", {"data": data.id}) + force_authenticate(request, self.contributor) + expected = VariantCallSerializer(self.calls[:1], many=True).data + expected = [] + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, expected) + + data.set_permission(Permission.VIEW, self.contributor) + request = APIRequestFactory().get("/variantcall", {"data": data.id}) + force_authenticate(request, self.contributor) + 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) + + data.delete() + proc.delete() + self.calls[0].data = None + self.calls[0].save() + # Filter by id. request = APIRequestFactory().get("/variantcall", {"id": self.calls[0].id}) force_authenticate(request, self.contributor) @@ -1306,6 +1363,9 @@ def setUp(self) -> None: return super().setUp() def test_filter(self): + # Set the permission on sample. + self.sample.set_permission(Permission.EDIT, self.contributor) + # No filter. request = APIRequestFactory().get("/variantexperiment") expected = VariantExperimentSerializer(self.experiments, many=True).data @@ -1368,6 +1428,54 @@ def test_filter(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertCountEqual(response.data, expected) + # Filter by sample, no permissions on sample. + request = APIRequestFactory().get( + "/variantexperiment", {"variant_calls__sample": self.sample.id} + ) + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, []) + + # Filter by sample with permissions. + request = APIRequestFactory().get( + "/variantexperiment", {"variant_calls__sample": self.sample.id} + ) + force_authenticate(request, self.contributor) + 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) + + # No permissions to Sample object. + sample = Sample.objects.create(contributor=self.contributor, name="New sample") + self.calls[1].sample = sample + self.calls[1].save() + request = APIRequestFactory().get( + "/variantexperiment", {"variant_calls__sample": sample.id} + ) + force_authenticate(request, self.contributor) + expected = [] + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, []) + + # Add permissions to Sample object. + sample.set_permission(Permission.EDIT, self.contributor) + request = APIRequestFactory().get( + "/variantexperiment", {"variant_calls__sample": sample.id} + ) + force_authenticate(request, self.contributor) + 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) + + # Restore the test data. + self.calls[1].sample = self.sample + self.calls[1].save() + sample.delete() + self.sample.set_permission(Permission.NONE, self.contributor) + def test_ordering(self): # Order by id. request = APIRequestFactory().get("/variantexperiment", {"ordering": "id"})