lino 25.3.1__py3-none-any.whl → 25.3.3__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 (49) hide show
  1. lino/__init__.py +6 -7
  2. lino/api/dd.py +1 -0
  3. lino/api/doctest.py +4 -68
  4. lino/api/rt.py +2 -4
  5. lino/core/actors.py +22 -16
  6. lino/core/boundaction.py +17 -7
  7. lino/core/dashboard.py +5 -4
  8. lino/core/dbtables.py +15 -16
  9. lino/core/fields.py +3 -3
  10. lino/core/menus.py +1 -1
  11. lino/core/model.py +1 -1
  12. lino/core/renderer.py +10 -11
  13. lino/core/requests.py +11 -7
  14. lino/core/site.py +9 -76
  15. lino/core/tables.py +2 -2
  16. lino/core/utils.py +3 -3
  17. lino/core/views.py +3 -3
  18. lino/help_texts.py +1 -0
  19. lino/management/commands/buildsite.py +67 -0
  20. lino/management/commands/dump2py.py +5 -7
  21. lino/management/commands/initdb.py +12 -14
  22. lino/management/commands/install.py +2 -2
  23. lino/management/commands/prep.py +3 -7
  24. lino/mixins/sequenced.py +1 -1
  25. lino/modlib/comments/models.py +1 -1
  26. lino/modlib/comments/ui.py +2 -2
  27. lino/modlib/extjs/ext_renderer.py +2 -2
  28. lino/modlib/extjs/views.py +50 -48
  29. lino/modlib/help/config/makehelp/model.tpl.rst +1 -1
  30. lino/modlib/help/management/commands/makehelp.py +6 -5
  31. lino/modlib/jinja/renderer.py +2 -2
  32. lino/modlib/linod/management/commands/linod.py +5 -2
  33. lino/modlib/memo/__init__.py +1 -1
  34. lino/modlib/publisher/choicelists.py +3 -3
  35. lino/modlib/publisher/views.py +2 -2
  36. lino/modlib/search/models.py +5 -5
  37. lino/modlib/system/choicelists.py +6 -3
  38. lino/modlib/tinymce/views.py +1 -1
  39. lino/modlib/uploads/models.py +1 -1
  40. lino/modlib/users/ui.py +2 -3
  41. lino/utils/__init__.py +5 -1
  42. lino/utils/dbhash.py +112 -0
  43. lino/utils/diag.py +1 -1
  44. lino/utils/fieldutils.py +79 -0
  45. {lino-25.3.1.dist-info → lino-25.3.3.dist-info}/METADATA +1 -1
  46. {lino-25.3.1.dist-info → lino-25.3.3.dist-info}/RECORD +49 -46
  47. {lino-25.3.1.dist-info → lino-25.3.3.dist-info}/WHEEL +0 -0
  48. {lino-25.3.1.dist-info → lino-25.3.3.dist-info}/licenses/AUTHORS.rst +0 -0
  49. {lino-25.3.1.dist-info → lino-25.3.3.dist-info}/licenses/COPYING +0 -0
@@ -24,14 +24,11 @@ Summary from <http://en.wikipedia.org/wiki/Restful>:
24
24
  import json
25
25
  import os
26
26
 
27
- from pathlib import Path
28
-
29
27
  from django import http
30
- from django.contrib.messages import success
31
28
  from django.db import models
32
29
  from django.conf import settings
33
30
  from django.core.cache import cache
34
- from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
31
+ from django.core.exceptions import PermissionDenied
35
32
  from django.views.generic import View
36
33
  from django.views.generic.base import TemplateView
37
34
  from django.views.decorators.cache import never_cache
@@ -40,27 +37,24 @@ from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect
40
37
  from django.utils.translation import gettext as _
41
38
  from django.utils.encoding import force_str
42
39
  from django.utils.html import mark_safe
43
- from lino.core import auth
44
40
 
45
41
  from lino.core.signals import pre_ui_delete
46
42
  from lino.core.utils import obj2unicode
47
43
 
48
44
  # from etgen import html as xghtml
49
- from lino.utils.html import E, tostring
45
+ from lino.utils.html import E, tostring, escape
50
46
  from etgen.html import Document
51
47
 
52
48
  from lino.utils import ucsv
53
49
  from lino import logger
54
50
  # from lino.utils import dblogger
55
51
  from lino.core import constants
56
- from lino.core import actions
57
- from lino.core import fields
58
52
  from lino.core.fields import choices_for_field
59
53
  from lino.core.views import requested_actor, action_request
60
- from lino.core.views import json_response, json_response_kw
54
+ from lino.core.views import json_response
61
55
  from lino.core.views import choices_response
62
56
  from lino.core.requests import BaseRequest
63
- from lino.core.utils import PhantomRow, is_devserver
57
+ from lino.core.utils import is_devserver
64
58
 
65
59
  MAX_ROW_COUNT = 300
66
60
 
@@ -207,7 +201,7 @@ class ActionParamChoices(View):
207
201
  raise Exception("Unknown action %r for %s" % (an, actor))
208
202
  field = ba.action.get_param_elem(field)
209
203
  qs, row2dict = choices_for_field(
210
- ba.request(request=request), ba.action, field)
204
+ ba.create_request(request=request), ba.action, field)
211
205
  if field.blank:
212
206
  emptyValue = "<br/>"
213
207
  else:
@@ -229,7 +223,7 @@ class Choices(View):
229
223
  rpt = requested_actor(app_label, rptname)
230
224
  emptyValue = None
231
225
  if fldname is None:
232
- ar = rpt.request(request=request)
226
+ ar = rpt.create_request(request=request)
233
227
  qs = ar.data_iterator
234
228
 
235
229
  def row2dict(obj, d):
@@ -247,7 +241,7 @@ class Choices(View):
247
241
  if field.blank:
248
242
  # logger.info("views.Choices: %r is blank",field)
249
243
  emptyValue = "<br/>"
250
- ar = rpt.request(request=request)
244
+ ar = rpt.create_request(request=request)
251
245
  # if str(rpt) == 'comments.CommentsByRFC':
252
246
  # print("20230426", ar.master_instance)
253
247
  qs, row2dict = choices_for_field(ar, rpt, field)
@@ -263,7 +257,7 @@ class Restful(View):
263
257
  @method_decorator(csrf_protect)
264
258
  def post(self, request, app_label=None, actor=None, pk=None):
265
259
  rpt = requested_actor(app_label, actor)
266
- ar = rpt.request(request=request)
260
+ ar = rpt.create_request(request=request)
267
261
 
268
262
  instance = ar.create_instance()
269
263
  # store uploaded files.
@@ -284,14 +278,14 @@ class Restful(View):
284
278
  @method_decorator(csrf_protect)
285
279
  def delete(self, request, app_label=None, actor=None, pk=None):
286
280
  rpt = requested_actor(app_label, actor)
287
- ar = rpt.request(request=request)
281
+ ar = rpt.create_request(request=request)
288
282
  ar.set_selected_pks(pk)
289
283
  return delete_element(ar, ar.selected_rows[0])
290
284
 
291
285
  def get(self, request, app_label=None, actor=None, pk=None):
292
286
  rpt = requested_actor(app_label, actor)
293
287
  assert pk is None, 20120814
294
- ar = rpt.request(request=request)
288
+ ar = rpt.create_request(request=request)
295
289
  rh = ar.ah
296
290
  rows = [
297
291
  rh.store.row2dict(ar, row, rh.store.grid_fields)
@@ -304,7 +298,7 @@ class Restful(View):
304
298
  @method_decorator(csrf_protect)
305
299
  def put(self, request, app_label=None, actor=None, pk=None):
306
300
  rpt = requested_actor(app_label, actor)
307
- ar = rpt.request(request=request)
301
+ ar = rpt.create_request(request=request)
308
302
  ar.set_selected_pks(pk)
309
303
  elem = ar.selected_rows[0]
310
304
  rh = ar.ah
@@ -313,7 +307,7 @@ class Restful(View):
313
307
  data = json.loads(data)
314
308
  # 20240404 a = rpt.get_url_action(rpt.default_list_action_name)
315
309
  a = rpt.default_action
316
- ar = rpt.request(request=request, action=a)
310
+ ar = rpt.create_request(request=request, action=a)
317
311
  ar.renderer = settings.SITE.kernel.extjs_renderer
318
312
  ar.form2obj_and_save(data, elem, False)
319
313
  # Ext.ensible needs grid_fields, not detail_fields
@@ -326,6 +320,10 @@ NOT_FOUND = "%s has no row with primary key %r"
326
320
 
327
321
 
328
322
  class ApiElement(View):
323
+ """
324
+ The view that responds to
325
+ ``r'api/(?P<app_label>\w+)/(?P<actor>\w+)/(?P<pk>[^/]+)$'``.
326
+ """
329
327
 
330
328
  @method_decorator(ensure_csrf_cookie)
331
329
  def get(self, request, app_label=None, actor=None, pk=None):
@@ -353,48 +351,53 @@ class ApiElement(View):
353
351
  ba.action.default_format)
354
352
 
355
353
  try:
354
+ ar = ba.create_request(
355
+ request=request, renderer=settings.SITE.kernel.default_renderer)
356
356
  if pk and pk != "-99999" and pk != "-99998":
357
- sr = [pk]
358
357
  if issubclass(rpt.model, models.Model):
359
358
  try:
360
- ar = ba.request(request=request, selected_pks=sr)
361
- # except ObjectDoesNotExist as e: # 20250212
359
+ ar.set_selected_pks(pk)
362
360
  except rpt.model.DoesNotExist:
363
361
  if fmt == constants.URL_FORMAT_JSON:
364
- # rescue_ar: without sr and even request, to render a table request (grid view action) on breadcrumb
365
- rescue_ar = rpt.request(
362
+ # rescue_ar: without sr and even request, to render
363
+ # a table request (grid view action) on breadcrumb
364
+ safe_kw = dict(
365
+ user=request.user,
366
366
  renderer=settings.SITE.kernel.default_renderer)
367
- default_table = rpt.model.get_default_table()
368
-
367
+ rescue_ar = rpt.create_request(**safe_kw)
369
368
  title = tostring(rescue_ar.href_to_request(
370
369
  rescue_ar, icon_name=None))
371
-
372
- def get_response():
373
- msg = mark_safe(
374
- f'Record (pk={pk}) is no longer available on current table.')
375
- datarec = dict(
376
- success=False, message=msg, title=title)
377
- datarec.update(**vm)
378
- return datarec
379
-
370
+ datarec = dict(alert=False, success=False,
371
+ title=title, **vm)
372
+ msg = _("Row {pk} is not visible here.").format(pk=pk)
373
+ default_table = rpt.model.get_default_table()
374
+ if default_table is rpt:
375
+ datarec.update(message=mark_safe(msg))
376
+ return json_response(datarec)
377
+ ar = default_table.detail_action.create_request(parent=ar)
380
378
  try:
381
379
  # take default table and try to show the row
382
- ar = default_table.detail_action.request(
383
- request=request, selected_pks=sr)
380
+ ar.set_selected_pks(pk)
384
381
  except default_table.model.DoesNotExist:
385
- return json_response(get_response())
386
-
387
- url = ar.obj2url(ar.selected_rows[0])
388
- datarec = get_response()
389
- datarec['message'] += mark_safe(
390
- f' Reload in <a href="{url}">{default_table}</a>.')
382
+ msg += " " + _("Neither is it visible in {table}.").format(
383
+ table=ar.get_title())
384
+ datarec.update(message=mark_safe(msg))
385
+ return json_response(datarec)
386
+
387
+ lnk = ar.obj2htmls(ar.selected_rows[0], ar.get_title())
388
+ s = escape(_("But you can see it in {}.")).format(lnk)
389
+ msg += "<br>" + s
390
+ # msg = format_html("{}<br>{}", )
391
+ # msg = format_html("{}<br>{}", _("But you can see it in {link}"))
392
+ # url = ar.obj2url(dtar.selected_rows[0])
393
+ # msg += "<br>" + _('But you can see it in <a href="{url}">{table}</a>.').format(
394
+ # url=url, table=ar.get_title())
395
+ datarec.update(message=mark_safe(msg))
391
396
  return json_response(datarec)
392
- # print("20240911", e)
393
- raise http.Http404(
394
- f"Object {sr} does not exist on {rpt}")
397
+ raise http.Http404(f"Row {pk} does not exist on {rpt}")
395
398
  else:
396
- ar = ba.request(request=request, selected_pks=sr)
397
- # ar = ba.request(request=request, selected_pks=sr)
399
+ ar.set_selected_pks(pk)
400
+ # ar = ba.create_request(request=request, selected_pks=sr)
398
401
  elem = ar.selected_rows[0]
399
402
  # print(
400
403
  # "20170116 views.ApiElement.get", ba,
@@ -405,7 +408,6 @@ class ApiElement(View):
405
408
  # raise http.Http404(
406
409
  # "No permission to see {} {}.".format(rpt, action_name))
407
410
  else:
408
- ar = ba.request(request=request)
409
411
  elem = None
410
412
  except Exception as e:
411
413
 
@@ -8,4 +8,4 @@
8
8
 
9
9
  {{header(2, str(_("Database fields")))}}
10
10
 
11
- {{doctest.fields_help(model, all=True)}}
11
+ {{dd.fields_help(model, all=True)}}
@@ -41,7 +41,7 @@ from lino.core.model import Model
41
41
  from lino.core.utils import model_class_path
42
42
  from lino.modlib.help.utils import HelpTextsLoader, simplify_name
43
43
  from lino.modlib.gfks.fields import GenericForeignKey
44
- from lino.api.dd import full_model_name
44
+ from lino.api import dd
45
45
 
46
46
  # removed import doctest because it caused "pytest not installed" during
47
47
  # makehelp on LF:
@@ -91,7 +91,7 @@ def runcmd(cmd, **kw):
91
91
 
92
92
  def field_ref(f):
93
93
  if isinstance(f.model, Model):
94
- return ":ref:`{}.{}`".format(full_model_name(f.model), f.name)
94
+ return ":ref:`{}.{}`".format(dd.full_model_name(f.model), f.name)
95
95
  # parameter field
96
96
  return ":ref:`{}.{}`".format(str(f.model), f.name)
97
97
 
@@ -136,7 +136,7 @@ def model_referenced_from(model):
136
136
  def ddhfmt(ddh):
137
137
  return ", ".join(
138
138
  [
139
- "{}.{}".format(full_model_name(model), fk.name)
139
+ "{}.{}".format(dd.full_model_name(model), fk.name)
140
140
  for model, fk in ddh.fklist
141
141
  ]
142
142
  )
@@ -265,7 +265,8 @@ class Command(GeneratingCommand):
265
265
  # ~ py2rst=rstgen.py2rst,
266
266
  languages=[lng.django_code for lng in settings.SITE.languages],
267
267
  get_models=apps.get_models,
268
- full_model_name=full_model_name,
268
+ full_model_name=dd.full_model_name,
269
+ dd=dd,
269
270
  model_overview=self.model_overview,
270
271
  actor2par=self.actor2par,
271
272
  actors2table=self.actors2table,
@@ -324,7 +325,7 @@ class Command(GeneratingCommand):
324
325
  elif issubclass(x, models.Model):
325
326
  if text is None:
326
327
  text = x.__name__
327
- return ":doc:`" + text + " <" + full_model_name(x) + ">`"
328
+ return ":doc:`" + text + " <" + dd.full_model_name(x) + ">`"
328
329
  if isinstance(x, BoundAction):
329
330
  if text is None:
330
331
  text = x.action_name
@@ -82,7 +82,7 @@ class JinjaRenderer(HtmlRenderer):
82
82
 
83
83
  def as_table(action_spec):
84
84
  a = settings.SITE.models.resolve(action_spec)
85
- ar = a.request(user=settings.SITE.get_anonymous_user())
85
+ ar = a.create_request(user=settings.SITE.get_anonymous_user())
86
86
  return self.as_table(ar)
87
87
 
88
88
  def as_table2(ar):
@@ -99,7 +99,7 @@ class JinjaRenderer(HtmlRenderer):
99
99
 
100
100
  def as_ul(action_spec):
101
101
  a = settings.SITE.models.resolve(action_spec)
102
- ar = a.request(user=settings.SITE.get_anonymous_user())
102
+ ar = a.create_request(user=settings.SITE.get_anonymous_user())
103
103
  # 20150810
104
104
  ar.renderer = self
105
105
  # return tostring(E.ul(*[obj.as_paragraph(ar) for obj in ar]))
@@ -51,8 +51,11 @@ class Command(BaseCommand):
51
51
  # print("20240424 Run Lino daemon without channels")
52
52
 
53
53
  async def main():
54
- u = await settings.SITE.user_model.objects.aget(
55
- username=settings.SITE.plugins.linod.daemon_user)
54
+ try:
55
+ u = await settings.SITE.user_model.objects.aget(
56
+ username=settings.SITE.plugins.linod.daemon_user)
57
+ except settings.SITE.user_model.DoesNotExist:
58
+ u = None
56
59
  ar = BaseRequest(user=u)
57
60
  # ar = rt.login(dd.plugins.linod.daemon_user)
58
61
  await asyncio.gather(start_log_server(), start_task_runner(ar))
@@ -93,7 +93,7 @@ class Plugin(ad.Plugin):
93
93
  # kwargs = dict(header_level=3) #, nosummary=True)
94
94
  kwargs = dict() # , nosummary=True)
95
95
  dv = self.site.models.resolve(s)
96
- sar = dv.request(parent=ar, limit=dv.preview_limit)
96
+ sar = dv.create_request(parent=ar, limit=dv.preview_limit)
97
97
  rv = ""
98
98
  # rv += "20230325 [show {}]".format(dv)
99
99
  for e in sar.renderer.table2story(sar, **kwargs):
@@ -149,7 +149,7 @@ class PageFiller(Choice):
149
149
  def get_dynamic_story(self, ar, obj, **kwargs):
150
150
  txt = ""
151
151
  dv = self.data_view
152
- sar = dv.request(parent=ar, limit=dv.preview_limit)
152
+ sar = dv.create_request(parent=ar, limit=dv.preview_limit)
153
153
  # print("20231028", dv, list(sar))
154
154
  # print("20230409", ar.renderer)
155
155
  # rv += "20230325 [show {}]".format(dv)
@@ -159,8 +159,8 @@ class PageFiller(Choice):
159
159
 
160
160
  def get_dynamic_paragraph(self, ar, obj, **kwargs):
161
161
  dv = self.data_view
162
- # sar = dv.request(parent=ar, limit=dv.preview_limit)
163
- sar = dv.request(parent=ar)
162
+ # sar = dv.create_request(parent=ar, limit=dv.preview_limit)
163
+ sar = dv.create_request(parent=ar)
164
164
  return " / ".join([sar.obj2htmls(row) for row in sar])
165
165
 
166
166
 
@@ -37,7 +37,7 @@ class Element(View):
37
37
  kw.update(selected_pks=[pk])
38
38
 
39
39
  try:
40
- ar = self.table_class.request(request=request, **kw)
40
+ ar = self.table_class.create_request(request=request, **kw)
41
41
  except ObjectDoesNotExist as e:
42
42
  # print("20240911", e)
43
43
  return http.HttpResponseNotFound(f"No row #{pk} in {self.table_class} ({e})")
@@ -54,5 +54,5 @@ class Index(View):
54
54
  dv = settings.SITE.models.publisher.Pages
55
55
  index_node = dv.model.objects.get(ref="index", language=request.LANGUAGE_CODE)
56
56
  # print("20231025", index_node)
57
- ar = dv.request(request=request, renderer=rnd, selected_rows=[index_node])
57
+ ar = dv.create_request(request=request, renderer=rnd, selected_rows=[index_node])
58
58
  return index_node.get_publisher_response(ar)
@@ -52,7 +52,7 @@ class SiteSearchBase(dd.VirtualTable):
52
52
  if t is None:
53
53
  return
54
54
  t = cls.get_table_for_role(t, ar.get_user().user_type.role)
55
- sar = t.request(parent=ar)
55
+ sar = t.create_request(parent=ar)
56
56
  return obj.as_search_item(sar)
57
57
 
58
58
  @classmethod
@@ -68,7 +68,7 @@ class SiteSearchBase(dd.VirtualTable):
68
68
  if t is None:
69
69
  return
70
70
  t = cls.get_table_for_role(t, ar.get_user().user_type.role)
71
- sar = t.request(parent=ar)
71
+ sar = t.create_request(parent=ar)
72
72
  return t.get_card_title(sar, obj)
73
73
 
74
74
  @classmethod
@@ -158,7 +158,7 @@ class SiteSearch(SiteSearchBase):
158
158
  t = cls.get_table_for_role(t, user_type.role)
159
159
  if not t.get_view_permission(user_type):
160
160
  continue
161
- sar = t.request(parent=ar, quick_search=ar.quick_search)
161
+ sar = t.create_request(parent=ar, quick_search=ar.quick_search)
162
162
  try:
163
163
  for obj in sar:
164
164
  if obj.show_in_site_search: # don't show calview.HeaderRow
@@ -197,7 +197,7 @@ if settings.SITE.use_elasticsearch and has_elasticsearch:
197
197
  user_type = ar.get_user().user_type
198
198
  query = MultiMatch(query=ar.quick_search)
199
199
  s = search.query(query)
200
- s = s[ar.offset : ar.offset + ar.limit]
200
+ s = s[ar.offset: ar.offset + ar.limit]
201
201
  sq = execute_search(s, save=False)
202
202
  return sq
203
203
  return []
@@ -210,7 +210,7 @@ if settings.SITE.use_solr and has_haystack:
210
210
  class SolrSiteSearch(SiteSearchBase):
211
211
  @classmethod
212
212
  def get_rows_from_search_query(cls, sqs, ar):
213
- results = sqs[ar.offset : ar.offset + ar.limit]
213
+ results = sqs[ar.offset: ar.offset + ar.limit]
214
214
  for result in results:
215
215
  yield result.model.objects.get(pk=result.pk)
216
216
 
@@ -222,9 +222,9 @@ add("P", _("per weekday"), "per_weekday") # deprecated
222
222
  add("E", _("Relative to Easter"), "easter")
223
223
 
224
224
 
225
-
226
225
  class DisplayColor(Choice):
227
226
  font_color = None
227
+
228
228
  def __init__(self, value, text, names, font_color="white"):
229
229
  super().__init__(value, text, names)
230
230
  self.font_color = font_color
@@ -247,12 +247,14 @@ class DisplayColors(ChoiceList):
247
247
  # text = escape(bc.text)
248
248
  # txt = f"""<span style="background-color:{bc.name};color:{bc.font_color}">{text}</span>"""
249
249
  # txt = mark_safe(txt)
250
- sample = f"""<span style="padding:3pt;background-color:{bc.name};color:{bc.font_color}">(sample)</span>"""
250
+ sample = f"""<span style="padding:3pt;background-color:{
251
+ bc.name};color:{bc.font_color}">(sample)</span>"""
251
252
  sample = mark_safe(sample)
252
253
  txt = format_html("{} {}", bc.text, sample)
253
254
  # raise Exception(f"20250118 {txt.__class__}")
254
255
  return txt
255
256
 
257
+
256
258
  add = DisplayColors.add_item
257
259
  # cssColors = 'White Silver Gray Black Red Maroon Yellow Olive Lime Green Aqua Teal Blue Navy Fuchsia Purple'
258
260
  # cssColors = 'white silver gray black red maroon yellow olive lime green aqua teal blue navy fuchsia purple'
@@ -275,7 +277,7 @@ add("220", _("Orange"), "orange", "white")
275
277
  add("230", _("Yellow"), "yellow", "black")
276
278
  add("240", _("Green"), "green", "white")
277
279
  add("250", _("Blue"), "blue", "white")
278
- add("260", _("Magenta"), "magenta","white")
280
+ add("260", _("Magenta"), "magenta", "white")
279
281
  add("270", _("Violet"), "violet", "white")
280
282
 
281
283
  # Other colors
@@ -291,6 +293,7 @@ add("342", _("DarkGreen"), "darkgreen", "white")
291
293
  add("343", _("PaleGreen"), "palegreen", "black")
292
294
  add("344", _("Chartreuse"), "chartreuse", "black")
293
295
  add("345", _("Lime"), "lime", "black")
296
+ add("346", _("Teal"), "teal", "white")
294
297
  add("350", _("Fuchsia"), "fuchsia", "white")
295
298
  add("351", _("Cyan"), "cyan", "black")
296
299
  add("361", _("Purple"), "purple", "white")
@@ -29,7 +29,7 @@ class Templates(View):
29
29
  ):
30
30
  if request.method == "GET":
31
31
  rpt = requested_actor(app_label, actor)
32
- ar = rpt.request(request=request)
32
+ ar = rpt.create_request(request=request)
33
33
  elem = rpt.get_row_by_pk(ar, pk)
34
34
  if elem is None:
35
35
  raise http.Http404("%s %s does not exist." % (rpt, pk))
@@ -485,7 +485,7 @@ def on_sanitize(soup, save=False, ar=None):
485
485
  if (src := tag.get('src')) and src.startswith("data:image"):
486
486
  file = base64_to_image(src)
487
487
  upload = rt.models.uploads.Upload(file=file, user=ar.get_user())
488
- sar = upload.get_default_table().request(parent=ar)
488
+ sar = upload.get_default_table().create_request(parent=ar)
489
489
  upload.save_new_instance(sar)
490
490
  tag.replace_with(f'[file {upload.pk}]')
491
491
 
lino/modlib/users/ui.py CHANGED
@@ -3,6 +3,7 @@
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
  # Documentation: :doc:`/specs/users` and :doc:`/dev/users`
5
5
 
6
+ from lino.core.site import has_socialauth
6
7
  from datetime import datetime
7
8
  from textwrap import wrap
8
9
  from importlib import import_module
@@ -134,7 +135,7 @@ class UsersOverview(Users):
134
135
  pv.update(password=dd.plugins.users.demo_password)
135
136
  btn = rt.models.about.About.get_action_by_name("sign_in")
136
137
  # print btn.get_row_permission(ar, None, None)
137
- btn = btn.request(
138
+ btn = btn.create_request(
138
139
  action_param_values=pv, renderer=settings.SITE.kernel.default_renderer
139
140
  )
140
141
  btn = btn.ar2button(label=self.username)
@@ -212,8 +213,6 @@ class AuthoritiesTaken(Authorities):
212
213
  auto_fit_column_widths = True
213
214
 
214
215
 
215
- from lino.core.site import has_socialauth
216
-
217
216
  if has_socialauth and dd.get_plugin_setting("users", "third_party_authentication"):
218
217
  import social_django
219
218
 
lino/utils/__init__.py CHANGED
@@ -18,9 +18,11 @@ function for general use. It has also many subpackages and submodules.
18
18
  dataserializer
19
19
  dbfreader
20
20
  dblogger
21
+ dbhash
21
22
  diag
22
23
  djangotest
23
24
  dpy
25
+ fieldutils
24
26
  html
25
27
  html2odf
26
28
  html2xhtml
@@ -78,6 +80,7 @@ from rstgen.utils import confirm, i2d, i2t
78
80
 
79
81
  DATE_TO_DIR_TPL = "%Y/%m"
80
82
 
83
+
81
84
  def read_exception(excinfo):
82
85
  # TODO: why not use traceback.format_exc(excinfo) intead of this?
83
86
  f = StringIO()
@@ -353,7 +356,7 @@ def hex2str(value):
353
356
  raise Exception("hex2str got value %r" % value)
354
357
  r = ""
355
358
  for i in range(old_div(len(value), 2)):
356
- s = value[i * 2 : i * 2 + 2]
359
+ s = value[i * 2: i * 2 + 2]
357
360
  h = int(s, 16)
358
361
  r += chr(h)
359
362
  return r
@@ -364,6 +367,7 @@ curry = lambda func, *args, **kw: lambda *p, **n: func(
364
367
  *args + p, **dict(list(kw.items()) + list(n.items()))
365
368
  )
366
369
 
370
+
367
371
  def capture_output(func, *args, **kwargs):
368
372
  s = StringIO()
369
373
  with redirect_stdout(s):
lino/utils/dbhash.py ADDED
@@ -0,0 +1,112 @@
1
+ # -*- coding: UTF-8 -*-
2
+ # Copyright 2009-2025 Rumma & Ko Ltd
3
+ # License: GNU Affero General Public License v3 (see file COPYING for details)
4
+
5
+ """
6
+ Utilities around a "database hash".
7
+ See :doc:`/utils/dbhash`.
8
+ """
9
+
10
+ import json
11
+ from importlib import import_module
12
+ from pathlib import Path
13
+ from django.conf import settings
14
+ from django.apps import apps
15
+ from django.db.models.deletion import ProtectedError
16
+
17
+ mod = import_module(settings.SETTINGS_MODULE)
18
+ HASH_FILE = Path(mod.__file__).parent / "dbhash.json"
19
+
20
+
21
+ def fmn(m):
22
+ return f"{m._meta.app_label}.{m._meta.object_name}"
23
+
24
+
25
+ def compute_dbhash():
26
+ """
27
+ Return a dictionary with a hash value of the current database content.
28
+ """
29
+ rv = dict()
30
+ for m in apps.get_models(include_auto_created=True):
31
+ k = fmn(m)
32
+ if k != "sessions.Session":
33
+ # rv[k] = m.objects.count()
34
+ rv[k] = list(m.objects.values_list('pk', flat=True))
35
+ return rv
36
+
37
+
38
+ def mark_virgin():
39
+ """
40
+ Mark the database as virgin. This is called by :manage:`prep`.
41
+ """
42
+ dbhash = compute_dbhash()
43
+ with HASH_FILE.open("w") as fp:
44
+ json.dump(dbhash, fp)
45
+
46
+
47
+ def load_dbhash():
48
+ """
49
+ Load the dbhash that was saved in :xfile:`dbhash.json`
50
+ """
51
+ if not HASH_FILE.exists():
52
+ raise Exception(
53
+ f"No file {HASH_FILE} (did you run `django-admin prep`?)")
54
+ with HASH_FILE.open("r") as fp:
55
+ return json.load(fp)
56
+
57
+
58
+ def check_virgin(restore=True, verbose=True):
59
+ """
60
+ Verify whether the database is virgin. Print the differences if there
61
+ are any.
62
+ """
63
+ new = compute_dbhash()
64
+ old = load_dbhash()
65
+
66
+ first_diff = True
67
+ can_restore = True
68
+ must_delete = {}
69
+ for k, v in new.items():
70
+ v = set(v)
71
+ oldv = set(old.get(k, None))
72
+ if oldv != v:
73
+ if first_diff:
74
+ if verbose:
75
+ print(f"Database {HASH_FILE.parent} isn't virgin:")
76
+ first_diff = False
77
+ diffs = []
78
+ added = v - oldv
79
+ if len(added):
80
+ diffs.append(f"{len(added)} rows added")
81
+ must_delete[apps.get_model(k)] = added
82
+ # for pk in added:
83
+ # must_delete.append(m.objects.get(pk=pk))
84
+ if (removed := len(oldv-v)):
85
+ diffs.append(f"{removed} rows deleted")
86
+ can_restore = False
87
+ if verbose:
88
+ print(f"- {k}: {', '.join(diffs)}")
89
+ if len(must_delete) == 0 or not restore:
90
+ return
91
+ if not can_restore:
92
+ raise Exception(
93
+ "Cannot restore database because some rows have been deleted")
94
+ # print(f"Tidy up {len(must_delete)} rows from database")
95
+ # It can happen that some rows refer to each other with a protected fk
96
+ # We call bulk delete() to avoid Lino deleting the items of an invoice
97
+ must_delete = list(must_delete.items())
98
+ while len(must_delete):
99
+ todo = []
100
+ hope = False
101
+ for m, added in must_delete:
102
+ try:
103
+ m.objects.filter(pk__in=added).delete()
104
+ # obj.delete()
105
+ hope = True
106
+ except Exception:
107
+ todo.append((m, added))
108
+ if not hope:
109
+ raise Exception(f"Failed to delete {todo}")
110
+ must_delete = todo
111
+ if verbose:
112
+ print("Database has been restored.")
lino/utils/diag.py CHANGED
@@ -443,7 +443,7 @@ def py2rst(self, doctestfmt=False, fmt=None):
443
443
  from lino.core.store import get_atomizer
444
444
 
445
445
  if isinstance(self, models.Model) and fmt is None:
446
- ar = self.get_default_table().request()
446
+ ar = self.get_default_table().create_request()
447
447
 
448
448
  def fmt(e):
449
449
  s = elem_label(e)