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
@@ -4,43 +4,49 @@
4
4
 
5
5
  from django.utils.translation import gettext_lazy as _
6
6
 
7
- from lino.api import dd
8
- from lino.utils import ONE_DAY
7
+ # from lino.api import dd
8
+ from lino.core.choicelists import Choice, ChoiceList
9
+ from lino.core.workflows import Workflow
10
+ from lino.core.fields import displayfield
9
11
 
10
12
 
11
- class PeriodType(dd.Choice):
13
+ class PeriodType(Choice):
12
14
  ref_template = None
13
15
 
14
16
  def __init__(self, value, text, duration, ref_template):
15
17
  super().__init__(value, text, value)
16
18
  self.ref_template = ref_template
17
19
  self.duration = duration
20
+ self.seqnos = tuple(range(1, int(12/duration)+1))
18
21
 
19
- class PeriodTypes(dd.ChoiceList):
22
+
23
+ class PeriodTypes(ChoiceList):
20
24
  item_class = PeriodType
21
25
  verbose_name = _("Period type")
22
26
  verbose_name_plural = _("Period types")
23
27
  column_names = "value text duration ref_template"
24
28
 
25
- @dd.displayfield(_("Duration"))
29
+ @displayfield(_("Duration"))
26
30
  def duration(cls, p, ar):
27
31
  return str(p.duration)
28
32
 
29
- @dd.displayfield(_("Template for reference"))
33
+ @displayfield(_("Template for reference"))
30
34
  def ref_template(cls, p, ar):
31
35
  return p.ref_template
32
36
 
37
+
33
38
  add = PeriodTypes.add_item
34
39
  # value/names, text, duration, ref_template
35
40
  add("month", _("Month"), 1, "{month:0>2}")
36
- add("quarter", _("Quarter"), 3, "Q{period}")
37
- add("trimester", _("Trimester"), 4, "T{period}")
38
- add("semester", _("Semester"), 6, "S{period}")
41
+ add("quarter", _("Quarter"), 3, "Q{seqno}")
42
+ add("trimester", _("Trimester"), 4, "T{seqno}")
43
+ add("semester", _("Semester"), 6, "S{seqno}")
39
44
 
40
45
 
41
- class PeriodStates(dd.Workflow):
46
+ class PeriodStates(Workflow):
42
47
  pass
43
48
 
49
+
44
50
  add = PeriodStates.add_item
45
51
  add('10', _("Open"), 'open')
46
52
  add('20', _("Closed"), 'closed')
@@ -4,13 +4,14 @@
4
4
 
5
5
  import datetime
6
6
  from django.db import models
7
+ from django.core.exceptions import ValidationError
7
8
  from django.utils.translation import gettext_lazy as _
8
9
 
9
10
  from lino.api import dd
10
- from lino import mixins
11
11
  from lino.utils import ONE_DAY
12
12
  from lino.mixins.periods import DateRange
13
- from lino.mixins import Referrable
13
+ from lino.mixins.ref import Referrable
14
+ from lino.mixins.sequenced import Sequenced
14
15
  from lino.modlib.system.choicelists import DurationUnits
15
16
  from lino.modlib.office.roles import OfficeStaff
16
17
  from .choicelists import PeriodTypes, PeriodStates
@@ -97,7 +98,7 @@ class StoredYear(DateRange, Referrable):
97
98
  # return self.__class__.get_or_create_from_date(nextyear)
98
99
 
99
100
 
100
- class StoredPeriod(DateRange, Referrable):
101
+ class StoredPeriod(DateRange, Referrable, Sequenced):
101
102
 
102
103
  class Meta:
103
104
  ordering = ['ref']
@@ -112,6 +113,47 @@ class StoredPeriod(DateRange, Referrable):
112
113
  null=True, related_name="periods")
113
114
  remark = models.CharField(_("Remark"), max_length=250, blank=True)
114
115
 
116
+ @classmethod
117
+ # get_default_for_date until 20241020
118
+ def get_or_create_from_date(cls, date, save=True):
119
+ pt = dd.plugins.periods.period_type
120
+ month = date.month
121
+ month_offset = month - dd.plugins.periods.start_month
122
+ if month_offset < 0:
123
+ month_offset += 12
124
+ seqno = int(month_offset / pt.duration) + 1
125
+ ref = pt.ref_template.format(**locals())
126
+ ref = StoredYear.get_ref_for_date(date) + YEAR_PERIOD_SEP + ref
127
+ obj = cls.get_by_ref(ref, None)
128
+ if obj is None:
129
+ sd, ed = cls.get_range_for_date(date)
130
+ obj = cls(ref=ref, start_date=sd, end_date=ed, seqno=seqno)
131
+ if save:
132
+ obj.full_clean()
133
+ obj.save()
134
+ return obj
135
+
136
+ def full_clean(self, *args, **kwargs):
137
+ if self.start_date is None:
138
+ self.start_date = dd.today().replace(day=1)
139
+ if self.year_id is None:
140
+ self.year = StoredYear.get_or_create_from_date(self.start_date)
141
+ if not self.state:
142
+ self.state = self.year.state
143
+ super().full_clean(*args, **kwargs)
144
+ pt = dd.plugins.periods.period_type
145
+ if self.seqno not in pt.seqnos:
146
+ raise ValidationError(f"seqno must be in {pt.seqnos}")
147
+
148
+ def __str__(self):
149
+ if not self.ref:
150
+ return dd.obj2str(self)
151
+ # "{0} {1} (#{0})".format(self.pk, self.year)
152
+ return self.ref
153
+
154
+ def get_siblings(self):
155
+ return self.__class__.objects.filter(year=self.year)
156
+
115
157
  @classmethod
116
158
  def get_simple_parameters(cls):
117
159
  yield super().get_simple_parameters()
@@ -170,18 +212,6 @@ class StoredPeriod(DateRange, Referrable):
170
212
  kwargs[fieldname + '__in'] = periods
171
213
  return kwargs
172
214
 
173
- @classmethod
174
- def get_ref_for_date(cls, date):
175
- pt = dd.plugins.periods.period_type
176
- month = date.month
177
- month_offset = month - dd.plugins.periods.start_month
178
- if month_offset < 0:
179
- month_offset += 12
180
- period = int(month_offset / pt.duration) + 1
181
- # periods_per_year = int(12 / p.duration)
182
- # period = (month_offset % (periods_per_year-1)) + 1
183
- return pt.ref_template.format(**locals())
184
-
185
215
  @classmethod
186
216
  def get_range_for_date(cls, date):
187
217
  """
@@ -205,32 +235,6 @@ class StoredPeriod(DateRange, Referrable):
205
235
  ed = DurationUnits.months.add_duration(sd, pt.duration) - ONE_DAY
206
236
  return (sd, ed)
207
237
 
208
- @classmethod
209
- def get_or_create_from_date(cls, date): # get_default_for_date until 20241020
210
- ref = date2ref(date)
211
- obj = cls.get_by_ref(ref, None)
212
- if obj is None:
213
- sd, ed = cls.get_range_for_date(date)
214
- obj = cls(ref=ref, start_date=sd, end_date=ed)
215
- obj.full_clean()
216
- obj.save()
217
- return obj
218
-
219
- def full_clean(self, *args, **kwargs):
220
- if self.start_date is None:
221
- self.start_date = dd.today().replace(day=1)
222
- if not self.year_id:
223
- self.year = StoredYear.get_or_create_from_date(self.start_date)
224
- if not self.state:
225
- self.state = self.year.state
226
- super().full_clean(*args, **kwargs)
227
-
228
- def __str__(self):
229
- if not self.ref:
230
- return dd.obj2str(self)
231
- # "{0} {1} (#{0})".format(self.pk, self.year)
232
- return self.ref
233
-
234
238
  # def get_str_words(self, ar):
235
239
  # # if ar.is_obvious_field("year"):
236
240
  # if self.year.covers_date(dd.today()):
@@ -251,10 +255,6 @@ class StoredPeriod(DateRange, Referrable):
251
255
  StoredPeriod.set_widget_options('ref', width=6)
252
256
 
253
257
 
254
- def date2ref(d):
255
- return StoredYear.get_ref_for_date(d) + YEAR_PERIOD_SEP + StoredPeriod.get_ref_for_date(d)
256
-
257
-
258
258
  class StoredYears(dd.Table):
259
259
  model = 'periods.StoredYear'
260
260
  required_roles = dd.login_required(OfficeStaff)
@@ -2,13 +2,9 @@
2
2
  # Copyright 2023 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
- from lino.core import constants as ext_requests
6
- from lino.core.renderer import HtmlRenderer
5
+ from lino import logger
7
6
  from lino.core.renderer import add_user_language
8
-
9
7
  from lino.modlib.bootstrap3.renderer import Renderer
10
- from .mixins import Publishable
11
- # from .choicelists import PublisherViews
12
8
 
13
9
 
14
10
  class Renderer(Renderer):
@@ -27,6 +23,7 @@ class Renderer(Renderer):
27
23
  # if ar.actor is None or not isinstance(obj, ar.actor.model):
28
24
  loc = obj.__class__._lino_publisher_location
29
25
  if loc is None:
26
+ # logger.warning("No location for %s", obj.__class__)
30
27
  return None
31
28
  add_user_language(kwargs, ar)
32
29
  return self.front_end.buildurl(loc, str(obj.pk), **kwargs)
@@ -1,13 +1,15 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2017-2018 Luc Saffre
2
+ # Copyright 2017-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
  """Runs the :manage:`checksummaries` management command.
5
5
 
6
6
  """
7
7
 
8
8
  from django.core.management import call_command
9
+ from lino.api import dd
9
10
 
10
11
 
11
12
  def objects():
12
- call_command("checksummaries")
13
+ if not dd.is_installed("linod"):
14
+ call_command("checksummaries")
13
15
  return []
@@ -78,7 +78,7 @@ class SiteConfig(dd.Model):
78
78
  This is Lino's equivalent of Django's :setting:`SITE_ID` setting.
79
79
  Lino applications don't need ``django.contrib.sites`` (`The
80
80
  "sites" framework
81
- <https://docs.djangoproject.com/en/5.0/ref/contrib/sites/>`_)
81
+ <https://docs.djangoproject.com/en/5.2/ref/contrib/sites/>`_)
82
82
  because an analog functionality is provided by
83
83
  :mod:`lino.modlib.system`.
84
84
  """
@@ -94,14 +94,6 @@ class SiteConfig(dd.Model):
94
94
  simulate_today = models.DateField(
95
95
  _("Simulated date"), blank=True, null=True)
96
96
 
97
- site_company = dd.ForeignKey(
98
- "contacts.Company",
99
- blank=True,
100
- null=True,
101
- verbose_name=_("Site owner"),
102
- related_name="site_company_sites",
103
- )
104
-
105
97
  _site_config = None
106
98
 
107
99
  @classmethod
@@ -144,7 +136,7 @@ class SiteConfig(dd.Model):
144
136
  # said "SiteConfig 1 does not exist"
145
137
  # cannot save the instance here because the db table possibly doesn't yet exit.
146
138
  # ~ self._site_config.save()
147
- cls._site_config.on_startup()
139
+ # cls._site_config.on_startup()
148
140
  return cls._site_config
149
141
 
150
142
  def __str__(self):
@@ -171,14 +163,16 @@ class SiteConfig(dd.Model):
171
163
  # kw[fld.attname] = getattr(other, fld.attname)
172
164
  # self.update(**kw)
173
165
 
174
- def on_startup(self):
175
- # if not self.site_company:
176
- # raise Exception("20230423")
177
- if self.site_company:
178
- # print("20230423", self.site_company)
179
- settings.SITE.copyright_name = str(self.site_company)
180
- if self.site_company.url:
181
- settings.SITE.copyright_url = self.site_company.url
166
+ # def on_startup(self):
167
+ # # if not self.site_company:
168
+ # # raise Exception("20230423")
169
+ # # if self.site_company:
170
+ # site = settings.SITE
171
+ # if (owner := site.get_plugin_setting('contacts', 'site_owner')) is not None:
172
+ # # print("20230423", self.site_company)
173
+ # site.copyright_name = str(owner)
174
+ # if owner.url:
175
+ # site.copyright_url = owner.url
182
176
 
183
177
  # def full_clean(self, *args, **kw):
184
178
  # super().full_clean(*args, **kw)
@@ -207,6 +201,11 @@ class SiteConfig(dd.Model):
207
201
  super().save(*args, **kw)
208
202
  # settings.SITE.clear_site_config()
209
203
 
204
+ @property
205
+ def site_company(self):
206
+ # Backwards compatibility after 20250617
207
+ return settings.SITE.plugins.contacts.site_owner
208
+
210
209
 
211
210
  def my_handler(sender, **kw):
212
211
  # print("20180502 {} my_handler calls clear_site_config()".format(
@@ -2,15 +2,14 @@
2
2
  # Copyright 2015-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
- import os
6
5
  from lino.api import dd, rt
7
- from lino.modlib.uploads.mixins import make_uploaded_file
8
6
 
9
7
  try:
10
8
  from lino_book import DEMO_DATA
11
9
  except ImportError:
12
10
  DEMO_DATA = None
13
11
 
12
+
14
13
  def walk(p):
15
14
  # print("20230331", p)
16
15
  for c in sorted(p.iterdir()):
@@ -20,8 +19,15 @@ def walk(p):
20
19
  else:
21
20
  yield c
22
21
 
22
+
23
23
  def objects():
24
-
24
+
25
+ what = "removed" if dd.plugins.uploads.remove_orphaned_files else "collected"
26
+ orphan = dd.plugins.uploads.uploads_root / "orphan.txt"
27
+ orphan.parent.mkdir(parents=True, exist_ok=True)
28
+ orphan.write_text(f"This file will get {what} by uploads.UploadsFolderChecker")
29
+ dd.logger.info(f"Wrote {orphan}")
30
+
25
31
  if DEMO_DATA is None:
26
32
  # logger.info("No demo data because lino_book is not installed")
27
33
  return
@@ -192,8 +192,11 @@ class UploadBase(Commentable, GalleryViewable):
192
192
  dd.logger.info("Wrote uploaded file %s", ff.path)
193
193
 
194
194
  def get_gallery_item(self, ar):
195
- mf = self.get_media_file()
196
- return dict(image_src=mf.get_image_url())
195
+ if (mf := self.get_media_file()) is not None:
196
+ url = mf.get_image_url()
197
+ else:
198
+ url = "20250703"
199
+ return dict(image_src=url)
197
200
 
198
201
  def full_clean(self, *args, **kw):
199
202
  super().full_clean(*args, **kw)
@@ -2,7 +2,6 @@
2
2
  # Copyright 2008-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
5
  import os
7
6
  from os.path import join, exists
8
7
  from pathlib import Path
@@ -13,7 +12,7 @@ from django.conf import settings
13
12
  from django.core.exceptions import ValidationError
14
13
  from django.utils.text import format_lazy
15
14
  from django.utils.html import format_html, mark_safe
16
- from django.utils.translation import pgettext_lazy as pgettext
15
+ # from django.utils.translation import pgettext_lazy as pgettext
17
16
 
18
17
  from rstgen.sphinxconf.sigal_image import parse_image_spec
19
18
  # from rstgen.sphinxconf.sigal_image import Standard, Thumb, Tiny, Wide, Solo, Duo, Trio
@@ -22,10 +21,8 @@ from rstgen.sphinxconf.sigal_image import parse_image_spec
22
21
  from lino.utils.html import E, join_elems
23
22
  from lino.api import dd, rt, _
24
23
  from lino.modlib.gfks.mixins import Controllable
25
- from lino.modlib.users.mixins import UserAuthored, My
24
+ from lino.modlib.users.mixins import UserAuthored
26
25
 
27
- # from lino.modlib.office.roles import OfficeUser, OfficeStaff, OfficeOperator
28
- from lino.modlib.office.roles import OfficeStaff
29
26
  from lino.mixins import Referrable
30
27
  from lino.utils.soup import register_sanitizer
31
28
  from lino.utils.mldbc.mixins import BabelNamed
@@ -33,11 +30,12 @@ from lino.modlib.checkdata.choicelists import Checker
33
30
  from lino.modlib.publisher.mixins import Publishable
34
31
 
35
32
  from .actions import CameraStream
36
- from .choicelists import Shortcuts, UploadAreas, add_shortcut
33
+ from .choicelists import Shortcuts, UploadAreas
37
34
  from .mixins import UploadBase, base64_to_image
38
35
  from .utils import previewer, UploadMediaFile
39
36
 
40
37
  from . import VOLUMES_ROOT
38
+ from .ui import *
41
39
 
42
40
 
43
41
  class Volume(Referrable):
@@ -328,9 +326,16 @@ class UploadsFolderChecker(Checker):
328
326
  msg = format_lazy(
329
327
  _("File {} has no upload entry."), rel_filename)
330
328
  # print(msg)
331
- yield (dd.plugins.uploads.remove_orphaned_files, msg)
332
- if fix and dd.plugins.uploads.remove_orphaned_files:
333
- filename.unlink()
329
+ yield (True, msg)
330
+ if fix:
331
+ if dd.plugins.uploads.remove_orphaned_files:
332
+ filename.unlink()
333
+ else:
334
+ obj = Upload(
335
+ file=rel_filename, user=ar.get_user(),
336
+ description=f"Found on {dd.today()} by {self}")
337
+ obj.full_clean()
338
+ obj.save()
334
339
  # else:
335
340
  # print("{} has {} entries.".format(filename, n))
336
341
  # elif n > 1:
@@ -487,6 +492,7 @@ def on_sanitize(soup, save=False, ar=None):
487
492
  upload = rt.models.uploads.Upload(file=file, user=ar.get_user())
488
493
  sar = upload.get_default_table().create_request(parent=ar)
489
494
  upload.save_new_instance(sar)
495
+ rt.models.checkdata.fix_instance(ar, upload)
490
496
  tag.replace_with(f'[file {upload.pk}]')
491
497
 
492
498
 
@@ -93,6 +93,9 @@ class FilePreviewer(Previewer):
93
93
  if needs_update(src, dst):
94
94
  yield (True, format_lazy(_("Must build thumbnail for {}"), mf.url))
95
95
  if fix:
96
+ dst.parent.mkdir(parents=True, exist_ok=True)
97
+ # Make parent dir also for pdf previews. See #6181 (Issues after
98
+ # uploading two PDFs to froinde)
96
99
  if src.suffix.lower() == ".pdf":
97
100
  doc = pymupdf.open(src)
98
101
  page = doc.load_page(0)
@@ -101,9 +104,9 @@ class FilePreviewer(Previewer):
101
104
  return
102
105
  with Image.open(src) as im:
103
106
  im.thumbnail((self.max_width, self.max_width))
104
- dst.parent.mkdir(parents=True, exist_ok=True)
105
107
  im.save(dst)
106
108
 
109
+
107
110
  if with_thumbnails:
108
111
  previewer = FilePreviewer("thumbs", 720)
109
112
  else:
@@ -5,6 +5,7 @@
5
5
 
6
6
 
7
7
  from lino.api import ad, _
8
+ from lino import logger
8
9
 
9
10
 
10
11
  class Plugin(ad.Plugin):
@@ -22,6 +23,15 @@ class Plugin(ad.Plugin):
22
23
  # partner_model = 'contacts.Person'
23
24
  partner_model = "contacts.Partner"
24
25
  demo_password = "1234"
26
+ demo_username = None
27
+
28
+ def on_init(self):
29
+ super().on_init()
30
+ self.site.set_user_model("users.User")
31
+ from lino.core.site import has_socialauth
32
+
33
+ if has_socialauth and self.third_party_authentication:
34
+ self.needs_plugins.append("social_django")
25
35
 
26
36
  def pre_site_startup(self, site):
27
37
  super().pre_site_startup(site)
@@ -31,26 +41,27 @@ class Plugin(ad.Plugin):
31
41
  # return
32
42
  self.partner_model = site.models.resolve(self.partner_model)
33
43
 
34
- # def unused_on_plugins_loaded(self, site):
35
- # if self.allow_online_registration:
36
- # # If you use gmail smtp to send email.
37
- # # See: https://support.google.com/mail/answer/7126229?visit_id=1-636656345878819046-1400238651&rd=1#cantsignin&zippy=%2Ci-cant-sign-in-to-my-email-client
38
- # # For this setup you will have to allow less secure app access from
39
- # # your google accounts settings: https://myaccount.google.com/lesssecureapps
40
- # site.update_settings(
41
- # EMAIL_HOST='smtp.gmail.com',
42
- # EMAIL_PORT=587, # For TLS | use 465 for SSL
43
- # EMAIL_HOST_USER='username@gmail.com',
44
- # EMAIL_HOST_PASSWORD='*********',
45
- # EMAIL_USE_TLS=True)
44
+ def post_site_startup(self, site):
45
+ super().post_site_startup(site)
46
+ if self.demo_username is None:
47
+ if (kw := self.get_root_user_fields(site.DEFAULT_LANGUAGE)):
48
+ self.demo_username = kw['username']
46
49
 
47
- def on_init(self):
48
- super().on_init()
49
- self.site.set_user_model("users.User")
50
- from lino.core.site import has_socialauth
50
+ _demo_user = None # the cached User object
51
51
 
52
- if has_socialauth and self.third_party_authentication:
53
- self.needs_plugins.append("social_django")
52
+ def get_demo_user(self, checker, obj):
53
+ if self.demo_username is None:
54
+ return None
55
+ if self._demo_user is None:
56
+ User = self.site.models.users.User
57
+ try:
58
+ self._demo_user = User.objects.get(
59
+ username=self.demo_username)
60
+ except User.DoesNotExist:
61
+ msg = "Invalid username '{0}' in `demo_username` "
62
+ msg = msg.format(self.demo_username)
63
+ raise Exception(msg)
64
+ return self._demo_user
54
65
 
55
66
  def get_requirements(self, site):
56
67
  yield "social-auth-app-django"
@@ -86,3 +97,33 @@ class Plugin(ad.Plugin):
86
97
 
87
98
  def get_quicklinks(self):
88
99
  yield "users.Me"
100
+
101
+ def get_root_user_fields(self, lang, **kw):
102
+ # ~ kw.update(user_type='900') # UserTypes.admin)
103
+ # ~ print 20130219, UserTypes.items()
104
+ kw.update(user_type=self.site.models.users.UserTypes.admin)
105
+ kw.update(email=self.site.demo_email) # 'root@example.com'
106
+ lang = lang.django_code
107
+ kw.update(language=lang)
108
+ lang = lang[:2]
109
+ if lang == "en":
110
+ kw.update(first_name="Robin", last_name="Rood")
111
+ elif lang == "de":
112
+ kw.update(first_name="Rolf", last_name="Rompen")
113
+ elif lang == "fr":
114
+ kw.update(first_name="Romain", last_name="Raffault")
115
+ elif lang == "et":
116
+ kw.update(first_name="Rando", last_name="Roosi")
117
+ elif lang == "pt":
118
+ kw.update(first_name="Ronaldo", last_name="Rosa")
119
+ elif lang == "es":
120
+ kw.update(first_name="Rodrigo", last_name="Rosalez")
121
+ elif lang == "nl":
122
+ kw.update(first_name="Rik", last_name="Rozenbos")
123
+ elif lang == "bn":
124
+ kw.update(first_name="Roby", last_name="Raza")
125
+ else:
126
+ logger.warning("No demo user for language %r.", lang)
127
+ return None
128
+ kw.update(username=kw.get("first_name").lower())
129
+ return kw
@@ -237,21 +237,21 @@ class VerifyMe(VerifyUser):
237
237
  self.doit(ar, user, **kwargs)
238
238
 
239
239
 
240
- if settings.SITE.default_ui == "lino_react.react":
241
-
242
- class MySettings(dd.Action):
243
- label = _("My settings")
244
- select_rows = False
245
- show_in_toolbar = False
246
- # http_method = "POST"
247
- default_format = None
248
-
249
- def run_from_ui(self, ar, **kw):
250
- # assert len(ar.selected_rows) == 1
251
- # user = ar.selected_rows[0]
252
- # raise PermissionError("20210811")
253
- user = ar.get_user()
254
- ar.goto_instance(user)
240
+ # if settings.SITE.default_ui == "lino_react.react":
241
+ #
242
+ # class MySettings(dd.Action):
243
+ # label = _("My settings")
244
+ # select_rows = False
245
+ # show_in_toolbar = False
246
+ # # http_method = "POST"
247
+ # default_format = None
248
+ #
249
+ # def run_from_ui(self, ar, **kw):
250
+ # # assert len(ar.selected_rows) == 1
251
+ # # user = ar.selected_rows[0]
252
+ # # raise PermissionError("20210811")
253
+ # user = ar.get_user()
254
+ # ar.goto_instance(user)
255
255
 
256
256
 
257
257
  class SendWelcomeMail(dd.Action):
@@ -263,16 +263,16 @@ class SendWelcomeMail(dd.Action):
263
263
  show_in_toolbar = False
264
264
  show_in_workflow = True
265
265
  button_text = "\u2709" # ✉
266
+ select_rows = True
267
+ # required_roles = dd.login_required()
266
268
 
267
269
  # required_roles = dd.login_required(SiteAdmin)
268
270
 
269
271
  def get_action_permission(self, ar, obj, state):
270
- if not obj.email:
271
- return False
272
272
  user = ar.get_user()
273
- if user != obj:
274
- if not user.user_type.has_required_roles([SiteAdmin]):
275
- return False
273
+ if user != obj and not user.user_type.has_required_roles([SiteAdmin]):
274
+ # print(f"20250712 {user} != {obj}")
275
+ return False
276
276
  return super().get_action_permission(ar, obj, state)
277
277
 
278
278
  def run_from_ui(self, ar, **kw):
@@ -284,6 +284,10 @@ class SendWelcomeMail(dd.Action):
284
284
  obj.full_clean()
285
285
  obj.save()
286
286
 
287
+ if not obj.email:
288
+ ar.error(_("Cannot verify without email address"), alert=True)
289
+ return
290
+
287
291
  recipients = ["{} <{}>".format(obj.get_full_name(), obj.email)]
288
292
 
289
293
  def ok(ar):
@@ -2,41 +2,8 @@
2
2
  # Copyright 2010-2020 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
- from lino import logger
6
-
7
5
  from django.conf import settings
8
- from lino.modlib.users.choicelists import UserTypes
9
-
10
-
11
- def root_user(lang, **kw):
12
- # ~ kw.update(user_type='900') # UserTypes.admin)
13
- # ~ print 20130219, UserTypes.items()
14
- kw.update(user_type=UserTypes.admin)
15
- kw.update(email=settings.SITE.demo_email) # 'root@example.com'
16
- lang = lang.django_code
17
- kw.update(language=lang)
18
- lang = lang[:2]
19
- if lang == "en":
20
- kw.update(first_name="Robin", last_name="Rood")
21
- elif lang == "de":
22
- kw.update(first_name="Rolf", last_name="Rompen")
23
- elif lang == "fr":
24
- kw.update(first_name="Romain", last_name="Raffault")
25
- elif lang == "et":
26
- kw.update(first_name="Rando", last_name="Roosi")
27
- elif lang == "pt":
28
- kw.update(first_name="Ronaldo", last_name="Rosa")
29
- elif lang == "es":
30
- kw.update(first_name="Rodrigo", last_name="Rosalez")
31
- elif lang == "nl":
32
- kw.update(first_name="Rik", last_name="Rozenbos")
33
- elif lang == "bn":
34
- kw.update(first_name="Roby", last_name="Raza")
35
- else:
36
- logger.warning("No demo user for language %r.", lang)
37
- return None
38
- kw.update(username=kw.get("first_name").lower())
39
- return kw
6
+ from lino.api import dd
40
7
 
41
8
 
42
9
  def objects():
@@ -48,7 +15,7 @@ def objects():
48
15
  for lang in SITE.languages:
49
16
  if (SITE.hidden_languages is None
50
17
  or lang.django_code not in SITE.hidden_languages):
51
- kw = root_user(lang)
18
+ kw = dd.plugins.users.get_root_user_fields(lang)
52
19
  if kw:
53
20
  u = User(**kw)
54
21
  if SITE.is_demo_site: