lino 24.10.2__py3-none-any.whl → 24.11.0__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.
Files changed (53) hide show
  1. lino/__init__.py +1 -1
  2. lino/api/dd.py +1 -0
  3. lino/api/doctest.py +11 -10
  4. lino/api/rt.py +2 -3
  5. lino/config/admin_main_base.html +2 -2
  6. lino/core/actors.py +68 -33
  7. lino/core/choicelists.py +2 -2
  8. lino/core/dashboard.py +2 -1
  9. lino/core/dbtables.py +3 -3
  10. lino/core/elems.py +8 -4
  11. lino/core/fields.py +11 -3
  12. lino/core/kernel.py +4 -5
  13. lino/core/layouts.py +1 -1
  14. lino/core/model.py +1 -9
  15. lino/core/plugin.py +1 -0
  16. lino/core/renderer.py +19 -19
  17. lino/core/requests.py +73 -38
  18. lino/core/site.py +5 -45
  19. lino/core/store.py +16 -16
  20. lino/core/tables.py +0 -17
  21. lino/core/utils.py +58 -10
  22. lino/core/views.py +2 -1
  23. lino/help_texts.py +13 -0
  24. lino/management/commands/show.py +2 -4
  25. lino/mixins/periods.py +10 -4
  26. lino/mixins/polymorphic.py +3 -3
  27. lino/mixins/ref.py +6 -3
  28. lino/modlib/checkdata/__init__.py +3 -3
  29. lino/modlib/extjs/ext_renderer.py +1 -1
  30. lino/modlib/languages/__init__.py +1 -1
  31. lino/modlib/languages/models.py +1 -1
  32. lino/modlib/linod/consumers.py +2 -3
  33. lino/modlib/memo/mixins.py +1 -1
  34. lino/modlib/periods/__init__.py +31 -0
  35. lino/modlib/periods/fixtures/std.py +23 -0
  36. lino/modlib/periods/mixins.py +126 -0
  37. lino/modlib/periods/models.py +246 -0
  38. lino/modlib/printing/actions.py +2 -0
  39. lino/modlib/publisher/ui.py +2 -2
  40. lino/modlib/system/__init__.py +0 -2
  41. lino/modlib/system/choicelists.py +55 -1
  42. lino/modlib/system/models.py +1 -0
  43. lino/modlib/users/models.py +2 -2
  44. lino/modlib/weasyprint/__init__.py +2 -0
  45. lino/utils/__init__.py +8 -9
  46. lino/utils/djangotest.py +2 -1
  47. lino/utils/html.py +31 -0
  48. lino/utils/ranges.py +5 -15
  49. {lino-24.10.2.dist-info → lino-24.11.0.dist-info}/METADATA +1 -1
  50. {lino-24.10.2.dist-info → lino-24.11.0.dist-info}/RECORD +53 -49
  51. {lino-24.10.2.dist-info → lino-24.11.0.dist-info}/WHEEL +0 -0
  52. {lino-24.10.2.dist-info → lino-24.11.0.dist-info}/licenses/AUTHORS.rst +0 -0
  53. {lino-24.10.2.dist-info → lino-24.11.0.dist-info}/licenses/COPYING +0 -0
lino/core/requests.py CHANGED
@@ -35,6 +35,7 @@ from asgiref.sync import sync_to_async
35
35
 
36
36
  from lino.utils.html import E, tostring, iselement
37
37
  from lino.utils import AttrDict
38
+ from lino.utils import capture_output
38
39
  from lino.utils import MissingRow
39
40
  from lino.utils.html import html2text
40
41
  from lino.core import constants
@@ -237,8 +238,8 @@ class ValidActionResponses(object):
237
238
 
238
239
 
239
240
  inheritable_attrs = frozenset(
240
- "user subst_user renderer requesting_panel master_instance logger".split()
241
- )
241
+ ["user", "subst_user", "renderer", "requesting_panel", "master_instance",
242
+ "logger", "show_urls"] )
242
243
 
243
244
 
244
245
  def bool2text(x):
@@ -311,6 +312,8 @@ class BaseRequest:
311
312
  no_data_text = _("No data to display")
312
313
  is_on_main_actor = True
313
314
  permalink_uris = False
315
+ show_urls = True
316
+ master = None
314
317
  master_instance = None
315
318
  request = None
316
319
  selected_rows = []
@@ -326,12 +329,13 @@ class BaseRequest:
326
329
  self,
327
330
  request=None,
328
331
  parent=None,
329
- hash_router=None,
330
332
  is_on_main_actor=True,
331
333
  permalink_uris=None,
332
334
  **kw
333
335
  ):
334
336
  self.response = dict()
337
+ # if str(kw.get('actor', None)) == "cal.EntriesByController":
338
+ # if kw.get('actor', None):
335
339
  if request is not None:
336
340
  assert parent is None
337
341
  self.request = request
@@ -339,8 +343,6 @@ class BaseRequest:
339
343
  kw = self.parse_req(request, self.rqdata, **kw)
340
344
  if permalink_uris is None:
341
345
  permalink_uris = False # todo: which default value?
342
- # if hash_router is None:
343
- # hash_router = False
344
346
  elif parent is not None:
345
347
  # if parent.actor is None:
346
348
  # 20190926 we want to have javascript extjs links in dasboard
@@ -369,15 +371,16 @@ class BaseRequest:
369
371
  # is_on_main_actor = False
370
372
  if permalink_uris is None:
371
373
  permalink_uris = parent.permalink_uris
372
- # if hash_router is None:
373
- # hash_router = parent.hash_router
374
+ # else:
375
+ # kw.setdefault("show_urls", False)
374
376
 
375
377
  self.is_on_main_actor = is_on_main_actor
376
378
  self.permalink_uris = permalink_uris
377
- # self.hash_router = hash_router
378
379
 
379
380
  self.setup(**kw)
380
381
 
382
+ # print("20241101", self.__class__.__name__, self.show_urls)
383
+
381
384
  if self.master is not None and settings.SITE.strict_master_check:
382
385
  if self.master_instance is None:
383
386
  raise exceptions.BadRequest(
@@ -407,6 +410,7 @@ class BaseRequest:
407
410
  renderer=None,
408
411
  xcallback_answers=None,
409
412
  known_values={},
413
+ show_urls=None,
410
414
  ):
411
415
  if logger is not None:
412
416
  self.logger = logger
@@ -418,27 +422,31 @@ class BaseRequest:
418
422
  self.current_project = current_project
419
423
  if renderer is None:
420
424
  renderer = settings.SITE.kernel.text_renderer
425
+ # renderer = settings.SITE.kernel.default_renderer
421
426
  self.renderer = renderer
427
+ if show_urls is not None:
428
+ self.show_urls = show_urls
422
429
  self.subst_user = subst_user
423
430
  if xcallback_answers is not None:
424
431
  self.xcallback_answers = xcallback_answers
425
432
 
426
- if master is None and self.actor is not None:
427
- master = self.actor.master
428
- if master_type is not None:
429
- try:
430
- master = ContentType.objects.get(pk=master_type).model_class()
431
- except ContentType.DoesNotExist:
432
- raise exceptions.BadRequest("Invalid master_type {}".format(master_type)) from None
433
- self.master = master
434
-
435
- if self.master is not None and self.master_instance is None:
436
- # master_instance might have been inherited from parent
437
- if master_instance is None:
438
- master_instance = self.get_master_instance(
439
- self.master, master_key, master_type
440
- )
441
- self.master_instance = self.actor.cast_master_instance(master_instance)
433
+ if self.actor is not None:
434
+ if master is None:
435
+ master = self.actor.master
436
+ if master_type is not None and ContentType is not None:
437
+ try:
438
+ master = ContentType.objects.get(pk=master_type).model_class()
439
+ except ContentType.DoesNotExist:
440
+ raise exceptions.BadRequest("Invalid master_type {}".format(master_type)) from None
441
+ self.master = master
442
+
443
+ if self.master is not None and self.master_instance is None:
444
+ # master_instance might have been inherited from parent
445
+ if master_instance is None:
446
+ master_instance = self.get_master_instance(
447
+ self.master, master_key, master_type
448
+ )
449
+ self.master_instance = self.actor.cast_master_instance(master_instance)
442
450
 
443
451
  self.row_meta = dict(meta=True)
444
452
 
@@ -1166,6 +1174,9 @@ class BaseRequest:
1166
1174
  def story2rst(self, story, *args, **kwargs):
1167
1175
  return self.renderer.show_story(self, story, *args, **kwargs)
1168
1176
 
1177
+ def shows(self, *args, **kwargs):
1178
+ return capture_output(self.show, *args, **kwargs)
1179
+
1169
1180
  def show(
1170
1181
  self,
1171
1182
  spec=None,
@@ -1175,6 +1186,7 @@ class BaseRequest:
1175
1186
  language=None,
1176
1187
  nosummary=False,
1177
1188
  stripped=True,
1189
+ show_links=False,
1178
1190
  show_urls=False,
1179
1191
  max_width=None,
1180
1192
  header_links=False,
@@ -1194,10 +1206,14 @@ class BaseRequest:
1194
1206
 
1195
1207
  :column_names: overrides default list of columns
1196
1208
 
1197
- :show_urls: show links and other html formatting. Used
1209
+ :show_links: show links and other html formatting. Used
1198
1210
  .e.g. in :ref:`avanti.specs.roles` where we want
1199
1211
  to show whether cells are clickable or not.
1200
1212
 
1213
+ :show_urls: show the real URLs. URLs in doctests are usually replaced by
1214
+ "…" to increase readability. If this is explicitly set to True, Lino
1215
+ prints the full URLs.
1216
+
1201
1217
  :nosummary: if it is a table with :attr:`display_mode
1202
1218
  <lino.core.tables.AbstractTable.display_mode>`
1203
1219
  set to ``((None, DISPLAY_MODE_SUMMARY), )``,
@@ -1253,6 +1269,8 @@ class BaseRequest:
1253
1269
  ar = self.spawn(spec, **kwargs)
1254
1270
  # return self.renderer.show_story(spec, **kwargs)
1255
1271
 
1272
+ ar.show_urls = show_urls
1273
+
1256
1274
  def doit():
1257
1275
  # print 20160530, ar.renderer
1258
1276
  if issubclass(ar.actor, Report):
@@ -1275,7 +1293,7 @@ class BaseRequest:
1275
1293
  nosummary=nosummary,
1276
1294
  stripped=stripped,
1277
1295
  max_width=max_width,
1278
- show_urls=show_urls,
1296
+ show_links=show_links,
1279
1297
  display_mode=display_mode,
1280
1298
  )
1281
1299
  elif isinstance(ar.bound_action.action, actions.ShowDetail):
@@ -1630,9 +1648,9 @@ class ActionRequest(BaseRequest):
1630
1648
  Holds information about an individual web request and provides
1631
1649
  methods like
1632
1650
 
1633
- - :meth:`get_user <lino.core.actions.BaseRequest.get_user>`
1634
- - :meth:`confirm <lino.core.actions.BaseRequest.confirm>`
1635
- - :meth:`spawn <lino.core.actions.BaseRequest.spawn>`
1651
+ - :meth:`get_user <lino.core.requests.BaseRequest.get_user>`
1652
+ - :meth:`confirm <lino.core.requests.BaseRequest.confirm>`
1653
+ - :meth:`spawn <lino.core.requests.BaseRequest.spawn>`
1636
1654
 
1637
1655
  An `ActionRequest` is also a :class:`BaseRequest` and inherits its
1638
1656
  methods.
@@ -1845,7 +1863,7 @@ class ActionRequest(BaseRequest):
1845
1863
  return "{0} {1}".format(self.__class__.__name__, self.bound_action)
1846
1864
 
1847
1865
  def gen_insert_button(
1848
- self, target, button_attrs=dict(style="float: right;"), **values
1866
+ self, target=None, button_attrs=dict(style="float: right;"), **values
1849
1867
  ):
1850
1868
  """
1851
1869
  Generate an insert button using a cached insertable object.
@@ -1860,13 +1878,26 @@ class ActionRequest(BaseRequest):
1860
1878
  The difference is that gen_insert_button is more efficient when you do
1861
1879
  this more than once during a single request.
1862
1880
 
1863
- `target` is the actor into which we want to insert an object.
1864
- `button_label` is the optional button label.
1881
+ `target` is the actor into which we want to insert an object. When this is `None`, Lino uses :attr:`self.actor`.
1882
+ `button_attrs` if given, are forwarded to :meth:`ar2button`.
1865
1883
  `values` is a dict of extra default values to apply to the insertable object.
1866
1884
 
1885
+ The `values` must be atomized by the caller, which is especially
1886
+ important when you want to set a :term:`foreign key` field. So instead
1887
+ of saying::
1888
+
1889
+ gen_insert_button(None, user=u)
1890
+
1891
+ you must say::
1892
+
1893
+ gen_insert_button(None, user=u.pk, userHidden=str(u))
1894
+
1867
1895
  First usage example is in :mod:`lino_xl.lib.calview`.
1896
+ Second usage example is :class:`lino_prima.lib.prima.PupilsAndProjects`.
1868
1897
 
1869
1898
  """
1899
+ if target is None:
1900
+ target = self.actor
1870
1901
  if self._insert_sar is None:
1871
1902
  self._insert_sar = target.insert_action.request_from(self)
1872
1903
  else:
@@ -1877,7 +1908,7 @@ class ActionRequest(BaseRequest):
1877
1908
  # obj = st['data_record']
1878
1909
  # for k, v in values.items():
1879
1910
  # setattr(obj, k, v)
1880
- # print(20200302, st['data_record'])
1911
+ # print(20241018, st['data_record'])
1881
1912
  return self._insert_sar.ar2button(**button_attrs)
1882
1913
 
1883
1914
  def run(self, *args, **kw):
@@ -2030,7 +2061,10 @@ class ActionRequest(BaseRequest):
2030
2061
  if self.create_kw:
2031
2062
  kw.update(self.create_kw)
2032
2063
  if self.known_values:
2033
- kw.update(self.known_values)
2064
+ # kw.update(self.known_values)
2065
+ for k, v in self.known_values.items():
2066
+ if not "__" in k:
2067
+ kw[k] = v
2034
2068
  obj = self.actor.create_instance(self, **kw)
2035
2069
  return obj
2036
2070
 
@@ -2143,8 +2177,8 @@ class ActionRequest(BaseRequest):
2143
2177
  # actor = self.actor
2144
2178
  # return super(ActorRequest, self).spawn(actor, **kw)
2145
2179
 
2146
- def row_as_summary(self, obj, *args, **kwargs):
2147
- return self.actor.row_as_summary(self, obj, *args, **kwargs)
2180
+ def row_as_summary(self, obj, text=None, **kwargs):
2181
+ return self.actor.row_as_summary(self, obj, text, **kwargs)
2148
2182
 
2149
2183
  def row_as_page(self, row, **kwargs):
2150
2184
  return self.actor.row_as_page(self, row, **kwargs)
@@ -2242,6 +2276,7 @@ class ActionRequest(BaseRequest):
2242
2276
  column_names=None,
2243
2277
  header_links=False,
2244
2278
  max_width=None, # ignored
2279
+ show_links=None, # ignored
2245
2280
  hide_sums=None,
2246
2281
  ):
2247
2282
  """
@@ -2431,12 +2466,12 @@ class ActionRequest(BaseRequest):
2431
2466
  if v is None:
2432
2467
  td = E.td(**cellattrs)
2433
2468
  else:
2469
+ td = col.value2html(self, v, **cellattrs)
2470
+ # print("20240506 {} {}".format(col.__class__, tostring(td)))
2434
2471
  nv = col.value2num(v)
2435
2472
  if nv != 0:
2436
2473
  sums[i] += nv
2437
2474
  has_numeric_value = True
2438
- td = col.value2html(self, v, **cellattrs)
2439
- # print("20240506 {} {}".format(col.__class__, tostring(td)))
2440
2475
  col.apply_cell_format(td)
2441
2476
  self.actor.apply_cell_format(self, row, col, recno, td)
2442
2477
  cells.append(td)
lino/core/site.py CHANGED
@@ -621,7 +621,6 @@ class Site(object):
621
621
  db = self.get_database_settings()
622
622
  if db is not None:
623
623
  self.django_settings.update(DATABASES=db)
624
-
625
624
  else:
626
625
  self.site_dir = self.master_site.site_dir
627
626
  self._history_aware_logging = self.master_site._history_aware_logging
@@ -868,16 +867,13 @@ class Site(object):
868
867
  for x in self.local_apps:
869
868
  add(x)
870
869
 
871
- # actual_apps = []
872
870
  plugins = []
873
871
 
874
- # disabled_plugins = set()
875
-
876
872
  def install_plugin(app_name, needed_by=None):
877
873
  # print("20210305 install_plugin({})".format(app_name))
878
874
  # Django does not accept newstr, and we don't want to see
879
875
  # ``u'applabel'`` in doctests.
880
- app_name = str(app_name)
876
+ # app_name = str(app_name)
881
877
  # print("20160524 install_plugin(%r)" % app_name)
882
878
  app_mod = import_module(app_name)
883
879
 
@@ -907,14 +903,11 @@ class Site(object):
907
903
 
908
904
  # Can an `__init__.py` file explicitly set ``Plugin =
909
905
  # None``? Is that feature being used?
910
- app_class = getattr(app_mod, "Plugin", None)
911
- if app_class is None:
912
- app_class = Plugin
906
+ app_class = getattr(app_mod, "Plugin", Plugin)
907
+ # if app_class is None:
908
+ # app_class = Plugin
913
909
  cfg = PLUGIN_CONFIGS.pop(k, None)
914
910
  ip = app_class(self, k, app_name, app_mod, needed_by, cfg or dict())
915
- # cfg = PLUGIN_CONFIGS.pop(k, None)
916
- # if cfg:
917
- # ip.configure(**cfg)
918
911
 
919
912
  self.plugins.define(k, ip)
920
913
 
@@ -928,8 +921,6 @@ class Site(object):
928
921
  # plugins.append(dep)
929
922
 
930
923
  plugins.append(ip)
931
- # for dp in ip.disables_plugins:
932
- # disabled_plugins.add(dp)
933
924
 
934
925
  # lino is always the first plugin:
935
926
  install_plugin("lino")
@@ -951,17 +942,10 @@ class Site(object):
951
942
  # afterwards.
952
943
  # if self.get_auth_method() == 'session':
953
944
  if self.user_model:
954
- k = str("django.contrib.sessions")
945
+ k = "django.contrib.sessions"
955
946
  if k not in self.plugins:
956
947
  install_plugin(k)
957
948
 
958
- # for p in plugins:
959
- # if p.app_label in disabled_plugins \
960
- # or p.app_name in disabled_plugins:
961
- # plugins.remove(p)
962
- # del self.plugins[p.app_label]
963
-
964
- # self.update_settings(INSTALLED_APPS=tuple(actual_apps))
965
949
  self.update_settings(INSTALLED_APPS=tuple([p.app_name for p in plugins]))
966
950
  self.installed_plugins = tuple(plugins)
967
951
 
@@ -1009,13 +993,6 @@ class Site(object):
1009
993
 
1010
994
  for p in self.installed_plugins:
1011
995
  reg(p.__class__)
1012
- # for pp in plugin_parents(p.__class__):
1013
- # if p.app_label == 'contacts':
1014
- # print("20160524c %s" % pp)
1015
- # reg(p.__class__)
1016
-
1017
- # for m, p in self.override_modlib_models.items():
1018
- # print("20160524 %s : %s" % (m, p))
1019
996
 
1020
997
  self.installed_plugin_modules = set()
1021
998
  for p in self.installed_plugins:
@@ -2304,23 +2281,6 @@ class Site(object):
2304
2281
 
2305
2282
  yield E.span(*p)
2306
2283
 
2307
- def login(self, username=None, **kw):
2308
- from lino.core import requests
2309
-
2310
- self.startup()
2311
- User = self.user_model
2312
- if User and username:
2313
- try:
2314
- kw.update(user=User.objects.get(username=username))
2315
- except User.DoesNotExist:
2316
- raise User.DoesNotExist("'{0}' : no such user".format(username))
2317
-
2318
- # if not 'renderer' in kw:
2319
- # kw.update(renderer=self.ui.text_renderer)
2320
-
2321
- # import lino.core.urls # hack: trigger ui instantiation
2322
- return requests.BaseRequest(**kw)
2323
-
2324
2284
  def get_letter_date_text(self, today=None):
2325
2285
  sc = self.site_config.site_company
2326
2286
  if today is None:
lino/core/store.py CHANGED
@@ -53,6 +53,8 @@ from lino.utils.format_date import fds
53
53
  from lino.utils import IncompleteDate
54
54
  from lino.core.utils import DelayedValue
55
55
 
56
+ FIELD_TYPES = {}
57
+
56
58
 
57
59
  class StoreField(object):
58
60
  """
@@ -207,6 +209,14 @@ class StoreField(object):
207
209
  setattr(instance, self.name, v)
208
210
  return True
209
211
 
212
+ @classmethod
213
+ def register_for_field(cls, dftype, elemtype):
214
+ if dftype in FIELD_TYPES:
215
+ raise Exception(f"Duplicate field type {dftype}")
216
+ FIELD_TYPES[dftype] = cls
217
+ from lino.core.elems import _FIELD2ELEM
218
+ _FIELD2ELEM.append((dftype, elemtype))
219
+
210
220
  def format_value(self, ar, v):
211
221
  """
212
222
  Return a plain textual representation as a unicode string
@@ -424,19 +434,6 @@ class PreviewTextStoreField(StoreField):
424
434
  for name in self.column_names():
425
435
  d[name] = getattr(row, name)
426
436
 
427
-
428
- class MeasurementStoreField(StoreField):
429
- def extract_form_data(self, obj, post_data, ar=None):
430
- unit = self.field.measurement.STANDARD_UNIT
431
- if hasattr(obj, "unit"):
432
- new_unit = post_data.get("unitHidden", None)
433
- if new_unit is not None:
434
- unit = new_unit
435
- elif obj.unit is not None:
436
- unit = obj.unit.name
437
- return self.field.measurement(**{unit: float(post_data.get(self.name, "0.0"))})
438
-
439
-
440
437
  # class LinkedForeignKeyField(ForeignKeyStoreField):
441
438
 
442
439
  # def get_rel_to(self,obj):
@@ -821,7 +818,10 @@ class DateStoreField(StoreField):
821
818
  try:
822
819
  return datetime.date(*settings.SITE.parse_date(v))
823
820
  except Exception as e:
824
- raise Warning("Invalid date '{}'' : {}".format(v, e))
821
+ # The front end is responsible for validating before submitting.
822
+ # Here it's too late to complain.
823
+ return None
824
+ # raise Warning("Invalid date '{}'' : {}".format(v, e))
825
825
 
826
826
  def format_value(self, ar, v):
827
827
  """Return a plain textual representation of this value as a unicode
@@ -1059,13 +1059,13 @@ def create_atomizer(holder, fld, name):
1059
1059
  return DisplayStoreField(fld, name)
1060
1060
  if isinstance(fld, models.IntegerField):
1061
1061
  return IntegerStoreField(fld, name)
1062
- if isinstance(fld, fields.MeasurementField):
1063
- return MeasurementStoreField(fld, name)
1064
1062
  if isinstance(fld, fields.PreviewTextField):
1065
1063
  return PreviewTextStoreField(fld, name)
1066
1064
  if isinstance(fld, models.ManyToOneRel):
1067
1065
  # raise Exception("20190625 {} {} {}".format(holder, fld, name))
1068
1066
  return
1067
+ if (sft := FIELD_TYPES.get(fld.__class__, None)) is not None:
1068
+ return sft(fld, name)
1069
1069
  kw = {}
1070
1070
  if choosers.uses_simple_values(holder, fld):
1071
1071
  return StoreField(fld, name, **kw)
lino/core/tables.py CHANGED
@@ -639,23 +639,6 @@ method in order to sort the rows of the queryset.
639
639
  # # print("20181230 detail_pointer() {}".format(cls))
640
640
  # return obj.as_summary_item(ar)
641
641
 
642
- @classmethod
643
- def request(self, master_instance=None, **kw):
644
- """Return a new :class:`ActionRequest
645
- <lino.core.requests.ActionRequest>` on this table.
646
-
647
- The :attr:`master_instance
648
- <lino.core.requests.ActionRequest.master_instance>` can be
649
- specified as optional first positional argument.
650
-
651
- """
652
- from lino.core.requests import ActionRequest
653
- kw.update(actor=self)
654
- if master_instance is not None:
655
- kw.update(master_instance=master_instance)
656
- kw.setdefault("action", self.default_action)
657
- return ActionRequest(**kw)
658
-
659
642
  @classmethod
660
643
  def run_action_from_console(self, pk=None, an=None):
661
644
  """
lino/core/utils.py CHANGED
@@ -23,6 +23,8 @@ from django.core import exceptions
23
23
  from django.http import QueryDict
24
24
 
25
25
  from lino.utils.html import E, assert_safe, tostring
26
+ from lino.utils import capture_output
27
+ from lino.utils.ranges import isrange
26
28
 
27
29
  from django.core.validators import validate_email, ValidationError, URLValidator
28
30
 
@@ -309,21 +311,41 @@ def range_filter(value, f1, f2):
309
311
  q2 = Q(**{f2 + "__isnull": True}) | Q(**{f2 + "__gte": value})
310
312
  return Q(q1, q2)
311
313
 
312
-
313
314
  def inrange_filter(fld, rng, **kw):
314
315
  """Assuming a database model with a field named `fld`, return a Q
315
- object to select those rows whose `fld` value is not null and
316
- within the given range `rng`. `rng` must be a tuple or list with
317
- two items.
318
-
316
+ object to select the rows having value for `fld` within the given range `rng`.
317
+ `rng` must be a tuple or list with two items.
319
318
  """
320
- assert rng[0] <= rng[1]
319
+
320
+ # assert rng[0] <= rng[1]
321
321
  kw[fld + "__isnull"] = False
322
- kw[fld + "__gte"] = rng[0]
323
- kw[fld + "__lte"] = rng[1]
322
+ if rng[0] is not None:
323
+ kw[fld + "__gte"] = rng[0]
324
+ if rng[1] is not None:
325
+ kw[fld + "__lte"] = rng[1]
324
326
  return Q(**kw)
325
327
 
326
328
 
329
+ def overlap_range_filter(sv, ev, f1, f2, **kw):
330
+ """
331
+
332
+ Return a Q object to select all objects having fields `f1` and `f2` define a
333
+ range that overlaps with the range specified by `sv` (start value) and `ev`
334
+ (end value).
335
+
336
+ For example, when I specify filter parameters `Date from` 2024-02-21 and
337
+ `until` 2024-03-12 in a :class:`lino.modlib.periods.StoredPeriods` table, I
338
+ want both February and March. Tested examples see
339
+ :ref:`dg.plugins.periods.period_filter`.
340
+
341
+ """
342
+ # if not isrange(rng[0], rng[1]):
343
+ # raise ValueError(f"{rng} is not a valid range")
344
+ if not ev:
345
+ ev = sv
346
+ return Q(**{f1+"__lte" : ev, f2+"__gte" : sv})
347
+
348
+
327
349
  def babelkw(*args, **kw):
328
350
  return settings.SITE.babelkw(*args, **kw)
329
351
 
@@ -679,7 +701,7 @@ class ParameterPanel(object):
679
701
 
680
702
  Subclassed e.g. by
681
703
  :class:`lino.mixins.periods.ObservedDateRange`.
682
- :class:`lino_xl.lib.accounting.AccountingPeriodRange`.
704
+ :class:`lino_xl.lib.accounting.PeriodRangeParameters`.
683
705
  """
684
706
 
685
707
  def __init__(self, **kw):
@@ -1042,7 +1064,7 @@ class InstanceAction:
1042
1064
  """
1043
1065
  if len(args) and isinstance(args[0], BaseRequest):
1044
1066
  raise ChangedAPI("20181004")
1045
- ar = self.bound_action.request()
1067
+ ar = self.bound_action.request(renderer=settings.SITE.kernel.text_renderer)
1046
1068
  self.run_from_code(ar, *args, **kwargs)
1047
1069
  return ar.response
1048
1070
 
@@ -1087,3 +1109,29 @@ class PhantomRow(VirtualRow):
1087
1109
 
1088
1110
  def __str__(self):
1089
1111
  return str(self._ar.get_action_title())
1112
+
1113
+
1114
+
1115
+ def login(username=None, **kwargs):
1116
+ """Return a basic :term:`action request` with the specified user signed in.
1117
+ """
1118
+ from lino.core.requests import BaseRequest # avoid circular import
1119
+ # settings.SITE.startup()
1120
+ User = settings.SITE.user_model
1121
+ if User and username:
1122
+ try:
1123
+ kwargs.update(user=User.objects.get(username=username))
1124
+ except User.DoesNotExist:
1125
+ raise User.DoesNotExist(f"'{username}' : no such user")
1126
+
1127
+ kwargs.setdefault("show_urls", False)
1128
+ # import lino.core.urls # hack: trigger ui instantiation
1129
+ return BaseRequest(**kwargs)
1130
+
1131
+ def show(*args, **kwargs):
1132
+ """Print the specified data table to stdout."""
1133
+ return login().show(*args, **kwargs)
1134
+
1135
+ def shows(*args, **kwargs):
1136
+ """Return the output of :func:`show`."""
1137
+ return capture_output(show, *args, **kwargs)
lino/core/views.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2010-2020 Rumma & Ko Ltd
1
+ # Copyright 2010-2024 Rumma & Ko Ltd
2
2
  # License: GNU Affero General Public License v3 (see file COPYING for details)
3
3
  """Utility functions used by :mod:`lino.modlib.extjs.views`.
4
4
 
@@ -99,6 +99,7 @@ def action_request(app_label, actor, request, rqdata, is_list, **kw):
99
99
  # The text of an Exception may not be
100
100
  # internationalized because some error handling code
101
101
  # may want to write it to a plain ascii stream.
102
+ kw.update(renderer=settings.SITE.kernel.default_renderer)
102
103
  ar = rpt.request(request=request, action=a, rqdata=rqdata, **kw)
103
104
  # print("20210403b", a.action.__class__, rqdata)
104
105
  return ar
lino/help_texts.py CHANGED
@@ -376,6 +376,18 @@ help_texts = {
376
376
  'lino.modlib.linod.SystemTask.log_level' : _("""The logging level to apply when running this task."""),
377
377
  'lino.modlib.linod.SystemTask.run' : _("""Performs a routine job."""),
378
378
  'lino.modlib.linod.SystemTasks' : _("""The default actor for the SystemTask model."""),
379
+ 'lino.modlib.periods.StoredYear' : _("""The Django model used to store an fiscal year."""),
380
+ 'lino.modlib.periods.StoredPeriod' : _("""The Django model used to store an accounting period."""),
381
+ 'lino.modlib.periods.StoredYears' : _("""The fiscal years defined in this database."""),
382
+ 'lino.modlib.periods.StoredPeriods' : _("""The accounting periods defined in this database."""),
383
+ 'lino.modlib.periods.PeriodTypes' : _("""A list of choices for the values allowed as periods.period_type."""),
384
+ 'lino.modlib.periods.PeriodRange' : _("""Model mixin for objects that cover a range of accounting periods."""),
385
+ 'lino.modlib.periods.PeriodRange.start_period' : _("""The first period of the range to cover."""),
386
+ 'lino.modlib.periods.PeriodRange.end_period' : _("""The last period of the range to cover."""),
387
+ 'lino.modlib.periods.PeriodRangeObservable' : _("""Model mixin for objects that can be filtered by a range of accounting periods. This adds two parameter fields start_period and end_period to every table on this model."""),
388
+ 'lino.modlib.periods.StoredPeriodRange' : _("""A parameter panel with two fields:"""),
389
+ 'lino.modlib.periods.StoredPeriodRange.start_period' : _("""Start of observed period range."""),
390
+ 'lino.modlib.periods.StoredPeriodRange.end_period' : _("""Optional end of observed period range. Leave empty to consider only the Start period."""),
379
391
  'lino.modlib.publisher.Page' : _("""The Django model that represents a content page."""),
380
392
  'lino.modlib.publisher.PublisherBuildMethod' : _("""This deserves better documentation."""),
381
393
  'lino.modlib.publisher.Publishable' : _("""Model mixin to add to models that are potentially publishable."""),
@@ -645,6 +657,7 @@ help_texts = {
645
657
  'lino.modlib.system.DurationUnits' : _("""The list of possible duration units defined by this application."""),
646
658
  'lino.modlib.system.DurationUnit' : _("""Base class for the choices in the DurationUnits choicelist."""),
647
659
  'lino.modlib.system.DurationUnit.add_duration' : _("""Return a date or datetime obtained by adding value times this unit to the specified value orig. Returns None is orig is empty."""),
660
+ 'lino.modlib.system.DisplayColors' : _("""A list of colors to be specified for displaying."""),
648
661
  'lino.modlib.system.BleachChecker' : _("""A data checker used to find unbleached html content."""),
649
662
  'lino.modlib.system.Genders' : _("""Defines the possible choices for the gender of a person (“male”, “female” and “nonbinary”)."""),
650
663
  'lino.modlib.system.YesNo' : _("""A choicelist with two values “Yes” and “No”."""),
@@ -3,11 +3,9 @@
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
5
  import argparse
6
-
7
- from django.conf import settings
6
+ from lino.api import rt
8
7
  from django.core.management.base import BaseCommand, CommandError
9
8
 
10
-
11
9
  class Command(BaseCommand):
12
10
  help = "Show the content of a specified table to standard output."
13
11
 
@@ -43,6 +41,6 @@ class Command(BaseCommand):
43
41
  spec = args[0]
44
42
 
45
43
  username = options["username"]
46
- ses = settings.SITE.login(username)
44
+ ses = rt.login(username)
47
45
 
48
46
  ses.show(spec, language=options["language"])