Skip to content

Commit

Permalink
Bundle python3-flask-bootstrap assets into repository.
Browse files Browse the repository at this point in the history
* 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
sadsfae committed Apr 26, 2024
1 parent af15f27 commit 1245b38
Show file tree
Hide file tree
Showing 27 changed files with 21,904 additions and 1 deletion.
198 changes: 198 additions & 0 deletions assets/flask_bootstrap/__init__.py
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
153 changes: 153 additions & 0 deletions assets/flask_bootstrap/forms.py
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')
Loading

0 comments on commit 1245b38

Please sign in to comment.