lino 25.2.1__py3-none-any.whl → 25.2.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 (43) hide show
  1. lino/__init__.py +8 -3
  2. lino/api/dd.py +13 -0
  3. lino/api/doctest.py +49 -17
  4. lino/api/selenium.py +1 -1
  5. lino/core/actors.py +16 -7
  6. lino/core/boundaction.py +1 -1
  7. lino/core/choicelists.py +1 -1
  8. lino/core/dbtables.py +15 -15
  9. lino/core/elems.py +1 -1
  10. lino/core/renderer.py +2 -2
  11. lino/core/requests.py +49 -13
  12. lino/core/site.py +5 -5
  13. lino/help_texts.py +8 -3
  14. lino/modlib/comments/models.py +1 -1
  15. lino/modlib/comments/ui.py +1 -1
  16. lino/modlib/extjs/__init__.py +2 -2
  17. lino/modlib/extjs/views.py +75 -29
  18. lino/modlib/help/config/makehelp/conf.tpl.py +1 -1
  19. lino/modlib/jinja/mixins.py +62 -0
  20. lino/modlib/jinja/models.py +6 -0
  21. lino/modlib/linod/fixtures/__init__.py +0 -0
  22. lino/modlib/linod/fixtures/linod.py +32 -0
  23. lino/modlib/linod/mixins.py +15 -1
  24. lino/modlib/memo/mixins.py +11 -3
  25. lino/modlib/memo/parser.py +1 -1
  26. lino/modlib/notify/models.py +1 -1
  27. lino/modlib/printing/actions.py +6 -12
  28. lino/modlib/printing/choicelists.py +7 -7
  29. lino/modlib/uploads/__init__.py +9 -6
  30. lino/modlib/uploads/models.py +3 -3
  31. lino/modlib/uploads/ui.py +5 -2
  32. lino/modlib/users/mixins.py +4 -0
  33. lino/utils/__init__.py +0 -1
  34. lino/utils/jscompressor.py +4 -4
  35. lino/utils/restify.py +2 -2
  36. lino/utils/soup.py +4 -4
  37. lino/utils/xml.py +19 -5
  38. {lino-25.2.1.dist-info → lino-25.2.3.dist-info}/METADATA +1 -1
  39. {lino-25.2.1.dist-info → lino-25.2.3.dist-info}/RECORD +42 -39
  40. lino/utils/requests.py +0 -55
  41. {lino-25.2.1.dist-info → lino-25.2.3.dist-info}/WHEEL +0 -0
  42. {lino-25.2.1.dist-info → lino-25.2.3.dist-info}/licenses/AUTHORS.rst +0 -0
  43. {lino-25.2.1.dist-info → lino-25.2.3.dist-info}/licenses/COPYING +0 -0
@@ -1,5 +1,5 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2009-2018 Rumma & Ko Ltd
2
+ # Copyright 2009-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
  """
5
5
 
@@ -27,6 +27,7 @@ import os
27
27
  from pathlib import Path
28
28
 
29
29
  from django import http
30
+ from django.contrib.messages import success
30
31
  from django.db import models
31
32
  from django.conf import settings
32
33
  from django.core.cache import cache
@@ -38,6 +39,7 @@ from django.utils.decorators import method_decorator
38
39
  from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect
39
40
  from django.utils.translation import gettext as _
40
41
  from django.utils.encoding import force_str
42
+ from django.utils.html import mark_safe
41
43
  from lino.core import auth
42
44
 
43
45
  from lino.core.signals import pre_ui_delete
@@ -48,7 +50,8 @@ from lino.utils.html import E, tostring
48
50
  from etgen.html import Document
49
51
 
50
52
  from lino.utils import ucsv
51
- from lino.utils import dblogger
53
+ from lino import logger
54
+ # from lino.utils import dblogger
52
55
  from lino.core import constants
53
56
  from lino.core import actions
54
57
  from lino.core import fields
@@ -57,7 +60,7 @@ from lino.core.views import requested_actor, action_request
57
60
  from lino.core.views import json_response, json_response_kw
58
61
  from lino.core.views import choices_response
59
62
  from lino.core.requests import BaseRequest
60
- from lino.core.utils import PhantomRow
63
+ from lino.core.utils import PhantomRow, is_devserver
61
64
 
62
65
  MAX_ROW_COUNT = 300
63
66
 
@@ -103,7 +106,7 @@ def delete_element(ar, elem):
103
106
  try:
104
107
  elem.delete()
105
108
  except Exception as e:
106
- dblogger.exception(e)
109
+ logger.exception(e)
107
110
  msg = _("Failed to delete %(record)s : %(error)s.") % dict(
108
111
  record=obj2unicode(elem), error=e
109
112
  )
@@ -343,42 +346,85 @@ class ApiElement(View):
343
346
  if ba is None:
344
347
  raise http.Http404("%s has no detail_action" % rpt)
345
348
 
346
- if pk and pk != "-99999" and pk != "-99998":
347
- sr = [pk]
348
- # Until 20240910 we checked for selected_rows (sr) in the URL and
349
- # used the first sr if defined. Don't ask me why we did this. It
350
- # simply made no sense. because the PK is given the url path.
351
- # sr = request.GET.getlist(constants.URL_PARAM_SELECTED)
352
- # if len(sr) == 0:
353
- # sr = [pk]
354
- try:
355
- ar = ba.request(request=request, selected_pks=sr)
356
- except ObjectDoesNotExist as e:
357
- # print("20240911", e)
358
- raise http.Http404(f"Object {sr} does not exist on {rpt}")
359
- # print(
360
- # "20170116 views.ApiElement.get", ba,
361
- # ar.action_param_values)
362
- if len(ar.selected_rows):
349
+ fmt = request.GET.get(constants.URL_PARAM_FORMAT, ba.action.default_format)
350
+
351
+ try:
352
+ if pk and pk != "-99999" and pk != "-99998":
353
+ sr = [pk]
354
+ if issubclass(rpt.model, models.Model):
355
+ try:
356
+ ar = ba.request(request=request, selected_pks=sr)
357
+ # except ObjectDoesNotExist as e: # 20250212
358
+ except rpt.model.DoesNotExist as e:
359
+ if fmt == constants.URL_FORMAT_JSON:
360
+ # rescue_ar: without sr and even request, to render a table request (grid view action) on breadcrumb
361
+ rescue_ar = rpt.request(renderer=settings.SITE.kernel.default_renderer)
362
+ default_table = rpt.model.get_default_table()
363
+
364
+ title = tostring(rescue_ar.href_to_request(rescue_ar, icon_name=None))
365
+ def get_response():
366
+ msg = mark_safe(f'Record (pk={pk}) is no longer available on current table.')
367
+ datarec = dict(success=False, message=msg, title=title)
368
+ datarec.update(**vm)
369
+ return datarec
370
+
371
+ try:
372
+ # take default table and try to show the row
373
+ ar = default_table.detail_action.request(request=request, selected_pks=sr)
374
+ except default_table.model.DoesNotExist as e:
375
+ return json_response(get_response())
376
+
377
+ url = ar.obj2url(ar.selected_rows[0])
378
+ datarec = get_response()
379
+ datarec['message'] += mark_safe(f' Reload in <a href="{url}">{default_table}</a>.')
380
+ return json_response(datarec)
381
+ # print("20240911", e)
382
+ raise http.Http404(f"Object {sr} does not exist on {rpt}")
383
+ else:
384
+ ar = ba.request(request=request, selected_pks=sr)
385
+ # ar = ba.request(request=request, selected_pks=sr)
363
386
  elem = ar.selected_rows[0]
387
+ # print(
388
+ # "20170116 views.ApiElement.get", ba,
389
+ # ar.action_param_values)
390
+ # if len(ar.selected_rows):
391
+ # elem = ar.selected_rows[0]
392
+ # else:
393
+ # raise http.Http404(
394
+ # "No permission to see {} {}.".format(rpt, action_name))
364
395
  else:
365
- raise http.Http404(
366
- "No permission to see {} {}.".format(rpt, action_name))
367
- else:
368
- ar = ba.request(request=request)
369
- elem = None
396
+ ar = ba.request(request=request)
397
+ elem = None
398
+ except Exception as e:
399
+
400
+ # Instantiating an action request can cause all kinds of errors,
401
+ # ranging from invalid primary key to subtle bugs like #5924 (Menu
402
+ # "My invoicing plan" fails), which was caused by the custom
403
+ # invoicing.MyPlans.get_row_by_pk() method, which raised a
404
+ # RelatedObjectDoesNotExist when accessing a non-nullable field.
405
+
406
+ # On a production site we turn every exception into a 400 response
407
+ # because otherwise every GET with a wrong pk would cause an email
408
+ # to ADMINS.
409
+
410
+ # logger.exception(e)
411
+ msg = f"Invalid request for {repr(pk)} on {rpt} ({e})"
412
+ logger.info("Error during ApiElement.get(): %s", msg)
413
+ if is_devserver():
414
+ # can be interesting during development but disturbs on a
415
+ # production server
416
+ logger.exception(e)
417
+ raise http.Http404(msg) from None
370
418
 
371
419
  ar.renderer = settings.SITE.kernel.default_renderer
372
420
 
373
421
  if not ar.get_permission():
374
- msg = "No permission to run {}".format(ar)
422
+ msg = f"No permission to run {ar}"
375
423
  # raise Exception(msg)
376
424
  raise PermissionDenied(msg)
377
425
 
378
426
  # print("20240402 permission", ar, "granted to", ar.get_user(), ar.bound_action.action.select_rows, ar.selected_rows)
379
427
 
380
- fmt = request.GET.get(constants.URL_PARAM_FORMAT, ba.action.default_format)
381
-
382
428
  if ba.action.opens_a_window:
383
429
  if fmt == constants.URL_FORMAT_JSON:
384
430
  if pk == "-99999":
@@ -10,7 +10,7 @@ intersphinx_mapping = {}
10
10
 
11
11
  {% if makehelp.language.index == 0 -%}
12
12
 
13
- html_context = dict(public_url="{{settings.SITE.server_url}}media/cache/help")
13
+ html_context = dict(public_url="{{settings.SITE.server_url}}/media/cache/help")
14
14
 
15
15
  from rstgen.sphinxconf import configure ; configure(globals())
16
16
  from lino.sphinxcontrib import configure ; configure(globals())
@@ -0,0 +1,62 @@
1
+ # -*- coding: UTF-8 -*-
2
+ # Copyright 2022 Rumma & Ko Ltd
3
+ # License: GNU Affero General Public License v3 (see file COPYING for details)
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from lxml import etree
8
+
9
+ from django.conf import settings
10
+ from django.utils import translation
11
+ from django.utils.html import mark_safe, escape
12
+
13
+ from lino.api import dd
14
+ from lino.utils.xml import validate_xml
15
+
16
+ def xml_element(name, value):
17
+ if value:
18
+ return f"<{name}>{escape(str(value))}</{name}>"
19
+ return ""
20
+
21
+
22
+ class XMLMaker(dd.Model):
23
+
24
+ class Meta:
25
+ abstract = True
26
+
27
+ xml_validator_file = None
28
+ xml_file_template = None
29
+ xml_file_name = None
30
+
31
+ def make_xml_file(self, ar):
32
+ renderer = settings.SITE.plugins.jinja.renderer
33
+ tpl = renderer.jinja_env.get_template(self.xml_file_template)
34
+ context = self.get_printable_context(ar)
35
+ context.update(xml_element=xml_element)
36
+ xml = tpl.render(**context)
37
+ parts = [
38
+ dd.plugins.accounting.xml_media_dir,
39
+ self.xml_file_name.format(self=self)]
40
+ xmlfile = Path(settings.MEDIA_ROOT, *parts)
41
+ ar.logger.info("Make %s from %s ...", xmlfile, self)
42
+ xmlfile.parent.mkdir(exist_ok=True, parents=True)
43
+ xmlfile.write_text(xml)
44
+ # xmlfile.write_text(etree.tostring(xml))
45
+
46
+ if self.xml_validator_file:
47
+ # print("20250218 {xml[:100]}")
48
+ # doc = etree.fromstring(xml.encode("utf-8"))
49
+ ar.logger.info("Validate %s against %s ...", xmlfile.name, self.xml_validator_file)
50
+ if True:
51
+ validate_xml(xmlfile, self.xml_validator_file)
52
+ else:
53
+ try:
54
+ validate_xml(xmlfile, self.xml_validator_file)
55
+ except Exception as e:
56
+ msg = _("XML validation failed: {}").format(e)
57
+ # print(msg)
58
+ raise Warning(msg)
59
+
60
+ url = settings.SITE.build_media_url(*parts)
61
+ # return mark_safe(f"""<a href="{url}">{url}</a>""")
62
+ return (xmlfile, url)
@@ -0,0 +1,6 @@
1
+ # -*- coding: UTF-8 -*-
2
+ # Copyright 2025 Rumma & Ko Ltd
3
+ # License: GNU Affero General Public License v3 (see file COPYING for details)
4
+
5
+ from .mixins import XMLMaker
6
+ from .choicelists import JinjaBuildMethod
File without changes
@@ -0,0 +1,32 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright 2025 Rumma & Ko Ltd
3
+ # License: GNU Affero General Public License v3 (see file COPYING for details)
4
+ from asgiref.sync import async_to_sync
5
+ from lino.api import rt
6
+ from lino.modlib.linod.mixins import start_task_runner
7
+
8
+ # import logging
9
+ # from lino import logger
10
+ # def getHandlerByName(name):
11
+ # for l in logger.handlers:
12
+ # if l.name == name:
13
+ # return l
14
+
15
+ def objects():
16
+ raise Exception("""
17
+
18
+ This fixture isn't used at the moment. I wrote it because I thought it
19
+ would be nice to run the system task runner automatically when ``pm prep``
20
+ in order to cover the sync_ibanity system task. But (1) this would require
21
+ me to integrate also the ``checkdata`` and ``checksummaries`` fixtures into
22
+ it (otherwise they would run again as a system task) and (2) we don't want
23
+ to start `sync_ibanity` automatically on GitLab because it can't work
24
+ without credentials.
25
+
26
+ """)
27
+ ar = rt.login("robin")
28
+ # logger.setLevel(logging.DEBUG)
29
+ # getHandlerByName('console').setLevel(logging.DEBUG)
30
+ # ar.debug("Coucou")
31
+ async_to_sync(start_task_runner)(ar, max_count=1)
32
+ return []
@@ -1,5 +1,5 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2023-2024 Rumma & Ko Ltd
2
+ # Copyright 2023-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
  # See https://dev.lino-framework.org/plugins/linod.html
5
5
 
@@ -13,6 +13,7 @@ from io import StringIO
13
13
  from django.conf import settings
14
14
  from django.db import models
15
15
  from django.utils import timezone
16
+ from django.core.exceptions import ValidationError
16
17
  from asgiref.sync import sync_to_async, async_to_sync
17
18
 
18
19
  from lino import logger
@@ -80,6 +81,11 @@ class Runnable(Sequenced, RecurrenceSet):
80
81
 
81
82
  def full_clean(self, *args, **kwargs):
82
83
  super().full_clean(*args, **kwargs)
84
+ # 20250213 The following caused 'Invalid procedure invoicing.Task for
85
+ # linod.SystemTask' during restore.py:
86
+ # class_name = dd.full_model_name(self.__class__)
87
+ # if self.procedure.class_name != class_name:
88
+ # raise ValidationError(f"Invalid procedure {self.procedure.class_name} for {class_name}")
83
89
  if self.every_unit is None:
84
90
  self.every_unit = Recurrences.never
85
91
  if not self.name:
@@ -91,6 +97,11 @@ class Runnable(Sequenced, RecurrenceSet):
91
97
  def run_task(self, ar):
92
98
  raise NotImplementedError()
93
99
 
100
+ @dd.chooser()
101
+ def procedure_choices(cls):
102
+ # print([p.class_name for p in Procedures.get_list_items()])
103
+ return Procedures.filter(class_name=dd.full_model_name(cls))
104
+
94
105
  async def start_task(self, ar):
95
106
  # print("20231102 start_task", self)
96
107
  if self.is_running():
@@ -191,6 +202,9 @@ async def start_task_runner(ar, max_count=None):
191
202
  nst = await sync_to_async(self.get_next_suggested_date)(
192
203
  self.last_end_time, ar.logger
193
204
  )
205
+ if nst is None:
206
+ await ar.adebug("No time suggested to start %s", astr(self))
207
+ continue
194
208
  if nst > now:
195
209
  await ar.adebug("Too early to start %s", astr(self))
196
210
  next_time = min(next_time, nst)
@@ -39,12 +39,20 @@ MARKDOWNCFG = dict(
39
39
 
40
40
 
41
41
  def rich_text_to_elems(ar, description):
42
- if description.startswith("<"):
42
+ description = ar.parse_memo(description)
43
+
44
+ # After 20250213 #5929 (Links in the description of a ticket aren't rendered
45
+ # correctly) we no longer try to automatically detect reSTructuredText
46
+ # markup in a RichTextField. Anyway nobody has ever used this feature
47
+ # (except for the furniture fixture of the products plugin).
48
+
49
+ # if description.startswith("<"):
50
+ if True:
43
51
  # desc = E.raw('<div>%s</div>' % self.description)
44
- desc = fragments_fromstring(ar.parse_memo(description))
52
+ desc = fragments_fromstring(description)
45
53
  return desc
46
54
  # desc = E.raw('<div>%s</div>' % self.description)
47
- html = restify(ar.parse_memo(description))
55
+ html = restify(description)
48
56
  # logger.info(u"20180320 restify %s --> %s", description, html)
49
57
  # html = html.strip()
50
58
  try:
@@ -135,7 +135,7 @@ class Parser:
135
135
  def compile_suggester_regex(self):
136
136
  triggers = "".join(
137
137
  [
138
- "\\" if key in "[\^$.|?*+(){}" else "" + key
138
+ r"\\" if key in r"[\^$.|?*+(){}" else "" + key
139
139
  for key in self.suggesters.keys()
140
140
  ]
141
141
  )
@@ -386,7 +386,7 @@ class MyMessages(My, Messages):
386
386
  default_display_modes = {None: constants.DISPLAY_MODE_SUMMARY}
387
387
 
388
388
  @classmethod
389
- def unused_get_table_summary(cls, mi, ar):
389
+ def unused_get_table_summary(cls, ar):
390
390
  # 20240710 Replaced by table_as_summary(), which is now more simple. But
391
391
  # I leave the old version here in case some unexpected regression
392
392
  # occurs.
@@ -1,5 +1,5 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2009-2024 Rumma & Ko Ltd
2
+ # Copyright 2009-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
5
  from lino import logger
@@ -24,10 +24,6 @@ from lino.utils.xml import validate_xml
24
24
 
25
25
  from .choicelists import BuildMethods
26
26
 
27
- # davlink = settings.SITE.plugins.get('davlink', None)
28
- # has_davlink = davlink is not None and settings.SITE.use_java
29
- has_davlink = False
30
-
31
27
 
32
28
  class BasePrintAction(Action):
33
29
  sort_index = 50
@@ -98,10 +94,7 @@ class BasePrintAction(Action):
98
94
  # msg %= dict(doc=leaf, help=etree.tostring(
99
95
  # help_url, encoding="unicode"))
100
96
  kw.update(message=msg, alert=True)
101
- if has_davlink and bm.use_webdav and ar.request is not None:
102
- kw.update(open_webdav_url=ar.request.build_absolute_uri(url))
103
- else:
104
- kw.update(open_url=url)
97
+ kw.update(open_url=url)
105
98
  ar.success(**kw)
106
99
  return
107
100
 
@@ -137,7 +130,7 @@ class DirectPrintAction(BasePrintAction):
137
130
  class WriteXmlAction(DirectPrintAction):
138
131
  """Generate an XML file from this database object."""
139
132
 
140
- combo_group = "writexml"
133
+ # combo_group = "writexml"
141
134
  label = _("XML")
142
135
 
143
136
  build_method = "xml"
@@ -148,6 +141,7 @@ class WriteXmlAction(DirectPrintAction):
148
141
  def validate_result_file(self, bm, xmlfile):
149
142
  if self.xsd_file:
150
143
  logger.info("Validate %s against %s ...", xmlfile, self.xsd_file)
144
+ # doc = etree.parse(xmlfile)
151
145
  if True:
152
146
  validate_xml(xmlfile, self.xsd_file)
153
147
  else:
@@ -221,7 +215,7 @@ class EditTemplate(BasePrintAction):
221
215
 
222
216
  def attach_to_actor(self, actor, name):
223
217
  # if not settings.SITE.is_installed('davlink'):
224
- if not (settings.SITE.webdav_protocol or has_davlink):
218
+ if not settings.SITE.webdav_protocol:
225
219
  return False
226
220
  return super(EditTemplate, self).attach_to_actor(actor, name)
227
221
 
@@ -253,7 +247,7 @@ class EditTemplate(BasePrintAction):
253
247
  url = settings.SITE.build_media_url(*parts)
254
248
  # url = ar.build_webdav_uri(url)
255
249
 
256
- if not (settings.SITE.webdav_protocol or has_davlink):
250
+ if not settings.SITE.webdav_protocol:
257
251
  msg = "cp %s %s" % (filename, local_file)
258
252
  ar.debug(msg)
259
253
  raise Warning(
@@ -1,16 +1,15 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2009-2023 Rumma & Ko Ltd
2
+ # Copyright 2009-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
  """
5
5
  Choicelists for `lino.modlib.printing`.
6
6
 
7
7
  """
8
8
 
9
- from lino import logger
10
-
11
9
  import os
12
10
  import io
13
11
  from copy import copy
12
+ from pathlib import Path
14
13
 
15
14
  from django.conf import settings
16
15
  from django.utils import translation
@@ -25,6 +24,7 @@ from django.template.loader import select_template
25
24
  from lino.core.choicelists import ChoiceList, Choice
26
25
  from lino.utils.media import MediaFile
27
26
  from lino.api import rt, _
27
+ from lino import logger
28
28
 
29
29
  try:
30
30
  import pyratemp
@@ -42,9 +42,7 @@ class BuildMethod(Choice):
42
42
  # same.
43
43
  if names is None:
44
44
  names = self.name
45
- super(BuildMethod, self).__init__(
46
- names, self.__class__.__name__, names, **kwargs
47
- )
45
+ super().__init__(names, self.__class__.__name__, names, **kwargs)
48
46
 
49
47
  def get_target(self, action, obj):
50
48
  # Used by get_target_name()
@@ -72,7 +70,7 @@ class TemplatedBuildMethod(BuildMethod):
72
70
  default_template = "" # overridden by lino_xl.lib.appypod
73
71
 
74
72
  def __init__(self, *args, **kwargs):
75
- super(TemplatedBuildMethod, self).__init__(*args, **kwargs)
73
+ super().__init__(*args, **kwargs)
76
74
  if self.templates_name is None:
77
75
  assert len(self.names) == 1
78
76
  self.templates_name = self.names[0]
@@ -234,11 +232,13 @@ class XmlBuildMethod(DjangoBuildMethod):
234
232
  filename = action.before_build(self, elem)
235
233
  if filename is None:
236
234
  return
235
+ filename = Path(filename)
237
236
  tpl = self.get_template(action, elem)
238
237
 
239
238
  lang = str(elem.get_print_language() or translation.get_language())
240
239
  # or settings.SITE.DEFAULT_LANGUAGE.django_code)
241
240
 
241
+
242
242
  with translation.override(lang):
243
243
  cmd_options = elem.get_build_options(self)
244
244
  logger.info(
@@ -35,12 +35,12 @@ class Plugin(ad.Plugin):
35
35
  # TODO: Also remove the Volume model and its actors when with_volumes is set
36
36
  # to False.
37
37
 
38
- def get_uploads_root(self):
39
- # return join(self.site.django_settings["MEDIA_ROOT"], "uploads")
40
- return self.site.media_root / UPLOADS_ROOT
38
+ # def get_uploads_root(self):
39
+ # # return join(self.site.django_settings["MEDIA_ROOT"], "uploads")
40
+ # return self.site.media_root / UPLOADS_ROOT
41
41
 
42
- def get_volumes_root(self):
43
- return self.site.media_root / VOLUMES_ROOT
42
+ # def get_volumes_root(self):
43
+ # return self.site.media_root / VOLUMES_ROOT
44
44
 
45
45
  def setup_main_menu(self, site, user_type, m, ar=None):
46
46
  mg = self.get_menu_group()
@@ -61,6 +61,9 @@ class Plugin(ad.Plugin):
61
61
 
62
62
  def post_site_startup(self, site):
63
63
 
64
+ self.uploads_root = site.media_root / UPLOADS_ROOT
65
+ self.volumes_root = site.media_root / VOLUMES_ROOT
66
+
64
67
  if site.is_installed("memo"):
65
68
 
66
69
  def gallery(ar, text, cmdname, mentions, context):
@@ -76,7 +79,7 @@ class Plugin(ad.Plugin):
76
79
  super().post_site_startup(site)
77
80
 
78
81
  # site.makedirs_if_missing(self.get_uploads_root())
79
- # site.makedirs_if_missing(self.get_volumes_root())
82
+ # site.makedirs_if_missing(self.volumes_root)
80
83
 
81
84
  def get_requirements(self, site):
82
85
  if self.with_thumbnails:
@@ -57,7 +57,7 @@ class Volume(Referrable):
57
57
 
58
58
  def full_clean(self, *args, **kw):
59
59
  super().full_clean(*args, **kw)
60
- pth = dd.plugins.uploads.get_volumes_root() / self.ref
60
+ pth = dd.plugins.uploads.volumes_root / self.ref
61
61
  if pth.exists():
62
62
  if pth.resolve().absolute() != Path(self.root_dir).resolve().absolute():
63
63
  raise ValidationError(
@@ -176,7 +176,7 @@ class Upload(UploadBase, UserAuthored, Controllable, Publishable):
176
176
  if self.file:
177
177
  return self.file.size
178
178
  if self.volume_id and self.library_file:
179
- pth = dd.plugins.uploads.get_volumes_root() / self.volume.ref / self.library_file
179
+ pth = dd.plugins.uploads.volumes_root / self.volume.ref / self.library_file
180
180
  return pth.stat().st_size
181
181
  # return os.path.getsize(pth)
182
182
 
@@ -308,7 +308,7 @@ class UploadsFolderChecker(Checker):
308
308
  def get_checkdata_problems(self, obj, fix=False):
309
309
  assert obj is None # this is an unbound checker
310
310
  Upload = rt.models.uploads.Upload
311
- pth = dd.plugins.uploads.get_uploads_root()
311
+ pth = dd.plugins.uploads.uploads_root
312
312
  assert str(pth).startswith(settings.MEDIA_ROOT)
313
313
  start = len(settings.MEDIA_ROOT) + 1
314
314
  for filename in Path(pth).rglob("*"):
lino/modlib/uploads/ui.py CHANGED
@@ -153,6 +153,7 @@ class AreaUploads(Uploads):
153
153
  required_roles = dd.login_required(UploadsReader)
154
154
  stay_in_grid = True
155
155
  default_display_modes = {None: constants.DISPLAY_MODE_SUMMARY}
156
+ detailed_summary = False
156
157
 
157
158
  # 20180119
158
159
  # @classmethod
@@ -170,7 +171,8 @@ class AreaUploads(Uploads):
170
171
  return obj.description or filename_leaf(obj.file.name) or str(obj.id)
171
172
 
172
173
  @classmethod
173
- def get_table_summary(self, obj, ar):
174
+ def get_table_summary(self, ar):
175
+ obj = ar.master_instance
174
176
  if obj is None:
175
177
  return
176
178
  UploadType = rt.models.uploads.UploadType
@@ -237,7 +239,8 @@ class AreaUploads(Uploads):
237
239
  types.append(chunks)
238
240
  # logger.info("20140430 %s", [tostring(e) for e in types])
239
241
  # elems += [str(ar.bound_action.action.__class__), " "]
240
- if ar.bound_action.action.window_type == "d":
242
+ # if ar.bound_action.action.window_type == "d":
243
+ if self.detailed_summary:
241
244
  if len(types) == 0:
242
245
  elems.append(E.ul(E.li(str(ar.no_data_text))))
243
246
  else:
@@ -271,6 +271,10 @@ class UserPlan(UserAuthored):
271
271
  plan.save()
272
272
  return plan
273
273
 
274
+ def __str__(self):
275
+ return _("{plan} by {user}").format(
276
+ plan=self._meta.verbose_name, user=self.user)
277
+
274
278
  def run_update_plan(self, ar):
275
279
  raise NotImplementedError()
276
280
 
lino/utils/__init__.py CHANGED
@@ -40,7 +40,6 @@ function for general use. It has also many subpackages and submodules.
40
40
  pyuca
41
41
  quantities
42
42
  ranges
43
- requests
44
43
  restify
45
44
  screenshots
46
45
  sqllog
@@ -39,18 +39,18 @@ class JSCompressor(object):
39
39
 
40
40
  literalMarker = "@_@%d@_@" # temporary replacement
41
41
  # put the string literals back in
42
- backSubst = re.compile("@_@(\d+)@_@")
42
+ backSubst = re.compile(r"@_@(\d+)@_@")
43
43
 
44
44
  # /* ... */ comments on single line
45
45
  mlc1 = re.compile(r"(\/\*.*?\*\/)")
46
46
  mlc = re.compile(r"(\/\*.*?\*\/)", re.DOTALL) # real multiline comments
47
- slc = re.compile("\/\/.*") # remove single line comments
47
+ slc = re.compile(r"\/\/.*") # remove single line comments
48
48
 
49
49
  # collapse successive non-leading white space characters into one
50
- collapseWs = re.compile("(?<=\S)[ \t]+")
50
+ collapseWs = re.compile(r"(?<=\S)[ \t]+")
51
51
 
52
52
  squeeze = re.compile(
53
- """
53
+ r"""
54
54
  \s+(?=[\}\]\)\:\&\|\=\;\,\.\+]) | # remove whitespace preceding control characters
55
55
  (?<=[\{\[\(\:\&\|\=\;\,\.\+])\s+ | # ... or following such
56
56
  [ \t]+(?=\W) | # remove spaces or tabs preceding non-word characters
lino/utils/restify.py CHANGED
@@ -17,7 +17,7 @@ import re
17
17
 
18
18
  # This regular expression finds the indentation of every non-blank
19
19
  # line in a string.
20
- _INDENT_RE = re.compile("^([ ]*)(?=\S)", re.MULTILINE)
20
+ _INDENT_RE = re.compile(r"^([ ]*)(?=\S)", re.MULTILINE)
21
21
 
22
22
 
23
23
  def min_indent(s):
@@ -451,7 +451,7 @@ def rst2latex(
451
451
 
452
452
 
453
453
  if __name__ == "__main__":
454
- test = """
454
+ test = r"""
455
455
  Test example
456
456
  ============
457
457
 
lino/utils/soup.py CHANGED
@@ -327,10 +327,10 @@ def sanitized_soup(old):
327
327
  comment.extract()
328
328
 
329
329
  # remove the wrapper tag if it is useless
330
- if len(soup.contents) == 1:
331
- main_tag = soup.contents[0]
332
- if main_tag.name in useless_main_tags and not main_tag.attrs:
333
- main_tag.unwrap()
330
+ # if len(soup.contents) == 1:
331
+ # main_tag = soup.contents[0]
332
+ # if main_tag.name in useless_main_tags and not main_tag.attrs:
333
+ # main_tag.unwrap()
334
334
 
335
335
  return soup
336
336