lino 25.6.0__py3-none-any.whl → 25.7.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.
- lino/__init__.py +1 -1
- lino/api/doctest.py +21 -0
- lino/core/actions.py +59 -25
- lino/core/actors.py +38 -16
- lino/core/boundaction.py +16 -0
- lino/core/choicelists.py +7 -7
- lino/core/constants.py +3 -0
- lino/core/dashboard.py +1 -0
- lino/core/dbtables.py +1 -1
- lino/core/elems.py +38 -13
- lino/core/fields.py +20 -11
- lino/core/kernel.py +8 -0
- lino/core/layouts.py +6 -2
- lino/core/menus.py +3 -6
- lino/core/model.py +5 -4
- lino/core/renderer.py +14 -5
- lino/core/requests.py +8 -7
- lino/core/signals.py +1 -0
- lino/core/site.py +48 -28
- lino/core/store.py +4 -2
- lino/core/tables.py +23 -10
- lino/core/utils.py +4 -1
- lino/core/workflows.py +2 -1
- lino/help_texts.py +1 -2
- lino/management/commands/prep.py +2 -2
- lino/management/commands/show.py +8 -10
- lino/mixins/__init__.py +14 -13
- lino/mixins/periods.py +2 -0
- lino/mixins/sequenced.py +1 -1
- lino/modlib/about/models.py +4 -3
- lino/modlib/checkdata/__init__.py +42 -36
- lino/modlib/checkdata/choicelists.py +9 -1
- lino/modlib/checkdata/fixtures/checkdata.py +4 -2
- lino/modlib/checkdata/models.py +9 -2
- lino/modlib/comments/models.py +4 -3
- lino/modlib/extjs/ext_renderer.py +4 -4
- lino/modlib/extjs/views.py +8 -2
- lino/modlib/gfks/fields.py +1 -1
- lino/modlib/help/__init__.py +3 -3
- lino/modlib/help/config/makehelp/conf.tpl.py +2 -2
- lino/modlib/help/fixtures/demo2.py +6 -1
- lino/modlib/help/management/commands/makehelp.py +4 -1
- lino/modlib/help/models.py +2 -1
- lino/modlib/help/utils.py +12 -6
- lino/modlib/linod/choicelists.py +57 -4
- lino/modlib/linod/fixtures/{linod.py → checkdata.py} +3 -13
- lino/modlib/linod/management/commands/linod.py +0 -13
- lino/modlib/linod/mixins.py +8 -0
- lino/modlib/linod/models.py +29 -30
- lino/modlib/memo/__init__.py +7 -7
- lino/modlib/memo/management/__init__,py +0 -0
- lino/modlib/memo/management/commands/__init__.py +0 -0
- lino/modlib/memo/management/commands/removeurls.py +67 -0
- lino/modlib/memo/mixins.py +1 -9
- lino/modlib/memo/parser.py +1 -1
- lino/modlib/notify/config/notify/summary.eml +5 -2
- lino/modlib/notify/fixtures/demo2.py +5 -6
- lino/modlib/notify/models.py +9 -10
- lino/modlib/periods/__init__.py +11 -8
- lino/modlib/periods/choicelists.py +16 -10
- lino/modlib/periods/models.py +45 -45
- lino/modlib/summaries/fixtures/checksummaries.py +4 -2
- lino/modlib/system/models.py +17 -18
- lino/modlib/uploads/fixtures/demo.py +9 -3
- lino/modlib/uploads/mixins.py +5 -2
- lino/modlib/uploads/models.py +15 -9
- lino/modlib/uploads/utils.py +4 -1
- lino/modlib/users/__init__.py +59 -18
- lino/modlib/users/actions.py +24 -20
- lino/modlib/users/fixtures/demo_users.py +2 -35
- lino/modlib/users/mixins.py +3 -4
- lino/modlib/users/models.py +53 -13
- lino/modlib/users/ui.py +30 -16
- lino/modlib/users/utils.py +5 -6
- lino/projects/std/settings.py +1 -1
- lino/sphinxcontrib/logo/templates/footer.html +1 -0
- lino/utils/ajax.py +1 -1
- lino/utils/cycler.py +5 -0
- lino/utils/dbhash.py +4 -9
- lino/utils/dpy.py +2 -2
- lino/utils/format_date.py +4 -3
- lino/utils/html.py +13 -5
- lino/utils/jsgen.py +1 -1
- lino/utils/quantities.py +8 -0
- lino/utils/soup.py +75 -94
- {lino-25.6.0.dist-info → lino-25.7.0.dist-info}/METADATA +1 -1
- {lino-25.6.0.dist-info → lino-25.7.0.dist-info}/RECORD +90 -87
- {lino-25.6.0.dist-info → lino-25.7.0.dist-info}/WHEEL +0 -0
- {lino-25.6.0.dist-info → lino-25.7.0.dist-info}/licenses/AUTHORS.rst +0 -0
- {lino-25.6.0.dist-info → lino-25.7.0.dist-info}/licenses/COPYING +0 -0
lino/modlib/users/models.py
CHANGED
@@ -11,7 +11,7 @@ from django.conf import settings
|
|
11
11
|
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
|
12
12
|
from django.utils import timezone
|
13
13
|
|
14
|
-
from lino.api import dd, rt, _
|
14
|
+
from lino.api import dd, rt, _, gettext
|
15
15
|
from lino.core import userprefs
|
16
16
|
from lino.core.roles import Supervisor
|
17
17
|
from lino.core.roles import SiteAdmin
|
@@ -21,13 +21,13 @@ from lino.mixins import DateRange
|
|
21
21
|
from lino.modlib.about.choicelists import TimeZones, DateFormats
|
22
22
|
from lino.modlib.publisher.mixins import Publishable
|
23
23
|
from lino.modlib.about.models import About
|
24
|
-
from lino.utils.html import
|
24
|
+
from lino.utils.html import tostring, format_html, mark_safe
|
25
25
|
|
26
26
|
from .choicelists import UserTypes
|
27
27
|
from .mixins import UserAuthored # , TimezoneHolder
|
28
28
|
from .actions import ChangePassword, SignOut, CheckedSubmitInsert
|
29
29
|
from .actions import SendWelcomeMail, SignIn, ConnectAccount
|
30
|
-
from .actions import
|
30
|
+
from .actions import CreateAccount, ResetPassword, VerifyUser, VerifyMe
|
31
31
|
|
32
32
|
from .ui import *
|
33
33
|
|
@@ -90,9 +90,8 @@ class User(AbstractBaseUser, Contactable, CreatedModified, Publishable, DateRang
|
|
90
90
|
quick_search_fields = "username user_type first_name last_name remarks"
|
91
91
|
allow_merge_action = True
|
92
92
|
|
93
|
-
|
94
|
-
|
95
|
-
username = models.CharField(_("Username"), max_length=30, unique=True)
|
93
|
+
username = models.CharField(
|
94
|
+
_("Username"), max_length=30, unique=True, null=True, blank=True)
|
96
95
|
user_type = UserTypes.field(blank=True)
|
97
96
|
initials = models.CharField(_("Initials"), max_length=10, blank=True)
|
98
97
|
if dd.plugins.users.with_nickname:
|
@@ -109,7 +108,6 @@ class User(AbstractBaseUser, Contactable, CreatedModified, Publishable, DateRang
|
|
109
108
|
verification_password = models.CharField(max_length=200, blank=True)
|
110
109
|
verification_code = models.CharField(max_length=200, blank=True, default="!")
|
111
110
|
verification_code_sent_on = models.DateTimeField(null=True, blank=True)
|
112
|
-
verify_me = VerifyMe()
|
113
111
|
|
114
112
|
if settings.USE_TZ:
|
115
113
|
time_zone = TimeZones.field(default="default")
|
@@ -129,23 +127,33 @@ class User(AbstractBaseUser, Contactable, CreatedModified, Publishable, DateRang
|
|
129
127
|
)
|
130
128
|
|
131
129
|
submit_insert = CheckedSubmitInsert()
|
130
|
+
|
131
|
+
verify_me = VerifyMe()
|
132
132
|
send_welcome_email = SendWelcomeMail()
|
133
133
|
change_password = ChangePassword()
|
134
134
|
# sign_in = SignIn()
|
135
135
|
sign_out = SignOut()
|
136
136
|
|
137
|
-
if
|
138
|
-
|
137
|
+
if dd.get_plugin_setting("users", "third_party_authentication"):
|
138
|
+
connect_account = ConnectAccount()
|
139
139
|
|
140
|
-
|
140
|
+
# if settings.SITE.default_ui == "lino_react.react":
|
141
|
+
# my_settings = MySettings()
|
141
142
|
|
142
143
|
def __str__(self):
|
143
144
|
return self.nickname or self.get_full_name()
|
144
145
|
|
146
|
+
@classmethod
|
147
|
+
def get_simple_parameters(cls):
|
148
|
+
for p in super().get_simple_parameters():
|
149
|
+
yield p
|
150
|
+
yield "user_type"
|
151
|
+
|
145
152
|
@property
|
146
153
|
def is_active(self):
|
147
154
|
# if not self.has_usable_password():
|
148
|
-
|
155
|
+
if not self.user_type or not self.username:
|
156
|
+
return False
|
149
157
|
if self.start_date and self.start_date > dd.today():
|
150
158
|
return False
|
151
159
|
if self.end_date and self.end_date < dd.today():
|
@@ -401,6 +409,17 @@ class User(AbstractBaseUser, Contactable, CreatedModified, Publishable, DateRang
|
|
401
409
|
# def get_default_table(cls):
|
402
410
|
# return rt.models.users.MySettings
|
403
411
|
|
412
|
+
def dt_astimezone(self, dt):
|
413
|
+
"""
|
414
|
+
Convert datetime to user timezone if time_zone is not None otherwise to default timezone.
|
415
|
+
Used in template notify/summary.eml
|
416
|
+
"""
|
417
|
+
default_tz = rt.models.about.TimeZones.default
|
418
|
+
aware_dt = dt.astimezone(default_tz.tzinfo)
|
419
|
+
if self.time_zone:
|
420
|
+
aware_dt = aware_dt.astimezone(self.time_zone.tzinfo)
|
421
|
+
return aware_dt
|
422
|
+
|
404
423
|
|
405
424
|
settings.AUTH_USER_MODEL = "users.User"
|
406
425
|
|
@@ -474,5 +493,26 @@ def setup_memo_commands(sender=None, **kwargs):
|
|
474
493
|
)
|
475
494
|
|
476
495
|
|
477
|
-
|
478
|
-
|
496
|
+
def welcome_messages(ar):
|
497
|
+
me = ar.get_user()
|
498
|
+
if not me.is_verified():
|
499
|
+
# sar = rt.models.users.Me.create_request(parent=ar)
|
500
|
+
sar = ar
|
501
|
+
if me.email:
|
502
|
+
# verify_me =
|
503
|
+
msg = format_html(
|
504
|
+
_("Your email address ({email}) is not verified, "
|
505
|
+
"please check your mailbox and {verify} or {resend}."),
|
506
|
+
email=me.email,
|
507
|
+
verify=tostring(sar.instance_action_button(
|
508
|
+
me.verify_me, _("verify now"))),
|
509
|
+
resend=tostring(sar.instance_action_button(
|
510
|
+
me.send_welcome_email, _("re-send our welcome email"))))
|
511
|
+
else:
|
512
|
+
msg = format_html(
|
513
|
+
_("You have no email address, please {edit}."),
|
514
|
+
edit=tostring(sar.obj2html(me, _("edit your user settings"))))
|
515
|
+
yield mark_safe(msg)
|
516
|
+
|
517
|
+
|
518
|
+
dd.add_welcome_handler(welcome_messages)
|
lino/modlib/users/ui.py
CHANGED
@@ -10,20 +10,21 @@ from importlib import import_module
|
|
10
10
|
from lino.utils.html import E, tostring
|
11
11
|
|
12
12
|
from django.contrib.humanize.templatetags.humanize import naturaltime
|
13
|
-
from django.utils import timezone
|
13
|
+
# from django.utils import timezone
|
14
14
|
from django.utils.html import mark_safe
|
15
15
|
from django.conf import settings
|
16
16
|
from django.db import models
|
17
|
+
from django.db.models import Q
|
17
18
|
|
18
19
|
from lino.api import dd, rt, _
|
19
20
|
from lino.core import actions
|
20
|
-
from lino.core import constants
|
21
21
|
from lino.core.roles import SiteAdmin, SiteUser, UserRole
|
22
22
|
from lino.core.utils import djangoname
|
23
23
|
from lino.core.auth import SESSION_KEY
|
24
24
|
|
25
25
|
from .choicelists import UserTypes
|
26
|
-
from .actions import SendWelcomeMail
|
26
|
+
# from .actions import SendWelcomeMail
|
27
|
+
# from .actions import SendWelcomeMail, SignInWithSocialAuth
|
27
28
|
|
28
29
|
|
29
30
|
def mywrap(t, ls=80):
|
@@ -31,6 +32,16 @@ def mywrap(t, ls=80):
|
|
31
32
|
return "\n".join(wrap(t, ls))
|
32
33
|
|
33
34
|
|
35
|
+
def format_timestamp(dt):
|
36
|
+
if dt is None:
|
37
|
+
return ""
|
38
|
+
return "{} {} ({})".format(
|
39
|
+
dt.strftime(settings.SITE.date_format_strftime),
|
40
|
+
dt.strftime(settings.SITE.time_format_strftime),
|
41
|
+
naturaltime(dt),
|
42
|
+
)
|
43
|
+
|
44
|
+
|
34
45
|
class UserDetail(dd.DetailLayout):
|
35
46
|
box1 = """
|
36
47
|
username user_type:20
|
@@ -83,9 +94,11 @@ class Users(dd.Table):
|
|
83
94
|
abstract = True
|
84
95
|
required_roles = dd.login_required(SiteAdmin)
|
85
96
|
|
86
|
-
parameters = dict(
|
97
|
+
parameters = dict(show_active=dd.YesNo.field(_("Active"), blank=True))
|
98
|
+
|
99
|
+
params_layout = "user_type show_active start_date end_date"
|
87
100
|
|
88
|
-
simple_parameters = ["user_type"]
|
101
|
+
# simple_parameters = ["user_type"]
|
89
102
|
|
90
103
|
# ~ column_names = 'username first_name last_name is_active is_staff is_expert is_superuser *'
|
91
104
|
column_names = "username user_type first_name last_name *"
|
@@ -112,7 +125,18 @@ class Users(dd.Table):
|
|
112
125
|
|
113
126
|
|
114
127
|
class AllUsers(Users):
|
115
|
-
send_welcome_email = SendWelcomeMail()
|
128
|
+
# send_welcome_email = SendWelcomeMail()
|
129
|
+
|
130
|
+
@classmethod
|
131
|
+
def get_request_queryset(cls, ar, **filter):
|
132
|
+
qs = super().get_request_queryset(ar, **filter)
|
133
|
+
if (pv := ar.param_values) is None:
|
134
|
+
return qs
|
135
|
+
if pv.show_active == dd.YesNo.no:
|
136
|
+
qs = qs.filter(Q(username__isnull=True) | Q(user_type=''))
|
137
|
+
elif pv.show_active == dd.YesNo.yes:
|
138
|
+
qs = qs.exclude(Q(username__isnull=True) | Q(user_type=''))
|
139
|
+
return qs
|
116
140
|
|
117
141
|
|
118
142
|
class UsersOverview(Users):
|
@@ -282,16 +306,6 @@ class UserRoles(dd.VirtualTable):
|
|
282
306
|
cls.column_names = "name:20 " + " ".join(names)
|
283
307
|
|
284
308
|
|
285
|
-
def format_timestamp(dt):
|
286
|
-
if dt is None:
|
287
|
-
return ""
|
288
|
-
return "{} {} ({})".format(
|
289
|
-
dt.strftime(settings.SITE.date_format_strftime),
|
290
|
-
dt.strftime(settings.SITE.time_format_strftime),
|
291
|
-
naturaltime(dt),
|
292
|
-
)
|
293
|
-
|
294
|
-
|
295
309
|
class KillSession(actions.Action):
|
296
310
|
label = _("Kill")
|
297
311
|
custom_handler = True
|
lino/modlib/users/utils.py
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
-
# Copyright 2011-
|
1
|
+
# Copyright 2011-2025 Rumma & Ko Ltd
|
2
2
|
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
3
3
|
|
4
4
|
import threading
|
5
|
+
from django.conf import settings
|
6
|
+
from lino.utils import camelize
|
5
7
|
|
6
8
|
user_profile_rlock = threading.RLock()
|
7
9
|
_for_user_profile = None
|
@@ -50,9 +52,6 @@ class UserTypeContext(object):
|
|
50
52
|
|
51
53
|
# set_for_user_profile = set_user_profile
|
52
54
|
|
53
|
-
from lino.utils import camelize
|
54
|
-
from lino.api import rt
|
55
|
-
|
56
55
|
|
57
56
|
def create_user(username, user_type=None, with_person=False, **kw):
|
58
57
|
# with_person was used in noi1e demo
|
@@ -66,7 +65,7 @@ def create_user(username, user_type=None, with_person=False, **kw):
|
|
66
65
|
kw.update(username=username, user_type=user_type)
|
67
66
|
kw.update(first_name=first_name)
|
68
67
|
# kw.update(partner=person)
|
69
|
-
return
|
68
|
+
return settings.SITE.models.users.User(**kw)
|
70
69
|
else:
|
71
70
|
# return dd.plugins.skills.supplier_model(first_name=first_name)
|
72
|
-
return
|
71
|
+
return settings.SITE.models.contacts.Person(first_name=first_name)
|
lino/projects/std/settings.py
CHANGED
@@ -60,7 +60,7 @@ EMAIL_HOST = "mail.example.com"
|
|
60
60
|
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}}
|
61
61
|
|
62
62
|
# Django wants system admins to define their own `SECRET_KEY
|
63
|
-
# <https://docs.djangoproject.com/en/5.
|
63
|
+
# <https://docs.djangoproject.com/en/5.2/ref/settings/#secret-key>`__
|
64
64
|
# setting. Hint: as long as you're on a development server you just
|
65
65
|
# put some non-empty string and that's okay.
|
66
66
|
|
lino/utils/ajax.py
CHANGED
@@ -56,7 +56,7 @@ class AjaxExceptionResponse(MiddlewareMixin):
|
|
56
56
|
# at all.
|
57
57
|
|
58
58
|
def process_exception(self, request, exception):
|
59
|
-
# if request.is_ajax(): # See https://docs.djangoproject.com/en/5.
|
59
|
+
# if request.is_ajax(): # See https://docs.djangoproject.com/en/5.2/releases/3.1/
|
60
60
|
if request.headers.get("x-requested-with") == "XMLHttpRequest":
|
61
61
|
(exc_type, exc_info, tb) = sys.exc_info()
|
62
62
|
# response to client:
|
lino/utils/cycler.py
CHANGED
@@ -37,6 +37,7 @@ class Cycler(object):
|
|
37
37
|
else:
|
38
38
|
self.items = args
|
39
39
|
self.current = 0
|
40
|
+
self.loop_no = 1
|
40
41
|
|
41
42
|
def pop(self):
|
42
43
|
if len(self.items) == 0:
|
@@ -45,6 +46,7 @@ class Cycler(object):
|
|
45
46
|
self.current += 1
|
46
47
|
if self.current >= len(self.items):
|
47
48
|
self.current = 0
|
49
|
+
self.loop_no += 1
|
48
50
|
if isinstance(item, Cycler):
|
49
51
|
return item.pop()
|
50
52
|
return item
|
@@ -52,6 +54,9 @@ class Cycler(object):
|
|
52
54
|
def __len__(self):
|
53
55
|
return len(self.items)
|
54
56
|
|
57
|
+
def __repr__(self):
|
58
|
+
return f"Cycler({self.current} of {len(self.items)} in loop {self.loop_no})"
|
59
|
+
|
55
60
|
def reset(self):
|
56
61
|
self.current = 0
|
57
62
|
|
lino/utils/dbhash.py
CHANGED
@@ -8,14 +8,9 @@ See :doc:`/utils/dbhash`.
|
|
8
8
|
"""
|
9
9
|
|
10
10
|
import json
|
11
|
-
from importlib import import_module
|
12
|
-
from pathlib import Path
|
13
11
|
from django.conf import settings
|
14
12
|
from django.apps import apps
|
15
|
-
from django.db.models.deletion import ProtectedError
|
16
13
|
|
17
|
-
# mod = import_module(settings.SETTINGS_MODULE)
|
18
|
-
# HASH_FILE = Path(mod.__file__).parent / "dbhash.json"
|
19
14
|
HASH_FILE = settings.SITE.site_dir / "dbhash.json"
|
20
15
|
|
21
16
|
|
@@ -36,7 +31,7 @@ def compute_dbhash():
|
|
36
31
|
return rv
|
37
32
|
|
38
33
|
|
39
|
-
def mark_virgin():
|
34
|
+
def mark_virgin(**kwargs):
|
40
35
|
"""
|
41
36
|
Mark the database as virgin. This is called by :manage:`prep`.
|
42
37
|
"""
|
@@ -87,11 +82,11 @@ def check_virgin(restore=True, verbose=True):
|
|
87
82
|
can_restore = False
|
88
83
|
if verbose:
|
89
84
|
print(f"- {k}: {', '.join(diffs)}")
|
90
|
-
if len(must_delete) == 0 or not restore:
|
85
|
+
if can_restore and len(must_delete) == 0 or not restore:
|
91
86
|
return
|
92
87
|
if not can_restore:
|
93
|
-
|
94
|
-
|
88
|
+
print("Cannot restore database because some rows have been deleted")
|
89
|
+
return
|
95
90
|
must_delete = list(must_delete.items())
|
96
91
|
if verbose:
|
97
92
|
print(f"Tidy up {len(must_delete)} rows from database: {must_delete}.")
|
lino/utils/dpy.py
CHANGED
@@ -374,7 +374,7 @@ class DpyDeserializer(LoaderBase):
|
|
374
374
|
See e.g. :ticket:`1029`. We consider it an odd behaviour of
|
375
375
|
Django to search for fixtures also in the current directory (and
|
376
376
|
not, as `documented
|
377
|
-
<https://docs.djangoproject.com/en/5.
|
377
|
+
<https://docs.djangoproject.com/en/5.2/howto/initial-data/#where-django-finds-fixture-files>`__,
|
378
378
|
in the `fixtures` subdirs of plugins and the optional
|
379
379
|
:setting:`FIXTURE_DIRS`).
|
380
380
|
|
@@ -443,7 +443,7 @@ def Deserializer(fp, **options):
|
|
443
443
|
This is done by setting ``SERIALIZATION_MODULES={"py": "lino.utils.dpy"}``.
|
444
444
|
|
445
445
|
See `SERIALIZATION_MODULES
|
446
|
-
<https://docs.djangoproject.com/en/5.
|
446
|
+
<https://docs.djangoproject.com/en/5.2/ref/settings/#serialization-modules>`__.
|
447
447
|
|
448
448
|
"""
|
449
449
|
d = DpyDeserializer()
|
lino/utils/format_date.py
CHANGED
@@ -42,9 +42,10 @@ def fdmy(d):
|
|
42
42
|
|
43
43
|
|
44
44
|
def format_date(d, format="medium"):
|
45
|
-
"""Return the given date `d`
|
46
|
-
|
47
|
-
|
45
|
+
"""Return a str expressing the given date `d`
|
46
|
+
using
|
47
|
+
`Babel's date formatting <https://babel.pocoo.org/en/latest/dates.html>`_
|
48
|
+
and Django's current language.
|
48
49
|
|
49
50
|
"""
|
50
51
|
if not d:
|
lino/utils/html.py
CHANGED
@@ -16,6 +16,7 @@ from django.utils.html import SafeString, mark_safe, escape, format_html
|
|
16
16
|
|
17
17
|
SAFE_EMPTY = mark_safe("")
|
18
18
|
|
19
|
+
|
19
20
|
def html2text(html, **kwargs):
|
20
21
|
"""
|
21
22
|
Convert the given HTML-formatted text into equivalent Markdown-structured
|
@@ -25,6 +26,8 @@ def html2text(html, **kwargs):
|
|
25
26
|
|
26
27
|
text_maker = HTML2Text()
|
27
28
|
text_maker.unicode_snob = True
|
29
|
+
# text_maker.table_start = True
|
30
|
+
text_maker.pad_tables = True
|
28
31
|
for k, v in kwargs.items():
|
29
32
|
setattr(text_maker, k, v)
|
30
33
|
return text_maker.handle(html)
|
@@ -76,26 +79,31 @@ class Grouper:
|
|
76
79
|
|
77
80
|
def __init__(self, ar):
|
78
81
|
self.ar = ar
|
79
|
-
if ar.actor.group_by is None:
|
82
|
+
if ar.actor.group_by is None:
|
83
|
+
return
|
80
84
|
self.last_values = [None for f in ar.actor.group_by]
|
81
85
|
|
82
86
|
def begin(self):
|
83
|
-
if self.ar.actor.group_by is None:
|
87
|
+
if self.ar.actor.group_by is None:
|
88
|
+
return SAFE_EMPTY
|
84
89
|
return SAFE_EMPTY
|
85
90
|
|
86
91
|
def stop(self):
|
87
|
-
if self.ar.actor.group_by is None:
|
92
|
+
if self.ar.actor.group_by is None:
|
93
|
+
return SAFE_EMPTY
|
88
94
|
return SAFE_EMPTY
|
89
95
|
|
90
96
|
def before_row(self, obj):
|
91
|
-
if self.ar.actor.group_by is None:
|
97
|
+
if self.ar.actor.group_by is None:
|
98
|
+
return SAFE_EMPTY
|
92
99
|
self.current_values = [f(obj) for f in self.ar.actor.group_by]
|
93
100
|
if self.current_values == self.last_values:
|
94
101
|
return SAFE_EMPTY
|
95
102
|
return self.ar.actor.before_group_change(self, obj)
|
96
103
|
|
97
104
|
def after_row(self, obj):
|
98
|
-
if self.ar.actor.group_by is None:
|
105
|
+
if self.ar.actor.group_by is None:
|
106
|
+
return SAFE_EMPTY
|
99
107
|
if self.current_values == self.last_values:
|
100
108
|
return SAFE_EMPTY
|
101
109
|
self.last_values = self.current_values
|
lino/utils/jsgen.py
CHANGED
@@ -494,7 +494,7 @@ def py2js(v, compact=True):
|
|
494
494
|
return repr(v)
|
495
495
|
# return json.encoder.encode_basestring(v)
|
496
496
|
# print repr(v)
|
497
|
-
# https://docs.djangoproject.com/en/5.
|
497
|
+
# https://docs.djangoproject.com/en/5.2/topics/serialization/
|
498
498
|
# if not isinstance(v, (str,unicode)):
|
499
499
|
# raise Exception("20120121 %r is of type %s" % (v,type(v)))
|
500
500
|
return json.dumps(v, sort_keys=True, indent=4, separators=(",", ": "))
|
lino/utils/quantities.py
CHANGED
@@ -5,6 +5,7 @@
|
|
5
5
|
|
6
6
|
import datetime
|
7
7
|
from decimal import Decimal, ROUND_HALF_UP, InvalidOperation
|
8
|
+
from lino.utils.sums import myround
|
8
9
|
|
9
10
|
DEC2HOUR = Decimal(1) / Decimal(60)
|
10
11
|
|
@@ -29,6 +30,13 @@ class Quantity(Decimal):
|
|
29
30
|
def __str__(self):
|
30
31
|
return self._text
|
31
32
|
|
33
|
+
def as_decimal(self, places=None):
|
34
|
+
rv = Decimal(self)
|
35
|
+
if places is not None:
|
36
|
+
q = Decimal(10) ** -places
|
37
|
+
rv = rv.quantize(q, rounding=ROUND_HALF_UP)
|
38
|
+
return rv
|
39
|
+
|
32
40
|
def limit_length(self, max_length, excl=Exception):
|
33
41
|
rv = self
|
34
42
|
while len(rv) > max_length:
|