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.
- lino/__init__.py +1 -1
- lino/core/fields.py +4 -2
- lino/core/kernel.py +4 -1
- lino/core/requests.py +1 -1
- lino/core/site.py +58 -68
- lino/core/utils.py +7 -2
- lino/help_texts.py +0 -2
- lino/mixins/registrable.py +5 -4
- lino/modlib/checkdata/models.py +9 -1
- lino/modlib/jinja/mixins.py +2 -0
- lino/modlib/linod/management/commands/linod.py +5 -20
- lino/modlib/linod/mixins.py +0 -39
- lino/modlib/linod/routing.py +49 -46
- lino/modlib/publisher/choicelists.py +41 -20
- lino/modlib/publisher/config/publisher/page.pub.html +30 -0
- lino/modlib/publisher/fixtures/demo2.py +12 -0
- lino/modlib/publisher/fixtures/std.py +2 -1
- lino/modlib/publisher/fixtures/synodalworld.py +30 -0
- lino/modlib/publisher/mixins.py +7 -0
- lino/modlib/publisher/models.py +28 -3
- lino/modlib/publisher/ui.py +2 -2
- lino/static/bootstrap.css +1 -1
- lino/utils/__init__.py +1 -56
- lino/utils/dbfreader.py +64 -32
- lino/utils/dbhash.py +3 -2
- lino/utils/sums.py +75 -0
- {lino-25.4.3.dist-info → lino-25.4.5.dist-info}/METADATA +1 -1
- {lino-25.4.3.dist-info → lino-25.4.5.dist-info}/RECORD +31 -28
- {lino-25.4.3.dist-info → lino-25.4.5.dist-info}/WHEEL +0 -0
- {lino-25.4.3.dist-info → lino-25.4.5.dist-info}/licenses/AUTHORS.rst +0 -0
- {lino-25.4.3.dist-info → lino-25.4.5.dist-info}/licenses/COPYING +0 -0
@@ -1,5 +1,5 @@
|
|
1
1
|
# -*- coding: UTF-8 -*-
|
2
|
-
# Copyright 2020-
|
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,
|
185
|
-
self.
|
186
|
-
|
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.
|
189
|
-
super().__init__(
|
190
|
-
if
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
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("
|
231
|
-
add("
|
232
|
-
add("
|
233
|
-
add("
|
234
|
-
add("
|
235
|
-
add("
|
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
|
+
|
lino/modlib/publisher/mixins.py
CHANGED
@@ -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
|
lino/modlib/publisher/models.py
CHANGED
@@ -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>{}</
|
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
|
|
lino/modlib/publisher/ui.py
CHANGED
@@ -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.
|
150
|
+
default_display_modes = {None: constants.DISPLAY_MODE_LIST}
|
151
151
|
|
152
152
|
|
153
153
|
class PagesByParent(Pages):
|
lino/static/bootstrap.css
CHANGED
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|
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)" %
|
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
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
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)
|