funkwhale/api/funkwhale_api/radios/filters.py

229 wiersze
6.9 KiB
Python
Czysty Zwykły widok Historia

2018-01-07 21:13:32 +00:00
import collections
2018-06-10 08:55:16 +00:00
import persisting_theory
2018-01-07 21:13:32 +00:00
from django.core.exceptions import ValidationError
from django.db.models import Q, functions
2018-01-07 21:13:32 +00:00
from django.urls import reverse_lazy
from funkwhale_api.music import models
class RadioFilterRegistry(persisting_theory.Registry):
def prepare_data(self, data):
return data()
def prepare_name(self, data, name=None):
return data.code
@property
def exposed_filters(self):
2018-06-09 13:36:16 +00:00
return [f for f in self.values() if f.expose_in_api]
2018-01-07 21:13:32 +00:00
registry = RadioFilterRegistry()
def run(filters, **kwargs):
2018-06-09 13:36:16 +00:00
candidates = kwargs.pop("candidates", models.Track.objects.all())
2018-01-07 21:13:32 +00:00
final_query = None
2018-06-09 13:36:16 +00:00
final_query = registry["group"].get_query(candidates, filters=filters, **kwargs)
2018-01-07 21:13:32 +00:00
if final_query:
candidates = candidates.filter(final_query)
return candidates.order_by("pk").distinct()
2018-01-07 21:13:32 +00:00
def validate(filter_config):
try:
2018-06-09 13:36:16 +00:00
f = registry[filter_config["type"]]
2018-01-07 21:13:32 +00:00
except KeyError:
2018-06-09 13:36:16 +00:00
raise ValidationError('Invalid type "{}"'.format(filter_config["type"]))
2018-01-07 21:13:32 +00:00
f.validate(filter_config)
return True
def test(filter_config, **kwargs):
"""
Run validation and also gather the candidates for the given config
"""
2018-06-09 13:36:16 +00:00
data = {"errors": [], "candidates": {"count": None, "sample": None}}
2018-01-07 21:13:32 +00:00
try:
validate(filter_config)
except ValidationError as e:
2018-06-09 13:36:16 +00:00
data["errors"] = [e.message]
2018-01-07 21:13:32 +00:00
return data
candidates = run([filter_config], **kwargs)
2018-06-09 13:36:16 +00:00
data["candidates"]["count"] = candidates.count()
data["candidates"]["sample"] = candidates[:10]
2018-01-07 21:13:32 +00:00
return data
def clean_config(filter_config):
2018-06-09 13:36:16 +00:00
f = registry[filter_config["type"]]
2018-01-07 21:13:32 +00:00
return f.clean_config(filter_config)
class RadioFilter(object):
help_text = None
label = None
fields = []
expose_in_api = True
def get_query(self, candidates, **kwargs):
return candidates
def clean_config(self, filter_config):
return filter_config
def validate(self, config):
2018-06-09 13:36:16 +00:00
operator = config.get("operator", "and")
2018-01-07 21:13:32 +00:00
try:
2018-06-09 13:36:16 +00:00
assert operator in ["or", "and"]
2018-01-07 21:13:32 +00:00
except AssertionError:
2018-06-09 13:36:16 +00:00
raise ValidationError('Invalid operator "{}"'.format(config["operator"]))
2018-01-07 21:13:32 +00:00
@registry.register
class GroupFilter(RadioFilter):
2018-06-09 13:36:16 +00:00
code = "group"
2018-01-07 21:13:32 +00:00
expose_in_api = False
2018-06-09 13:36:16 +00:00
2018-01-07 21:13:32 +00:00
def get_query(self, candidates, filters, **kwargs):
if not filters:
return
final_query = None
for filter_config in filters:
2018-06-09 13:36:16 +00:00
f = registry[filter_config["type"]]
2018-01-07 21:13:32 +00:00
conf = collections.ChainMap(filter_config, kwargs)
query = f.get_query(candidates, **conf)
2018-06-09 13:36:16 +00:00
if filter_config.get("not", False):
# query = ~query *should* work but it doesn't (see #950)
# The line below generate a proper subquery
query = ~Q(pk__in=candidates.filter(query).values_list("pk", flat=True))
2018-01-07 21:13:32 +00:00
if not final_query:
final_query = query
else:
2018-06-09 13:36:16 +00:00
operator = filter_config.get("operator", "and")
if operator == "and":
2018-01-07 21:13:32 +00:00
final_query &= query
2018-06-09 13:36:16 +00:00
elif operator == "or":
2018-01-07 21:13:32 +00:00
final_query |= query
else:
2018-06-09 13:36:16 +00:00
raise ValueError('Invalid query operator "{}"'.format(operator))
2018-01-07 21:13:32 +00:00
return final_query
def validate(self, config):
super().validate(config)
2018-06-09 13:36:16 +00:00
for fc in config["filters"]:
registry[fc["type"]].validate(fc)
2018-01-07 21:13:32 +00:00
@registry.register
class ArtistFilter(RadioFilter):
2018-06-09 13:36:16 +00:00
code = "artist"
label = "Artist"
help_text = "Select tracks for a given artist"
2018-01-07 21:13:32 +00:00
fields = [
{
2018-06-09 13:36:16 +00:00
"name": "ids",
"type": "list",
"subtype": "number",
"autocomplete": reverse_lazy("api:v1:search"),
2018-06-09 13:36:16 +00:00
"autocomplete_qs": "q={query}",
"autocomplete_fields": {
"remoteValues": "artists",
"name": "name",
"value": "id",
},
2018-06-09 13:36:16 +00:00
"label": "Artist",
"placeholder": "Select artists",
2018-01-07 21:13:32 +00:00
}
]
def clean_config(self, filter_config):
filter_config = super().clean_config(filter_config)
2018-06-09 13:36:16 +00:00
filter_config["ids"] = sorted(filter_config["ids"])
names = (
models.Artist.objects.filter(pk__in=filter_config["ids"])
.annotate(__size=functions.Length("name"))
.order_by("__size", "id")
2018-06-09 13:36:16 +00:00
.values_list("name", flat=True)
)
filter_config["names"] = list(names)
2018-01-07 21:13:32 +00:00
return filter_config
def get_query(self, candidates, ids, **kwargs):
return Q(artist__pk__in=ids)
def validate(self, config):
super().validate(config)
try:
2018-06-09 13:36:16 +00:00
pks = models.Artist.objects.filter(pk__in=config["ids"]).values_list(
"pk", flat=True
)
diff = set(config["ids"]) - set(pks)
2018-01-07 21:13:32 +00:00
assert len(diff) == 0
except KeyError:
2018-06-09 13:36:16 +00:00
raise ValidationError("You must provide an id")
2018-01-07 21:13:32 +00:00
except AssertionError:
2018-06-09 13:36:16 +00:00
raise ValidationError('No artist matching ids "{}"'.format(diff))
2018-01-07 21:13:32 +00:00
@registry.register
class TagFilter(RadioFilter):
2018-06-09 13:36:16 +00:00
code = "tag"
2018-01-07 21:13:32 +00:00
fields = [
{
2018-06-09 13:36:16 +00:00
"name": "names",
"type": "list",
"subtype": "string",
"autocomplete": reverse_lazy("api:v1:search"),
2018-06-09 13:36:16 +00:00
"autocomplete_fields": {
"remoteValues": "tags",
2018-06-09 13:36:16 +00:00
"name": "name",
"value": "name",
2018-06-09 13:36:16 +00:00
},
"autocomplete_qs": "q={query}",
2018-06-09 13:36:16 +00:00
"label": "Tags",
"placeholder": "Select tags",
2018-01-07 21:13:32 +00:00
}
]
2018-06-09 13:36:16 +00:00
help_text = "Select tracks with a given tag"
label = "Tag"
2018-01-07 21:13:32 +00:00
def get_query(self, candidates, names, **kwargs):
return (
Q(tagged_items__tag__name__in=names)
| Q(artist__tagged_items__tag__name__in=names)
| Q(album__tagged_items__tag__name__in=names)
)
def clean_config(self, filter_config):
filter_config = super().clean_config(filter_config)
filter_config["names"] = sorted(filter_config["names"])
names = (
models.tags_models.Tag.objects.filter(name__in=filter_config["names"])
.annotate(__size=functions.Length("name"))
.order_by("__size", "pk")
.values_list("name", flat=True)
)
filter_config["names"] = list(names)
return filter_config
def validate(self, config):
super().validate(config)
try:
names = models.tags_models.Tag.objects.filter(
name__in=config["names"]
).values_list("name", flat=True)
diff = set(config["names"]) - set(names)
assert len(diff) == 0
except KeyError:
raise ValidationError("You must provide a name")
except AssertionError:
raise ValidationError('No tag matching names "{}"'.format(diff))