lino 25.4.3__py3-none-any.whl → 25.4.5__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.
@@ -1,5 +1,5 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2020-2022 Rumma & Ko Ltd
2
+ # Copyright 2020-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
5
  import os
@@ -8,7 +8,7 @@ from copy import copy
8
8
  from django.db import models
9
9
  from django.conf import settings
10
10
  from django.utils import translation
11
- from django.utils.text import format_lazy
11
+ # from django.utils.text import format_lazy
12
12
 
13
13
  from lino.api import dd, rt, _
14
14
  from lino.utils.html import E, tostring, join_elems
@@ -181,19 +181,40 @@ class SpecialPage(dd.Choice):
181
181
  # pointing_field_name = 'publisher.Page.special_page'
182
182
  # show_values = True
183
183
 
184
- def __init__(self, *args, **kwargs):
185
- self.default_values = dict()
186
- for k in ("ref", "title"):
184
+ def __init__(self, name, text=None, parent=None, **kwargs):
185
+ self.parent_value = parent
186
+ self._default_values = dict()
187
+ for k in ("ref", "title", "filler", "body"):
187
188
  if k in kwargs:
188
- self.default_values[k] = kwargs.pop(k)
189
- super().__init__(*args, **kwargs)
190
- if not "title" in self.default_values:
191
- self.default_values["title"] = self.text
192
-
193
- def create_object(self, **kwargs):
194
- kwargs.update(self.default_values)
195
- kwargs.update(special_page=self)
196
- return self.pointing_field.model(**kwargs)
189
+ self._default_values[k] = kwargs.pop(k)
190
+ super().__init__(name, text, name, **kwargs)
191
+ # if (filler := self.default_values.get('filler', None)):
192
+ # if "title" not in self.default_values:
193
+ # self.default_values["title"] = filler.data_view.get_actor_label()
194
+ # else:
195
+ # if "title" not in self.default_values:
196
+ # self.default_values["title"] = self.text
197
+
198
+ def on_page_created(self, obj):
199
+ for k, v in self._default_values.items():
200
+ setattr(obj, k, v)
201
+ if obj.filler and not obj.title:
202
+ obj.title = obj.filler.data_view.get_actor_label()
203
+ if not obj.title:
204
+ obj.title = self.text or "20250422"
205
+ if self.parent_value:
206
+ psp = self.choicelist.get_by_value(self.parent_value)
207
+ obj.parent = psp.get_object()
208
+
209
+ # def get_object(self, ar):
210
+ def get_object(self):
211
+ language = translation.get_language()
212
+ # if len(settings.SITE.languages) == 1:
213
+ # language = translation.get_language()
214
+ # else:
215
+ # language = ar.request.LANGUAGE_CODE
216
+ # return rt.models.publisher.Page.objects.get(ref=self.defaul_values['ref'], language=language)
217
+ return rt.models.publisher.Page.objects.get(special_page=self, language=language)
197
218
 
198
219
 
199
220
  class SpecialPages(dd.ChoiceList):
@@ -227,9 +248,9 @@ class SpecialPages(dd.ChoiceList):
227
248
 
228
249
  add = SpecialPages.add_item
229
250
 
230
- add("100", _("Home"), "home", ref="index", body=_("Welcome to our great website."))
231
- add("200", _("Terms and conditions"), "terms")
232
- add("300", _("Privacy policy"), "privacy")
233
- add("400", _("Cookie settings"), "cookies")
234
- add("500", _("Copyright"), "copyright")
235
- add("600", _("About us"), "about")
251
+ add("home", _("Home"), ref="index", body=_("Welcome to our great website."))
252
+ add("terms", _("Terms and conditions"))
253
+ add("privacy", _("Privacy policy"))
254
+ add("cookies", _("Cookie settings"))
255
+ add("copyright", _("Copyright"))
256
+ add("about", _("About us"))
@@ -6,6 +6,12 @@
6
6
  {% if isinstance(obj, rt.models.publisher.Page) -%}
7
7
  {{obj.get_prev_link(ar)}} | {{obj.get_next_link(ar)}} |
8
8
  {% endif -%}
9
+ {% if dd.is_installed('react') %}
10
+ {% set ar_react = obj.get_default_table().request(parent=ar, permalink_uris=True, renderer=dd.plugins.react.renderer) %}
11
+ {% if ar_react.obj2url(obj) %}
12
+ {{ tostring(ar_react.obj2html(obj, text='Edit')) }} |
13
+ {% endif %}
14
+ {% endif %}
9
15
  {% if ar.request and ar.request.path != '/' -%}
10
16
  <a href="{{ar.get_home_url()}}">{{_("Home")}}</a>
11
17
  {% else -%}
@@ -32,6 +38,30 @@
32
38
  </div>
33
39
  {% endblock %}
34
40
 
41
+ {% block navbar %}
42
+ <nav class="navbar navbar-default" role="navigation">
43
+ <div class="container-fluid">
44
+ {% set index_node, home_children = obj.home_and_children(ar) %}
45
+ <div class="navbar-header">
46
+ <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-navbar-collapsible-content" aria-expanded="false">
47
+ <span class="sr-only">Toggle navigation</span>
48
+ <span class="icon-bar"></span>
49
+ <span class="icon-bar"></span>
50
+ <span class="icon-bar"></span>
51
+ </button>
52
+ <a class="navbar-brand" href="{{ar.get_home_url()}}">{{ index_node.title }}</a>
53
+ </div>
54
+ <div class="collapse navbar-collapse" id="bs-navbar-collapsible-content">
55
+ <ul class="nav navbar-nav">
56
+ {% for child in home_children %}
57
+ <li>{{tostring(ar.obj2html(child))}}</li>
58
+ {% endfor %}
59
+ </ul>
60
+ </div>
61
+ </div>
62
+ </nav>
63
+ {% endblock %}
64
+
35
65
  {% block content %}
36
66
  <div class="row-fluid">
37
67
  {% for chunk in obj.as_page(ar) %}
@@ -0,0 +1,12 @@
1
+ from asgiref.sync import async_to_sync
2
+ from django.conf import settings
3
+ from lino.modlib.linod.choicelists import Procedures
4
+ from lino.api import rt
5
+
6
+
7
+ def objects():
8
+ yield None
9
+ ar = rt.login("robin")
10
+ if True: # settings.SITE.use_linod:
11
+ Procedures.update_publisher_pages.run(ar)
12
+ # async_to_sync(Procedures.update_publisher_pages.run)(ar)
@@ -18,10 +18,11 @@ def objects():
18
18
  with translation.override(lng.django_code):
19
19
  kwargs = dict(language=lng.django_code, special_page=sp)
20
20
  kwargs.update(publishing_state="published")
21
- kwargs.update(sp.default_values)
21
+ # kwargs.update(sp.default_values)
22
22
  if lng.suffix:
23
23
  kwargs.update(translated_from=translated_from)
24
24
  obj = Page(**kwargs)
25
+ sp.on_page_created(obj)
25
26
  yield obj
26
27
  if not lng.suffix:
27
28
  translated_from = obj
@@ -0,0 +1,30 @@
1
+ from lino.api import rt, _
2
+
3
+ home_children = [
4
+ (_("Mission"), None, []),
5
+ (_("Maxim"), None, []),
6
+ (_("Propaganda"), None, []),
7
+ (_("About us"), None, [
8
+ (_("Team"), None, []),
9
+ (_("History"), None, []),
10
+ (_("Contact"), None, []),
11
+ (_("Terms & conditions"), None, []),
12
+ ]),
13
+ ]
14
+
15
+
16
+ def objects():
17
+ image = rt.models.uploads.Upload.objects.first()
18
+ print('='*80)
19
+ print(image)
20
+ def iterate(iterable):
21
+ try:
22
+ for obj in iterable:
23
+ yield iterate(obj)
24
+ except TypeError:
25
+ if (obj := iterable).title == 'Mission':
26
+ obj.main_image = image
27
+ yield obj
28
+ for obj in rt.models.publisher.make_demo_pages(home_children):
29
+ yield iterate(obj)
30
+
@@ -7,6 +7,7 @@ from lino.utils.html import tostring
7
7
  from lino.api import dd, rt, _
8
8
 
9
9
  from django import http
10
+ from django.db import models
10
11
  from django.conf import settings
11
12
  from django.utils.translation import get_language
12
13
  from lino.core.renderer import add_user_language
@@ -124,6 +125,11 @@ class Publishable(Printable):
124
125
  # # context = dict(obj=self, request=request, language=get_language())
125
126
  # return template.render(**context)
126
127
 
128
+ def home_and_children(self, ar):
129
+ home = rt.models.publisher.SpecialPages.home.get_object()
130
+ return home, rt.models.publisher.Page.objects.filter(parent=home)
131
+ # return dv.model.objects.filter(models.Q(parent=index_node) | models.Q(ref='index'), language=language)
132
+
127
133
  def get_publisher_response(self, ar):
128
134
  if not self.is_public():
129
135
  return http.HttpResponseNotFound(
@@ -147,6 +153,7 @@ class PublishableContent(Publishable):
147
153
  language = dd.LanguageField()
148
154
  publishing_state = PublishingStates.field(default="draft")
149
155
  filler = PageFillers.field(blank=True, null=True)
156
+ main_image = dd.ForeignKey('uploads.Upload', blank=True, null=True, verbose_name=_("Main image"))
150
157
 
151
158
  def get_print_language(self):
152
159
  return self.language
@@ -9,10 +9,13 @@ from django.conf import settings
9
9
  from django.utils import translation
10
10
  from django.utils.translation import pgettext_lazy
11
11
 
12
- from lorem import get_paragraph
13
12
  from django.utils import translation
14
13
  from django.conf import settings
15
14
 
15
+ try:
16
+ from lorem import get_paragraph
17
+ except ImportError:
18
+ lorem = None
16
19
 
17
20
  # from django.utils.translation import get_language
18
21
  from django.utils.html import mark_safe
@@ -108,6 +111,17 @@ class Page(
108
111
  # def as_story_item(self, ar, **kwargs):
109
112
  # return "".join(self.as_page(ar, **kwargs))
110
113
 
114
+ def as_paragraph(self, ar):
115
+ title = E.b(escape(self.title))
116
+ url = ar.obj2url(self)
117
+ if url is not None:
118
+ title = E.a(title, href=url, style="text-decoration: none; color: black;")
119
+ body = self.get_body_parsed(ar, short=True)
120
+ if body:
121
+ body = " - " + body
122
+ item = E.li(title, body)
123
+ return tostring(item)
124
+
111
125
  def toc_html(self, ar, max_depth=1):
112
126
  def li(obj):
113
127
  # return "<li>{}</li>".format(obj.memo2html(ar, str(obj)))
@@ -134,7 +148,7 @@ class Page(
134
148
  else:
135
149
  title = "<b>{}</b> — ".format(escape(self.title))
136
150
  title += self.get_body_parsed(ar, short=True)
137
- title = "<li>{}</i>".format(title)
151
+ title = "<li>{}</li>".format(title)
138
152
  # edit_url = ar.renderer.obj2url(ar, self)
139
153
  # url = self.publisher_url(ar)
140
154
  # print("20231029", ar.renderer)
@@ -161,9 +175,20 @@ class Page(
161
175
 
162
176
  # if display_mode in ("detail", "story"):
163
177
  if display_mode == "detail":
164
- if hlevel == 1 and not dd.plugins.memo.use_markup:
178
+ if hlevel == 1 and not dd.plugins.memo.use_markup and self.ref != 'index':
165
179
  yield self.toc_html(ar)
166
180
 
181
+ if hlevel == 1 and self.main_image:
182
+ yield f"""
183
+ <div class="row">
184
+ <div class="center-block">
185
+ <a href="#" class="thumbnail">
186
+ <img src="{self.main_image.get_media_file().get_image_url()}">
187
+ </a>
188
+ </div>
189
+ </div>
190
+ """
191
+
167
192
  # yield self.body_full_preview
168
193
  yield self.get_body_parsed(ar, short=False)
169
194
 
@@ -131,7 +131,7 @@ class PageDetail(dd.DetailLayout):
131
131
  right_panel = """
132
132
  ref language
133
133
  parent seqno
134
- child_node_depth
134
+ child_node_depth main_image
135
135
  #page_type filler
136
136
  publishing_state special_page
137
137
  publisher.TranslationsByPage
@@ -147,7 +147,7 @@ class Pages(dd.Table):
147
147
  ref
148
148
  #page_type filler
149
149
  """
150
- default_display_modes = {None: constants.DISPLAY_MODE_STORY}
150
+ default_display_modes = {None: constants.DISPLAY_MODE_LIST}
151
151
 
152
152
 
153
153
  class PagesByParent(Pages):
lino/static/bootstrap.css CHANGED
@@ -1,7 +1,7 @@
1
1
  /* styles to be applied after bootstrap css */
2
2
 
3
3
  .publisher-document {
4
- width: 840px;
4
+ max-width: 840px;
5
5
  }
6
6
 
7
7
  .publisher-toc {
lino/utils/__init__.py CHANGED
@@ -63,7 +63,6 @@ import traceback
63
63
  from dateutil.relativedelta import relativedelta
64
64
  import re
65
65
  from decimal import Decimal
66
- from collections import OrderedDict
67
66
  from urllib.parse import urlencode
68
67
 
69
68
  # import locale
@@ -76,6 +75,7 @@ from etgen.utils import join_elems
76
75
 
77
76
  from lino.utils.cycler import Cycler
78
77
  from lino.utils.code import codefiles, codetime
78
+ from .sums import SumCollector
79
79
 
80
80
  from rstgen.utils import confirm, i2d, i2t
81
81
 
@@ -609,61 +609,6 @@ def puts(s):
609
609
  print(s)
610
610
 
611
611
 
612
- class SumCollector(object):
613
- """A dictionary of sums to be collected using an arbitrary key.
614
-
615
- This is also included in the default context used by the Jinja
616
- renderer (:mod:`lino.modlib.jinja`) when rendering templates,
617
- which makes it a more complete solution for a problem asked also
618
- elsewhere, e.g. on `Stackoverflow
619
- <https://stackoverflow.com/questions/7537439/how-to-increment-a-variable-on-a-for-loop-in-jinja-template>`__.
620
-
621
- See examples in :doc:`/topics/utils`.
622
-
623
- """
624
-
625
- def __init__(self):
626
- self._sums = OrderedDict()
627
-
628
- def collect(self, k, value):
629
- """Add the given value to the sum at the given key k."""
630
- if value is None:
631
- return
632
- if k in self._sums:
633
- # print("20230614 a", k, "=", self._sums[k], "+", value)
634
- self._sums[k] += value
635
- else:
636
- # print("20230614 b", k, "=", value)
637
- self._sums[k] = value
638
-
639
- def __getattr__(self, k):
640
- return self._sums.get(k)
641
-
642
- def __getitem__(self, k):
643
- return self._sums.get(k)
644
-
645
- def total(self):
646
- return sum(self._sums.values())
647
-
648
- def items(self, *args, **kwargs):
649
- return self._sums.items(*args, **kwargs)
650
-
651
- def keys(self, *args, **kwargs):
652
- return self._sums.keys(*args, **kwargs)
653
-
654
- def values(self, *args, **kwargs):
655
- return self._sums.values(*args, **kwargs)
656
-
657
- def __len__(self):
658
- return self._sums.__len__()
659
-
660
- def __str__(self):
661
- return str(self._sums)
662
-
663
- def __repr__(self):
664
- return repr(self._sums)
665
-
666
-
667
612
  class SimpleSingleton(object):
668
613
  _instance = None
669
614
 
lino/utils/dbfreader.py CHANGED
@@ -9,9 +9,10 @@ DBF and DBT files when both settings :attr:`use_dbfread
9
9
  <lino_xl.lib.tim2lino.Plugin.use_dbf_py>` are `False` (which is default).
10
10
 
11
11
  Based on original work by Lars Garshol
12
- http://www.garshol.priv.no/download/software/python/dbfreader.py
12
+ https://www.garshol.priv.no/download/software/python/dbfreader.py
13
13
 
14
- Modified by Luc Saffre to add support for Clipper dialect.
14
+ Modified by Luc Saffre to add support for Clipper dialect. And to work under
15
+ Python 3. And to support char fields longer than 255 characters.
15
16
 
16
17
  Sources of information:
17
18
 
@@ -20,6 +21,7 @@ Sources of information:
20
21
 
21
22
  `Xbase & dBASE File Format Description by Erik Bachmann
22
23
  <https://www.clicketyclick.dk/databases/xbase/format/>`__
24
+
23
25
  """
24
26
 
25
27
  import datetime
@@ -37,16 +39,11 @@ codepages = {
37
39
 
38
40
 
39
41
  def unpack_long(number):
40
- # print(f"20250123 unpack_long {number}")
41
- return number[0] + 256 * (
42
- number[1] + 256 * (number[2] + 256 * number[3])
43
- )
42
+ return number[0] + 256 * (number[1] + 256 * (number[2] + 256 * number[3]))
44
43
 
45
44
 
46
45
  def unpack_long_rev(number):
47
- return number[3] + 256 * (
48
- number[2] + 256 * (number[1] + 256 * number[0])
49
- )
46
+ return number[3] + 256 * (number[2] + 256 * (number[1] + 256 * number[0]))
50
47
 
51
48
 
52
49
  def unpack_int(number):
@@ -64,6 +61,7 @@ def hex_analyze(number):
64
61
 
65
62
  # --- A class for the entire file
66
63
 
64
+
67
65
  class DBFFile(object):
68
66
  "Represents a single DBF file."
69
67
 
@@ -88,7 +86,10 @@ class DBFFile(object):
88
86
  day = header[3]
89
87
  self.lastUpdate = datetime.date(year, month, day)
90
88
  # print(f"20250123 Loading {filename} (last updated {self.lastUpdate})")
89
+
90
+ # Number of records in data file:
91
91
  self.rec_num = unpack_long(header[4:8])
92
+ # length of header structure. Stored as binary (little endian), unsigned:
92
93
  self.first_rec = unpack_int(header[8:10])
93
94
  self.rec_len = unpack_int(header[10:12])
94
95
  self.codepage = codepage # s[header[29]]
@@ -99,13 +100,21 @@ class DBFFile(object):
99
100
  while 1:
100
101
  ch = infile.read(1)
101
102
  # if ch == 0x0D:
102
- if ch == b'\r':
103
+ # if ch == b'\r':
104
+ if ch == b'\x0D':
103
105
  break
104
106
  field = DBFField(ch + infile.read(31), self)
105
107
  self.fields[field.name] = field
106
108
  self.field_list.append(field)
107
109
  # if len(self.field_list) > 20:
108
110
  # sys.exit(1)
111
+ # print(f"20250423 {self.field_list}")
112
+ # n = 0
113
+ # for fld in self.field_list:
114
+ # print(f"20250423 {fld.name} {fld.field_type} {
115
+ # fld.field_len} {fld.field_places}")
116
+ # n += fld.get_len()
117
+ # print(f"20250423 {n}")
109
118
 
110
119
  infile.close()
111
120
  if self.has_memo():
@@ -155,8 +164,11 @@ class DBFFile(object):
155
164
  def get_next_record(self):
156
165
  values = {}
157
166
  ch = self.infile.read(1)
167
+ if len(ch) == 0:
168
+ raise Exception("Unexpected end of file")
158
169
  self.recno += 1
159
- if ch == 0x1A or len(ch) == 0:
170
+ # if ch == 0x1A or len(ch) == 0:
171
+ if ch == b'\x1A':
160
172
  return None
161
173
  if ch == b"*":
162
174
  deleted = True
@@ -170,6 +182,7 @@ class DBFFile(object):
170
182
  values[field.get_name()] = field.interpret(data)
171
183
  if deleted and not self.deleted:
172
184
  return self.get_next_record()
185
+ # print(f"20250423 found {values}")
173
186
  return DBFRecord(self, values, deleted)
174
187
 
175
188
  def close(self):
@@ -255,11 +268,25 @@ class DBFField(object):
255
268
  self.name = buf[:pos].decode()
256
269
  # print(f"20250123 field {self.name}")
257
270
  self.field_type = chr(buf[11])
258
- self.field_pos = unpack_long(buf[12:16])
271
+ # self.field_pos = unpack_long(buf[12:16])
272
+ # self.field_pos_raw = buf[12:16]
259
273
  self.field_len = buf[16]
260
- self.field_places = buf[17]
274
+ if self.field_type == "C":
275
+ self.field_places = 0
276
+ self.field_len += 256 * buf[17]
277
+ # https://www.clicketyclick.dk/databases/xbase/format/data_types.html
278
+ # Character fields can be up to 32 KB long (in Clipper and FoxPro)
279
+ # using decimal count (field_places) as high byte in field length.
280
+ # It's possible to use up to 64KB long fields by reading length as
281
+ # unsigned.
282
+ else:
283
+ self.field_places = buf[17]
261
284
  self.dbf = dbf
262
285
 
286
+ def __repr__(self):
287
+ return (f"DBFField({self.name}, {self.field_type}, "
288
+ f"{self.field_len}, {self.field_places})")
289
+
263
290
  # if self.field_type=="M" or self.field_type=="P" or \
264
291
  # self.field_type=="G" :
265
292
  # self.blockfile=blockfile
@@ -267,8 +294,8 @@ class DBFField(object):
267
294
  def get_name(self):
268
295
  return self.name
269
296
 
270
- def get_pos(self):
271
- return self.field_pos
297
+ # def get_pos(self):
298
+ # return self.field_pos
272
299
 
273
300
  def get_type(self):
274
301
  return self.field_type
@@ -283,7 +310,7 @@ class DBFField(object):
283
310
  # raise Exception("20250123 good")
284
311
  # print(f"20250123 interpret {repr(self.field_type)}")
285
312
  if self.field_type == "C":
286
- if not self.dbf.codepage is None:
313
+ if self.dbf.codepage is not None:
287
314
  data = data.decode(self.dbf.codepage)
288
315
  data = data.strip()
289
316
  # ~ if len(data) == 0: return None
@@ -301,6 +328,8 @@ class DBFField(object):
301
328
  # ~ return None
302
329
  return ""
303
330
  raise Exception("bad memo block number %s" % repr(data))
331
+ # print("20250422 bad memo block number %s" % repr(data))
332
+ # return ""
304
333
  return self.dbf.blockfile.get_block(num)
305
334
 
306
335
  elif self.field_type == "N":
@@ -316,7 +345,10 @@ class DBFField(object):
316
345
  try:
317
346
  return dateparser.parse(data)
318
347
  except ValueError as e:
319
- raise ValueError("Invalid date value %r (%s)" % (data, e))
348
+ raise ValueError("Invalid date value %r (%s) in field %s" %
349
+ (data, e, self.name))
350
+ # print("20250422 Invalid date value %r (%s) in field %s" %
351
+ # (data, e, self.name))
320
352
  # ~ return data # string "YYYYMMDD", use the time module or mxDateTime
321
353
  else:
322
354
  raise NotImplementedError("Unknown data type " + self.field_type)
@@ -401,21 +433,21 @@ class DBTFile(object):
401
433
  # primary key
402
434
 
403
435
 
404
- class DBFHash(object):
405
- def __init__(self, file, key):
406
- self.file = DBFFile(file)
407
- self.hash = {}
408
- self.key = key
409
-
410
- self.file.open()
411
- while 1:
412
- rec = self.file.get_next_record()
413
- if rec is None:
414
- break
415
- self.hash[rec[self.key]] = rec
416
-
417
- def __getitem__(self, key):
418
- return self.hash[key]
436
+ # class DBFHash(object):
437
+ # def __init__(self, file, key):
438
+ # self.file = DBFFile(file)
439
+ # self.hash = {}
440
+ # self.key = key
441
+ #
442
+ # self.file.open()
443
+ # while 1:
444
+ # rec = self.file.get_next_record()
445
+ # if rec is None:
446
+ # break
447
+ # self.hash[rec[self.key]] = rec
448
+ #
449
+ # def __getitem__(self, key):
450
+ # return self.hash[key]
419
451
 
420
452
 
421
453
  # --- Utility functions
lino/utils/dbhash.py CHANGED
@@ -14,8 +14,9 @@ from django.conf import settings
14
14
  from django.apps import apps
15
15
  from django.db.models.deletion import ProtectedError
16
16
 
17
- mod = import_module(settings.SETTINGS_MODULE)
18
- HASH_FILE = Path(mod.__file__).parent / "dbhash.json"
17
+ # mod = import_module(settings.SETTINGS_MODULE)
18
+ # HASH_FILE = Path(mod.__file__).parent / "dbhash.json"
19
+ HASH_FILE = settings.SITE.site_dir / "dbhash.json"
19
20
 
20
21
 
21
22
  def fmn(m):
lino/utils/sums.py ADDED
@@ -0,0 +1,75 @@
1
+ # -*- coding: UTF-8 -*-
2
+ # Copyright 2012-2025 Rumma & Ko Ltd
3
+ # License: GNU Affero General Public License v3 (see file COPYING for details)
4
+ # Documentation: https://dev.lino-framework.org/plugins/accounting.html
5
+
6
+ from collections import OrderedDict
7
+ from decimal import Decimal, ROUND_HALF_UP
8
+
9
+ ZERO = Decimal(0)
10
+ CENT = Decimal('.01')
11
+ HUNDRED = Decimal('100.00')
12
+ ONE = Decimal('1.00')
13
+ MAX_AMOUNT = Decimal("9999999.00")
14
+
15
+
16
+ def myround(d):
17
+ return d.quantize(CENT, rounding=ROUND_HALF_UP)
18
+
19
+
20
+ class SumCollector:
21
+ """A dictionary of sums to be collected using an arbitrary key.
22
+
23
+ This is also included in the default context used by the Jinja
24
+ renderer (:mod:`lino.modlib.jinja`) when rendering templates,
25
+ which makes it a more complete solution for a problem asked also
26
+ elsewhere, e.g. on `Stackoverflow
27
+ <https://stackoverflow.com/questions/7537439/how-to-increment-a-variable-on-a-for-loop-in-jinja-template>`__.
28
+
29
+ See examples in :doc:`/topics/utils`.
30
+
31
+ """
32
+
33
+ def __init__(self):
34
+ self._sums = OrderedDict()
35
+
36
+ def collect(self, k, value):
37
+ """Add the given value to the sum at the given key k."""
38
+ if value is None:
39
+ return
40
+ if k in self._sums:
41
+ # print("20230614 a", k, "=", self._sums[k], "+", value)
42
+ self._sums[k] += value
43
+ else:
44
+ # print("20230614 b", k, "=", value)
45
+ self._sums[k] = value
46
+
47
+ def myround(self):
48
+ self._sums = {k: myround(v) for k, v in self._sums.items()}
49
+
50
+ def __getattr__(self, k):
51
+ return self._sums.get(k)
52
+
53
+ def __getitem__(self, k):
54
+ return self._sums.get(k)
55
+
56
+ def total(self):
57
+ return sum(self._sums.values())
58
+
59
+ def items(self, *args, **kwargs):
60
+ return self._sums.items(*args, **kwargs)
61
+
62
+ def keys(self, *args, **kwargs):
63
+ return self._sums.keys(*args, **kwargs)
64
+
65
+ def values(self, *args, **kwargs):
66
+ return self._sums.values(*args, **kwargs)
67
+
68
+ def __len__(self):
69
+ return self._sums.__len__()
70
+
71
+ def __str__(self):
72
+ return str(self._sums)
73
+
74
+ def __repr__(self):
75
+ return repr(self._sums)