lino 24.11.0__py3-none-any.whl → 25.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. lino/__init__.py +1 -1
  2. lino/core/actions.py +2 -4
  3. lino/core/actors.py +35 -14
  4. lino/core/dbtables.py +12 -12
  5. lino/core/fields.py +27 -18
  6. lino/core/inject.py +9 -2
  7. lino/core/kernel.py +11 -12
  8. lino/core/model.py +25 -39
  9. lino/core/renderer.py +3 -4
  10. lino/core/requests.py +91 -92
  11. lino/core/site.py +5 -46
  12. lino/core/store.py +4 -4
  13. lino/core/tables.py +0 -16
  14. lino/core/utils.py +5 -2
  15. lino/help_texts.py +7 -5
  16. lino/locale/bn/LC_MESSAGES/django.po +1225 -909
  17. lino/locale/de/LC_MESSAGES/django.mo +0 -0
  18. lino/locale/de/LC_MESSAGES/django.po +1748 -1392
  19. lino/locale/django.pot +1150 -909
  20. lino/locale/es/LC_MESSAGES/django.po +1721 -1347
  21. lino/locale/et/LC_MESSAGES/django.po +1267 -954
  22. lino/locale/fr/LC_MESSAGES/django.mo +0 -0
  23. lino/locale/fr/LC_MESSAGES/django.po +1207 -925
  24. lino/locale/nl/LC_MESSAGES/django.po +1261 -942
  25. lino/locale/pt_BR/LC_MESSAGES/django.po +1204 -906
  26. lino/locale/zh_Hant/LC_MESSAGES/django.po +1204 -906
  27. lino/mixins/dupable.py +8 -7
  28. lino/mixins/periods.py +10 -8
  29. lino/modlib/checkdata/__init__.py +1 -1
  30. lino/modlib/comments/choicelists.py +1 -1
  31. lino/modlib/comments/fixtures/demo2.py +4 -1
  32. lino/modlib/comments/mixins.py +9 -10
  33. lino/modlib/comments/models.py +4 -4
  34. lino/modlib/comments/ui.py +11 -2
  35. lino/modlib/dupable/models.py +10 -14
  36. lino/modlib/gfks/mixins.py +8 -1
  37. lino/modlib/jinja/choicelists.py +3 -3
  38. lino/modlib/jinja/renderer.py +2 -0
  39. lino/modlib/languages/fixtures/all_languages.py +4 -6
  40. lino/modlib/linod/mixins.py +3 -2
  41. lino/modlib/memo/mixins.py +17 -215
  42. lino/modlib/memo/models.py +41 -12
  43. lino/modlib/memo/parser.py +7 -3
  44. lino/modlib/notify/mixins.py +35 -34
  45. lino/modlib/periods/choicelists.py +46 -0
  46. lino/modlib/periods/mixins.py +26 -1
  47. lino/modlib/periods/models.py +17 -45
  48. lino/modlib/printing/choicelists.py +3 -3
  49. lino/modlib/publisher/choicelists.py +1 -4
  50. lino/modlib/search/models.py +17 -11
  51. lino/modlib/system/fixtures/__init__.py +0 -0
  52. lino/modlib/system/fixtures/std.py +5 -0
  53. lino/modlib/system/models.py +3 -2
  54. lino/modlib/uploads/__init__.py +10 -1
  55. lino/modlib/uploads/choicelists.py +3 -3
  56. lino/modlib/uploads/mixins.py +49 -51
  57. lino/modlib/uploads/models.py +144 -61
  58. lino/modlib/uploads/ui.py +12 -6
  59. lino/modlib/uploads/utils.py +113 -0
  60. lino/modlib/users/mixins.py +9 -6
  61. lino/modlib/weasyprint/choicelists.py +17 -7
  62. lino/utils/__init__.py +6 -0
  63. lino/utils/choosers.py +21 -8
  64. lino/utils/html.py +1 -1
  65. lino/utils/instantiator.py +9 -0
  66. lino/utils/media.py +2 -3
  67. lino/utils/soup.py +311 -0
  68. {lino-24.11.0.dist-info → lino-25.1.0.dist-info}/METADATA +2 -2
  69. {lino-24.11.0.dist-info → lino-25.1.0.dist-info}/RECORD +72 -67
  70. {lino-24.11.0.dist-info → lino-25.1.0.dist-info}/WHEEL +1 -1
  71. {lino-24.11.0.dist-info → lino-25.1.0.dist-info}/licenses/AUTHORS.rst +0 -0
  72. {lino-24.11.0.dist-info → lino-25.1.0.dist-info}/licenses/COPYING +0 -0
@@ -2,8 +2,6 @@
2
2
  # Copyright 2016-2024 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
- from bs4 import BeautifulSoup, NavigableString
6
- from bs4.element import Tag
7
5
  from lxml.html import fragments_fromstring
8
6
  from lino.utils.html import E, tostring, mark_safe
9
7
  import lxml
@@ -22,212 +20,24 @@ from lino.core.gfks import gfk2lookup
22
20
  from lino.core.model import Model
23
21
  from lino.core.fields import fields_list, RichTextField, PreviewTextField
24
22
  from lino.utils.restify import restify
23
+ from lino.utils.soup import truncate_comment
25
24
  from lino.utils.mldbc.fields import BabelTextField
26
25
  from lino.core.exceptions import ChangedAPI
27
26
  from lino.modlib.checkdata.choicelists import Checker
28
27
  from lino.api import rt, dd, _
29
28
 
30
29
 
31
- def old_truncate_comment(html_str, max_p_len=None):
32
- # returns a single paragraph with a maximum number of visible chars.
33
- # No longer used. Replaced by new truncate_comment() below
34
- if max_p_len is None:
35
- max_p_len = settings.SITE.plugins.memo.short_preview_length
36
- html_str = html_str.strip() # remove leading or trailing newlines
37
-
38
- if not html_str.startswith("<"):
39
- if len(html_str) > max_p_len:
40
- txt = html_str[:max_p_len] + "..."
41
- else:
42
- txt = html_str
43
- return txt
44
- soup = BeautifulSoup(html_str, "html.parser")
45
- ps = soup.find_all(
46
- ["p", "h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "h9", "pre"]
47
- )
48
- if len(ps) > 0:
49
- anchor_end = "</a>"
50
- txt = ""
51
- for p in ps:
52
- text = ""
53
- for c in p.contents:
54
- if isinstance(c, Tag):
55
- if c.name == "a":
56
- text += str(c)
57
- max_p_len = max_p_len + len(text) - len(c.text)
58
- else:
59
- # text += str(c)
60
- text += c.text
61
- else:
62
- text += str(c)
63
-
64
- if len(txt) + len(text) > max_p_len:
65
- txt += text
66
- if anchor_end in txt:
67
- ae_index = txt.index(anchor_end) + len(anchor_end)
68
- if ae_index >= max_p_len:
69
- txt = txt[:ae_index]
70
- txt += "..."
71
- break
72
- txt = txt[:max_p_len]
73
- txt += "..."
74
- break
75
- else:
76
- txt += text + "\n\n"
77
- return txt
78
- return html_str
79
-
80
-
81
30
  def django_truncate_comment(html_str):
82
31
  # works, but we don't use it because (...)
83
32
  return Truncator(html_str).chars(
84
33
  settings.SITE.plugins.memo.short_preview_length, html=True
85
34
  )
86
35
 
87
-
88
- PARAGRAPH_TAGS = {
89
- "p",
90
- "h1",
91
- "h2",
92
- "h3",
93
- "h4",
94
- "h5",
95
- "h6",
96
- "h7",
97
- "h8",
98
- "h9",
99
- "pre",
100
- "li",
101
- "div",
102
- }
103
- WHITESPACE_TAGS = PARAGRAPH_TAGS | {
104
- "[document]",
105
- "span",
106
- "ul",
107
- "html",
108
- "head",
109
- "body",
110
- "base",
111
- }
112
-
113
-
114
36
  MARKDOWNCFG = dict(
115
37
  extensions=["toc"], extension_configs=dict(toc=dict(toc_depth=3, permalink=True))
116
38
  )
117
39
 
118
40
 
119
- class Style:
120
- # TODO: Extend rstgen.sphinxconf.sigal_image.Format to incoroporate this.
121
- def __init__(self, s):
122
- self._map = {}
123
- if s:
124
- for i in s.split(";"):
125
- k, v = i.split(":", maxsplit=1)
126
- self._map[k.strip()] = v.strip()
127
- self.is_dirty = False
128
-
129
- def __contains__(self, *args):
130
- return self._map.__contains__(*args)
131
-
132
- def __setitem__(self, k, v):
133
- if k in self._map and self._map[k] == v:
134
- return
135
- self._map[k] = v
136
- self.is_dirty = True
137
-
138
- def __delitem__(self, k):
139
- if k in self._map:
140
- self.is_dirty = True
141
- return self._map.__delitem__(k)
142
-
143
- def adjust_size(self):
144
- # if self['float'] == "none":
145
- # return
146
- if "width" in self._map:
147
- del self["width"]
148
- self["height"] = dd.plugins.memo.short_preview_image_height
149
-
150
- def as_string(self):
151
- return ";".join(["{}:{}".format(*kv) for kv in self._map.items()])
152
-
153
-
154
- class TextCollector:
155
- def __init__(self, max_length=None):
156
- self.text = ""
157
- self.sep = "" # becomes "\n\n" after a PARAGRAPH_TAGS
158
- self.remaining = max_length or settings.SITE.plugins.memo.short_preview_length
159
- self.image = None
160
-
161
- def add_chunk(self, ch):
162
- # print("20230712 add_chunk", ch.name, ch)
163
-
164
- if ch.name in WHITESPACE_TAGS:
165
- for c in ch.children:
166
- if not self.add_chunk(c):
167
- return False
168
- if ch.name in PARAGRAPH_TAGS:
169
- self.sep = "\n\n"
170
- else:
171
- self.sep = " "
172
- return True
173
-
174
- assert ch.name != "IMG"
175
-
176
- if ch.name == "img":
177
- if self.image is not None:
178
- # Ignore all images except the first one.
179
- self.text += self.sep
180
- return True
181
- style = Style(ch.get("style", None))
182
- if not "float" in style:
183
- style["float"] = "right"
184
- style.adjust_size()
185
- if style.is_dirty:
186
- ch["style"] = style.as_string()
187
- self.image = ch
188
- # print("20231023 a", ch)
189
-
190
- we_want_more = True
191
- if ch.string is not None:
192
- if len(ch.string) > self.remaining:
193
- # print("20231023", len(ch.string), '>', self.remaining)
194
- ch.string = ch.string[: self.remaining] + "..."
195
- we_want_more = False
196
- # print("20230927", ch.string, ch)
197
- # self.text += str(ch.string) + "..."
198
- # return False
199
- self.remaining -= len(ch.string)
200
-
201
- if isinstance(ch, NavigableString):
202
- self.text += self.sep + ch.string
203
- else:
204
- self.text += self.sep + str(ch)
205
-
206
- self.remaining -= len(self.sep)
207
- self.sep = ""
208
- return we_want_more
209
-
210
-
211
- def truncate_comment(html_str, max_length=300):
212
- # new implementation since 20230713
213
- html_str = html_str.strip() # remove leading or trailing newlines
214
-
215
- if not html_str.startswith("<"):
216
- # print("20231023 c", html_str)
217
- if len(html_str) > max_length:
218
- return html_str[:max_length] + "..."
219
- return html_str
220
-
221
- # if "choose one or the other" in html_str:
222
- # print(html_str)
223
- # raise Exception("20230928 {} {}".format(len(html_str), max_length))
224
-
225
- soup = BeautifulSoup(html_str, "html.parser")
226
- tc = TextCollector(max_length)
227
- tc.add_chunk(soup)
228
- return tc.text
229
-
230
-
231
41
  def rich_text_to_elems(ar, description):
232
42
  if description.startswith("<"):
233
43
  # desc = E.raw('<div>%s</div>' % self.description)
@@ -267,21 +77,16 @@ class MemoReferrable(dd.Model):
267
77
 
268
78
  memo_command = None
269
79
 
270
- @classmethod
271
- def on_analyze(cls, site):
272
- super().on_analyze(site)
80
+ if dd.is_installed("memo"):
273
81
 
274
- if cls.memo_command is None or not site.is_installed("memo"):
275
- return
276
-
277
- mp = site.plugins.memo.parser
278
- mp.register_django_model(cls.memo_command, cls)
279
- # mp.add_suggester("[" + cls.memo_command + " ", cls.objects.all(), 'pk')
280
-
281
- # def get_memo_title(self):
282
- # """A text to be used as title of the ``<a href>``."""
283
- # return None
284
- # return str(self)
82
+ @classmethod
83
+ def on_analyze(cls, site):
84
+ super().on_analyze(site)
85
+ if cls.memo_command is None:
86
+ return
87
+ mp = site.plugins.memo.parser
88
+ mp.register_django_model(cls.memo_command, cls)
89
+ # mp.add_suggester("[" + cls.memo_command + " ", cls.objects.all(), 'pk')
285
90
 
286
91
  def memo2html(self, ar, txt, **kwargs):
287
92
  if txt:
@@ -290,9 +95,6 @@ class MemoReferrable(dd.Model):
290
95
  return tostring(e)
291
96
  # return ar.obj2str(self, **kwargs)
292
97
 
293
- # return "<p>Oops, undefined memo2html()</p>"
294
-
295
- # def obj2memo(self, title=str):
296
98
  def obj2memo(self, text=None):
297
99
  """Render the given database object as memo markup."""
298
100
  if self.memo_command is None:
@@ -369,24 +171,24 @@ class BasePreviewable(dd.Model):
369
171
  )
370
172
  short = truncate_comment(full, self.get_preview_length())
371
173
  if not full.startswith("<"):
372
- if markdown is not None:
174
+ if dd.get_plugin_setting("memo", "use_markup"):
373
175
  full = markdown.markdown(full, **MARKDOWNCFG)
374
176
  return (short, full)
375
177
 
376
178
  def get_saved_mentions(self):
377
179
  Mention = rt.models.memo.Mention
378
180
  flt = gfk2lookup(Mention.owner, self)
379
- return Mention.objects.filter(**flt).order_by("source_type", "source_id")
181
+ return Mention.objects.filter(**flt).order_by("target_type", "target_id")
380
182
 
381
183
  def synchronize_mentions(self, mentions):
382
184
  Mention = rt.models.memo.Mention
383
185
  for obj in self.get_saved_mentions():
384
- if obj.source in mentions:
385
- mentions.remove(obj.source)
186
+ if obj.target in mentions:
187
+ mentions.remove(obj.target)
386
188
  else:
387
189
  obj.delete()
388
- for source in mentions:
389
- obj = Mention(owner=self, source=source)
190
+ for target in mentions:
191
+ obj = Mention(owner=self, target=target)
390
192
  # source_id=source.pk,
391
193
  # source_type=ContentType.objects.get_for_model(source.__class__))
392
194
  obj.full_clean()
@@ -472,7 +274,7 @@ class PreviewableChecker(Checker):
472
274
  ):
473
275
  yield (True, _("Preview differs from source."))
474
276
  is_broken = True
475
- found_mentions = set([obj.source for obj in obj.get_saved_mentions()])
277
+ found_mentions = set([obj.target for obj in obj.get_saved_mentions()])
476
278
  if expected_mentions != found_mentions:
477
279
  yield (True, _("Mentions differ from expected mentions."))
478
280
  is_broken = True
@@ -9,6 +9,7 @@ from django.utils.text import format_lazy
9
9
 
10
10
  # from rstgen.sphinxconf.sigal_image import line2html
11
11
  from lino.api import dd, rt, _
12
+ from lino.core import constants
12
13
  from lino.core.roles import SiteStaff
13
14
  from lino.core.gfks import gfk2lookup
14
15
  from lino.modlib.gfks.mixins import Controllable
@@ -17,7 +18,7 @@ from .parser import split_name_rest
17
18
  # from .mixins import *
18
19
 
19
20
  # Translators: will also be concatenated with '(type)' '(object)'
20
- source_label = _("Source")
21
+ target_label = _("Target")
21
22
 
22
23
 
23
24
  class Mention(Controllable):
@@ -27,36 +28,64 @@ class Mention(Controllable):
27
28
  verbose_name = _("Mention")
28
29
  verbose_name_plural = _("Mentions")
29
30
 
30
- source_type = dd.ForeignKey(
31
+ target_type = dd.ForeignKey(
31
32
  ContentType,
32
33
  editable=True,
33
34
  blank=True,
34
35
  null=True,
35
- related_name="%(app_label)s_%(class)s_source_set",
36
- verbose_name=format_lazy("{} {}", source_label, _("(type)")),
36
+ related_name="%(app_label)s_%(class)s_target_set",
37
+ verbose_name=format_lazy("{} {}", target_label, _("(type)")),
37
38
  )
38
39
 
39
- source_id = GenericForeignKeyIdField(
40
- source_type,
40
+ target_id = GenericForeignKeyIdField(
41
+ target_type,
41
42
  editable=True,
42
43
  blank=True,
43
44
  null=True,
44
- verbose_name=format_lazy("{} {}", source_label, _("(object)")),
45
+ verbose_name=format_lazy("{} {}", target_label, _("(object)")),
45
46
  )
46
47
 
47
- source = GenericForeignKey("source_type", "source_id", verbose_name=source_label)
48
+ target = GenericForeignKey("target_type", "target_id", verbose_name=target_label)
48
49
 
50
+ @classmethod
51
+ def get_simple_parameters(cls):
52
+ for p in super().get_simple_parameters():
53
+ yield p
54
+ yield "target_type"
55
+ yield "target_id"
56
+
57
+ def as_summary_item(self, ar, text=None, **kwargs):
58
+ # raise Exception("20240613")
59
+ if ar is None:
60
+ obj = super()
61
+ elif ar.is_obvious_field('target'):
62
+ obj = self.owner
63
+ elif ar.is_obvious_field('owner'):
64
+ obj = self.target
65
+ else:
66
+ obj = super()
67
+ return obj.as_summary_item(ar, text, **kwargs)
68
+
69
+ dd.update_field(Mention, 'owner', verbose_name=_("Referrer"))
49
70
 
50
71
  class Mentions(dd.Table):
51
72
  required_roles = dd.login_required(SiteStaff)
52
73
  editable = False
53
74
  model = "memo.Mention"
54
- column_names = "source owner *"
75
+ column_names = "owner target *"
55
76
  # detail_layout = """
56
77
  # id comment owner created
57
78
  # """
58
79
 
80
+ # Not used because when you are on the owner, you can see the mentions in the memo text
81
+ # class MentionsByOwner(Mentions):
82
+ # label = _("Mentions")
83
+ # master_key = "owner"
84
+ # column_names = "target *"
85
+ # default_display_modes = {None: constants.DISPLAY_MODE_SUMMARY}
59
86
 
60
- class MentionsByOwner(Mentions):
61
- master_key = "owner"
62
- column_names = "source *"
87
+ class MentionsByTarget(Mentions):
88
+ label = _("Mentioned by")
89
+ master_key = "target"
90
+ column_names = "owner *"
91
+ default_display_modes = {None: constants.DISPLAY_MODE_SUMMARY}
@@ -159,7 +159,7 @@ class Parser:
159
159
  )
160
160
  self.commands[cmdname] = func
161
161
 
162
- def register_django_model(self, name, model, cmd=None):
162
+ def register_django_model(self, name, model, cmd=None, rnd=None):
163
163
  """
164
164
  Register the given string `name` as command for referring to
165
165
  database rows of the given Django database model `model`.
@@ -172,6 +172,8 @@ class Parser:
172
172
  # if rnd is None:
173
173
  # def rnd(obj):
174
174
  # return "[{} {}] ({})".format(name, obj.id, title(obj))
175
+ if rnd is None:
176
+ rnd = model.memo2html
175
177
  if cmd is None:
176
178
 
177
179
  def cmd(ar, s, cmdname, mentions, context):
@@ -200,7 +202,8 @@ class Parser:
200
202
  # caption = obj.get_memo_title()
201
203
  # txt = "#{0}".format(obj.id)
202
204
  # kw.update(title=title(obj))
203
- return obj.memo2html(ar, text)
205
+ # return obj.memo2html(ar, text)
206
+ return rnd(obj, ar, text)
204
207
  # e = ar.obj2html(obj, txt, **kw)
205
208
  # # return str(ar)
206
209
  # return etree.tostring(e)
@@ -210,7 +213,8 @@ class Parser:
210
213
  # pass
211
214
 
212
215
  cmd._for_model = model
213
- cmd.__doc__ = """
216
+ if cmd.__doc__ is None:
217
+ cmd.__doc__ = rnd.__doc__ or """
214
218
  Insert a reference to the specified {}.
215
219
 
216
220
  The first argument is mandatory and specifies the primary key.
@@ -1,53 +1,54 @@
1
- # Copyright 2016-2022 Rumma & Ko Ltd
1
+ # Copyright 2016-2024 Rumma & Ko Ltd
2
2
  # License: GNU Affero General Public License v3 (see file COPYING for details)
3
3
 
4
- from lino.utils.html import E, tostring
4
+ from lino.utils.html import E, tostring, format_html, mark_safe
5
5
  from lino.api import dd, rt, _
6
6
 
7
7
  # PUBLIC_GROUP = "all_users_channel"
8
8
 
9
9
 
10
10
  class ChangeNotifier(dd.Model):
11
- class Meta(object):
12
- abstract = True
13
11
 
14
- if dd.is_installed("notify"):
12
+ class Meta:
13
+ abstract = True
15
14
 
16
- def get_change_subject(self, ar, cw):
17
- ctx = dict(user=ar.user, what=str(self))
18
- if cw is None:
19
- return _("{user} created {what}").format(**ctx)
20
- # msg = _("has been created by {user}").format(**ctx)
21
- # return "{} {}".format(self, msg)
22
- if len(list(cw.get_updates())) == 0:
23
- return
24
- return _("{user} modified {what}").format(**ctx)
25
- # msg = _("has been modified by {user}").format(**ctx)
15
+ def get_change_subject(self, ar, cw):
16
+ ctx = dict(user=ar.user, what=str(self))
17
+ if cw is None:
18
+ return _("{user} created {what}").format(**ctx)
19
+ # msg = _("has been created by {user}").format(**ctx)
26
20
  # return "{} {}".format(self, msg)
21
+ if len(list(cw.get_updates())) == 0:
22
+ return
23
+ return _("{user} modified {what}").format(**ctx)
24
+ # msg = _("has been modified by {user}").format(**ctx)
25
+ # return "{} {}".format(self, msg)
26
+
27
+ def get_change_body(self, ar, cw):
28
+ ctx = dict(user=ar.user, what=ar.obj2htmls(self))
29
+ if cw is None:
30
+ html = format_html(_("{user} created {what}"), **ctx)
31
+ html += self.get_change_info(ar, cw)
32
+ html = format_html("<p>{}</p>.", html)
33
+ else:
34
+ items = list(cw.get_updates_html(["_user_cache"]))
35
+ if len(items) == 0:
36
+ return
37
+ txt = format_html(_("{user} modified {what}"), **ctx)
38
+ html = format_html("<p>{}:</p>", txt)
39
+ html += tostring(E.ul(*items))
40
+ html += self.get_change_info(ar, cw)
41
+ return format_html("<div>{}</div>", html)
42
+
43
+ def get_change_info(self, ar, cw):
44
+ return mark_safe("")
45
+
46
+ if dd.is_installed("notify"):
27
47
 
28
48
  def add_change_watcher(self, user):
29
49
  pass
30
50
  # raise NotImplementedError()
31
51
 
32
- def get_change_body(self, ar, cw):
33
- ctx = dict(user=ar.user, what=ar.obj2htmls(self))
34
- if cw is None:
35
- html = _("{user} created {what}").format(**ctx)
36
- html += self.get_change_info(ar, cw)
37
- html = "<p>{}</p>.".format(html)
38
- else:
39
- items = list(cw.get_updates_html(["_user_cache"]))
40
- if len(items) == 0:
41
- return
42
- html = _("{user} modified {what}").format(**ctx)
43
- html = "<p>{}:</p>".format(html)
44
- html += tostring(E.ul(*items))
45
- html += self.get_change_info(ar, cw)
46
- return "<div>{}</div>".format(html)
47
-
48
- def get_change_info(self, ar, cw):
49
- return ""
50
-
51
52
  def get_change_owner(self):
52
53
  return self
53
54
 
@@ -0,0 +1,46 @@
1
+ # -*- coding: UTF-8 -*-
2
+ # Copyright 2008-2024 Rumma & Ko Ltd
3
+ # License: GNU Affero General Public License v3 (see file COPYING for details)
4
+
5
+ from django.utils.translation import gettext_lazy as _
6
+
7
+ from lino.api import dd
8
+ from lino.utils import ONE_DAY
9
+
10
+
11
+ class PeriodType(dd.Choice):
12
+ ref_template = None
13
+
14
+ def __init__(self, value, text, duration, ref_template):
15
+ super().__init__(value, text, value)
16
+ self.ref_template = ref_template
17
+ self.duration = duration
18
+
19
+ class PeriodTypes(dd.ChoiceList):
20
+ item_class = PeriodType
21
+ verbose_name = _("Period type")
22
+ verbose_name_plural = _("Period types")
23
+ column_names = "value text duration ref_template"
24
+
25
+ @dd.displayfield(_("Duration"))
26
+ def duration(cls, p, ar):
27
+ return str(p.duration)
28
+
29
+ @dd.displayfield(_("Template for reference"))
30
+ def ref_template(cls, p, ar):
31
+ return p.ref_template
32
+
33
+ add = PeriodTypes.add_item
34
+ # value/names, text, duration, ref_template
35
+ 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}")
39
+
40
+
41
+ class PeriodStates(dd.Workflow):
42
+ pass
43
+
44
+ add = PeriodStates.add_item
45
+ add('10', _("Open"), 'open')
46
+ add('20', _("Closed"), 'closed')
@@ -2,15 +2,40 @@
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
+ import datetime
5
6
  from django.db import models
6
7
  from django.utils.translation import gettext_lazy as _
7
8
 
8
9
  from lino.api import dd, rt
9
10
  from lino import mixins
10
- from lino.mixins.periods import DateRange
11
11
  from lino.mixins import Referrable
12
+ from lino.utils import ONE_DAY
12
13
 
13
14
  from lino.modlib.office.roles import OfficeStaff
15
+ from lino.modlib.system.choicelists import DurationUnits
16
+
17
+
18
+ def get_range_for_date(date):
19
+ """
20
+ Return the default start and end date of the period to create for the given
21
+ date.
22
+ """
23
+ pt = dd.plugins.periods.period_type
24
+ month = date.month
25
+ year = date.year
26
+ month -= dd.plugins.periods.start_month
27
+ if month < 0:
28
+ month += 12
29
+ year -= 1
30
+ period = int(month / pt.duration)
31
+ month = dd.plugins.periods.start_month + period * pt.duration
32
+ if month > 12:
33
+ month -= 12
34
+ year += 1
35
+ sd = datetime.date(year, month, 1)
36
+ # ed = sd.replace(month=sd.month + pt.duration + 1, 1) - ONE_DAY
37
+ ed = DurationUnits.months.add_duration(sd, pt.duration) - ONE_DAY
38
+ return (sd, ed)
14
39
 
15
40
 
16
41
  class PeriodRange(dd.Model):