lino 25.4.1__py3-none-any.whl → 25.4.3__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 (52) hide show
  1. lino/__init__.py +1 -1
  2. lino/api/dd.py +8 -10
  3. lino/core/dbtables.py +2 -3
  4. lino/core/fields.py +30 -15
  5. lino/core/kernel.py +33 -7
  6. lino/core/renderer.py +3 -3
  7. lino/core/requests.py +24 -9
  8. lino/core/site.py +38 -29
  9. lino/core/tables.py +27 -29
  10. lino/help_texts.py +7 -3
  11. lino/management/commands/demotest.py +16 -22
  12. lino/mixins/__init__.py +32 -35
  13. lino/mixins/dupable.py +2 -4
  14. lino/mixins/registrable.py +5 -2
  15. lino/modlib/about/models.py +2 -2
  16. lino/modlib/changes/models.py +2 -2
  17. lino/modlib/checkdata/choicelists.py +4 -4
  18. lino/modlib/checkdata/models.py +2 -2
  19. lino/modlib/comments/fixtures/demo2.py +4 -0
  20. lino/modlib/comments/models.py +1 -1
  21. lino/modlib/dupable/mixins.py +3 -5
  22. lino/modlib/export_excel/__init__.py +6 -8
  23. lino/modlib/export_excel/models.py +1 -3
  24. lino/modlib/extjs/ext_renderer.py +1 -1
  25. lino/modlib/extjs/views.py +1 -1
  26. lino/modlib/help/fixtures/demo2.py +3 -2
  27. lino/modlib/jinja/mixins.py +20 -4
  28. lino/modlib/linod/mixins.py +17 -12
  29. lino/modlib/linod/models.py +1 -1
  30. lino/modlib/memo/mixins.py +3 -2
  31. lino/modlib/notify/api.py +33 -14
  32. lino/modlib/notify/mixins.py +16 -1
  33. lino/modlib/notify/models.py +10 -25
  34. lino/modlib/printing/mixins.py +1 -1
  35. lino/modlib/publisher/models.py +55 -6
  36. lino/modlib/publisher/ui.py +3 -3
  37. lino/modlib/publisher/views.py +9 -2
  38. lino/modlib/system/models.py +1 -1
  39. lino/modlib/uploads/mixins.py +1 -1
  40. lino/modlib/uploads/models.py +2 -2
  41. lino/modlib/users/actions.py +2 -4
  42. lino/modlib/users/models.py +13 -19
  43. lino/modlib/users/ui.py +1 -1
  44. lino/sphinxcontrib/logo/templates/part-of-synodalsoft.html +0 -1
  45. lino/utils/format_date.py +10 -5
  46. lino/utils/media.py +16 -31
  47. {lino-25.4.1.dist-info → lino-25.4.3.dist-info}/METADATA +1 -1
  48. {lino-25.4.1.dist-info → lino-25.4.3.dist-info}/RECORD +51 -52
  49. lino/management/commands/monitor.py +0 -160
  50. {lino-25.4.1.dist-info → lino-25.4.3.dist-info}/WHEEL +0 -0
  51. {lino-25.4.1.dist-info → lino-25.4.3.dist-info}/licenses/AUTHORS.rst +0 -0
  52. {lino-25.4.1.dist-info → lino-25.4.3.dist-info}/licenses/COPYING +0 -0
lino/modlib/notify/api.py CHANGED
@@ -15,6 +15,8 @@ LIVE_PANEL_UPDATE = "PANEL_UPDATE"
15
15
 
16
16
  NOTIFICATION_TYPES = [NOTIFICATION, CHAT, LIVE_PANEL_UPDATE]
17
17
 
18
+ DONT_CATCH = True
19
+
18
20
 
19
21
  # def send_panel_update(actorIDs: list[str], pk: int):
20
22
  def send_panel_update(data):
@@ -27,16 +29,24 @@ def send_panel_update(data):
27
29
  from channels.layers import get_channel_layer
28
30
  from asgiref.sync import async_to_sync
29
31
 
32
+ if settings.SITE.loading_from_dump:
33
+ return
34
+
30
35
  channel_layer = get_channel_layer()
31
36
 
32
37
  data.update(type=LIVE_PANEL_UPDATE)
33
38
 
34
- try:
39
+ if DONT_CATCH:
35
40
  async_to_sync(channel_layer.send)(
36
41
  CHANNEL_NAME, {"type": "send.panel.update", "text": json.dumps(data)}
37
42
  )
38
- except Exception as e:
39
- logger.exception(e)
43
+ else:
44
+ try:
45
+ async_to_sync(channel_layer.send)(
46
+ CHANNEL_NAME, {"type": "send.panel.update", "text": json.dumps(data)}
47
+ )
48
+ except Exception as e:
49
+ logger.exception(e)
40
50
 
41
51
 
42
52
  def send_notification(
@@ -54,11 +64,9 @@ def send_notification(
54
64
  OK button of their desktop notification.
55
65
 
56
66
  """
57
- if user is None:
67
+ if user is None or settings.SITE.loading_from_dump:
58
68
  return
59
69
 
60
- created = created.strftime("%a %d %b %Y %H:%M")
61
-
62
70
  if dd.get_plugin_setting("linod", "use_channels"):
63
71
  # importing channels at module level would cause certain things to fail
64
72
  # when channels isn't installed, e.g. `manage.py prep` in `lino_book.projects.workflows`.
@@ -73,7 +81,7 @@ def send_notification(
73
81
  subject=subject,
74
82
  id=primary_key,
75
83
  body=body,
76
- created=created,
84
+ created=created.strftime("%a %d %b %Y %H:%M"),
77
85
  action_url=action_url,
78
86
  )
79
87
 
@@ -86,6 +94,7 @@ def send_notification(
86
94
  },
87
95
  ) # data
88
96
  except Exception as e:
97
+ raise # 20250415
89
98
  logger.exception(e)
90
99
 
91
100
  if dd.plugins.notify.use_push_api:
@@ -95,7 +104,7 @@ def send_notification(
95
104
  body=body,
96
105
  action_title=action_title,
97
106
  )
98
- try:
107
+ if DONT_CATCH:
99
108
  async_to_sync(channel_layer.send)(
100
109
  CHANNEL_NAME,
101
110
  {
@@ -104,9 +113,19 @@ def send_notification(
104
113
  "user_id": user.id if user is not None else None,
105
114
  },
106
115
  )
107
- except (
108
- ChannelFull
109
- ) as e: # happens on older Pythons and can cause many tracebacks
110
- # logger.exception(e)
111
- # logger.warning(str(e))
112
- pass
116
+ else:
117
+ try:
118
+ async_to_sync(channel_layer.send)(
119
+ CHANNEL_NAME,
120
+ {
121
+ "type": "send.push",
122
+ "data": data,
123
+ "user_id": user.id if user is not None else None,
124
+ },
125
+ )
126
+ except (
127
+ ChannelFull
128
+ ) as e: # happens on older Pythons and can cause many tracebacks
129
+ # logger.exception(e)
130
+ # logger.warning(str(e))
131
+ pass
@@ -1,12 +1,27 @@
1
- # Copyright 2016-2024 Rumma & Ko Ltd
1
+ # Copyright 2016-2025 Rumma & Ko Ltd
2
2
  # License: GNU Affero General Public License v3 (see file COPYING for details)
3
3
 
4
4
  from lino.utils.html import E, tostring, format_html, mark_safe
5
+ from lino.core.utils import full_model_name as fmn
5
6
  from lino.api import dd, rt, _
6
7
 
7
8
  # PUBLIC_GROUP = "all_users_channel"
8
9
 
9
10
 
11
+ def get_updatables(instance, ar=None):
12
+ data = {
13
+ "actorIDs": instance.updatable_panels,
14
+ "pk": instance.pk,
15
+ "model": f"{fmn(instance)}",
16
+ "mk": None, "master_model": None
17
+ }
18
+ if ar is None:
19
+ return data
20
+ if mi := ar.master_instance:
21
+ data.update(mk=mi.pk, master_model=f"{fmn(mi)}")
22
+ return data
23
+
24
+
10
25
  class ChangeNotifier(dd.Model):
11
26
 
12
27
  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)
@@ -260,7 +260,7 @@ class CachedPrintableChecker(Checker):
260
260
  model = CachedPrintable
261
261
  verbose_name = _("Check for missing target files")
262
262
 
263
- def get_checkdata_problems(self, obj, fix=False):
263
+ def get_checkdata_problems(self, ar, obj, fix=False):
264
264
  if obj.build_time is not None:
265
265
  t = obj.get_cache_mtime()
266
266
  if t is None:
@@ -2,12 +2,6 @@
2
2
  # Copyright 2012-2024 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
- from .ui import *
6
- from lino.api import rt, dd
7
-
8
- from .choicelists import PublishingStates, PageFillers, SpecialPages
9
- from .mixins import Publishable
10
-
11
5
  from html import escape
12
6
  from django.db import models
13
7
  from django.http import HttpResponseRedirect
@@ -15,6 +9,11 @@ from django.conf import settings
15
9
  from django.utils import translation
16
10
  from django.utils.translation import pgettext_lazy
17
11
 
12
+ from lorem import get_paragraph
13
+ from django.utils import translation
14
+ from django.conf import settings
15
+
16
+
18
17
  # from django.utils.translation import get_language
19
18
  from django.utils.html import mark_safe
20
19
 
@@ -38,6 +37,10 @@ from lino.mixins.polymorphic import Polymorphic
38
37
  from lino_xl.lib.topics.mixins import Taggable
39
38
  # from .utils import render_node
40
39
 
40
+ from lino.api import rt, dd
41
+ from .choicelists import PublishingStates, PageFillers, SpecialPages
42
+ from .mixins import Publishable
43
+ from .ui import *
41
44
 
42
45
  # class Node(Referrable, Hierarchical, Sequenced, Previewable, Publishable, Commentable):
43
46
  # Polymorphic,
@@ -401,3 +404,49 @@ def update_publisher_pages(ar):
401
404
  prev = obj
402
405
  count += 1
403
406
  ar.logger.info("%d pages have been updated.", count)
407
+
408
+
409
+ def make_demo_pages(pages_desc):
410
+ # Translation = rt.models.pages.Translation
411
+ # for lc in settings.SITE.LANGUAGE_CHOICES:
412
+ # language = lc[0]
413
+ # kwargs = dict(language=language, ref='index')
414
+ # with translation.override(language):
415
+
416
+ parent_nodes = []
417
+ for lng in settings.SITE.languages:
418
+ counter = {None: 0}
419
+ # count = 0
420
+ home_page = Page.objects.get(
421
+ special_page=SpecialPages.home, language=lng.django_code)
422
+
423
+ with translation.override(lng.django_code):
424
+
425
+ def make_pages(pages, parent=None):
426
+ for page in pages:
427
+ if len(page) != 3:
428
+ raise Exception(f"Oops {page}")
429
+ title, body, children = page
430
+ kwargs = dict(title=title)
431
+ if body is None:
432
+ kwargs.update(body=get_paragraph())
433
+ else:
434
+ kwargs.update(body=body)
435
+ if parent is None:
436
+ # kwargs.update(ref='index')
437
+ continue # home page is created by SpecialPages
438
+ if lng.suffix:
439
+ kwargs.update(
440
+ translated_from=parent_nodes[counter[None]])
441
+ kwargs.update(language=lng.django_code)
442
+ if dd.is_installed("publisher"):
443
+ kwargs.update(publishing_state='published')
444
+ obj = Page(parent=parent, **kwargs)
445
+ yield obj
446
+ if not lng.suffix:
447
+ parent_nodes.append(obj)
448
+ counter[None] += 1
449
+ # print("20230324", title, kwargs)
450
+ yield make_pages(children, obj)
451
+
452
+ yield make_pages(pages_desc, parent=home_page)
@@ -92,7 +92,7 @@ else:
92
92
 
93
93
 
94
94
  class PageDetail(dd.DetailLayout):
95
- main = "first_panel general more"
95
+ main = "general first_panel more"
96
96
 
97
97
  first_panel = dd.Panel(
98
98
  """
@@ -129,11 +129,11 @@ class PageDetail(dd.DetailLayout):
129
129
  # """
130
130
 
131
131
  right_panel = """
132
- ref
132
+ ref language
133
133
  parent seqno
134
134
  child_node_depth
135
135
  #page_type filler
136
- language
136
+ publishing_state special_page
137
137
  publisher.TranslationsByPage
138
138
  """
139
139
 
@@ -5,6 +5,7 @@
5
5
  from django.conf import settings
6
6
  from django import http
7
7
  from django.views.generic import View
8
+ from django.utils import translation
8
9
 
9
10
  from lino.api import dd
10
11
  from lino.core import auth
@@ -52,7 +53,13 @@ class Index(View):
52
53
  def get(self, request, pk=1):
53
54
  rnd = settings.SITE.plugins.publisher.renderer
54
55
  dv = settings.SITE.models.publisher.Pages
55
- index_node = dv.model.objects.get(ref="index", language=request.LANGUAGE_CODE)
56
+ if len(settings.SITE.languages) == 1:
57
+ # language = settings.SITE.languages[0].django_code
58
+ language = translation.get_language()
59
+ else:
60
+ language = request.LANGUAGE_CODE
61
+ index_node = dv.model.objects.get(ref="index", language=language)
56
62
  # print("20231025", index_node)
57
- ar = dv.create_request(request=request, renderer=rnd, selected_rows=[index_node])
63
+ ar = dv.create_request(request=request, renderer=rnd,
64
+ selected_rows=[index_node])
58
65
  return index_node.get_publisher_response(ar)
@@ -326,7 +326,7 @@ class BleachChecker(Checker):
326
326
  if len(m._bleached_fields):
327
327
  yield m
328
328
 
329
- def get_checkdata_problems(self, obj, fix=False):
329
+ def get_checkdata_problems(self, ar, obj, fix=False):
330
330
  t = tuple(obj.fields_to_bleach(save=False))
331
331
  if len(t):
332
332
  fldnames = ", ".join([f.name for f, old, new in t])
@@ -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'):
@@ -285,7 +285,7 @@ class UploadChecker(Checker):
285
285
  verbose_name = _("Check metadata of upload files")
286
286
  model = "uploads.Upload"
287
287
 
288
- def get_checkdata_problems(self, obj, fix=False):
288
+ def get_checkdata_problems(self, ar, obj, fix=False):
289
289
  if obj.file:
290
290
  if not exists(join(settings.MEDIA_ROOT, obj.file.name)):
291
291
  yield (
@@ -311,7 +311,7 @@ UploadChecker.activate()
311
311
  class UploadsFolderChecker(Checker):
312
312
  verbose_name = _("Find orphaned files in uploads folder")
313
313
 
314
- def get_checkdata_problems(self, obj, fix=False):
314
+ def get_checkdata_problems(self, ar, obj, fix=False):
315
315
  assert obj is None # this is an unbound checker
316
316
  Upload = rt.models.uploads.Upload
317
317
  pth = dd.plugins.uploads.uploads_root
@@ -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))
lino/utils/media.py CHANGED
@@ -1,14 +1,12 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2013-2021 Rumma & Ko Ltd
2
+ # Copyright 2013-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
  """Defines the :class:`MediaFile` class.
5
5
  """
6
6
 
7
- from os.path import join
8
7
  from pathlib import Path
9
-
10
8
  from django.conf import settings
11
- from lino.core.utils import is_devserver
9
+ # from lino.core.utils import is_devserver
12
10
 
13
11
  # davlink = settings.SITE.plugins.get('davlink', None)
14
12
  # has_davlink = davlink is not None and settings.SITE.use_java
@@ -24,7 +22,19 @@ class MediaFile(object):
24
22
  :attr:`webdav_root <lino.core.site.Site.webdav_root>`,
25
23
  :attr:`webdav_protocol <lino.core.site.Site.webdav_protocol>`
26
24
  and
27
- :attr:`webdav_url <lino.core.site.Site.webdav_url>`
25
+ :attr:`webdav_url <lino.core.site.Site.webdav_url>`.
26
+
27
+ .. attribute:: path
28
+
29
+ A :class:`pathlib.Path` naming the file on the server's file system.
30
+
31
+ .. attribute:: url
32
+
33
+ The URL to use for getting this file from a web client.
34
+
35
+ Used by :meth:`lino.modlib.jinja.XMLMaker.get_xml_file`,
36
+ :attr:`lino.core.tables.AbstractTable.export_excel` and others.
37
+
28
38
  """
29
39
 
30
40
  def __init__(self, editable, *parts):
@@ -37,7 +47,7 @@ class MediaFile(object):
37
47
  # webdav server and a protocol handler.
38
48
  # if is_devserver():
39
49
  if False:
40
- url = "file://" + join(settings.SITE.webdav_root, *parts)
50
+ url = "file://" + settings.SITE.webdav_root + "/".join(parts)
41
51
  else:
42
52
  url = settings.SITE.webdav_url + "/".join(parts)
43
53
  if settings.SITE.webdav_protocol:
@@ -48,31 +58,6 @@ class MediaFile(object):
48
58
  self.url = url
49
59
  self.path = path
50
60
 
51
- # @property
52
- # def path(self):
53
- # "Return the full filename on the server as a Path object."
54
-
55
- # @property
56
- # def name(self):
57
- # "return the filename on the server"
58
- # if self.editable and (has_davlink or settings.SITE.webdav_protocol):
59
- # return join(settings.SITE.webdav_root, *self.parts)
60
- # return join(settings.MEDIA_ROOT, *self.parts)
61
-
62
- # def get_url(self, request):
63
- # "return the url that points to file on the server"
64
- # if self.editable and request is not None:
65
- # if is_devserver():
66
- # url = "file://" + join(settings.SITE.webdav_root, *self.parts)
67
- # else:
68
- # url = settings.SITE.webdav_url + "/".join(self.parts)
69
- # url = request.build_absolute_uri(url)
70
- # if settings.SITE.webdav_protocol:
71
- # url = settings.SITE.webdav_protocol + "://" + url
72
- # return url
73
- #
74
- # return settings.SITE.build_media_url(*self.parts)
75
-
76
61
 
77
62
  class TmpMediaFile(MediaFile):
78
63
  def __init__(self, ar, fmt):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lino
3
- Version: 25.4.1
3
+ Version: 25.4.3
4
4
  Summary: A framework for writing desktop-like web applications using Django and ExtJS or React
5
5
  Project-URL: Homepage, https://www.lino-framework.org
6
6
  Project-URL: Repository, https://gitlab.com/lino-framework/lino