lino 24.10.3__py3-none-any.whl → 24.11.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 (48) 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/actors.py +68 -33
  6. lino/core/choicelists.py +2 -2
  7. lino/core/dashboard.py +2 -1
  8. lino/core/dbtables.py +3 -3
  9. lino/core/elems.py +8 -4
  10. lino/core/fields.py +11 -3
  11. lino/core/kernel.py +4 -5
  12. lino/core/layouts.py +1 -1
  13. lino/core/model.py +1 -9
  14. lino/core/plugin.py +1 -0
  15. lino/core/renderer.py +19 -19
  16. lino/core/requests.py +73 -38
  17. lino/core/site.py +5 -45
  18. lino/core/store.py +12 -15
  19. lino/core/tables.py +0 -17
  20. lino/core/utils.py +28 -1
  21. lino/core/views.py +2 -1
  22. lino/help_texts.py +6 -2
  23. lino/management/commands/show.py +2 -4
  24. lino/mixins/periods.py +9 -3
  25. lino/mixins/polymorphic.py +3 -3
  26. lino/mixins/ref.py +6 -3
  27. lino/modlib/checkdata/__init__.py +3 -3
  28. lino/modlib/extjs/ext_renderer.py +1 -1
  29. lino/modlib/linod/consumers.py +2 -3
  30. lino/modlib/memo/mixins.py +1 -1
  31. lino/modlib/periods/__init__.py +12 -1
  32. lino/modlib/periods/fixtures/std.py +2 -1
  33. lino/modlib/periods/models.py +79 -75
  34. lino/modlib/printing/actions.py +2 -0
  35. lino/modlib/publisher/ui.py +2 -2
  36. lino/modlib/system/__init__.py +0 -2
  37. lino/modlib/system/choicelists.py +55 -1
  38. lino/modlib/system/models.py +1 -0
  39. lino/modlib/users/models.py +2 -2
  40. lino/modlib/weasyprint/__init__.py +2 -0
  41. lino/utils/__init__.py +8 -9
  42. lino/utils/djangotest.py +2 -1
  43. lino/utils/html.py +31 -0
  44. {lino-24.10.3.dist-info → lino-24.11.0.dist-info}/METADATA +1 -1
  45. {lino-24.10.3.dist-info → lino-24.11.0.dist-info}/RECORD +48 -48
  46. {lino-24.10.3.dist-info → lino-24.11.0.dist-info}/WHEEL +0 -0
  47. {lino-24.10.3.dist-info → lino-24.11.0.dist-info}/licenses/AUTHORS.rst +0 -0
  48. {lino-24.10.3.dist-info → lino-24.11.0.dist-info}/licenses/COPYING +0 -0
@@ -20,7 +20,8 @@ class Plugin(ad.Plugin):
20
20
  "The config descriptor for this plugin."
21
21
 
22
22
  verbose_name = _("Checkdata")
23
- needs_plugins = ["lino.modlib.users", "lino.modlib.gfks", "lino.modlib.linod"]
23
+ needs_plugins = ["lino.modlib.users", "lino.modlib.gfks",
24
+ "lino.modlib.office", "lino.modlib.linod"]
24
25
 
25
26
  # plugin settings
26
27
  responsible_user = None # the username (a string)
@@ -50,8 +51,7 @@ class Plugin(ad.Plugin):
50
51
  User = self.site.models.users.User
51
52
  try:
52
53
  self._responsible_user = User.objects.get(
53
- username=self.responsible_user
54
- )
54
+ username=self.responsible_user)
55
55
  except User.DoesNotExist:
56
56
  msg = "Invalid username '{0}' in `responsible_user` "
57
57
  msg = msg.format(self.responsible_user)
@@ -986,7 +986,7 @@ class ExtRenderer(JsCacheRenderer):
986
986
  tbl = dh.layout._datasource
987
987
  yield ""
988
988
  yield "Lino.%s = Ext.extend(Lino.ActionFormPanel,{" % dh.layout._formpanel_name
989
- for k, v in list(dh.main.ext_options().items()):
989
+ for k, v in dh.main.ext_options().items():
990
990
  if k != "items":
991
991
  yield " %s: %s," % (k, py2js(v))
992
992
  assert tbl.action_name is not None
@@ -57,9 +57,8 @@ class LinodConsumer(AsyncConsumer):
57
57
  async def run_background_tasks(self, event: dict):
58
58
  # 'run.background.tasks' in `pm linod`
59
59
  from lino.modlib.linod.mixins import start_task_runner
60
-
61
- # start_task_runner = settings.SITE.models.linod.start_task_runner
62
- ar = settings.SITE.login()
60
+ from lino.core.requests import BaseRequest
61
+ ar = BaseRequest()
63
62
  asyncio.ensure_future(start_task_runner(ar))
64
63
 
65
64
  async def send_push(self, event):
@@ -286,7 +286,7 @@ class MemoReferrable(dd.Model):
286
286
  def memo2html(self, ar, txt, **kwargs):
287
287
  if txt:
288
288
  kwargs.update(title=txt)
289
- e = self.as_summary_item(ar, **kwargs)
289
+ e = self.as_summary_item(ar)
290
290
  return tostring(e)
291
291
  # return ar.obj2str(self, **kwargs)
292
292
 
@@ -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))
@@ -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:
@@ -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))
@@ -14,9 +14,7 @@ class Plugin(ad.Plugin):
14
14
  "See :doc:`/dev/plugins`."
15
15
 
16
16
  verbose_name = _("System")
17
-
18
17
  needs_plugins = ["lino.modlib.printing"]
19
-
20
18
  use_dashboard_layouts = False
21
19
  """Whether to use system.DashboardLayouts. This feature is broken.
22
20
  """
@@ -1,5 +1,5 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2011-2023 Rumma & Ko Ltd
2
+ # Copyright 2011-2024 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
5
  import datetime
@@ -12,6 +12,7 @@ from django.utils.translation import gettext_lazy as _
12
12
 
13
13
  from lino.utils import isidentifier
14
14
  from lino.core.choicelists import ChoiceList, Choice
15
+ from lino.core.roles import login_required, Explorer
15
16
  from lino.utils.dates import DateRangeValue
16
17
  from lino.utils.format_date import day_and_month, fds
17
18
 
@@ -229,3 +230,56 @@ add("M", _("monthly"), "monthly", du_freq=MONTHLY)
229
230
  add("Y", _("yearly"), "yearly", du_freq=YEARLY)
230
231
  add("P", _("per weekday"), "per_weekday") # deprecated
231
232
  add("E", _("Relative to Easter"), "easter")
233
+
234
+
235
+
236
+ class DisplayColor(Choice):
237
+ font_color = None
238
+ def __init__(self, value, text, names, font_color="white"):
239
+ super().__init__(value, text, names)
240
+ self.font_color = font_color
241
+
242
+
243
+ class DisplayColors(ChoiceList):
244
+ verbose_name = _("Display color")
245
+ verbose_name_plural = _("Display colors")
246
+ item_class = DisplayColor
247
+ required_roles = login_required(Explorer)
248
+
249
+
250
+ add = DisplayColors.add_item
251
+ # cssColors = 'White Silver Gray Black Red Maroon Yellow Olive Lime Green Aqua Teal Blue Navy Fuchsia Purple'
252
+ # cssColors = 'white silver gray black red maroon yellow olive lime green aqua teal blue navy fuchsia purple'
253
+ # for color in cssColors.split():
254
+ # add(color, _(color), color, font_color="white")
255
+ #
256
+ # lightColors = 'White Silver Gray'
257
+ # # lightColors = 'white silver gray'
258
+ # for color in lightColors.split():
259
+ # DisplayColors.get_by_value(color).font_color = "black"
260
+
261
+ # B&W
262
+ add("100", _("White"), "white", "black")
263
+ add("110", _("Gray"), "gray", "black")
264
+ add("120", _("Black"), "black", "white")
265
+
266
+ # Rainbow colors
267
+ add("210", _("Red"), "red", "white")
268
+ add("220", _("Orange"), "orange", "white")
269
+ add("230", _("Yellow"), "yellow", "black")
270
+ add("240", _("Green"), "green", "white")
271
+ add("250", _("Blue"), "blue", "white")
272
+ add("260", _("Magenta"), "magenta","white")
273
+ add("270", _("Violet"), "violet", "white")
274
+
275
+ # Other colors
276
+ add("300", _("Silver"), "silver", "black")
277
+ add("310", _("Maroon"), "maroon", "white")
278
+ add("311", _("Peru"), "peru", "white")
279
+ add("320", _("Olive"), "olive", "white")
280
+ add("330", _("Aqua"), "aqua", "white")
281
+ add("340", _("Navy"), "navy", "white")
282
+ add("350", _("Fuchsia"), "fuchsia","white")
283
+ add("351", _("Purple"), "purple", "white")
284
+
285
+ # List of all named colors: https://www.w3schools.com/colors/colors_names.asp
@@ -33,6 +33,7 @@ from .choicelists import (
33
33
  DurationUnits,
34
34
  Recurrences,
35
35
  Weekdays,
36
+ DisplayColors
36
37
  )
37
38
  from .mixins import Lockable
38
39
 
@@ -99,7 +99,7 @@ class User(AbstractBaseUser, Contactable, CreatedModified, Publishable, DateRang
99
99
  user_type = UserTypes.field(blank=True)
100
100
  initials = models.CharField(_("Initials"), max_length=10, blank=True)
101
101
  if dd.plugins.users.with_nickname:
102
- nickname = models.CharField(_("Nickname"), max_length=20, blank=True)
102
+ nickname = models.CharField(_("Nickname"), max_length=50, blank=True)
103
103
  else:
104
104
  nickname = dd.DummyField()
105
105
  first_name = models.CharField(_("First name"), max_length=30, blank=True)
@@ -144,7 +144,7 @@ class User(AbstractBaseUser, Contactable, CreatedModified, Publishable, DateRang
144
144
  my_settings = MySettings()
145
145
 
146
146
  def __str__(self):
147
- return self.nickname if self.nickname else self.get_full_name()
147
+ return self.nickname or self.get_full_name()
148
148
 
149
149
  @property
150
150
  def is_active(self):
@@ -29,6 +29,8 @@ class Plugin(ad.Plugin):
29
29
 
30
30
  verbose_name = _("WeasyPrint")
31
31
 
32
+ needs_plugins = ["lino.modlib.jinja"]
33
+
32
34
  header_height = 20
33
35
  """Height of header in mm. Set to `None` if you want no header."""
34
36
 
lino/utils/__init__.py CHANGED
@@ -67,7 +67,7 @@ from urllib.parse import urlencode
67
67
  # import locale
68
68
  import dateparser
69
69
  from io import StringIO
70
- import contextlib
70
+ from contextlib import redirect_stdout, contextmanager
71
71
  from pathlib import Path
72
72
 
73
73
  from etgen.utils import join_elems
@@ -77,13 +77,6 @@ from lino.utils.code import codefiles, codetime
77
77
 
78
78
  from rstgen.utils import confirm, i2d, i2t
79
79
 
80
- try:
81
- import lino_book
82
- DEMO_DATA = Path(lino_book.__file__).parent.parent.absolute() / 'demo_data'
83
- """The root directory with demo data included in the Developer Guide."""
84
- except ImportError:
85
- DEMO_DATA = None
86
-
87
80
  DATE_TO_DIR_TPL = "%Y/%m"
88
81
 
89
82
  def read_exception(excinfo):
@@ -366,6 +359,12 @@ curry = lambda func, *args, **kw: lambda *p, **n: func(
366
359
  *args + p, **dict(list(kw.items()) + list(n.items()))
367
360
  )
368
361
 
362
+ def capture_output(func, *args, **kwargs):
363
+ s = StringIO()
364
+ with redirect_stdout(s):
365
+ func(*args, **kwargs)
366
+ return s.getvalue()
367
+
369
368
 
370
369
  class IncompleteDate(object):
371
370
  """Naive representation of a potentially incomplete gregorian date.
@@ -694,7 +693,7 @@ class MissingRow:
694
693
  return "MissingRow({!r})".format(self.message)
695
694
 
696
695
 
697
- @contextlib.contextmanager
696
+ @contextmanager
698
697
  def logging_disabled(level):
699
698
  try:
700
699
  logging.disable(level)
lino/utils/djangotest.py CHANGED
@@ -20,6 +20,7 @@ from django.db import connection, reset_queries, connections, DEFAULT_DB_ALIAS
20
20
  from django.utils import translation
21
21
 
22
22
  from lino.utils import AttrDict
23
+ from lino.api import rt
23
24
  from lino.core.signals import testcase_setup # , database_ready
24
25
  from lino.core.callbacks import applyCallbackChoice
25
26
  from .test import CommonTestCase
@@ -112,7 +113,7 @@ class DjangoManageTestCase(DjangoTestCase, CommonTestCase):
112
113
  the response's content (which is expected to contain a dict), convert
113
114
  this dict to an AttrDict before returning it.
114
115
  """
115
- ar = settings.SITE.login(username)
116
+ ar = rt.login(username)
116
117
  self.client.force_login(ar.user)
117
118
  extra[settings.SITE.remote_user_header] = username
118
119
  # extra.update(REMOTE_USER=username)
lino/utils/html.py CHANGED
@@ -14,6 +14,7 @@ from html2text import HTML2Text
14
14
  from django.utils.html import SafeString, mark_safe, escape
15
15
  # from lino.utils import tostring
16
16
 
17
+ SAFE_EMPTY = mark_safe("")
17
18
 
18
19
  def html2text(html, **kwargs):
19
20
  """
@@ -69,3 +70,33 @@ def assert_safe(s):
69
70
  if not isinstance(s, SafeString):
70
71
  raise Exception("%r is not a safe string" % s)
71
72
  # assert isinstance(s, SafeString)
73
+
74
+
75
+ class Grouper:
76
+
77
+ def __init__(self, ar):
78
+ self.ar = ar
79
+ if ar.actor.group_by is None: return
80
+ self.last_values = [None for f in ar.actor.group_by]
81
+
82
+ def begin(self):
83
+ if self.ar.actor.group_by is None: return SAFE_EMPTY
84
+ return SAFE_EMPTY
85
+
86
+ def stop(self):
87
+ if self.ar.actor.group_by is None: return SAFE_EMPTY
88
+ return SAFE_EMPTY
89
+
90
+ def before_row(self, obj):
91
+ if self.ar.actor.group_by is None: return SAFE_EMPTY
92
+ self.current_values = [f(obj) for f in self.ar.actor.group_by]
93
+ if self.current_values == self.last_values:
94
+ return SAFE_EMPTY
95
+ return self.ar.actor.before_group_change(self, obj)
96
+
97
+ def after_row(self, obj):
98
+ if self.ar.actor.group_by is None: return SAFE_EMPTY
99
+ if self.current_values == self.last_values:
100
+ return SAFE_EMPTY
101
+ self.last_values = self.current_values
102
+ return self.ar.actor.after_group_change(self, obj)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: lino
3
- Version: 24.10.3
3
+ Version: 24.11.0
4
4
  Summary: A framework for writing desktop-like web applications using Django and ExtJS or React
5
5
  Project-URL: Homepage, https://www.lino-framework.org
6
6
  Project-URL: Repository, https://gitlab.com/lino-framework/lino