lino 25.4.0__py3-none-any.whl → 25.4.2__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/help_texts.py CHANGED
@@ -153,13 +153,13 @@ help_texts = {
153
153
  'lino.modlib.memo.Plugin' : _("""Base class for this plugin."""),
154
154
  'lino.modlib.memo.Plugin.parser' : _("""An instance of lino.modlib.memo.parser.Parser."""),
155
155
  'lino.modlib.memo.Plugin.front_end' : _("""The front end to use when writing previews."""),
156
+ 'lino.modlib.memo.parser.Suggester' : _("""Holds the configuration for the behaviour of a given “trigger”."""),
156
157
  'lino.modlib.memo.parser.Parser' : _("""The memo parser."""),
157
158
  'lino.modlib.memo.parser.Parser.add_suggester' : _("""Add a Suggester (see there for args and kwargs)."""),
158
- 'lino.modlib.memo.parser.Parser.get_referred_objects' : _("""Yield all database objects referred in the given text using a suggester."""),
159
- 'lino.modlib.memo.parser.Parser.parse' : _("""Parse the given string src, replacing memo commands by their result."""),
160
159
  'lino.modlib.memo.parser.Parser.register_command' : _("""Register a memo command identified by the given text cmd."""),
161
160
  'lino.modlib.memo.parser.Parser.register_django_model' : _("""Register the given string name as command for referring to database rows of the given Django database model model."""),
162
- 'lino.modlib.memo.parser.Suggester' : _("""Holds the configuration for the behaviour of a given “trigger”."""),
161
+ 'lino.modlib.memo.parser.Parser.get_referred_objects' : _("""Yield all database objects referred in the given text using a suggester."""),
162
+ 'lino.modlib.memo.parser.Parser.parse' : _("""Parse the given string src, replacing memo commands by their result."""),
163
163
  'lino.modlib.restful.Plugin' : _("""See /dev/plugins."""),
164
164
  'lino.modlib.smtpd.Plugin' : _("""See /dev/plugins."""),
165
165
  'lino.modlib.system.Plugin' : _("""See /dev/plugins."""),
@@ -253,6 +253,8 @@ help_texts = {
253
253
  'lino.utils.jsgen.VisibleComponent' : _("""A visible component"""),
254
254
  'lino.utils.jsgen.VisibleComponent.install_permission_handler' : _("""Define the allow_read handler used by get_view_permission(). This must be done only once, but after having configured debug_permissions and required_roles."""),
255
255
  'lino.utils.media.MediaFile' : _("""Represents a file on the server below MEDIA_ROOT with two properties path and url."""),
256
+ 'lino.utils.media.MediaFile.path' : _("""A pathlib.Path naming the file on the server’s file system."""),
257
+ 'lino.utils.media.MediaFile.url' : _("""The URL to use for getting this file from a web client."""),
256
258
  'lino.utils.mldbc.fields.BabelCharField' : _("""Define a variable number of CharField database fields, one for each language of your lino.core.site.Site.languages. See mldbc."""),
257
259
  'lino.utils.mldbc.fields.BabelTextField' : _("""Used for the clones of the master field, one for each non-default language. See mldbc."""),
258
260
  'lino.utils.mldbc.fields.LanguageField' : _("""A field that lets the user select a language from the available lino.core.site.Site.languages."""),
@@ -551,6 +553,8 @@ help_texts = {
551
553
  'lino.modlib.jinja.XMLMaker.xml_file_name' : _("""The name of the XML file to generate. This file will be overwritten without asking. The name formatted with one name self in the context."""),
552
554
  'lino.modlib.jinja.XMLMaker.xml_file_template' : _("""The name of a Jinja template to render for generating the XML content."""),
553
555
  'lino.modlib.jinja.XMLMaker.xml_validator_file' : _("""The name of a “validator” to use for validating the XML content."""),
556
+ 'lino.modlib.jinja.XMLMaker.get_xml_file' : _("""Get the name of the XML file to be generated for this database row."""),
557
+ 'lino.modlib.jinja.XMLMaker.make_xml_file' : _("""Make the XML file for this database row."""),
554
558
  'lino.modlib.memo.Previewable' : _("""Adds three rich text fields (lino.core.fields.RichTextField):"""),
555
559
  'lino.modlib.memo.Previewable.body' : _("""An editable text body."""),
556
560
  'lino.modlib.memo.Previewable.body_short_preview' : _("""A read-only preview of the first paragraph of body."""),
lino/mixins/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2010-2021 Rumma & Ko Ltd
2
+ # Copyright 2010-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
  """
5
5
  This package contains model mixins, some of which are heavily used
@@ -22,18 +22,37 @@ by applications and the :ref:`xl`. But none of them is mandatory.
22
22
  from django.db import models
23
23
  from django.conf import settings
24
24
  from django.utils.translation import gettext_lazy as _
25
- from django.utils.html import format_html
26
- from django.utils.text import format_lazy
27
- from django.utils import timezone
25
+ # from django.utils.html import format_html
26
+ # from django.utils.text import format_lazy
27
+ # from django.utils import timezone
28
28
  from django.contrib.humanize.templatetags.humanize import naturaltime
29
29
 
30
+ # Note that reordering the imports here can cause the field ordering to change
31
+ # in models like lino_voga.lib.courses.TeacherType, which inherits from
32
+ # `(Referrable, BabelNamed, Printable)`. This can cause doctests like
33
+ # docs/specs/voga/courses.rst to fail because the `ref` field then came after
34
+ # the name field. The TeacherTypes table has no explicit `column_names`, so it
35
+ # uses the "natural" field ordering, which is, as this observation shows, quite
36
+ # unpredictable.
37
+
30
38
  from lino.core import actions
31
39
  from lino.core import fields
32
40
  from lino.core import model
33
- from lino.core.workflows import ChangeStateAction
41
+ # from lino.core.workflows import ChangeStateAction
42
+ # from lino.core.exceptions import ChangedAPI
34
43
  from lino.utils.mldbc.fields import LanguageField
35
- from lino.core.exceptions import ChangedAPI
36
44
  from lino.utils.html import E
45
+ from lino.utils.mldbc.mixins import BabelNamed, BabelDesignated
46
+ from lino.utils.mldbc.fields import BabelCharField, BabelTextField
47
+
48
+ from .human import Human
49
+ from .polymorphic import Polymorphic
50
+ from .periods import ObservedDateRange, Yearly, Monthly, Today
51
+ from .periods import DateRange
52
+ from .sequenced import Sequenced, Hierarchical
53
+ from .duplicable import Duplicable, Duplicate
54
+ from .registrable import Registrable, RegistrableState
55
+ from .ref import Referrable, StructuredReferrable
37
56
 
38
57
 
39
58
  class Contactable(model.Model):
@@ -97,7 +116,7 @@ class Modified(model.Model):
97
116
  super().save(*args, **kwargs)
98
117
 
99
118
  def touch(self):
100
- self.modified = timezone.now()
119
+ self.modified = settings.SITE.now()
101
120
 
102
121
 
103
122
  class Created(model.Model):
@@ -124,7 +143,7 @@ class Created(model.Model):
124
143
 
125
144
  def save(self, *args, **kwargs):
126
145
  if self.created is None and not settings.SITE.loading_from_dump:
127
- self.created = timezone.now()
146
+ self.created = settings.SITE.now()
128
147
  super().save(*args, **kwargs)
129
148
 
130
149
 
@@ -264,25 +283,3 @@ class Draggable(model.Model):
264
283
 
265
284
  def on_dropped(self, ar, **kwargs):
266
285
  pass
267
-
268
-
269
- from .ref import Referrable, StructuredReferrable
270
- from .registrable import Registrable, RegistrableState
271
- from lino.mixins.duplicable import Duplicable, Duplicate
272
- from lino.mixins.sequenced import Sequenced, Hierarchical
273
- from lino.mixins.periods import DateRange
274
- from lino.mixins.periods import ObservedDateRange, Yearly, Monthly, Today
275
- from lino.mixins.polymorphic import Polymorphic
276
-
277
- # Observation: moving the following two lines to the top (to be together with
278
- # the import of LanguageField) caused the field ordering to change in models
279
- # like `lino_voga.lib.courses.TeacherType` which inherits from `(Referrable,
280
- # BabelNamed, Printable)`. Which caused docs/specs/voga/courses.rst to fail
281
- # because the `ref` field then came after the name field. The TeacherTypes table
282
- # has no explicit `column_names`, so it uses the "natural" field ordering, which
283
- # is, as this observation shows, quite unpredictable.
284
-
285
- from lino.utils.mldbc.fields import BabelCharField, BabelTextField
286
- from lino.utils.mldbc.mixins import BabelNamed, BabelDesignated
287
-
288
- from lino.mixins.human import Human
@@ -119,7 +119,7 @@ class About(EmptyTable):
119
119
  packages = set(["django"])
120
120
 
121
121
  items.append(
122
- E.li(gettext("Server timestamp"), " : ", E.b(dtfmt(site.kernel.code_mtime)))
122
+ E.li(gettext("Server timestamp"), " : ", E.b(dtfmt(site.lino_version)))
123
123
  )
124
124
 
125
125
  for p in site.installed_plugins:
@@ -159,7 +159,7 @@ class ChangesByMaster(Changes):
159
159
  def log_change(type, request, master, obj, msg="", changed_fields=""):
160
160
  Change(
161
161
  type=type,
162
- time=timezone.now(),
162
+ time=dd.now(),
163
163
  master=master,
164
164
  user=request.user,
165
165
  object=obj,
@@ -177,7 +177,7 @@ if remove_after:
177
177
  def delete_older_changes(ar):
178
178
  days = datetime.timedelta(days=remove_after)
179
179
  # django.core.exceptions.FieldError: Cannot resolve keyword 'time_lt' into field. Choices are: changed_fields, diff, id, list_item, master, master_id, master_type, master_type_id, name_column, navigation_panel, object, object_id, object_type, object_type_id, overview, time, type, user, user_id, workflow_buttons
180
- qs = Change.objects.filter(time__lt=timezone.now() - days)
180
+ qs = Change.objects.filter(time__lt=dd.now() - days)
181
181
  if qs.count() > 0:
182
182
  ar.logger.info(
183
183
  "Removing %d changes older than %d days.", qs.count(), remove_after
@@ -1,12 +1,7 @@
1
- # Copyright 2014-2020 Rumma & Ko Ltd
1
+ # Copyright 2014-2025 Rumma & Ko Ltd
2
2
  # License: GNU Affero General Public License v3 (see file COPYING for details)
3
- """This plugin installs a button to export any table to excel xls format.
4
-
5
- To use it, simply add the following line to your
6
- :meth:`lino.core.site.Site.get_installed_plugins`::
7
-
8
- yield 'lino.modlib.export_excel'
9
-
3
+ """
4
+ See :doc:`/plugins/export_excel`.
10
5
  """
11
6
 
12
7
  from lino import ad, _
@@ -16,3 +11,6 @@ class Plugin(ad.Plugin):
16
11
  "See :doc:`/dev/plugins`."
17
12
 
18
13
  verbose_name = _("Export to Excel xls format")
14
+
15
+ def get_requirements(self, site):
16
+ yield "openpyxl"
@@ -1,9 +1,7 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2014-2024 Rumma & Ko Ltd
2
+ # Copyright 2014-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
- """Database models for `lino.modlib.export_excel`.
5
4
 
6
- """
7
5
  import os
8
6
 
9
7
  from django.conf import settings
@@ -1481,7 +1481,7 @@ class ExtRenderer(JsCacheRenderer):
1481
1481
  # if settings.SITE.never_build_site_cache:
1482
1482
  # yield "GEN_TIMESTAMP = '%s';" % settings.SITE.kernel.lino_version
1483
1483
  # else:
1484
- yield "GEN_TIMESTAMP = %s;" % py2js(settings.SITE.kernel.code_mtime)
1484
+ yield "GEN_TIMESTAMP = %s;" % py2js(settings.SITE.lino_version)
1485
1485
 
1486
1486
  return "\n".join(fn())
1487
1487
 
@@ -138,7 +138,7 @@ def test_version_mismatch(request):
138
138
  if os.environ.get("PYCHARM_HOSTED", False):
139
139
  return {}
140
140
  lv = request.GET.get(constants.URL_PARAM_LINO_VERSION)
141
- if lv is None or float(lv) == settings.SITE.kernel.code_mtime:
141
+ if lv is None or float(lv) == settings.SITE.lino_version:
142
142
  return {}
143
143
  # print("20201217", lv, settings.SITE.kernel.code_mtime)
144
144
  cache.clear()
@@ -35,6 +35,9 @@ class XMLMaker(dd.Model):
35
35
  yield 'xml'
36
36
  yield self.get_printable_target_stem() + ".xml"
37
37
 
38
+ def get_xml_file(self):
39
+ return MediaFile(False, *self.get_xml_file_parts())
40
+
38
41
  def make_xml_file(self, ar):
39
42
  renderer = settings.SITE.plugins.jinja.renderer
40
43
  tpl = renderer.jinja_env.get_template(self.xml_file_template)
@@ -45,7 +48,7 @@ class XMLMaker(dd.Model):
45
48
  # parts = [
46
49
  # dd.plugins.accounting.xml_media_dir,
47
50
  # self.xml_file_name.format(self=self)]
48
- xmlfile = MediaFile(False, *self.get_xml_file_parts())
51
+ xmlfile = self.get_xml_file()
49
52
  # xmlfile = Path(settings.MEDIA_ROOT, *parts)
50
53
  ar.logger.info("Make %s from %s ...", xmlfile.path, self)
51
54
  xmlfile.path.parent.mkdir(exist_ok=True, parents=True)
@@ -45,15 +45,15 @@ class RunNow(dd.Action):
45
45
 
46
46
  def run_from_ui(self, ar, **kwargs):
47
47
  # print("20231102 RunNow", ar.selected_rows)
48
+ now = dd.now()
48
49
  for obj in ar.selected_rows:
49
50
  assert issubclass(obj.__class__, Runnable)
50
51
  if True: # dd.plugins.linod.use_channels:
51
52
  obj.last_start_time = None
52
53
  obj.last_end_time = None
53
- obj.requested_at = timezone.now()
54
- obj.message = "{} requested to run this task at {}.".format(
55
- ar.get_user(), dd.ftl(timezone.now())
56
- )
54
+ obj.requested_at = now
55
+ tpl = _("{0} requested to run this task at {1}.")
56
+ obj.message = tpl.format(ar.get_user(), dd.fdtl(now))
57
57
  # obj.disabled = False
58
58
  obj.full_clean()
59
59
  obj.save()
@@ -155,10 +155,12 @@ class Runnable(Sequenced, RecurrenceSet):
155
155
  await ar.adebug("Start %s with logging level %s", self, self.log_level)
156
156
  # ar.info("Start %s with logging level %s", astr(self), self.log_level)
157
157
  # forget about any previous run:
158
- self.last_start_time = timezone.now()
158
+ now = await sync_to_async(dd.now)()
159
+ self.last_start_time = now
159
160
  self.requested_at = None
160
161
  self.last_end_time = None
161
- self.message = ""
162
+ self.message = f"Started at {self.last_start_time} " \
163
+ f"with logging level {self.log_level}"
162
164
  # print("20231102 full_clean")
163
165
  await sync_to_async(self.full_clean)()
164
166
  # self.full_clean()
@@ -184,19 +186,21 @@ class Runnable(Sequenced, RecurrenceSet):
184
186
  self.disabled = True
185
187
  await ar.awarning("Disabled %s after exception %s", self, e)
186
188
  # ar.warning("Disabled %s after exception %s", astr(self), e)
187
- self.last_end_time = timezone.now()
189
+ now = await sync_to_async(dd.now)()
190
+ self.last_end_time = now
188
191
  self.message = "<pre>" + self.message + "</pre>"
189
192
  await sync_to_async(self.full_clean)()
190
193
  # self.full_clean()
191
194
  await self.asave()
195
+ await sync_to_async(dd.post_ui_save.send)(sender=self.__class__, instance=self)
192
196
 
193
197
  @dd.displayfield("Status")
194
198
  def status(self, ar=None):
195
199
  if self.is_running():
196
- return _("Running since {}").format(dd.ftf(self.last_start_time))
200
+ return _("Running since {}").format(dd.fdtf(self.last_start_time))
197
201
  if self.requested_at is not None:
198
202
  return _("Requested to run asap (since {})").format(
199
- dd.ftf(self.requested_at))
203
+ dd.fdtf(self.requested_at))
200
204
  if self.disabled:
201
205
  return _("Disabled")
202
206
  if self.last_start_time is None or self.last_end_time is None:
@@ -206,7 +210,7 @@ class Runnable(Sequenced, RecurrenceSet):
206
210
  next_time = self.get_next_suggested_date(self.last_end_time)
207
211
  if next_time is None:
208
212
  return _("Not scheduled")
209
- return _("Scheduled to run at {}").format(dd.ftf(next_time))
213
+ return _("Scheduled to run at {}").format(dd.fdtf(next_time))
210
214
 
211
215
 
212
216
  async def start_task_runner(ar=None, max_count=None):
@@ -219,7 +223,7 @@ async def start_task_runner(ar=None, max_count=None):
219
223
  while True:
220
224
  await ar.adebug("Start next task runner loop.")
221
225
 
222
- now = timezone.now()
226
+ now = await sync_to_async(dd.now)()
223
227
  next_time = now + \
224
228
  timedelta(seconds=dd.plugins.linod.background_sleep_time)
225
229
 
@@ -282,7 +286,8 @@ async def start_task_runner(ar=None, max_count=None):
282
286
  if max_count is not None and count >= max_count:
283
287
  await ar.ainfo("Stop after %s loops.", max_count)
284
288
  return next_time
285
- if (to_sleep := (next_time - timezone.now()).total_seconds()) <= 0:
289
+ now = await sync_to_async(dd.now)()
290
+ if (to_sleep := (next_time - now).total_seconds()) <= 0:
286
291
  continue
287
292
  await ar.adebug("Let task runner sleep for %s seconds.", to_sleep)
288
293
  await asyncio.sleep(to_sleep)
@@ -13,6 +13,9 @@ TODO:
13
13
  which would be empty for # and @ but "]" for memo commands.
14
14
 
15
15
  """
16
+ from etgen import etree
17
+ from django.db.models import Model
18
+ from django.conf import settings
16
19
  from lino import logger
17
20
 
18
21
  import re
@@ -24,9 +27,6 @@ import warnings
24
27
 
25
28
  warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning)
26
29
 
27
- from django.conf import settings
28
- from django.db.models import Model
29
- from etgen import etree
30
30
 
31
31
  # COMMAND_REGEX = re.compile(r"\[(\w+)\s*((?:[^[\]]|\[.*?\])*?)\]")
32
32
  # ===...... .......=
@@ -139,7 +139,7 @@ class Parser:
139
139
  for key in self.suggesters.keys()
140
140
  ]
141
141
  )
142
- return re.compile(r"([^\w])?([" + triggers + "])(\w+)")
142
+ return re.compile(r"([^\w])?([" + triggers + r"])(\w+)")
143
143
 
144
144
  def register_command(self, cmdname, func: Callable[[Any, str, str, dict], None]):
145
145
  """Register a memo command identified by the given text `cmd`.
@@ -7,6 +7,20 @@ from lino.api import dd, rt, _
7
7
  # PUBLIC_GROUP = "all_users_channel"
8
8
 
9
9
 
10
+ def get_updatables(instance, ar=None):
11
+ data = {
12
+ "actorIDs": instance.updatable_panels,
13
+ "pk": instance.pk,
14
+ "model": f"{instance._meta.app_label}.{instance.__class__.__name__}",
15
+ "mk": None, "master_model": None
16
+ }
17
+ if ar is None:
18
+ return data
19
+ if mi := ar.master_instance:
20
+ data.update(mk=mi.pk, master_model=f"{mi._meta.app_label}.{mi.__class__.__name__}")
21
+ return data
22
+
23
+
10
24
  class ChangeNotifier(dd.Model):
11
25
 
12
26
  class Meta:
@@ -1,5 +1,5 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2011-2024 Rumma & Ko Ltd
2
+ # Copyright 2011-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
5
  # import json
@@ -9,7 +9,6 @@ from datetime import timedelta
9
9
 
10
10
  from django.db import models
11
11
  from django.conf import settings
12
- from django.utils import timezone
13
12
  from django.utils import translation
14
13
 
15
14
  from lino import logger
@@ -25,6 +24,7 @@ from lino.modlib.users.mixins import UserAuthored, My
25
24
  from lino.modlib.linod.choicelists import schedule_daily, schedule_often
26
25
  from lino.modlib.office.roles import OfficeUser
27
26
 
27
+ from .mixins import get_updatables
28
28
  from .choicelists import MessageTypes, MailModes
29
29
  from .api import send_notification, NOTIFICATION, send_panel_update
30
30
 
@@ -57,7 +57,7 @@ class MarkAllSeen(dd.Action):
57
57
  user=ar.get_user(), seen__isnull=True
58
58
  )
59
59
  for obj in qs:
60
- obj.seen = timezone.now()
60
+ obj.seen = dd.now()
61
61
  obj.save()
62
62
  ar.success(
63
63
  eval_js='window.top.document.querySelectorAll(".'
@@ -81,7 +81,7 @@ class MarkSeen(dd.Action):
81
81
 
82
82
  def run_from_ui(self, ar):
83
83
  for obj in ar.selected_rows:
84
- obj.seen = timezone.now()
84
+ obj.seen = dd.now()
85
85
  obj.save()
86
86
  ar.success(refresh_all=True)
87
87
 
@@ -297,9 +297,9 @@ class Message(UserAuthored, Controllable, Created):
297
297
  # dd.logger.info("20240902 %s", sender)
298
298
  ar.send_email(subject, sender, body, [user.email])
299
299
  for msg in messages:
300
- msg.sent = timezone.now()
300
+ msg.sent = dd.now()
301
301
  if dd.plugins.notify.mark_seen_when_sent:
302
- msg.seen = timezone.now()
302
+ msg.seen = dd.now()
303
303
  msg.save()
304
304
 
305
305
  # def send_browser_message_for_all_users(self, user):
@@ -500,7 +500,7 @@ if remove_after:
500
500
  def clear_seen_messages(ar):
501
501
  Message = rt.models.notify.Message
502
502
  qs = Message.objects.filter(
503
- created__lt=timezone.now() - timedelta(days=remove_after)
503
+ created__lt=dd.now() - timedelta(days=remove_after)
504
504
  )
505
505
  what = "notification messages older than {} days".format(remove_after)
506
506
  if dd.plugins.notify.keep_unseen:
@@ -522,26 +522,11 @@ if remove_after:
522
522
 
523
523
 
524
524
  @dd.receiver(dd.post_ui_save)
525
- def notify_panels(sender, instance, ar, **kwargs):
525
+ def notify_panels(sender, instance, ar=None, **kwargs):
526
526
  if (
527
527
  not dd.get_plugin_setting("linod", "use_channels", False)
528
- or not (ups := instance.updatable_panels)
529
- or not hasattr(ar, "rqdata")
528
+ or not instance.updatable_panels
530
529
  ):
531
530
  return
532
- data = {
533
- "actorIDs": ups,
534
- "pk": instance.pk,
535
- "model": f"{instance._meta.app_label}.{instance.__class__.__name__}",
536
- }
537
- data.update(
538
- **(
539
- {
540
- "mk": (mi := ar.master_instance).pk,
541
- "master_model": f"{mi._meta.app_label}.{mi.__class__.__name__}",
542
- }
543
- if ar.master_instance
544
- else {"mk": None, "master_model": None}
545
- )
546
- )
531
+ data = get_updatables(instance, ar)
547
532
  send_panel_update(data)
@@ -72,7 +72,7 @@ def make_uploaded_file(filename, src=None, upload_date=None):
72
72
  def base64_to_image(imgstring):
73
73
  type, file = imgstring.split(";base64,")
74
74
  imgdata = base64.b64decode(file)
75
- return make_captured_image(imgdata, timezone.now(), ext=f".{type.split('/')[1]}")
75
+ return make_captured_image(imgdata, dd.now(), ext=f".{type.split('/')[1]}")
76
76
 
77
77
 
78
78
  def make_captured_image(imgdata, upload_date=None, filename=None, ext='.jpg'):
@@ -1,5 +1,5 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2011-2024 Rumma & Ko Ltd
2
+ # Copyright 2011-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
5
  import datetime
@@ -9,7 +9,6 @@ from lino.utils.html import E, tostring
9
9
  from django.db import models
10
10
  from django.conf import settings
11
11
  from django.http import HttpResponse
12
- from django.utils import timezone
13
12
  from django.utils.translation import gettext
14
13
  from django.contrib.auth.password_validation import validate_password
15
14
  from django.core.exceptions import ValidationError
@@ -115,7 +114,6 @@ class CreateAccount(dd.Action):
115
114
  obj.full_clean()
116
115
  obj.save()
117
116
 
118
-
119
117
  ar.selected_rows = [obj]
120
118
  recipients = ["{} <{}>".format(obj.get_full_name(), obj.email)]
121
119
  send_welcome_email(ar, obj, recipients)
@@ -452,7 +450,7 @@ class SignOut(dd.Action):
452
450
  def validate_sessions_limit(request):
453
451
  if dd.plugins.users.active_sessions_limit == -1:
454
452
  return
455
- qs = rt.models.sessions.Session.objects.filter(expire_date__gt=timezone.now())
453
+ qs = rt.models.sessions.Session.objects.filter(expire_date__gt=dd.now())
456
454
  if request.session.session_key:
457
455
  qs = qs.exclude(session_key=request.session.session_key)
458
456
  if qs.count() >= dd.plugins.users.active_sessions_limit:
@@ -1,7 +1,9 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2011-2023 Rumma & Ko Ltd
2
+ # Copyright 2011-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
+ import random
6
+ import string
5
7
  from datetime import timedelta
6
8
  from django.db import models
7
9
  from django.db.models import Q
@@ -9,15 +11,17 @@ from django.conf import settings
9
11
  from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
10
12
  from django.utils import timezone
11
13
 
12
- from lino.utils.html import E
13
14
  from lino.api import dd, rt, _
14
15
  from lino.core import userprefs
15
-
16
- # from lino.core.fields import NullCharField
16
+ from lino.core.roles import Supervisor
17
17
  from lino.core.roles import SiteAdmin
18
-
18
+ # from lino.core.fields import NullCharField
19
19
  from lino.mixins import CreatedModified, Contactable
20
20
  from lino.mixins import DateRange
21
+ from lino.modlib.about.choicelists import TimeZones, DateFormats
22
+ from lino.modlib.publisher.mixins import Publishable
23
+ from lino.modlib.about.models import About
24
+ from lino.utils.html import E
21
25
 
22
26
  from .choicelists import UserTypes
23
27
  from .mixins import UserAuthored # , TimezoneHolder
@@ -25,14 +29,8 @@ from .actions import ChangePassword, SignOut, CheckedSubmitInsert
25
29
  from .actions import SendWelcomeMail, SignIn, ConnectAccount
26
30
  from .actions import SendWelcomeMail, CreateAccount, ResetPassword, VerifyUser, VerifyMe
27
31
 
28
- # from .actions import SignIn
29
- from lino.modlib.about.choicelists import TimeZones, DateFormats
30
- from lino.modlib.publisher.mixins import Publishable
32
+ from .ui import *
31
33
 
32
- from lino.core.roles import Supervisor
33
-
34
- import random
35
- import string
36
34
 
37
35
  if multi_ledger := dd.is_installed("ledgers"):
38
36
  from lino_xl.lib.ledgers.actions import SubscribeToLedger
@@ -100,7 +98,7 @@ class User(AbstractBaseUser, Contactable, CreatedModified, Publishable, DateRang
100
98
  if dd.plugins.users.with_nickname:
101
99
  nickname = models.CharField(_("Nickname"), max_length=50, blank=True)
102
100
  else:
103
- nickname = dd.DummyField()
101
+ nickname = dd.DummyField("")
104
102
  first_name = models.CharField(_("First name"), max_length=30, blank=True)
105
103
  last_name = models.CharField(_("Last name"), max_length=30, blank=True)
106
104
  remarks = models.TextField(_("Remarks"), blank=True) # ,null=True)
@@ -156,7 +154,7 @@ class User(AbstractBaseUser, Contactable, CreatedModified, Publishable, DateRang
156
154
 
157
155
  def must_verify(self):
158
156
  self.verification_code = id_generator(12)
159
- self.verification_code_sent_on = timezone.now()
157
+ self.verification_code_sent_on = dd.now()
160
158
 
161
159
  def is_verified(self):
162
160
  return not self.verification_code
@@ -170,7 +168,7 @@ class User(AbstractBaseUser, Contactable, CreatedModified, Publishable, DateRang
170
168
  return (
171
169
  self.verification_code_sent_on
172
170
  + timedelta(minutes=dd.plugins.users.verification_code_expires)
173
- < timezone.now()
171
+ < dd.now()
174
172
  )
175
173
 
176
174
  def get_as_user(self):
@@ -451,8 +449,6 @@ class Permission(dd.Model):
451
449
  abstract = True
452
450
 
453
451
 
454
- from lino.modlib.about.models import About
455
-
456
452
  About.sign_in = SignIn()
457
453
  About.reset_password = ResetPassword()
458
454
  About.verify_user = VerifyUser()
@@ -478,7 +474,5 @@ def setup_memo_commands(sender=None, **kwargs):
478
474
  )
479
475
 
480
476
 
481
- from .ui import *
482
-
483
477
  if dd.get_plugin_setting("users", "third_party_authentication"):
484
478
  Me.connect_account = ConnectAccount()
lino/modlib/users/ui.py CHANGED
@@ -118,7 +118,7 @@ class AllUsers(Users):
118
118
  class UsersOverview(Users):
119
119
  required_roles = set([])
120
120
  column_names = "username user_type language"
121
- exclude = dict(user_type="")
121
+ exclude = models.Q(user_type="")
122
122
  # abstract = not settings.SITE.is_demo_site
123
123
  detail_layout = None
124
124
 
@@ -5,7 +5,6 @@ This website is part of the Synodalsoft project:
5
5
  <a class="reference external" href="https://using.lino-framework.org">User Guide</a> |
6
6
  <a class="reference external" href="https://hosting.lino-framework.org">Hosting Guide</a> |
7
7
  <a class="reference external" href="https://dev.lino-framework.org">Developer Guide</a> |
8
- <a class="reference external" href="https://community.lino-framework.org">Community Guide</a> |
9
8
  <a class="reference external" href="https://luc.lino-framework.org">Luc’s blog</a>
10
9
  </p><p>
11
10
  <a href="https://www.synodalsoft.net">
lino/utils/format_date.py CHANGED
@@ -109,13 +109,18 @@ def day_and_weekday(d):
109
109
  # return d.strftime("%a%d")
110
110
 
111
111
 
112
- def ftl(t):
113
- # "format time long"
112
+ def fts(t):
113
+ # "format time short"
114
+ return t.strftime(settings.SITE.time_format_strftime)
115
+
116
+
117
+ def fdtl(t):
118
+ # "format datetime long"
114
119
  return "{} {}".format(
115
120
  t.strftime(settings.SITE.date_format_strftime),
116
121
  t.strftime(settings.SITE.time_format_strftime))
117
122
 
118
123
 
119
- def ftf(t):
120
- # "format time full"
121
- return "{} ({})".format(ftl(t), naturaltime(t))
124
+ def fdtf(t):
125
+ # "format datetime full"
126
+ return "{} ({})".format(fdtl(t), naturaltime(t))