Skip to content

Commit

Permalink
Allow filtering variant experiment by sample
Browse files Browse the repository at this point in the history
  • Loading branch information
gregorjerse committed Nov 26, 2024
1 parent 74730fc commit 5499c32
Show file tree
Hide file tree
Showing 3 changed files with 224 additions and 53 deletions.
13 changes: 9 additions & 4 deletions docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ All notable changes to this project are documented in this file.
This project adheres to `Semantic Versioning <http://semver.org/>`_.


==========
Unreleased
==========

Added
-----
- Add filtering ``Variant`` by ``VariantExperiment`` and ``VariantCall``


===================
61.0.0 - 2024-11-21
===================
Expand All @@ -20,10 +29,6 @@ Changed
60.0.0 - 2024-11-19
===================

Added
-----
- Add filtering ``Variant`` by ``VariantExperiment`` and ``VariantCall``

Changed
-------
- **BACKWARD INCOMPATIBLE:** Require Resolwe 41.x
Expand Down
189 changes: 144 additions & 45 deletions resolwe_bio/variants/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
"""

import types
from copy import deepcopy
from functools import partial

import django_filters as filters
from django.db import models
from django_filters.constants import EMPTY_VALUES

from resolwe.flow.filters import (
DATETIME_LOOKUPS,
Expand All @@ -24,9 +30,139 @@
)


class VariantFilter(CheckQueryParamsMixin, filters.FilterSet):
class FilterWithPermissionsMeta(filters.filterset.FilterSetMetaclass):
"""Add all variant_calls filters prefixed with 'variant_calls'."""

def __new__(mcs, name, bases, namespace):
"""Inject filters with permissions."""

def _add_filters_with_permissions(
prefix: str,
BaseModel: models.Model,
BaseFilter: filters.FilterSet,
include_fields: list[str],
):
"""Add filters on related objects with permissions.
Consider all filters from BaseFilter filterset and add the one for fields
in include_fields list.
"""

def filter_permissions(
self,
qs: models.QuerySet,
value: str,
original_filter: filters.Filter,
original_model: models.Model,
):
"""Respect permissions on entities."""

# Do not filter when value is empty. At least one of the values must be
# non-empty since form in the AnnotationValueFilter class requires it.
if value in EMPTY_VALUES:
return qs

visible_objects = list(
original_filter.filter(original_model.objects.all(), value)
.filter_for_user(self.parent.request.user)
.values_list("pk", flat=True)
)
return qs.filter(**{f"{prefix}__in": visible_objects})

# Add all filters from EntityFilter to namespaces before creating class.
for filter_name, filter in BaseFilter.get_filters().items():
if filter.field_name not in include_fields:
continue

new_filter_name = f"{prefix}__{filter_name}"
if filter_name == "id" or filter_name.startswith("id__"):
new_filter_name = f"{prefix}{filter_name[2:]}"

filter_copy = deepcopy(filter)
filter_copy.field_name = f"{prefix}__{filter_copy.field_name}"
filter_method = partial(
filter_permissions, original_filter=filter, original_model=BaseModel
)
# Bind the new_filter to filter instance and set it as new filter.
filter_copy.filter = types.MethodType(filter_method, filter_copy)
namespace[new_filter_name] = filter_copy
# If filter uses a method, add it to the namespace as well.
if filter_copy.method is not None:
namespace[filter_copy.method] = deepcopy(
getattr(BaseFilter, filter_copy.method)
)

for prefix, base_model, base_filter, included_fields in namespace.get(
"permission_filters", []
):
_add_filters_with_permissions(
prefix, base_model, base_filter, included_fields
)

# Create class with added filters.
klass = super().__new__(mcs, name, bases, namespace)
return klass


class VariantCallFilter(
CheckQueryParamsMixin, filters.FilterSet, metaclass=FilterWithPermissionsMeta
):
"""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 VariantFilter(
CheckQueryParamsMixin, filters.FilterSet, metaclass=FilterWithPermissionsMeta
):
"""Filter the Variant objects endpoint."""

permission_filters = [
(
"variant_calls",
VariantCall,
VariantCallFilter,
[
"id",
"sample",
"sample__slug",
"quality",
"depth",
"experiment",
"filter",
"genotype",
"genotype_quality",
"alternative_allele_depth",
"depth_norm_quality",
],
)
]

class Meta:
"""Filter configuration."""

Expand All @@ -51,18 +187,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
Expand Down Expand Up @@ -94,40 +218,15 @@ 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(
CheckQueryParamsMixin, filters.FilterSet, metaclass=FilterWithPermissionsMeta
):
"""Filter the VariantExperiment objects endpoint."""

permission_filters = [
("variant_calls", VariantCall, VariantCallFilter, ["id", "sample"])
]

class Meta:
"""Filter configuration."""

Expand Down
Loading

0 comments on commit 5499c32

Please sign in to comment.