lino 24.10.0__py3-none-any.whl → 24.10.2__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/fields.py +2 -1
- 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.2.dist-info}/METADATA +1 -1
- {lino-24.10.0.dist-info → lino-24.10.2.dist-info}/RECORD +26 -27
- lino/core/tablerequest.py +0 -758
- {lino-24.10.0.dist-info → lino-24.10.2.dist-info}/WHEEL +0 -0
- {lino-24.10.0.dist-info → lino-24.10.2.dist-info}/licenses/AUTHORS.rst +0 -0
- {lino-24.10.0.dist-info → lino-24.10.2.dist-info}/licenses/COPYING +0 -0
lino/core/requests.py
CHANGED
@@ -6,6 +6,10 @@ See introduction in :doc:`/dev/ar` and API reference in
|
|
6
6
|
:class:`lino.api.core.Request`.
|
7
7
|
"""
|
8
8
|
|
9
|
+
# from past.utils import old_div
|
10
|
+
|
11
|
+
import sys
|
12
|
+
import json
|
9
13
|
import html
|
10
14
|
import logging
|
11
15
|
from io import StringIO
|
@@ -14,6 +18,7 @@ from contextlib import contextmanager
|
|
14
18
|
from types import GeneratorType
|
15
19
|
from copy import copy, deepcopy
|
16
20
|
from xml.sax.saxutils import escape
|
21
|
+
from etgen import html as xghtml
|
17
22
|
|
18
23
|
from django.conf import settings
|
19
24
|
from django.utils.translation import gettext_lazy as _
|
@@ -22,17 +27,20 @@ from django.utils.translation import get_language, activate
|
|
22
27
|
from django.utils import translation
|
23
28
|
from django.utils import timezone
|
24
29
|
from django.core.mail import send_mail
|
30
|
+
from django.core.exceptions import ObjectDoesNotExist, SuspiciousOperation
|
25
31
|
from django.core import exceptions
|
26
32
|
from django.db import models
|
33
|
+
from django.db.models.query import QuerySet
|
27
34
|
from asgiref.sync import sync_to_async
|
28
35
|
|
29
36
|
from lino.utils.html import E, tostring, iselement
|
30
|
-
from lino.core import constants
|
31
37
|
from lino.utils import AttrDict
|
32
38
|
from lino.utils import MissingRow
|
33
39
|
from lino.utils.html import html2text
|
40
|
+
from lino.core import constants
|
34
41
|
from lino.core import callbacks
|
35
42
|
from lino.core import actions
|
43
|
+
from lino.core.tables import AbstractTable
|
36
44
|
from lino.core.boundaction import BoundAction
|
37
45
|
from lino.core.signals import on_ui_created, pre_ui_save
|
38
46
|
from lino.core.diff import ChangeWatcher
|
@@ -40,12 +48,23 @@ from lino.core.utils import getrqdata
|
|
40
48
|
from lino.core.utils import obj2unicode
|
41
49
|
from lino.core.utils import obj2str
|
42
50
|
from lino.core.utils import UnresolvedModel
|
51
|
+
from lino.core.utils import format_request
|
52
|
+
from lino.core.utils import PhantomRow
|
53
|
+
from lino.core.store import get_atomizer
|
43
54
|
from lino.core.exceptions import ChangedAPI
|
44
55
|
|
45
|
-
try:
|
56
|
+
# try:
|
57
|
+
# from django.contrib.contenttypes.models import ContentType
|
58
|
+
# except RuntimeError:
|
59
|
+
# pass
|
60
|
+
|
61
|
+
if settings.SITE.is_installed("contenttypes"):
|
46
62
|
from django.contrib.contenttypes.models import ContentType
|
47
|
-
|
48
|
-
|
63
|
+
else:
|
64
|
+
ContentType = None
|
65
|
+
|
66
|
+
from .fields import RemoteField, FakeField, TableRow
|
67
|
+
|
49
68
|
|
50
69
|
CATCHED_AJAX_EXCEPTIONS = (Warning, exceptions.ValidationError)
|
51
70
|
|
@@ -217,29 +236,6 @@ class ValidActionResponses(object):
|
|
217
236
|
editing_mode = False
|
218
237
|
|
219
238
|
|
220
|
-
class VirtualRow(object):
|
221
|
-
def __init__(self, **kw):
|
222
|
-
self.update(**kw)
|
223
|
-
|
224
|
-
def update(self, **kw):
|
225
|
-
for k, v in list(kw.items()):
|
226
|
-
setattr(self, k, v)
|
227
|
-
|
228
|
-
def get_row_permission(self, ar, state, ba):
|
229
|
-
if ba.action.readonly:
|
230
|
-
return True
|
231
|
-
return False
|
232
|
-
|
233
|
-
|
234
|
-
class PhantomRow(VirtualRow):
|
235
|
-
def __init__(self, request, **kw):
|
236
|
-
self._ar = request
|
237
|
-
VirtualRow.__init__(self, **kw)
|
238
|
-
|
239
|
-
def __str__(self):
|
240
|
-
return str(self._ar.get_action_title())
|
241
|
-
|
242
|
-
|
243
239
|
inheritable_attrs = frozenset(
|
244
240
|
"user subst_user renderer requesting_panel master_instance logger".split()
|
245
241
|
)
|
@@ -251,7 +247,58 @@ def bool2text(x):
|
|
251
247
|
return _("No")
|
252
248
|
|
253
249
|
|
254
|
-
|
250
|
+
|
251
|
+
class SearchQuerySet:
|
252
|
+
pass
|
253
|
+
|
254
|
+
|
255
|
+
class SearchQuery:
|
256
|
+
pass
|
257
|
+
|
258
|
+
|
259
|
+
if settings.SITE.is_installed("search"):
|
260
|
+
if settings.SITE.use_elasticsearch:
|
261
|
+
try:
|
262
|
+
from elasticsearch_django.models import SearchQuery
|
263
|
+
except ImportError:
|
264
|
+
pass
|
265
|
+
elif settings.SITE.use_solr:
|
266
|
+
try:
|
267
|
+
from haystack.query import SearchQuerySet
|
268
|
+
except ImportError:
|
269
|
+
pass
|
270
|
+
|
271
|
+
WARNINGS_LOGGED = dict()
|
272
|
+
|
273
|
+
|
274
|
+
def column_header(col):
|
275
|
+
# ~ if col.label:
|
276
|
+
# ~ return join_elems(col.label.split('\n'),sep=E.br)
|
277
|
+
# ~ return [unicode(col.name)]
|
278
|
+
label = col.get_label()
|
279
|
+
if label is None:
|
280
|
+
return col.name
|
281
|
+
return str(label)
|
282
|
+
|
283
|
+
|
284
|
+
def sliced_data_iterator(qs, offset, limit):
|
285
|
+
# qs is either a Django QuerySet or iterable
|
286
|
+
# When limit is None or 0, offset cannot be -1
|
287
|
+
if offset is not None:
|
288
|
+
if isinstance(qs, QuerySet):
|
289
|
+
num = qs.count()
|
290
|
+
else:
|
291
|
+
num = len(qs)
|
292
|
+
if offset == -1:
|
293
|
+
page_num = num // limit
|
294
|
+
offset = limit * page_num
|
295
|
+
qs = qs[offset:num]
|
296
|
+
if limit is not None and limit != 0:
|
297
|
+
qs = qs[:limit]
|
298
|
+
return qs
|
299
|
+
|
300
|
+
|
301
|
+
class BaseRequest:
|
255
302
|
# Base class of all :term:`action requests <action request>`.
|
256
303
|
user = None
|
257
304
|
subst_user = None
|
@@ -272,23 +319,8 @@ class BaseRequest(object):
|
|
272
319
|
xcallback_answers = {}
|
273
320
|
row_meta = None
|
274
321
|
logger = logger
|
275
|
-
_status = None # cache when get_status() is called multiple times
|
276
322
|
_alogger = None
|
277
|
-
|
278
|
-
@property
|
279
|
-
def alogger(self):
|
280
|
-
if self._alogger is None:
|
281
|
-
self._alogger = AttrDict(
|
282
|
-
{
|
283
|
-
"info": sync_to_async(self.logger.info),
|
284
|
-
"debug": sync_to_async(self.logger.debug),
|
285
|
-
"warning": sync_to_async(self.logger.warning),
|
286
|
-
"warn": sync_to_async(self.logger.warn),
|
287
|
-
"error": sync_to_async(self.logger.error),
|
288
|
-
"exception": sync_to_async(self.logger.exception),
|
289
|
-
}
|
290
|
-
)
|
291
|
-
return self._alogger
|
323
|
+
_status = None # cache when get_status() is called multiple times
|
292
324
|
|
293
325
|
def __init__(
|
294
326
|
self,
|
@@ -297,10 +329,9 @@ class BaseRequest(object):
|
|
297
329
|
hash_router=None,
|
298
330
|
is_on_main_actor=True,
|
299
331
|
permalink_uris=None,
|
300
|
-
**kw
|
332
|
+
**kw
|
301
333
|
):
|
302
334
|
self.response = dict()
|
303
|
-
|
304
335
|
if request is not None:
|
305
336
|
assert parent is None
|
306
337
|
self.request = request
|
@@ -324,7 +355,8 @@ class BaseRequest(object):
|
|
324
355
|
else:
|
325
356
|
kw[k] = getattr(parent, k)
|
326
357
|
kv = kw.setdefault("known_values", {})
|
327
|
-
|
358
|
+
if parent.actor is self.actor:
|
359
|
+
kv.update(parent.known_values)
|
328
360
|
# kw.setdefault('user', parent.user)
|
329
361
|
# kw.setdefault('subst_user', parent.subst_user)
|
330
362
|
# kw.setdefault('renderer', parent.renderer)
|
@@ -343,7 +375,9 @@ class BaseRequest(object):
|
|
343
375
|
self.is_on_main_actor = is_on_main_actor
|
344
376
|
self.permalink_uris = permalink_uris
|
345
377
|
# self.hash_router = hash_router
|
378
|
+
|
346
379
|
self.setup(**kw)
|
380
|
+
|
347
381
|
if self.master is not None and settings.SITE.strict_master_check:
|
348
382
|
if self.master_instance is None:
|
349
383
|
raise exceptions.BadRequest(
|
@@ -363,8 +397,6 @@ class BaseRequest(object):
|
|
363
397
|
user=None,
|
364
398
|
subst_user=None,
|
365
399
|
current_project=None,
|
366
|
-
selected_pks=None,
|
367
|
-
selected_rows=None,
|
368
400
|
master=None,
|
369
401
|
master_instance=None,
|
370
402
|
master_key=None,
|
@@ -388,9 +420,6 @@ class BaseRequest(object):
|
|
388
420
|
renderer = settings.SITE.kernel.text_renderer
|
389
421
|
self.renderer = renderer
|
390
422
|
self.subst_user = subst_user
|
391
|
-
if selected_rows is not None:
|
392
|
-
self.selected_rows = selected_rows
|
393
|
-
assert selected_pks is None
|
394
423
|
if xcallback_answers is not None:
|
395
424
|
self.xcallback_answers = xcallback_answers
|
396
425
|
|
@@ -411,15 +440,23 @@ class BaseRequest(object):
|
|
411
440
|
)
|
412
441
|
self.master_instance = self.actor.cast_master_instance(master_instance)
|
413
442
|
|
414
|
-
# set_selected_pks() is called when the master_instance has been set.
|
415
|
-
# e.g. SuggestedMovements.set_selected_pks() needs to know the voucher
|
416
|
-
# because the D/C of a DueMovement depends on the "target".
|
417
|
-
|
418
|
-
if selected_pks is not None:
|
419
|
-
self.set_selected_pks(*selected_pks)
|
420
|
-
|
421
443
|
self.row_meta = dict(meta=True)
|
422
444
|
|
445
|
+
@property
|
446
|
+
def alogger(self):
|
447
|
+
if self._alogger is None:
|
448
|
+
self._alogger = AttrDict(
|
449
|
+
{
|
450
|
+
"info": sync_to_async(self.logger.info),
|
451
|
+
"debug": sync_to_async(self.logger.debug),
|
452
|
+
"warning": sync_to_async(self.logger.warning),
|
453
|
+
"warn": sync_to_async(self.logger.warn),
|
454
|
+
"error": sync_to_async(self.logger.error),
|
455
|
+
"exception": sync_to_async(self.logger.exception),
|
456
|
+
}
|
457
|
+
)
|
458
|
+
return self._alogger
|
459
|
+
|
423
460
|
@contextmanager
|
424
461
|
def capture_logger(self, level=logging.INFO):
|
425
462
|
old_logger = self.logger
|
@@ -461,10 +498,64 @@ class BaseRequest(object):
|
|
461
498
|
self.row_meta = dict(meta=True)
|
462
499
|
|
463
500
|
def parse_req(self, request, rqdata, **kw):
|
464
|
-
"""
|
465
|
-
Parse the given incoming HttpRequest and set up this action
|
501
|
+
"""Parse the given incoming HttpRequest and set up this action
|
466
502
|
request from it.
|
503
|
+
|
504
|
+
The `mt` url param is parsed only when needed. Usually it is
|
505
|
+
not needed because the `master_class` is constant and known
|
506
|
+
per actor. But there are exceptions:
|
507
|
+
|
508
|
+
- `master` is `ContentType`
|
509
|
+
|
510
|
+
- `master` is some abstract model
|
511
|
+
|
512
|
+
- `master` is not a subclass of Model, e.g.
|
513
|
+
:class:`lino_xl.lib.polls.AnswersByResponse`, a
|
514
|
+
virtual table that defines :meth:`get_row_by_pk
|
515
|
+
<lino.core.actors.Actor.get_row_by_pk>`.
|
516
|
+
|
467
517
|
"""
|
518
|
+
# logger.info("20120723 %s.parse_req() %s", self.actor, rqdata)
|
519
|
+
# ~ rh = self.ah
|
520
|
+
|
521
|
+
if "master_instance" not in kw:
|
522
|
+
kw.update(
|
523
|
+
master_type=rqdata.get(constants.URL_PARAM_MASTER_TYPE, None),
|
524
|
+
master_key=rqdata.get(constants.URL_PARAM_MASTER_PK, None),
|
525
|
+
)
|
526
|
+
|
527
|
+
# if settings.SITE.use_filterRow:
|
528
|
+
# exclude = dict()
|
529
|
+
# for f in self.ah.store.fields:
|
530
|
+
# if f.field:
|
531
|
+
# filterOption = rqdata.get(
|
532
|
+
# 'filter[%s_filterOption]' % f.field.name)
|
533
|
+
# if filterOption == 'empty':
|
534
|
+
# kw[f.field.name + "__isnull"] = True
|
535
|
+
# elif filterOption == 'notempty':
|
536
|
+
# kw[f.field.name + "__isnull"] = False
|
537
|
+
# else:
|
538
|
+
# filterValue = rqdata.get('filter[%s]' % f.field.name)
|
539
|
+
# if filterValue:
|
540
|
+
# if not filterOption:
|
541
|
+
# filterOption = 'contains'
|
542
|
+
# if filterOption == 'contains':
|
543
|
+
# kw[f.field.name + "__icontains"] = filterValue
|
544
|
+
# elif filterOption == 'doesnotcontain':
|
545
|
+
# exclude[f.field.name +
|
546
|
+
# "__icontains"] = filterValue
|
547
|
+
# else:
|
548
|
+
# print("unknown filterOption %r" % filterOption)
|
549
|
+
# if len(exclude):
|
550
|
+
# kw.update(exclude=exclude)
|
551
|
+
|
552
|
+
if settings.SITE.use_gridfilters:
|
553
|
+
filter = rqdata.get(constants.URL_PARAM_GRIDFILTER, None)
|
554
|
+
if filter is not None:
|
555
|
+
filter = json.loads(filter)
|
556
|
+
kw["gridfilters"] = [constants.dict2kw(flt) for flt in filter]
|
557
|
+
|
558
|
+
# kw = ActionRequest.parse_req(self, request, rqdata, **kw)
|
468
559
|
if settings.SITE.user_model:
|
469
560
|
kw.update(user=request.user)
|
470
561
|
kw.update(subst_user=request.subst_user)
|
@@ -483,10 +574,6 @@ class BaseRequest(object):
|
|
483
574
|
# logger.info("20150130 b %s", tab)
|
484
575
|
self.set_response(active_tab=tab)
|
485
576
|
|
486
|
-
if not "selected_pks" in kw and not "selected_rows" in kw:
|
487
|
-
selected = rqdata.getlist(constants.URL_PARAM_SELECTED)
|
488
|
-
kw.update(selected_pks=selected)
|
489
|
-
|
490
577
|
kw.update(
|
491
578
|
xcallback_answers={
|
492
579
|
id: rqdata[id]
|
@@ -506,6 +593,54 @@ class BaseRequest(object):
|
|
506
593
|
# ~ except settings.SITE.user_model.DoesNotExist, e:
|
507
594
|
# ~ pass
|
508
595
|
# logger.info("20140503 ActionRequest.parse_req() %s", kw)
|
596
|
+
|
597
|
+
# if str(self.actor) == 'comments.CommentsByRFC':
|
598
|
+
# print("20230426", kw)
|
599
|
+
|
600
|
+
quick_search = rqdata.get(constants.URL_PARAM_FILTER, None)
|
601
|
+
if quick_search:
|
602
|
+
kw.update(quick_search=quick_search)
|
603
|
+
|
604
|
+
sort = rqdata.get(constants.URL_PARAM_SORT, None)
|
605
|
+
if sort:
|
606
|
+
sortfld = self.actor.get_data_elem(sort)
|
607
|
+
if isinstance(sortfld, FakeField):
|
608
|
+
sort = sortfld.sortable_by
|
609
|
+
# sort might be None when user asked to sort a virtual
|
610
|
+
# field without sortable_by.
|
611
|
+
else:
|
612
|
+
sort = [sort]
|
613
|
+
# print("20231006 sort", sortfld, sort)
|
614
|
+
if sort is not None:
|
615
|
+
|
616
|
+
def si(k):
|
617
|
+
if k[0] == "-":
|
618
|
+
return k[1:]
|
619
|
+
else:
|
620
|
+
return "-" + k
|
621
|
+
|
622
|
+
sort_dir = rqdata.get(constants.URL_PARAM_SORTDIR, "ASC")
|
623
|
+
if sort_dir == "DESC":
|
624
|
+
sort = [si(k) for k in sort]
|
625
|
+
# sort = ['-' + k for k in sort]
|
626
|
+
# print("20171123", sort)
|
627
|
+
kw.update(order_by=sort)
|
628
|
+
|
629
|
+
if self.actor is not None and issubclass(self.actor, AbstractTable):
|
630
|
+
try:
|
631
|
+
offset = rqdata.get(constants.URL_PARAM_START, None)
|
632
|
+
if offset:
|
633
|
+
kw.update(offset=int(offset))
|
634
|
+
limit = rqdata.get(constants.URL_PARAM_LIMIT, self.actor.preview_limit)
|
635
|
+
if limit:
|
636
|
+
kw.update(limit=int(limit))
|
637
|
+
except ValueError:
|
638
|
+
# Example: invalid literal for int() with base 10:
|
639
|
+
# 'fdpkvcnrfdybhur'
|
640
|
+
raise SuspiciousOperation("Invalid value for limit or offset")
|
641
|
+
|
642
|
+
kw = self.actor.parse_req(request, rqdata, **kw)
|
643
|
+
# print("20171123 %s.parse_req() --> %s" % (self, kw))
|
509
644
|
return kw
|
510
645
|
|
511
646
|
def setup_from(self, other):
|
@@ -633,16 +768,17 @@ class BaseRequest(object):
|
|
633
768
|
"""
|
634
769
|
# ~ print 20131003, selected_pks
|
635
770
|
self.selected_rows = []
|
636
|
-
for pk in selected_pks:
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
771
|
+
# for pk in selected_pks:
|
772
|
+
# if pk and pk != "-99998" and pk != "-99999":
|
773
|
+
# self.selected_rows.append(self.get_row_by_pk(pk))
|
774
|
+
try:
|
775
|
+
for pk in selected_pks:
|
776
|
+
if pk and pk != "-99998" and pk != "-99999":
|
777
|
+
self.selected_rows.append(self.get_row_by_pk(pk))
|
778
|
+
except ObjectDoesNotExist:
|
779
|
+
# raise exceptions.BadRequest(
|
780
|
+
raise ObjectDoesNotExist(
|
781
|
+
f"No row with primary key {pk} in {self.actor}") from None
|
646
782
|
# self.selected_rows = filter(lambda x: x, self.selected_rows)
|
647
783
|
# note: ticket #523 was because the GET contained an empty pk ("&sr=")
|
648
784
|
|
@@ -952,20 +1088,6 @@ class BaseRequest(object):
|
|
952
1088
|
# logger.info("20140430 set_content_type(%r)", ct)
|
953
1089
|
self.content_type = ct
|
954
1090
|
|
955
|
-
def must_execute(self):
|
956
|
-
return True
|
957
|
-
|
958
|
-
def get_total_count(self):
|
959
|
-
"""
|
960
|
-
TableRequest overrides this to return the number of rows.
|
961
|
-
|
962
|
-
For other requests we assume that there is one row. This is used e.g.
|
963
|
-
when courses.StatusReport is shown in the the dashboard. A Report
|
964
|
-
returns always 1 because otherwise the dashboard believes it is empty.
|
965
|
-
|
966
|
-
"""
|
967
|
-
return 1
|
968
|
-
|
969
1091
|
def get_data_value(self, obj, name):
|
970
1092
|
"""
|
971
1093
|
Return the value of the virtual field `name` for this action
|
@@ -1015,7 +1137,7 @@ class BaseRequest(object):
|
|
1015
1137
|
"""
|
1016
1138
|
return self.subst_user or self.user
|
1017
1139
|
|
1018
|
-
def run(self, ia, *args, **kwargs):
|
1140
|
+
def run(self, ia, *args, **kwargs): # BaseRequest
|
1019
1141
|
"""
|
1020
1142
|
Run the given :term:`instance action` `ia` in a child request of this
|
1021
1143
|
request.
|
@@ -1520,43 +1642,369 @@ class ActionRequest(BaseRequest):
|
|
1520
1642
|
- :meth:`lino.core.actors.Actor.request`
|
1521
1643
|
- :meth:`lino.core.actions.Action.request`
|
1522
1644
|
|
1523
|
-
|
1524
1645
|
"""
|
1525
1646
|
|
1526
1647
|
create_kw = None
|
1527
1648
|
renderer = None
|
1528
|
-
|
1529
1649
|
offset = None
|
1530
1650
|
limit = None
|
1531
1651
|
order_by = None
|
1652
|
+
master = None
|
1653
|
+
extra = None
|
1654
|
+
title = None
|
1655
|
+
filter = None
|
1656
|
+
limit = None
|
1657
|
+
offset = None
|
1658
|
+
|
1659
|
+
_data_iterator = None
|
1660
|
+
_sliced_data_iterator = None
|
1661
|
+
_insert_sar = None
|
1532
1662
|
|
1533
1663
|
def __init__(
|
1534
1664
|
self,
|
1535
1665
|
actor=None,
|
1536
|
-
unused_request=None,
|
1537
1666
|
action=None,
|
1538
|
-
unused_renderer=None,
|
1539
1667
|
rqdata=None,
|
1540
|
-
**kw
|
1668
|
+
**kw
|
1541
1669
|
):
|
1542
1670
|
# print("20170116 ActionRequest.__init__()", actor, kw)
|
1543
|
-
assert unused_renderer is None
|
1544
|
-
assert unused_request is None
|
1545
1671
|
self.actor = actor
|
1546
1672
|
self.rqdata = rqdata
|
1547
1673
|
self.bound_action = action or actor.default_action
|
1548
|
-
|
1674
|
+
super().__init__(**kw)
|
1549
1675
|
if not actor.is_abstract():
|
1550
1676
|
self.ah = actor.get_request_handle(self)
|
1551
1677
|
# if self.ah.store is None:
|
1552
1678
|
# raise Exception("20240530 {} has no store!?".format(self.ah))
|
1553
1679
|
|
1680
|
+
def parse_req(self, request, rqdata, **kw):
|
1681
|
+
if not "selected_pks" in kw and not "selected_rows" in kw:
|
1682
|
+
selected = rqdata.getlist(constants.URL_PARAM_SELECTED)
|
1683
|
+
kw.update(selected_pks=selected)
|
1684
|
+
return super().parse_req(request, rqdata, **kw)
|
1685
|
+
|
1686
|
+
def setup(
|
1687
|
+
self,
|
1688
|
+
known_values=None,
|
1689
|
+
param_values=None,
|
1690
|
+
action_param_values={},
|
1691
|
+
quick_search=None,
|
1692
|
+
order_by=None,
|
1693
|
+
offset=None,
|
1694
|
+
limit=None,
|
1695
|
+
title=None,
|
1696
|
+
filter=None,
|
1697
|
+
gridfilters=None,
|
1698
|
+
exclude=None,
|
1699
|
+
selected_pks=None,
|
1700
|
+
selected_rows=None,
|
1701
|
+
extra=None,
|
1702
|
+
**kw
|
1703
|
+
):
|
1704
|
+
self.quick_search = quick_search
|
1705
|
+
self.order_by = order_by
|
1706
|
+
self.filter = filter
|
1707
|
+
self.gridfilters = gridfilters
|
1708
|
+
self.extra = extra
|
1709
|
+
|
1710
|
+
if title is not None:
|
1711
|
+
self.title = title
|
1712
|
+
if offset is not None:
|
1713
|
+
self.offset = offset
|
1714
|
+
if limit is not None:
|
1715
|
+
self.limit = limit
|
1716
|
+
|
1717
|
+
if selected_rows is not None:
|
1718
|
+
assert selected_pks is None
|
1719
|
+
self.selected_rows = selected_rows
|
1720
|
+
|
1721
|
+
super().setup(**kw)
|
1722
|
+
|
1723
|
+
if self.bound_action is None:
|
1724
|
+
return # 20200825 e.g. a request on an abstract table
|
1725
|
+
assert self.actor is not None
|
1726
|
+
request = self.request
|
1727
|
+
|
1728
|
+
kv = dict()
|
1729
|
+
for k, v in self.actor.known_values.items():
|
1730
|
+
kv.setdefault(k, v)
|
1731
|
+
if known_values:
|
1732
|
+
kv.update(known_values)
|
1733
|
+
self.known_values = kv
|
1734
|
+
|
1735
|
+
if self.actor.parameters is not None:
|
1736
|
+
pv = self.actor.param_defaults(self)
|
1737
|
+
for k in pv.keys():
|
1738
|
+
if k not in self.actor.parameters:
|
1739
|
+
raise Exception(
|
1740
|
+
"%s.param_defaults() returned invalid keyword %r"
|
1741
|
+
% (self.actor, k)
|
1742
|
+
)
|
1743
|
+
|
1744
|
+
# New since 20120913. E.g. newcomers.Newcomers is a
|
1745
|
+
# simple pcsw.Clients with
|
1746
|
+
# known_values=dict(client_state=newcomer) and since there
|
1747
|
+
# is a parameter `client_state`, we override that
|
1748
|
+
# parameter's default value.
|
1749
|
+
|
1750
|
+
for k, v in self.known_values.items():
|
1751
|
+
if k in pv:
|
1752
|
+
pv[k] = v
|
1753
|
+
|
1754
|
+
# New since 20120914. MyClientsByGroup has a `group` as
|
1755
|
+
# master, this must also appear as `group` parameter
|
1756
|
+
# value. Lino now understands tables where the master_key
|
1757
|
+
# is also a parameter.
|
1758
|
+
|
1759
|
+
if self.actor.master_key is not None:
|
1760
|
+
if self.actor.master_key in pv:
|
1761
|
+
pv[self.actor.master_key] = self.master_instance
|
1762
|
+
if param_values is None:
|
1763
|
+
if self.actor.params_layout is None:
|
1764
|
+
pass # 20200825 e.g. users.Users
|
1765
|
+
# raise Exception(
|
1766
|
+
# "{} has parameters ({}) but no params_layout. {}".format(
|
1767
|
+
# self.actor, self.actor.parameters, self.actor._setup_done))
|
1768
|
+
|
1769
|
+
elif request is not None:
|
1770
|
+
# call get_layout_handle to make sure that
|
1771
|
+
# params_store has been created:
|
1772
|
+
self.actor.params_layout.get_layout_handle()
|
1773
|
+
ps = self.actor.params_layout.params_store
|
1774
|
+
# print('20160329 requests.py', ps, self.actor.parameters)
|
1775
|
+
if ps is not None:
|
1776
|
+
pv.update(ps.parse_params(request))
|
1777
|
+
else:
|
1778
|
+
raise Exception(
|
1779
|
+
"20160329 params_layout {0} has no params_store "
|
1780
|
+
"in {1!r}".format(self.actor.params_layout, self.actor)
|
1781
|
+
)
|
1782
|
+
else:
|
1783
|
+
for k in param_values.keys():
|
1784
|
+
if k not in pv:
|
1785
|
+
raise Exception(
|
1786
|
+
"Invalid key '%s' in param_values of %s "
|
1787
|
+
"request (possible keys are %s)"
|
1788
|
+
% (k, self.actor, list(pv.keys()))
|
1789
|
+
)
|
1790
|
+
pv.update(param_values)
|
1791
|
+
# print("20160329 ok", pv)
|
1792
|
+
self.param_values = AttrDict(**pv)
|
1793
|
+
# self.actor.check_params(self.param_values)
|
1794
|
+
|
1795
|
+
action = self.bound_action.action
|
1796
|
+
if action.parameters is not None:
|
1797
|
+
if len(self.selected_rows) == 1:
|
1798
|
+
apv = action.action_param_defaults(self, self.selected_rows[0])
|
1799
|
+
else:
|
1800
|
+
apv = action.action_param_defaults(self, None)
|
1801
|
+
# msg = "20170116 selected_rows is {} for {!r}".format(
|
1802
|
+
# self.selected_rows, action)
|
1803
|
+
# raise Exception(msg)
|
1804
|
+
if request is not None:
|
1805
|
+
apv.update(action.params_layout.params_store.parse_params(request))
|
1806
|
+
self.action_param_values = AttrDict(**apv)
|
1807
|
+
# action.check_params(action_param_values)
|
1808
|
+
self.set_action_param_values(**action_param_values)
|
1809
|
+
self.bound_action.setup_action_request(self)
|
1810
|
+
|
1811
|
+
self.actor.setup_request(self)
|
1812
|
+
|
1813
|
+
# if str(self.actor) == 'outbox.MyOutbox':
|
1814
|
+
# if self.master_instance is None:
|
1815
|
+
# raise Exception("20230426 b {}".format(self.master))
|
1816
|
+
|
1817
|
+
if issubclass(self.actor, AbstractTable):
|
1818
|
+
|
1819
|
+
self.exclude = exclude or self.actor.exclude
|
1820
|
+
self.page_length = self.actor.page_length
|
1821
|
+
|
1822
|
+
if isinstance(self.actor.master_field, RemoteField):
|
1823
|
+
# cannot insert rows in a slave table with a remote master
|
1824
|
+
# field
|
1825
|
+
self.create_kw = None
|
1826
|
+
elif isinstance(self.master_instance, MissingRow):
|
1827
|
+
self.create_kw = None # 20230426
|
1828
|
+
else:
|
1829
|
+
self.create_kw = self.actor.get_filter_kw(self)
|
1830
|
+
|
1831
|
+
# set_selected_pks() needs the master_instance and the param_values to
|
1832
|
+
# be set up. e.g. SuggestedMovements.set_selected_pks() needs to know
|
1833
|
+
# the voucher because the D/C of a DueMovement depends on the "target".
|
1834
|
+
# Or working.MySessionsByDay needs param_values
|
1835
|
+
|
1836
|
+
if selected_pks is not None:
|
1837
|
+
# self.param_values = None
|
1838
|
+
# print(f"20241003 {selected_pks}")
|
1839
|
+
self.set_selected_pks(*selected_pks)
|
1840
|
+
|
1554
1841
|
def __str__(self):
|
1555
1842
|
return "{0} {1}".format(self.__class__.__name__, self.bound_action)
|
1556
1843
|
|
1557
1844
|
def __repr__(self):
|
1558
1845
|
return "{0} {1}".format(self.__class__.__name__, self.bound_action)
|
1559
1846
|
|
1847
|
+
def gen_insert_button(
|
1848
|
+
self, target, button_attrs=dict(style="float: right;"), **values
|
1849
|
+
):
|
1850
|
+
"""
|
1851
|
+
Generate an insert button using a cached insertable object.
|
1852
|
+
|
1853
|
+
This is functionally equivalent to saying::
|
1854
|
+
|
1855
|
+
if self.insert_action is not None:
|
1856
|
+
ir = self.insert_action.request_from(ar)
|
1857
|
+
if ir.get_permission():
|
1858
|
+
return ir.ar2button()
|
1859
|
+
|
1860
|
+
The difference is that gen_insert_button is more efficient when you do
|
1861
|
+
this more than once during a single request.
|
1862
|
+
|
1863
|
+
`target` is the actor into which we want to insert an object.
|
1864
|
+
`button_label` is the optional button label.
|
1865
|
+
`values` is a dict of extra default values to apply to the insertable object.
|
1866
|
+
|
1867
|
+
First usage example is in :mod:`lino_xl.lib.calview`.
|
1868
|
+
|
1869
|
+
"""
|
1870
|
+
if self._insert_sar is None:
|
1871
|
+
self._insert_sar = target.insert_action.request_from(self)
|
1872
|
+
else:
|
1873
|
+
assert self._insert_sar.actor is target
|
1874
|
+
if self._insert_sar.get_permission():
|
1875
|
+
st = self._insert_sar.get_status()
|
1876
|
+
st["data_record"]["data"].update(values)
|
1877
|
+
# obj = st['data_record']
|
1878
|
+
# for k, v in values.items():
|
1879
|
+
# setattr(obj, k, v)
|
1880
|
+
# print(20200302, st['data_record'])
|
1881
|
+
return self._insert_sar.ar2button(**button_attrs)
|
1882
|
+
|
1883
|
+
def run(self, *args, **kw):
|
1884
|
+
"""
|
1885
|
+
Runs this action request.
|
1886
|
+
"""
|
1887
|
+
return self.bound_action.action.run_from_code(self, *args, **kw)
|
1888
|
+
|
1889
|
+
def execute(self):
|
1890
|
+
"""This will actually call the :meth:`get_data_iterator` and run the
|
1891
|
+
database query.
|
1892
|
+
|
1893
|
+
Automatically called when either :attr:`data_iterator`
|
1894
|
+
or :attr:`sliced_data_iterator` is accesed.
|
1895
|
+
|
1896
|
+
"""
|
1897
|
+
# print("20181121 execute", self.actor)
|
1898
|
+
try:
|
1899
|
+
self._data_iterator = self.get_data_iterator()
|
1900
|
+
except Warning as e:
|
1901
|
+
# ~ logger.info("20130809 Warning %s",e)
|
1902
|
+
self.no_data_text = str(e)
|
1903
|
+
self._data_iterator = []
|
1904
|
+
except Exception as e:
|
1905
|
+
if not settings.SITE.catch_layout_exceptions:
|
1906
|
+
raise
|
1907
|
+
# Report this exception. But since such errors may occur
|
1908
|
+
# rather often and since exception loggers usually send an
|
1909
|
+
# email to the local system admin, make sure to log each
|
1910
|
+
# exception only once.
|
1911
|
+
self.no_data_text = f"{e} (set catch_layout_exceptions to see details)"
|
1912
|
+
self._data_iterator = []
|
1913
|
+
w = WARNINGS_LOGGED.get(str(e))
|
1914
|
+
if w is None:
|
1915
|
+
WARNINGS_LOGGED[str(e)] = True
|
1916
|
+
# raise
|
1917
|
+
# logger.exception(e)
|
1918
|
+
logger.warning(f"Error while executing {repr(self)}: {e}\n"
|
1919
|
+
"(Subsequent warnings will be silenced.)")
|
1920
|
+
|
1921
|
+
if self._data_iterator is None:
|
1922
|
+
raise Exception(f"No data iterator for {self}")
|
1923
|
+
|
1924
|
+
if isinstance(self._data_iterator, GeneratorType):
|
1925
|
+
# print 20150718, self._data_iterator
|
1926
|
+
self._data_iterator = tuple(self._data_iterator)
|
1927
|
+
if isinstance(self._data_iterator, (SearchQuery, SearchQuerySet)):
|
1928
|
+
self._sliced_data_iterator = tuple(
|
1929
|
+
self.actor.get_rows_from_search_query(self._data_iterator, self)
|
1930
|
+
)
|
1931
|
+
else:
|
1932
|
+
self._sliced_data_iterator = sliced_data_iterator(
|
1933
|
+
self._data_iterator, self.offset, self.limit
|
1934
|
+
)
|
1935
|
+
# logger.info("20171116 executed : %s", self._sliced_data_iterator)
|
1936
|
+
|
1937
|
+
def must_execute(self):
|
1938
|
+
if issubclass(self.actor, AbstractTable):
|
1939
|
+
return self._data_iterator is None
|
1940
|
+
return True
|
1941
|
+
|
1942
|
+
def get_data_iterator_property(self):
|
1943
|
+
if self._data_iterator is None:
|
1944
|
+
self.execute()
|
1945
|
+
return self._data_iterator
|
1946
|
+
|
1947
|
+
def get_sliced_data_iterator_property(self):
|
1948
|
+
if self._sliced_data_iterator is None:
|
1949
|
+
self.execute()
|
1950
|
+
return self._sliced_data_iterator
|
1951
|
+
|
1952
|
+
data_iterator = property(get_data_iterator_property)
|
1953
|
+
sliced_data_iterator = property(get_sliced_data_iterator_property)
|
1954
|
+
|
1955
|
+
def get_data_iterator(self):
|
1956
|
+
assert issubclass(self.actor, AbstractTable)
|
1957
|
+
self.actor.check_params(self.param_values)
|
1958
|
+
if self.actor.get_data_rows is not None:
|
1959
|
+
l = []
|
1960
|
+
for row in self.actor.get_data_rows(self):
|
1961
|
+
group = self.actor.group_from_row(row)
|
1962
|
+
group.process_row(l, row)
|
1963
|
+
return l
|
1964
|
+
# ~ logger.info("20120914 tables.get_data_iterator %s",self)
|
1965
|
+
# ~ logger.info("20120914 tables.get_data_iterator %s",self.actor)
|
1966
|
+
# print("20181121 get_data_iterator", self.actor)
|
1967
|
+
return self.actor.get_request_queryset(self)
|
1968
|
+
|
1969
|
+
def get_total_count(self):
|
1970
|
+
"""
|
1971
|
+
Return the number of rows.
|
1972
|
+
|
1973
|
+
When actor is not a table, we assume that there is one row. This is
|
1974
|
+
used e.g. when courses.StatusReport is shown in the the dashboard. A
|
1975
|
+
Report must return 1 because otherwise the dashboard believes it is
|
1976
|
+
empty.
|
1977
|
+
|
1978
|
+
Calling `len()` on a QuerySet would execute the whole SELECT.
|
1979
|
+
See `/blog/2012/0124`
|
1980
|
+
"""
|
1981
|
+
if self.actor is None or not issubclass(self.actor, AbstractTable):
|
1982
|
+
return 1
|
1983
|
+
di = self.data_iterator
|
1984
|
+
if isinstance(di, SearchQuery):
|
1985
|
+
return di.total_hits
|
1986
|
+
if isinstance(di, QuerySet):
|
1987
|
+
return di.count()
|
1988
|
+
# try:
|
1989
|
+
# return di.count()
|
1990
|
+
# except Exception as e:
|
1991
|
+
# raise e.__class__("{} : {}".format(self, e))
|
1992
|
+
# ~ if di is None:
|
1993
|
+
# ~ raise Exception("data_iterator is None: %s" % self)
|
1994
|
+
if True:
|
1995
|
+
return len(di)
|
1996
|
+
else:
|
1997
|
+
try:
|
1998
|
+
return len(di)
|
1999
|
+
except TypeError:
|
2000
|
+
raise TypeError("{0} has no length".format(di))
|
2001
|
+
|
2002
|
+
def __iter__(self):
|
2003
|
+
return self.data_iterator.__iter__()
|
2004
|
+
|
2005
|
+
def __getitem__(self, i):
|
2006
|
+
return self.data_iterator.__getitem__(i)
|
2007
|
+
|
1560
2008
|
def create_phantom_rows(self, **kw):
|
1561
2009
|
# phantom row disturbs when there is an insert button in
|
1562
2010
|
# the toolbar
|
@@ -1619,6 +2067,7 @@ class ActionRequest(BaseRequest):
|
|
1619
2067
|
this request.
|
1620
2068
|
|
1621
2069
|
"""
|
2070
|
+
# print("20230331 712 get_status() {}".format(self.subst_user))
|
1622
2071
|
if self._status is not None and not kw:
|
1623
2072
|
return self._status
|
1624
2073
|
if self.actor.parameters:
|
@@ -1629,15 +2078,46 @@ class ActionRequest(BaseRequest):
|
|
1629
2078
|
)
|
1630
2079
|
|
1631
2080
|
kw = self.bound_action.action.get_status(self, **kw)
|
1632
|
-
|
1633
2081
|
bp = kw.setdefault("base_params", {})
|
1634
|
-
|
1635
2082
|
if self.current_project is not None:
|
1636
2083
|
bp[constants.URL_PARAM_PROJECT] = self.current_project
|
1637
|
-
|
1638
2084
|
if self.subst_user is not None:
|
1639
2085
|
# raise Exception("20230331")
|
1640
2086
|
bp[constants.URL_PARAM_SUBST_USER] = self.subst_user.id
|
2087
|
+
if self.quick_search:
|
2088
|
+
bp[constants.URL_PARAM_FILTER] = self.quick_search
|
2089
|
+
|
2090
|
+
if self.order_by:
|
2091
|
+
sort = self.order_by[0]
|
2092
|
+
if sort.startswith("-"):
|
2093
|
+
sort = sort[1:]
|
2094
|
+
bp[constants.URL_PARAM_SORTDIR] = "DESC"
|
2095
|
+
bp[constants.URL_PARAM_SORT] = sort
|
2096
|
+
|
2097
|
+
if self.known_values:
|
2098
|
+
for k, v in self.known_values.items():
|
2099
|
+
if self.actor.known_values.get(k, None) != v:
|
2100
|
+
bp[k] = v
|
2101
|
+
if self.master_instance is not None:
|
2102
|
+
if isinstance(self.master_instance, (models.Model, TableRow)):
|
2103
|
+
bp[constants.URL_PARAM_MASTER_PK] = self.master_instance.pk
|
2104
|
+
if ContentType is not None and isinstance(
|
2105
|
+
self.master_instance, models.Model
|
2106
|
+
):
|
2107
|
+
assert not self.master_instance._meta.abstract
|
2108
|
+
mt = ContentType.objects.get_for_model(
|
2109
|
+
self.master_instance.__class__
|
2110
|
+
).pk
|
2111
|
+
bp[constants.URL_PARAM_MASTER_TYPE] = mt
|
2112
|
+
# else:
|
2113
|
+
# logger.warning("20141205 %s %s",
|
2114
|
+
# self.master_instance,
|
2115
|
+
# ContentType)
|
2116
|
+
else: # if self.master is None:
|
2117
|
+
# e.g. in accounting.MovementsByMatch the master is a `str`
|
2118
|
+
bp[constants.URL_PARAM_MASTER_PK] = self.master_instance
|
2119
|
+
# raise Exception("No master key for {} {!r}".format(
|
2120
|
+
# self.master_instance.__class__, self.master_instance))
|
1641
2121
|
self._status = kw
|
1642
2122
|
return kw
|
1643
2123
|
|
@@ -1707,106 +2187,6 @@ class ActionRequest(BaseRequest):
|
|
1707
2187
|
def pk2url(self, pk):
|
1708
2188
|
return self.get_detail_url(self.actor, pk)
|
1709
2189
|
|
1710
|
-
def run(self, *args, **kw):
|
1711
|
-
"""
|
1712
|
-
Runs this action request.
|
1713
|
-
"""
|
1714
|
-
return self.bound_action.action.run_from_code(self, *args, **kw)
|
1715
|
-
|
1716
|
-
def setup(self, known_values=None, param_values=None, action_param_values={}, **kw):
|
1717
|
-
BaseRequest.setup(self, **kw)
|
1718
|
-
# ~ 20120111
|
1719
|
-
# ~ self.known_values = known_values or self.report.known_values
|
1720
|
-
# ~ if self.report.known_values:
|
1721
|
-
# ~ d = dict(self.report.known_values)
|
1722
|
-
kv = dict()
|
1723
|
-
for k, v in self.actor.known_values.items():
|
1724
|
-
kv.setdefault(k, v)
|
1725
|
-
if known_values:
|
1726
|
-
kv.update(known_values)
|
1727
|
-
self.known_values = kv
|
1728
|
-
|
1729
|
-
request = self.request
|
1730
|
-
|
1731
|
-
if self.actor.parameters is not None:
|
1732
|
-
pv = self.actor.param_defaults(self)
|
1733
|
-
for k in pv.keys():
|
1734
|
-
if k not in self.actor.parameters:
|
1735
|
-
raise Exception(
|
1736
|
-
"%s.param_defaults() returned invalid keyword %r"
|
1737
|
-
% (self.actor, k)
|
1738
|
-
)
|
1739
|
-
|
1740
|
-
# New since 20120913. E.g. newcomers.Newcomers is a
|
1741
|
-
# simple pcsw.Clients with
|
1742
|
-
# known_values=dict(client_state=newcomer) and since there
|
1743
|
-
# is a parameter `client_state`, we override that
|
1744
|
-
# parameter's default value.
|
1745
|
-
|
1746
|
-
for k, v in self.known_values.items():
|
1747
|
-
if k in pv:
|
1748
|
-
pv[k] = v
|
1749
|
-
|
1750
|
-
# New since 20120914. MyClientsByGroup has a `group` as
|
1751
|
-
# master, this must also appear as `group` parameter
|
1752
|
-
# value. Lino now understands tables where the master_key
|
1753
|
-
# is also a parameter.
|
1754
|
-
|
1755
|
-
if self.actor.master_key is not None:
|
1756
|
-
if self.actor.master_key in pv:
|
1757
|
-
pv[self.actor.master_key] = self.master_instance
|
1758
|
-
if param_values is None:
|
1759
|
-
if self.actor.params_layout is None:
|
1760
|
-
pass # 20200825 e.g. users.Users
|
1761
|
-
# raise Exception(
|
1762
|
-
# "{} has parameters ({}) but no params_layout. {}".format(
|
1763
|
-
# self.actor, self.actor.parameters, self.actor._setup_done))
|
1764
|
-
|
1765
|
-
elif request is not None:
|
1766
|
-
# call get_layout_handle to make sure that
|
1767
|
-
# params_store has been created:
|
1768
|
-
self.actor.params_layout.get_layout_handle()
|
1769
|
-
ps = self.actor.params_layout.params_store
|
1770
|
-
# print('20160329 requests.py', ps, self.actor.parameters)
|
1771
|
-
if ps is not None:
|
1772
|
-
pv.update(ps.parse_params(request))
|
1773
|
-
else:
|
1774
|
-
raise Exception(
|
1775
|
-
"20160329 params_layout {0} has no params_store "
|
1776
|
-
"in {1!r}".format(self.actor.params_layout, self.actor)
|
1777
|
-
)
|
1778
|
-
else:
|
1779
|
-
for k in param_values.keys():
|
1780
|
-
if k not in pv:
|
1781
|
-
raise Exception(
|
1782
|
-
"Invalid key '%s' in param_values of %s "
|
1783
|
-
"request (possible keys are %s)"
|
1784
|
-
% (k, self.actor, list(pv.keys()))
|
1785
|
-
)
|
1786
|
-
pv.update(param_values)
|
1787
|
-
# print("20160329 ok", pv)
|
1788
|
-
self.param_values = AttrDict(**pv)
|
1789
|
-
# self.actor.check_params(self.param_values)
|
1790
|
-
|
1791
|
-
if self.bound_action is None:
|
1792
|
-
return # 20200825 e.g. a request on an abstract table
|
1793
|
-
|
1794
|
-
action = self.bound_action.action
|
1795
|
-
if action.parameters is not None:
|
1796
|
-
if len(self.selected_rows) == 1:
|
1797
|
-
apv = action.action_param_defaults(self, self.selected_rows[0])
|
1798
|
-
else:
|
1799
|
-
apv = action.action_param_defaults(self, None)
|
1800
|
-
# msg = "20170116 selected_rows is {} for {!r}".format(
|
1801
|
-
# self.selected_rows, action)
|
1802
|
-
# raise Exception(msg)
|
1803
|
-
if request is not None:
|
1804
|
-
apv.update(action.params_layout.params_store.parse_params(request))
|
1805
|
-
self.action_param_values = AttrDict(**apv)
|
1806
|
-
# action.check_params(action_param_values)
|
1807
|
-
self.set_action_param_values(**action_param_values)
|
1808
|
-
self.bound_action.setup_action_request(self)
|
1809
|
-
|
1810
2190
|
def set_action_param_values(self, **action_param_values):
|
1811
2191
|
apv = self.action_param_values
|
1812
2192
|
for k in action_param_values.keys():
|
@@ -1818,121 +2198,318 @@ class ActionRequest(BaseRequest):
|
|
1818
2198
|
)
|
1819
2199
|
apv.update(action_param_values)
|
1820
2200
|
|
1821
|
-
def get_data_iterator(self):
|
1822
|
-
raise NotImplementedError
|
1823
|
-
|
1824
2201
|
def get_base_filename(self):
|
1825
2202
|
return str(self.actor)
|
1826
2203
|
# ~ s = self.get_title()
|
1827
2204
|
# ~ return s.encode('us-ascii','replace')
|
1828
2205
|
|
1829
2206
|
|
1830
|
-
|
1831
|
-
|
1832
|
-
|
1833
|
-
model instance.
|
1834
|
-
|
1835
|
-
.. attribute:: bound_action
|
1836
|
-
|
1837
|
-
The bound action that will run.
|
1838
|
-
|
1839
|
-
.. attribute:: instance
|
1840
|
-
|
1841
|
-
The database object on which the action will run.
|
2207
|
+
def to_rst(self, *args, **kw):
|
2208
|
+
"""Returns a string representing this table request in
|
2209
|
+
reStructuredText markup.
|
1842
2210
|
|
1843
|
-
.. attribute:: owner
|
1844
|
-
|
1845
|
-
|
1846
|
-
"""
|
1847
|
-
|
1848
|
-
def __init__(self, action, actor, instance, owner):
|
1849
|
-
# ~ print "Bar"
|
1850
|
-
# ~ self.action = action
|
1851
|
-
if actor is None:
|
1852
|
-
actor = instance.get_default_table()
|
1853
|
-
self.bound_action = actor.get_action_by_name(action.action_name)
|
1854
|
-
if self.bound_action is None:
|
1855
|
-
raise Exception("%s has not action %r" % (actor, action))
|
1856
|
-
# Happened 20131020 from lino_xl.lib.beid.eid_info() :
|
1857
|
-
# When `use_eid_jslib` was False, then
|
1858
|
-
# `Action.attach_to_actor` returned False.
|
1859
|
-
self.instance = instance
|
1860
|
-
self.owner = owner
|
1861
|
-
|
1862
|
-
def __str__(self):
|
1863
|
-
return "{0} on {1}".format(self.bound_action, obj2str(self.instance))
|
1864
|
-
|
1865
|
-
def run_from_code(self, ar, *args, **kw):
|
1866
|
-
"""
|
1867
|
-
Probably to be deprecated.
|
1868
|
-
Run this action on this instance in the given session, updating
|
1869
|
-
the response of the session. Returns the return value of the
|
1870
|
-
action.
|
1871
2211
|
"""
|
1872
|
-
|
1873
|
-
|
1874
|
-
|
2212
|
+
stdout = sys.stdout
|
2213
|
+
sys.stdout = StringIO()
|
2214
|
+
self.table2rst(*args, **kw)
|
2215
|
+
rv = sys.stdout.getvalue()
|
2216
|
+
sys.stdout = stdout
|
2217
|
+
return rv
|
1875
2218
|
|
1876
|
-
def
|
2219
|
+
def table2rst(self, *args, **kwargs):
|
1877
2220
|
"""
|
1878
|
-
|
1879
|
-
|
2221
|
+
Print a reStructuredText representation of this table request to
|
2222
|
+
stdout.
|
1880
2223
|
"""
|
1881
|
-
|
1882
|
-
# kw.update(selected_rows=[self.instance])
|
1883
|
-
ar.selected_rows = [self.instance]
|
1884
|
-
self.bound_action.action.run_from_ui(ar, *args, **kwargs)
|
2224
|
+
settings.SITE.kernel.text_renderer.show_table(self, *args, **kwargs)
|
1885
2225
|
|
1886
|
-
def
|
2226
|
+
def table2xhtml(self, **kwargs):
|
1887
2227
|
"""
|
1888
|
-
|
1889
|
-
the action.
|
2228
|
+
Return an HTML representation of this table request.
|
1890
2229
|
"""
|
1891
|
-
|
1892
|
-
|
1893
|
-
|
1894
|
-
|
2230
|
+
t = xghtml.Table()
|
2231
|
+
self.dump2html(t, self.sliced_data_iterator, **kwargs)
|
2232
|
+
e = t.as_element()
|
2233
|
+
# print "20150822 table2xhtml", tostring(e)
|
2234
|
+
# if header_level is not None:
|
2235
|
+
# return E.div(E.h2(str(self.actor.label)), e)
|
2236
|
+
return e
|
1895
2237
|
|
1896
|
-
def
|
2238
|
+
def dump2html(
|
2239
|
+
self,
|
2240
|
+
tble,
|
2241
|
+
data_iterator,
|
2242
|
+
column_names=None,
|
2243
|
+
header_links=False,
|
2244
|
+
max_width=None, # ignored
|
2245
|
+
hide_sums=None,
|
2246
|
+
):
|
1897
2247
|
"""
|
1898
|
-
|
2248
|
+
Render this table into an existing :class:`etgen.html.Table`
|
2249
|
+
instance. This central method is used by all Lino
|
2250
|
+
renderers.
|
1899
2251
|
|
1900
|
-
|
1901
|
-
Returns the response of the child request.
|
1902
|
-
Doesn't modify response of parent request.
|
1903
|
-
"""
|
1904
|
-
ar = self.request_from(ses, **kwargs)
|
1905
|
-
self.bound_action.action.run_from_code(ar)
|
1906
|
-
return ar.response
|
2252
|
+
Arguments:
|
1907
2253
|
|
1908
|
-
|
1909
|
-
"""
|
1910
|
-
Run this instance action in an anonymous base request.
|
2254
|
+
`tble` An instance of :class:`etgen.html.Table`.
|
1911
2255
|
|
1912
|
-
|
1913
|
-
|
1914
|
-
"""
|
1915
|
-
if len(args) and isinstance(args[0], BaseRequest):
|
1916
|
-
raise ChangedAPI("20181004")
|
1917
|
-
ar = self.bound_action.request()
|
1918
|
-
self.run_from_code(ar, *args, **kwargs)
|
1919
|
-
return ar.response
|
2256
|
+
`data_iterator` the iterable provider of table rows. This can
|
2257
|
+
be a queryset or a list.
|
1920
2258
|
|
1921
|
-
|
1922
|
-
|
1923
|
-
|
1924
|
-
|
2259
|
+
`column_names` is an optional string with space-separated
|
2260
|
+
column names. If this is None, the table's
|
2261
|
+
:attr:`column_names <lino.core.tables.Table.column_names>` is
|
2262
|
+
used.
|
1925
2263
|
|
1926
|
-
|
1927
|
-
|
1928
|
-
execute this action on this instance. This is being used in
|
1929
|
-
the :ref:`lino.tutorial.polls`.
|
2264
|
+
`header_links` says whether to render column headers clickable
|
2265
|
+
with a link that sorts the table.
|
1930
2266
|
|
2267
|
+
`hide_sums` : whether to hide sums. If this is not given, use
|
2268
|
+
the :attr:`hide_sums <lino.core.tables.Table.hide_sums>` of
|
2269
|
+
the :attr:`actor`.
|
1931
2270
|
"""
|
1932
|
-
|
2271
|
+
ar = self
|
2272
|
+
tble.attrib.update(self.renderer.tableattrs)
|
2273
|
+
tble.attrib.setdefault("name", self.bound_action.full_name())
|
2274
|
+
|
2275
|
+
grid = ar.ah.grid_layout.main
|
2276
|
+
# from lino.core.widgets import GridWidget
|
2277
|
+
# if not isinstance(grid, GridWidget):
|
2278
|
+
# raise Exception("20160529 %r is not a GridElement", grid)
|
2279
|
+
columns = grid.columns
|
2280
|
+
fields, headers, cellwidths = ar.get_field_info(column_names)
|
2281
|
+
columns = fields
|
2282
|
+
|
2283
|
+
sums = [fld.zero for fld in columns]
|
2284
|
+
if not self.ah.actor.hide_headers:
|
2285
|
+
headers = [
|
2286
|
+
x
|
2287
|
+
for x in grid.headers2html(
|
2288
|
+
self, columns, headers, header_links, **self.renderer.cellattrs
|
2289
|
+
)
|
2290
|
+
]
|
2291
|
+
# if cellwidths and self.renderer.is_interactive:
|
2292
|
+
if cellwidths:
|
2293
|
+
totwidth = sum([int(w) for w in cellwidths])
|
2294
|
+
widths = [str(int(int(w) * 100 / totwidth)) + "%" for w in cellwidths]
|
2295
|
+
for i, td in enumerate(headers):
|
2296
|
+
# td.set('width', six.text_type(cellwidths[i]))
|
2297
|
+
td.set("width", widths[i])
|
2298
|
+
tble.head.append(xghtml.E.tr(*headers))
|
2299
|
+
# ~ print 20120623, ar.actor
|
2300
|
+
recno = 0
|
2301
|
+
for obj in data_iterator:
|
2302
|
+
cells = ar.row2html(recno, columns, obj, sums, **self.renderer.cellattrs)
|
2303
|
+
if cells is not None:
|
2304
|
+
recno += 1
|
2305
|
+
tble.body.append(xghtml.E.tr(*cells))
|
2306
|
+
|
2307
|
+
if recno == 0:
|
2308
|
+
tble.clear()
|
2309
|
+
tble.body.append(str(ar.no_data_text))
|
2310
|
+
|
2311
|
+
if hide_sums is None:
|
2312
|
+
hide_sums = ar.actor.hide_sums
|
2313
|
+
|
2314
|
+
if not hide_sums:
|
2315
|
+
has_sum = False
|
2316
|
+
for i in sums:
|
2317
|
+
if i:
|
2318
|
+
has_sum = True
|
2319
|
+
break
|
2320
|
+
if has_sum:
|
2321
|
+
cells = ar.sums2html(columns, sums, **self.renderer.cellattrs)
|
2322
|
+
tble.body.append(xghtml.E.tr(*cells))
|
2323
|
+
|
2324
|
+
def get_field_info(ar, column_names=None):
|
2325
|
+
"""
|
2326
|
+
Return a tuple `(fields, headers, widths)` which expresses which
|
2327
|
+
columns, headers and widths the user wants for this
|
2328
|
+
request. If `self` has web request info (:attr:`request` is
|
2329
|
+
not None), checks for GET parameters cn, cw and ch. Also
|
2330
|
+
calls the tables's :meth:`override_column_headers
|
2331
|
+
<lino.core.actors.Actor.override_column_headers>` method.
|
2332
|
+
"""
|
2333
|
+
from lino.modlib.users.utils import with_user_profile
|
2334
|
+
from lino.core.layouts import ColumnsLayout
|
2335
|
+
|
2336
|
+
def getit():
|
2337
|
+
if ar.request is None:
|
2338
|
+
columns = None
|
2339
|
+
else:
|
2340
|
+
data = getrqdata(ar.request)
|
2341
|
+
columns = [str(x) for x in data.getlist(constants.URL_PARAM_COLUMNS)]
|
2342
|
+
if columns:
|
2343
|
+
all_widths = data.getlist(constants.URL_PARAM_WIDTHS)
|
2344
|
+
hiddens = [
|
2345
|
+
(x == "true") for x in data.getlist(constants.URL_PARAM_HIDDENS)
|
2346
|
+
]
|
2347
|
+
fields = []
|
2348
|
+
widths = []
|
2349
|
+
ah = ar.actor.get_handle()
|
2350
|
+
for i, cn in enumerate(columns):
|
2351
|
+
col = None
|
2352
|
+
for e in ah.grid_layout.main.columns:
|
2353
|
+
if e.name == cn:
|
2354
|
+
col = e
|
2355
|
+
break
|
2356
|
+
if col is None:
|
2357
|
+
raise Exception(
|
2358
|
+
"No column named %r in %s"
|
2359
|
+
% (cn, ar.ah.grid_layout.main.columns)
|
2360
|
+
)
|
2361
|
+
if not hiddens[i]:
|
2362
|
+
fields.append(col)
|
2363
|
+
widths.append(int(all_widths[i]))
|
2364
|
+
else:
|
2365
|
+
if column_names:
|
2366
|
+
ll = ColumnsLayout(column_names, datasource=ar.actor)
|
2367
|
+
lh = ll.get_layout_handle()
|
2368
|
+
columns = lh.main.columns
|
2369
|
+
columns = [e for e in columns if not e.hidden]
|
2370
|
+
else:
|
2371
|
+
ah = ar.actor.get_request_handle(ar)
|
2372
|
+
|
2373
|
+
columns = ah.grid_layout.main.columns
|
2374
|
+
# print(20160530, ah, columns, ah.grid_layout.main)
|
2375
|
+
|
2376
|
+
# render them so that babelfields in hidden_languages
|
2377
|
+
# get hidden:
|
2378
|
+
for e in columns:
|
2379
|
+
e.value = e.ext_options()
|
2380
|
+
# try:
|
2381
|
+
# e.value = e.ext_options()
|
2382
|
+
# except AttributeError as ex:
|
2383
|
+
# raise AttributeError("20160529 %s : %s" % (e, ex))
|
2384
|
+
#
|
2385
|
+
columns = [e for e in columns if not e.value.get("hidden", False)]
|
2386
|
+
|
2387
|
+
columns = [e for e in columns if not e.hidden]
|
2388
|
+
|
2389
|
+
# if str(ar.actor) == "isip.ExamPolicies":
|
2390
|
+
# from lino.modlib.extjs.elems import is_hidden_babel_field
|
2391
|
+
# print("20180103", [c.name for c in columns])
|
2392
|
+
# print("20180103", [c.field for c in columns])
|
2393
|
+
# print("20180103", [c.value['hidden'] for c in columns])
|
2394
|
+
# print("20180103", [
|
2395
|
+
# is_hidden_babel_field(c.field) for c in columns])
|
2396
|
+
# print("20180103", [
|
2397
|
+
# getattr(c.field, '_babel_language', None)
|
2398
|
+
# for c in columns])
|
2399
|
+
widths = ["%d" % (col.width or col.preferred_width) for col in columns]
|
2400
|
+
# print("20180831 {}".format(widths))
|
2401
|
+
# ~ 20130415 widths = ["%d%%" % (col.width or col.preferred_width) for col in columns]
|
2402
|
+
# ~ fields = [col.field._lino_atomizer for col in columns]
|
2403
|
+
fields = columns
|
2404
|
+
|
2405
|
+
headers = [column_header(col) for col in fields]
|
2406
|
+
|
2407
|
+
# if str(ar.actor).endswith("DailySlave"):
|
2408
|
+
# print("20181022", fields[0].field.verbose_name)
|
2409
|
+
|
2410
|
+
oh = ar.actor.override_column_headers(ar)
|
2411
|
+
if oh:
|
2412
|
+
for i, e in enumerate(columns):
|
2413
|
+
header = oh.get(e.name, None)
|
2414
|
+
if header is not None:
|
2415
|
+
headers[i] = header
|
2416
|
+
# ~ print 20120507, oh, headers
|
2417
|
+
|
2418
|
+
return fields, headers, widths
|
2419
|
+
|
2420
|
+
u = ar.get_user()
|
2421
|
+
if u is None:
|
2422
|
+
return getit()
|
2423
|
+
else:
|
2424
|
+
return with_user_profile(u.user_type, getit)
|
2425
|
+
|
2426
|
+
def row2html(self, recno, columns, row, sums, **cellattrs):
|
2427
|
+
has_numeric_value = False
|
2428
|
+
cells = []
|
2429
|
+
for i, col in enumerate(columns):
|
2430
|
+
v = col.field._lino_atomizer.full_value_from_object(row, self)
|
2431
|
+
if v is None:
|
2432
|
+
td = E.td(**cellattrs)
|
2433
|
+
else:
|
2434
|
+
nv = col.value2num(v)
|
2435
|
+
if nv != 0:
|
2436
|
+
sums[i] += nv
|
2437
|
+
has_numeric_value = True
|
2438
|
+
td = col.value2html(self, v, **cellattrs)
|
2439
|
+
# print("20240506 {} {}".format(col.__class__, tostring(td)))
|
2440
|
+
col.apply_cell_format(td)
|
2441
|
+
self.actor.apply_cell_format(self, row, col, recno, td)
|
2442
|
+
cells.append(td)
|
2443
|
+
if self.actor.hide_zero_rows and not has_numeric_value:
|
2444
|
+
return None
|
2445
|
+
return cells
|
2446
|
+
|
2447
|
+
def row2text(self, fields, row, sums):
|
2448
|
+
"""
|
2449
|
+
Render the given `row` into an iteration of text cells, using the given
|
2450
|
+
list of `fields` and collecting sums into `sums`.
|
2451
|
+
"""
|
2452
|
+
# print(20160530, fields)
|
2453
|
+
for i, fld in enumerate(fields):
|
2454
|
+
if fld.field is not None:
|
2455
|
+
sf = get_atomizer(row.__class__, fld.field, fld.field.name)
|
2456
|
+
# print(20160530, fld.field.name, sf)
|
2457
|
+
if False:
|
2458
|
+
try:
|
2459
|
+
getter = sf.full_value_from_object
|
2460
|
+
v = getter(row, self)
|
2461
|
+
except Exception as e:
|
2462
|
+
raise Exception("20150218 %s: %s" % (sf, e))
|
2463
|
+
# was used to find bug 20130422:
|
2464
|
+
yield "%s:\n%s" % (fld.field, e)
|
2465
|
+
continue
|
2466
|
+
else:
|
2467
|
+
getter = sf.full_value_from_object
|
2468
|
+
v = getter(row, self)
|
1933
2469
|
|
1934
|
-
|
1935
|
-
|
1936
|
-
|
1937
|
-
|
1938
|
-
|
2470
|
+
if v is None:
|
2471
|
+
# if not v:
|
2472
|
+
yield ""
|
2473
|
+
else:
|
2474
|
+
sums[i] += fld.value2num(v)
|
2475
|
+
# # In case you want the field name in error message:
|
2476
|
+
# try:
|
2477
|
+
# sums[i] += fld.value2num(v)
|
2478
|
+
# except Exception as e:
|
2479
|
+
# raise e.__class__("%s %s" % (fld.field, e))
|
2480
|
+
yield fld.format_value(self, v)
|
2481
|
+
|
2482
|
+
def sums2html(self, columns, sums, **cellattrs):
|
2483
|
+
sums = {fld.name: sums[i] for i, fld in enumerate(columns)}
|
2484
|
+
return [
|
2485
|
+
fld.sum2html(self, sums, i, **cellattrs) for i, fld in enumerate(columns)
|
2486
|
+
]
|
2487
|
+
|
2488
|
+
def get_title(self):
|
2489
|
+
# print(20200125, self.title, self.master_instance)
|
2490
|
+
if self.title is not None:
|
2491
|
+
return self.title
|
2492
|
+
# if self.master_instance is not None:
|
2493
|
+
# self.master_instance
|
2494
|
+
return self.actor.get_title(self)
|
2495
|
+
|
2496
|
+
def __repr__(self):
|
2497
|
+
kw = dict()
|
2498
|
+
if self.master_instance is not None:
|
2499
|
+
kw.update(master_instance=obj2str(self.master_instance))
|
2500
|
+
if self.filter is not None:
|
2501
|
+
kw.update(filter=repr(self.filter))
|
2502
|
+
if self.known_values:
|
2503
|
+
kw.update(known_values=self.known_values)
|
2504
|
+
if self.requesting_panel:
|
2505
|
+
kw.update(requesting_panel=self.requesting_panel)
|
2506
|
+
u = self.get_user()
|
2507
|
+
if u is not None:
|
2508
|
+
kw.update(user=u.username)
|
2509
|
+
if False: # self.request:
|
2510
|
+
kw.update(request=format_request(self.request))
|
2511
|
+
return "<%s %s(%s)>" % (
|
2512
|
+
self.__class__.__name__,
|
2513
|
+
self.bound_action.full_name(),
|
2514
|
+
kw,
|
2515
|
+
)
|