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.
- lino/__init__.py +8 -3
- lino/api/dd.py +13 -0
- lino/api/doctest.py +49 -17
- lino/api/selenium.py +1 -1
- lino/core/actors.py +16 -7
- lino/core/boundaction.py +1 -1
- lino/core/choicelists.py +1 -1
- lino/core/dbtables.py +15 -15
- lino/core/elems.py +1 -1
- lino/core/renderer.py +2 -2
- lino/core/requests.py +49 -13
- lino/core/site.py +5 -5
- lino/help_texts.py +8 -3
- lino/modlib/comments/models.py +1 -1
- lino/modlib/comments/ui.py +1 -1
- lino/modlib/extjs/__init__.py +2 -2
- lino/modlib/extjs/views.py +75 -29
- lino/modlib/help/config/makehelp/conf.tpl.py +1 -1
- lino/modlib/jinja/mixins.py +62 -0
- lino/modlib/jinja/models.py +6 -0
- lino/modlib/linod/fixtures/__init__.py +0 -0
- lino/modlib/linod/fixtures/linod.py +32 -0
- lino/modlib/linod/mixins.py +15 -1
- lino/modlib/memo/mixins.py +11 -3
- lino/modlib/memo/parser.py +1 -1
- lino/modlib/notify/models.py +1 -1
- lino/modlib/printing/actions.py +6 -12
- lino/modlib/printing/choicelists.py +7 -7
- lino/modlib/uploads/__init__.py +9 -6
- lino/modlib/uploads/models.py +3 -3
- lino/modlib/uploads/ui.py +5 -2
- lino/modlib/users/mixins.py +4 -0
- lino/utils/__init__.py +0 -1
- lino/utils/jscompressor.py +4 -4
- lino/utils/restify.py +2 -2
- lino/utils/soup.py +4 -4
- lino/utils/xml.py +19 -5
- {lino-25.2.1.dist-info → lino-25.2.3.dist-info}/METADATA +1 -1
- {lino-25.2.1.dist-info → lino-25.2.3.dist-info}/RECORD +42 -39
- lino/utils/requests.py +0 -55
- {lino-25.2.1.dist-info → lino-25.2.3.dist-info}/WHEEL +0 -0
- {lino-25.2.1.dist-info → lino-25.2.3.dist-info}/licenses/AUTHORS.rst +0 -0
- {lino-25.2.1.dist-info → lino-25.2.3.dist-info}/licenses/COPYING +0 -0
lino/modlib/extjs/views.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# -*- coding: UTF-8 -*-
|
2
|
-
# Copyright 2009-
|
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
|
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
|
-
|
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
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
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
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
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 {}"
|
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)
|
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 []
|
lino/modlib/linod/mixins.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# -*- coding: UTF-8 -*-
|
2
|
-
# Copyright 2023-
|
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)
|
lino/modlib/memo/mixins.py
CHANGED
@@ -39,12 +39,20 @@ MARKDOWNCFG = dict(
|
|
39
39
|
|
40
40
|
|
41
41
|
def rich_text_to_elems(ar, description):
|
42
|
-
|
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(
|
52
|
+
desc = fragments_fromstring(description)
|
45
53
|
return desc
|
46
54
|
# desc = E.raw('<div>%s</div>' % self.description)
|
47
|
-
html = restify(
|
55
|
+
html = restify(description)
|
48
56
|
# logger.info(u"20180320 restify %s --> %s", description, html)
|
49
57
|
# html = html.strip()
|
50
58
|
try:
|
lino/modlib/memo/parser.py
CHANGED
lino/modlib/notify/models.py
CHANGED
@@ -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,
|
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.
|
lino/modlib/printing/actions.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# -*- coding: UTF-8 -*-
|
2
|
-
# Copyright 2009-
|
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
|
-
|
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
|
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
|
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-
|
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(
|
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(
|
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(
|
lino/modlib/uploads/__init__.py
CHANGED
@@ -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
|
-
|
40
|
-
|
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
|
-
|
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.
|
82
|
+
# site.makedirs_if_missing(self.volumes_root)
|
80
83
|
|
81
84
|
def get_requirements(self, site):
|
82
85
|
if self.with_thumbnails:
|
lino/modlib/uploads/models.py
CHANGED
@@ -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.
|
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.
|
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.
|
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,
|
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:
|
lino/modlib/users/mixins.py
CHANGED
@@ -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
lino/utils/jscompressor.py
CHANGED
@@ -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
|
-
|
332
|
-
|
333
|
-
|
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
|
|