lino 25.6.1__py3-none-any.whl → 25.7.1__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 (93) hide show
  1. lino/__init__.py +1 -1
  2. lino/api/dd.py +1 -0
  3. lino/api/doctest.py +21 -0
  4. lino/core/actions.py +80 -25
  5. lino/core/actors.py +54 -27
  6. lino/core/boundaction.py +16 -0
  7. lino/core/choicelists.py +7 -7
  8. lino/core/constants.py +3 -0
  9. lino/core/dashboard.py +4 -2
  10. lino/core/dbtables.py +2 -2
  11. lino/core/elems.py +38 -13
  12. lino/core/fields.py +20 -11
  13. lino/core/kernel.py +8 -0
  14. lino/core/layouts.py +6 -2
  15. lino/core/menus.py +3 -6
  16. lino/core/model.py +5 -4
  17. lino/core/renderer.py +20 -9
  18. lino/core/requests.py +8 -7
  19. lino/core/signals.py +1 -0
  20. lino/core/site.py +48 -28
  21. lino/core/store.py +4 -2
  22. lino/core/tables.py +23 -10
  23. lino/core/utils.py +4 -1
  24. lino/core/workflows.py +2 -1
  25. lino/help_texts.py +1 -2
  26. lino/management/commands/prep.py +2 -2
  27. lino/management/commands/show.py +8 -10
  28. lino/mixins/__init__.py +14 -13
  29. lino/mixins/periods.py +2 -0
  30. lino/mixins/sequenced.py +1 -1
  31. lino/modlib/about/models.py +4 -3
  32. lino/modlib/checkdata/__init__.py +42 -36
  33. lino/modlib/checkdata/choicelists.py +9 -1
  34. lino/modlib/checkdata/fixtures/checkdata.py +4 -2
  35. lino/modlib/checkdata/management/commands/checkdata.py +3 -3
  36. lino/modlib/checkdata/models.py +9 -2
  37. lino/modlib/comments/models.py +4 -3
  38. lino/modlib/extjs/ext_renderer.py +4 -4
  39. lino/modlib/extjs/views.py +8 -2
  40. lino/modlib/gfks/fields.py +1 -1
  41. lino/modlib/help/__init__.py +3 -3
  42. lino/modlib/help/config/makehelp/conf.tpl.py +2 -2
  43. lino/modlib/help/fixtures/demo2.py +6 -1
  44. lino/modlib/help/management/commands/makehelp.py +4 -1
  45. lino/modlib/help/models.py +4 -1
  46. lino/modlib/help/utils.py +12 -6
  47. lino/modlib/linod/choicelists.py +57 -4
  48. lino/modlib/linod/fixtures/{linod.py → checkdata.py} +3 -13
  49. lino/modlib/linod/management/commands/linod.py +0 -13
  50. lino/modlib/linod/mixins.py +8 -0
  51. lino/modlib/linod/models.py +29 -30
  52. lino/modlib/memo/__init__.py +7 -7
  53. lino/modlib/memo/management/__init__,py +0 -0
  54. lino/modlib/memo/management/commands/__init__.py +0 -0
  55. lino/modlib/memo/management/commands/removeurls.py +67 -0
  56. lino/modlib/memo/mixins.py +1 -9
  57. lino/modlib/memo/parser.py +1 -1
  58. lino/modlib/notify/config/notify/summary.eml +5 -2
  59. lino/modlib/notify/fixtures/demo2.py +5 -6
  60. lino/modlib/notify/models.py +9 -10
  61. lino/modlib/periods/__init__.py +11 -8
  62. lino/modlib/periods/choicelists.py +16 -10
  63. lino/modlib/periods/models.py +45 -45
  64. lino/modlib/publisher/renderer.py +2 -5
  65. lino/modlib/summaries/fixtures/checksummaries.py +4 -2
  66. lino/modlib/system/models.py +17 -18
  67. lino/modlib/uploads/fixtures/demo.py +9 -3
  68. lino/modlib/uploads/mixins.py +5 -2
  69. lino/modlib/uploads/models.py +15 -9
  70. lino/modlib/uploads/utils.py +4 -1
  71. lino/modlib/users/__init__.py +59 -18
  72. lino/modlib/users/actions.py +24 -20
  73. lino/modlib/users/fixtures/demo_users.py +2 -35
  74. lino/modlib/users/mixins.py +3 -4
  75. lino/modlib/users/models.py +53 -13
  76. lino/modlib/users/ui.py +30 -16
  77. lino/modlib/users/utils.py +5 -6
  78. lino/projects/std/settings.py +1 -1
  79. lino/sphinxcontrib/logo/templates/footer.html +1 -0
  80. lino/utils/ajax.py +1 -1
  81. lino/utils/cycler.py +5 -0
  82. lino/utils/dbhash.py +4 -9
  83. lino/utils/dpy.py +2 -2
  84. lino/utils/format_date.py +4 -3
  85. lino/utils/html.py +13 -5
  86. lino/utils/jsgen.py +3 -2
  87. lino/utils/quantities.py +8 -0
  88. lino/utils/soup.py +75 -106
  89. {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/METADATA +1 -1
  90. {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/RECORD +93 -90
  91. {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/WHEEL +0 -0
  92. {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/licenses/AUTHORS.rst +0 -0
  93. {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/licenses/COPYING +0 -0
@@ -237,8 +237,8 @@ class UserPlan(UserAuthored):
237
237
  abstract = True
238
238
 
239
239
  today = models.DateField(_("Today"), default=dd.today)
240
- # 20240621 today is no longer readonly because it doesn't get set
241
- # automatically
240
+ # 20240621 today is no longer readonly because the user may want to continue
241
+ # a plan they started yesterday
242
242
 
243
243
  update_plan = UpdatePlan()
244
244
  start_plan = StartPlan()
@@ -263,8 +263,7 @@ class UserPlan(UserAuthored):
263
263
  else:
264
264
  if num > 1:
265
265
  dd.logger.warning(
266
- "Got {} {} for {}".format(num, cls._meta.verbose_name_plural, user)
267
- )
266
+ f"Got {num} {cls._meta.verbose_name_plural} for {user}")
268
267
  qs.delete()
269
268
  plan = cls(user=user, **options)
270
269
  plan.full_clean()
@@ -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 E
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 SendWelcomeMail, CreateAccount, ResetPassword, VerifyUser, VerifyMe
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
- # seems that Django doesn't like nullable username
94
- # username = dd.NullCharField(_('Username'), max_length=30, unique=True)
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 settings.SITE.default_ui == "lino_react.react":
138
- from lino.modlib.users.actions import MySettings
137
+ if dd.get_plugin_setting("users", "third_party_authentication"):
138
+ connect_account = ConnectAccount()
139
139
 
140
- my_settings = MySettings()
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
- # return False
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
- if dd.get_plugin_setting("users", "third_party_authentication"):
478
- Me.connect_account = ConnectAccount()
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, SignInWithSocialAuth
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(user_type=UserTypes.field(blank=True))
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
@@ -1,7 +1,9 @@
1
- # Copyright 2011-2024 Rumma & Ko Ltd
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 rt.models.users.User(**kw)
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 rt.models.contacts.Person(first_name=first_name)
71
+ return settings.SITE.models.contacts.Person(first_name=first_name)
@@ -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.0/ref/settings/#secret-key>`__
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
 
@@ -1,5 +1,6 @@
1
1
  <footer role="contentinfo">
2
2
  {%- include 'copyright.html' %}
3
+ <a class="reference external" href="https://www.saffre-rumma.net/team/">Contact</a>.
3
4
  {%- include 'last-updated.html' %}
4
5
  {%- include 'sphinx.html' %}
5
6
  {%- include 'show-source.html' %}
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.0/releases/3.1/
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
- raise Exception(
94
- "Cannot restore database because some rows have been deleted")
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.0/howto/initial-data/#where-django-finds-fixture-files>`__,
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.0/ref/settings/#serialization-modules>`__.
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` formatted with `Babel's date formatting
46
- <https://babel.pocoo.org/en/latest/dates.html>`_ and
47
- using Django's current language.
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: return
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: return SAFE_EMPTY
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: return SAFE_EMPTY
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: return SAFE_EMPTY
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: return SAFE_EMPTY
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
@@ -430,7 +430,8 @@ def py2js(v, compact=True):
430
430
  # raise Exception("Please call the function yourself")
431
431
  return "\n".join([ln for ln in v()])
432
432
  if isinstance(v, MissingRow):
433
- raise Exception("Cannot render {}".format(v))
433
+ return json.dumps(repr(v))
434
+ # raise Exception("Cannot render {}".format(v))
434
435
  if isinstance(v, js_code):
435
436
  return str(v.s) # v.s might be a unicode
436
437
  if v is None:
@@ -494,7 +495,7 @@ def py2js(v, compact=True):
494
495
  return repr(v)
495
496
  # return json.encoder.encode_basestring(v)
496
497
  # print repr(v)
497
- # https://docs.djangoproject.com/en/5.0/topics/serialization/
498
+ # https://docs.djangoproject.com/en/5.2/topics/serialization/
498
499
  # if not isinstance(v, (str,unicode)):
499
500
  # raise Exception("20120121 %r is of type %s" % (v,type(v)))
500
501
  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: