lino 25.1.5__py3-none-any.whl → 25.2.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 (41) hide show
  1. lino/__init__.py +1 -1
  2. lino/core/actions.py +10 -3
  3. lino/core/actors.py +9 -3
  4. lino/core/callbacks.py +3 -2
  5. lino/core/fields.py +4 -0
  6. lino/core/keyboard.py +7 -2
  7. lino/core/model.py +1 -0
  8. lino/core/requests.py +7 -7
  9. lino/core/site.py +1 -2
  10. lino/core/store.py +2 -2
  11. lino/help_texts.py +4 -1
  12. lino/locale/bn/LC_MESSAGES/django.po +782 -710
  13. lino/locale/de/LC_MESSAGES/django.mo +0 -0
  14. lino/locale/de/LC_MESSAGES/django.po +1259 -1280
  15. lino/locale/django.pot +751 -702
  16. lino/locale/es/LC_MESSAGES/django.po +777 -708
  17. lino/locale/et/LC_MESSAGES/django.po +784 -709
  18. lino/locale/fr/LC_MESSAGES/django.po +1339 -1191
  19. lino/locale/nl/LC_MESSAGES/django.po +787 -712
  20. lino/locale/pt_BR/LC_MESSAGES/django.po +769 -700
  21. lino/locale/zh_Hant/LC_MESSAGES/django.po +769 -700
  22. lino/management/commands/demotest.py +7 -3
  23. lino/mixins/__init__.py +1 -1
  24. lino/modlib/checkdata/choicelists.py +5 -4
  25. lino/modlib/checkdata/models.py +9 -8
  26. lino/modlib/comments/fixtures/demo2.py +4 -2
  27. lino/modlib/help/models.py +5 -0
  28. lino/modlib/jinja/__init__.py +0 -4
  29. lino/modlib/memo/__init__.py +1 -1
  30. lino/modlib/periods/mixins.py +1 -25
  31. lino/modlib/periods/models.py +42 -9
  32. lino/modlib/system/choicelists.py +12 -11
  33. lino/utils/config.py +2 -0
  34. lino/utils/dbfreader.py +18 -24
  35. lino/utils/dpy.py +15 -3
  36. lino/utils/soup.py +136 -103
  37. {lino-25.1.5.dist-info → lino-25.2.0.dist-info}/METADATA +1 -1
  38. {lino-25.1.5.dist-info → lino-25.2.0.dist-info}/RECORD +41 -41
  39. {lino-25.1.5.dist-info → lino-25.2.0.dist-info}/WHEEL +0 -0
  40. {lino-25.1.5.dist-info → lino-25.2.0.dist-info}/licenses/AUTHORS.rst +0 -0
  41. {lino-25.1.5.dist-info → lino-25.2.0.dist-info}/licenses/COPYING +0 -0
@@ -59,6 +59,7 @@ class TestCase(DemoTestCase):
59
59
 
60
60
  def test_ipdict(self):
61
61
  if not settings.SITE.use_ipdict:
62
+ # print("20250126 no test because use_ipdict is False")
62
63
  return
63
64
  ipdict = dd.plugins.ipdict
64
65
 
@@ -100,7 +101,8 @@ class TestCase(DemoTestCase):
100
101
  # every new failure will now blacklist it again, the
101
102
  # max_failed_auth_per_ip no longer counts.
102
103
 
103
- time.sleep(1)
104
+ # time.sleep(1.5)
105
+ time.sleep(5)
104
106
  self.assertEqual(login("bad"), "Failed to sign in as robin.")
105
107
  self.assertEqual(rec.login_failures, 5)
106
108
  self.assertEqual(
@@ -108,8 +110,10 @@ class TestCase(DemoTestCase):
108
110
  )
109
111
  self.assertEqual(rec.login_failures, 5)
110
112
 
111
- time.sleep(1)
112
- self.assertEqual(login(dd.plugins.users.demo_password), "Now signed in as Robin Rood")
113
+ time.sleep(1.5)
114
+ self.assertEqual(
115
+ login(dd.plugins.users.demo_password),
116
+ "Now signed in as Robin Rood")
113
117
 
114
118
  # Once you manage to authenticate, your ip address gets removed from the
115
119
  # blacklist, i.e. when you log out and in for some reason, you get again
lino/mixins/__init__.py CHANGED
@@ -70,7 +70,7 @@ class Phonable(model.Model):
70
70
 
71
71
  class Modified(model.Model):
72
72
  """
73
- Adds a a timestamp field which holds the last modification time of
73
+ Adds a a timestamp field that holds the last modification time of
74
74
  every individual database object.
75
75
 
76
76
  .. attribute:: modified
@@ -34,8 +34,9 @@ class Checker(dd.Choice):
34
34
  self = None
35
35
  model = None
36
36
  help_text = None
37
+ no_auto = False # SupplierChecker.activate() sets no_auto to True
37
38
 
38
- def __init__(self):
39
+ def __init__(self, **kwargs):
39
40
  # value = self.__module__ + '.' + self.__class__.__name__
40
41
  value = self.__module__.split(".")[-2] + "." + self.__class__.__name__
41
42
  # if isinstance(self.model, six.string_types):
@@ -46,13 +47,13 @@ class Checker(dd.Choice):
46
47
  text = value
47
48
  else:
48
49
  text = self.verbose_name
49
- super().__init__(value, text, None)
50
+ super().__init__(value, text, None, **kwargs)
50
51
 
51
52
  @classmethod
52
- def activate(cls):
53
+ def activate(cls, **kwargs):
53
54
  if cls.self is not None:
54
55
  raise Exception("Duplicate call to {0}.activate()".format(cls))
55
- cls.self = cls()
56
+ cls.self = cls(**kwargs)
56
57
  Checkers.add_item_instance(cls.self)
57
58
 
58
59
  @classmethod
@@ -250,18 +250,19 @@ def get_checkers_for(model):
250
250
  return get_checkable_models()[model]
251
251
 
252
252
 
253
- def check_instance(obj):
254
- """
255
- Run all checkers on the given instance. Return list of problem messages.
256
- """
253
+ def check_instance(obj, **kwargs):
257
254
  for chk in get_checkers_for(obj.__class__):
258
- for prb in chk.check_instance(obj):
259
- yield prb
255
+ for fixable, msg in chk.check_instance(obj, **kwargs):
256
+ if fixable:
257
+ msg = f"(\u2605) {msg}"
258
+ print(msg)
260
259
 
261
260
 
262
- def get_checkable_models(*args):
261
+ def get_checkable_models(*args, only_auto=False):
263
262
  checkable_models = OrderedDict()
264
263
  for chk in Checkers.get_list_items():
264
+ if only_auto and chk.no_auto:
265
+ continue
265
266
  if len(args):
266
267
  skip = True
267
268
  for arg in args:
@@ -280,7 +281,7 @@ def check_data(ar, args=[], fix=True, prune=False):
280
281
  # verbosity 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output
281
282
  Message = rt.models.checkdata.Message
282
283
  # raise Exception("20231230")
283
- mc = get_checkable_models(*args)
284
+ mc = get_checkable_models(*args, only_auto=True)
284
285
  if len(mc) == 0 and len(args) > 0:
285
286
  raise Exception("No checker matches {0}".format(args))
286
287
  if prune:
@@ -1,4 +1,4 @@
1
- # Copyright 2016-2023 Rumma & Ko Ltd
1
+ # Copyright 2016-2025 Rumma & Ko Ltd
2
2
  # License: GNU Affero General Public License v3 (see file COPYING for details)
3
3
  """
4
4
  Adds some demo comments.
@@ -61,7 +61,9 @@ def objects():
61
61
  if dd.get_plugin_setting("uploads", "with_volumes", False):
62
62
  Upload = rt.models.uploads.Upload
63
63
  SCREENSHOTS = Cycler(Upload.objects.filter(volume__ref="screenshots"))
64
- assert len(SCREENSHOTS) > 0
64
+ if len(SCREENSHOTS) == 0:
65
+ # e.g. when lino_book is not installed
66
+ SCREENSHOTS = None
65
67
  else:
66
68
  SCREENSHOTS = None
67
69
 
@@ -16,6 +16,11 @@ class OpenHelpWindow(dd.Action):
16
16
  action_name = "open_help"
17
17
  # icon_name = 'help'
18
18
  default_format = "ajax"
19
+
20
+ # add spaces because React enlarges button_text when len() is 1 and for "?"
21
+ # this doesn't look nice. But adding spaces breaks a series of doctests, so
22
+ # I undid that change:
23
+ # button_text = " ? "
19
24
  button_text = "?"
20
25
  select_rows = False
21
26
  help_text = _("Open Help Window")
@@ -39,10 +39,6 @@ class Plugin(ad.Plugin):
39
39
 
40
40
  self.renderer = JinjaRenderer(self)
41
41
 
42
- # internal backwards compat:
43
- # kernel.site.jinja_env = self.renderer.jinja_env
44
- # TODO: remove above lines and convert old code
45
-
46
42
  def list_templates(self, ext, *groups):
47
43
  """Return a list of possible choices for a field that contains a
48
44
  template name.
@@ -59,7 +59,7 @@ class Plugin(ad.Plugin):
59
59
  """
60
60
 
61
61
  short_preview_length = 300
62
- short_preview_image_height = "8em"
62
+ # short_preview_image_height = "8em"
63
63
 
64
64
  def get_requirements(self, site):
65
65
  if self.use_markup:
@@ -1,5 +1,5 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2008-2024 Rumma & Ko Ltd
2
+ # Copyright 2008-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
5
  import datetime
@@ -12,30 +12,6 @@ from lino.mixins import Referrable
12
12
  from lino.utils import ONE_DAY
13
13
 
14
14
  from lino.modlib.office.roles import OfficeStaff
15
- from lino.modlib.system.choicelists import DurationUnits
16
-
17
-
18
- def get_range_for_date(date):
19
- """
20
- Return the default start and end date of the period to create for the given
21
- date.
22
- """
23
- pt = dd.plugins.periods.period_type
24
- month = date.month
25
- year = date.year
26
- month -= dd.plugins.periods.start_month
27
- if month < 0:
28
- month += 12
29
- year -= 1
30
- period = int(month / pt.duration)
31
- month = dd.plugins.periods.start_month + period * pt.duration
32
- if month > 12:
33
- month -= 12
34
- year += 1
35
- sd = datetime.date(year, month, 1)
36
- # ed = sd.replace(month=sd.month + pt.duration + 1, 1) - ONE_DAY
37
- ed = DurationUnits.months.add_duration(sd, pt.duration) - ONE_DAY
38
- return (sd, ed)
39
15
 
40
16
 
41
17
  class PeriodRange(dd.Model):
@@ -1,5 +1,5 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2008-2024 Rumma & Ko Ltd
2
+ # Copyright 2008-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
5
  import datetime
@@ -11,9 +11,8 @@ from lino import mixins
11
11
  from lino.utils import ONE_DAY
12
12
  from lino.mixins.periods import DateRange
13
13
  from lino.mixins import Referrable
14
-
14
+ from lino.modlib.system.choicelists import DurationUnits
15
15
  from lino.modlib.office.roles import OfficeStaff
16
- from .mixins import get_range_for_date
17
16
  from .choicelists import PeriodTypes, PeriodStates
18
17
 
19
18
  NEXT_YEAR_SEP = "/"
@@ -59,15 +58,26 @@ class StoredYear(DateRange, Referrable):
59
58
  return str(year) + NEXT_YEAR_SEP + str(year+1)[-2:]
60
59
 
61
60
  @classmethod
62
- def get_or_create_from_date(cls, date):
61
+ def get_range_for_date(cls, date):
62
+ month = date.month
63
+ year = date.year
64
+ month -= dd.plugins.periods.start_month - 1
65
+ if month < 1:
66
+ year -= 1
67
+ sd = datetime.date(year, dd.plugins.periods.start_month, 1)
68
+ ed = sd.replace(year=year+1) - ONE_DAY
69
+ return (sd, ed)
70
+
71
+ @classmethod
72
+ def get_or_create_from_date(cls, date, save=True):
63
73
  ref = cls.get_ref_for_date(date)
64
74
  obj = cls.get_by_ref(ref, None)
65
75
  if obj is None:
66
- sd = datetime.date(date.year, dd.plugins.periods.start_month, 1)
67
- ed = sd.replace(year=date.year+1) - ONE_DAY
76
+ sd, ed = cls.get_range_for_date(date)
68
77
  obj = cls(ref=ref, start_date=sd, end_date=ed)
69
- obj.full_clean()
70
- obj.save()
78
+ if save:
79
+ obj.full_clean()
80
+ obj.save()
71
81
  return obj
72
82
 
73
83
  def __str__(self):
@@ -157,12 +167,35 @@ class StoredPeriod(DateRange, Referrable):
157
167
  # period = (month_offset % (periods_per_year-1)) + 1
158
168
  return pt.ref_template.format(**locals())
159
169
 
170
+ @classmethod
171
+ def get_range_for_date(cls, date):
172
+ """
173
+ Return the default start and end date of the period to create for the given
174
+ date.
175
+ """
176
+ pt = dd.plugins.periods.period_type
177
+ month = date.month
178
+ year = date.year
179
+ month -= dd.plugins.periods.start_month
180
+ if month < 1:
181
+ month += 12
182
+ year -= 1
183
+ period = int(month / pt.duration)
184
+ month = dd.plugins.periods.start_month + period * pt.duration
185
+ if month > 12:
186
+ month -= 12
187
+ year += 1
188
+ sd = datetime.date(year, month, 1)
189
+ # ed = sd.replace(month=sd.month + pt.duration + 1, 1) - ONE_DAY
190
+ ed = DurationUnits.months.add_duration(sd, pt.duration) - ONE_DAY
191
+ return (sd, ed)
192
+
160
193
  @classmethod
161
194
  def get_or_create_from_date(cls, date): # get_default_for_date until 20241020
162
195
  ref = date2ref(date)
163
196
  obj = cls.get_by_ref(ref, None)
164
197
  if obj is None:
165
- sd, ed = get_range_for_date(date)
198
+ sd, ed = cls.get_range_for_date(date)
166
199
  obj = cls(ref=ref, start_date=sd, end_date=ed)
167
200
  obj.full_clean()
168
201
  obj.save()
@@ -17,7 +17,7 @@ from lino.core.roles import login_required, Explorer
17
17
  from lino.core.fields import virtualfield
18
18
  from lino.utils.dates import DateRangeValue
19
19
  from lino.utils.format_date import day_and_month, fds
20
- from lino.utils.html import mark_safe, format_html
20
+ from lino.utils.html import mark_safe, format_html, escape
21
21
 
22
22
 
23
23
  class YesNo(ChoiceList):
@@ -236,21 +236,22 @@ class DisplayColors(ChoiceList):
236
236
  item_class = DisplayColor
237
237
  required_roles = login_required(Explorer)
238
238
  column_names = "value name text font_color"
239
+ preferred_width = 10
239
240
 
240
241
  @virtualfield(models.CharField(_("Font color")))
241
242
  def font_color(cls, choice, ar):
242
243
  return choice.font_color
243
244
 
244
- # TODO: The combobox used to select a color should include a sample of each
245
- # color. The following should theoretically do it, but the <span> tag gets
246
- # escaped somewhere even though it is marked as safe.
247
- # @classmethod
248
- # def display_text(cls, bc):
249
- # sample = f"""<span style="background-color:{bc.name};color:{bc.font_color}">(sample)</span>"""
250
- # sample = mark_safe(sample)
251
- # txt = format_html("{} {}", bc.text, sample)
252
- # # raise Exception(f"20250118 {txt.__class__}")
253
- # return txt
245
+ @classmethod
246
+ def display_text(cls, bc):
247
+ # text = escape(bc.text)
248
+ # txt = f"""<span style="background-color:{bc.name};color:{bc.font_color}">{text}</span>"""
249
+ # txt = mark_safe(txt)
250
+ sample = f"""<span style="padding:3pt;background-color:{bc.name};color:{bc.font_color}">(sample)</span>"""
251
+ sample = mark_safe(sample)
252
+ txt = format_html("{} {}", bc.text, sample)
253
+ # raise Exception(f"20250118 {txt.__class__}")
254
+ return txt
254
255
 
255
256
  add = DisplayColors.add_item
256
257
  # cssColors = 'White Silver Gray Black Red Maroon Yellow Olive Lime Green Aqua Teal Blue Navy Fuchsia Purple'
lino/utils/config.py CHANGED
@@ -65,6 +65,8 @@ class ConfigDirCache(object):
65
65
  config_dirs = []
66
66
 
67
67
  def add_config_dir(name, mod):
68
+ if mod.__file__ is None:
69
+ raise Exception(f"20250206 Module {mod} has no __file__!")
68
70
  pth = join(dirname(mod.__file__), SUBDIR_NAME)
69
71
  if isdir(pth):
70
72
  # logger.info("add_config_dir %s %s", name, pth)
lino/utils/dbfreader.py CHANGED
@@ -1,20 +1,26 @@
1
- # Copyright 2003-2009-2016 Luc Saffre
1
+ # Copyright 2003-2025 Rumma & Ko Ltd
2
2
  # License: GNU Affero General Public License v3 (see file COPYING for details)
3
3
 
4
- # Based on original work by Lars Garshol
5
- # http://www.garshol.priv.no/download/software/python/dbfreader.py
6
-
7
- # modified by Luc Saffre who is interested in support for Clipper
8
- # rather than FoxPro support.
9
4
  """
10
- What's the format of a Clipper .dbf file?
11
- http://www.the-oasis.net/clipper-12.html#ss12.4
12
5
 
13
- http://www.clicketyclick.dk/databases/xbase/format/
6
+ Defines the :class:`DBFFile` class, used by :doc:`lino_xl.lib.tim2lino` to read
7
+ DBF and DBT files when both settings :attr:`use_dbfread
8
+ <lino_xl.lib.tim2lino.Plugin.use_dbfread>` and :attr:`use_dbf_py
9
+ <lino_xl.lib.tim2lino.Plugin.use_dbf_py>` are `False` (which is default).
10
+
11
+ Based on original work by Lars Garshol
12
+ http://www.garshol.priv.no/download/software/python/dbfreader.py
13
+
14
+ Modified by Luc Saffre to add support for Clipper dialect.
15
+
16
+ Sources of information:
17
+
18
+ - `What's the format of a Clipper .dbf file?
19
+ <https://www.the-oasis.net/clipper-12.html#ss12.4>`__ (broken link)
20
+
21
+ `Xbase & dBASE File Format Description by Erik Bachmann
22
+ <https://www.clicketyclick.dk/databases/xbase/format/>`__
14
23
  """
15
- # from builtins import hex
16
- # from builtins import str
17
- # from builtins import object
18
24
 
19
25
  import datetime
20
26
  from dateutil import parser as dateparser
@@ -56,20 +62,8 @@ def hex_analyze(number):
56
62
  print("%s\t%s\t%d" % (hex(ch), ch, ch))
57
63
 
58
64
 
59
- # def sort_by_key(list,key_func):
60
- # for ix in range(len(list)):
61
- # list[ix]=(key_func(list[ix]),list[ix])
62
-
63
- # list.sort()
64
-
65
- # for ix in range(len(list)):
66
- # list[ix]=list[ix][1]
67
-
68
- # return list
69
-
70
65
  # --- A class for the entire file
71
66
 
72
-
73
67
  class DBFFile(object):
74
68
  "Represents a single DBF file."
75
69
 
lino/utils/dpy.py CHANGED
@@ -10,7 +10,8 @@ from lino import logger
10
10
  from packaging.version import Version
11
11
 
12
12
  import os
13
- import imp
13
+ import importlib.util
14
+ import sys
14
15
  from unipath import Path
15
16
  # from lino import AFTER17
16
17
 
@@ -33,6 +34,16 @@ from lino.core.utils import obj2str, full_model_name
33
34
  SUFFIX = ".py"
34
35
 
35
36
 
37
+ # Importing a source file directly from path
38
+ # https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
39
+ def import_from_path(module_name, file_path):
40
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
41
+ module = importlib.util.module_from_spec(spec)
42
+ sys.modules[module_name] = module
43
+ spec.loader.exec_module(module)
44
+ return module
45
+
46
+
36
47
  def create_mti_child(parent_model, pk, child_model, **kw):
37
48
  """Similar to :func:`lino.utils.mti.insert_child`, but for usage in
38
49
  Python dumps (generated by :cmd:`pm dump2py`).
@@ -396,11 +407,12 @@ class DpyDeserializer(LoaderBase):
396
407
  fqname = fqname[: -len(SUFFIX)]
397
408
  print(fqname)
398
409
 
399
- desc = (SUFFIX, "r", imp.PY_SOURCE)
410
+ # desc = (SUFFIX, "r", imp.PY_SOURCE)
400
411
  # logger.info("20160817 %s...", options)
401
412
  logger.info("Loading data from %s", fp.name)
402
413
 
403
- module = imp.load_module(fqname, fp, fp.name, desc)
414
+ module = import_from_path(fqname, fp.name)
415
+ # module = imp.load_module(fqname, fp, fp.name, desc)
404
416
  # module = __import__(filename)
405
417
 
406
418
  for o in self.deserialize_module(module, **options):