lino 24.10.3__py3-none-any.whl → 24.11.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 (80) hide show
  1. lino/__init__.py +1 -1
  2. lino/api/doctest.py +11 -10
  3. lino/api/rt.py +2 -3
  4. lino/config/admin_main_base.html +2 -2
  5. lino/core/actions.py +2 -4
  6. lino/core/actors.py +70 -35
  7. lino/core/choicelists.py +2 -2
  8. lino/core/dashboard.py +2 -1
  9. lino/core/dbtables.py +15 -15
  10. lino/core/elems.py +8 -4
  11. lino/core/fields.py +12 -3
  12. lino/core/inject.py +9 -2
  13. lino/core/kernel.py +11 -11
  14. lino/core/layouts.py +1 -1
  15. lino/core/model.py +25 -36
  16. lino/core/plugin.py +1 -0
  17. lino/core/renderer.py +21 -21
  18. lino/core/requests.py +94 -83
  19. lino/core/site.py +9 -90
  20. lino/core/store.py +16 -19
  21. lino/core/tables.py +0 -17
  22. lino/core/utils.py +32 -2
  23. lino/core/views.py +2 -1
  24. lino/help_texts.py +10 -5
  25. lino/locale/bn/LC_MESSAGES/django.po +1210 -907
  26. lino/locale/de/LC_MESSAGES/django.po +1760 -1375
  27. lino/locale/django.pot +1136 -906
  28. lino/locale/es/LC_MESSAGES/django.po +1709 -1347
  29. lino/locale/et/LC_MESSAGES/django.po +1206 -906
  30. lino/locale/fr/LC_MESSAGES/django.mo +0 -0
  31. lino/locale/fr/LC_MESSAGES/django.po +1193 -923
  32. lino/locale/nl/LC_MESSAGES/django.po +1247 -942
  33. lino/locale/pt_BR/LC_MESSAGES/django.po +1190 -903
  34. lino/locale/zh_Hant/LC_MESSAGES/django.po +1190 -903
  35. lino/management/commands/show.py +2 -4
  36. lino/mixins/periods.py +15 -7
  37. lino/mixins/polymorphic.py +3 -3
  38. lino/mixins/ref.py +6 -3
  39. lino/modlib/checkdata/__init__.py +3 -3
  40. lino/modlib/comments/choicelists.py +1 -1
  41. lino/modlib/comments/fixtures/demo2.py +4 -1
  42. lino/modlib/comments/mixins.py +9 -10
  43. lino/modlib/comments/models.py +4 -4
  44. lino/modlib/comments/ui.py +5 -0
  45. lino/modlib/extjs/ext_renderer.py +1 -1
  46. lino/modlib/linod/consumers.py +2 -3
  47. lino/modlib/linod/mixins.py +3 -2
  48. lino/modlib/memo/mixins.py +11 -209
  49. lino/modlib/notify/mixins.py +33 -32
  50. lino/modlib/periods/__init__.py +12 -1
  51. lino/modlib/periods/fixtures/std.py +2 -1
  52. lino/modlib/periods/mixins.py +0 -1
  53. lino/modlib/periods/models.py +79 -75
  54. lino/modlib/printing/actions.py +2 -0
  55. lino/modlib/printing/choicelists.py +3 -3
  56. lino/modlib/publisher/ui.py +2 -2
  57. lino/modlib/search/models.py +17 -11
  58. lino/modlib/system/__init__.py +0 -2
  59. lino/modlib/system/choicelists.py +55 -1
  60. lino/modlib/system/fixtures/__init__.py +0 -0
  61. lino/modlib/system/fixtures/std.py +5 -0
  62. lino/modlib/system/models.py +4 -2
  63. lino/modlib/uploads/__init__.py +10 -1
  64. lino/modlib/uploads/choicelists.py +3 -3
  65. lino/modlib/uploads/mixins.py +30 -32
  66. lino/modlib/uploads/models.py +89 -56
  67. lino/modlib/uploads/ui.py +12 -6
  68. lino/modlib/uploads/utils.py +107 -0
  69. lino/modlib/users/models.py +2 -2
  70. lino/modlib/weasyprint/__init__.py +2 -0
  71. lino/utils/__init__.py +14 -9
  72. lino/utils/djangotest.py +2 -1
  73. lino/utils/html.py +32 -1
  74. lino/utils/media.py +2 -3
  75. lino/utils/soup.py +311 -0
  76. {lino-24.10.3.dist-info → lino-24.11.1.dist-info}/METADATA +1 -3
  77. {lino-24.10.3.dist-info → lino-24.11.1.dist-info}/RECORD +80 -76
  78. {lino-24.10.3.dist-info → lino-24.11.1.dist-info}/WHEEL +1 -1
  79. {lino-24.10.3.dist-info → lino-24.11.1.dist-info}/licenses/AUTHORS.rst +0 -0
  80. {lino-24.10.3.dist-info → lino-24.11.1.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,32 +77,24 @@ 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:
288
93
  kwargs.update(title=txt)
289
- e = self.as_summary_item(ar, **kwargs)
94
+ e = self.as_summary_item(ar)
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:
@@ -8,46 +8,47 @@ from lino.api import dd, rt, _
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 = _("{user} created {what}").format(**ctx)
31
+ html += self.get_change_info(ar, cw)
32
+ html = "<p>{}</p>.".format(html)
33
+ else:
34
+ items = list(cw.get_updates_html(["_user_cache"]))
35
+ if len(items) == 0:
36
+ return
37
+ html = _("{user} modified {what}").format(**ctx)
38
+ html = "<p>{}:</p>".format(html)
39
+ html += tostring(E.ul(*items))
40
+ html += self.get_change_info(ar, cw)
41
+ return "<div>{}</div>".format(html)
42
+
43
+ def get_change_info(self, ar, cw):
44
+ return ""
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
 
@@ -10,11 +10,22 @@ class Plugin(ad.Plugin):
10
10
  period_name_plural = _("Accounting periods")
11
11
  year_name = _("Fiscal year")
12
12
  year_name_plural = _("Fiscal years")
13
- fix_y2k = False
14
13
  start_year = 2012
14
+ start_month = 1
15
+ period_type = "month"
16
+ fix_y2k = False
17
+ short_ref = False
15
18
 
16
19
  def setup_config_menu(self, site, user_type, m, ar=None):
17
20
  p = self.get_menu_group()
18
21
  m = m.add_menu(p.app_label, p.verbose_name)
19
22
  m.add_action("periods.StoredYears")
20
23
  m.add_action("periods.StoredPeriods")
24
+
25
+ def before_analyze(self):
26
+ if self.fix_y2k and self.start_month != 1:
27
+ raise Exception("When fix_y2k is set, start_month must be 1")
28
+ if isinstance(self.period_type, str):
29
+ self.period_type = self.site.models.periods.PeriodTypes.get_by_name(
30
+ self.period_type)
31
+ super().before_analyze()
@@ -18,5 +18,6 @@ def objects():
18
18
  raise Exception("plugins.periods.start_year is after the_demo_date")
19
19
  today = site.the_demo_date or datetime.date.today()
20
20
  for y in range(start_year, today.year + 6):
21
- yield StoredYear.create_from_year(y)
21
+ # yield StoredYear.create_from_year(y)
22
+ yield StoredYear.get_or_create_from_date(datetime.date(y, today.month, today.day))
22
23
  # StoredYears.add_item(StoredYear.year2value(y), str(y))
@@ -7,7 +7,6 @@ from django.utils.translation import gettext_lazy as _
7
7
 
8
8
  from lino.api import dd, rt
9
9
  from lino import mixins
10
- from lino.mixins.periods import DateRange
11
10
  from lino.mixins import Referrable
12
11
 
13
12
  from lino.modlib.office.roles import OfficeStaff
@@ -8,12 +8,45 @@ from django.utils.translation import gettext_lazy as _
8
8
 
9
9
  from lino.api import dd
10
10
  from lino import mixins
11
- from lino.utils import last_day_of_month
11
+ from lino.utils import last_day_of_month, ONE_DAY
12
12
  from lino.mixins.periods import DateRange
13
13
  from lino.mixins import Referrable
14
14
 
15
15
  from lino.modlib.office.roles import OfficeStaff
16
16
 
17
+ NEXT_YEAR_SEP = "/"
18
+ YEAR_PERIOD_SEP = "-"
19
+
20
+ class PeriodType(dd.Choice):
21
+ ref_template = None
22
+ ref_template = None
23
+
24
+ def __init__(self, value, text, duration, ref_template):
25
+ super().__init__(value, text, value)
26
+ self.ref_template = ref_template
27
+ self.duration = duration
28
+
29
+ class PeriodTypes(dd.ChoiceList):
30
+ item_class = PeriodType
31
+ verbose_name = _("Period type")
32
+ verbose_name_plural = _("Period types")
33
+ column_names = "value text duration ref_template"
34
+
35
+ @dd.displayfield(_("Duration"))
36
+ def duration(cls, p, ar):
37
+ return str(p.duration)
38
+
39
+ @dd.displayfield(_("Template for reference"))
40
+ def ref_template(cls, p, ar):
41
+ return p.ref_template
42
+
43
+ add = PeriodTypes.add_item
44
+ # value/names, text, duration, ref_template
45
+ add("month", _("Month"), 1, "{month:0>2}")
46
+ add("quarter", _("Quarter"), 3, "Q{period}")
47
+ add("trimester", _("Trimester"), 4, "T{period}")
48
+ add("semester", _("Semester"), 6, "S{period}")
49
+
17
50
 
18
51
  class PeriodStates(dd.Workflow):
19
52
  pass
@@ -27,8 +60,8 @@ class StoredYear(DateRange, Referrable):
27
60
 
28
61
  class Meta:
29
62
  app_label = 'periods'
30
- verbose_name = _("Fiscal year")
31
- verbose_name_plural = _("Fiscal years")
63
+ verbose_name = dd.plugins.periods.year_name
64
+ verbose_name_plural = dd.plugins.periods.year_name_plural
32
65
  ordering = ['ref']
33
66
 
34
67
  preferred_foreignkey_width = 10
@@ -41,45 +74,33 @@ class StoredYear(DateRange, Referrable):
41
74
  yield "state"
42
75
 
43
76
  @classmethod
44
- def year2ref(cls, year):
77
+ def get_ref_for_date(cls, date):
78
+ year = date.year
79
+ if date.month < dd.plugins.periods.start_month:
80
+ year -= 1
45
81
  if dd.plugins.periods.fix_y2k:
46
82
  if year < 2000:
47
83
  return str(year)[-2:]
48
84
  elif year < 3000:
49
85
  return chr(int(str(year)[-3:-1]) + 65) + str(year)[-1]
50
86
  else:
51
- raise Exception("20180827")
52
- # elif year < 2010:
53
- # return "A" + str(year)[-1]
54
- # elif year < 2020:
55
- # return "B" + str(year)[-1]
56
- # elif year < 2030:
57
- # return "C" + str(year)[-1]
58
- # else:
59
- # raise Exception(20160304)
60
- # return str(year)[2:]
61
- return str(year)
62
-
63
- @classmethod
64
- def from_int(cls, year, *args):
65
- ref = cls.year2ref(year)
66
- return cls.get_by_ref(ref, *args)
67
-
68
- @classmethod
69
- def create_from_year(cls, year):
70
- ref = cls.year2ref(year)
71
- return cls(ref=ref,
72
- start_date=datetime.date(year, 1, 1),
73
- end_date=datetime.date(year, 12, 31))
74
- # obj.full_clean()
75
- # obj.save()
76
- # return obj
87
+ raise Exception("fix_y2k not supported after 2999")
88
+ elif dd.plugins.periods.short_ref:
89
+ if dd.plugins.periods.start_month == 1:
90
+ return str(year)[-2:]
91
+ return str(year)[-2:] + NEXT_YEAR_SEP + str(year+1)[-2:]
92
+ elif dd.plugins.periods.start_month == 1:
93
+ return str(year)
94
+ return str(year) + NEXT_YEAR_SEP + str(year+1)[-2:]
77
95
 
78
96
  @classmethod
79
97
  def get_or_create_from_date(cls, date):
80
- obj = cls.from_int(date.year, None)
98
+ ref = cls.get_ref_for_date(date)
99
+ obj = cls.get_by_ref(ref, None)
81
100
  if obj is None:
82
- obj = cls.create_from_year(date.year)
101
+ sd = datetime.date(date.year, dd.plugins.periods.start_month, 1)
102
+ ed = sd.replace(year=date.year+1) - ONE_DAY
103
+ obj = cls(ref=ref, start_date=sd, end_date=ed)
83
104
  obj.full_clean()
84
105
  obj.save()
85
106
  return obj
@@ -138,43 +159,6 @@ class StoredPeriod(DateRange, Referrable):
138
159
  fkw = dict(start_date__lte=today, end_date__gte=today)
139
160
  return cls.objects.filter(**fkw)
140
161
 
141
- @classmethod
142
- def get_ref_for_date(cls, d):
143
- """Return a text to be used as :attr:`ref` for a new period.
144
-
145
- Alternative implementation for usage on a site with movements
146
- before year 2000::
147
-
148
- @classmethod
149
- def get_ref_for_date(cls, d):
150
- if d.year < 2000:
151
- y = str(d.year - 1900)
152
- elif d.year < 2010:
153
- y = "A" + str(d.year - 2000)
154
- elif d.year < 2020:
155
- y = "B" + str(d.year - 2010)
156
- elif d.year < 2030:
157
- y = "C" + str(d.year - 2020)
158
- return y + "{:0>2}".format(d.month)
159
-
160
- """
161
- y = StoredYear.year2ref(d.year)
162
- return "{}-{:0>2}".format(y, d.month)
163
-
164
- # if dd.plugins.periods.fix_y2k:
165
- # return rt.models.periods.StoredYear.from_int(d.year).ref \
166
- # + "{:0>2}".format(d.month)
167
-
168
- # return "{0.year}-{0.month:0>2}".format(d)
169
-
170
- # """The template used for building the :attr:`ref` of an
171
- # :class:`StoredPeriod`.
172
- #
173
- # `Format String Syntax
174
- # <https://docs.python.org/2/library/string.html#formatstrings>`_
175
- #
176
- # """
177
-
178
162
  @classmethod
179
163
  def get_periods_in_range(cls, p1, p2):
180
164
  return cls.objects.filter(ref__gte=p1.ref, ref__lte=p2.ref)
@@ -197,14 +181,26 @@ class StoredPeriod(DateRange, Referrable):
197
181
  return kwargs
198
182
 
199
183
  @classmethod
200
- def get_default_for_date(cls, d):
201
- ref = cls.get_ref_for_date(d)
184
+ def get_ref_for_date(cls, date):
185
+ pt = dd.plugins.periods.period_type
186
+ month = date.month
187
+ month_offset = month - dd.plugins.periods.start_month
188
+ if month_offset < 0:
189
+ month_offset += 12
190
+ period = int(month_offset / pt.duration) + 1
191
+ # periods_per_year = int(12 / p.duration)
192
+ # period = (month_offset % (periods_per_year-1)) + 1
193
+ return pt.ref_template.format(**locals())
194
+
195
+ @classmethod
196
+ def get_or_create_from_date(cls, date): # get_default_for_date until 20241020
197
+ ref = date2ref(date)
202
198
  obj = cls.get_by_ref(ref, None)
203
199
  if obj is None:
204
- values = dict(start_date=d.replace(day=1))
205
- values.update(end_date=last_day_of_month(d))
206
- values.update(ref=ref)
207
- obj = StoredPeriod(**values)
200
+ obj = cls(
201
+ ref=ref,
202
+ start_date=date.replace(day=1),
203
+ end_date=last_day_of_month(date))
208
204
  obj.full_clean()
209
205
  obj.save()
210
206
  return obj
@@ -222,9 +218,17 @@ class StoredPeriod(DateRange, Referrable):
222
218
  # "{0} {1} (#{0})".format(self.pk, self.year)
223
219
  return self.ref
224
220
 
221
+ @property
222
+ def nickname(self):
223
+ if self.year.covers_date(dd.today()):
224
+ if len(parts := self.ref.split(YEAR_PERIOD_SEP)) == 2:
225
+ return parts[1]
226
+ return self.ref
225
227
 
226
228
  StoredPeriod.set_widget_options('ref', width=6)
227
229
 
230
+ def date2ref(d):
231
+ return StoredYear.get_ref_for_date(d) + YEAR_PERIOD_SEP + StoredPeriod.get_ref_for_date(d)
228
232
 
229
233
  class StoredYears(dd.Table):
230
234
  model = 'periods.StoredYear'
@@ -119,6 +119,7 @@ class BasePrintAction(Action):
119
119
  class DirectPrintAction(BasePrintAction):
120
120
  url_action_name = None
121
121
  icon_name = "printer"
122
+ # button_text = "🖶" # 1F5B6
122
123
  tplname = None
123
124
 
124
125
  def __init__(self, label=None, tplname=None, build_method=None, **kw):
@@ -162,6 +163,7 @@ class CachedPrintAction(BasePrintAction):
162
163
  # select_rows = False
163
164
  http_method = "POST"
164
165
  icon_name = "printer"
166
+ # button_text = "🖶" # 1F5B6
165
167
 
166
168
  def before_build(self, bm, elem):
167
169
  if elem.build_time:
@@ -46,14 +46,14 @@ class BuildMethod(Choice):
46
46
  names, self.__class__.__name__, names, **kwargs
47
47
  )
48
48
 
49
- def get_target(self, action, elem):
50
- "used by `get_target_name`"
49
+ def get_target(self, action, obj):
50
+ # Used by get_target_name()
51
51
  # assert self.name is not None
52
52
  return MediaFile(
53
53
  self.use_webdav,
54
54
  self.cache_name,
55
55
  self.value,
56
- elem.filename_root() + self.target_ext,
56
+ obj.filename_root() + self.target_ext,
57
57
  )
58
58
 
59
59
  def get_target_name(self, action, elem):
@@ -172,6 +172,6 @@ class TranslationsByPage(Pages):
172
172
  default_display_modes = {None: constants.DISPLAY_MODE_SUMMARY}
173
173
 
174
174
  @classmethod
175
- def row_as_summary(cls, ar, obj, **kwargs):
175
+ def row_as_summary(cls, ar, obj, text=None, **kwargs):
176
176
  # return format_html("({}) {}", obj.language, obj.as_summary_row(ar, **kwargs))
177
- return E.span("({}) ".format(obj.language), obj.as_summary_item(ar, **kwargs))
177
+ return E.span("({}) ".format(obj.language), obj.as_summary_item(ar, text, **kwargs))