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.
- lino/__init__.py +1 -1
- lino/core/actions.py +2 -4
- lino/core/actors.py +35 -14
- lino/core/dbtables.py +12 -12
- lino/core/fields.py +27 -18
- lino/core/inject.py +9 -2
- lino/core/kernel.py +11 -12
- lino/core/model.py +25 -39
- lino/core/renderer.py +3 -4
- lino/core/requests.py +91 -92
- lino/core/site.py +5 -46
- lino/core/store.py +4 -4
- lino/core/tables.py +0 -16
- lino/core/utils.py +5 -2
- lino/help_texts.py +7 -5
- lino/locale/bn/LC_MESSAGES/django.po +1225 -909
- lino/locale/de/LC_MESSAGES/django.mo +0 -0
- lino/locale/de/LC_MESSAGES/django.po +1748 -1392
- lino/locale/django.pot +1150 -909
- lino/locale/es/LC_MESSAGES/django.po +1721 -1347
- lino/locale/et/LC_MESSAGES/django.po +1267 -954
- lino/locale/fr/LC_MESSAGES/django.mo +0 -0
- lino/locale/fr/LC_MESSAGES/django.po +1207 -925
- lino/locale/nl/LC_MESSAGES/django.po +1261 -942
- lino/locale/pt_BR/LC_MESSAGES/django.po +1204 -906
- lino/locale/zh_Hant/LC_MESSAGES/django.po +1204 -906
- lino/mixins/dupable.py +8 -7
- lino/mixins/periods.py +10 -8
- lino/modlib/checkdata/__init__.py +1 -1
- lino/modlib/comments/choicelists.py +1 -1
- lino/modlib/comments/fixtures/demo2.py +4 -1
- lino/modlib/comments/mixins.py +9 -10
- lino/modlib/comments/models.py +4 -4
- lino/modlib/comments/ui.py +11 -2
- lino/modlib/dupable/models.py +10 -14
- lino/modlib/gfks/mixins.py +8 -1
- lino/modlib/jinja/choicelists.py +3 -3
- lino/modlib/jinja/renderer.py +2 -0
- lino/modlib/languages/fixtures/all_languages.py +4 -6
- lino/modlib/linod/mixins.py +3 -2
- lino/modlib/memo/mixins.py +17 -215
- lino/modlib/memo/models.py +41 -12
- lino/modlib/memo/parser.py +7 -3
- lino/modlib/notify/mixins.py +35 -34
- lino/modlib/periods/choicelists.py +46 -0
- lino/modlib/periods/mixins.py +26 -1
- lino/modlib/periods/models.py +17 -45
- lino/modlib/printing/choicelists.py +3 -3
- lino/modlib/publisher/choicelists.py +1 -4
- lino/modlib/search/models.py +17 -11
- lino/modlib/system/fixtures/__init__.py +0 -0
- lino/modlib/system/fixtures/std.py +5 -0
- lino/modlib/system/models.py +3 -2
- lino/modlib/uploads/__init__.py +10 -1
- lino/modlib/uploads/choicelists.py +3 -3
- lino/modlib/uploads/mixins.py +49 -51
- lino/modlib/uploads/models.py +144 -61
- lino/modlib/uploads/ui.py +12 -6
- lino/modlib/uploads/utils.py +113 -0
- lino/modlib/users/mixins.py +9 -6
- lino/modlib/weasyprint/choicelists.py +17 -7
- lino/utils/__init__.py +6 -0
- lino/utils/choosers.py +21 -8
- lino/utils/html.py +1 -1
- lino/utils/instantiator.py +9 -0
- lino/utils/media.py +2 -3
- lino/utils/soup.py +311 -0
- {lino-24.11.0.dist-info → lino-25.1.0.dist-info}/METADATA +2 -2
- {lino-24.11.0.dist-info → lino-25.1.0.dist-info}/RECORD +72 -67
- {lino-24.11.0.dist-info → lino-25.1.0.dist-info}/WHEEL +1 -1
- {lino-24.11.0.dist-info → lino-25.1.0.dist-info}/licenses/AUTHORS.rst +0 -0
- {lino-24.11.0.dist-info → lino-25.1.0.dist-info}/licenses/COPYING +0 -0
lino/modlib/memo/mixins.py
CHANGED
@@ -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
|
-
|
271
|
-
def on_analyze(cls, site):
|
272
|
-
super().on_analyze(site)
|
80
|
+
if dd.is_installed("memo"):
|
273
81
|
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
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
|
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("
|
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.
|
385
|
-
mentions.remove(obj.
|
186
|
+
if obj.target in mentions:
|
187
|
+
mentions.remove(obj.target)
|
386
188
|
else:
|
387
189
|
obj.delete()
|
388
|
-
for
|
389
|
-
obj = Mention(owner=self,
|
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.
|
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
|
lino/modlib/memo/models.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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)
|
36
|
-
verbose_name=format_lazy("{} {}",
|
36
|
+
related_name="%(app_label)s_%(class)s_target_set",
|
37
|
+
verbose_name=format_lazy("{} {}", target_label, _("(type)")),
|
37
38
|
)
|
38
39
|
|
39
|
-
|
40
|
-
|
40
|
+
target_id = GenericForeignKeyIdField(
|
41
|
+
target_type,
|
41
42
|
editable=True,
|
42
43
|
blank=True,
|
43
44
|
null=True,
|
44
|
-
verbose_name=format_lazy("{} {}",
|
45
|
+
verbose_name=format_lazy("{} {}", target_label, _("(object)")),
|
45
46
|
)
|
46
47
|
|
47
|
-
|
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 = "
|
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
|
61
|
-
|
62
|
-
|
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}
|
lino/modlib/memo/parser.py
CHANGED
@@ -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.
|
lino/modlib/notify/mixins.py
CHANGED
@@ -1,53 +1,54 @@
|
|
1
|
-
# Copyright 2016-
|
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
|
-
|
12
|
+
class Meta:
|
13
|
+
abstract = True
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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')
|
lino/modlib/periods/mixins.py
CHANGED
@@ -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):
|