search.py 12.2 KB
Newer Older
1
2
from typing import Set

3
4
from sqlalchemy import and_, case, or_, orm

5
from aurweb import db, models
6
from aurweb.models import Package, PackageBase, User
7
from aurweb.models.dependency_type import CHECKDEPENDS_ID, DEPENDS_ID, MAKEDEPENDS_ID, OPTDEPENDS_ID
8
9
10
11
from aurweb.models.package_comaintainer import PackageComaintainer
from aurweb.models.package_keyword import PackageKeyword
from aurweb.models.package_notification import PackageNotification
from aurweb.models.package_vote import PackageVote
12
13
14


class PackageSearch:
15
    """A Package search query builder."""
16
17
18
19

    # A constant mapping of short to full name sort orderings.
    FULL_SORT_ORDER = {"d": "desc", "a": "asc"}

20
    def __init__(self, user: models.User = None):
21
        self.query = db.query(Package).join(PackageBase)
22

23
        self.user = user
24
25
        if self.user:
            self.query = self.query.join(
26
                PackageVote,
27
28
29
30
31
                and_(
                    PackageVote.PackageBaseID == PackageBase.ID,
                    PackageVote.UsersID == self.user.ID,
                ),
                isouter=True,
32
33
            ).join(
                PackageNotification,
34
35
36
37
38
                and_(
                    PackageNotification.PackageBaseID == PackageBase.ID,
                    PackageNotification.UserID == self.user.ID,
                ),
                isouter=True,
39
            )
40

41
42
43
44
45
46
47
48
49
50
51
52
53
        self.ordering = "d"

        # Setup SeB (Search By) callbacks.
        self.search_by_cb = {
            "nd": self._search_by_namedesc,
            "n": self._search_by_name,
            "b": self._search_by_pkgbase,
            "N": self._search_by_exact_name,
            "B": self._search_by_exact_pkgbase,
            "k": self._search_by_keywords,
            "m": self._search_by_maintainer,
            "c": self._search_by_comaintainer,
            "M": self._search_by_co_or_maintainer,
54
            "s": self._search_by_submitter,
55
56
57
58
59
60
61
62
63
64
        }

        # Setup SB (Sort By) callbacks.
        self.sort_by_cb = {
            "n": self._sort_by_name,
            "v": self._sort_by_votes,
            "p": self._sort_by_popularity,
            "w": self._sort_by_voted,
            "o": self._sort_by_notify,
            "m": self._sort_by_maintainer,
65
            "l": self._sort_by_last_modified,
66
67
        }

68
69
        self._joined_user = False
        self._joined_keywords = False
70
        self._joined_comaint = False
71

72
    def _join_user(self, outer: bool = True) -> orm.Query:
73
        """Centralized joining of a package base's maintainer."""
74
        if not self._joined_user:
75
            self.query = self.query.join(
76
                User, User.ID == PackageBase.MaintainerUID, isouter=outer
77
            )
78
            self._joined_user = True
79
        return self.query
80
81
82
83
84

    def _join_keywords(self) -> orm.Query:
        if not self._joined_keywords:
            self.query = self.query.join(PackageKeyword)
            self._joined_keywords = True
85
86
87
88
89
90
91
        return self.query

    def _join_comaint(self, isouter: bool = False) -> orm.Query:
        if not self._joined_comaint:
            self.query = self.query.join(
                PackageComaintainer,
                PackageComaintainer.PackageBaseID == PackageBase.ID,
92
                isouter=isouter,
93
94
95
            )
            self._joined_comaint = True
        return self.query
96

97
    def _search_by_namedesc(self, keywords: str) -> orm.Query:
98
        self._join_user()
99
        self.query = self.query.filter(
100
101
102
103
            or_(
                Package.Name.like(f"%{keywords}%"),
                Package.Description.like(f"%{keywords}%"),
            )
104
105
106
107
        )
        return self

    def _search_by_name(self, keywords: str) -> orm.Query:
108
109
        self._join_user()
        self.query = self.query.filter(Package.Name.like(f"%{keywords}%"))
110
111
112
        return self

    def _search_by_exact_name(self, keywords: str) -> orm.Query:
113
114
        self._join_user()
        self.query = self.query.filter(Package.Name == keywords)
115
116
117
        return self

    def _search_by_pkgbase(self, keywords: str) -> orm.Query:
118
119
        self._join_user()
        self.query = self.query.filter(PackageBase.Name.like(f"%{keywords}%"))
120

121
122
123
        return self

    def _search_by_exact_pkgbase(self, keywords: str) -> orm.Query:
124
125
        self._join_user()
        self.query = self.query.filter(PackageBase.Name == keywords)
126
127
        return self

128
    def _search_by_keywords(self, keywords: Set[str]) -> orm.Query:
129
        self._join_user()
130
        self._join_keywords()
131
        keywords = set(k.lower() for k in keywords)
132
        self.query = self.query.filter(PackageKeyword.Keyword.in_(keywords))
133
134
135
        return self

    def _search_by_maintainer(self, keywords: str) -> orm.Query:
136
        self._join_user()
137
138
        if keywords:
            self.query = self.query.filter(
139
                and_(User.Username == keywords, User.ID == PackageBase.MaintainerUID)
140
141
142
            )
        else:
            self.query = self.query.filter(PackageBase.MaintainerUID.is_(None))
143
144
145
        return self

    def _search_by_comaintainer(self, keywords: str) -> orm.Query:
146
        self._join_user()
147
        self._join_comaint()
148
149
        user = db.query(User).filter(User.Username == keywords).first()
        uid = 0 if not user else user.ID
150
        self.query = self.query.filter(PackageComaintainer.UsersID == uid)
151
152
153
        return self

    def _search_by_co_or_maintainer(self, keywords: str) -> orm.Query:
154
        self._join_user()
155
        self._join_comaint(True)
156
157
        user = db.query(User).filter(User.Username == keywords).first()
        uid = 0 if not user else user.ID
158
159
160
        self.query = self.query.filter(
            or_(PackageComaintainer.UsersID == uid, User.ID == uid)
        )
161
162
163
        return self

    def _search_by_submitter(self, keywords: str) -> orm.Query:
164
165
166
167
168
169
170
171
        self._join_user()

        uid = 0
        user = db.query(User).filter(User.Username == keywords).first()
        if user:
            uid = user.ID

        self.query = self.query.filter(PackageBase.SubmitterUID == uid)
172
173
174
175
176
177
178
179
180
181
        return self

    def search_by(self, search_by: str, keywords: str) -> orm.Query:
        if search_by not in self.search_by_cb:
            search_by = "nd"  # Default: Name, Description
        callback = self.search_by_cb.get(search_by)
        result = callback(keywords)
        return result

    def _sort_by_name(self, order: str):
182
        column = getattr(models.Package.Name, order)
183
184
185
186
        self.query = self.query.order_by(column())
        return self

    def _sort_by_votes(self, order: str):
187
        column = getattr(models.PackageBase.NumVotes, order)
188
189
        name = getattr(models.Package.Name, order)
        self.query = self.query.order_by(column(), name())
190
191
192
        return self

    def _sort_by_popularity(self, order: str):
193
        column = getattr(models.PackageBase.Popularity, order)
194
195
        name = getattr(models.Package.Name, order)
        self.query = self.query.order_by(column(), name())
196
197
198
199
200
201
202
        return self

    def _sort_by_voted(self, order: str):
        # FIXME: Currently, PHP is destroying this implementation
        # in terms of performance. We should improve this; there's no
        # reason it should take _longer_.
        column = getattr(
203
            case([(models.PackageVote.UsersID == self.user.ID, 1)], else_=0), order
204
        )
205
206
        name = getattr(models.Package.Name, order)
        self.query = self.query.order_by(column(), name())
207
208
209
210
211
212
213
        return self

    def _sort_by_notify(self, order: str):
        # FIXME: Currently, PHP is destroying this implementation
        # in terms of performance. We should improve this; there's no
        # reason it should take _longer_.
        column = getattr(
214
215
            case([(models.PackageNotification.UserID == self.user.ID, 1)], else_=0),
            order,
216
        )
217
218
        name = getattr(models.Package.Name, order)
        self.query = self.query.order_by(column(), name())
219
220
221
        return self

    def _sort_by_maintainer(self, order: str):
222
        column = getattr(models.User.Username, order)
223
224
        name = getattr(models.Package.Name, order)
        self.query = self.query.order_by(column(), name())
225
226
227
        return self

    def _sort_by_last_modified(self, order: str):
228
        column = getattr(models.PackageBase.ModifiedTS, order)
229
230
        name = getattr(models.Package.Name, order)
        self.query = self.query.order_by(column(), name())
231
232
233
234
        return self

    def sort_by(self, sort_by: str, ordering: str = "d") -> orm.Query:
        if sort_by not in self.sort_by_cb:
235
            sort_by = "p"  # Default: Popularity
236
237
        callback = self.sort_by_cb.get(sort_by)
        if ordering not in self.FULL_SORT_ORDER:
238
            ordering = "d"  # Default: Descending
239
240
241
        ordering = self.FULL_SORT_ORDER.get(ordering)
        return callback(ordering)

242
    def count(self) -> int:
243
        """Return internal query's count."""
244
        return self.query.count()
245
246

    def results(self) -> orm.Query:
247
        """Return internal query."""
248
        return self.query
249
250
251


class RPCSearch(PackageSearch):
252
    """A PackageSearch-derived RPC package search query builder.
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272

    With RPC search, we need a subset of PackageSearch's handlers,
    with a few additional handlers added. So, within the RPCSearch
    constructor, we pop unneeded keys out of inherited self.search_by_cb
    and add a few more keys to it, namely: depends, makedepends,
    optdepends and checkdepends.

    Additionally, some logic within the inherited PackageSearch.search_by
    method is not needed, so it is overridden in this class without
    sanitization done for the PackageSearch `by` argument.
    """

    keys_removed = ("b", "N", "B", "k", "c", "M", "s")

    def __init__(self) -> "RPCSearch":
        super().__init__()

        # Fix-up inherited search_by_cb to reflect RPC-specific by params.
        # We keep: "nd", "n" and "m". We also overlay four new by params
        # on top: "depends", "makedepends", "optdepends" and "checkdepends".
273
        self.search_by_cb = {
274
275
            k: v
            for k, v in self.search_by_cb.items()
276
277
            if k not in RPCSearch.keys_removed
        }
278
279
280
281
282
283
284
285
        self.search_by_cb.update(
            {
                "depends": self._search_by_depends,
                "makedepends": self._search_by_makedepends,
                "optdepends": self._search_by_optdepends,
                "checkdepends": self._search_by_checkdepends,
            }
        )
286

287
288
289
        # We always want an optional Maintainer in the RPC.
        self._join_user()

290
    def _join_depends(self, dep_type_id: int) -> orm.Query:
291
        """Join Package with PackageDependency and filter results
292
293
294
295
296
297
        based on `dep_type_id`.

        :param dep_type_id: DependencyType ID
        :returns: PackageDependency-joined orm.Query
        """
        self.query = self.query.join(models.PackageDependency).filter(
298
299
            models.PackageDependency.DepTypeID == dep_type_id
        )
300
301
302
303
        return self.query

    def _search_by_depends(self, keywords: str) -> "RPCSearch":
        self.query = self._join_depends(DEPENDS_ID).filter(
304
305
            models.PackageDependency.DepName == keywords
        )
306
307
308
309
        return self

    def _search_by_makedepends(self, keywords: str) -> "RPCSearch":
        self.query = self._join_depends(MAKEDEPENDS_ID).filter(
310
311
            models.PackageDependency.DepName == keywords
        )
312
313
314
315
        return self

    def _search_by_optdepends(self, keywords: str) -> "RPCSearch":
        self.query = self._join_depends(OPTDEPENDS_ID).filter(
316
317
            models.PackageDependency.DepName == keywords
        )
318
319
320
321
        return self

    def _search_by_checkdepends(self, keywords: str) -> "RPCSearch":
        self.query = self._join_depends(CHECKDEPENDS_ID).filter(
322
323
            models.PackageDependency.DepName == keywords
        )
324
325
326
        return self

    def search_by(self, by: str, keywords: str) -> "RPCSearch":
327
        """Override inherited search_by. In this override, we reduce the
328
329
330
331
332
333
334
335
336
337
338
339
340
        scope of what we handle within this function. We do not set `by`
        to a default of "nd" in the RPC, as the RPC returns an error when
        incorrect `by` fields are specified.

        :param by: RPC `by` argument
        :param keywords: RPC `arg` argument
        :returns: self
        """
        callback = self.search_by_cb.get(by)
        result = callback(keywords)
        return result

    def results(self) -> orm.Query:
341
        return self.query.filter(models.PackageBase.PackagerUID.isnot(None))