Revision 5a845c93
Tri des événements dans VigiBoard (#1186)
Ajoute la possibilité de trier sur les colonnes de VigiBoard.
Change-Id: Ibd7e5c71dfd6a9d81be3846942b19d7dc067b397
Refs: #1186
Reviewed-on: https://vigilo-dev.si.c-s.fr/review/1282
Tested-by: Build system <qa@vigilo-dev.si.c-s.fr>
Reviewed-by: Thomas BURGUIERE <thomas.burguiere@c-s.fr>
vigiboard/controllers/plugins/__init__.py | ||
---|---|---|
84 | 84 |
def get_search_fields(self): |
85 | 85 |
return [] |
86 | 86 |
|
87 |
def get_sort_criterion(self, query, column): |
|
88 |
""" |
|
89 |
Cette méthode renvoie le critère à utiliser par SQLAlchemy pour trier |
|
90 |
la requête alimentant le tableau des événements. |
|
91 |
|
|
92 |
Cette méthode DEVRAIT être surchargée dans les classes dérivées |
|
93 |
si le plugin en question implémente un tri. |
|
94 |
|
|
95 |
@param query: La requête VigiBoard servant à alimenter le tableau des |
|
96 |
événements. |
|
97 |
@type query: L{VigiboardRequest} |
|
98 |
@param column: La colonne sur laquelle l'utilisateur souhaite opérer le |
|
99 |
tri. |
|
100 |
@type column: C{str} |
|
101 |
""" |
|
102 |
pass |
|
103 |
|
|
87 | 104 |
def handle_search_fields(self, query, search, state, subqueries): |
88 | 105 |
pass |
vigiboard/controllers/plugins/date.py | ||
---|---|---|
105 | 105 |
'date': event[0].cause.timestamp, |
106 | 106 |
'duration': duration, |
107 | 107 |
} |
108 |
|
|
109 |
def get_sort_criterion(self, query, column): |
|
110 |
if column == 'date': |
|
111 |
return tables.Event.timestamp |
|
112 |
return None |
|
113 |
|
vigiboard/controllers/plugins/hls.py | ||
---|---|---|
100 | 100 |
@rtype: C{dict} |
101 | 101 |
""" |
102 | 102 |
|
103 |
if not events_ids: |
|
104 |
return {} |
|
105 |
|
|
103 | 106 |
imp_hls1 = aliased(tables.ImpactedHLS) |
104 | 107 |
imp_hls2 = aliased(tables.ImpactedHLS) |
105 | 108 |
|
vigiboard/controllers/plugins/hostname.py | ||
---|---|---|
52 | 52 |
return { |
53 | 53 |
'hostname': event.hostname, |
54 | 54 |
} |
55 |
|
|
56 |
def get_sort_criterion(self, query, column): |
|
57 |
if column == 'hostname': |
|
58 |
return query.items.c.hostname |
|
59 |
return None |
|
60 |
|
vigiboard/controllers/plugins/occurrences.py | ||
---|---|---|
22 | 22 |
Un plugin pour VigiBoard qui ajoute une colonne avec le nombre |
23 | 23 |
d'occurrences d'un événement corrélé donné. |
24 | 24 |
""" |
25 |
from vigilo.models.tables import StateName |
|
25 |
from vigilo.models.tables import StateName, CorrEvent
|
|
26 | 26 |
from vigiboard.controllers.plugins import VigiboardRequestPlugin |
27 | 27 |
|
28 | 28 |
class PluginOccurrences(VigiboardRequestPlugin): |
... | ... | |
38 | 38 |
'state': state, |
39 | 39 |
'occurrences': event[0].occurrence, |
40 | 40 |
} |
41 |
|
|
42 |
def get_sort_criterion(self, query, column): |
|
43 |
if column == 'occurrences': |
|
44 |
return CorrEvent.occurrence |
|
45 |
return None |
|
46 |
|
vigiboard/controllers/plugins/output.py | ||
---|---|---|
52 | 52 |
return { |
53 | 53 |
'output': event[0].cause.message, |
54 | 54 |
} |
55 |
|
|
56 |
def get_sort_criterion(self, query, column): |
|
57 |
if column == 'output': |
|
58 |
return Event.message |
|
59 |
return None |
|
60 |
|
vigiboard/controllers/plugins/priority.py | ||
---|---|---|
137 | 137 |
'state': state, |
138 | 138 |
'priority': event[0].priority, |
139 | 139 |
} |
140 |
|
|
141 |
def get_sort_criterion(self, query, column): |
|
142 |
if column == 'priority': |
|
143 |
return CorrEvent.priority |
|
144 |
return None |
|
145 |
|
vigiboard/controllers/plugins/servicename.py | ||
---|---|---|
55 | 55 |
return { |
56 | 56 |
'servicename': event.servicename, |
57 | 57 |
} |
58 |
|
|
59 |
def get_sort_criterion(self, query, column): |
|
60 |
if column == 'servicename': |
|
61 |
return query.items.c.servicename |
|
62 |
return None |
|
63 |
|
vigiboard/controllers/plugins/status.py | ||
---|---|---|
116 | 116 |
'id': event[0].idcorrevent, |
117 | 117 |
'ack': ack, |
118 | 118 |
} |
119 |
|
|
120 |
def get_sort_criterion(self, query, column): |
|
121 |
criteria = { |
|
122 |
'ticket': CorrEvent.trouble_ticket, |
|
123 |
'ack': CorrEvent.ack, |
|
124 |
} |
|
125 |
return criteria.get(column) |
|
126 |
|
vigiboard/controllers/root.py | ||
---|---|---|
131 | 131 |
"""Schéma de validation de la méthode index.""" |
132 | 132 |
# Si on ne passe pas le paramètre "page" ou qu'on passe une valeur |
133 | 133 |
# invalide ou pas de valeur du tout, alors on affiche la 1ère page. |
134 |
page = validators.Int(min=1, if_missing=1, if_invalid=1, not_empty=True) |
|
134 |
page = validators.Int( |
|
135 |
min=1, |
|
136 |
if_missing=1, |
|
137 |
if_invalid=1, |
|
138 |
not_empty=True |
|
139 |
) |
|
140 |
|
|
141 |
# Paramètres de tri |
|
142 |
sort = validators.String(if_missing=None) |
|
143 |
order = validators.OneOf(['asc', 'desc'], if_missing='asc') |
|
135 | 144 |
|
136 | 145 |
# Nécessaire pour que les critères de recherche soient conservés. |
137 | 146 |
allow_extra_fields = True |
... | ... | |
146 | 155 |
@expose('events_table.html') |
147 | 156 |
@expose('events_table.html', content_type='text/csv') |
148 | 157 |
@require(access_restriction) |
149 |
def index(self, page, **search): |
|
158 |
def index(self, page, sort=None, order=None, **search):
|
|
150 | 159 |
""" |
151 | 160 |
Page d'accueil de Vigiboard. Elle affiche, suivant la page demandée |
152 | 161 |
(page 1 par defaut), la liste des événements, rangés par ordre de prise |
... | ... | |
155 | 164 |
|
156 | 165 |
@param page: Numéro de la page souhaitée, commence à 1 |
157 | 166 |
@type page: C{int} |
167 |
@param sort: Colonne de tri |
|
168 |
@type sort: C{str} or C{None} |
|
169 |
@param order: Ordre du tri (asc ou desc) |
|
170 |
@type order: C{str} or C{None} |
|
158 | 171 |
@param search: Dictionnaire contenant les critères de recherche. |
159 | 172 |
@type search: C{dict} |
160 | 173 |
|
... | ... | |
168 | 181 |
self.get_failures() |
169 | 182 |
|
170 | 183 |
user = get_current_user() |
171 |
aggregates = VigiboardRequest(user, search=search) |
|
184 |
aggregates = VigiboardRequest(user, search=search, sort=sort, order=order)
|
|
172 | 185 |
|
173 | 186 |
aggregates.add_table( |
174 | 187 |
CorrEvent, |
... | ... | |
249 | 262 |
servicename = None, |
250 | 263 |
plugins_data = plugins_data, |
251 | 264 |
page = page, |
265 |
sort = sort, |
|
266 |
order = order, |
|
252 | 267 |
event_edit_status_options = edit_event_status_options, |
253 | 268 |
search_form = create_search_form, |
254 | 269 |
search = search, |
... | ... | |
310 | 325 |
Affichage de la liste des événements bruts masqués d'un événement |
311 | 326 |
corrélé (événements agrégés dans l'événement corrélé). |
312 | 327 |
|
328 |
@param page: numéro de la page à afficher. |
|
329 |
@type page: C{int} |
|
313 | 330 |
@param idcorrevent: identifiant de l'événement corrélé souhaité. |
314 |
@type idcorrevent: C{int} |
|
331 |
@type idcorrevent: C{int}
|
|
315 | 332 |
""" |
316 | 333 |
|
317 | 334 |
# Auto-supervision |
... | ... | |
448 | 465 |
|
449 | 466 |
class ItemSchema(schema.Schema): |
450 | 467 |
"""Schéma de validation de la méthode item.""" |
468 |
# Si on ne passe pas le paramètre "page" ou qu'on passe une valeur |
|
469 |
# invalide ou pas de valeur du tout, alors on affiche la 1ère page. |
|
451 | 470 |
page = validators.Int(min=1, if_missing=1, if_invalid=1) |
471 |
|
|
472 |
# Paramètres de tri |
|
473 |
sort = validators.String(if_missing=None) |
|
474 |
order = validators.OneOf(['asc', 'desc'], if_missing='asc') |
|
475 |
|
|
476 |
# L'hôte / service dont on doit afficher les évènements |
|
452 | 477 |
host = validators.String(not_empty=True) |
453 | 478 |
service = validators.String(if_missing=None) |
454 | 479 |
|
... | ... | |
457 | 482 |
error_handler = process_form_errors) |
458 | 483 |
@expose('events_table.html') |
459 | 484 |
@require(access_restriction) |
460 |
def item(self, page, host, service): |
|
485 |
def item(self, page, host, service, sort=None, order=None):
|
|
461 | 486 |
""" |
462 | 487 |
Affichage de l'historique de l'ensemble des événements corrélés |
463 | 488 |
jamais ouverts sur l'hôte / service demandé. |
464 | 489 |
Pour accéder à cette page, l'utilisateur doit être authentifié. |
465 | 490 |
|
466 | 491 |
@param page: Numéro de la page à afficher. |
492 |
@type: C{int} |
|
467 | 493 |
@param host: Nom de l'hôte souhaité. |
494 |
@type: C{str} |
|
468 | 495 |
@param service: Nom du service souhaité |
496 |
@type: C{str} |
|
497 |
@param sort: Colonne de tri |
|
498 |
@type: C{str} or C{None} |
|
499 |
@param order: Ordre du tri (asc ou desc) |
|
500 |
@type: C{str} or C{None} |
|
469 | 501 |
|
470 | 502 |
Cette méthode permet de satisfaire l'exigence |
471 | 503 |
VIGILO_EXIG_VIGILO_BAC_0080. |
... | ... | |
480 | 512 |
redirect('/') |
481 | 513 |
|
482 | 514 |
user = get_current_user() |
483 |
aggregates = VigiboardRequest(user, False) |
|
515 |
aggregates = VigiboardRequest(user, False, sort=sort, order=order)
|
|
484 | 516 |
aggregates.add_table( |
485 | 517 |
CorrEvent, |
486 | 518 |
aggregates.items.c.hostname, |
... | ... | |
520 | 552 |
servicename = service, |
521 | 553 |
plugins_data = plugins_data, |
522 | 554 |
page = page, |
555 |
sort = sort, |
|
556 |
order = order, |
|
523 | 557 |
event_edit_status_options = edit_event_status_options, |
524 | 558 |
search_form = create_search_form, |
525 | 559 |
search = {}, |
... | ... | |
993 | 1027 |
opérée sur l'un des événements dont l'identifiant |
994 | 1028 |
fait partie de la liste passée en paramètre. |
995 | 1029 |
""" |
996 |
last_modification_timestamp = DBSession.query( |
|
1030 |
if not event_id_list: |
|
1031 |
last_modification_timestamp = None |
|
1032 |
else: |
|
1033 |
last_modification_timestamp = DBSession.query( |
|
997 | 1034 |
func.max(EventHistory.timestamp), |
998 | 1035 |
).filter(EventHistory.idevent.in_(event_id_list) |
999 | 1036 |
).scalar() |
1037 |
|
|
1000 | 1038 |
if not last_modification_timestamp: |
1001 | 1039 |
if not value_if_none: |
1002 | 1040 |
return None |
vigiboard/controllers/vigiboardrequest.py | ||
---|---|---|
43 | 43 |
le préformatage des événements et celui des historiques |
44 | 44 |
""" |
45 | 45 |
|
46 |
def __init__(self, user, mask_closed_events=True, search=None): |
|
46 |
def __init__(self, user, mask_closed_events=True, search=None, sort=None, order=None):
|
|
47 | 47 |
""" |
48 | 48 |
Initialisation de l'objet qui effectue les requêtes de VigiBoard |
49 | 49 |
sur la base de données. |
50 | 50 |
Cet objet est responsable de la vérification des droits de |
51 | 51 |
l'utilisateur sur les données manipulées. |
52 |
|
|
53 |
@param user: Nom de l'utilisateur cherchant à afficher les événements. |
|
54 |
@type user: C{str} |
|
55 |
@param mask_closed_events: Booléen indiquant si l'on souhaite masquer les |
|
56 |
événements fermés ou non. |
|
57 |
@type mask_closed_events: C{boolean} |
|
58 |
@param search: Dictionnaire contenant les critères de recherche. |
|
59 |
@type search: C{dict} |
|
60 |
@param sort: Colonne de tri; vide en l'absence de tri. |
|
61 |
@type sort: C{unicode} |
|
62 |
@param order: Ordre du tri ("asc" ou "desc"); vide en l'absence de tri. |
|
63 |
@type order: C{unicode} |
|
64 |
|
|
52 | 65 |
""" |
53 | 66 |
|
54 | 67 |
# Permet s'appliquer des filtres de recherche aux sous-requêtes. |
... | ... | |
82 | 95 |
StateName.statename, |
83 | 96 |
] |
84 | 97 |
|
85 |
# Permet de définir le sens de tri pour la priorité. |
|
86 |
if config['vigiboard_priority_order'] == 'asc': |
|
87 |
priority_order = asc(CorrEvent.priority) |
|
88 |
else: |
|
89 |
priority_order = desc(CorrEvent.priority) |
|
90 |
|
|
91 |
# Tris (ORDER BY) |
|
92 |
# Permet de répondre aux exigences suivantes : |
|
93 |
# - VIGILO_EXIG_VIGILO_BAC_0050 |
|
94 |
# - VIGILO_EXIG_VIGILO_BAC_0060 |
|
95 |
self.orderby = [ |
|
96 |
asc(CorrEvent.ack), # État acquittement |
|
97 |
asc(StateName.statename.in_([u'OK', u'UP'])), # Vert / Pas vert |
|
98 |
priority_order, # Priorité ITIL |
|
99 |
] |
|
100 |
|
|
101 |
if asbool(config.get('state_first', True)): |
|
102 |
self.orderby.extend([ |
|
103 |
desc(StateName.order), # Etat courant |
|
104 |
desc(Event.timestamp), # Timestamp |
|
105 |
]) |
|
106 |
else: |
|
107 |
self.orderby.extend([ |
|
108 |
desc(Event.timestamp), # Timestamp |
|
109 |
desc(StateName.order), # Etat courant |
|
110 |
]) |
|
111 | 98 |
|
112 | 99 |
self.req = DBSession |
113 | 100 |
self.plugin = [] |
... | ... | |
176 | 163 |
# Permet d'avoir le même format que pour l'autre requête. |
177 | 164 |
self.items = items.subquery() |
178 | 165 |
|
166 |
# Tris (ORDER BY) |
|
167 |
# Permet de répondre aux exigences suivantes : |
|
168 |
# - VIGILO_EXIG_VIGILO_BAC_0050 |
|
169 |
# - VIGILO_EXIG_VIGILO_BAC_0060 |
|
170 |
self.orderby = [] |
|
171 |
if sort: |
|
172 |
for _plugin, instance in config.get('columns_plugins', []): |
|
173 |
criterion = instance.get_sort_criterion(self, sort) |
|
174 |
if criterion is not None: |
|
175 |
if order == 'asc': |
|
176 |
self.orderby.append(asc(criterion)) |
|
177 |
else: |
|
178 |
self.orderby.append(desc(criterion)) |
|
179 |
|
|
180 |
# Permet de définir le sens de tri pour la priorité. |
|
181 |
if config['vigiboard_priority_order'] == 'asc': |
|
182 |
priority_order = asc(CorrEvent.priority) |
|
183 |
else: |
|
184 |
priority_order = desc(CorrEvent.priority) |
|
185 |
|
|
186 |
self.orderby.extend([ |
|
187 |
asc(CorrEvent.ack), # État acquittement |
|
188 |
asc(StateName.statename.in_([u'OK', u'UP'])), # Vert / Pas vert |
|
189 |
priority_order, # Priorité ITIL |
|
190 |
]) |
|
191 |
|
|
192 |
if asbool(config.get('state_first', True)): |
|
193 |
self.orderby.extend([ |
|
194 |
desc(StateName.order), # Etat courant |
|
195 |
desc(Event.timestamp), # Timestamp |
|
196 |
]) |
|
197 |
else: |
|
198 |
self.orderby.extend([ |
|
199 |
desc(Event.timestamp), # Timestamp |
|
200 |
desc(StateName.order), # Etat courant |
|
201 |
]) |
|
202 |
|
|
179 | 203 |
if search is not None: |
180 | 204 |
# 2nde passe pour les filtres : self.items est désormais défini. |
181 | 205 |
for _plugin, instance in config.get('columns_plugins', []): |
vigiboard/tests/functional/test_sorting.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# vim:set expandtab tabstop=4 shiftwidth=4: |
|
3 |
# Copyright (C) 2006-2013 CS-SI |
|
4 |
# License: GNU GPL v2 <http://www.gnu.org/licenses/gpl-2.0.html> |
|
5 |
|
|
6 |
""" |
|
7 |
Test du tri de Vigiboard |
|
8 |
""" |
|
9 |
|
|
10 |
from __future__ import absolute_import |
|
11 |
|
|
12 |
from nose.tools import assert_true, assert_equal |
|
13 |
|
|
14 |
import transaction |
|
15 |
from vigilo.models.demo import functions |
|
16 |
from vigilo.models.session import DBSession |
|
17 |
from vigilo.models import tables |
|
18 |
|
|
19 |
from vigiboard.tests import TestController |
|
20 |
from tg import config |
|
21 |
|
|
22 |
def populate_DB(): |
|
23 |
""" Peuple la base de données en vue des tests. """ |
|
24 |
|
|
25 |
# On crée deux hôtes de test. |
|
26 |
host1 = functions.add_host(u'host1') |
|
27 |
host2 = functions.add_host(u'host2') |
|
28 |
DBSession.flush() |
|
29 |
|
|
30 |
# On ajoute un service sur chaque hôte. |
|
31 |
service1 = functions.add_lowlevelservice( |
|
32 |
host2, u'service1') |
|
33 |
service2 = functions.add_lowlevelservice( |
|
34 |
host1, u'service2') |
|
35 |
DBSession.flush() |
|
36 |
|
|
37 |
# On ajoute un événement brut sur chaque service. |
|
38 |
event1 = functions.add_event(service1, u'WARNING', u'foo') |
|
39 |
event2 = functions.add_event(service2, u'CRITICAL', u'foo') |
|
40 |
DBSession.flush() |
|
41 |
|
|
42 |
# On ajoute un événement corrélé pour chaque événement brut. |
|
43 |
functions.add_correvent([event1]) |
|
44 |
functions.add_correvent([event2]) |
|
45 |
DBSession.flush() |
|
46 |
transaction.commit() |
|
47 |
|
|
48 |
class TestSorting(TestController): |
|
49 |
""" |
|
50 |
Test du tri de Vigiboard |
|
51 |
""" |
|
52 |
def setUp(self): |
|
53 |
super(TestSorting, self).setUp() |
|
54 |
populate_DB() |
|
55 |
|
|
56 |
def test_ascending_order(self): |
|
57 |
""" Tri dans l'ordre croissant """ |
|
58 |
|
|
59 |
# On affiche la page principale de VigiBoard |
|
60 |
# triée sur le nom d'hôte par ordre croissant |
|
61 |
environ = {'REMOTE_USER': 'manager'} |
|
62 |
response = self.app.get( |
|
63 |
'/?sort=hostname&order=asc', extra_environ=environ) |
|
64 |
|
|
65 |
# Il doit y avoir 2 lignes de résultats : |
|
66 |
# - la 1ère concerne 'service2' sur 'host1' ; |
|
67 |
# - la 2nde concerne 'service1' sur 'host2'. |
|
68 |
# Il doit y avoir plusieurs colonnes dans la ligne de résultats. |
|
69 |
hostnames = response.lxml.xpath( |
|
70 |
'//table[@class="vigitable"]/tbody/tr/' \ |
|
71 |
'td[@class="plugin_hostname"]/text()') |
|
72 |
assert_equal(hostnames, ['host1', 'host2']) |
|
73 |
servicenames = response.lxml.xpath( |
|
74 |
'//table[@class="vigitable"]/tbody/tr/' \ |
|
75 |
'td[@class="plugin_servicename"]/text()') |
|
76 |
assert_equal(servicenames, ['service2', 'service1']) |
|
77 |
|
|
78 |
def test_descending_order(self): |
|
79 |
""" Tri dans l'ordre décroissant """ |
|
80 |
|
|
81 |
# On affiche la page principale de VigiBoard |
|
82 |
# triée sur le nom de service par ordre décroissant |
|
83 |
environ = {'REMOTE_USER': 'manager'} |
|
84 |
response = self.app.get( |
|
85 |
'/?sort=servicename&order=desc', extra_environ=environ) |
|
86 |
|
|
87 |
# Il doit y avoir 2 lignes de résultats : |
|
88 |
# - la 1ère concerne 'service2' sur 'host1' ; |
|
89 |
# - la 2nde concerne 'service1' sur 'host2'. |
|
90 |
# Il doit y avoir plusieurs colonnes dans la ligne de résultats. |
|
91 |
hostnames = response.lxml.xpath( |
|
92 |
'//table[@class="vigitable"]/tbody/tr/' \ |
|
93 |
'td[@class="plugin_hostname"]/text()') |
|
94 |
assert_equal(hostnames, ['host1', 'host2']) |
|
95 |
servicenames = response.lxml.xpath( |
|
96 |
'//table[@class="vigitable"]/tbody/tr/' \ |
|
97 |
'td[@class="plugin_servicename"]/text()') |
|
98 |
assert_equal(servicenames, ['service2', 'service1']) |
|
99 |
|
|
100 |
def test_pagination(self): |
|
101 |
""" Pagination du tri """ |
|
102 |
|
|
103 |
# On crée autant d'événements qu'on peut en afficher par page + 1, |
|
104 |
# afin d'avoir 2 pages dans le bac à événements. |
|
105 |
host3 = functions.add_host(u'host3') |
|
106 |
service3 = functions.add_lowlevelservice( |
|
107 |
host3, u'service3') |
|
108 |
DBSession.flush() |
|
109 |
items_per_page = int(config['vigiboard_items_per_page']) |
|
110 |
for i in xrange(items_per_page - 1): |
|
111 |
event = functions.add_event(service3, u'WARNING', u'foo') |
|
112 |
functions.add_correvent([event]) |
|
113 |
DBSession.flush() |
|
114 |
transaction.commit() |
|
115 |
|
|
116 |
# On affiche la seconde page de VigiBoard avec |
|
117 |
# un tri par ordre décroissant sur le nom d'hôte |
|
118 |
environ = {'REMOTE_USER': 'manager'} |
|
119 |
response = self.app.get( |
|
120 |
'/?page=2&sort=hostname&order=desc', extra_environ=environ) |
|
121 |
|
|
122 |
# Il ne doit y avoir qu'une seule ligne de |
|
123 |
# résultats concernant "service2" sur "host1" |
|
124 |
hostnames = response.lxml.xpath( |
|
125 |
'//table[@class="vigitable"]/tbody/tr/' \ |
|
126 |
'td[@class="plugin_hostname"]/text()') |
|
127 |
assert_equal(hostnames, ['host1']) |
|
128 |
servicenames = response.lxml.xpath( |
|
129 |
'//table[@class="vigitable"]/tbody/tr/' \ |
|
130 |
'td[@class="plugin_servicename"]/text()') |
|
131 |
assert_equal(servicenames, ['service2']) |
|
132 |
|
|
133 |
|
vigiboard/widgets/search_form.py | ||
---|---|---|
45 | 45 |
style = 'display: none' |
46 | 46 |
|
47 | 47 |
fields = [ |
48 |
twf.HiddenField('page') |
|
48 |
twf.HiddenField('page'), |
|
49 |
twf.HiddenField('sort'), |
|
50 |
twf.HiddenField('order') |
|
49 | 51 |
] |
50 | 52 |
for plugin, instance in tg.config.get('columns_plugins', []): |
51 | 53 |
fields.extend(instance.get_search_fields()) |
Also available in: Unified diff