ayame 0.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.
- ayame/__init__.py +17 -0
- ayame/__version__.py +3 -0
- ayame/app/Ayame.properties +21 -0
- ayame/app.py +199 -0
- ayame/basic.py +122 -0
- ayame/border/FeedbackFieldBorder.html +13 -0
- ayame/border.py +122 -0
- ayame/converter.py +268 -0
- ayame/core.py +745 -0
- ayame/exception.py +86 -0
- ayame/form.py +513 -0
- ayame/http.py +226 -0
- ayame/i18n.py +170 -0
- ayame/link.py +90 -0
- ayame/local.py +53 -0
- ayame/markup.py +983 -0
- ayame/model.py +114 -0
- ayame/page/HTTPStatusPage.html +40 -0
- ayame/page.py +29 -0
- ayame/panel/FeedbackPanel.html +14 -0
- ayame/panel.py +74 -0
- ayame/res.py +127 -0
- ayame/route.py +474 -0
- ayame/session.py +33 -0
- ayame/uri.py +115 -0
- ayame/util.py +581 -0
- ayame/validator.py +179 -0
- ayame-0.3.dist-info/LICENSE.txt +19 -0
- ayame-0.3.dist-info/METADATA +114 -0
- ayame-0.3.dist-info/RECORD +32 -0
- ayame-0.3.dist-info/WHEEL +5 -0
- ayame-0.3.dist-info/top_level.txt +1 -0
ayame/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#
|
|
2
|
+
# ayame
|
|
3
|
+
#
|
|
4
|
+
# Copyright (c) 2011-2021 Akinori Hattori <hattya@gmail.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: MIT
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
__author__ = 'Akinori Hattori <hattya@gmail.com>'
|
|
10
|
+
try:
|
|
11
|
+
from .__version__ import version as __version__
|
|
12
|
+
except ImportError:
|
|
13
|
+
__version__ = 'unknown'
|
|
14
|
+
|
|
15
|
+
from .app import *
|
|
16
|
+
from .core import *
|
|
17
|
+
from .exception import *
|
ayame/__version__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Required = '{label}' is required
|
|
2
|
+
Converter = The value of '{label}' is not a valid type '{type}'
|
|
3
|
+
|
|
4
|
+
Choice.single = The value of '{label}' is not a valid choice
|
|
5
|
+
Choice.multiple = The values of '{label}' contain invalid choices
|
|
6
|
+
|
|
7
|
+
RangeValidator.type = The value of '{label}' cannot validate
|
|
8
|
+
RangeValidator.minimum = The value of '{label}' must be at least {min}
|
|
9
|
+
RangeValidator.maximum = The value of '{label}' must be at most {max}
|
|
10
|
+
RangeValidator.range = The value of '{label}' must be between {min} and {max}
|
|
11
|
+
RangeValidator.exact = The value of '{label}' must be exactly {exact}
|
|
12
|
+
|
|
13
|
+
StringValidator.type = The value of '{label}' cannot validate
|
|
14
|
+
StringValidator.minimum = The value of '{label}' must be at least {min} characters long
|
|
15
|
+
StringValidator.maximum = The value of '{label}' must be at most {max} characters long
|
|
16
|
+
StringValidator.range = The value of '{label}' must be between {min} and {max} characters long
|
|
17
|
+
StringValidator.exact = The value of '{label}' must be exactly {exact} characters long
|
|
18
|
+
|
|
19
|
+
RegexValidator = The value of '{label}' does not match pattern '{pattern}'
|
|
20
|
+
EmailValidator = The value of '{label}' is not a valid email address
|
|
21
|
+
URLValidator = The value of '{label}' is not a valid URL
|
ayame/app.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#
|
|
2
|
+
# ayame.app
|
|
3
|
+
#
|
|
4
|
+
# Copyright (c) 2011-2024 Akinori Hattori <hattya@gmail.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: MIT
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
import locale
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
import werkzeug.datastructures
|
|
14
|
+
|
|
15
|
+
from . import (converter, core, http, i18n, local, markup, page, res, route,
|
|
16
|
+
session, uri, util)
|
|
17
|
+
from .exception import AyameError, _Redirect
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ['Ayame', 'Request']
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Ayame:
|
|
24
|
+
|
|
25
|
+
def __init__(self, name):
|
|
26
|
+
self._name = name
|
|
27
|
+
try:
|
|
28
|
+
self._root = os.path.abspath(os.path.dirname(sys.modules[name].__file__))
|
|
29
|
+
except (AttributeError, KeyError):
|
|
30
|
+
self._root = os.getcwd()
|
|
31
|
+
session_dir = os.path.join(self._root, 'session')
|
|
32
|
+
self.config = {
|
|
33
|
+
'ayame.converter.registry': converter.ConverterRegistry(),
|
|
34
|
+
'ayame.i18n.cache': util.LRUCache(64),
|
|
35
|
+
'ayame.i18n.localizer': i18n.Localizer(),
|
|
36
|
+
'ayame.markup.cache': util.LRUCache(64),
|
|
37
|
+
'ayame.markup.encoding': 'utf-8',
|
|
38
|
+
'ayame.markup.loader': markup.MarkupLoader,
|
|
39
|
+
'ayame.markup.pretty': False,
|
|
40
|
+
'ayame.markup.renderer': markup.MarkupRenderer,
|
|
41
|
+
'ayame.markup.separator': '.',
|
|
42
|
+
'ayame.max.redirect': 7,
|
|
43
|
+
'ayame.page.http': page.HTTPStatusPage,
|
|
44
|
+
'ayame.request': Request,
|
|
45
|
+
'ayame.resource.loader': res.ResourceLoader(),
|
|
46
|
+
'ayame.route.map': route.Map(),
|
|
47
|
+
'ayame.session.store': session.FileSystemSessionStore(session_dir, 'ayame_%s.sess'),
|
|
48
|
+
'ayame.session.name': 'session_id',
|
|
49
|
+
'ayame.session.expires': None,
|
|
50
|
+
'ayame.session.max_age': None,
|
|
51
|
+
'ayame.session.domain': None,
|
|
52
|
+
'ayame.session.path': '/',
|
|
53
|
+
'ayame.session.secure': False,
|
|
54
|
+
'ayame.session.httponly': True,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def context(self):
|
|
59
|
+
return local.context()
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def environ(self):
|
|
63
|
+
return self.context.environ
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def request(self):
|
|
67
|
+
return self.context.request
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def session(self):
|
|
71
|
+
return self.context.session
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def _router(self):
|
|
75
|
+
return self.context._router
|
|
76
|
+
|
|
77
|
+
def __call__(self, environ, start_response):
|
|
78
|
+
try:
|
|
79
|
+
ctx = local.push(self, environ)
|
|
80
|
+
ctx._router = self.config['ayame.route.map'].bind(environ)
|
|
81
|
+
# dispatch
|
|
82
|
+
o, values = ctx._router.match()
|
|
83
|
+
ctx.request = self.config['ayame.request'](environ, values)
|
|
84
|
+
ctx.session = session.get(self, environ)
|
|
85
|
+
for _ in range(self.config['ayame.max.redirect']):
|
|
86
|
+
try:
|
|
87
|
+
status, headers, content = self.handle_request(o)
|
|
88
|
+
except _Redirect as r:
|
|
89
|
+
if r.args[3] == _Redirect.PERMANENT:
|
|
90
|
+
raise http.MovedPermanently(uri.application_uri(environ)
|
|
91
|
+
+ self.uri_for(*r.args[:3], relative=True)[1:])
|
|
92
|
+
elif r.args[3] != _Redirect.INTERNAL:
|
|
93
|
+
raise http.Found(uri.application_uri(environ)
|
|
94
|
+
+ self.uri_for(*r.args[:3], relative=True)[1:])
|
|
95
|
+
o = r.args[0]
|
|
96
|
+
ctx.request.path = None
|
|
97
|
+
continue
|
|
98
|
+
break
|
|
99
|
+
else:
|
|
100
|
+
raise AyameError('reached to the maximum number of internal redirects')
|
|
101
|
+
exc_info = None
|
|
102
|
+
set_cookie = session.save(self, ctx.session)
|
|
103
|
+
if set_cookie:
|
|
104
|
+
headers.append(set_cookie)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
status, headers, exc_info, content = self.handle_error(e)
|
|
107
|
+
finally:
|
|
108
|
+
ctx = local.pop()
|
|
109
|
+
if ctx.request is not None:
|
|
110
|
+
ctx.request.close()
|
|
111
|
+
|
|
112
|
+
start_response(status, headers, exc_info)
|
|
113
|
+
return content
|
|
114
|
+
|
|
115
|
+
def handle_request(self, object):
|
|
116
|
+
if isinstance(object, type):
|
|
117
|
+
if issubclass(object, core.Page):
|
|
118
|
+
object = object()
|
|
119
|
+
else:
|
|
120
|
+
# type is callable, so it might cause unexpected error
|
|
121
|
+
object = None
|
|
122
|
+
if callable(object):
|
|
123
|
+
return object()
|
|
124
|
+
raise http.NotFound(uri.request_path(self.environ))
|
|
125
|
+
|
|
126
|
+
def handle_error(self, error):
|
|
127
|
+
if isinstance(error, http.HTTPStatus):
|
|
128
|
+
page = self.config['ayame.page.http'](error)
|
|
129
|
+
status, headers, content = page()
|
|
130
|
+
exc_info = None
|
|
131
|
+
else:
|
|
132
|
+
status, headers, content = http.InternalServerError.status, [], []
|
|
133
|
+
exc_info = sys.exc_info()
|
|
134
|
+
return status, headers, exc_info, content
|
|
135
|
+
|
|
136
|
+
def forward(self, object, values=None, anchor=None):
|
|
137
|
+
raise _Redirect(object, values, anchor, _Redirect.INTERNAL)
|
|
138
|
+
|
|
139
|
+
def redirect(self, object, values=None, anchor=None, permanent=False):
|
|
140
|
+
raise _Redirect(object, values, anchor, _Redirect.PERMANENT if permanent else _Redirect.TEMPORARY)
|
|
141
|
+
|
|
142
|
+
def uri_for(self, *args, **kwargs):
|
|
143
|
+
return self._router.build(*args, **kwargs)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class Request:
|
|
147
|
+
|
|
148
|
+
__slots__ = ('environ', 'method', 'uri', 'query', 'form_data', 'path',
|
|
149
|
+
'locale')
|
|
150
|
+
|
|
151
|
+
def __init__(self, environ, values):
|
|
152
|
+
self.environ = environ
|
|
153
|
+
self.method = environ['REQUEST_METHOD']
|
|
154
|
+
self.uri = values
|
|
155
|
+
self.query = uri.parse_qs(environ)
|
|
156
|
+
self.form_data = http.parse_form_data(environ)
|
|
157
|
+
# retrieve ayame:path
|
|
158
|
+
if self.method == 'GET':
|
|
159
|
+
self.path = self.query.get(core.AYAME_PATH)
|
|
160
|
+
elif self.method == 'POST':
|
|
161
|
+
self.path = self.form_data.get(core.AYAME_PATH)
|
|
162
|
+
else:
|
|
163
|
+
self.path = None
|
|
164
|
+
if self.path:
|
|
165
|
+
self.path = self.path[0]
|
|
166
|
+
self.locale = self._parse_locales(environ)
|
|
167
|
+
|
|
168
|
+
def _parse_locales(self, environ):
|
|
169
|
+
values = http.parse_accept(environ.get('HTTP_ACCEPT_LANGUAGE'))
|
|
170
|
+
if values:
|
|
171
|
+
v = values[0][0]
|
|
172
|
+
sep = '-'
|
|
173
|
+
else:
|
|
174
|
+
v = locale.getlocale()[0]
|
|
175
|
+
sep = '_'
|
|
176
|
+
if v:
|
|
177
|
+
v = v.split(sep, 1)
|
|
178
|
+
return (v[0].lower(), v[1].upper() if len(v) > 1 else None)
|
|
179
|
+
return (None,) * 2
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def input(self):
|
|
183
|
+
return self.environ['wsgi.input']
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def session(self):
|
|
187
|
+
return local.context().session
|
|
188
|
+
|
|
189
|
+
def __enter__(self):
|
|
190
|
+
return self
|
|
191
|
+
|
|
192
|
+
def __exit__(self, *exc_info):
|
|
193
|
+
self.close()
|
|
194
|
+
|
|
195
|
+
def close(self):
|
|
196
|
+
for _, data in self.form_data.items():
|
|
197
|
+
for v in data:
|
|
198
|
+
if isinstance(v, werkzeug.datastructures.FileStorage):
|
|
199
|
+
v.close()
|
ayame/basic.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#
|
|
2
|
+
# ayame.basic
|
|
3
|
+
#
|
|
4
|
+
# Copyright (c) 2011-2021 Akinori Hattori <hattya@gmail.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: MIT
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
import collections.abc
|
|
10
|
+
|
|
11
|
+
from . import core, markup, uri
|
|
12
|
+
from . import model as mm
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = ['Label', 'ListView', 'PropertyListView', 'ContextPathGenerator',
|
|
16
|
+
'ContextImage', 'ContextLink']
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Label(core.Component):
|
|
20
|
+
|
|
21
|
+
def __init__(self, id, model=None):
|
|
22
|
+
if isinstance(model, str):
|
|
23
|
+
model = mm.Model(model)
|
|
24
|
+
super().__init__(id, model)
|
|
25
|
+
|
|
26
|
+
def on_render(self, element):
|
|
27
|
+
element[:] = (self.model_object_as_string(),)
|
|
28
|
+
return element
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ListView(core.MarkupContainer):
|
|
32
|
+
|
|
33
|
+
def __init__(self, id, model=None, populate_item=None):
|
|
34
|
+
if isinstance(model, collections.abc.Sequence):
|
|
35
|
+
model = mm.Model(model)
|
|
36
|
+
super().__init__(id, model)
|
|
37
|
+
self._populate_item = populate_item
|
|
38
|
+
|
|
39
|
+
def on_before_render(self):
|
|
40
|
+
o = self.model_object
|
|
41
|
+
if o is not None:
|
|
42
|
+
for i in range(len(o)):
|
|
43
|
+
li = self.new_item(i)
|
|
44
|
+
self.add(li)
|
|
45
|
+
self.populate_item(li)
|
|
46
|
+
super().on_before_render()
|
|
47
|
+
|
|
48
|
+
def on_render(self, element):
|
|
49
|
+
skel = element.copy()
|
|
50
|
+
skel.qname = markup.DIV
|
|
51
|
+
del element[:]
|
|
52
|
+
for c in self.children:
|
|
53
|
+
element.extend(c.on_render(skel.copy()))
|
|
54
|
+
return element
|
|
55
|
+
|
|
56
|
+
def populate_item(self, item):
|
|
57
|
+
if callable(self._populate_item):
|
|
58
|
+
return self._populate_item(item)
|
|
59
|
+
|
|
60
|
+
def new_item(self, index):
|
|
61
|
+
return _ListItem(index, self.new_model(index))
|
|
62
|
+
|
|
63
|
+
def new_model(self, index):
|
|
64
|
+
return _ListItemModel(self, index)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class _ListItem(core.MarkupContainer):
|
|
68
|
+
|
|
69
|
+
def __init__(self, index, model):
|
|
70
|
+
super().__init__(str(index), model)
|
|
71
|
+
self.__index = index
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def index(self):
|
|
75
|
+
return self.__index
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class _ListItemModel(mm.Model):
|
|
79
|
+
|
|
80
|
+
def __init__(self, list_view, index):
|
|
81
|
+
self.__list_view = list_view
|
|
82
|
+
self.__index = index
|
|
83
|
+
|
|
84
|
+
def object():
|
|
85
|
+
def fget(self):
|
|
86
|
+
return self.__list_view.model_object[self.__index]
|
|
87
|
+
|
|
88
|
+
def fset(self, object):
|
|
89
|
+
self.__list_view.model_object[self.__index] = object
|
|
90
|
+
|
|
91
|
+
return locals()
|
|
92
|
+
|
|
93
|
+
object = property(**object())
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class PropertyListView(ListView):
|
|
97
|
+
|
|
98
|
+
def new_model(self, index):
|
|
99
|
+
return mm.CompoundModel(super().new_model(index))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ContextPathGenerator(core.AttributeModifier):
|
|
103
|
+
|
|
104
|
+
def __init__(self, attr, rel_path):
|
|
105
|
+
super().__init__(attr, mm.Model(rel_path))
|
|
106
|
+
|
|
107
|
+
def new_value(self, value, new_value):
|
|
108
|
+
return uri.relative_uri(self.environ, new_value)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ContextImage(core.Component):
|
|
112
|
+
|
|
113
|
+
def __init__(self, id, rel_path):
|
|
114
|
+
super().__init__(id)
|
|
115
|
+
self.add(ContextPathGenerator('src', rel_path))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ContextLink(core.Component):
|
|
119
|
+
|
|
120
|
+
def __init__(self, id, rel_path):
|
|
121
|
+
super().__init__(id)
|
|
122
|
+
self.add(ContextPathGenerator('href', rel_path))
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<?xml version="1.0"?>
|
|
2
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
|
3
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:ayame="http://hattya.github.io/ayame">
|
|
4
|
+
<head>
|
|
5
|
+
<title>FeedbackFieldBorder</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<ayame:border>
|
|
9
|
+
<ayame:body />
|
|
10
|
+
<p ayame:id="feedback" class="feedback">message</p>
|
|
11
|
+
</ayame:border>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
ayame/border.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#
|
|
2
|
+
# ayame.border
|
|
3
|
+
#
|
|
4
|
+
# Copyright (c) 2011-2021 Akinori Hattori <hattya@gmail.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: MIT
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
from . import basic, core, form, markup
|
|
10
|
+
from .exception import RenderingError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = ['Border', 'FeedbackFieldBorder']
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Border(core.MarkupContainer):
|
|
17
|
+
|
|
18
|
+
def __init__(self, id, model=None):
|
|
19
|
+
super().__init__(id, model)
|
|
20
|
+
self.has_markup = True
|
|
21
|
+
self.render_body_only = True
|
|
22
|
+
# ayame:body element
|
|
23
|
+
self.body = _BorderBodyContainer(id)
|
|
24
|
+
self.add_to_border(self.body)
|
|
25
|
+
self.__body = False
|
|
26
|
+
|
|
27
|
+
def add_to_border(self, *args):
|
|
28
|
+
return super().add(*args)
|
|
29
|
+
|
|
30
|
+
def add(self, *args):
|
|
31
|
+
for o in args:
|
|
32
|
+
if isinstance(o, core._MessageContainer):
|
|
33
|
+
self.add_to_border(o)
|
|
34
|
+
else:
|
|
35
|
+
self.body.add(o)
|
|
36
|
+
return self
|
|
37
|
+
|
|
38
|
+
def on_render(self, element):
|
|
39
|
+
def step(element, depth):
|
|
40
|
+
return element.qname not in (markup.AYAME_BODY, markup.AYAME_HEAD)
|
|
41
|
+
|
|
42
|
+
# load markup for Panel
|
|
43
|
+
m = self.load_markup()
|
|
44
|
+
if m.root is None:
|
|
45
|
+
# markup is empty
|
|
46
|
+
return element
|
|
47
|
+
|
|
48
|
+
ayame_border = ayame_body = ayame_head = None
|
|
49
|
+
for elem, _ in m.root.walk(step=step):
|
|
50
|
+
if elem.qname == markup.AYAME_BORDER:
|
|
51
|
+
if ayame_border is None:
|
|
52
|
+
ayame_border = elem
|
|
53
|
+
elif elem.qname == markup.AYAME_BODY:
|
|
54
|
+
if (ayame_border is not None
|
|
55
|
+
and ayame_body is None):
|
|
56
|
+
# replace children of ayame:body element
|
|
57
|
+
ayame_body = elem
|
|
58
|
+
ayame_body.type = markup.Element.OPEN
|
|
59
|
+
ayame_body[:] = element
|
|
60
|
+
elif elem.qname == markup.AYAME_HEAD:
|
|
61
|
+
if ('html' in m.lang
|
|
62
|
+
and ayame_head is None):
|
|
63
|
+
ayame_head = elem
|
|
64
|
+
if ayame_border is None:
|
|
65
|
+
raise RenderingError(self, "'ayame:border' element is not found")
|
|
66
|
+
elif ayame_body is None:
|
|
67
|
+
raise RenderingError(self, "'ayame:body' element is not found")
|
|
68
|
+
# append ayame:head element to Page
|
|
69
|
+
if ayame_head is not None:
|
|
70
|
+
self.page().head.extend(ayame_head)
|
|
71
|
+
# render border
|
|
72
|
+
element[:] = ayame_border
|
|
73
|
+
return super().on_render(element)
|
|
74
|
+
|
|
75
|
+
def on_render_element(self, element):
|
|
76
|
+
if element.qname == markup.AYAME_BODY:
|
|
77
|
+
# skip duplicates
|
|
78
|
+
if not self.__body:
|
|
79
|
+
element.attrib[markup.AYAME_ID] = self.body.id
|
|
80
|
+
self.__body = True
|
|
81
|
+
return element
|
|
82
|
+
return super().on_render_element(element)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class _BorderBodyContainer(core.MarkupContainer):
|
|
86
|
+
|
|
87
|
+
def __init__(self, id, model=None):
|
|
88
|
+
super().__init__(id + '_body', model)
|
|
89
|
+
self.render_body_only = True
|
|
90
|
+
|
|
91
|
+
def on_render_element(self, element):
|
|
92
|
+
if element.qname == markup.AYAME_BODY:
|
|
93
|
+
return element
|
|
94
|
+
return super().on_render_element(element)
|
|
95
|
+
|
|
96
|
+
def tr(self, key, component=None):
|
|
97
|
+
# retrieve message from parent of Border
|
|
98
|
+
return super().tr(key, self.parent.parent)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class FeedbackFieldBorder(Border):
|
|
102
|
+
|
|
103
|
+
def __init__(self, id):
|
|
104
|
+
super().__init__(id)
|
|
105
|
+
self.render_body_only = False
|
|
106
|
+
|
|
107
|
+
self.__feedback = basic.Label('feedback', '')
|
|
108
|
+
self.add_to_border(self.__feedback)
|
|
109
|
+
self.add_to_border(self._ClassModifier('class', self.__feedback.model))
|
|
110
|
+
|
|
111
|
+
def on_configure(self):
|
|
112
|
+
for c, _ in self.walk():
|
|
113
|
+
if isinstance(c, form.FormComponent):
|
|
114
|
+
self.__feedback.model.object = c.error
|
|
115
|
+
self.__feedback.visible = bool(c.error)
|
|
116
|
+
|
|
117
|
+
class _ClassModifier(core.AttributeModifier):
|
|
118
|
+
|
|
119
|
+
def new_value(self, value, error):
|
|
120
|
+
if error:
|
|
121
|
+
return value + '-error' if value else 'error'
|
|
122
|
+
return value
|