-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Bundle python3-flask-bootstrap assets into repository.
* This is no longer packaged in Fedora but we need them so including them into our repo and package. * fixes: #470 Change-Id: I320b16a49c06dd3f6189cc9aa90e657fd9fc4861
- Loading branch information
Showing
27 changed files
with
21,904 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
#!/usr/bin/env python | ||
# coding=utf8 | ||
|
||
import re | ||
|
||
from flask import Blueprint, current_app, url_for | ||
|
||
try: | ||
from wtforms.fields import HiddenField | ||
except ImportError: | ||
|
||
def is_hidden_field_filter(field): | ||
raise RuntimeError('WTForms is not installed.') | ||
else: | ||
|
||
def is_hidden_field_filter(field): | ||
return isinstance(field, HiddenField) | ||
|
||
|
||
from .forms import render_form | ||
|
||
__version__ = '3.3.7.1.dev1' | ||
BOOTSTRAP_VERSION = re.sub(r'^(\d+\.\d+\.\d+).*', r'\1', __version__) | ||
JQUERY_VERSION = '1.12.4' | ||
HTML5SHIV_VERSION = '3.7.3' | ||
RESPONDJS_VERSION = '1.4.2' | ||
|
||
|
||
class CDN(object): | ||
"""Base class for CDN objects.""" | ||
|
||
def get_resource_url(self, filename): | ||
"""Return resource url for filename.""" | ||
raise NotImplementedError | ||
|
||
|
||
class StaticCDN(object): | ||
"""A CDN that serves content from the local application. | ||
:param static_endpoint: Endpoint to use. | ||
:param rev: If ``True``, honor ``BOOTSTRAP_QUERYSTRING_REVVING``. | ||
""" | ||
|
||
def __init__(self, static_endpoint='static', rev=False): | ||
self.static_endpoint = static_endpoint | ||
self.rev = rev | ||
|
||
def get_resource_url(self, filename): | ||
extra_args = {} | ||
|
||
if self.rev and current_app.config['BOOTSTRAP_QUERYSTRING_REVVING']: | ||
extra_args['bootstrap'] = __version__ | ||
|
||
return url_for(self.static_endpoint, filename=filename, **extra_args) | ||
|
||
|
||
class WebCDN(object): | ||
"""Serves files from the Web. | ||
:param baseurl: The baseurl. Filenames are simply appended to this URL. | ||
""" | ||
|
||
def __init__(self, baseurl): | ||
self.baseurl = baseurl | ||
|
||
def get_resource_url(self, filename): | ||
return self.baseurl + filename | ||
|
||
|
||
class ConditionalCDN(object): | ||
"""Serves files from one CDN or another, depending on whether a | ||
configuration value is set. | ||
:param confvar: Configuration variable to use. | ||
:param primary: CDN to use if the configuration variable is ``True``. | ||
:param fallback: CDN to use otherwise. | ||
""" | ||
|
||
def __init__(self, confvar, primary, fallback): | ||
self.confvar = confvar | ||
self.primary = primary | ||
self.fallback = fallback | ||
|
||
def get_resource_url(self, filename): | ||
if current_app.config[self.confvar]: | ||
return self.primary.get_resource_url(filename) | ||
return self.fallback.get_resource_url(filename) | ||
|
||
|
||
def bootstrap_find_resource(filename, cdn, use_minified=None, local=True): | ||
"""Resource finding function, also available in templates. | ||
Tries to find a resource, will force SSL depending on | ||
``BOOTSTRAP_CDN_FORCE_SSL`` settings. | ||
:param filename: File to find a URL for. | ||
:param cdn: Name of the CDN to use. | ||
:param use_minified': If set to ``True``/``False``, use/don't use | ||
minified. If ``None``, honors | ||
``BOOTSTRAP_USE_MINIFIED``. | ||
:param local: If ``True``, uses the ``local``-CDN when | ||
``BOOTSTRAP_SERVE_LOCAL`` is enabled. If ``False``, uses | ||
the ``static``-CDN instead. | ||
:return: A URL. | ||
""" | ||
config = current_app.config | ||
|
||
if None == use_minified: | ||
use_minified = config['BOOTSTRAP_USE_MINIFIED'] | ||
|
||
if use_minified: | ||
filename = '%s.min.%s' % tuple(filename.rsplit('.', 1)) | ||
|
||
cdns = current_app.extensions['bootstrap']['cdns'] | ||
resource_url = cdns[cdn].get_resource_url(filename) | ||
|
||
if resource_url.startswith('//') and config['BOOTSTRAP_CDN_FORCE_SSL']: | ||
resource_url = 'https:%s' % resource_url | ||
|
||
return resource_url | ||
|
||
|
||
class Bootstrap(object): | ||
def __init__(self, app=None): | ||
if app is not None: | ||
self.init_app(app) | ||
|
||
def init_app(self, app): | ||
app.config.setdefault('BOOTSTRAP_USE_MINIFIED', True) | ||
app.config.setdefault('BOOTSTRAP_CDN_FORCE_SSL', False) | ||
|
||
app.config.setdefault('BOOTSTRAP_QUERYSTRING_REVVING', True) | ||
app.config.setdefault('BOOTSTRAP_SERVE_LOCAL', False) | ||
|
||
app.config.setdefault('BOOTSTRAP_LOCAL_SUBDOMAIN', None) | ||
|
||
blueprint = Blueprint( | ||
'bootstrap', | ||
__name__, | ||
template_folder='templates', | ||
static_folder='static', | ||
static_url_path=app.static_url_path + '/bootstrap', | ||
subdomain=app.config['BOOTSTRAP_LOCAL_SUBDOMAIN']) | ||
|
||
# add the form rendering template filter | ||
blueprint.add_app_template_filter(render_form) | ||
|
||
app.register_blueprint(blueprint) | ||
|
||
app.jinja_env.globals['bootstrap_is_hidden_field'] =\ | ||
is_hidden_field_filter | ||
app.jinja_env.globals['bootstrap_find_resource'] =\ | ||
bootstrap_find_resource | ||
app.jinja_env.add_extension('jinja2.ext.do') | ||
|
||
if not hasattr(app, 'extensions'): | ||
app.extensions = {} | ||
|
||
local = StaticCDN('bootstrap.static', rev=True) | ||
static = StaticCDN() | ||
|
||
def lwrap(cdn, primary=static): | ||
return ConditionalCDN('BOOTSTRAP_SERVE_LOCAL', primary, cdn) | ||
|
||
bootstrap = lwrap( | ||
WebCDN('//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/%s/' % | ||
BOOTSTRAP_VERSION), local) | ||
|
||
jquery = lwrap( | ||
WebCDN('//cdnjs.cloudflare.com/ajax/libs/jquery/%s/' % | ||
JQUERY_VERSION), local) | ||
|
||
html5shiv = lwrap( | ||
WebCDN('//cdnjs.cloudflare.com/ajax/libs/html5shiv/%s/' % | ||
HTML5SHIV_VERSION)) | ||
|
||
respondjs = lwrap( | ||
WebCDN('//cdnjs.cloudflare.com/ajax/libs/respond.js/%s/' % | ||
RESPONDJS_VERSION)) | ||
|
||
app.extensions['bootstrap'] = { | ||
'cdns': { | ||
'local': local, | ||
'static': static, | ||
'bootstrap': bootstrap, | ||
'jquery': jquery, | ||
'html5shiv': html5shiv, | ||
'respond.js': respondjs, | ||
}, | ||
} | ||
|
||
# setup support for flask-nav | ||
renderers = app.extensions.setdefault('nav_renderers', {}) | ||
renderer_name = (__name__ + '.nav', 'BootstrapRenderer') | ||
renderers['bootstrap'] = renderer_name | ||
|
||
# make bootstrap the default renderer | ||
renderers[None] = renderer_name |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
from dominate import tags | ||
from dominate.util import raw | ||
from flask import current_app | ||
from markupsafe import Markup | ||
from visitor import Visitor | ||
|
||
|
||
def render_form(form, **kwargs): | ||
r = WTFormsRenderer(**kwargs) | ||
|
||
return Markup(r.visit(form)) | ||
|
||
|
||
class WTFormsRenderer(Visitor): | ||
def __init__(self, | ||
action='', | ||
id=None, | ||
method='post', | ||
extra_classes=[], | ||
role='form', | ||
enctype=None): | ||
self.action = action | ||
self.form_id = id | ||
self.method = method | ||
self.extra_classes = extra_classes | ||
self.role = role | ||
self.enctype = enctype | ||
|
||
def _visited_file_field(self): | ||
if self._real_enctype is None: | ||
self._real_enctype = u'multipart/form-data' | ||
|
||
def _get_wrap(self, node, classes='form-group'): | ||
# add required class, which strictly speaking isn't bootstrap, but | ||
# a common enough customization | ||
if node.flags.required: | ||
classes += ' required' | ||
|
||
div = tags.div(_class=classes) | ||
if current_app.debug: | ||
div.add(tags.comment(' Field: {} ({}) '.format( | ||
node.name, node.__class__.__name__))) | ||
|
||
return div | ||
|
||
def _wrapped_input(self, node, | ||
type='text', | ||
classes=['form-control'], **kwargs): | ||
wrap = self._get_wrap(node) | ||
wrap.add(tags.label(node.label.text, _for=node.id)) | ||
wrap.add(tags.input(type=type, _class=' '.join(classes), **kwargs)) | ||
|
||
return wrap | ||
|
||
def visit_BooleanField(self, node): | ||
wrap = self._get_wrap(node, classes='checkbox') | ||
|
||
label = wrap.add(tags.label(_for=node.id)) | ||
label.add(tags.input(type='checkbox')) | ||
label.add(node.label.text) | ||
|
||
return wrap | ||
|
||
def visit_DateField(self, node): | ||
return self._wrapped_input(node, 'date') | ||
|
||
def visit_DateTimeField(self, node): | ||
return self._wrapped_input(node, 'datetime-local') | ||
|
||
def visit_DecimalField(self, node): | ||
# FIXME: if range-validator is present, add limits? | ||
return self._wrapped_input(node, 'number') | ||
|
||
def visit_EmailField(self, node): | ||
# note: WTForms does not actually have an EmailField, this function | ||
# is called by visit_StringField based on which validators are enabled | ||
return self._wrapped_input(node, 'email') | ||
|
||
def visit_Field(self, node): | ||
# FIXME: add error class | ||
|
||
wrap = self._get_wrap(node) | ||
|
||
# add the label | ||
wrap.add(tags.label(node.label.text, _for=node.id)) | ||
wrap.add(raw(node())) | ||
|
||
if node.description: | ||
wrap.add(tags.p(node.description, _class='help-block')) | ||
|
||
return wrap | ||
|
||
def visit_FileField(self, node): | ||
self._visited_file_field() | ||
return self._wrapped_input(node, 'file', classes=[]) | ||
|
||
def visit_FloatField(self, node): | ||
# FIXME: if range-validator is present, add limits? | ||
return self._wrapped_input(node, 'number') | ||
|
||
def visit_Form(self, node): | ||
form = tags.form(_class=' '.join(['form'] + self.extra_classes)) | ||
|
||
if self.action: | ||
form['action'] = self.action | ||
|
||
if self.form_id: | ||
form['id'] = self.form_id | ||
|
||
if self.method: | ||
form['method'] = self.method | ||
|
||
# prepare enctype, this will be auto-updated by file fields if | ||
# necessary | ||
self._real_enctype = self.enctype | ||
|
||
# render fields | ||
for field in node: | ||
elem = self.visit(field) | ||
form.add(elem) | ||
|
||
if self._real_enctype: | ||
form['enctype'] = self._real_enctype | ||
|
||
return form | ||
|
||
def visit_HiddenField(self, node): | ||
return raw(node()) | ||
|
||
def visit_IntegerField(self, node): | ||
# FIXME: if range-validator is present, add limits? | ||
return self._wrapped_input(node, 'number', step=1) | ||
|
||
def visit_PasswordField(self, node): | ||
return self._wrapped_input(node, 'password') | ||
|
||
def visit_SubmitField(self, node): | ||
button = tags.button(node.label.text, | ||
_class='btn btn-default', | ||
type='submit') | ||
return button | ||
|
||
def visit_TextField(self, node): | ||
# legacy support for TextField, deprecated in WTForms 2.0 | ||
return self.visit_StringField(node) | ||
|
||
def visit_StringField(self, node): | ||
for v in node.validators: | ||
if v.__class__.__name__ == 'Email': | ||
# render email fields differently | ||
return self.visit_EmailField(node) | ||
|
||
return self._wrapped_input(node, 'text') |
Oops, something went wrong.