-
Notifications
You must be signed in to change notification settings - Fork 49
/
searchconfig.py
262 lines (219 loc) · 8.04 KB
/
searchconfig.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# -*- coding: utf-8 -*-
#
# Copyright (C) 2022 CERN.
#
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.
"""Search app configuration helper."""
from copy import deepcopy
from flask import current_app
class SearchOptionsSelector:
"""Generic helper to select and validate facet/sort options."""
def __init__(self, available_options, selected_options):
"""Initialize selector."""
self.available_options = available_options
self.selected_options = selected_options
def __iter__(self):
"""Iterate over options to produce RSK options."""
for o in self.selected_options:
yield self.map_option(o, self.available_options[o])
def map_option(self, key, option):
"""Map an option."""
return (key, option)
class SortConfig(SearchOptionsSelector):
"""Sort options for the search configuration."""
def __init__(
self, available_options, selected_options, default=None, default_no_query=None
):
"""Initialize sort options."""
super().__init__(available_options, selected_options)
self.default = selected_options[0] if default is None else default
self.default_no_query = (
selected_options[1] if default_no_query is None else default_no_query
)
def map_option(self, key, option):
"""Generate a RSK search option."""
return {"sortBy": key, "text": option["title"]}
class FacetsConfig(SearchOptionsSelector):
"""Facets options for the search configuration."""
def map_option(self, key, option):
"""Generate an RSK aggregation option."""
title = option.get("title", option["facet"]._label)
ui = deepcopy(option["ui"])
ui.update(
{
"aggName": key,
"title": title,
}
)
# Nested facets
if "childAgg" in ui:
ui["childAgg"].setdefault("aggName", "inner")
ui["childAgg"].setdefault("title", title)
return ui
class SearchAppConfig:
"""Configuration generator for React-SearchKit."""
default_options = dict(
endpoint=None,
hidden_params=None,
app_id="search",
headers=None,
list_view=True,
grid_view=False,
pagination_options=(10, 20, 50),
default_size=10,
default_page=1,
default_max_results=10000,
facets=None,
sort=None,
initial_filters=[],
)
def __init__(self, configuration_options):
"""Initialize the search configuration.
:param endpoint: The URL path to the REST API.
:param hidden_params: Nested arrays containing any additional query
parameters to be used in the search.
:param app_id: The string ID of the Search Application.
:param headers: Dictionary containing additional headers to be included
in the request.
:param list_view: Boolean enabling the list view of the results.
:param grid_view: Boolean enabling the grid view of the results.
:param pagination_options: An integer array providing the results per
page options.
:param default_size: An integer setting the default number of results
per page.
:param default_page: An integer setting the default page.
:param default_max_results: An integer setting the default maximum total results.
"""
options = deepcopy(self.default_options)
options.update(configuration_options)
for key, value in options.items():
setattr(self, key, value)
@property
def appId(self):
"""The React appplication id."""
return self.app_id
@property
def initialQueryState(self):
"""Generate initialQueryState."""
return {
"hiddenParams": self.hidden_params,
"layout": "list" if self.list_view else "grid",
"size": self.default_size,
"sortBy": self.sort.default,
"page": self.default_page,
"filters": self.initial_filters,
}
@property
def searchApi(self):
"""Generate searchAPI configuration."""
return {
"axios": {
"url": self.endpoint,
"withCredentials": True,
"headers": self.headers,
},
"invenio": {
"requestSerializer": "InvenioRecordsResourcesRequestSerializer",
},
}
@property
def layoutOptions(self):
"""Generate the Layout Options.
:returns: A dict with the options for React-SearchKit JS.
"""
return {"listView": self.list_view, "gridView": self.grid_view}
@property
def sortOptions(self):
"""Format sort options to be used in React-SearchKit JS.
:returns: A list of dicts with sorting options for React-SearchKit JS.
"""
return list(self.sort) if self.sort is not None else []
@property
def aggs(self):
"""Format the aggs configuration to be used in React-SearchKit JS.
:returns: A list of dicts for React-SearchKit JS.
"""
return list(self.facets) if self.facets is not None else []
@property
def paginationOptions(self):
"""Format the pagination options to be used in React-SearchKit JS."""
if (
not getattr(self, "default_size")
or self.default_size not in self.pagination_options
):
raise ValueError(
"Parameter default_size should be part of pagination_options"
)
return {
"resultsPerPage": [
{"text": str(option), "value": option}
for option in self.pagination_options
],
"defaultValue": self.default_size,
"maxTotalResults": self.default_max_results,
}
@property
def defaultSortingOnEmptyQueryString(self):
"""Defines the default sorting options when there is no query."""
return {
"sortBy": self.sort.default_no_query,
}
@classmethod
def generate(cls, options, **kwargs):
"""Create JSON config for React-Searchkit."""
generator_object = cls(options)
config = {
"appId": generator_object.appId,
"initialQueryState": generator_object.initialQueryState,
"searchApi": generator_object.searchApi,
"sortOptions": generator_object.sortOptions,
"aggs": generator_object.aggs,
"layoutOptions": generator_object.layoutOptions,
"sortOrderDisabled": True,
"paginationOptions": generator_object.paginationOptions,
"defaultSortingOnEmptyQueryString": (
generator_object.defaultSortingOnEmptyQueryString,
),
}
config.update(kwargs)
return config
#
# Application state context generators, to be used in context processors
#
def sort_config(config_name, sort_options):
"""Sort configuration."""
return SortConfig(
sort_options,
current_app.config[config_name].get("sort", []),
current_app.config[config_name].get("sort_default", None),
current_app.config[config_name].get("sort_default_no_query"),
)
def facets_config(config_name, available_facets):
"""Facets configuration."""
return FacetsConfig(
available_facets, current_app.config[config_name].get("facets", [])
)
def search_app_config(
config_name,
available_facets,
sort_options,
endpoint,
headers,
overrides=None,
**kwargs
):
"""Search app config.
Generates search app config expected by React-Searchkit with
InvenioRecordsResource config.
"""
opts = dict(
endpoint=endpoint,
headers=headers,
grid_view=False,
sort=sort_config(config_name, sort_options),
facets=facets_config(config_name, available_facets),
)
opts.update(kwargs)
overrides = overrides or {}
return SearchAppConfig.generate(opts, **overrides)