lino 24.10.0__py3-none-any.whl → 24.10.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lino/__init__.py +1 -1
- lino/api/doctest.py +11 -5
- lino/core/__init__.py +0 -1
- lino/core/actions.py +1 -1
- lino/core/actors.py +17 -22
- lino/core/choicelists.py +28 -11
- lino/core/dashboard.py +18 -15
- lino/core/dbtables.py +21 -15
- lino/core/model.py +4 -2
- lino/core/renderer.py +19 -19
- lino/core/requests.py +872 -295
- lino/core/store.py +6 -6
- lino/core/tables.py +7 -7
- lino/core/utils.py +134 -0
- lino/modlib/bootstrap3/views.py +4 -3
- lino/modlib/extjs/views.py +2 -1
- lino/modlib/forms/views.py +3 -3
- lino/modlib/odata/views.py +2 -2
- lino/modlib/publisher/views.py +4 -1
- lino/utils/report.py +3 -10
- {lino-24.10.0.dist-info → lino-24.10.1.dist-info}/METADATA +1 -1
- {lino-24.10.0.dist-info → lino-24.10.1.dist-info}/RECORD +25 -26
- lino/core/tablerequest.py +0 -758
- {lino-24.10.0.dist-info → lino-24.10.1.dist-info}/WHEEL +0 -0
- {lino-24.10.0.dist-info → lino-24.10.1.dist-info}/licenses/AUTHORS.rst +0 -0
- {lino-24.10.0.dist-info → lino-24.10.1.dist-info}/licenses/COPYING +0 -0
lino/core/tablerequest.py
DELETED
@@ -1,758 +0,0 @@
|
|
1
|
-
# -*- coding: UTF-8 -*-
|
2
|
-
# Copyright 2009-2023 Rumma & Ko Ltd
|
3
|
-
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
4
|
-
"""Defines the `TableRequest` class.
|
5
|
-
|
6
|
-
"""
|
7
|
-
from past.utils import old_div
|
8
|
-
|
9
|
-
from types import GeneratorType
|
10
|
-
import sys
|
11
|
-
from io import StringIO
|
12
|
-
import json
|
13
|
-
|
14
|
-
from django.db import models
|
15
|
-
from django.conf import settings
|
16
|
-
from django.db.models.query import QuerySet
|
17
|
-
from django.core.exceptions import ObjectDoesNotExist, SuspiciousOperation
|
18
|
-
|
19
|
-
if settings.SITE.is_installed("contenttypes"):
|
20
|
-
from django.contrib.contenttypes.models import ContentType
|
21
|
-
else:
|
22
|
-
ContentType = None
|
23
|
-
|
24
|
-
from lino.core.utils import obj2str
|
25
|
-
from lino.core.utils import format_request
|
26
|
-
from lino.core.store import get_atomizer
|
27
|
-
from lino.core import constants
|
28
|
-
from etgen import html as xghtml
|
29
|
-
from lino.utils.html import E, tostring
|
30
|
-
from lino.utils import jsgen
|
31
|
-
from lino.utils import MissingRow
|
32
|
-
from lino.core.utils import getrqdata
|
33
|
-
|
34
|
-
from .fields import RemoteField, FakeField, TableRow
|
35
|
-
from .requests import ActionRequest
|
36
|
-
|
37
|
-
|
38
|
-
class SearchQuerySet:
|
39
|
-
pass
|
40
|
-
|
41
|
-
|
42
|
-
class SearchQuery:
|
43
|
-
pass
|
44
|
-
|
45
|
-
|
46
|
-
if settings.SITE.is_installed("search"):
|
47
|
-
if settings.SITE.use_elasticsearch:
|
48
|
-
try:
|
49
|
-
from elasticsearch_django.models import SearchQuery
|
50
|
-
except ImportError:
|
51
|
-
pass
|
52
|
-
elif settings.SITE.use_solr:
|
53
|
-
try:
|
54
|
-
from haystack.query import SearchQuerySet
|
55
|
-
except ImportError:
|
56
|
-
pass
|
57
|
-
|
58
|
-
WARNINGS_LOGGED = dict()
|
59
|
-
|
60
|
-
|
61
|
-
def column_header(col):
|
62
|
-
# ~ if col.label:
|
63
|
-
# ~ return join_elems(col.label.split('\n'),sep=E.br)
|
64
|
-
# ~ return [unicode(col.name)]
|
65
|
-
label = col.get_label()
|
66
|
-
if label is None:
|
67
|
-
return col.name
|
68
|
-
return str(label)
|
69
|
-
|
70
|
-
|
71
|
-
def sliced_data_iterator(qs, offset, limit):
|
72
|
-
# qs is either a Django QuerySet or iterable
|
73
|
-
# When limit is None or 0, offset cannot be -1
|
74
|
-
if offset is not None:
|
75
|
-
if isinstance(qs, QuerySet):
|
76
|
-
num = qs.count()
|
77
|
-
else:
|
78
|
-
num = len(qs)
|
79
|
-
if offset == -1:
|
80
|
-
page_num = num // limit
|
81
|
-
offset = limit * page_num
|
82
|
-
qs = qs[offset:num]
|
83
|
-
if limit is not None and limit != 0:
|
84
|
-
qs = qs[:limit]
|
85
|
-
return qs
|
86
|
-
|
87
|
-
|
88
|
-
class TableRequest(ActionRequest):
|
89
|
-
"""A specialized :class:`ActionRequest
|
90
|
-
<lino.core.requests.ActionRequest>` whose actor is a :class:`table
|
91
|
-
<lino.core.tables.AbstractTable>`.
|
92
|
-
|
93
|
-
"""
|
94
|
-
|
95
|
-
master = None
|
96
|
-
extra = None
|
97
|
-
title = None
|
98
|
-
filter = None
|
99
|
-
# known_values = None
|
100
|
-
limit = None
|
101
|
-
offset = None
|
102
|
-
|
103
|
-
_data_iterator = None
|
104
|
-
_sliced_data_iterator = None
|
105
|
-
_insert_sar = None
|
106
|
-
|
107
|
-
def gen_insert_button(
|
108
|
-
self, target, button_attrs=dict(style="float: right;"), **values
|
109
|
-
):
|
110
|
-
"""
|
111
|
-
Generate an insert button using a cached insertable object.
|
112
|
-
|
113
|
-
This is functionally equivalent to saying::
|
114
|
-
|
115
|
-
if self.insert_action is not None:
|
116
|
-
ir = self.insert_action.request_from(ar)
|
117
|
-
if ir.get_permission():
|
118
|
-
return ir.ar2button()
|
119
|
-
|
120
|
-
The difference is that gen_insert_button is more efficient when you do
|
121
|
-
this more than once during a single request.
|
122
|
-
|
123
|
-
`target` is the actor into which we want to insert an object.
|
124
|
-
`button_label` is the optional button label.
|
125
|
-
`values` is a dict of extra default values to apply to the insertable object.
|
126
|
-
|
127
|
-
First usage example is in :mod:`lino_xl.lib.calview`.
|
128
|
-
|
129
|
-
"""
|
130
|
-
if self._insert_sar is None:
|
131
|
-
self._insert_sar = target.insert_action.request_from(self)
|
132
|
-
else:
|
133
|
-
assert self._insert_sar.actor is target
|
134
|
-
if self._insert_sar.get_permission():
|
135
|
-
st = self._insert_sar.get_status()
|
136
|
-
st["data_record"]["data"].update(values)
|
137
|
-
# obj = st['data_record']
|
138
|
-
# for k, v in values.items():
|
139
|
-
# setattr(obj, k, v)
|
140
|
-
# print(20200302, st['data_record'])
|
141
|
-
return self._insert_sar.ar2button(**button_attrs)
|
142
|
-
|
143
|
-
def execute(self):
|
144
|
-
"""This will actually call the :meth:`get_data_iterator` and run the
|
145
|
-
database query.
|
146
|
-
|
147
|
-
Automatically called when either :attr:`data_iterator`
|
148
|
-
or :attr:`sliced_data_iterator` is accesed.
|
149
|
-
|
150
|
-
"""
|
151
|
-
# print("20181121 execute", self.actor)
|
152
|
-
try:
|
153
|
-
self._data_iterator = self.get_data_iterator()
|
154
|
-
if self._data_iterator is None:
|
155
|
-
raise Exception("No data iterator for {}".format(self))
|
156
|
-
except Warning as e:
|
157
|
-
# ~ logger.info("20130809 Warning %s",e)
|
158
|
-
self.no_data_text = str(e)
|
159
|
-
self._data_iterator = []
|
160
|
-
except Exception as e:
|
161
|
-
if not settings.SITE.catch_layout_exceptions:
|
162
|
-
raise
|
163
|
-
# Report this exception. But since such errors may occur
|
164
|
-
# rather often and since exception loggers usually send an
|
165
|
-
# email to the local system admin, make sure to log each
|
166
|
-
# exception only once.
|
167
|
-
self.no_data_text = (
|
168
|
-
"{} (set catch_layout_exceptions to see details)".format(e)
|
169
|
-
)
|
170
|
-
w = WARNINGS_LOGGED.get(str(e))
|
171
|
-
if w is None:
|
172
|
-
WARNINGS_LOGGED[str(e)] = True
|
173
|
-
raise
|
174
|
-
# logger.exception(e)
|
175
|
-
self._data_iterator = []
|
176
|
-
|
177
|
-
if isinstance(self._data_iterator, GeneratorType):
|
178
|
-
# print 20150718, self._data_iterator
|
179
|
-
self._data_iterator = tuple(self._data_iterator)
|
180
|
-
if isinstance(self._data_iterator, (SearchQuery, SearchQuerySet)):
|
181
|
-
self._sliced_data_iterator = tuple(
|
182
|
-
self.actor.get_rows_from_search_query(self._data_iterator, self)
|
183
|
-
)
|
184
|
-
else:
|
185
|
-
self._sliced_data_iterator = sliced_data_iterator(
|
186
|
-
self._data_iterator, self.offset, self.limit
|
187
|
-
)
|
188
|
-
# logger.info("20171116 executed : %s", self._sliced_data_iterator)
|
189
|
-
|
190
|
-
def must_execute(self):
|
191
|
-
return self._data_iterator is None
|
192
|
-
|
193
|
-
def get_data_iterator_property(self):
|
194
|
-
if self._data_iterator is None:
|
195
|
-
self.execute()
|
196
|
-
return self._data_iterator
|
197
|
-
|
198
|
-
def get_sliced_data_iterator_property(self):
|
199
|
-
if self._sliced_data_iterator is None:
|
200
|
-
self.execute()
|
201
|
-
return self._sliced_data_iterator
|
202
|
-
|
203
|
-
data_iterator = property(get_data_iterator_property)
|
204
|
-
sliced_data_iterator = property(get_sliced_data_iterator_property)
|
205
|
-
|
206
|
-
def get_data_iterator(self):
|
207
|
-
self.actor.check_params(self.param_values)
|
208
|
-
if self.actor.get_data_rows is not None:
|
209
|
-
l = []
|
210
|
-
for row in self.actor.get_data_rows(self):
|
211
|
-
group = self.actor.group_from_row(row)
|
212
|
-
group.process_row(l, row)
|
213
|
-
return l
|
214
|
-
# ~ logger.info("20120914 tables.get_data_iterator %s",self)
|
215
|
-
# ~ logger.info("20120914 tables.get_data_iterator %s",self.actor)
|
216
|
-
# print("20181121 get_data_iterator", self.actor)
|
217
|
-
return self.actor.get_request_queryset(self)
|
218
|
-
|
219
|
-
def get_total_count(self):
|
220
|
-
"""
|
221
|
-
Calling `len()` on a QuerySet would execute the whole SELECT.
|
222
|
-
See `/blog/2012/0124`
|
223
|
-
"""
|
224
|
-
di = self.data_iterator
|
225
|
-
if isinstance(di, SearchQuery):
|
226
|
-
return di.total_hits
|
227
|
-
if isinstance(di, QuerySet):
|
228
|
-
return di.count()
|
229
|
-
# try:
|
230
|
-
# return di.count()
|
231
|
-
# except Exception as e:
|
232
|
-
# raise e.__class__("{} : {}".format(self, e))
|
233
|
-
# ~ if di is None:
|
234
|
-
# ~ raise Exception("data_iterator is None: %s" % self)
|
235
|
-
if True:
|
236
|
-
return len(di)
|
237
|
-
else:
|
238
|
-
try:
|
239
|
-
return len(di)
|
240
|
-
except TypeError:
|
241
|
-
raise TypeError("{0} has no length".format(di))
|
242
|
-
|
243
|
-
def __iter__(self):
|
244
|
-
return self.data_iterator.__iter__()
|
245
|
-
|
246
|
-
def __getitem__(self, i):
|
247
|
-
return self.data_iterator.__getitem__(i)
|
248
|
-
|
249
|
-
def parse_req(self, request, rqdata, **kw):
|
250
|
-
"""Parse the incoming HttpRequest and translate it into keyword
|
251
|
-
arguments to be used by :meth:`setup`.
|
252
|
-
|
253
|
-
The `mt` url param is parsed only when needed. Usually it is
|
254
|
-
not needed because the `master_class` is constant and known
|
255
|
-
per actor. But there are exceptions:
|
256
|
-
|
257
|
-
- `master` is `ContentType`
|
258
|
-
|
259
|
-
- `master` is some abstract model
|
260
|
-
|
261
|
-
- `master` is not a subclass of Model, e.g.
|
262
|
-
:class:`lino_xl.lib.polls.AnswersByResponse`, a
|
263
|
-
virtual table that defines :meth:`get_row_by_pk
|
264
|
-
<lino.core.actors.Actor.get_row_by_pk>`.
|
265
|
-
|
266
|
-
"""
|
267
|
-
# logger.info("20120723 %s.parse_req() %s", self.actor, rqdata)
|
268
|
-
# ~ rh = self.ah
|
269
|
-
|
270
|
-
if "master_instance" not in kw:
|
271
|
-
kw.update(
|
272
|
-
master_type=rqdata.get(constants.URL_PARAM_MASTER_TYPE, None),
|
273
|
-
master_key=rqdata.get(constants.URL_PARAM_MASTER_PK, None),
|
274
|
-
)
|
275
|
-
|
276
|
-
# if settings.SITE.use_filterRow:
|
277
|
-
# exclude = dict()
|
278
|
-
# for f in self.ah.store.fields:
|
279
|
-
# if f.field:
|
280
|
-
# filterOption = rqdata.get(
|
281
|
-
# 'filter[%s_filterOption]' % f.field.name)
|
282
|
-
# if filterOption == 'empty':
|
283
|
-
# kw[f.field.name + "__isnull"] = True
|
284
|
-
# elif filterOption == 'notempty':
|
285
|
-
# kw[f.field.name + "__isnull"] = False
|
286
|
-
# else:
|
287
|
-
# filterValue = rqdata.get('filter[%s]' % f.field.name)
|
288
|
-
# if filterValue:
|
289
|
-
# if not filterOption:
|
290
|
-
# filterOption = 'contains'
|
291
|
-
# if filterOption == 'contains':
|
292
|
-
# kw[f.field.name + "__icontains"] = filterValue
|
293
|
-
# elif filterOption == 'doesnotcontain':
|
294
|
-
# exclude[f.field.name +
|
295
|
-
# "__icontains"] = filterValue
|
296
|
-
# else:
|
297
|
-
# print("unknown filterOption %r" % filterOption)
|
298
|
-
# if len(exclude):
|
299
|
-
# kw.update(exclude=exclude)
|
300
|
-
|
301
|
-
if settings.SITE.use_gridfilters:
|
302
|
-
filter = rqdata.get(constants.URL_PARAM_GRIDFILTER, None)
|
303
|
-
if filter is not None:
|
304
|
-
filter = json.loads(filter)
|
305
|
-
kw["gridfilters"] = [constants.dict2kw(flt) for flt in filter]
|
306
|
-
|
307
|
-
kw = ActionRequest.parse_req(self, request, rqdata, **kw)
|
308
|
-
|
309
|
-
# if str(self.actor) == 'comments.CommentsByRFC':
|
310
|
-
# print("20230426", kw)
|
311
|
-
|
312
|
-
# ~ kw.update(self.report.known_values)
|
313
|
-
# ~ for fieldname, default in self.report.known_values.items():
|
314
|
-
# ~ v = request.REQUEST.get(fieldname,None)
|
315
|
-
# ~ if v is not None:
|
316
|
-
# ~ kw[fieldname] = v
|
317
|
-
|
318
|
-
quick_search = rqdata.get(constants.URL_PARAM_FILTER, None)
|
319
|
-
if quick_search:
|
320
|
-
kw.update(quick_search=quick_search)
|
321
|
-
|
322
|
-
sort = rqdata.get(constants.URL_PARAM_SORT, None)
|
323
|
-
if sort:
|
324
|
-
sortfld = self.actor.get_data_elem(sort)
|
325
|
-
if isinstance(sortfld, FakeField):
|
326
|
-
sort = sortfld.sortable_by
|
327
|
-
# sort might be None when user asked to sort a virtual
|
328
|
-
# field without sortable_by.
|
329
|
-
else:
|
330
|
-
sort = [sort]
|
331
|
-
# print("20231006 sort", sortfld, sort)
|
332
|
-
if sort is not None:
|
333
|
-
|
334
|
-
def si(k):
|
335
|
-
if k[0] == "-":
|
336
|
-
return k[1:]
|
337
|
-
else:
|
338
|
-
return "-" + k
|
339
|
-
|
340
|
-
sort_dir = rqdata.get(constants.URL_PARAM_SORTDIR, "ASC")
|
341
|
-
if sort_dir == "DESC":
|
342
|
-
sort = [si(k) for k in sort]
|
343
|
-
# sort = ['-' + k for k in sort]
|
344
|
-
# print("20171123", sort)
|
345
|
-
kw.update(order_by=sort)
|
346
|
-
|
347
|
-
try:
|
348
|
-
offset = rqdata.get(constants.URL_PARAM_START, None)
|
349
|
-
if offset:
|
350
|
-
kw.update(offset=int(offset))
|
351
|
-
limit = rqdata.get(constants.URL_PARAM_LIMIT, self.actor.preview_limit)
|
352
|
-
if limit:
|
353
|
-
kw.update(limit=int(limit))
|
354
|
-
except ValueError:
|
355
|
-
# Example: invalid literal for int() with base 10:
|
356
|
-
# 'fdpkvcnrfdybhur'
|
357
|
-
raise SuspiciousOperation("Invalid value for limit or offset")
|
358
|
-
|
359
|
-
kw = self.actor.parse_req(request, rqdata, **kw)
|
360
|
-
# print("20171123 %s.parse_req() --> %s" % (self, kw))
|
361
|
-
return kw
|
362
|
-
|
363
|
-
def setup(
|
364
|
-
self,
|
365
|
-
quick_search=None,
|
366
|
-
order_by=None,
|
367
|
-
offset=None,
|
368
|
-
limit=None,
|
369
|
-
title=None,
|
370
|
-
filter=None,
|
371
|
-
gridfilters=None,
|
372
|
-
exclude=None,
|
373
|
-
extra=None,
|
374
|
-
**kw,
|
375
|
-
):
|
376
|
-
self.quick_search = quick_search
|
377
|
-
self.order_by = order_by
|
378
|
-
self.filter = filter
|
379
|
-
self.gridfilters = gridfilters
|
380
|
-
self.extra = extra
|
381
|
-
self.exclude = exclude or self.actor.exclude
|
382
|
-
self.page_length = self.actor.page_length
|
383
|
-
|
384
|
-
if title is not None:
|
385
|
-
self.title = title
|
386
|
-
if offset is not None:
|
387
|
-
self.offset = offset
|
388
|
-
if limit is not None:
|
389
|
-
self.limit = limit
|
390
|
-
|
391
|
-
super().setup(**kw)
|
392
|
-
self.actor.setup_request(self)
|
393
|
-
|
394
|
-
# if str(self.actor) == 'outbox.MyOutbox':
|
395
|
-
# if self.master_instance is None:
|
396
|
-
# raise Exception("20230426 b {}".format(self.master))
|
397
|
-
|
398
|
-
if isinstance(self.actor.master_field, RemoteField):
|
399
|
-
# cannot insert rows in a slave table with a remote master
|
400
|
-
# field
|
401
|
-
self.create_kw = None
|
402
|
-
elif isinstance(self.master_instance, MissingRow):
|
403
|
-
self.create_kw = None # 20230426
|
404
|
-
else:
|
405
|
-
self.create_kw = self.actor.get_filter_kw(self)
|
406
|
-
|
407
|
-
def to_rst(self, *args, **kw):
|
408
|
-
"""Returns a string representing this table request in
|
409
|
-
reStructuredText markup.
|
410
|
-
|
411
|
-
"""
|
412
|
-
stdout = sys.stdout
|
413
|
-
sys.stdout = StringIO()
|
414
|
-
self.table2rst(*args, **kw)
|
415
|
-
rv = sys.stdout.getvalue()
|
416
|
-
sys.stdout = stdout
|
417
|
-
return rv
|
418
|
-
|
419
|
-
def table2rst(self, *args, **kwargs):
|
420
|
-
"""
|
421
|
-
Print a reStructuredText representation of this table request to
|
422
|
-
stdout.
|
423
|
-
"""
|
424
|
-
settings.SITE.kernel.text_renderer.show_table(self, *args, **kwargs)
|
425
|
-
|
426
|
-
def table2xhtml(self, **kwargs):
|
427
|
-
"""
|
428
|
-
Return an HTML representation of this table request.
|
429
|
-
"""
|
430
|
-
t = xghtml.Table()
|
431
|
-
self.dump2html(t, self.sliced_data_iterator, **kwargs)
|
432
|
-
e = t.as_element()
|
433
|
-
# print "20150822 table2xhtml", tostring(e)
|
434
|
-
# if header_level is not None:
|
435
|
-
# return E.div(E.h2(str(self.actor.label)), e)
|
436
|
-
return e
|
437
|
-
|
438
|
-
def dump2html(
|
439
|
-
self,
|
440
|
-
tble,
|
441
|
-
data_iterator,
|
442
|
-
column_names=None,
|
443
|
-
header_links=False,
|
444
|
-
max_width=None, # ignored
|
445
|
-
hide_sums=None,
|
446
|
-
):
|
447
|
-
"""
|
448
|
-
Render this table into an existing :class:`etgen.html.Table`
|
449
|
-
instance. This central method is used by all Lino
|
450
|
-
renderers.
|
451
|
-
|
452
|
-
Arguments:
|
453
|
-
|
454
|
-
`tble` An instance of :class:`etgen.html.Table`.
|
455
|
-
|
456
|
-
`data_iterator` the iterable provider of table rows. This can
|
457
|
-
be a queryset or a list.
|
458
|
-
|
459
|
-
`column_names` is an optional string with space-separated
|
460
|
-
column names. If this is None, the table's
|
461
|
-
:attr:`column_names <lino.core.tables.Table.column_names>` is
|
462
|
-
used.
|
463
|
-
|
464
|
-
`header_links` says whether to render column headers clickable
|
465
|
-
with a link that sorts the table.
|
466
|
-
|
467
|
-
`hide_sums` : whether to hide sums. If this is not given, use
|
468
|
-
the :attr:`hide_sums <lino.core.tables.Table.hide_sums>` of
|
469
|
-
the :attr:`actor`.
|
470
|
-
"""
|
471
|
-
ar = self
|
472
|
-
tble.attrib.update(self.renderer.tableattrs)
|
473
|
-
tble.attrib.setdefault("name", self.bound_action.full_name())
|
474
|
-
|
475
|
-
grid = ar.ah.grid_layout.main
|
476
|
-
# from lino.core.widgets import GridWidget
|
477
|
-
# if not isinstance(grid, GridWidget):
|
478
|
-
# raise Exception("20160529 %r is not a GridElement", grid)
|
479
|
-
columns = grid.columns
|
480
|
-
fields, headers, cellwidths = ar.get_field_info(column_names)
|
481
|
-
columns = fields
|
482
|
-
|
483
|
-
sums = [fld.zero for fld in columns]
|
484
|
-
if not self.ah.actor.hide_headers:
|
485
|
-
headers = [
|
486
|
-
x
|
487
|
-
for x in grid.headers2html(
|
488
|
-
self, columns, headers, header_links, **self.renderer.cellattrs
|
489
|
-
)
|
490
|
-
]
|
491
|
-
# if cellwidths and self.renderer.is_interactive:
|
492
|
-
if cellwidths:
|
493
|
-
totwidth = sum([int(w) for w in cellwidths])
|
494
|
-
widths = [str(int(int(w) * 100 / totwidth)) + "%" for w in cellwidths]
|
495
|
-
for i, td in enumerate(headers):
|
496
|
-
# td.set('width', six.text_type(cellwidths[i]))
|
497
|
-
td.set("width", widths[i])
|
498
|
-
tble.head.append(xghtml.E.tr(*headers))
|
499
|
-
# ~ print 20120623, ar.actor
|
500
|
-
recno = 0
|
501
|
-
for obj in data_iterator:
|
502
|
-
cells = ar.row2html(recno, columns, obj, sums, **self.renderer.cellattrs)
|
503
|
-
if cells is not None:
|
504
|
-
recno += 1
|
505
|
-
tble.body.append(xghtml.E.tr(*cells))
|
506
|
-
|
507
|
-
if recno == 0:
|
508
|
-
tble.clear()
|
509
|
-
tble.body.append(str(ar.no_data_text))
|
510
|
-
|
511
|
-
if hide_sums is None:
|
512
|
-
hide_sums = ar.actor.hide_sums
|
513
|
-
|
514
|
-
if not hide_sums:
|
515
|
-
has_sum = False
|
516
|
-
for i in sums:
|
517
|
-
if i:
|
518
|
-
has_sum = True
|
519
|
-
break
|
520
|
-
if has_sum:
|
521
|
-
cells = ar.sums2html(columns, sums, **self.renderer.cellattrs)
|
522
|
-
tble.body.append(xghtml.E.tr(*cells))
|
523
|
-
|
524
|
-
def get_field_info(ar, column_names=None):
|
525
|
-
"""
|
526
|
-
Return a tuple `(fields, headers, widths)` which expresses which
|
527
|
-
columns, headers and widths the user wants for this
|
528
|
-
request. If `self` has web request info (:attr:`request` is
|
529
|
-
not None), checks for GET parameters cn, cw and ch. Also
|
530
|
-
calls the tables's :meth:`override_column_headers
|
531
|
-
<lino.core.actors.Actor.override_column_headers>` method.
|
532
|
-
"""
|
533
|
-
from lino.modlib.users.utils import with_user_profile
|
534
|
-
from lino.core.layouts import ColumnsLayout
|
535
|
-
|
536
|
-
def getit():
|
537
|
-
if ar.request is None:
|
538
|
-
columns = None
|
539
|
-
else:
|
540
|
-
data = getrqdata(ar.request)
|
541
|
-
columns = [str(x) for x in data.getlist(constants.URL_PARAM_COLUMNS)]
|
542
|
-
if columns:
|
543
|
-
all_widths = data.getlist(constants.URL_PARAM_WIDTHS)
|
544
|
-
hiddens = [
|
545
|
-
(x == "true") for x in data.getlist(constants.URL_PARAM_HIDDENS)
|
546
|
-
]
|
547
|
-
fields = []
|
548
|
-
widths = []
|
549
|
-
ah = ar.actor.get_handle()
|
550
|
-
for i, cn in enumerate(columns):
|
551
|
-
col = None
|
552
|
-
for e in ah.grid_layout.main.columns:
|
553
|
-
if e.name == cn:
|
554
|
-
col = e
|
555
|
-
break
|
556
|
-
if col is None:
|
557
|
-
raise Exception(
|
558
|
-
"No column named %r in %s"
|
559
|
-
% (cn, ar.ah.grid_layout.main.columns)
|
560
|
-
)
|
561
|
-
if not hiddens[i]:
|
562
|
-
fields.append(col)
|
563
|
-
widths.append(int(all_widths[i]))
|
564
|
-
else:
|
565
|
-
if column_names:
|
566
|
-
ll = ColumnsLayout(column_names, datasource=ar.actor)
|
567
|
-
lh = ll.get_layout_handle()
|
568
|
-
columns = lh.main.columns
|
569
|
-
columns = [e for e in columns if not e.hidden]
|
570
|
-
else:
|
571
|
-
ah = ar.actor.get_request_handle(ar)
|
572
|
-
|
573
|
-
columns = ah.grid_layout.main.columns
|
574
|
-
# print(20160530, ah, columns, ah.grid_layout.main)
|
575
|
-
|
576
|
-
# render them so that babelfields in hidden_languages
|
577
|
-
# get hidden:
|
578
|
-
for e in columns:
|
579
|
-
e.value = e.ext_options()
|
580
|
-
# try:
|
581
|
-
# e.value = e.ext_options()
|
582
|
-
# except AttributeError as ex:
|
583
|
-
# raise AttributeError("20160529 %s : %s" % (e, ex))
|
584
|
-
#
|
585
|
-
columns = [e for e in columns if not e.value.get("hidden", False)]
|
586
|
-
|
587
|
-
columns = [e for e in columns if not e.hidden]
|
588
|
-
|
589
|
-
# if str(ar.actor) == "isip.ExamPolicies":
|
590
|
-
# from lino.modlib.extjs.elems import is_hidden_babel_field
|
591
|
-
# print("20180103", [c.name for c in columns])
|
592
|
-
# print("20180103", [c.field for c in columns])
|
593
|
-
# print("20180103", [c.value['hidden'] for c in columns])
|
594
|
-
# print("20180103", [
|
595
|
-
# is_hidden_babel_field(c.field) for c in columns])
|
596
|
-
# print("20180103", [
|
597
|
-
# getattr(c.field, '_babel_language', None)
|
598
|
-
# for c in columns])
|
599
|
-
widths = ["%d" % (col.width or col.preferred_width) for col in columns]
|
600
|
-
# print("20180831 {}".format(widths))
|
601
|
-
# ~ 20130415 widths = ["%d%%" % (col.width or col.preferred_width) for col in columns]
|
602
|
-
# ~ fields = [col.field._lino_atomizer for col in columns]
|
603
|
-
fields = columns
|
604
|
-
|
605
|
-
headers = [column_header(col) for col in fields]
|
606
|
-
|
607
|
-
# if str(ar.actor).endswith("DailySlave"):
|
608
|
-
# print("20181022", fields[0].field.verbose_name)
|
609
|
-
|
610
|
-
oh = ar.actor.override_column_headers(ar)
|
611
|
-
if oh:
|
612
|
-
for i, e in enumerate(columns):
|
613
|
-
header = oh.get(e.name, None)
|
614
|
-
if header is not None:
|
615
|
-
headers[i] = header
|
616
|
-
# ~ print 20120507, oh, headers
|
617
|
-
|
618
|
-
return fields, headers, widths
|
619
|
-
|
620
|
-
u = ar.get_user()
|
621
|
-
if u is None:
|
622
|
-
return getit()
|
623
|
-
else:
|
624
|
-
return with_user_profile(u.user_type, getit)
|
625
|
-
|
626
|
-
def row2html(self, recno, columns, row, sums, **cellattrs):
|
627
|
-
has_numeric_value = False
|
628
|
-
cells = []
|
629
|
-
for i, col in enumerate(columns):
|
630
|
-
v = col.field._lino_atomizer.full_value_from_object(row, self)
|
631
|
-
if v is None:
|
632
|
-
td = E.td(**cellattrs)
|
633
|
-
else:
|
634
|
-
nv = col.value2num(v)
|
635
|
-
if nv != 0:
|
636
|
-
sums[i] += nv
|
637
|
-
has_numeric_value = True
|
638
|
-
td = col.value2html(self, v, **cellattrs)
|
639
|
-
# print("20240506 {} {}".format(col.__class__, tostring(td)))
|
640
|
-
col.apply_cell_format(td)
|
641
|
-
self.actor.apply_cell_format(self, row, col, recno, td)
|
642
|
-
cells.append(td)
|
643
|
-
if self.actor.hide_zero_rows and not has_numeric_value:
|
644
|
-
return None
|
645
|
-
return cells
|
646
|
-
|
647
|
-
def row2text(self, fields, row, sums):
|
648
|
-
"""
|
649
|
-
Render the given `row` into an iteration of text cells, using the given
|
650
|
-
list of `fields` and collecting sums into `sums`.
|
651
|
-
"""
|
652
|
-
# print(20160530, fields)
|
653
|
-
for i, fld in enumerate(fields):
|
654
|
-
if fld.field is not None:
|
655
|
-
sf = get_atomizer(row.__class__, fld.field, fld.field.name)
|
656
|
-
# print(20160530, fld.field.name, sf)
|
657
|
-
if False:
|
658
|
-
try:
|
659
|
-
getter = sf.full_value_from_object
|
660
|
-
v = getter(row, self)
|
661
|
-
except Exception as e:
|
662
|
-
raise Exception("20150218 %s: %s" % (sf, e))
|
663
|
-
# was used to find bug 20130422:
|
664
|
-
yield "%s:\n%s" % (fld.field, e)
|
665
|
-
continue
|
666
|
-
else:
|
667
|
-
getter = sf.full_value_from_object
|
668
|
-
v = getter(row, self)
|
669
|
-
|
670
|
-
if v is None:
|
671
|
-
# if not v:
|
672
|
-
yield ""
|
673
|
-
else:
|
674
|
-
sums[i] += fld.value2num(v)
|
675
|
-
# # In case you want the field name in error message:
|
676
|
-
# try:
|
677
|
-
# sums[i] += fld.value2num(v)
|
678
|
-
# except Exception as e:
|
679
|
-
# raise e.__class__("%s %s" % (fld.field, e))
|
680
|
-
yield fld.format_value(self, v)
|
681
|
-
|
682
|
-
def sums2html(self, columns, sums, **cellattrs):
|
683
|
-
sums = {fld.name: sums[i] for i, fld in enumerate(columns)}
|
684
|
-
return [
|
685
|
-
fld.sum2html(self, sums, i, **cellattrs) for i, fld in enumerate(columns)
|
686
|
-
]
|
687
|
-
|
688
|
-
def get_title(self):
|
689
|
-
# print(20200125, self.title, self.master_instance)
|
690
|
-
if self.title is not None:
|
691
|
-
return self.title
|
692
|
-
# if self.master_instance is not None:
|
693
|
-
# self.master_instance
|
694
|
-
return self.actor.get_title(self)
|
695
|
-
|
696
|
-
def get_status(self, **kw):
|
697
|
-
"""Extends :meth:`lino.core.requests.ActorRequest.get_status`."""
|
698
|
-
if self._status is not None and not kw:
|
699
|
-
return self._status
|
700
|
-
# print("20230331 712 get_status() {}".format(self.subst_user))
|
701
|
-
self._status = kw = super().get_status(**kw)
|
702
|
-
bp = kw["base_params"]
|
703
|
-
if self.quick_search:
|
704
|
-
bp[constants.URL_PARAM_FILTER] = self.quick_search
|
705
|
-
|
706
|
-
if self.order_by:
|
707
|
-
sort = self.order_by[0]
|
708
|
-
if sort.startswith("-"):
|
709
|
-
sort = sort[1:]
|
710
|
-
bp[constants.URL_PARAM_SORTDIR] = "DESC"
|
711
|
-
bp[constants.URL_PARAM_SORT] = sort
|
712
|
-
|
713
|
-
if self.known_values:
|
714
|
-
for k, v in self.known_values.items():
|
715
|
-
if self.actor.known_values.get(k, None) != v:
|
716
|
-
bp[k] = v
|
717
|
-
if self.master_instance is not None:
|
718
|
-
if isinstance(self.master_instance, (models.Model, TableRow)):
|
719
|
-
bp[constants.URL_PARAM_MASTER_PK] = self.master_instance.pk
|
720
|
-
if ContentType is not None and isinstance(
|
721
|
-
self.master_instance, models.Model
|
722
|
-
):
|
723
|
-
assert not self.master_instance._meta.abstract
|
724
|
-
mt = ContentType.objects.get_for_model(
|
725
|
-
self.master_instance.__class__
|
726
|
-
).pk
|
727
|
-
bp[constants.URL_PARAM_MASTER_TYPE] = mt
|
728
|
-
# else:
|
729
|
-
# logger.warning("20141205 %s %s",
|
730
|
-
# self.master_instance,
|
731
|
-
# ContentType)
|
732
|
-
else: # if self.master is None:
|
733
|
-
# e.g. in accounting.MovementsByMatch the master is a `str`
|
734
|
-
bp[constants.URL_PARAM_MASTER_PK] = self.master_instance
|
735
|
-
# raise Exception("No master key for {} {!r}".format(
|
736
|
-
# self.master_instance.__class__, self.master_instance))
|
737
|
-
return kw
|
738
|
-
|
739
|
-
def __repr__(self):
|
740
|
-
kw = dict()
|
741
|
-
if self.master_instance is not None:
|
742
|
-
kw.update(master_instance=obj2str(self.master_instance))
|
743
|
-
if self.filter is not None:
|
744
|
-
kw.update(filter=repr(self.filter))
|
745
|
-
if self.known_values:
|
746
|
-
kw.update(known_values=self.known_values)
|
747
|
-
if self.requesting_panel:
|
748
|
-
kw.update(requesting_panel=self.requesting_panel)
|
749
|
-
u = self.get_user()
|
750
|
-
if u is not None:
|
751
|
-
kw.update(user=u.username)
|
752
|
-
if False: # self.request:
|
753
|
-
kw.update(request=format_request(self.request))
|
754
|
-
return "<%s %s(%s)>" % (
|
755
|
-
self.__class__.__name__,
|
756
|
-
self.bound_action.full_name(),
|
757
|
-
kw,
|
758
|
-
)
|