filters.py 5.75 KB
Newer Older
1
2
import collections

Eliot Berriot's avatar
Eliot Berriot committed
3
import persisting_theory
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from django.core.exceptions import ValidationError
from django.db.models import Q
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):
Eliot Berriot's avatar
Eliot Berriot committed
20
        return [f for f in self.values() if f.expose_in_api]
21
22
23
24
25
26


registry = RadioFilterRegistry()


def run(filters, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
27
    candidates = kwargs.pop("candidates", models.Track.objects.all())
28
    final_query = None
Eliot Berriot's avatar
Eliot Berriot committed
29
    final_query = registry["group"].get_query(candidates, filters=filters, **kwargs)
30
31
32

    if final_query:
        candidates = candidates.filter(final_query)
33
    return candidates.order_by("pk").distinct()
34
35
36
37


def validate(filter_config):
    try:
Eliot Berriot's avatar
Eliot Berriot committed
38
        f = registry[filter_config["type"]]
39
    except KeyError:
Eliot Berriot's avatar
Eliot Berriot committed
40
        raise ValidationError('Invalid type "{}"'.format(filter_config["type"]))
41
42
43
44
45
46
47
48
    f.validate(filter_config)
    return True


def test(filter_config, **kwargs):
    """
    Run validation and also gather the candidates for the given config
    """
Eliot Berriot's avatar
Eliot Berriot committed
49
    data = {"errors": [], "candidates": {"count": None, "sample": None}}
50
51
52
    try:
        validate(filter_config)
    except ValidationError as e:
Eliot Berriot's avatar
Eliot Berriot committed
53
        data["errors"] = [e.message]
54
55
56
        return data

    candidates = run([filter_config], **kwargs)
Eliot Berriot's avatar
Eliot Berriot committed
57
58
    data["candidates"]["count"] = candidates.count()
    data["candidates"]["sample"] = candidates[:10]
59
60
61
62
63

    return data


def clean_config(filter_config):
Eliot Berriot's avatar
Eliot Berriot committed
64
    f = registry[filter_config["type"]]
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
    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):
Eliot Berriot's avatar
Eliot Berriot committed
81
        operator = config.get("operator", "and")
82
        try:
Eliot Berriot's avatar
Eliot Berriot committed
83
            assert operator in ["or", "and"]
84
        except AssertionError:
Eliot Berriot's avatar
Eliot Berriot committed
85
            raise ValidationError('Invalid operator "{}"'.format(config["operator"]))
86
87
88
89


@registry.register
class GroupFilter(RadioFilter):
Eliot Berriot's avatar
Eliot Berriot committed
90
    code = "group"
91
    expose_in_api = False
Eliot Berriot's avatar
Eliot Berriot committed
92

93
94
95
96
97
98
    def get_query(self, candidates, filters, **kwargs):
        if not filters:
            return

        final_query = None
        for filter_config in filters:
Eliot Berriot's avatar
Eliot Berriot committed
99
            f = registry[filter_config["type"]]
100
101
            conf = collections.ChainMap(filter_config, kwargs)
            query = f.get_query(candidates, **conf)
Eliot Berriot's avatar
Eliot Berriot committed
102
            if filter_config.get("not", False):
103
104
105
                # 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))
106
107
108
109

            if not final_query:
                final_query = query
            else:
Eliot Berriot's avatar
Eliot Berriot committed
110
111
                operator = filter_config.get("operator", "and")
                if operator == "and":
112
                    final_query &= query
Eliot Berriot's avatar
Eliot Berriot committed
113
                elif operator == "or":
114
115
                    final_query |= query
                else:
Eliot Berriot's avatar
Eliot Berriot committed
116
                    raise ValueError('Invalid query operator "{}"'.format(operator))
117
118
119
120
        return final_query

    def validate(self, config):
        super().validate(config)
Eliot Berriot's avatar
Eliot Berriot committed
121
122
        for fc in config["filters"]:
            registry[fc["type"]].validate(fc)
123
124
125
126


@registry.register
class ArtistFilter(RadioFilter):
Eliot Berriot's avatar
Eliot Berriot committed
127
128
129
    code = "artist"
    label = "Artist"
    help_text = "Select tracks for a given artist"
130
131
    fields = [
        {
Eliot Berriot's avatar
Eliot Berriot committed
132
133
134
135
136
137
138
139
            "name": "ids",
            "type": "list",
            "subtype": "number",
            "autocomplete": reverse_lazy("api:v1:artists-list"),
            "autocomplete_qs": "q={query}",
            "autocomplete_fields": {"name": "name", "value": "id"},
            "label": "Artist",
            "placeholder": "Select artists",
140
141
142
143
144
        }
    ]

    def clean_config(self, filter_config):
        filter_config = super().clean_config(filter_config)
Eliot Berriot's avatar
Eliot Berriot committed
145
146
147
148
149
150
151
        filter_config["ids"] = sorted(filter_config["ids"])
        names = (
            models.Artist.objects.filter(pk__in=filter_config["ids"])
            .order_by("id")
            .values_list("name", flat=True)
        )
        filter_config["names"] = list(names)
152
153
154
155
156
157
158
159
        return filter_config

    def get_query(self, candidates, ids, **kwargs):
        return Q(artist__pk__in=ids)

    def validate(self, config):
        super().validate(config)
        try:
Eliot Berriot's avatar
Eliot Berriot committed
160
161
162
163
            pks = models.Artist.objects.filter(pk__in=config["ids"]).values_list(
                "pk", flat=True
            )
            diff = set(config["ids"]) - set(pks)
164
165
            assert len(diff) == 0
        except KeyError:
Eliot Berriot's avatar
Eliot Berriot committed
166
            raise ValidationError("You must provide an id")
167
        except AssertionError:
Eliot Berriot's avatar
Eliot Berriot committed
168
            raise ValidationError('No artist matching ids "{}"'.format(diff))
169
170
171
172


@registry.register
class TagFilter(RadioFilter):
Eliot Berriot's avatar
Eliot Berriot committed
173
    code = "tag"
174
175
    fields = [
        {
Eliot Berriot's avatar
Eliot Berriot committed
176
177
178
179
180
181
182
            "name": "names",
            "type": "list",
            "subtype": "string",
            "autocomplete": reverse_lazy("api:v1:tags-list"),
            "autocomplete_fields": {
                "remoteValues": "results",
                "name": "name",
183
                "value": "name",
Eliot Berriot's avatar
Eliot Berriot committed
184
            },
185
            "autocomplete_qs": "q={query}&ordering=length",
Eliot Berriot's avatar
Eliot Berriot committed
186
187
            "label": "Tags",
            "placeholder": "Select tags",
188
189
        }
    ]
Eliot Berriot's avatar
Eliot Berriot committed
190
191
    help_text = "Select tracks with a given tag"
    label = "Tag"
192
193

    def get_query(self, candidates, names, **kwargs):
194
195
196
197
198
        return (
            Q(tagged_items__tag__name__in=names)
            | Q(artist__tagged_items__tag__name__in=names)
            | Q(album__tagged_items__tag__name__in=names)
        )