pybald 0.5.8__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.
- pybald/__init__.py +230 -0
- pybald/config/__init__.py +0 -0
- pybald/config/constants.py +40 -0
- pybald/core/__init__.py +0 -0
- pybald/core/controllers.py +313 -0
- pybald/core/default_templates/forms/fieldset.form.template +9 -0
- pybald/core/default_templates/forms/grid.form.template +24 -0
- pybald/core/default_templates/stack_trace.html.template +137 -0
- pybald/core/django/__init__.py +0 -0
- pybald/core/django/paginator.py +148 -0
- pybald/core/forms.py +34 -0
- pybald/core/helpers.py +279 -0
- pybald/core/logs.py +100 -0
- pybald/core/middleware/__init__.py +0 -0
- pybald/core/middleware/db_middleware.py +51 -0
- pybald/core/middleware/errors.py +95 -0
- pybald/core/middleware/sessions.py +34 -0
- pybald/core/middleware/users.py +43 -0
- pybald/core/models.py +285 -0
- pybald/core/page.py +60 -0
- pybald/core/router.py +213 -0
- pybald/core/templates.py +153 -0
- pybald/db/__init__.py +1 -0
- pybald/db/cache.py +99 -0
- pybald/db/db_engine.py +22 -0
- pybald/db/ext.py +158 -0
- pybald/db/instrumented_proxy.py +50 -0
- pybald/db/magic.py +115 -0
- pybald/default.py +37 -0
- pybald/project_template/readme.rst +0 -0
- pybald/test.py +56 -0
- pybald/util/__init__.py +8 -0
- pybald/util/command_line.py +24 -0
- pybald/util/console.py +92 -0
- pybald/util/context.py +147 -0
- pybald/util/dev_server.py +29 -0
- pybald/util/static_serve.py +67 -0
- pybald/util/text.py +80 -0
- pybald/watch.py +45 -0
- pybald-0.5.8.dist-info/METADATA +60 -0
- pybald-0.5.8.dist-info/RECORD +43 -0
- pybald-0.5.8.dist-info/WHEEL +4 -0
- pybald-0.5.8.dist-info/licenses/LICENSE +54 -0
pybald/__init__.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# encoding: utf-8
|
|
3
|
+
# __init__.py
|
|
4
|
+
'''
|
|
5
|
+
Pybald
|
|
6
|
+
======
|
|
7
|
+
|
|
8
|
+
An MVC-like framework using many best-of-breed components (i.e. webob,
|
|
9
|
+
sqlalchemy).
|
|
10
|
+
|
|
11
|
+
(c) 2015 Michael Kowalchik
|
|
12
|
+
MIT License, see LICENSE file
|
|
13
|
+
'''
|
|
14
|
+
import types
|
|
15
|
+
import sys
|
|
16
|
+
import os
|
|
17
|
+
from pybald.util.context import AppContext
|
|
18
|
+
import logging
|
|
19
|
+
from .default import default_config
|
|
20
|
+
from collections import namedtuple
|
|
21
|
+
log = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
__version__ = '0.5.8'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def read_config_module(root_path='.', filename='project.py'):
|
|
27
|
+
'''Read a configuration module and append it's path to sys.path'''
|
|
28
|
+
# since the config module may have relative imports
|
|
29
|
+
# add the _config files_ path to root before executing the config
|
|
30
|
+
if root_path not in sys.path:
|
|
31
|
+
sys.path.insert(1, root_path)
|
|
32
|
+
filename = os.path.join(root_path, filename)
|
|
33
|
+
config_module = types.ModuleType("config")
|
|
34
|
+
config_module.__dict__['__file__'] = filename
|
|
35
|
+
config_module.__dict__['path'] = root_path
|
|
36
|
+
with open(filename) as config_file:
|
|
37
|
+
exec(compile(config_file.read(), filename, 'exec'),
|
|
38
|
+
config_module.__dict__)
|
|
39
|
+
return config_module
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Unconfigured(object):
|
|
43
|
+
def __getattr__(self, key):
|
|
44
|
+
raise RuntimeError("Pybald is not configured, you must run "
|
|
45
|
+
"pybald.configure() before context.{0} can"
|
|
46
|
+
" be used".format(key))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class DefaultApp(dict):
|
|
50
|
+
def __init__(self, *pargs, **kargs):
|
|
51
|
+
self.register('config', {})
|
|
52
|
+
self.register('controller_registry', [])
|
|
53
|
+
# self.register('model_registry', [])
|
|
54
|
+
self.unconfigured = True
|
|
55
|
+
super(DefaultApp, self).__init__(*pargs, **kargs)
|
|
56
|
+
|
|
57
|
+
# def __getattr__(self, key):
|
|
58
|
+
# if key in ('models', 'engine', 'metdata'):
|
|
59
|
+
# raise RuntimeError("Pybald is not configured, you must run "
|
|
60
|
+
# "pybald.configure() before context.{0} can"
|
|
61
|
+
# " be used".format(key))
|
|
62
|
+
# else:
|
|
63
|
+
# return super(DefaultApp, self).__getattr__(key)
|
|
64
|
+
|
|
65
|
+
def register(self, key, value):
|
|
66
|
+
self[key] = value
|
|
67
|
+
setattr(self, key, self[key])
|
|
68
|
+
|
|
69
|
+
# unconfigured application stack context
|
|
70
|
+
context = AppContext()
|
|
71
|
+
# register the context as a module
|
|
72
|
+
sys.modules['pybald.context'] = context
|
|
73
|
+
# push the default placeholder app onto the context stack
|
|
74
|
+
# the default allows for temporary storage of elements required
|
|
75
|
+
# for bootstrapping (and eliminates some sequencing requirements
|
|
76
|
+
# for configuration)
|
|
77
|
+
context._push(DefaultApp())
|
|
78
|
+
|
|
79
|
+
# the template engine and database session are built at config time
|
|
80
|
+
context_template = '''
|
|
81
|
+
from pybald.core.templates import TemplateEngine
|
|
82
|
+
from pybald.util.command_line import start
|
|
83
|
+
from pybald.core.models import ContextBoundModels
|
|
84
|
+
from pybald.db.db_engine import create_dump_engine
|
|
85
|
+
|
|
86
|
+
render = TemplateEngine()
|
|
87
|
+
dump_engine = create_dump_engine()
|
|
88
|
+
if config.database_engine_uri:
|
|
89
|
+
models = ContextBoundModels()
|
|
90
|
+
db = models.db
|
|
91
|
+
else:
|
|
92
|
+
models = Unconfigured()
|
|
93
|
+
db = Unconfigured()
|
|
94
|
+
|
|
95
|
+
def register(key, value):
|
|
96
|
+
globals()[key] = value
|
|
97
|
+
|
|
98
|
+
'''
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def create_config_dict_from_module(module):
|
|
102
|
+
'''Converts a module into a dictionary filtering out common module
|
|
103
|
+
level attributes'''
|
|
104
|
+
return {key: getattr(module, key) for key in module.__dict__.keys() if
|
|
105
|
+
not key.startswith('_') and key not in ('sys', 'os')}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def create_config_object(config_object):
|
|
109
|
+
'''Convert a config dictionary to a config object named tuple
|
|
110
|
+
and combines it with the default configuration dictionary.'''
|
|
111
|
+
keys = set(default_config.keys()) | set(config_object.keys())
|
|
112
|
+
ConfigObject = namedtuple("ConfigObject", keys)
|
|
113
|
+
return ConfigObject(**dict(list(default_config.items()) +
|
|
114
|
+
list(config_object.items())))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def bootstrap(bootstrap_file=None):
|
|
118
|
+
'''Execute the python file passed in. It's assumed that the bootstrap
|
|
119
|
+
python module file will execute configure() before returning although
|
|
120
|
+
not strictly necessary
|
|
121
|
+
|
|
122
|
+
This is intended for complicated projects that need to do a lot of
|
|
123
|
+
dynamic stuff before configuring the project. It's primarily a convenience
|
|
124
|
+
hook that sets the sys.path to the current working directory before
|
|
125
|
+
executing the module import.
|
|
126
|
+
|
|
127
|
+
Use of this method should be rare, ask yourself if you really need it :)
|
|
128
|
+
'''
|
|
129
|
+
# since bootstrap can be anywhere, add bootstrap to sys.path as well
|
|
130
|
+
if os.getcwd() not in sys.path:
|
|
131
|
+
sys.path.insert(1, os.getcwd())
|
|
132
|
+
possible_root_path = os.getcwd()
|
|
133
|
+
root_path, filename = os.path.split(os.path.join(possible_root_path,
|
|
134
|
+
bootstrap_file))
|
|
135
|
+
if root_path not in sys.path:
|
|
136
|
+
sys.path.insert(1, root_path)
|
|
137
|
+
filename = os.path.join(root_path, filename)
|
|
138
|
+
bootstrap_module = types.ModuleType("bootstrap")
|
|
139
|
+
bootstrap_module.__dict__['__file__'] = filename
|
|
140
|
+
with open(filename) as bootstrap_file:
|
|
141
|
+
exec(compile(bootstrap_file.read(), filename, 'exec'),
|
|
142
|
+
bootstrap_module.__dict__)
|
|
143
|
+
return bootstrap_module
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def configure(name=None, config_file=None, config_object=None, **kargs):
|
|
147
|
+
'''
|
|
148
|
+
Generate a dynamic context module that's pushed / popped on
|
|
149
|
+
the application context stack.
|
|
150
|
+
'''
|
|
151
|
+
script_name = sys.argv[0]
|
|
152
|
+
# always add current working directory to the path regardless
|
|
153
|
+
# was trying to avoid this but too much path munging happening
|
|
154
|
+
if os.getcwd() not in sys.path:
|
|
155
|
+
sys.path.insert(1, os.getcwd())
|
|
156
|
+
|
|
157
|
+
if config_object:
|
|
158
|
+
if 'project_name' not in config_object:
|
|
159
|
+
config_object['project_name'] = script_name
|
|
160
|
+
# create a named tuple that's the combo of default plus input dict
|
|
161
|
+
root_path = config_object['path'] = config_object.get('path',
|
|
162
|
+
os.getcwd())
|
|
163
|
+
config = create_config_object(config_object)
|
|
164
|
+
# add root path to config?
|
|
165
|
+
elif config_file:
|
|
166
|
+
# possible root path if relative filename specified
|
|
167
|
+
possible_root_path = os.getcwd()
|
|
168
|
+
root_path, filename = os.path.split(os.path.join(possible_root_path,
|
|
169
|
+
config_file))
|
|
170
|
+
try:
|
|
171
|
+
config_module = read_config_module(root_path=root_path,
|
|
172
|
+
filename=filename)
|
|
173
|
+
except IOError:
|
|
174
|
+
log.exception("Config Error:\n"
|
|
175
|
+
"File Error or File not "
|
|
176
|
+
"found\n{0}".format(config_file))
|
|
177
|
+
sys.exit(1)
|
|
178
|
+
config = create_config_object(create_config_dict_from_module(config_module))
|
|
179
|
+
elif kargs:
|
|
180
|
+
if 'project_name' not in kargs:
|
|
181
|
+
kargs['project_name'] = script_name
|
|
182
|
+
|
|
183
|
+
root_path = kargs['path'] = kargs.get('path',
|
|
184
|
+
os.getcwd())
|
|
185
|
+
config = create_config_object(kargs)
|
|
186
|
+
else:
|
|
187
|
+
root_path = os.getcwd()
|
|
188
|
+
log.warning("Warning: Using current path for the config "
|
|
189
|
+
"file {0}".format(root_path))
|
|
190
|
+
if root_path not in sys.path:
|
|
191
|
+
sys.path.insert(1, root_path)
|
|
192
|
+
try:
|
|
193
|
+
config_module = read_config_module(root_path=root_path,
|
|
194
|
+
filename='project.py')
|
|
195
|
+
log.warning("Found project.py in default path, "
|
|
196
|
+
"using for the config file")
|
|
197
|
+
except IOError:
|
|
198
|
+
config = create_config_object(default_config)
|
|
199
|
+
log.warning("No config file, using default pybald configuration")
|
|
200
|
+
else:
|
|
201
|
+
config = create_config_object(create_config_dict_from_module(config_module))
|
|
202
|
+
|
|
203
|
+
# if the discovered root_path is not in sys.path, add it in the least ugly
|
|
204
|
+
# way possible
|
|
205
|
+
if root_path not in sys.path:
|
|
206
|
+
sys.path.insert(1, root_path)
|
|
207
|
+
|
|
208
|
+
if config.debug:
|
|
209
|
+
from pybald.core.logs import default_debug_log
|
|
210
|
+
default_debug_log()
|
|
211
|
+
|
|
212
|
+
new_context = types.ModuleType("context")
|
|
213
|
+
|
|
214
|
+
if context._proxied() and hasattr(context._proxied(), 'unconfigured'):
|
|
215
|
+
# if we're at the root, consume any placeholder values
|
|
216
|
+
placeholder = context._pop()
|
|
217
|
+
new_context.__dict__.update(placeholder)
|
|
218
|
+
else:
|
|
219
|
+
new_context.__dict__['controller_registry'] = []
|
|
220
|
+
# always set the runtime config
|
|
221
|
+
new_context.__dict__['path'] = root_path
|
|
222
|
+
new_context.__dict__['config'] = config
|
|
223
|
+
new_context.__dict__['name'] = name
|
|
224
|
+
new_context.__dict__['__file__'] = None
|
|
225
|
+
new_context.__dict__['Unconfigured'] = Unconfigured
|
|
226
|
+
# new_context.__dict__['__path__'] = None
|
|
227
|
+
# now execute the app context with this config
|
|
228
|
+
context._push(new_context)
|
|
229
|
+
exec(compile(context_template, '<string>', 'exec'), new_context.__dict__)
|
|
230
|
+
return new_context
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'''The constants helper module is to allow for creating 'safer' ini files for
|
|
2
|
+
values that will be injected into templates. The basic rationale is that there
|
|
3
|
+
are often values that you want non-technical people to control (phone numbers,
|
|
4
|
+
marketing claims, addresses) and similar values. Using constants allows these
|
|
5
|
+
individuals to use friendlier ini format but still have the values available
|
|
6
|
+
for page templates.
|
|
7
|
+
'''
|
|
8
|
+
# TODO: The constants helper object should probably do something 'safe' when
|
|
9
|
+
# constants are deleted, maybe issuing a warning. Just a saftey net to avoid
|
|
10
|
+
# breaking templates if sections or values are accidently deleted.
|
|
11
|
+
try:
|
|
12
|
+
from ConfigParser import ConfigParser as configparser
|
|
13
|
+
except ImportError:
|
|
14
|
+
from configparser import ConfigParser as configparser
|
|
15
|
+
from collections import namedtuple
|
|
16
|
+
|
|
17
|
+
# generate constants for templates
|
|
18
|
+
# this is a little squirrley looking, but all it does is use the config
|
|
19
|
+
# parser to read an INI file and then generates a two level set of named
|
|
20
|
+
# tuples to surface the data. Inside the template the section/data of the
|
|
21
|
+
# constants ini is dereferenced using constants.SECTION.KEY
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def read(filename='constants.ini', constants_file=None):
|
|
25
|
+
'''Read a constants ini file and return the constants helper object.
|
|
26
|
+
|
|
27
|
+
When added to the page_options config, these constants can be used inside
|
|
28
|
+
the templates where the section/data of the constants ini is dereferenced
|
|
29
|
+
using constants.SECTION.KEY.
|
|
30
|
+
'''
|
|
31
|
+
config = configparser()
|
|
32
|
+
if constants_file:
|
|
33
|
+
config.read_file(constants_file)
|
|
34
|
+
else:
|
|
35
|
+
config.read(filename)
|
|
36
|
+
constants = namedtuple("Constant", config.sections())._make([
|
|
37
|
+
namedtuple('Field', dict(config.items(section)).keys()
|
|
38
|
+
)._make(dict(config.items(section)).values())
|
|
39
|
+
for section in config.sections() ])
|
|
40
|
+
return constants
|
pybald/core/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# encoding: utf-8
|
|
3
|
+
from six import with_metaclass
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from webob import Request, Response, exc
|
|
6
|
+
import re
|
|
7
|
+
from pybald.util import camel_to_underscore
|
|
8
|
+
from routes import redirect_to
|
|
9
|
+
from pybald import context
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import random
|
|
13
|
+
|
|
14
|
+
import uuid
|
|
15
|
+
import logging
|
|
16
|
+
console = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
controller_pattern = re.compile(r'(\w+)Controller')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CSRFValidationFailure(exc.HTTPForbidden):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def csrf_protected(action_func):
|
|
26
|
+
"""
|
|
27
|
+
Decorator to add CSRF (cross-site request forgery) protection to POST
|
|
28
|
+
requests on an action. To use, include this decorator and provide the
|
|
29
|
+
token in any submitted forms.
|
|
30
|
+
|
|
31
|
+
For example, in the controller, do:
|
|
32
|
+
|
|
33
|
+
@action
|
|
34
|
+
@csrf_protected
|
|
35
|
+
def my_action(self, req):
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
And in the template:
|
|
39
|
+
|
|
40
|
+
<form action='my_action' method='post'>
|
|
41
|
+
${csrf_input}
|
|
42
|
+
...
|
|
43
|
+
</form>
|
|
44
|
+
"""
|
|
45
|
+
CSRF_TOKEN_POST_VARIABLE = '__csrf_token__'
|
|
46
|
+
|
|
47
|
+
@wraps(action_func)
|
|
48
|
+
def replacement(self, req):
|
|
49
|
+
if req.method == 'POST':
|
|
50
|
+
try:
|
|
51
|
+
if CSRF_TOKEN_POST_VARIABLE not in req.POST:
|
|
52
|
+
raise CSRFValidationFailure(
|
|
53
|
+
("No %r value found in POST data. Please make sure "
|
|
54
|
+
"that a ${csrf_input} is used in the form template.")
|
|
55
|
+
% CSRF_TOKEN_POST_VARIABLE)
|
|
56
|
+
|
|
57
|
+
if not self.session.stash.get("csrf_token"):
|
|
58
|
+
raise CSRFValidationFailure(
|
|
59
|
+
"CSRF validation failed: no validation token available "
|
|
60
|
+
"in this session.")
|
|
61
|
+
|
|
62
|
+
provided_csrf_token = req.POST.get(CSRF_TOKEN_POST_VARIABLE)
|
|
63
|
+
|
|
64
|
+
if provided_csrf_token != self.session.stash.get("csrf_token"):
|
|
65
|
+
raise CSRFValidationFailure(
|
|
66
|
+
"CSRF validation failed: token mismatch.")
|
|
67
|
+
|
|
68
|
+
else:
|
|
69
|
+
# success! wipe out the used token
|
|
70
|
+
self.session.stash(csrf_token=None)
|
|
71
|
+
#del self.session.csrf_token
|
|
72
|
+
|
|
73
|
+
except CSRFValidationFailure:
|
|
74
|
+
# gentle mode, redirect to GET version of the page
|
|
75
|
+
# return self._redirect_to(req.path_qs)
|
|
76
|
+
raise
|
|
77
|
+
|
|
78
|
+
# always stash a new token
|
|
79
|
+
new_token = str(uuid.uuid4()).replace("-", "")
|
|
80
|
+
self.session.stash(csrf_token=new_token)
|
|
81
|
+
|
|
82
|
+
self.csrf_input = ("<input type='hidden' name='%s' value='%s' />" % (
|
|
83
|
+
CSRF_TOKEN_POST_VARIABLE, new_token))
|
|
84
|
+
|
|
85
|
+
return action_func(self, req)
|
|
86
|
+
return replacement
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# a no-op placeholder
|
|
90
|
+
def noop_func(*pargs, **kargs):
|
|
91
|
+
'''Do nothing'''
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_template_name(instance, method_name):
|
|
96
|
+
'''
|
|
97
|
+
Defines the template id to match against.
|
|
98
|
+
|
|
99
|
+
:param instance: the instance to generate a template for
|
|
100
|
+
:param method_name: the method to combine with the instance class
|
|
101
|
+
|
|
102
|
+
template path = controller name + '/' + action name, except in the case
|
|
103
|
+
of index. If the template is specified as part of the processed object
|
|
104
|
+
return that, short circuiting any other template name processing
|
|
105
|
+
|
|
106
|
+
This form may removed later, considered a candidate for deprecation
|
|
107
|
+
'''
|
|
108
|
+
template_id = getattr(instance, 'template_id', None)
|
|
109
|
+
if template_id:
|
|
110
|
+
return template_id
|
|
111
|
+
# build a default template name if one isn't explicitly set
|
|
112
|
+
try:
|
|
113
|
+
template_root_name = camel_to_underscore(
|
|
114
|
+
controller_pattern.search(instance.__class__.__name__
|
|
115
|
+
).group(1))
|
|
116
|
+
except AttributeError:
|
|
117
|
+
template_root_name = ''
|
|
118
|
+
return "/".join(filter(None, [template_root_name, method_name]))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# action / method decorator
|
|
122
|
+
def action(method):
|
|
123
|
+
'''
|
|
124
|
+
Decorates methods that are WSGI apps to turn them into pybald-style actions.
|
|
125
|
+
|
|
126
|
+
:param method: A method to turn into a pybald-style action.
|
|
127
|
+
|
|
128
|
+
This decorator is usually used to take the method of a controller instance
|
|
129
|
+
and add some syntactic sugar around it to allow the method to use WebOb
|
|
130
|
+
Request and Response objects. It will work with any method that
|
|
131
|
+
implements the WSGI spec.
|
|
132
|
+
|
|
133
|
+
It allows actions to work with WebOb request / response objects and handles
|
|
134
|
+
default behaviors, such as displaying the view when nothing is returned,
|
|
135
|
+
or setting up a plain text Response if a string is returned. It also
|
|
136
|
+
assigns instance variables from the ``pybald.extension`` environ variables
|
|
137
|
+
that can be set from other parts of the WSGI pipeline.
|
|
138
|
+
|
|
139
|
+
This decorator is optional but recommended for making working
|
|
140
|
+
with requests and responses easier.
|
|
141
|
+
'''
|
|
142
|
+
# the default template name is the controller class + method name
|
|
143
|
+
# the method name is pulled during decoration and stored for use
|
|
144
|
+
# in template lookups
|
|
145
|
+
template_name = method.__name__
|
|
146
|
+
# special case where 'call' or 'index' use the base class name
|
|
147
|
+
# for the template otherwise use the base name
|
|
148
|
+
if template_name in ('index', '__call__'):
|
|
149
|
+
template_name = ''
|
|
150
|
+
|
|
151
|
+
@wraps(method)
|
|
152
|
+
def action_wrapper(self, environ, start_response):
|
|
153
|
+
req = Request(environ)
|
|
154
|
+
# add any url variables as members of the controller
|
|
155
|
+
for varname, value in req.urlvars.items():
|
|
156
|
+
# Set the controller object to contain the url variables
|
|
157
|
+
# parsed from the dispatcher / router
|
|
158
|
+
setattr(self, varname, value)
|
|
159
|
+
|
|
160
|
+
# add the pybald extension dict to the controller
|
|
161
|
+
# object
|
|
162
|
+
for key, value in req.environ.setdefault('pybald.extension', {}).items():
|
|
163
|
+
setattr(self, key, value)
|
|
164
|
+
|
|
165
|
+
# TODO: fixme this is a hack
|
|
166
|
+
setattr(self, 'request', req)
|
|
167
|
+
setattr(self, 'request_url', req.url)
|
|
168
|
+
|
|
169
|
+
# set pre/post/view to a no-op if they don't exist
|
|
170
|
+
pre = getattr(self, '_pre', noop_func)
|
|
171
|
+
post = getattr(self, '_post', noop_func)
|
|
172
|
+
|
|
173
|
+
# set the template_id for this request
|
|
174
|
+
self.template_id = get_template_name(self, template_name)
|
|
175
|
+
|
|
176
|
+
# The response is either the controllers _pre code, whatever
|
|
177
|
+
# is returned from the controller
|
|
178
|
+
# or the view. So pre has precedence over
|
|
179
|
+
# the return which has precedence over the view
|
|
180
|
+
resp = (pre(req) or
|
|
181
|
+
method(self, req) or
|
|
182
|
+
context.render(template=self.template_id,
|
|
183
|
+
data=self.__dict__ or {}))
|
|
184
|
+
# if the response is currently a string
|
|
185
|
+
# wrap it in a response object
|
|
186
|
+
if isinstance(resp, str) or isinstance(resp, bytes):
|
|
187
|
+
resp = Response(body=resp, charset="utf-8")
|
|
188
|
+
# run the controllers post code
|
|
189
|
+
post(req, resp)
|
|
190
|
+
return resp(environ, start_response)
|
|
191
|
+
return action_wrapper
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# def caching_pre(keys, method_name, prefix=''):
|
|
195
|
+
# '''Decorator for pybald _pre to return cached responses if available.'''
|
|
196
|
+
# if keys is None:
|
|
197
|
+
# keys = []
|
|
198
|
+
|
|
199
|
+
# def pre_wrapper(pre):
|
|
200
|
+
# def replacement(self, req):
|
|
201
|
+
# val = ":".join([prefix] + [str(getattr(self, k, '')) for
|
|
202
|
+
# k in keys] + [method_name])
|
|
203
|
+
# self.cache_key = base64.urlsafe_b64encode(hashlib.md5(val).digest())
|
|
204
|
+
# resp = project.mc.get(self.cache_key)
|
|
205
|
+
# if resp:
|
|
206
|
+
# return resp
|
|
207
|
+
# return pre(req)
|
|
208
|
+
# return replacement
|
|
209
|
+
# return pre_wrapper
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# def caching_post(time=0):
|
|
213
|
+
# '''Decorator for pybald _post to cache/store responses.'''
|
|
214
|
+
# def post_wrapper(post):
|
|
215
|
+
# def replacement(self, req, resp):
|
|
216
|
+
# post(req, resp)
|
|
217
|
+
# # only cache 2XX or 4XX responses
|
|
218
|
+
# if (200 <= resp.status_code < 300) or (400 <= resp.status_code < 500):
|
|
219
|
+
# if 'X-Cache' not in resp.headers:
|
|
220
|
+
# resp.headerlist.append(('X-Cache', 'MISS'))
|
|
221
|
+
# project.mc.set(self.cache_key, resp, time)
|
|
222
|
+
# else:
|
|
223
|
+
# resp.headers['X-Cache'] = 'HIT'
|
|
224
|
+
# return replacement
|
|
225
|
+
# return post_wrapper
|
|
226
|
+
|
|
227
|
+
# regenerate a content_cache_prefix on every reload so that content will
|
|
228
|
+
# be force loaded after any full application restart
|
|
229
|
+
# This provides a way to cache static content for the duration of the
|
|
230
|
+
# application lifespan.
|
|
231
|
+
content_cache_prefix = hex(random.randrange(0, 2 ** 32 - 1))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# # memcache for actions
|
|
235
|
+
# def action_cached(prefix=content_cache_prefix, keys=None, time=0):
|
|
236
|
+
# '''
|
|
237
|
+
# Wrap actions and return pre-generated responses when appropriate.
|
|
238
|
+
# '''
|
|
239
|
+
# if keys is None:
|
|
240
|
+
# keys = []
|
|
241
|
+
|
|
242
|
+
# # def cached_wrapper(my_action_method):
|
|
243
|
+
# # @wraps(my_action_method)
|
|
244
|
+
# # def replacement(self, environ, start_response):
|
|
245
|
+
# # # bind newly wrapped methods to self
|
|
246
|
+
# # self._pre = caching_pre(keys,
|
|
247
|
+
# # my_action_method.__name__,
|
|
248
|
+
# # prefix=prefix)(self._pre
|
|
249
|
+
# # ).__get__(self, self.__class__)
|
|
250
|
+
# # self._post = caching_post(time)(self._post
|
|
251
|
+
# # ).__get__(self, self.__class__)
|
|
252
|
+
# # return my_action_method(self, environ, start_response)
|
|
253
|
+
# # # don't enable caching if requested
|
|
254
|
+
# # if project.DISABLE_STATIC_CONTENT_CACHE:
|
|
255
|
+
# # return my_action_method
|
|
256
|
+
# # return replacement
|
|
257
|
+
# # return cached_wrapper
|
|
258
|
+
|
|
259
|
+
class RegistryMount(type):
|
|
260
|
+
'''
|
|
261
|
+
A registry creating metaclass that keeps track of all defined classes that
|
|
262
|
+
inherit from a base class using this metaclass.
|
|
263
|
+
'''
|
|
264
|
+
# lifted almost verbatim from: http://martyalchin.com/2008/jan/10/simple-plugin-framework/
|
|
265
|
+
def __init__(cls, name, bases, attrs):
|
|
266
|
+
try:
|
|
267
|
+
cls.registry.append(cls)
|
|
268
|
+
except AttributeError:
|
|
269
|
+
# this is processing the first class (the mount point)
|
|
270
|
+
cls.registry = context.controller_registry
|
|
271
|
+
|
|
272
|
+
return super(RegistryMount, cls).__init__(name, bases, attrs)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class Controller(with_metaclass(RegistryMount, object)):
|
|
276
|
+
'''Base Controller that provides a registry mount
|
|
277
|
+
|
|
278
|
+
The registry keeps track of all countrollers defined in the project. The
|
|
279
|
+
Controller class also has some minor convenience methods attached.
|
|
280
|
+
'''
|
|
281
|
+
|
|
282
|
+
def __init__(self, *pargs, **kargs):
|
|
283
|
+
for key, value in kargs.items():
|
|
284
|
+
setattr(self, key, value)
|
|
285
|
+
|
|
286
|
+
def _pre(self, req):
|
|
287
|
+
pass
|
|
288
|
+
|
|
289
|
+
def _post(self, req, resp):
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
def _redirect_to(self, *pargs, **kargs):
|
|
293
|
+
'''Redirect the controller'''
|
|
294
|
+
return redirect_to(*pargs, **kargs)
|
|
295
|
+
|
|
296
|
+
def _not_found(self, text=None):
|
|
297
|
+
'''Raise the 404 http_client_error exception.'''
|
|
298
|
+
raise exc.HTTPNotFound(text)
|
|
299
|
+
|
|
300
|
+
def _status(self, code):
|
|
301
|
+
'''Raise an http_client_error exception using a specific code'''
|
|
302
|
+
raise exc.status_map[int(code)]
|
|
303
|
+
|
|
304
|
+
def _JSON(self, data, status=200):
|
|
305
|
+
'''Return JSON object with the proper-ish headers.'''
|
|
306
|
+
res = Response(body=json.dumps(data),
|
|
307
|
+
status=status,
|
|
308
|
+
content_type="application/json",
|
|
309
|
+
charset='UTF-8')
|
|
310
|
+
return res
|
|
311
|
+
|
|
312
|
+
# alias for backwards copatibility
|
|
313
|
+
BaseController = Controller
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<thead>
|
|
2
|
+
<tr>
|
|
3
|
+
%for field in collection.render_fields.itervalues():
|
|
4
|
+
<th>${field.label()|h}</th>
|
|
5
|
+
%endfor
|
|
6
|
+
</tr>
|
|
7
|
+
</thead>
|
|
8
|
+
|
|
9
|
+
<tbody>
|
|
10
|
+
%for i, row in enumerate(collection.rows):
|
|
11
|
+
<% collection._set_active(row) %>
|
|
12
|
+
<% row_errors = collection.get_errors(row) %>
|
|
13
|
+
<tr class="${i % 2 and 'odd' or 'even'}">
|
|
14
|
+
%for field in collection.render_fields.itervalues():
|
|
15
|
+
<td>
|
|
16
|
+
${field.render()|n}
|
|
17
|
+
%for error in row_errors.get(field, []):
|
|
18
|
+
<span class="grid_error">${error}</span>
|
|
19
|
+
%endfor
|
|
20
|
+
</td>
|
|
21
|
+
%endfor
|
|
22
|
+
</tr>
|
|
23
|
+
%endfor
|
|
24
|
+
</tbody>
|