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/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
- except RuntimeError:
48
- pass
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
- class BaseRequest(object):
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
- kv.update(parent.known_values)
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
- if pk and pk != "-99998" and pk != "-99999":
638
- self.selected_rows.append(self.get_row_by_pk(pk))
639
- # try:
640
- # for pk in selected_pks:
641
- # if pk and pk != "-99998" and pk != "-99999":
642
- # self.selected_rows.append(self.get_row_by_pk(pk))
643
- # except ObjectDoesNotExist as e:
644
- # raise exceptions.BadRequest(
645
- # "Invalid primary key {0} for {1} ({2})".format(pk, self.actor, e)) from None
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
- BaseRequest.__init__(self, **kw)
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
- class InstanceAction(object):
1831
- """
1832
- Volatile object that wraps a given action to be run on a given
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
- # raise Exception("20170129 is this still used?")
1873
- ar.selected_rows = [self.instance]
1874
- return self.bound_action.action.run_from_code(ar, *args, **kw)
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 run_from_ui(self, ar, *args, **kwargs):
2219
+ def table2rst(self, *args, **kwargs):
1877
2220
  """
1878
- Run this action on this instance in the given session, updating
1879
- the response of the session. Returns nothing.
2221
+ Print a reStructuredText representation of this table request to
2222
+ stdout.
1880
2223
  """
1881
- # raise Exception("20170129 is this still used?")
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 request_from(self, ses, **kwargs):
2226
+ def table2xhtml(self, **kwargs):
1887
2227
  """
1888
- Create an action request on this instance action without running
1889
- the action.
2228
+ Return an HTML representation of this table request.
1890
2229
  """
1891
- kwargs.update(selected_rows=[self.instance])
1892
- kwargs.update(parent=ses)
1893
- ar = self.bound_action.request(**kwargs)
1894
- return ar
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 run_from_session(self, ses, **kwargs):
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
- Run this instance action in a child request of given session.
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
- Additional arguments are forwarded to the action.
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
- def __call__(self, *args, **kwargs):
1909
- """
1910
- Run this instance action in an anonymous base request.
2254
+ `tble` An instance of :class:`etgen.html.Table`.
1911
2255
 
1912
- Additional arguments are forwarded to the action.
1913
- Returns the response of the base request.
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
- def as_button_elem(self, ar, label=None, **kwargs):
1922
- return settings.SITE.kernel.row_action_button(
1923
- self.instance, ar, self.bound_action, label, **kwargs
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
- def as_button(self, *args, **kwargs):
1927
- """Return a HTML chunk with a "button" which, when clicked, will
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
- return tostring(self.as_button_elem(*args, **kwargs))
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
- def get_row_permission(self, ar):
1935
- state = self.bound_action.actor.get_row_state(self.instance)
1936
- # logger.info("20150202 ia.get_row_permission() %s using %s",
1937
- # self, state)
1938
- return self.bound_action.get_row_permission(ar, self.instance, state)
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
+ )