├── .checkignore ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── flask_extras ├── __init__.py ├── decorators.py ├── filters │ ├── __init__.py │ ├── config.py │ ├── datetimes.py │ ├── filters.py │ ├── layout.py │ ├── munging.py │ └── random.py ├── forms │ ├── __init__.py │ ├── validators │ │ ├── __init__.py │ │ ├── network.py │ │ └── serialization.py │ └── wizard.py ├── macros │ ├── __init__.py │ ├── bootstrap.html │ ├── content_blocks.html │ ├── dates.html │ ├── extras_code.html │ ├── extras_msg.html │ ├── macros.html │ └── utils.html └── views │ ├── __init__.py │ └── statuses.py ├── macros.md ├── setup.py ├── test_app ├── app.py ├── static │ ├── bootstrap.min.css │ ├── bootstrap.min.js │ ├── font-awesome.min.css │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ ├── glyphicons-halflings-regular.woff2 │ └── jquery-2.2.4.min.js └── templates │ ├── layouts │ └── base.html │ └── pages │ ├── bootstrap.html │ ├── content_blocks.html │ ├── dates.html │ ├── extras_code.html │ ├── extras_msg.html │ ├── index.html │ ├── macros.html │ └── utils.html ├── tests ├── __init__.py ├── conftest.py ├── test_config.py ├── test_datetimes.py ├── test_decorators.py ├── test_filters.py ├── test_layout.py ├── test_munging.py ├── test_random.py ├── test_validators_network.py ├── test_validators_serialization.py └── test_wizard.py ├── tox.ini └── wiki └── old_setup.md /.checkignore: -------------------------------------------------------------------------------- 1 | # Ignore folder content 2 | tests/* 3 | 4 | # Ignore file in all folders 5 | tests/**/test_*.py 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ---------- Misc --------- # 2 | 3 | *.sublime-* 4 | *.log 5 | *.pyc 6 | settings_dev.py 7 | *.db 8 | node_modules/ 9 | /**/node_modules/ 10 | /**/*.log 11 | ignored 12 | .DS_Store 13 | .idea 14 | *.log 15 | src/ensemble-app 16 | __pycache__ 17 | 18 | # Byte-compiled / optimized / DLL files 19 | __pycache__/ 20 | *.py[cod] 21 | *$py.class 22 | 23 | python3* 24 | python2.* 25 | 26 | # C extensions 27 | *.so 28 | 29 | # Distribution / packaging 30 | .Python 31 | env/ 32 | build/ 33 | develop-eggs/ 34 | ./dist/ 35 | downloads/ 36 | eggs/ 37 | .eggs/ 38 | lib/ 39 | lib64/ 40 | parts/ 41 | sdist/ 42 | var/ 43 | *.egg-info/ 44 | .installed.cfg 45 | *.egg 46 | 47 | # PyInstaller 48 | # Usually these files are written by a python script from a template 49 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 50 | *.manifest 51 | *.spec 52 | 53 | # Installer logs 54 | pip-log.txt 55 | pip-delete-this-directory.txt 56 | 57 | # Unit test / coverage reports 58 | htmlcov/ 59 | .tox/ 60 | .coverage 61 | .coverage.* 62 | .cache 63 | nosetests.xml 64 | coverage.xml 65 | *,cover 66 | .hypothesis/ 67 | 68 | # Translations 69 | *.mo 70 | *.pot 71 | 72 | # Django stuff: 73 | *.log 74 | local_settings.py 75 | 76 | # Flask stuff: 77 | instance/ 78 | .webassets-cache 79 | 80 | # Scrapy stuff: 81 | .scrapy 82 | 83 | # Sphinx documentation 84 | docs/_build/ 85 | 86 | # PyBuilder 87 | target/ 88 | 89 | # IPython Notebook 90 | .ipynb_checkpoints 91 | 92 | # pyenv 93 | .python-version 94 | 95 | # celery beat schedule file 96 | celerybeat-schedule 97 | 98 | # dotenv 99 | .env 100 | 101 | # virtualenv 102 | venv/ 103 | ENV/ 104 | 105 | # Spyder project settings 106 | .spyderproject 107 | 108 | # Rope project settings 109 | .ropeproject 110 | 111 | # ---------- Virtual Env --------- # 112 | bin/ 113 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | test: 2 | script 3 | sudo: false 4 | python: 5 | - "2.7" 6 | language: python 7 | install: 8 | - pip install -U pytest 9 | - python setup.py install 10 | script: pytest tests 11 | after_install: 12 | - pip install pytest-cov 13 | - pytest tests --cov=flask_jsondash tests 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Chris Tabor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft flask_extras 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | nosetests -w tests 3 | all: test 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/1f8f45e92a9b4ed1ab5029ee7a0e5534)](https://www.codacy.com/app/dxdstudio/flask_extras?utm_source=github.com&utm_medium=referral&utm_content=christabor/flask_extras&utm_campaign=badger) 2 | [![Build Status](https://travis-ci.org/christabor/flask_extras.svg?branch=master)](https://travis-ci.org/christabor/flask_extras) 3 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/christabor/flask_extras/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/christabor/flask_extras/?branch=master) 4 | [![Code Climate](https://codeclimate.com/github/christabor/flask_extras/badges/gpa.svg)](https://codeclimate.com/github/christabor/flask_extras) 5 | [![Coverage Status](https://coveralls.io/repos/github/christabor/flask_extras/badge.svg?branch=master)](https://coveralls.io/github/christabor/flask_extras?branch=master) 6 | [![Code Health](https://landscape.io/github/christabor/flask_extras/master/landscape.svg?style=flat)](https://landscape.io/github/christabor/flask_extras/master) 7 | 8 | # Flask Extras 9 | Assorted useful flask views, blueprints, Jinja2 template filters, and templates/macros. 10 | 11 | ## Overall setup 12 | 13 | As of `3.4.0`, filters and templates will automatically be registered and available through the following simple command: 14 | 15 | ```python 16 | from flask_extras import FlaskExtras 17 | app = Flask('myapp') 18 | FlaskExtras(app) 19 | ``` 20 | 21 | For the old way, check out [this page](wiki/old_setup.md) 22 | 23 | ## Available features 24 | 25 | ### Views 26 | 27 | Import them like usual: 28 | 29 | ```python 30 | from flask_extras.views import ( 31 | statuses, 32 | ) 33 | ``` 34 | 35 | *Note:* each view must have a valid template in your apps templates dir. See each view for the required names and locations. 36 | 37 | *Note:* each view has configuration helpers to inject or configure your app. See source for details. 38 | 39 | ### Macros 40 | 41 | All macro DEMOS are available by cloning this repo and firing up the flask test server, e.g: 42 | 43 | ```shell 44 | git clone https://github.com/christabor/flask_extras 45 | cd flask_extras 46 | virtualenv env 47 | source env/bin/active 48 | python setup.py install 49 | cd test_app 50 | python app.py 51 | open http://localhost:5014 52 | ``` 53 | 54 | #### Philosophy 55 | 56 | Many macros leverage the structure of data to find a common mapping. For example, a dictionary looks a lot like a definition list (dl), and a list looks really like a ...list, in html terms. But stepping even further into things, more complex data structures can have fairly elegant mappings when using macros to hide away a lot of html cruft. 57 | 58 | This makes rendering complex server side data easy, without having to do lots of transformation. This won't work for cases where every last DOM element need to be stylized, but many users will find incredibly powerful tools available to make UI development much easier. 59 | 60 | **Many more macros** are available. You can use them like so: 61 | 62 | ```html 63 | {% from 'macros.html' import list_group, objects2table %} 64 | ``` 65 | 66 | For the most comprehensive docs, check out each [macro](flask_extras/macros/). Comment "docstrings" are inline using jinja2 comments (these are not rendered in your html). 67 | 68 | Also, check the source and/or output to see what classes are available for style overrides. 69 | 70 | ### Statuses 71 | 72 | Provides views for common status codes. Usage: 73 | 74 | ```python 75 | app = statuses.inject_error_views(app) 76 | ``` 77 | 78 | See source for more. 79 | 80 | ### Decorators 81 | 82 | See the source for more. Usage example: 83 | 84 | ```python 85 | from flask_extras.decorators import require_headers 86 | 87 | app.route('/') 88 | @require_headers(['X-Foo']) 89 | def foo(): 90 | pass 91 | ``` 92 | 93 | 94 | ### Forms 95 | 96 | #### WTForm Multi-step wizard 97 | 98 | A WTForm extension for handling an arbitrary number of separate forms as a single, multi-step, multi-POST wizard. All state and data are handled by apps' session backend. Building forms is just like you're used to -- simple and intuitive. Just inherit the `MultiStepWizard` class and put a `__forms__` key on it, which is just a list of all the forms you want to use. *Note*: list order matters for your form steps. 99 | 100 | Usage example: 101 | 102 | ```python 103 | from flask.ext.wtf import FlaskForm 104 | 105 | from flask_extras.forms.wizard import MultiStepWizard 106 | 107 | 108 | class MultiStepTest1(FlaskForm): 109 | field1 = StringField(validators=[validators.DataRequired()],) 110 | field2 = IntegerField(validators=[validators.DataRequired()],) 111 | 112 | 113 | class MultiStepTest2(FlaskForm): 114 | field3 = StringField(validators=[validators.DataRequired()],) 115 | field4 = IntegerField(validators=[validators.DataRequired()],) 116 | 117 | 118 | class MyCoolForm(MultiStepWizard): 119 | __forms__ = [ 120 | MultiStepTest1, 121 | MultiStepTest2, 122 | ] 123 | ``` 124 | 125 | and an example route: 126 | 127 | ```python 128 | from forms import MyCoolForm 129 | 130 | @app.route('/', methods=['GET', 'POST']) 131 | def index(): 132 | curr_step = request.args.get('curr_step') 133 | form_kwargs = dict(session_key='mycustomkey') 134 | if curr_step is not None: 135 | form_kwargs.update(curr_step=curr_step) 136 | form = forms.MyCoolForm(**form_kwargs) 137 | kwargs = dict(form=form) 138 | if request.method == 'POST': 139 | if form.validate_on_submit(): 140 | if form.is_complete(): 141 | data = form.alldata(combine_fields=True, flush_after=True) 142 | flash('Form validated and complete! data = {}'.format(data), 143 | 'success') 144 | return jsonify(data) 145 | else: 146 | flash('Great job, but not done yet ({} steps remain!).'.format(form.remaining)) 147 | else: 148 | flash('Invalid form data.', 'error') 149 | return render_template('index.html', **kwargs) 150 | ``` 151 | 152 | and an example html page (using the [wtform_form](flask_extras/macros/macros.html) macro also available): 153 | 154 | ```html 155 | {% if form.is_complete() %} 156 | Complete! 157 | {% else %} 158 | 172 | {{ wtform_form(form, 173 | classes=['form', 'form-horizontal'], 174 | btn_classes=['btn btn-primary', 'btn-lg'], 175 | align='right', 176 | action=url_for('app.index'), 177 | method='POST', 178 | reset_btn=False, 179 | horizontal=True, 180 | ) }} 181 | {% endif %} 182 | ``` 183 | -------------------------------------------------------------------------------- /flask_extras/__init__.py: -------------------------------------------------------------------------------- 1 | """Hook to setup app easily.""" 2 | 3 | import os 4 | 5 | from flask_extras import macros 6 | from flask_extras.filters import config as filter_conf 7 | 8 | import jinja2 9 | 10 | 11 | def FlaskExtras(app): 12 | """Setup app config.""" 13 | extra_folders = jinja2.ChoiceLoader([ 14 | app.jinja_loader, 15 | jinja2.FileSystemLoader(os.path.dirname(macros.__file__)), 16 | ]) 17 | app.jinja_loader = extra_folders 18 | # Setup template filters 19 | filter_conf.config_flask_filters(app) 20 | -------------------------------------------------------------------------------- /flask_extras/decorators.py: -------------------------------------------------------------------------------- 1 | """App view decorators.""" 2 | 3 | from functools import wraps 4 | 5 | from flask import ( 6 | abort, 7 | request, 8 | ) 9 | 10 | 11 | def require_headers(headers=[]): 12 | """Check for required headers in a view. 13 | 14 | @require_headers(headers=['X-Foo']) 15 | def view(): 16 | pass 17 | """ 18 | def outer(func, *args, **kwargs): 19 | @wraps(func) 20 | def inner(*args, **kwargs): 21 | if headers: 22 | s1, s2 = set(headers), set([h[0] for h in request.headers]) 23 | matches = s1.intersection(s2) 24 | diff = s1.difference(s2) 25 | if len(s1) != len(matches): 26 | raise ValueError( 27 | 'Missing required header(s): {}'.format(list(diff))) 28 | return func(*args, **kwargs) 29 | return inner 30 | return outer 31 | 32 | 33 | def require_cookies(cookies=[]): 34 | """Check for required cookies in a view. 35 | 36 | @require_cookies(cookies=['csrftoken', 'session']) 37 | def view(): 38 | pass 39 | """ 40 | def outer(func, *args, **kwargs): 41 | @wraps(func) 42 | def inner(*args, **kwargs): 43 | if cookies: 44 | s1 = set(cookies) 45 | s2 = set([k for k, v in request.cookies.items()]) 46 | matches = s1.intersection(s2) 47 | diff = s1.difference(s2) 48 | if len(s1) != len(matches): 49 | raise ValueError( 50 | 'Missing required cookie(s): {}'.format(list(diff))) 51 | return func(*args, **kwargs) 52 | return inner 53 | return outer 54 | 55 | 56 | def require_args(params=[]): 57 | """Check for required args (and values) in a view. 58 | 59 | @require_args(params=['paginate']) 60 | def view(): 61 | pass 62 | 63 | or, if you want to check both key and value: 64 | 65 | @require_args(params={'paginate': True}) 66 | def view(): 67 | pass 68 | """ 69 | def outer(func, *args, **kwargs): 70 | @wraps(func) 71 | def inner(*args, **kwargs): 72 | if params: 73 | if isinstance(params, list): 74 | s1 = set(params) 75 | s2 = set([k for k, v in request.args.items()]) 76 | matches = s1.intersection(s2) 77 | diff = s1.difference(s2) 78 | if len(s1) != len(matches): 79 | raise ValueError( 80 | 'Missing required arg(s): {}'.format(list(diff))) 81 | else: 82 | for param, val in params.items(): 83 | arg = request.args.get(param) 84 | if arg is None: 85 | raise ValueError( 86 | 'Missing param `{}`'.format(param)) 87 | if arg != val: 88 | raise ValueError( 89 | 'Invalid value `{}` ' 90 | 'for param {}.'.format(arg, param)) 91 | return func(*args, **kwargs) 92 | return inner 93 | return outer 94 | 95 | 96 | def require_form(values=[]): 97 | """Check for required form values. 98 | 99 | @require_form(values=['name', 'address']) 100 | def view(): 101 | pass 102 | """ 103 | def outer(func, *args, **kwargs): 104 | @wraps(func) 105 | def inner(*args, **kwargs): 106 | if request.method == 'POST': 107 | if values: 108 | s1 = set(values) 109 | s2 = set([k for k, v in request.form.items()]) 110 | matches = s1.intersection(s2) 111 | diff = s1.difference(s2) 112 | if len(s1) != len(matches): 113 | raise ValueError( 114 | 'Missing required form ' 115 | 'field(s): {}'.format(list(diff))) 116 | return func(*args, **kwargs) 117 | return inner 118 | return outer 119 | 120 | 121 | def xhr_only(status_code=415): 122 | """Asssure request is XHR only. 123 | 124 | @xhr_only() 125 | def view(): 126 | pass 127 | """ 128 | def outer(func, *args, **kwargs): 129 | @wraps(func) 130 | def inner(*args, **kwargs): 131 | if not request.is_xhr: 132 | # Default to status "unsupported media type". 133 | abort(status_code) 134 | return func(*args, **kwargs) 135 | return inner 136 | return outer 137 | -------------------------------------------------------------------------------- /flask_extras/filters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christabor/flask_extras/f57300bc2922aa4105d1aa393351b63c86c26048/flask_extras/filters/__init__.py -------------------------------------------------------------------------------- /flask_extras/filters/config.py: -------------------------------------------------------------------------------- 1 | """Provides configuration utilities for using the filters.""" 2 | 3 | from __future__ import absolute_import 4 | 5 | from inspect import getmembers 6 | from inspect import isfunction 7 | 8 | from . import datetimes 9 | from . import filters 10 | from . import munging 11 | from . import random 12 | 13 | 14 | def _get_funcs(module): 15 | """Extract all functions from a module. 16 | 17 | Args: 18 | module (module): A python module reference. 19 | 20 | Returns: 21 | funcs (dict): A dictionary of names and functions extracted. 22 | 23 | """ 24 | return {name: func for name, func 25 | in getmembers(module) if isfunction(func)} 26 | 27 | 28 | def _inject_filters(app, filters): 29 | """Inject a set of filters into a Flask app. 30 | 31 | Args: 32 | app (object): The Flask application. 33 | filters (dict): A dictionary of name and functions. 34 | 35 | Returns: 36 | app (object): The Flask application. 37 | """ 38 | for name, func in filters.iteritems(): 39 | app.jinja_env.filters[name] = func 40 | return app 41 | 42 | 43 | def config_flask_filters(app): 44 | """Register a Flask app with all the available filters. 45 | 46 | Args: 47 | app (object): The Flask application instance. 48 | filters (list): The list of filter functions to use. 49 | 50 | Returns: 51 | app (object): The modified Flask application instance. 52 | """ 53 | # Manually register all module functions that were imported. 54 | app = _inject_filters(app, _get_funcs(filters)) 55 | app = _inject_filters(app, _get_funcs(random)) 56 | app = _inject_filters(app, _get_funcs(munging)) 57 | app = _inject_filters(app, _get_funcs(datetimes)) 58 | return app 59 | 60 | 61 | def _inject_template_globals(app, funcs): 62 | """Inject a set of functions into a Flask app as template_globals. 63 | 64 | Args: 65 | app (object): The Flask application. 66 | funcs (dict): A dictionary of name and functions. 67 | 68 | Returns: 69 | app (object): The Flask application. 70 | """ 71 | for name, func in funcs.iteritems(): 72 | app.add_template_global(name, func) 73 | return app 74 | 75 | 76 | def config_flask_globals(app): 77 | """Configure a Flask app to use all functions as template_globals.""" 78 | app = _inject_template_globals(app, _get_funcs(filters)) 79 | return app 80 | -------------------------------------------------------------------------------- /flask_extras/filters/datetimes.py: -------------------------------------------------------------------------------- 1 | """Date and date-time related filters.""" 2 | 3 | from dateutil.parser import parse as dtparse 4 | 5 | 6 | def str2dt(timestr): 7 | """Convert a string date to a real date. 8 | 9 | Args: 10 | timestr (str) - the datetime as a raw string. 11 | Returns: 12 | dateutil.parser.parse - the parsed datetime object. 13 | """ 14 | try: 15 | return dtparse(timestr) 16 | except (TypeError, ValueError): 17 | if timestr in ['None', 'null']: 18 | return None 19 | return timestr 20 | -------------------------------------------------------------------------------- /flask_extras/filters/filters.py: -------------------------------------------------------------------------------- 1 | """Standard filters.""" 2 | 3 | import re 4 | 5 | import json 6 | 7 | from string import ascii_lowercase 8 | 9 | 10 | def camel2hyphen(string, **kwargs): 11 | """Convert a camelCaseString into a hyphenated one. 12 | 13 | Args: 14 | string (str): The string to format. 15 | 16 | Returns: 17 | string (str): The formatted string. 18 | """ 19 | # CREDIT: http://stackoverflow.com/questions/1175208/ 20 | # elegant-python-function-to-convert-camelcase-to-snake-case 21 | # (modified to use hyphen instead of underscores.) 22 | s1 = re.sub('(.)([A-Z][a-z]+)', r'\1-\2', string) 23 | return re.sub('([a-z0-9])([A-Z])', r'\1-\2', s1).lower() 24 | 25 | 26 | def to_json(string, **kwargs): 27 | """Render json using native python module. 28 | 29 | Args: 30 | string (str): The string to format. 31 | 32 | Returns: 33 | string (str): The formatted string. 34 | """ 35 | return json.dumps(string, **kwargs) 36 | 37 | 38 | def css_selector(string, lowercase=True): 39 | """Convert a string to a css selector friendly format. 40 | 41 | Args: 42 | word (str): The string to format. 43 | 44 | Returns: 45 | word (str): The formatted word. 46 | """ 47 | if not isinstance(string, str): 48 | return string 49 | string = re.sub(r'[^a-zA-Z0-9]', '-', string) 50 | string = re.sub(r'\-{2,}', '-', string) 51 | if not lowercase: 52 | return string 53 | return string.lower() 54 | 55 | 56 | def title(word, capitalize=False): 57 | """Convert a string to a title format, where the words are capitalized. 58 | 59 | Args: 60 | word (str): The string to format. 61 | 62 | Returns: 63 | word (str): The formatted word. 64 | """ 65 | def _capitalize(w): 66 | return '{0}{1}'.format(w[0].upper(), w[1:]) 67 | 68 | if word is None: 69 | return '' 70 | words = word.split(' ') 71 | for i, word in enumerate(words): 72 | if i == 0 or capitalize: 73 | words[i] = _capitalize(word) 74 | return ' '.join(words) 75 | 76 | 77 | def firstof(seq): 78 | """Return the first item that is truthy in a sequence. 79 | 80 | Equivalent to Djangos' firstof. 81 | 82 | Args: 83 | seq (list): A list of values. 84 | 85 | Returns: 86 | value (mixed): The output, depending on the truthiness of the input. 87 | """ 88 | if not any(seq): 89 | return '' 90 | if all(seq): 91 | return seq[0] 92 | while seq: 93 | item = seq.pop(0) 94 | if item: 95 | return item 96 | return '' 97 | 98 | 99 | def questionize_label(word): 100 | """Convert a word to a true/false style question format. 101 | 102 | If a user follows the convention of using `is_something`, or 103 | `has_something`, for a boolean value, the *property* text will 104 | automatically be converted into a more human-readable 105 | format, e.g. 'Something?' for is_ and Has Something? for has_. 106 | 107 | Args: 108 | word (str): The string to format. 109 | 110 | Returns: 111 | word (str): The formatted word. 112 | """ 113 | if word is None: 114 | return '' 115 | if word.startswith('is_'): 116 | return '{0}?'.format(word[3:]) 117 | elif word.startswith('has_'): 118 | return '{0}?'.format(word[4:]) 119 | return word 120 | 121 | 122 | def add(lst, arg): 123 | """Add an item to a list. 124 | 125 | Equivalent to Djangos' add. 126 | 127 | Args: 128 | lst (list): A list. 129 | arg (mixed): Any value to append to the list. 130 | 131 | Returns: 132 | list: The updated list. 133 | """ 134 | lst.append(arg) 135 | return lst 136 | 137 | 138 | def cut(val, removals): 139 | """Remove some value from a string. 140 | 141 | Similar to Djangos' cut, but accepts N arguments to remove, in turn. 142 | 143 | Args: 144 | val (str): A string. 145 | removals (list): Alist of values to remove in turn, from the value. 146 | 147 | Returns: 148 | str: The updated string. 149 | """ 150 | def _cut(val, tocut): 151 | return val.replace(tocut, '') 152 | for r in removals: 153 | val = _cut(val, r) 154 | return val 155 | 156 | 157 | def addslashes(val): 158 | """Add slashes before all single quotes in a given string. 159 | 160 | Equivalent to Djangos' addslashes. 161 | 162 | Args: 163 | val (str): A string. 164 | 165 | Returns: 166 | str: The updated string. 167 | """ 168 | return val.replace("'", "\\'") 169 | 170 | 171 | def default(val, default): 172 | """Default to a given value if another given value is falsy. 173 | 174 | Equivalent to Djangos' default. 175 | 176 | Args: 177 | val (mixed): A mixed value that is truthy or falsy. 178 | default (mixed): A default replacement value. 179 | 180 | Returns: 181 | mixed: The default given value, or the original value. 182 | """ 183 | return default if not val else val 184 | 185 | 186 | def default_if_none(val, default): 187 | """Default to a given value if another given value is None. 188 | 189 | Equivalent to Djangos' default_if_none. 190 | 191 | Args: 192 | val (mixed): A mixed value that may or may not be None. 193 | default (mixed): A default replacement value. 194 | 195 | Returns: 196 | mixed: The default given value, or the original value. 197 | """ 198 | return default if val is None else val 199 | 200 | 201 | def get_digit(val, index): 202 | """Return the digit of a value specified by an index. 203 | 204 | Equivalent to Djangos' get_digit. 205 | 206 | Args 207 | val (int): An integer. 208 | index (int): The index to check against. 209 | 210 | Returns: 211 | int: The original integer if index is invalid, otherwise the digit 212 | at the specified index. 213 | """ 214 | digits = reversed(list(str(val))) 215 | for k, digit in enumerate(digits): 216 | if k + 1 == int(index): 217 | return int(digit) 218 | return val 219 | 220 | 221 | def length_is(val, length): 222 | """Return True if the length of a given value matches a given length. 223 | 224 | Equivalent to Djangos' length_is. 225 | 226 | Args: 227 | val (mixed): A value to check the length of. 228 | length (int): The length to check. 229 | 230 | Returns: 231 | bool: The value of checking the length against length. 232 | """ 233 | return len(val) == length 234 | 235 | 236 | def is_url(val): 237 | """Return true if a value is a url string, otherwise false. 238 | 239 | Args: 240 | val (mixed): The value to check. 241 | 242 | Returns: 243 | bool: True if the value is an http string, False if not. 244 | """ 245 | if isinstance(val, (str, unicode)): 246 | return val.startswith('http://') or val.startswith('https://') 247 | return False 248 | 249 | 250 | def ljust(string, amt): 251 | """Left-align the value by the amount specified. 252 | 253 | Equivalent to Djangos' ljust. 254 | 255 | Args: 256 | string (str): The string to adjust. 257 | amt (int): The amount of space to adjust by. 258 | 259 | Returns: 260 | str: The padded string. 261 | """ 262 | return string.ljust(amt) 263 | 264 | 265 | def rjust(string, amt): 266 | """Right-align the value by the amount specified. 267 | 268 | Equivalent to Djangos' rjust. 269 | 270 | Args: 271 | string (str): The string to adjust. 272 | amt (int): The amount of space to adjust by. 273 | 274 | Returns: 275 | str: The padded string. 276 | """ 277 | return string.rjust(amt) 278 | 279 | 280 | def make_list(val, coerce_numbers=True): 281 | """Make a list from a given value. 282 | 283 | Roughly equivalent to Djangos' make_list, with some enhancements. 284 | 285 | Args: 286 | val (mixed): The value to convert. 287 | coerce_numbers (bool, optional): Whether or not string number 288 | should be coerced back to their original values. 289 | 290 | Returns: 291 | mixed: If dict is given, return d.items(). If list is given, return it. 292 | If integers given, return a list of digits. 293 | Otherwise, return a list of characters. 294 | """ 295 | if isinstance(val, dict): 296 | return val.items() 297 | if isinstance(val, list): 298 | return val 299 | vals = list(str(val)) 300 | if coerce_numbers and isinstance(val, str): 301 | lst = [] 302 | for v in vals: 303 | try: 304 | lst.append(int(v)) 305 | except ValueError: 306 | lst.append(v) 307 | return lst 308 | return vals 309 | 310 | 311 | def phone2numeric(phoneword): 312 | """Convert a phoneword string into the translated number equivalent. 313 | 314 | Roughly equivalent to Djangos' phone2numeric. 315 | 316 | Args: 317 | phoneword (str): The phoneword string. 318 | 319 | Returns: 320 | str: The converted string digits. 321 | """ 322 | two, three = ['a', 'b', 'c'], ['d', 'e', 'f'] 323 | four, five = ['g', 'h', 'i'], ['j', 'k', 'l'] 324 | six, seven = ['m', 'n', 'o'], ['p', 'q', 'r', 's'] 325 | eight, nine = ['t', 'u', 'v'], ['w', 'x', 'y', 'z'] 326 | newdigits = '' 327 | for digit in list(phoneword.lower()): 328 | if digit in two: 329 | newdigits += '2' 330 | elif digit in three: 331 | newdigits += '3' 332 | elif digit in four: 333 | newdigits += '4' 334 | elif digit in five: 335 | newdigits += '5' 336 | elif digit in six: 337 | newdigits += '6' 338 | elif digit in seven: 339 | newdigits += '7' 340 | elif digit in eight: 341 | newdigits += '8' 342 | elif digit in nine: 343 | newdigits += '9' 344 | else: 345 | newdigits += digit 346 | return newdigits 347 | 348 | 349 | def pagetitle(string, remove_first=False, divider=' > '): 350 | """Convert a string of characters to page-title format. 351 | 352 | Args: 353 | string (str): The string to conert. 354 | remove_first (bool, optional): Remove the first instance of the 355 | delimiter of the newly formed title. 356 | 357 | Returns: 358 | str: The converted string. 359 | """ 360 | _title = divider.join(string.split('/')) 361 | if remove_first: 362 | _title = _title.replace(divider, '', 1) 363 | return _title 364 | 365 | 366 | def slugify(string): 367 | """Convert a string of characters to URL slug format. 368 | 369 | All characters replacing all characters with hyphens if invalid. 370 | Roughly equivalent to Djangos' slugify. 371 | 372 | Args: 373 | string (str): The string to slugify. 374 | 375 | Returns: 376 | str: The slugified string. 377 | """ 378 | slug = '' 379 | accepted = ['-', '_'] + list(ascii_lowercase) + list('01234567890') 380 | end = len(string) - 1 381 | for k, char in enumerate(string.lower().strip()): 382 | if char not in accepted: 383 | # Forget about the last char if it would get replaced. 384 | if k < end: 385 | slug += '-' 386 | else: 387 | slug += char 388 | return slug 389 | 390 | 391 | def greet(name, greeting='Hello'): 392 | """Add a greeting to a given name. 393 | 394 | Args: 395 | name (str): The name to greet. 396 | greeting (str, optional): An optional greeting override. 397 | 398 | Returns: 399 | str: The updated greeting string. 400 | """ 401 | return '{0}, {1}!'.format(greeting, name) 402 | 403 | 404 | def islist(item): 405 | """Check if an is item is a list - not just a sequence. 406 | 407 | Args: 408 | item (mixed): The item to check as a list. 409 | 410 | Returns: 411 | result (bool): True if the item is a list, False if not. 412 | 413 | """ 414 | return isinstance(item, list) 415 | 416 | 417 | def sql2dict(queryset): 418 | """Return a SQL alchemy style query result into a list of dicts. 419 | 420 | Args: 421 | queryset (object): The SQL alchemy result. 422 | 423 | Returns: 424 | result (list): The converted query set. 425 | 426 | """ 427 | if queryset is None: 428 | return [] 429 | return [record.__dict__ for record in queryset] 430 | -------------------------------------------------------------------------------- /flask_extras/filters/layout.py: -------------------------------------------------------------------------------- 1 | """Filters for generating random data.""" 2 | 3 | from __future__ import absolute_import 4 | 5 | 6 | def bs3_cols(num_entries): 7 | """Return the appropriate bootstrap framework column width. 8 | 9 | Args: 10 | num_entries (int): The number of entries to determine column width for. 11 | 12 | Returns: 13 | int: The integer value for column width. 14 | """ 15 | if not isinstance(num_entries, int): 16 | return 12 17 | mappings = { 18 | 1: 12, 19 | 2: 6, 20 | 3: 4, 21 | 4: 3, 22 | 5: 2, 23 | 6: 2, 24 | } 25 | try: 26 | return mappings[num_entries] 27 | except KeyError: 28 | return 12 29 | -------------------------------------------------------------------------------- /flask_extras/filters/munging.py: -------------------------------------------------------------------------------- 1 | """Filters for working with data structures, munging, etc...""" 2 | 3 | from collections import OrderedDict 4 | 5 | 6 | def sort_dict_vals_from_reflist(dct, reflist): 7 | """Return sorted dict vals from reference list for reference (of vals). 8 | 9 | Args: 10 | dct (dict): The original dictionary 11 | reflist (list): The reference list of keys to use for sorting. 12 | Returns: 13 | list: A sorted list of 2-tuples representing 14 | the dictionary (as found in `dict.items()`) 15 | """ 16 | items = dct.items() 17 | items = [d for d in items if d[1] in reflist] 18 | return sorted(items, key=lambda x: reflist.index(x[1])) 19 | 20 | 21 | def sort_dict_keys_from_reflist(dct, reflist, omit=False): 22 | """Return sorted dict vals from reference list for reference (of keys). 23 | 24 | Args: 25 | dct (dict): The original dictionary 26 | reflist (list): The reference list of keys to use for sorting. 27 | Returns: 28 | list: A sorted list of 2-tuples representing 29 | the dictionary (as found in `dict.items()`) 30 | """ 31 | items = dct.items() 32 | items = [d for d in items if d[0] in reflist] 33 | return sorted(items, key=lambda x: reflist.index(x[0])) 34 | 35 | 36 | def filter_list(lst, vals): 37 | """Filter a list by vals. 38 | 39 | Args: 40 | lst (dict): The dictionary to filter. 41 | 42 | Returns: 43 | string (dict): The filtered dict. 44 | """ 45 | if any([not lst, not isinstance(lst, list), not isinstance(vals, list)]): 46 | return lst 47 | return list(set(lst).difference(set(vals))) 48 | 49 | 50 | def filter_vals(obj, vals): 51 | """Filter a dictionary by values. 52 | 53 | Args: 54 | obj (dict): The dictionary to filter. 55 | 56 | Returns: 57 | obj (dict): The filtered dict. 58 | """ 59 | if obj is None or not isinstance(vals, list): 60 | return obj 61 | newdict = {} 62 | for k, v in obj.items(): 63 | if v in vals: 64 | continue 65 | newdict[k] = v 66 | return newdict 67 | 68 | 69 | def filter_keys(obj, keys): 70 | """Filter a dictionary by keys. 71 | 72 | Args: 73 | obj (dict): The dictionary to filter. 74 | 75 | Returns: 76 | obj (dict): The filtered dict. 77 | """ 78 | if obj is None or not isinstance(keys, list): 79 | return obj 80 | newdict = {} 81 | for k, v in obj.items(): 82 | if k in keys: 83 | continue 84 | newdict[k] = v 85 | return newdict 86 | 87 | 88 | def group_by(objs, groups=[], attr='name', fallback='__unlabeled'): 89 | """Group a list of objects into an ordered dict grouped by specified keys. 90 | 91 | Args: 92 | objs: A list of objects 93 | keys: A list of 2-tuples where the first index is the group name, 94 | and the second key is a tuple of all matches. 95 | attr: The attr to use to get fields for matching (default: 'name') 96 | fallback: A fallback label to use for unspecified groups. 97 | 98 | Returns: 99 | An OrderedDict of grouped items. 100 | 101 | >>> group_by([obj1, obj2], 102 | groups=[('g1', ('name1', 'name2'))], attr='name') 103 | """ 104 | grouped = OrderedDict() 105 | if not groups or attr is None: 106 | return {fallback: objs} 107 | # Initial population since it's not a defaultdict. 108 | for ordered_group in groups: 109 | label, _ = ordered_group 110 | grouped[label] = [] 111 | seen = [] 112 | for ordered_group in groups: 113 | label, matches = ordered_group 114 | for curr in objs: 115 | attr_label = getattr(curr, attr) if hasattr(curr, attr) else '' 116 | if attr_label in seen: 117 | continue 118 | if attr_label in matches: 119 | idx = matches.index(attr_label) 120 | grouped[label].insert(idx, curr) 121 | seen.append(attr_label) 122 | # Add unlabeled extras last so order is preserved. 123 | grouped[fallback] = [ 124 | curr for curr in objs if getattr(curr, attr) not in seen 125 | ] 126 | return grouped 127 | -------------------------------------------------------------------------------- /flask_extras/filters/random.py: -------------------------------------------------------------------------------- 1 | """Filters for generating random data.""" 2 | 3 | from __future__ import absolute_import 4 | 5 | from random import choice, randrange 6 | 7 | 8 | def rand_choice(options): 9 | """Pick a random value from a range of numbers, like pythons' stdlib. 10 | 11 | Equivalent to Djangos' random. 12 | 13 | Args: 14 | options (list): A list of values. 15 | 16 | Returns: 17 | randchoice (mixed): The random choice, selected from the given list. 18 | """ 19 | return choice(options) 20 | 21 | 22 | def rand_name_title(name): 23 | """Pick a random title for a given name (e.g. ESQ. MD, etc...). 24 | 25 | Source: https://www.lehigh.edu/lewis/suffix.htm 26 | 27 | Args: 28 | options (str): A name, or other string. 29 | 30 | Returns: 31 | name (str): The name, with a random suffixed appended. 32 | """ 33 | titles = [ 34 | 'B.V.M.', 'CFRE', 'CLU', 'CPA', 'C.S.C.', 'C.S.J.', 'D.C.', 'D.D.', 35 | 'D.D.S.', 'D.M.D.', 'D.O.', 'D.V.M.', 'Ed.D.', 'Esq.', 'II', 'III', 36 | 'IV', 'Inc.', 'J.D.', 'Jr.', 'LL.D.', 'Ltd.', 'M.D.', 'O.D.', 37 | 'O.S.B.', 'P.C.', 'P.E.', 'Ph.D.', 'Ret.', 'R.G.S', 'R.N.', 'R.N.C.', 38 | 'S.H.C.J.', 'S.J.', 'S.N.J.M.', 'Sr.', 'S.S.M.O.', 'USA', 'USAF', 39 | 'USAFR', 'USAR', 'USCG', 'USMC', 'USMCR', 'USN', 'USNR', 40 | ] 41 | return '{0} {1}'.format(name, choice(titles)) 42 | 43 | 44 | def rand_color(alpha=100): 45 | """Return a random CSS friendly RGB color value. 46 | 47 | Args: 48 | alpha (int): The optional alpha value for the RGBA string. 49 | 50 | Returns: 51 | str: The RGB triplet for use with inline css. 52 | (e.g. rgba(10, 20, 30, 100)) 53 | """ 54 | return 'rgba({0}, {1}, {2}, {3})'.format( 55 | randrange(0, 255), randrange(0, 255), randrange(0, 255), alpha) 56 | -------------------------------------------------------------------------------- /flask_extras/forms/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christabor/flask_extras/f57300bc2922aa4105d1aa393351b63c86c26048/flask_extras/forms/__init__.py -------------------------------------------------------------------------------- /flask_extras/forms/validators/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christabor/flask_extras/f57300bc2922aa4105d1aa393351b63c86c26048/flask_extras/forms/validators/__init__.py -------------------------------------------------------------------------------- /flask_extras/forms/validators/network.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import socket 4 | 5 | from netaddr import iter_iprange 6 | from netaddr.core import AddrFormatError 7 | 8 | 9 | def is_ip(addr): 10 | """Determine if a string is really an ip, or a hostname instead. 11 | 12 | Args: 13 | addr (str): The ip address string to check 14 | 15 | Returns: 16 | bool: Whether or not `addr` is a valid ip. 17 | """ 18 | if '.' not in addr: 19 | return False 20 | parts = addr.split('.') 21 | for part in parts: 22 | try: 23 | int(part) 24 | except ValueError: 25 | return False 26 | return True 27 | 28 | 29 | def is_hostname(addr): 30 | """Determine if a string is a hostname. 31 | 32 | Based on https://en.wikipedia.org/wiki 33 | /Hostname#Restrictions_on_valid_hostnames 34 | 35 | Args: 36 | addr (str): The address string to check 37 | 38 | Returns: 39 | bool: Whether or not `addr` is a valid hostname. 40 | """ 41 | if any([ 42 | '_' in addr, 43 | addr.endswith('-'), 44 | is_ip(addr), 45 | ]): 46 | return False 47 | return True 48 | 49 | 50 | def valid_hosts(formcls, field): 51 | """Validate a list of IPs (Ipv4) or hostnames using python stdlib. 52 | 53 | This is more robust than the WTForm version as it also considers hostnames. 54 | 55 | Comma separated values: 56 | e.g. '10.7.223.101,10.7.12.0' 57 | Space separated values: 58 | e.g. '10.223.101 10.7.223.102' 59 | Ranges: 60 | e.g. '10.7.223.200-10.7.224.10' 61 | Hostnames: 62 | e.g. foo.x.y.com, baz.bar.z.com 63 | 64 | :param formcls (object): The form class. 65 | :param field (str): The list of ips. 66 | """ 67 | ip_re = re.compile(r'[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}') 68 | data = field.data 69 | if ',' in data: 70 | ips = [ip for ip in data.split(',') if ip] 71 | elif ' ' in data: 72 | ips = [ip for ip in data.split(' ') if ip] 73 | elif '-' in data and re.match(ip_re, data): 74 | try: 75 | start, end = data.split('-') 76 | ips = iter_iprange(start, end) 77 | ips = [str(ip) for ip in list(ips)] 78 | except ValueError: 79 | raise ValueError( 80 | 'Invalid range specified. Format should be: ' 81 | 'XXX.XXX.XXX.XXX-XXX.XXX.XXX.XXX ' 82 | '(e.g. 10.7.223.200-10.7.224.10)') 83 | except AddrFormatError as e: 84 | raise ValueError(e) 85 | else: 86 | # Just use the single ip 87 | ips = [data] 88 | # If any fails conversion, it is invalid. 89 | for ip in ips: 90 | # Skip hostnames 91 | if not is_ip(ip): 92 | if not is_hostname(ip): 93 | raise ValueError('Invalid hostname: "{}"'.format(ip)) 94 | else: 95 | continue 96 | try: 97 | socket.inet_aton(ip) 98 | except socket.error: 99 | raise ValueError('Invalid IP: {}'.format(ip)) 100 | -------------------------------------------------------------------------------- /flask_extras/forms/validators/serialization.py: -------------------------------------------------------------------------------- 1 | """Validators for various serialization formats.""" 2 | 3 | import json 4 | 5 | 6 | def valid_json(formcls, field): 7 | """Validate field data as a json. 8 | 9 | :param formcls (object): The form class. 10 | :param field (str): The list of ips. 11 | """ 12 | json.loads(field.data) 13 | -------------------------------------------------------------------------------- /flask_extras/forms/wizard.py: -------------------------------------------------------------------------------- 1 | """A simple multi-step wizard that uses the flask application session. 2 | 3 | Creating multi-step forms of arbitrary length is simple and intuitive. 4 | 5 | Example usage: 6 | 7 | ``` 8 | from flask.ext.wtf import FlaskForm 9 | 10 | class MultiStepTest1(FlaskForm): 11 | field1 = StringField(validators=[validators.DataRequired()],) 12 | field2 = StringField(validators=[validators.DataRequired()],) 13 | 14 | 15 | class MultiStepTest2(FlaskForm): 16 | field3 = StringField(validators=[validators.DataRequired()],) 17 | field4 = StringField(validators=[validators.DataRequired()],) 18 | 19 | 20 | class MyCoolForm(MultiStepWizard): 21 | __forms__ = [ 22 | MultiStepTest1, 23 | MultiStepTest2, 24 | ] 25 | ``` 26 | """ 27 | 28 | from flask import session 29 | from flask_wtf import FlaskForm 30 | 31 | 32 | class MultiStepWizard(FlaskForm): 33 | """Generates a multi-step wizard. 34 | 35 | The wizard uses the app specified session backend to store both 36 | form data and current step. 37 | 38 | TODO: make sure all the expected features of the typical form 39 | are exposed here, but automatically determining the active form 40 | and deferring to it. See __iter__ and data for examples. 41 | """ 42 | 43 | __forms__ = [] 44 | 45 | def __iter__(self): 46 | """Get the specific forms' fields for standard WTForm iteration.""" 47 | _, form = self.get_active() 48 | return form.__iter__() 49 | 50 | def __len__(self): 51 | """Override the len method to emulate standard wtforms.""" 52 | return len(self.__forms) 53 | 54 | def __getitem__(self, key): 55 | """Override getitem to emulate standard wtforms.""" 56 | return self.active_form.__getitem__(key) 57 | 58 | def __contains__(self, item): 59 | """Override contains to emulate standard wtforms.""" 60 | return self.active_form.__contains__(item) 61 | 62 | def __init__(self, *args, **kwargs): 63 | """Do all the required setup for managing the forms.""" 64 | super(MultiStepWizard, self).__init__(*args, **kwargs) 65 | # Store the name and session key by a user specified kwarg, 66 | # or fall back to this class name. 67 | self.name = kwargs.get('session_key', self.__class__.__name__) 68 | # Get the sessions' current step if it exists. 69 | curr_step = session.get(self.name, {}).get('curr_step', 1) 70 | # if the user specified a step, we'll use that instead. Form validation 71 | # will still occur, but this is useful for when the user may need 72 | # to go back a step or more. 73 | if 'curr_step' in kwargs: 74 | curr_step = int(kwargs.pop('curr_step')) 75 | if curr_step > len(self.__forms__): 76 | curr_step = 1 77 | self.step = curr_step 78 | # Store forms in a dunder because we want to avoid conflicts 79 | # with any WTForm objects or third-party libs. 80 | self.__forms = [] 81 | self._setup_session() 82 | self._populate_forms() 83 | invalid_forms_msg = 'Something happened during form population.' 84 | assert len(self.__forms) == len(self.__forms__), invalid_forms_msg 85 | assert len(self.__forms) > 0, 'Need at least one form!' 86 | self.active_form = self.get_active()[1] 87 | # Inject the required fields for the active form. 88 | # The multiform class will always be instantiated once 89 | # on account of separate POST requests, and so the previous form 90 | # values will no longer be attributes to be concerned with. 91 | self._setfields() 92 | 93 | def _setfields(self): 94 | """Dynamically set fields for this particular form step.""" 95 | _, form = self.get_active() 96 | for name, val in vars(form).items(): 97 | if repr(val).startswith(' 0: 144 | return 145 | for form in self.__forms__: 146 | data = session[self.name]['data'].get(form.__name__) 147 | init_form = form(**data) if data is not None else form() 148 | self.__forms.append(init_form) 149 | 150 | def _update_session_formdata(self, form): 151 | """Update session data for a given form key.""" 152 | # Add data to session for this current form! 153 | name = form.__class__.__name__ 154 | data = form.data 155 | # Update the session data for this particular form step. 156 | # The policy here is to always clobber old data. 157 | session[self.name]['data'][name] = data 158 | 159 | @property 160 | def active_name(self): 161 | """Return the nice name of this form class.""" 162 | return self.active_form.__class__.__name__ 163 | 164 | def next_step(self): 165 | """Set the step number in the session to the next value.""" 166 | next_step = session[self.name]['curr_step'] + 1 167 | self.curr_step = next_step 168 | if self.name in session: 169 | session[self.name]['curr_step'] += 1 170 | 171 | @property 172 | def step(self): 173 | """Get the current step.""" 174 | if self.name in session: 175 | return session[self.name]['curr_step'] 176 | 177 | @step.setter 178 | def step(self, step_val): 179 | """Set the step number in the session.""" 180 | self.curr_step = step_val 181 | if self.name in session: 182 | session[self.name]['curr_step'] = step_val 183 | 184 | def validate_on_submit(self, *args, **kwargs): 185 | """Override validator and setup session updates for persistence.""" 186 | # Update the step to the next form automagically for the user 187 | step, form = self.get_active() 188 | self._update_session_formdata(form) 189 | if not form.validate_on_submit(): 190 | self.step = step - 1 191 | return False 192 | # Update to next form if applicable. 193 | if step - 1 < len(self.__forms): 194 | self.curr_step += 1 195 | self.active_form = self.__forms[self.curr_step - 1] 196 | self.next_step() 197 | # Mark the current step as -1 to indicate it has been 198 | # fully completed, if the current step is the final step. 199 | elif step - 1 == len(self.__forms): 200 | self.step = -1 201 | return True 202 | 203 | @property 204 | def remaining(self): 205 | """Get the number of steps remaining.""" 206 | return len(self.__forms[self.curr_step:]) + 1 207 | 208 | @property 209 | def total_steps(self): 210 | """Get the number of steps for this form in a (non-zero index).""" 211 | return len(self.__forms) 212 | 213 | @property 214 | def steps(self): 215 | """Get a list of the steps for iterating in views, html, etc.""" 216 | return range(1, self.total_steps + 1) 217 | 218 | def get_active(self): 219 | """Get active step.""" 220 | form_index = self.curr_step - 1 if self.curr_step > 0 else 0 221 | return self.curr_step + 1, self.__forms[form_index] 222 | 223 | def flush(self): 224 | """Clear data and reset.""" 225 | del session[self.name] 226 | 227 | def is_complete(self): 228 | """Determine if all forms have been completed.""" 229 | if self.name not in session: 230 | return False 231 | # Make the current step index something unique for being "complete" 232 | completed = self.step == -1 233 | if completed: 234 | # Reset. 235 | self.curr_step = 1 236 | return completed 237 | -------------------------------------------------------------------------------- /flask_extras/macros/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christabor/flask_extras/f57300bc2922aa4105d1aa393351b63c86c26048/flask_extras/macros/__init__.py -------------------------------------------------------------------------------- /flask_extras/macros/bootstrap.html: -------------------------------------------------------------------------------- 1 | {% from 'utils.html' import apply_dattrs, apply_classes, apply_prop %} 2 | {% from 'macros.html' import objects2table %} 3 | 4 | {% macro progress(percent, 5 | animated=False, 6 | classes=[], 7 | context=None, 8 | min=0, 9 | max=100, 10 | striped=False, 11 | show_percent=True 12 | ) 13 | %} 14 | {# 15 | Create a bootstrap progress bar with lots of options. 16 | 17 | Usage: 18 | {{ progress(30) }} 19 | {{ progress(100, animated=True, striped=True) }} 20 | {{ progress(89, context='success') }} 21 | {{ progress(89, context='danger') }} 22 | {{ progress(89, context='warning') }} 23 | {{ progress(89, context='info') }} 24 | #} 25 |
26 |
27 | {{ percent }}% 28 |
29 |
30 | {% endmacro %} 31 | 32 | {% macro modal(id, content, 33 | classes=[], 34 | close_btns=True, 35 | fade=True, 36 | footer=None, 37 | show_footer=True, 38 | include_btn=False, 39 | include_btn_text='Trigger Modal', 40 | title=None 41 | ) 42 | %} 43 | {# 44 | Makes a bootstrap modal 45 | Usage: 46 | {{ modal('someId', 'Some html or text...', title='My Modal') }} 47 | #} 48 | {% if include_btn %} 49 | 50 | {{ include_btn_text }} 51 | 52 | {% endif %} 53 | 78 | {% endmacro %} 79 | 80 | {% macro dict2carousel(id, items, 81 | active=0, 82 | classes=[], 83 | data_attrs=[], 84 | show_arrows=True 85 | ) 86 | %} 87 | {# 88 | Makes a bootstrap carousel 89 | Usage: 90 | {{ dict2carousel( 91 | 'someId', 92 | [ 93 | {'content': '', 'caption': 'foo'}, 94 | {'content': '', 'caption': 'bar'}, 95 | {'content': '', 'caption': 'baz'}, 96 | ], 97 | active=2) 98 | }} 99 | #} 100 |
103 | 104 | 109 | 110 | 122 | 123 | {% if show_arrows %} 124 | 125 | 126 | 127 | 128 | 129 | 130 | {% endif %} 131 |
132 | {% endmacro %} 133 | 134 | 135 | {%- macro bs3_dictlist_group(data, 136 | classes=[], 137 | filterkeys=[], 138 | filtervals=[], 139 | data_attrs=[], 140 | asdict=False 141 | ) 142 | %} 143 | {# 144 | Makes a bootstrap list group from a set of dictionaries 145 | Usage: 146 | {{ bs3_dictlist_group({'foo': 'bar'}) }} 147 | 148 | Output: 149 |
150 |

foo

151 |

bar

152 |
153 | #} 154 | {% if asdict %}{% set data = data._asdict() %}{% endif %} 155 |
156 | {% for k, v in data.items() %} 157 | {% if k not in filterkeys and v not in filtervals %} 158 |
159 |

{{ k }}

160 |

{{ v }}

161 |
162 | {% endif %} 163 | {% endfor %} 164 |
165 | {% endmacro -%} 166 | 167 | 168 | {%- macro bs3_list_group(data, classes=[], type='ul') %} 169 | {# 170 | OL/UL based bootstrap list group 171 | 172 | Usage: 173 | {{ bs3_list_group(['foo', 'bar']) }} 174 | 175 | Output: 176 | 180 | #} 181 | <{{ type }} class="{{ apply_classes(classes + ['list-group']) }}"> 182 | {% for val in data %} 183 |
  • {{ val }}
  • 184 | {% endfor %} 185 | 186 | {% endmacro -%} 187 | 188 | 189 | {%- macro dictlist_group_badged(data, classes=[], type='ul', align='right') %} 190 | {# 191 | OL/UL based bootstrap list group 192 | with badge labels on the right (default). 193 | From: http://getbootstrap.com/components/#list-group-badges 194 | 195 | Usage: 196 | {{ dictlist_group_badged({'Messages': 20, 'Unread': 10, 'Read': 100, 'Starred': 34}) }} 197 | 198 | Output: 199 | 205 | #} 206 | <{{ type }} class="{{ apply_classes(classes + ['list-group']) }}"> 207 | {% for val, label in data.items() %} 208 |
  • 209 | {% if align == 'left' %} 210 | {{ label }} 211 | {% endif %} 212 | {{ val }} 213 | {% if align == 'right' %} 214 | {{ label }} 215 | {% endif %} 216 |
  • 217 | {% endfor %} 218 | 219 | {% endmacro -%} 220 | 221 | 222 | {%- macro bs3_label(value, label_map, text=None) -%} 223 | {# 224 | Convert a dict of values with corresponding label types given a value, to 225 | the appropriate label. 226 | 227 | Usage: 228 | {{ bs3_label('prod', {'dev': 'info', 'stage': 'warning', 'prod': 'danger'}) }} 229 | 230 | Output: 231 | prod 232 | #} 233 | {{ text if text else value }} 234 | {% endmacro -%} 235 | 236 | 237 | {%- macro bs3_panel(items, paneltype='default') -%} 238 | {# 239 | Generate bs3 panels for a dict of titles/body content. 240 | Usage: 241 | {{ bs3_panel({'Heading 1': 'Some body text... etc...', 'Heading 2': 'Some body text...'}, paneltype='info') }} 242 | 243 | Output: 244 |
    245 |
    246 |

    Heading 1

    247 |
    248 |
    249 | Some body text... etc... 250 |
    251 |
    252 |
    253 |
    254 |

    Heading 2

    255 |
    256 |
    257 | Some body text... 258 |
    259 |
    260 | #} 261 | {% for title, body in items.items() %} 262 |
    263 |
    264 |

    265 | {{ title }} 266 |

    267 |
    268 |
    269 | {{ body }} 270 |
    271 |
    272 | {% endfor %} 273 | {% endmacro -%} 274 | 275 | 276 | {%- macro bs3_breadcrumb(bcrumbs) %} 277 | {# 278 | Generate bs3 style breadcrumbs using a dynamic breadcrumbs object. 279 | (Most likely from something like flask-breadcrumbs.) 280 | Usage: 281 | {{ bs3_breadcrumb(breadcrumbs) }} 282 | #} 283 | {% if bcrumbs %} 284 | 295 | {% endif %} 296 | {% endmacro -%} 297 | 298 | 299 | {%- macro dict2labels(data, filterkeys=[], filtervals=[], aslist=False) %} 300 | {# 301 | Makes a dict of `name`:`label` into bootstrap labels 302 | Usage: 303 | {{ dict2labels({'foo': 'danger'}) }} 304 | foo 305 | 306 | {{ dict2labels({'foo': 'danger'}, aslist=False) }} 307 | foo 308 | 309 | Or wrap it in an html list by specifying `aslist`: 310 | {{ dict2labels({'foo': 'danger'}, aslist=True) }} 311 | #} 312 | {% if aslist %}{% endif %} 321 | {% endmacro -%} 322 | 323 | 324 | {%- macro dict2tabs(data, filterkeys=[], filtervals=[], 325 | asdict=False, 326 | id='', 327 | headings=False, 328 | heading_tag='h4', 329 | data_attrs=[], 330 | tab_links={}, 331 | active='', 332 | disabled=[], 333 | tab_classes=[], 334 | content_classes=[], 335 | tab_data_attrs=[], 336 | nav_tab_classes=[], 337 | tab_content_classes=[], 338 | content_data_attrs=[], 339 | classes=[]) %} 340 | {% if asdict %}{% set data = data._asdict() %}{% endif %} 341 | {# 342 | Make a dict into a bs3 tab group with tab content. Keys are tab labels, 343 | and values are tab content. 344 | Usage: 345 | {{ dict2tabs({"foo": "Some content rendered from html or macro"}) }} 346 | 347 | You can also customize styles, data-attrs, etc... see function signature for more info. 348 | 349 | You can even pass in content generated from other macros, such as the wtform_form macro: 350 | 351 | {{ 352 | dict2tabs({ 353 | "tab1": wtform_form(form1, action=url_for('app.page1')), 354 | "tab2": wtform_form(form2, action=url_for('app.page2')), 355 | id="MyCoolTabContainer" 356 | }) 357 | }} 358 | 359 | Note: unless active is specified, order will be determined by sorting the keys. If you need ordering to be specific to the dictionary key order, use `dictlist2tabs` instead. 360 | #} 361 |
    362 | 363 | 373 | 374 | 375 |
    376 | {% for label in data.keys()|sort %} 377 |
    379 | {% if headings %}<{{ heading_tag }}>{{ label }}{% endif %} 380 | {{ data[label]|safe }} 381 |
    382 | {% endfor %} 383 |
    384 |
    385 | {% endmacro -%} 386 | 387 | 388 | {%- macro dictlist2tabs(data, filterkeys=[], filtervals=[], 389 | asdict=False, 390 | id='', 391 | headings=False, 392 | heading_tag='h4', 393 | heading_macros={}, 394 | data_attrs=[], 395 | tab_links={}, 396 | active='', 397 | disabled=[], 398 | tab_classes=[], 399 | content_classes=[], 400 | tab_data_attrs=[], 401 | nav_tab_classes=[], 402 | tab_content_classes=[], 403 | content_data_attrs=[], 404 | classes=[]) %} 405 | {# 406 | Make a list of dicts into a bs3 tab group with tab content. Keys are tab labels, 407 | and values are tab content. 408 | Usage: 409 | {{ dictlist2tabs([{"foo": "Some content rendered from html or macro"}]) }} 410 | 411 | You can also customize styles, data-attrs, etc... see function signature for more info. 412 | 413 | You can even pass in content generated from other macros, such as the wtform_form macro: 414 | 415 | {{ 416 | dictlist2tabs([ 417 | {"tab1": wtform_form(form1, action=url_for('app.page1'))}, 418 | {"tab2": wtform_form(form2, action=url_for('app.page2'))}, 419 | ], 420 | id="MyCoolTabContainer", 421 | }) 422 | }} 423 | 424 | Note: unless active is specified, order will be determined by sorting the keys. 425 | #} 426 |
    427 | 428 | 445 | 446 | 447 |
    448 | {% for item in data %} 449 | {% set label = item.keys()[0] %} 450 |
    452 | {% if headings %}<{{ heading_tag }}>{{ label }}{% endif %} 453 | {{ item[label]|safe }} 454 |
    455 | {% endfor %} 456 |
    457 |
    458 | {% endmacro -%} 459 | 460 | 461 | {%- macro inline_list(lst, divider='|', classes=[], data_attrs=[]) %} 462 | 470 | {% endmacro -%} 471 | 472 | 473 | {%- macro inline_dictlist(dlist, divider='|', seperator=': ', classes=[], data_attrs=[]) %} 474 | 481 | {% endmacro -%} 482 | 483 | 484 | {%- macro dict2btn_group(dlist, size='md', classes=[], data_attrs=[], id=None) %} 485 |
    486 | {% for type, text in dlist.items() %} 487 | 488 | {% endfor %} 489 |
    490 | {% endmacro -%} 491 | 492 | 493 | {%- macro list2btn_group(list, btn_types='default', size='md', classes=[], data_attrs=[], id=None) %} 494 |
    495 | {% for item in list %} 496 | 497 | {% endfor %} 498 |
    499 | {% endmacro -%} 500 | 501 | 502 | {%- macro dict2_pagination(dct, prev=None, next=None, size='md', classes=[], data_attrs=[], id=None, disabled=[]) %} 503 | 526 | {% endmacro -%} 527 | 528 | 529 | {%- macro modal_carousel( 530 | items, 531 | active=0, 532 | show_arrows=True, 533 | show_footer=True, 534 | footer=None, 535 | id=None, 536 | fade=True, 537 | include_btn=False, 538 | close_btns=True, 539 | include_btn_text='Trigger Modal', 540 | title=None 541 | ) 542 | %} 543 | {{ modal(id, 544 | dict2carousel(id, 545 | items, 546 | active=active, 547 | show_arrows=show_arrows, 548 | ), 549 | close_btns=close_btns, 550 | fade=fade, 551 | show_footer=show_footer, 552 | footer=footer, 553 | include_btn=include_btn, 554 | include_btn_text=include_btn_text, 555 | title=title 556 | ) 557 | }} 558 | {% endmacro -%} 559 | 560 | 561 | {%- macro table_panels(items, 562 | paneltype='default', 563 | table_classes=[], 564 | table_data_attrs=[] 565 | ) 566 | -%} 567 | {% for item in items %} 568 | {% for title, data in item.items() %} 569 | {# Xform data into magical data-table html #} 570 | {% set html = objects2table(data, classes=table_classes, data_attrs=table_data_attrs) 571 | %} 572 | {{ bs3_panel({title: html}, paneltype=paneltype) }} 573 | {% endfor %} 574 | {% endfor %} 575 | {% endmacro -%} 576 | -------------------------------------------------------------------------------- /flask_extras/macros/content_blocks.html: -------------------------------------------------------------------------------- 1 | {% from "utils.html" import apply_classes %} 2 | 3 | {%- macro dict_heading_blocks(data, hsize='h2', 4 | heading_classes=[], 5 | para_classes=[]) %} 6 | {# 7 | Create paragraphs with headings for each, from a dict. 8 | Usage: 9 | {{ dict_heading_blocks({'My heading': 'Lorem ipsum...'}, heading_classes=['lead']) }} 10 |

    My heading

    11 |

    Lorem ipsum...

    12 | #} 13 | {% for heading, para in data.items() %} 14 | <{{ hsize }} class="{{ apply_classes(heading_classes) }}"> 15 | {{ heading }} 16 | 17 |

    18 | {{ para }} 19 |

    20 | {% endfor %} 21 | {% endmacro -%} 22 | -------------------------------------------------------------------------------- /flask_extras/macros/dates.html: -------------------------------------------------------------------------------- 1 | {%- macro simpledatetime(timestr) %} 2 | {% set t = timestr|str2dt %} 3 | {% if t %}{{ t.strftime('%Y-%m-%d %H:%M:%S') }}{% else %}{{ t }}{% endif %} 4 | {% endmacro -%} 5 | 6 | 7 | {%- macro simpledate(timestr) %} 8 | {% set t = timestr|str2dt %} 9 | {% if t %}{{ t.strftime('%Y-%m-%d') }}{% else %}{{ t }}{% endif %} 10 | {% endmacro -%} 11 | -------------------------------------------------------------------------------- /flask_extras/macros/extras_code.html: -------------------------------------------------------------------------------- 1 | {% from 'utils.html' import apply_dattrs, apply_classes %} 2 | 3 | {%- macro inline_code(code) %} 4 | {{ code }} 5 | {% endmacro -%} 6 | 7 | 8 | {%- macro code(code, lang='json') %} 9 |
    10 |         {{ code }}
    11 |     
    12 | {% endmacro -%} 13 | 14 | 15 | {%- macro tokenize_code(code, delimiter=' ', 16 | use_pre=True, 17 | use_code=True, 18 | classes=[], 19 | token_classes={}, 20 | token_dattrs={}, 21 | replacers=[], 22 | wrap_all=True) %} 23 | {# 24 | Convert a code string into a code block with span wrappers for all 25 | matching tokens specified. All values in `token_classes` will be used as classes to apply to each token wrapper. 26 | 27 | Tokens are determined by splitting on `delimiter`. 28 | 29 | Unmatched tokens will still be wrapped in a span unless `wrap_all` is False. 30 | 31 | Usage: 32 | tokenize_code( 33 | 'FOO bar BAZ "quux"', 34 | delimiter=' ', 35 | token_dattrs={'BAZ': {'val': 'foo'}}, 36 | token_classes={'FOO': ['foocls']}, 37 | replacers=['"'], 38 | wrap_all=True, 39 | ) 40 | 41 |
    42 |         
    43 |             FOO
    44 |             bar
    45 |             BAZ
    46 |             quux
    47 |         
    48 |     
    49 | #} 50 | {% if use_pre %}
    {% endif %}
    51 |     {% if use_code %}{% endif %}
    52 |         {% for token in code.split(delimiter) %}
    53 |             {% if wrap_all %}
    54 |                 {% set tok = token.strip()|cut(replacers) %}
    55 |                 {% set _dattrs = token_dattrs.get(tok, {})%}
    56 |                 {% set _classes = token_classes.get(tok, []) + classes%}
    57 |                 {{ tok }}
    61 |             {% endif %}
    62 |         {% endfor %}
    63 |     {% if use_code %}{% endif %}
    64 | {% if use_pre %}
    {% endif %} 65 | {% endmacro -%} 66 | -------------------------------------------------------------------------------- /flask_extras/macros/extras_msg.html: -------------------------------------------------------------------------------- 1 | {%- macro alert_type(val) %} 2 | {% set types = ['error', 'warning', 'info', 'success'] %} 3 | {%- if val == 'error' -%}alert-danger{% endif %} 4 | {%- if val == 'warning' -%}alert-warning{% endif %} 5 | {%- if val == 'info' -%}alert-info{% endif %} 6 | {%- if val == 'success' -%}alert-success{% endif %} 7 | {%- if val not in types -%}alert-info{% endif %} 8 | {% endmacro -%} 9 | 10 | {%- macro flash_messages(close_btn=True) %} 11 | {% with messages = get_flashed_messages(with_categories=True) %} 12 | {% if messages %} 13 | {% for category, message in messages %} 14 |
    15 | {% if close_btn %} 16 | 19 | {% endif %} 20 |

    21 | {{ category|capitalize }}: 22 | {{ message|safe }} 23 |

    24 |
    25 | {% endfor %} 26 | {% endif %} 27 | {% endwith %} 28 | {% endmacro -%} 29 | -------------------------------------------------------------------------------- /flask_extras/macros/macros.html: -------------------------------------------------------------------------------- 1 | {% from 'utils.html' import apply_dattrs, apply_classes %} 2 | 3 | {% macro dictlist_dl(data, filterkeys=[], filtervals=[], classes=[], data_attrs=[], asdict=False) %} 4 | {# 5 | Make a definition list from a dictionary - the correspondence should be dt: key, dd: value 6 | Usage: 7 | {{ dictlist_dl({'foo': 'bar'}) }} 8 |
    9 |
    foo
    10 |
    bar
    11 |
    12 | 13 | Or use a namedtuple by specifying `asdict`: 14 | {{ dict_list_dl(namedtuple, asdict=True) }} 15 | #} 16 | {% if asdict %}{% set data = data._asdict() %}{% endif %} 17 |
    18 | {% for k, v in data.items() -%} 19 | {% if k not in filterkeys and v not in filtervals %} 20 |
    {{ k }}
    21 |
    {{ v }}
    22 | {% endif %} 23 | {% endfor %} 24 |
    25 | {%- endmacro %} 26 | 27 | 28 | {% macro dict2list(data, type='ul', filterkeys=[], filtervals=[], classes=[], asdict=False) %} 29 | {# 30 | Makes a list from a dictionary. 31 | Usage: 32 | {{ dict2list({'foo': 'bar'}) }} 33 | 36 | 37 | Or use a namedtuple by specifying `asdict`: 38 | {{ dict_list_dl(namedtuple, asdict=True) }} 39 | #} 40 | {% if asdict %}{% set data = data._asdict() %}{% endif %} 41 | <{{ type }} class="{{ apply_classes(classes) }}"> 42 | {% for k, v in data.items() %} 43 | {% if k not in filterkeys and v not in filtervals %} 44 |
  • {{ k }}: {{ v }}
  • 45 | {% endif %} 46 | {% endfor %} 47 | 48 | {%- endmacro %} 49 | 50 | 51 | {% macro dict2linklist(data, type='ul', target='_blank', filterkeys=[], filtervals=[], classes=[], asdict=False) %} 52 | {# 53 | Makes a list from a dictionary. 54 | Usage: 55 | {{ dict2list({'foo': 'bar'}) }} 56 | 61 | 62 | Or use a namedtuple by specifying `asdict`: 63 | {{ dict_list_dl(namedtuple, asdict=True) }} 64 | #} 65 | {% if asdict %}{% set data = data._asdict() %}{% endif %} 66 | <{{ type }} class="{{ apply_classes(classes) }}"> 67 | {% for k, v in data.items() %} 68 | {% if k not in filterkeys and v not in filtervals %} 69 |
  • 70 | {{ v }} 71 |
  • 72 | {% endif %} 73 | {% endfor %} 74 | 75 | {%- endmacro %} 76 | 77 | 78 | {% macro list2list(data, type='ul', 79 | filtervals=[], icons={}, 80 | classes=[], 81 | icondir='left') %} 82 | {# 83 | Makes a OL/UL from a list, with optional icons 84 | Usage: 85 | {{ list2list(['Toyota', 'V2', '747', 'John Deere'], icons={'Toyota': ['fa', 'fa-car'], 'V2': ['fa', 'fa-rocket']}, icondir='right') }} 86 | Returns: 87 | 93 | 94 | Change alignment to left or right by specifying `icondir`: 95 | {{ list2list(['Toyota'], icons={'Toyota': ['car']}, icondir='left') }} 96 | #} 97 | <{{ type }} class="{{ apply_classes(classes) }}"> 98 | {% for item in data %} 99 | {% if item and item not in filtervals %} 100 |
  • 101 | {% if item in icons.keys() %} 102 | {% if icondir == 'left' %} 103 | 104 | {{ item }} 105 | {% else %} 106 | {{ item }} 107 | 108 | {% endif %} 109 | {% else %} 110 | {{ item }} 111 | {% endif %} 112 |
  • 113 | {% endif %} 114 | {% endfor %} 115 | 116 | {%- endmacro %} 117 | 118 | 119 | {% macro dictlist2nav(data, type='ul', filterkeys=[], filtervals=[], classes=[], data_attrs=[]) %} 120 | {# 121 | Make a list of links with nav element. 122 | Format must be a list of dictionaries. 123 | Supports *one* level of nesting. 124 | #} 125 | {% if asdict %}{% set data = data._asdict() %}{% endif %} 126 | 137 | {%- endmacro %} 138 | 139 | 140 | {% macro dictlist2dropdown(data, name=None, filterkeys=[], filtervals=[], classes=[], data_attrs=[], asdict=False) %} 141 | {# 142 | Make a dropdown element. 143 | Format must be a list of dictionaries. 144 | #} 145 | {% if asdict %}{% set data = data._asdict() %}{% endif %} 146 | 155 | {%- endmacro %} 156 | 157 | 158 | {% macro list2dropdown(data, filtervals=[], classes=[], data_attrs=[]) %} 159 | {# 160 | Make a dropdown element. 161 | Format must be a list. Value and text are the same. 162 | #} 163 | 170 | {%- endmacro %} 171 | 172 | 173 | {% macro dictlist2checkboxes(data, fieldset_class='fieldset-group', 174 | filterkeys=[], filtervals=[], data_attrs=[], 175 | asdict=False) %} 176 | {# 177 | Make a checkbox group, where keys are input names, and values are labels. 178 | Format must be a list of dictionaries. 179 | #} 180 | {% if asdict %}{% set data = data._asdict() %}{% endif %} 181 |
    182 | {% for item in data %} 183 | {% for k, v in item.items() %} 184 | {% if k not in filterkeys and v not in filtervals %} 185 | 189 | {% endif %} 190 | {% endfor %} 191 | {% endfor %} 192 |
    193 | {%- endmacro %} 194 | 195 | 196 | {% macro objects2table(objs, 197 | classes=[], 198 | data_attrs=[], 199 | filterkeys=[], 200 | filtervals=[], 201 | filter_headings=[], 202 | pk_link=None, 203 | handle_links=True, 204 | id=None, 205 | field_macros={}, 206 | header_macros={}, 207 | asdict=False, 208 | order=None 209 | ) 210 | -%} 211 | {# 212 | Create table from a list of objects, classes, etc... Also add links for primary keys if specified. 213 | 214 | {{ 215 | objects2table( 216 | data, 217 | data_attrs={'datatable': 'true'}, 218 | filterkeys=['do-not-want'], 219 | filtervals=['some-val'], 220 | field_macros={'somefield': some_macro}, 221 | header_macros={'some-header': some_header_macro}, 222 | filter_headings=['some-heading'], 223 | classes=['table', 'table-striped', 'table-bordered'], 224 | ) 225 | }} 226 | 227 | If your data is a named tuple, use asdict=True to convert it. 228 | #} 229 | {% if asdict %}{% set data = data._asdict() %}{% endif %} 230 | 233 | 234 | {% for obj in objs %} 235 | {% if order %} 236 | {% set obj = obj|sort_dict_keys_from_reflist(order) %} 237 | {% else %} 238 | {% set obj = obj.items() %} 239 | {% endif %} 240 | {% if loop.first %} 241 | {% set header_keys = header_macros.keys() %} 242 | {% for item in obj %} 243 | {% set heading = item[0] %} 244 | {# Allow filtering of headings. #} 245 | {% if heading not in filterkeys and heading not in filter_headings %} 246 | {% if heading in header_keys %} 247 | 248 | {% else %} 249 | 250 | {% endif %} 251 | {% endif %} 252 | {% endfor %} 253 | {% endif %} 254 | {% endfor %} 255 | 256 | 257 | {% for obj in objs %} 258 | {% if order %} 259 | {% set obj = obj|sort_dict_keys_from_reflist(order) %} 260 | {% else %} 261 | {% set obj = obj.items() %} 262 | {% endif %} 263 | 264 | {% for item in obj %} 265 | {% set k = item[0] %} 266 | {% set v = item[1] %} 267 | {# Allowing filtering of keys and values. #} 268 | {% if k not in filterkeys %} 269 | {% if v not in filtervals %} 270 | 283 | {% else %} 284 | 285 | {% endif %} 286 | {% endif %} 287 | {% endfor %} 288 | 289 | {% endfor %} 290 | 291 |
    {{ header_macros[heading](heading) }}{{ heading }}
    271 | {# Handle all primary key links, a common occurence #} 272 | {% if pk_link and k == 'id' %} 273 | {{ v }} 274 | {% elif handle_links and v|is_url %} 275 | {{ v }} 276 | {% elif k in field_macros.keys() %} 277 | {# If a field macro is specified by key, call it on this field for arbitrary levels of customization #} 278 | {{ field_macros[k](v) }} 279 | {% else %} 280 | {{ v }} 281 | {% endif %} 282 |
    292 | {%- endmacro %} 293 | 294 | 295 | {% macro wtform_errors_field(field, errors, bg=True) -%} 296 | {# 297 | Specify an error field list for a given field and its errors. 298 | Usage: 299 | {{ wtform_errors_field(, ) }} 300 | Disable or enable coloring with `bg`: 301 | {{ wtform_errors_field(, , bg=False) }} 302 | #} 303 | {% if bg %}
    {% endif %} 304 |

    Error(s) for '{{ field }}':

    305 |
      306 | {% for error in errors %} 307 |
    • {{ error }}
    • 308 | {% endfor %} 309 |
    310 | {% if bg %}
    {% endif %} 311 | {%- endmacro %} 312 | 313 | 314 | {% macro wtform_errors(formobj) -%} 315 | {# 316 | Show a list of form errors based on a given wtform instance. 317 | Usage: 318 | {{ wtform_errors() }} 319 | #} 320 | {% if formobj.errors %} 321 | {% for field, errors in formobj.errors.items() %} 322 | {{ wtform_errors_field(field, errors) }} 323 | {% endfor %} 324 | {% endif %} 325 | {%- endmacro %} 326 | 327 | 328 | {% macro wtform_form(formobj, 329 | action='.', 330 | align='left', 331 | btn_classes=[], 332 | btn_text='Submit', 333 | button_wrapper=True, 334 | colsizes=[4, 8], 335 | classes=[], 336 | data_attrs=[], 337 | enctype=None, 338 | field_classes={}, 339 | field_macros={}, 340 | fieldset_groups=[], 341 | fieldset_fallback='__unlabeled', 342 | formid=None, 343 | horizontal=False, 344 | hrule=True, 345 | input_classes=[], 346 | legend=None, 347 | linebreaks=False, 348 | method='GET', 349 | questionize=True, 350 | preserve_formfield=True, 351 | reset_btn=True, 352 | reset_btn_classes=[], 353 | submit=True, 354 | uploads=True, 355 | use_fieldset=True, 356 | wrap_inputs=False 357 | ) 358 | -%} 359 | {# 360 | Generate an entire wtform object with layout options and many customization options. Options include: 361 | - Error handling/styling 362 | - Horizontal or vertical layout 363 | - Per-field macro customization 364 | - Per-field styling 365 | - Data-attributes, classes, ids 366 | - Automatically add "?" to BooleanFields if `questionize` is set. 367 | - Add a button wrapper for styling 368 | - Add a input/label wrapper for styling 369 | - Add reset button 370 | - Wrap in fieldset/legend option 371 | - Upload support (enctype) 372 | - Other standard form options 373 | - All the other magic that comes from wtforms (descriptions, help text, defaults, error handling, etc...) 374 | - Automatically group fields by a various fieldsets (see below) 375 | - Determine column sizes 376 | 377 | Usage: 378 | {{ 379 | wtform_form( 380 | , 381 | action=url_for('myapp.index'), 382 | method='POST', 383 | classes=['form', 'form-inline'], 384 | input_classes=['input-lg', 'form-control'], 385 | btn_text='Go', 386 | formid='myform', 387 | horizontal=True, 388 | align='right', 389 | submit=False, 390 | field_macros={ 391 | 'field1': myfield1macro, 392 | 'field2': myfield2macro, 393 | }, 394 | field_classes={ 395 | 'myfield': ['class1', 'class2'], 396 | }, 397 | fieldset_groups=[ 398 | ('group1', ('foo1', 'bar1', 'foo2')), 399 | (('group2', 'Some intro description for this group.'), ('foo3', 'bar2', 'foo3')), 400 | ], 401 | ) 402 | }} 403 | 404 | Field macros can be used to customized individual fields however you like, given a macro. 405 | #} 406 |
    412 | {% if use_fieldset and not fieldset_groups %}
    {% endif %} 413 | {% if legend and not fieldset_groups %} 414 | {{ legend }} 415 | {% endif %} 416 | 417 | {# This macro is only to DRY up the usage in the below loop. #} 418 | {%- macro _load_field(field, last=False) %} 419 | {% set valid_field = field.type not in ['CSRFTokenField', 'HiddenField'] %} 420 | {% if valid_field %} 421 | {{ _wtform_field( 422 | field, 423 | colsizes=colsizes, 424 | input_classes=input_classes, 425 | horizontal=horizontal, 426 | hrule=hrule, 427 | align=align, 428 | questionize=questionize, 429 | linebreaks=linebreaks, 430 | wrap_inputs=wrap_inputs, 431 | field_classes=field_classes, 432 | field_macros=field_macros, 433 | last=last, 434 | ) 435 | }} 436 | {% else %} 437 | {{ field }} 438 | {% endif %} 439 | {% endmacro -%} 440 | 441 | {%- macro _fields(fields) %} 442 | {% for field in fields %} 443 | {# Deal with fields utilizing the `FormField` class for grouping subfields. #} 444 | {% if field.type == 'FormField' and not preserve_formfield %} 445 | {% for subfield in field %} 446 | {{ _load_field(subfield, last=loop.last) }} 447 | {% endfor %} 448 | {% else %} 449 | {{ _load_field(field, last=loop.last) }} 450 | {% endif %} 451 | {% endfor %} 452 | {% endmacro -%} 453 | 454 | {# 455 | Load all fields by groups if fieldset_groups is present, 456 | otherwise load as normal iteration. 457 | #} 458 | {% if fieldset_groups %} 459 | {% set groups = formobj|group_by( 460 | fieldset_groups, 'name', fallback=fieldset_fallback) %} 461 | {% for label, fields in groups.items() %} 462 | {% if label|length == 2 %} 463 | {# If the user specifies a 2-tuple inside here, 464 | we take the first index as the fieldset legend and second one as an optional legend 465 | introductory description #} 466 | {% set legend = label[0] %} 467 | {% set legend_desc = label[1] %} 468 | {% else %} 469 | {% set legend = label %} 470 | {% set legend_desc = '' %} 471 | {% endif %} 472 | {% if label != '__unlabeled' and fields %} 473 |
    474 | 475 | {{ legend }} 476 | {% if legend_desc %} 477 | {{ legend_desc }} 478 | {% endif %} 479 | 480 | {% endif %} 481 | {{ _fields(fields) }} 482 | {% if legend != '__unlabeled' and fields %} 483 |
    484 | {% endif %} 485 | {% endfor %} 486 | {% else %} 487 | {{ _fields(formobj) }} 488 | {% endif %} 489 | 490 | {% if use_fieldset and not fieldset_groups %}
    {% endif %} 491 | 492 | {% if submit %} 493 | {% if button_wrapper %}
    {% endif %} 494 | {# Add typical form submit button. #} 495 | 496 | {# Add a reset field values button if specified. #} 497 | {% if reset_btn %} 498 | 499 | {% endif %} 500 | {% if button_wrapper %}
    {% endif %} 501 | {% endif %} 502 |
    503 | {%- endmacro %} 504 | 505 | 506 | {# Handle the complex logic inside of `wtform_form` in a separate macro. Not for public use. #} 507 | {%- macro _wtform_field(field, 508 | input_classes=[], 509 | colsizes=[4, 8], 510 | horizontal=False, 511 | hrule=True, 512 | align='left', 513 | questionize=True, 514 | linebreaks=True, 515 | wrap_inputs=False, 516 | field_classes={}, 517 | field_macros={}, 518 | last=False 519 | ) %} 520 | {% set use_field_macro = field.name in field_macros.keys() %} 521 | 522 | {% if horizontal %}
    {% endif %} 523 | {# Only show labels and descriptions if they are normal fields. #} 524 | {% if wrap_inputs and not horizontal %} 525 | 526 | {% endif %} 527 | 528 | {% if horizontal %} 529 |
    530 | {% if wrap_inputs %} 531 | 532 | {% endif %} 533 | {% endif %} 534 | 535 | {% if not use_field_macro and field.type != 'SubmitField' %} 536 | {% set qmark = '?' 537 | if field.type == 'BooleanField' and questionize else '' 538 | %} 539 | 540 | {{ field.label(text=field.label.text + qmark) }} 541 | 542 | {% if field.flags.required %} 543 | {% if horizontal and linebreaks %}
    {% endif %} 544 | * Required 545 | {% if linebreaks %}
    {% endif %} 546 | {% endif %}{# end `if field.flags.required` #} 547 | 548 | {% endif %} 549 | 550 | {% if field.description and not use_field_macro %} 551 | {% if horizontal and linebreaks %}
    {% endif %} 552 | {{ field.description|safe }} 553 | {% endif %}{# end `if field.description` #} 554 | 555 | {% if horizontal %} 556 |
    557 | {# If horizontal mode is enabled, 558 | we can't group the fields etc into a single wrapper, 559 | so we apply to each section in each respective column #} 560 | {% if wrap_inputs %}
    {% endif %} 561 |
    562 | {% if wrap_inputs %} 563 | 564 | {% endif %} 565 | {% endif %}{# end `if horizontal` #} 566 | 567 | {% if horizontal %} 568 |
    569 | {% endif %}{# end `if horizontal` #} 570 | 571 | {% if use_field_macro %} 572 | {{ field_macros[field.name](field) }} 573 | {% else %} 574 | 575 | {# Add custom classes if set #} 576 | {% if field_classes.get(field.name) %} 577 | {% set _classes = input_classes + field_classes[field.name] %} 578 | {% else %} 579 | {% set _classes = input_classes %} 580 | {% endif %} 581 | 582 | {# Wrap error around individual field if not using horizontal wrapper #} 583 | {% if field.errors %}
    {% endif %} 584 | {{ field(class_=_classes|join(' ')) }} 585 | {% if field.errors %}
    {% endif %} 586 | 587 | {% endif %} 588 | 589 | {% if linebreaks and not use_field_macro %}
    {% endif %} 590 | {% if field.errors and not use_field_macro %} 591 | {{ wtform_errors_field(field.label, field.errors, bg=False) }} 592 | {% endif %} 593 | 594 | {% if horizontal %}
    {% endif %} 595 | {% if linebreaks and not use_field_macro %}
    {% endif %} 596 | 597 | {% if horizontal %} 598 |
    599 | {# If horizontal mode is enabled, 600 | we can't group the fields etc into a single wrapper, 601 | so we apply to each section in each respective column #} 602 | {% if wrap_inputs %}{% endif %} 603 | {% endif %} 604 | 605 | {% if horizontal %} 606 |
    607 | {% if not last and hrule %}
    {% endif %} 608 | {% endif %} 609 | {% if wrap_inputs and not horizontal %}{% endif %} 610 | {% endmacro -%} 611 | 612 | 613 | {%- macro recurse_dictlist(val, type='ul', classes=[], data_attrs=[]) %} 614 | {# 615 | Uses the jinja2 recursive looping to recurse over a dictionary and display as a list (ordered or unordered), or display a default value otherwise. 616 | #} 617 | {% if val is mapping %} 618 | <{{ type }} class="{{ apply_classes(classes) }}" {{ apply_dattrs(data_attrs) }}> 619 | {% for k, v in val.items() recursive %} 620 | {% if v is mapping %} 621 |
  • 622 | {{ k }}: 623 | <{{ type }} class="{{ apply_classes(classes) }}" {{ apply_dattrs(data_attrs) }}> 624 | {{ loop(v.items()) }} 625 | 626 |
  • 627 | {% else %} 628 |
  • 629 | {% if v|islist %} 630 | <{{ type }} class="{{ apply_classes(classes) }}" {{ apply_dattrs(data_attrs) }}> 631 | {% for item in v %} 632 |
  • 633 | {% for itemv in v recursive %} 634 | {{ recurse_dictlist(itemv, type=type) }} 635 | {% endfor %} 636 | {% endfor %} 637 |
  • 638 | 639 | {% else %} 640 | {{ k }}: {{ v }} 641 | {% endif %} 642 | 643 | {% endif %} 644 | {% endfor %} 645 | 646 | {% elif val|islist %} 647 | {# Handle presumed to be jinja2 dictsort formatted items #} 648 | <{{ type }} class="{{ apply_classes(classes) }}" {{ apply_dattrs(data_attrs) }}> 649 | {% for tupleval in val recursive %} 650 | {% if tupleval|length == 2 %} 651 | {% set k = tupleval[0] %} 652 | {% set v = tupleval[1] %} 653 |
  • 654 | {{ k }}: 655 | {{ recurse_dictlist(v, type=type) }} 656 |
  • 657 | {% endif %} 658 | {% endfor %} 659 | 660 | {% else %} 661 | {{ val }} 662 | {% endif %} 663 | {%- endmacro %} 664 | -------------------------------------------------------------------------------- /flask_extras/macros/utils.html: -------------------------------------------------------------------------------- 1 | {# 2 | NOTE: 3 | Many of these macros are single argument so they can be passed around and called, e.g. other macros. 4 | #} 5 | 6 | {% macro apply_classes(classes) -%} 7 | {# 8 | Apply css classes inside an element. 9 | Usage: 10 |
    11 |
    12 | #} 13 | {% for css in classes %}{{ css }} {% endfor %} 14 | {%- endmacro %} 15 | 16 | 17 | {% macro apply_prop(name, val) -%} 18 | {% if name and val %}{{ name }}="{{ val }}"{% endif %} 19 | {%- endmacro %} 20 | 21 | 22 | {% macro apply_dattrs(data_attrs) -%} 23 | {# 24 | Apply data-attributes inside an element. 25 | Usage: 26 |
    27 |
    28 | 29 | As a dictionary: 30 |
    31 |
    32 | #} 33 | {% if data_attrs is mapping %} 34 | {% for attr, val in data_attrs.items() %}data-{{ attr|camel2hyphen }}='{{ val }}' {% endfor %} 35 | {% else %} 36 | {% for attr in data_attrs %}data-{{ attr }} {% endfor %} 37 | {% endif %} 38 | {%- endmacro %} 39 | 40 | 41 | {%- macro ip_link(ipaddr) %} 42 | {{ ipaddr }} 43 | {% endmacro -%} 44 | 45 | 46 | {%- macro simpleurl(link, link_text='View url', blank=True) %} 47 | {% if not link.startswith('http') %} 48 | {% set link = 'http://%s'|format(link) %} 49 | {% endif %} 50 | {{ link_text }} 51 | {% endmacro -%} 52 | 53 | 54 | {%- macro boolean_icon_display(boolval) %} 55 | {% if boolval %} 56 | 57 | {% else %} 58 | 59 | {% endif %} 60 | {% endmacro -%} 61 | 62 | 63 | {%- macro titleize(val) %} 64 | {{ underscore2space(hyphen2space(val)).strip()|capitalize }} 65 | {% endmacro -%} 66 | 67 | 68 | {%- macro underscore2space(val) %} 69 | {{ val|replace('_', ' ') }} 70 | {% endmacro -%} 71 | 72 | 73 | {%- macro hyphen2space(val) %} 74 | {{ val|replace('-', ' ') }} 75 | {% endmacro -%} 76 | 77 | 78 | {%- macro dot2space(val) %} 79 | {{ val|replace('.', ' ') }} 80 | {% endmacro -%} 81 | 82 | 83 | {%- macro remove_parens(val) %} 84 | {{ val|replace('(', '')|replace(')', '') }} 85 | {% endmacro -%} 86 | 87 | 88 | {%- macro remove_brackets(val) %} 89 | {{ val|replace('[', '')|replace(']', '') }} 90 | {% endmacro -%} 91 | 92 | 93 | {%- macro remove_braces(val) %} 94 | {{ val|replace('{', '')|replace('}', '') }} 95 | {% endmacro -%} 96 | -------------------------------------------------------------------------------- /flask_extras/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christabor/flask_extras/f57300bc2922aa4105d1aa393351b63c86c26048/flask_extras/views/__init__.py -------------------------------------------------------------------------------- /flask_extras/views/statuses.py: -------------------------------------------------------------------------------- 1 | """Static pages for various HTTP codes. 2 | 3 | Format must be `page_XXX` where XXX is the code in question. This allows 4 | proper dynamic injection into the Flask app error handling mechanism. 5 | """ 6 | 7 | from inspect import isfunction 8 | 9 | from flask import render_template 10 | 11 | 12 | def page_400(error): 13 | """400 page.""" 14 | return render_template( 15 | 'status_codes/400.html', 16 | code=400, desc='bad request', error=error) 17 | 18 | 19 | def page_401(error): 20 | """401 page.""" 21 | return render_template( 22 | 'status_codes/401.html', 23 | code=401, desc='unauthorized access', error=error) 24 | 25 | 26 | def page_403(error): 27 | """403 page.""" 28 | return render_template( 29 | 'status_codes/403.html', 30 | code=403, desc='forbidden', error=error) 31 | 32 | 33 | def page_404(error): 34 | """404 page.""" 35 | return render_template( 36 | 'status_codes/404.html', 37 | code=404, desc='not found', error=error) 38 | 39 | 40 | def page_500(error): 41 | """500 page.""" 42 | return render_template( 43 | 'status_codes/500.html', 44 | code=500, desc='internal server error', error=error) 45 | 46 | 47 | def page_503(error): 48 | """503 page.""" 49 | return render_template( 50 | 'status_codes/500.html', 51 | code=503, desc='service unavailable', error=error) 52 | 53 | 54 | def _isview(name, func): 55 | """Check if arguments represent a valid Flask view. 56 | 57 | Args: 58 | name (str): String name of function. 59 | func (function): A function. 60 | 61 | Returns: 62 | bool: Whether the given args are valid. 63 | """ 64 | return isfunction(func) and name.startswith('page_') 65 | 66 | 67 | def _get_viewfuncs(): 68 | """Return all functions in this module that are relevant views. 69 | 70 | Returns: 71 | dict: The view functions, keyed by name. 72 | """ 73 | return {name: func for name, func 74 | in globals().iteritems() if _isview(name, func)} 75 | 76 | 77 | def inject_error_views(app): 78 | """Inject all relevant error (status code) views into an app. 79 | 80 | Args: 81 | app (object): The Flask instance. 82 | funcs (dict): A dict of functions by name. 83 | 84 | Returns: 85 | object: The modified Flask instance. 86 | """ 87 | # See flask.pocoo.org/docs/0.10/api/#flask.Flask.errorhandler 88 | for name, func in _get_viewfuncs().iteritems(): 89 | code = name.replace('page_', '') 90 | app.register_error_handler(int(code), func) 91 | return app 92 | -------------------------------------------------------------------------------- /macros.md: -------------------------------------------------------------------------------- 1 | # Macros 2 | 3 | An assortment of macros have been created for various reusable templating scenarios. 4 | 5 | *New in 3.6.1* - namedtuple support on all dictionary based macros. 6 | 7 | E.g. 8 | 9 | ```python 10 | from collections import namedtuple 11 | Person = namedtuple('Person', 'age dob sex loc name') 12 | person=Person(30, '01051986', 'M', 'seattle', 'chris') 13 | ``` 14 | 15 | Can now be used with any macro that supports `asdict` kwarg: 16 | 17 | `{{ dict2list(person, asdict=True) }}` 18 | 19 | ## `macros.html` 20 | 21 | ### apply_classes 22 | 23 | Apply a list of classes inline. 24 | 25 | ```jinja2 26 |
    27 | ``` 28 | 29 | becomes: 30 | 31 | ```html 32 |
    33 | ``` 34 | 35 | ### apply_dattrs 36 | 37 | Apply a list of HTML5 data-attributes inline. 38 | 39 | ```jinja2 40 |
    41 | ``` 42 | 43 | becomes: 44 | 45 | ```html 46 |
    47 | ``` 48 | 49 | ### dictlist_dl 50 | 51 | ... 52 | 53 | ### dict2list 54 | 55 | ... 56 | 57 | ### dictlist2nav 58 | 59 | ... 60 | 61 | ### dictlist2dropdown 62 | 63 | Convert a dict into a dropdown of options. 64 | 65 | ```jinja2 66 | {{ 67 | dictlist2dropdown({'foo': 'bar', 'bar': 'foo'}, 'options', classes=['form-dropdown']) 68 | }} 69 | ``` 70 | 71 | becomes 72 | 73 | ```html 74 | 78 | ``` 79 | 80 | ### dictlist2checkboxes 81 | 82 | Convert a dictionary into a fieldset of checkboxes. 83 | 84 | ```jinja2 85 | {{ 86 | dictlist2checkboxes({'foo': 'bar', 'bar': 'foo'}, fieldset_class='someclass') 87 | }} 88 | ``` 89 | 90 | becomes 91 | 92 | ```html 93 |
    94 | 97 | 100 |
    101 | ``` 102 | 103 | ### objects2table 104 | 105 | Create a table with headers and rows for a list of objects. Major customization is possible, even on a per column basis, by using the `field_macros` kwarg. 106 | 107 | ```jinja2 108 | {{ 109 | objects2table([obj1, obj2, obj3] 110 | classes=['table', 'table-striped'], 111 | data_attrs=['datatable'], 112 | filterkeys=['some_secret_key1', 'some_secret_key2'], 113 | filtervals=['someval1', 'secret name'], 114 | pk_link='/some/link/', 115 | field_macros={ 116 | 'field1': mymacro1, 117 | 'field2': mymacro2, 118 | }, 119 | ) 120 | }} 121 | ``` 122 | 123 | ### wtform_errors 124 | 125 | Show a list of form errors based on a given wtform instance. 126 | 127 | ### wtform_form 128 | 129 | Automatically render a wtform object, with various options including horizontal/vertical layouts, alignment, field overrides and more. 130 | 131 | ```jinja2 132 | {{ 133 | wtform_form(form, 134 | action=url_for('app.index'), 135 | method='POST', 136 | classes=['form', 'form-horizontal'], 137 | btn_classes=['btn', 'btn-primary', 'btn-md'], 138 | align='right', 139 | horizontal=True, 140 | ) 141 | }} 142 | ``` 143 | 144 | And using field specific overrides: 145 | 146 | ```jinja2 147 | {%- macro somefieldmacro(field) %} 148 | Look at my great field! {{ field.label }} {{ field }} 149 | {% endmacro -%} 150 | 151 | {{ 152 | wtform_form(form, 153 | field_macros={ 154 | 'somefield': somefieldmacro 155 | } 156 | ) 157 | }} 158 | ``` 159 | 160 | The field object passed to your macro is a standard wtform field object. 161 | 162 | ### recurse_dictlist 163 | 164 | Uses the jinja2 recursive looping to recurse over a dictionary and display as a list (ordered or unordered), or display a default value otherwise. 165 | 166 | ### dict2labels 167 | 168 | ... 169 | 170 | ### list2list 171 | 172 | ... 173 | 174 | ## `code.html` 175 | 176 | ### code 177 | 178 | ```jinja2 179 | {{ code('a, b, c = 1, 2, 3', lang='python') }} 180 | ``` 181 | 182 | becomes 183 | 184 | ```html 185 |
    186 |   a, b, c = 1, 2, 3
    187 | 
    188 | ``` 189 | 190 | ### inline_code 191 | 192 | ```jinja2 193 | {{ inline_code('a, b, c = 1, 2, 3') }} 194 | ``` 195 | 196 | becomes 197 | 198 | ```html 199 | a, b, c = 1, 2, 3 200 | ``` 201 | 202 | ## `messages.html` 203 | 204 | ### flash_messages 205 | 206 | Render a flask messages object inline, including background coloring (using bootstrap) for each status type (e.g. info, warning, error) 207 | 208 | ```jinja2 209 | {{ flash_messages() }} 210 | ``` 211 | 212 | ## `content_blocks.html` 213 | 214 | ### dict_heading_blocks 215 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup for Flask Extras.""" 2 | 3 | from setuptools import setup 4 | 5 | requirements = [ 6 | 'Flask-WTF==0.13', 7 | 'Flask==0.10.1', 8 | 'python-dateutil', 9 | 'netaddr', 10 | ] 11 | 12 | 13 | def readme(): 14 | """Grab the long README file.""" 15 | try: 16 | with open('README.md', 'r') as fobj: 17 | return fobj.read() 18 | except IOError: 19 | try: 20 | with open('README.rst', 'r') as fobj: 21 | return fobj.read() 22 | except IOError: 23 | return __doc__ 24 | 25 | 26 | setup( 27 | name='flask_extras', 28 | version='4.0.4', 29 | description=('Assorted useful flask views, blueprints, ' 30 | 'Jinja2 template filters, and templates/macros'), 31 | long_description=readme(), 32 | author='Chris Tabor', 33 | author_email='dxdstudio@gmail.com', 34 | url='https://github.com/christabor/flask_extras', 35 | license='MIT', 36 | classifiers=[ 37 | 'Topic :: Software Development', 38 | 'Programming Language :: Python :: 2.7', 39 | ], 40 | install_requires=requirements, 41 | package_dir={'flask_extras': 'flask_extras'}, 42 | packages=['flask_extras'], 43 | package_data={ 44 | 'flask_extras': [ 45 | 'macros/*.html', 46 | ], 47 | }, 48 | zip_safe=False, 49 | include_package_data=True, 50 | ) 51 | -------------------------------------------------------------------------------- /test_app/app.py: -------------------------------------------------------------------------------- 1 | """A Test app to demonstrate various aspects of flask_extras.""" 2 | 3 | from collections import OrderedDict 4 | from datetime import datetime as dt 5 | 6 | from flask import ( 7 | Flask, 8 | render_template, 9 | flash, 10 | request, 11 | ) 12 | 13 | from flask_wtf import FlaskForm 14 | 15 | from flask_extras import FlaskExtras 16 | from wtforms import ( 17 | BooleanField, 18 | IntegerField, 19 | RadioField, 20 | HiddenField, 21 | PasswordField, 22 | SelectField, 23 | SelectMultipleField, 24 | StringField, 25 | SubmitField, 26 | TextAreaField, 27 | validators, 28 | ) 29 | 30 | from flask_extras.views import statuses 31 | 32 | app = Flask('flask_extras_test') 33 | app.secret_key = 'abc1234' 34 | 35 | FlaskExtras(app) 36 | 37 | 38 | class SomeForm(FlaskForm): 39 | """Form.""" 40 | 41 | hideme = HiddenField() 42 | favorite_food = RadioField( 43 | choices=[('pizza', 'Pizza'), ('ice-cream', 'Ice Cream')] 44 | ) 45 | age = IntegerField(validators=[validators.DataRequired()]) 46 | name = StringField( 47 | description='enter your name', 48 | validators=[validators.DataRequired()], 49 | ) 50 | nickname = StringField('What do people call you?') 51 | 52 | 53 | class SomeForm2(FlaskForm): 54 | """Form.""" 55 | 56 | hideme = HiddenField() 57 | frobnicate = BooleanField() 58 | baz = StringField() 59 | quux = SelectMultipleField( 60 | choices=[(v, v) for v in ['quux', 'baz', 'foo']]) 61 | 62 | 63 | @app.context_processor 64 | def ctx(): 65 | """Add global ctx.""" 66 | return dict( 67 | ghub_url='https://github.com/christabor/flask_extras/blob/master/flask_extras/macros/', 68 | name=str(request.url_rule), 69 | links=sorted(get_rulesmap()), 70 | ) 71 | 72 | 73 | def get_rulesmap(): 74 | """Get all rules so we ensure they're dynamic and always updated.""" 75 | bad = ['static', 'index'] 76 | return [r.endpoint for r in app.url_map._rules if r.endpoint not in bad] 77 | 78 | 79 | @app.route('/') 80 | def index(): 81 | """Demo page links.""" 82 | return render_template('pages/index.html', **dict(home=True)) 83 | 84 | 85 | @app.route('/extras_msg.html') 86 | def extras_msg(): 87 | """Demo page.""" 88 | flash('I am a success message!', 'success') 89 | flash('I am warning message!', 'warning') 90 | flash('I am an error (danger) message!', 'error') 91 | flash('I am an info message!', 'info') 92 | return render_template('pages/extras_msg.html') 93 | 94 | 95 | @app.route('/content_blocks.html') 96 | def content_blocks(): 97 | """Demo page.""" 98 | return render_template('pages/content_blocks.html') 99 | 100 | 101 | @app.route('/extras_code.html') 102 | def extras_code(): 103 | """Demo page.""" 104 | return render_template('pages/extras_code.html') 105 | 106 | 107 | @app.route('/dates.html') 108 | def dates(): 109 | """Demo page.""" 110 | kwargs = dict(somedate=dt.now()) 111 | return render_template('pages/dates.html', **kwargs) 112 | 113 | 114 | @app.route('/utils.html') 115 | def utils(): 116 | """Demo page.""" 117 | kwargs = dict() 118 | return render_template('pages/utils.html', **kwargs) 119 | 120 | 121 | @app.route('/macros.html') 122 | def macros(): 123 | """Demo page.""" 124 | kwargs = dict( 125 | dicttest=dict( 126 | foo='Some bar', 127 | bar='Some foo', 128 | ), 129 | dictlist=[ 130 | dict( 131 | foo='Some bar', 132 | bar='Some foo', 133 | ) 134 | ], 135 | dictlist2=[ 136 | dict(name='foo', age=10, dob='01/01/1900', gender='M'), 137 | dict(name='bar', age=22, dob='01/01/1901', gender='F'), 138 | dict(name='quux', age=120, dob='01/01/1830', gender='X'), 139 | ], 140 | form=SomeForm(), 141 | recursedict=vars(request), 142 | ) 143 | return render_template('pages/macros.html', **kwargs) 144 | 145 | 146 | @app.route('/bootstrap.html') 147 | def bootstrap(): 148 | """Demo page.""" 149 | dicttest = dict( 150 | foo='Some bar', 151 | bar='Some foo', 152 | ) 153 | dictlist = [ 154 | dict(name='zomb', age=999, dob='03/01/2030', gender='Z102'), 155 | dict(name='foo', age=10, dob='01/01/1900', gender='M'), 156 | dict(name='bar', age=22, dob='01/01/1901', gender='F'), 157 | dict(name='quux', age=120, dob='01/01/1830', gender='X'), 158 | ] 159 | kwargs = dict( 160 | dicttest=dicttest, 161 | dictlist=dictlist, 162 | form=SomeForm(), 163 | form2=SomeForm2(), 164 | pagination=OrderedDict( 165 | zip(['/somelink/{}'.format(i) for i in range(10)], range(10)) 166 | ), 167 | ) 168 | return render_template('pages/bootstrap.html', **kwargs) 169 | 170 | 171 | if __name__ == '__main__': 172 | app.run(debug=True, host='0.0.0.0', port=5014) 173 | -------------------------------------------------------------------------------- /test_app/static/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */@font-face{font-family:'FontAwesome';src:url('fonts/fontawesome-webfont.eot?v=4.5.0');src:url('fonts/fontawesome-webfont.eot?#iefix&v=4.5.0') format('embedded-opentype'),url('fonts/fontawesome-webfont.woff2?v=4.5.0') format('woff2'),url('fonts/fontawesome-webfont.woff?v=4.5.0') format('woff'),url('fonts/fontawesome-webfont.ttf?v=4.5.0') format('truetype'),url('fonts/fontawesome-webfont.svg?v=4.5.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"} 5 | -------------------------------------------------------------------------------- /test_app/static/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christabor/flask_extras/f57300bc2922aa4105d1aa393351b63c86c26048/test_app/static/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /test_app/static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christabor/flask_extras/f57300bc2922aa4105d1aa393351b63c86c26048/test_app/static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /test_app/static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christabor/flask_extras/f57300bc2922aa4105d1aa393351b63c86c26048/test_app/static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /test_app/static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christabor/flask_extras/f57300bc2922aa4105d1aa393351b63c86c26048/test_app/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /test_app/static/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christabor/flask_extras/f57300bc2922aa4105d1aa393351b63c86c26048/test_app/static/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /test_app/static/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christabor/flask_extras/f57300bc2922aa4105d1aa393351b63c86c26048/test_app/static/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /test_app/static/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christabor/flask_extras/f57300bc2922aa4105d1aa393351b63c86c26048/test_app/static/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /test_app/static/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christabor/flask_extras/f57300bc2922aa4105d1aa393351b63c86c26048/test_app/static/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /test_app/static/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christabor/flask_extras/f57300bc2922aa4105d1aa393351b63c86c26048/test_app/static/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /test_app/templates/layouts/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | flask_extras demo page 6 | 7 | 8 | 9 | 10 | {% macro example(macroname, desc) %} 11 |
    12 |

    13 | {{ path }} > "{{ macroname }}" 14 |
    15 | {{ desc }} 16 |

    17 | {% endmacro %} 18 | {% if not home %} 19 |
    20 |
    21 |
    22 |
    23 | ← Go back 24 |

    25 | {{ name }} 26 |

    27 |
    28 |
    29 |
    30 | {% endif %} 31 | 32 | {% block body %}{% endblock %} 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /test_app/templates/pages/bootstrap.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/base.html' %} 2 | 3 | {% from 'layouts/base.html' import example %} 4 | 5 | {% from 'macros.html' import wtform_form %} 6 | {% import 'bootstrap.html' as bs %} 7 | 8 | {% block body %} 9 |
    10 |
    11 |
    12 |

    All macros here are specific to the bootstrap css/js framework

    13 | {{ example('progress', 'Generate a progressbar')}} 14 | 15 | {{ bs.progress(30) }} 16 | {{ bs.progress(100, animated=True, striped=True) }} 17 | {{ bs.progress(89, context='success') }} 18 | {{ bs.progress(89, context='danger') }} 19 | {{ bs.progress(89, context='warning') }} 20 | {{ bs.progress(89, context='info') }} 21 | 22 | {{ example('modal', 'Generate a modal with some sections toggleable')}} 23 | 24 | {{ bs.modal('someId', 'Some html or text...', title='My Modal', include_btn=True) }} 25 | 26 |

    Add footer and remove close buttons

    27 | 28 | {{ bs.modal('someId2', 'Some html or text...', footer='Some footer text/html.', close_btns=False, title='My Modal 2', include_btn=True) }} 29 | 30 | {{ example('dict2carousel', 'Generate a carousel from a dictionary') }} 31 | 32 | {{ bs.dict2carousel( 33 | 'someId', 34 | [ 35 | {'content': '', 'caption': 'foo'}, 36 | {'content': '', 'caption': 'bar'}, 37 | {'content': '', 'caption': 'baz'}, 38 | ], 39 | classes=['well'], 40 | active=2) 41 | }} 42 | 43 | {{ example('bs3_dictlist_group', 'Generate a list-group from a dict.') }} 44 | 45 | {{ bs.bs3_dictlist_group(dicttest) }} 46 | 47 | {{ example('bs3_list_group', 'Generate a list-group from a list.') }} 48 | 49 | {{ bs.bs3_list_group(['foo', 'bar', 'baz']) }} 50 | 51 | {{ example('dictlist_group_badged', 'Generate a list-group with badges from a dict of form {"title": "badge content"}') }} 52 | 53 | {{ bs.dictlist_group_badged({'Messages': 20, 'Unread': 10, 'Read': 100, 'Starred': 34}) }} 54 | 55 |

    Or left aligned

    56 | 57 | {{ bs.dictlist_group_badged({'Messages': 20, 'Unread': 10, 'Read': 100, 'Starred': 34}, align='left') }} 58 | 59 | {{ example('bs3_label', 'Generate a label from a string and a label mapping') }} 60 | 61 | {% set label_map = {'dev': 'info', 'stage': 'warning', 'prod': 'danger'} %} 62 | 63 | {{ bs.bs3_label('prod', label_map) }} 64 | {{ bs.bs3_label('stage', label_map) }} 65 | {{ bs.bs3_label('dev', label_map) }} 66 | 67 | {{ example('bs3_panel', 'Generate bs3 panels for a dict of titles/body content.') }} 68 | 69 | {{ bs.bs3_panel({'Heading 1': 'Some body text... etc...', 'Heading 2': 'Some body text...'}, paneltype='info') }} 70 | 71 | {{ example('bs3_breadcrumb', 'Generate breadcrumbs from a breadcrumb object (likely flask-breadcrumbs, but anything that supports the same attributes would work.') }} 72 | 73 | {{ bs.bs3_breadcrumb([{'text': 'Home'}, {'text': 'SomePage'}]) }} 74 | 75 | 76 | {{ example('dict2labels', 'Makes a dict of `name`:`label` into bootstrap labels') }} 77 | 78 | {{ bs.dict2labels({'foo': 'danger', 'bar': 'success'}) }} 79 | {{ bs.dict2labels({'foo': 'danger', 'bar': 'success'}, aslist=False) }} 80 |

    Or wrap it in an html list by specifying `aslist`:

    81 | 82 | {{ bs.dict2labels({'foo': 'danger', 'bar': 'success'}, aslist=True) }} 83 | 84 | {{ example('dict2tabs', 'Make a dict into a bs3 tab group with tab content. Keys are tab labels, and values are tab content.') }} 85 | 86 | {{ bs.dict2tabs({"foo": "

    Some content rendered from html or macro

    "}) }} 87 | 88 |
    89 | 90 |

    You can also customize styles, data-attrs, etc... see function signature for more info.

    91 | 92 |

    You can even pass in content generated from other macros, such as the wtform_form macro:

    93 | 94 | {{ 95 | bs.dict2tabs({ 96 | "tab1": wtform_form(form, classes=['well']), 97 | "tab2": wtform_form(form2, classes=['well']) 98 | }, 99 | id="MyCoolTabContainer" 100 | ) 101 | }} 102 | 103 | {{ example('dictlist2tabs', 'Same idea as above, but with a list of dicts instead. This ensures ordering of tabs is correct.') }} 104 | 105 | {{ 106 | bs.dictlist2tabs([ 107 | {"tab1": wtform_form(form, classes=['well'])}, 108 | {"tab2": wtform_form(form2, classes=['well'])} 109 | ], 110 | id="MyCoolTabContainer2" 111 | ) 112 | }} 113 | 114 | {{ example('inline_list', 'Generate from a list, an inline list with a custom divider.') }} 115 | 116 | {{ bs.inline_list(['foo', 'bar', 'baz']) }} 117 | 118 | {{ example('inline_dictlist', 'Generate from a list of dicts, an inline list with a custom divider and key/value.') }} 119 | 120 | {{ bs.inline_dictlist(dictlist, seperator='::') }} 121 | 122 | {{ example('dict2btn_group', 'Generated a button group from a dictionary of {"btn-type": "name"} dict list.') }} 123 | 124 | {{ bs.dict2btn_group({'info': 'bar', 'success': 'baz', 'default': 'foo'}, size='xs') }}
    125 | 126 | {{ bs.dict2btn_group({'info': 'bar', 'success': 'baz', 'default': 'foo'}, size='sm') }}
    127 | 128 | {{ bs.dict2btn_group({'info': 'bar', 'success': 'baz', 'default': 'foo'}, size='md') }}
    129 | 130 | {{ bs.dict2btn_group({'info': 'bar', 'success': 'baz', 'default': 'foo'}, size='lg') }} 131 | 132 | {{ example('list2btn_group', 'Generated a button group from a list.') }} 133 | 134 | {{ bs.list2btn_group(['foo', 'bar', 'baz'], size='xs') }}
    135 | {{ bs.list2btn_group(['foo', 'bar', 'baz'], size='sm') }}
    136 | {{ bs.list2btn_group(['foo', 'bar', 'baz'], size='md') }}
    137 | {{ bs.list2btn_group(['foo', 'bar', 'baz'], size='lg') }} 138 | 139 | {{ example('dict2_pagination', 'Generate pagination with a dict.') }} 140 | 141 | {{ bs.dict2_pagination(pagination, size='sm') }} 142 | {{ bs.dict2_pagination(pagination, size='md') }} 143 | {{ bs.dict2_pagination(pagination, size='lg') }} 144 | 145 |

    Disabled links + prev/next + custom prev/next text.

    146 | 147 | {{ bs.dict2_pagination(pagination, prev={'somelink': '«'}, next={'somelink': 'next'}, size='sm', disabled=[1, 2, 'prev', 'next']) }} 148 | 149 | {{ example('modal_carousel', 'Generate a carousel inside a modal with a dictionary for carousel items.') }} 150 | 151 | {{ bs.modal_carousel( 152 | [ 153 | {'content': '', 'caption': 'foo'}, 154 | {'content': '', 'caption': 'bar'}, 155 | {'content': '', 'caption': 'baz'}, 156 | ], 157 | id='someKindOfCarouselId', 158 | title='Some wonderful modal gallery.', 159 | show_footer=False, 160 | include_btn=True) 161 | }} 162 | 163 | {{ example('table_panels', 'Generate panels with a table inside each, using a list of dicts with headings/table data as keys/values.') }} 164 | 165 | {{ bs.table_panels([ 166 | {'Title 1': dictlist}, 167 | {'Title 2': dictlist}, 168 | ], 169 | table_classes=['table', 'table-striped'] 170 | ) 171 | }} 172 |
    173 |
    174 |
    175 | {% endblock %} 176 | -------------------------------------------------------------------------------- /test_app/templates/pages/content_blocks.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/base.html' %} 2 | 3 | {% from 'layouts/base.html' import example %} 4 | 5 | {% from 'content_blocks.html' import dict_heading_blocks %} 6 | 7 | {% block body %} 8 |
    9 |
    10 |
    11 | {{ example('dict_heading_blocks', 'Make headings and paragraphs from a dict()') }} 12 | {{ dict_heading_blocks( 13 | {'foo bar': 'bar and baz and quux', 'baz': 'baz and quux and bar foo'}, hsize='h4') 14 | }} 15 |
    16 |
    17 |
    18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /test_app/templates/pages/dates.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/base.html' %} 2 | 3 | {% from 'layouts/base.html' import example %} 4 | 5 | {% from 'dates.html' import simpledatetime, simpledate %} 6 | 7 | {% block body %} 8 |
    9 |
    10 |
    11 | {{ example('simpledatetime', 'Convert a string or timestamp into a proper time object and then format using Y-m-d H:M:S format.') }} 12 | {{ simpledatetime(somedate) }} 13 | 14 | {{ example('simpledate', 'Convert a string or timestamp into a proper time object and then format using Y-m-d format.') }} 15 | 16 | {{ simpledate(somedate) }} 17 |
    18 |
    19 |
    20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /test_app/templates/pages/extras_code.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/base.html' %} 2 | 3 | {% from 'layouts/base.html' import example %} 4 | 5 | {% from 'extras_code.html' import inline_code, code, tokenize_code %} 6 | 7 | {% block body %} 8 |
    9 |
    10 |
    11 | {{ example('inline_code', 'Show code styling, inline') }} 12 |

    13 | Here is some text with {{ inline_code('some_function(2, 4)') }} in it. 14 |

    15 | 16 | {{ example('code', 'Show code styling as a block and specified language options. (Syntax highlighting not provided, but classes are added for styling.)') }} 17 | 18 | {{ code('{"I": "am": "some": "json", "weee": 42}', lang='json') }} 19 | 20 | {{ example('tokenize_code', 'Show code styling as a block but wrap various tokens of code with specified data-attrs and/or classes for styling. Useful for showing log or other data output with specific delimited tokens editable/styled.') }} 21 | 22 | {{ tokenize_code( 23 | 'This is a log output stream ... FOO bar BAZ "quux" 01-01-0000 00:00:00 | GET | foobar', 24 | delimiter=' ', 25 | token_dattrs={'BAZ': {'val': 'foo'}}, 26 | token_classes={ 27 | 'FOO': ['label', 'label-info'], 28 | 'BAZ': ['label', 'label-danger'], 29 | 'GET': ['label', 'label-success'] 30 | }, 31 | replacers=['"'], 32 | use_pre=False, 33 | wrap_all=True, 34 | ) 35 | }} 36 |
    37 |
    38 |
    39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /test_app/templates/pages/extras_msg.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/base.html' %} 2 | 3 | {% from 'layouts/base.html' import example %} 4 | 5 | {% from 'extras_msg.html' import flash_messages, alert_type %} 6 | 7 | {% block body %} 8 |
    9 |
    10 |
    11 | {{ example('flash_messages', 'Makes flash messages using standard bootstrap alerts.') }} 12 | {{ flash_messages() }} 13 |
    14 |
    15 |
    16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /test_app/templates/pages/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/base.html' %} 2 | 3 | {% from 'layouts/base.html' import example %} 4 | 5 | {% from 'extras_msg.html' import flash_messages, alert_type %} 6 | {% from 'content_blocks.html' import dict_heading_blocks %} 7 | 8 | {% macro linklist(links) %} 9 |
      10 | {% for link in links %} 11 |
    • 12 | {{ link }}.html 13 |
    • 14 | {% endfor %} 15 |
    16 | {% endmacro %} 17 | 18 | {% block body %} 19 |
    20 |
    21 |
    22 |

    Macros

    23 | {{ linklist(links) }} 24 | 25 |

    Filters

    26 |

    See docs

    27 | 28 |

    WTForms

    29 |

    See docs

    30 | 31 |

    Decorators

    32 |

    See docs

    33 |
    34 |
    35 |
    36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /test_app/templates/pages/macros.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/base.html' %} 2 | 3 | {% from 'layouts/base.html' import example %} 4 | 5 | {% import 'macros.html' as mac %} 6 | 7 | {% block body %} 8 |
    9 |
    10 |
    11 | {{ example('dictlist_dl', 'Make a definition list from a dictionary (or tuple). Optionally filter keys/values and stylized with classes and data-attrs') }} 12 | 13 | {{ mac.dictlist_dl(dicttest) }} 14 | 15 | {{ example('dict2list', 'Make a list from a dictionary. Optionally filter keys/values and stylized with classes and data-attrs') }} 16 | 17 | {{ mac.dict2list(dicttest) }} 18 | 19 | {{ example('dict2linklist', 'Make a list of links from a dictionary. Optionally filter keys/values and stylized with classes and data-attrs') }} 20 | 21 | {{ mac.dict2linklist({ 22 | 'http://google.com': 'Google', 23 | 'http://yahoo.com': 'Yahoo', 24 | }) }} 25 | 26 | {{ example('list2list', 'Convert a (python) list to html OL or UL list. ') }} 27 | 28 | {{ mac.list2list(['Toyota', 'V2', '747', 'John Deere']) }} 29 | 30 |

    Supports appending icons and left/right order:

    31 | 32 | {{ mac.list2list(['Toyota', 'V2', '747', 'John Deere'], icons={'Toyota': ['fa', 'fa-car'], 'V2': ['fa', 'fa-rocket']}, icondir='left') }} 33 | {{ mac.list2list(['Toyota', 'V2', '747', 'John Deere'], icons={'Toyota': ['fa', 'fa-car'], 'V2': ['fa', 'fa-rocket']}, icondir='right') }} 34 | 35 | {{ example('dictlist2nav', 'Make a list of links with nav element. Format must be a list of dicts/tuples. Supports *one* level of nesting.') }} 36 | 37 | {{ mac.dictlist2nav(dictlist) }} 38 | 39 | {{ example('dictlist2dropdown', 'Make a select > option element. Format must be a list of dicts/tuples. Supports *one* level of nesting.') }} 40 | 41 | {{ mac.dictlist2dropdown(dictlist) }} 42 | 43 | {{ example('dictlist2checkboxes', 'Make a checkbox group, where keys are input names, and values are labels. Format must be a list of dicts/tuples.') }} 44 | 45 | {{ mac.dictlist2checkboxes(dictlist) }} 46 | 47 | 48 | {{ example('objects2table', 'Convert a list of dicts/tuples to a table.') }} 49 | 50 | {{ mac.objects2table(dictlist2) }} 51 | 52 |

    Custom styling

    53 | 54 | {{ mac.objects2table(dictlist2, classes=['table', 'table-striped']) }} 55 | 56 |

    Filtering

    57 | 58 | {{ mac.objects2table(dictlist2, filterkeys='name', classes=['table', 'table']) }} 59 | 60 |

    Also supports many other things, like custom macros *PER* header or field, giving you much more fine-grained controlled, IF you need it.

    61 | 62 | {{ example('wtform_form', 'Render a wtform object, with error handling, customizable options, layouts, text and much more.') }} 63 | 64 |

    Options include:

    65 |
      66 |
    • Error handling/styling
    • 67 |
    • Horizontal or vertical layout
    • 68 |
    • Per-field macro customization
    • 69 |
    • Per-field styling
    • 70 |
    • Data-attributes, classes, ids
    • 71 |
    • Automatically add "?" to BooleanFields if `questionize` is set.
    • 72 |
    • Add a button wrapper for styling
    • 73 |
    • Add a input/label wrapper for styling
    • 74 |
    • Add reset button
    • 75 |
    • Wrap in fieldset/legend option
    • 76 |
    • Upload support (enctype)
    • 77 |
    • Other standard form options
    • 78 |
    • All the other magic that comes from wtforms (descriptions, help text, defaults, error handling, etc...)
    • 79 |
    • Automatically group fields by a various fieldsets (see below)
    • 80 |
    • Determine column sizes
    • 81 |
    82 |

    And more. See actual macro for more.

    83 | 84 |

    Examples (wrapped in boxes for clarity using custom class).

    85 | 86 | {{ mac.wtform_form(form, classes=['form', 'well']) }} 87 | 88 |

    Horizontal mode

    89 | 90 | {{ mac.wtform_form(form, horizontal=True, classes=['form', 'well']) }} 91 | 92 |

    Horizontal/right-align mode

    93 | 94 | {{ mac.wtform_form(form, align='right', horizontal=True, classes=['form', 'well']) }} 95 | 96 |

    Custom grouping

    97 | 98 | {{ mac.wtform_form(form, 99 | classes=['form', 'well'], 100 | fieldset_groups=[ 101 | ('Important info!', ('name', 'age')), 102 | ('Not so important...', ('nickname', 'favorite_food')), 103 | ]) }} 104 | 105 | {{ example('recurse_dictlist', 'Recursively traverse a list of dictionaries and all sub-datastructures, to build out a nested UL or OL.') }} 106 | 107 | {{ mac.recurse_dictlist(recursedict) }} 108 |
    109 |
    110 |
    111 | {% endblock %} 112 | -------------------------------------------------------------------------------- /test_app/templates/pages/utils.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/base.html' %} 2 | 3 | {% from 'layouts/base.html' import example %} 4 | 5 | {% import 'utils.html' as utils %} 6 | 7 | {% block body %} 8 |
    9 |
    10 |
    11 | {{ example('ip_link', 'Convert an ip string into a real link.') }} 12 | {{ utils.ip_link('127.0.0.1:5001') }} 13 | 14 | {{ example('simpleurl', 'Convert a url string into a real link, and handle missing protocol etc') }} 15 | {{ utils.simpleurl('google.com') }} 16 | 17 | {{ example('boolean_icon_display', 'Convert a boolean into a fontAwesome icon') }} 18 |
    19 | "False": {{ utils.boolean_icon_display(False) }} 20 | "True": {{ utils.boolean_icon_display(True) }} 21 | 22 |
    23 | 24 | {{ example('Titleize', 'Remove underscores, hyphens and capitalize.')}} 25 | 26 | {% set test_title = 'Some-title__that-needs__adjusting' %} 27 | 28 | "{{ test_title }}" vs. "{{ utils.titleize(test_title) }}" 29 | 30 | {{ example('underscore2space', 'Self explanatory.') }} 31 | 32 | "{{ test_title }}" vs. "{{ utils.underscore2space(test_title) }}" 33 | 34 | {{ example('hyphen2space', 'Self explanatory.') }} 35 | 36 | "{{ test_title }}" vs. "{{ utils.hyphen2space(test_title) }}" 37 | 38 | {{ example('dot2space', 'Self explanatory.') }} 39 | 40 | "I.have.lots.of.dots..." vs. "{{ utils.hyphen2space('I.have.lots.of.dots...') }}" 41 | 42 | {{ example('remove_parens', 'Self explanatory.') }} 43 | 44 | "((lol)()()" vs. "{{ utils.remove_parens('((lol)()()') }}" 45 | 46 | 47 | {{ example('remove_brackets', 'Self explanatory.') }} 48 | 49 | "[[hello wor[ld]]]" vs. "{{ utils.remove_brackets('[[hello wor[ld]]]') }}" 50 | 51 | {{ example('remove_braces', 'Self explanatory.') }} 52 | 53 | {% set test_braces = '{{hello wor{ld}}}' %} 54 | 55 | "{{ test_braces }} " vs. "{{ utils.remove_braces(test_braces) }}" 56 |
    57 |
    58 |
    59 | {% endblock %} 60 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christabor/flask_extras/f57300bc2922aa4105d1aa393351b63c86c26048/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | import pytest 3 | 4 | from flask_extras import FlaskExtras 5 | 6 | app = Flask('__config_test') 7 | app.secret_key = '123' 8 | app.debug = True 9 | FlaskExtras(app) 10 | 11 | 12 | @pytest.fixture() 13 | def client(): 14 | return app, app.test_client() 15 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | """Test configuration utilities.""" 2 | 3 | from flask import Flask 4 | 5 | from flask_extras.filters import config 6 | 7 | 8 | class TestGetFuncs: 9 | """All tests for get funcs function.""" 10 | 11 | def test_get_module_funcs(self, client): 12 | """Test the return value.""" 13 | assert isinstance(config._get_funcs(config), dict) 14 | 15 | def test_get_module_funcs_notempty(self, client): 16 | """Test the return value functions length.""" 17 | assert len(config._get_funcs(config).items()) > 0 18 | 19 | 20 | class TestInjectFilters: 21 | """All tests for inject filters function.""" 22 | 23 | def test_inject_filters_inst(self, client): 24 | """Test the return value.""" 25 | app, test = client 26 | assert isinstance(config._inject_filters(app, {}), Flask) 27 | 28 | def test_inject_filters_count(self, client): 29 | """Test the return value.""" 30 | app, test = client 31 | old = len(app.jinja_env.filters) 32 | config._inject_filters(app, {'foo': lambda x: x}) 33 | new = len(app.jinja_env.filters) 34 | assert new > old 35 | assert 'foo' in app.jinja_env.filters 36 | 37 | 38 | class TestConfigFlaskFilters: 39 | """All tests for config flask filters function.""" 40 | 41 | def test_config_filters_inst(self, client): 42 | """Test the return value.""" 43 | app, test = client 44 | assert isinstance(config.config_flask_filters(app), Flask) 45 | 46 | def test_config_filters_count(self, client): 47 | """Test the return value.""" 48 | app, test = client 49 | del app.jinja_env.filters 50 | setattr(app.jinja_env, 'filters', dict()) 51 | old = len(app.jinja_env.filters) 52 | config.config_flask_filters(app) 53 | new = len(app.jinja_env.filters) 54 | assert new > old 55 | -------------------------------------------------------------------------------- /tests/test_datetimes.py: -------------------------------------------------------------------------------- 1 | """Test munging filters.""" 2 | 3 | from dateutil.parser import parse as dtparse 4 | 5 | from flask_extras.filters import datetimes 6 | 7 | 8 | class TestStr2Dt: 9 | """All tests for str2dt function.""" 10 | 11 | def test_title_returns_valid(self): 12 | """Test function.""" 13 | timestr = '01-05-1900 00:00:00' 14 | res = datetimes.str2dt(timestr) 15 | assert res == dtparse(timestr) == res 16 | 17 | def test_title_returns_invalid(self): 18 | """Test function.""" 19 | assert datetimes.str2dt(None) is None 20 | 21 | def test_title_returns_invalid_nonetype_str(self): 22 | """Test function.""" 23 | assert datetimes.str2dt('None') is None 24 | 25 | def test_title_returns_invalid_nonetype_str2(self): 26 | """Test function.""" 27 | assert datetimes.str2dt('null') is None 28 | -------------------------------------------------------------------------------- /tests/test_decorators.py: -------------------------------------------------------------------------------- 1 | """Test jinja filters.""" 2 | 3 | from flask import Flask 4 | import pytest 5 | 6 | from flask_extras import FlaskExtras 7 | from flask_extras import decorators 8 | 9 | app = Flask('test_flask_jsondash') 10 | app.debug = True 11 | app.secret_key = 'Foo' 12 | FlaskExtras(app) 13 | 14 | 15 | @app.route('/xhr-custom') 16 | @decorators.xhr_only(status_code=400) 17 | def foo_xhr_custom(): 18 | return '' 19 | 20 | 21 | @app.route('/xhr') 22 | @decorators.xhr_only() 23 | def foo_xhr(): 24 | return '' 25 | 26 | 27 | @app.route('/args') 28 | @decorators.require_args(params=['foo', 'bar']) 29 | def foo_args(): 30 | return '' 31 | 32 | 33 | @app.route('/cookies') 34 | @decorators.require_cookies(['foo']) 35 | def foo_cookies(): 36 | return '' 37 | 38 | 39 | @app.route('/headers') 40 | @decorators.require_headers(headers=['X-Foo']) 41 | def foo_headers(): 42 | return '' 43 | 44 | client = app.test_client() 45 | 46 | 47 | class TestXhr: 48 | """Test class for function.""" 49 | 50 | def test_invalid_xhr_decorator(self): 51 | """Test expected failure of function.""" 52 | with app.app_context(): 53 | res = client.get('/xhr') 54 | assert res.status_code == 415 55 | 56 | def test_invalid_xhr_custom_decorator(self): 57 | """Test expected success of function.""" 58 | with app.app_context(): 59 | res = client.get('/xhr-custom') 60 | assert res.status_code == 400 61 | 62 | 63 | class TestRequireArgs: 64 | """Test class for function.""" 65 | 66 | def test_invalid_require_args_decorator(self): 67 | """Test expected failure of function.""" 68 | with pytest.raises(ValueError): 69 | with app.app_context(): 70 | client.get('/args') 71 | 72 | def test_valid_require_args_decorator(self): 73 | """Test expected success of function.""" 74 | with app.app_context(): 75 | res = client.get('/args?foo=1&bar=1') 76 | assert res.status_code == 200 77 | 78 | 79 | class TestRequireCookies: 80 | """Test class for function.""" 81 | 82 | def test_invalid_require_cookies_decorator(self): 83 | """Test expected failure of function.""" 84 | with pytest.raises(ValueError): 85 | with app.app_context(): 86 | client.get('/cookies') 87 | 88 | 89 | class TestRequireHeaders: 90 | """Test class for function.""" 91 | 92 | def test_invalid_require_headers_decorator(self): 93 | """Test expected failure of function.""" 94 | with pytest.raises(ValueError): 95 | with app.app_context(): 96 | client.get('/headers') 97 | 98 | def test_valid_require_headers_decorator(self): 99 | """Test expected success of function.""" 100 | with app.app_context(): 101 | res = client.get('/headers', headers={'X-Foo': 'Foo'}) 102 | assert res.status_code == 200 103 | -------------------------------------------------------------------------------- /tests/test_filters.py: -------------------------------------------------------------------------------- 1 | """Test jinja filters.""" 2 | 3 | import json 4 | 5 | from flask_extras.filters import filters 6 | 7 | 8 | class MockClass: 9 | """Empty class for testing.""" 10 | 11 | 12 | class TestCamel2Hyphen: 13 | """All tests for camel2hyphen function.""" 14 | 15 | def test_empty_str(self): 16 | """Test.""" 17 | assert filters.camel2hyphen('', ) == '' 18 | 19 | def test_valid_single_upper_first(self): 20 | """Test.""" 21 | assert filters.camel2hyphen('CamelCase', ) == 'camel-case' 22 | 23 | def test_valid_three_hyphen(self): 24 | """Test.""" 25 | assert filters.camel2hyphen('bCamelCase', ) == 'b-camel-case' 26 | 27 | def test_valid_repeating_upper(self): 28 | """Test.""" 29 | assert filters.camel2hyphen('bCamelCASE', ) == 'b-camel-case' 30 | 31 | def test_valid_nubers(self): 32 | """Test.""" 33 | assert filters.camel2hyphen('bCamelCASE123', ) == 'b-camel-case123' 34 | 35 | 36 | class TestToJson: 37 | """All tests for to_json function.""" 38 | 39 | def test_empty_str(self): 40 | """Test.""" 41 | assert filters.to_json('', ) == json.dumps('') 42 | 43 | def test_empty_str_with_indent(self): 44 | """Test.""" 45 | assert filters.to_json('', indent=4) == json.dumps('', indent=4) 46 | 47 | def test_dict(self): 48 | """Test.""" 49 | data = dict(name='bar', id=123) 50 | assert filters.to_json(data) == json.dumps(data) 51 | 52 | def test_dict_with_indent(self): 53 | """Test.""" 54 | data = dict(name='bar', id=123) 55 | assert filters.to_json(data, indent=4) == json.dumps(data, indent=4) 56 | 57 | 58 | class TestCssSelector: 59 | """All tests for css_selector function.""" 60 | 61 | def test_title_returns_invalid(self): 62 | """Test the return value for a valid type.""" 63 | assert filters.css_selector(123) == 123 64 | 65 | def test_title_returns_str(self): 66 | """Test the return value for a valid type.""" 67 | assert isinstance(filters.css_selector('foo bar'), str) 68 | 69 | def test_basic(self): 70 | """Test the argument.""" 71 | assert filters.css_selector('Hello World') == 'hello-world' 72 | 73 | def test_no_lowercase(self): 74 | """Test the argument.""" 75 | expected = 'Hello-World' 76 | assert filters.css_selector('Hello World', lowercase=False) == expected 77 | 78 | 79 | class TestTitle: 80 | """All tests for title function.""" 81 | 82 | def test_title_returns_str(self): 83 | """Test the return value for a valid type.""" 84 | assert isinstance(filters.title('foo bar'), str) 85 | 86 | def test_title_word(self): 87 | """Test the `word` argument.""" 88 | assert filters.title('foo bar') == 'Foo bar' 89 | 90 | def test_title_capitalize(self): 91 | """Test the `capitalize` argument.""" 92 | assert filters.title('foo bar', capitalize=True) == 'Foo Bar' 93 | 94 | def test_title_capitalize_sentence(self): 95 | """Test the `capitalize` argument.""" 96 | res = filters.title('the quick brown fox... ah forget it', 97 | capitalize=True) 98 | expected = 'The Quick Brown Fox... Ah Forget It' 99 | assert res == expected 100 | 101 | def test_title_none(self): 102 | """Test the function with None argument.""" 103 | assert filters.questionize_label(None) == '' 104 | 105 | 106 | class TestQuestionizeLabel: 107 | """All tests for questionize label function.""" 108 | 109 | def test_questionize_label_returns_str(self): 110 | """Test the return value for a valid type.""" 111 | assert isinstance(filters.questionize_label('foo bar'), str) 112 | 113 | def test_questionize_label_word_is(self): 114 | """Test the `word` argument.""" 115 | assert filters.questionize_label('is_cool') == 'cool?' 116 | 117 | def test_questionize_label_word_has(self): 118 | """Test the `word` argument.""" 119 | assert filters.questionize_label('has_stuff') == 'stuff?' 120 | 121 | def test_questionize_label_none(self): 122 | """Test the function with None argument.""" 123 | assert filters.questionize_label(None) == '' 124 | 125 | 126 | class TestFirstOf: 127 | """All tests for first of function.""" 128 | 129 | def test_firstof_all_false(self): 130 | """Test what is returned when all values are falsy.""" 131 | assert filters.firstof([None, False, 0]) == '' 132 | 133 | def test_firstof_last_true(self): 134 | """Test what is returned when last value is true.""" 135 | assert filters.firstof([None, False, 0, 'yay']) == 'yay' 136 | 137 | def test_firstof_first_true(self): 138 | """Test what is returned when first value is true.""" 139 | assert filters.firstof(['yay', False, 0, 'yay']) == 'yay' 140 | 141 | 142 | class TestAdd: 143 | """All tests for add function.""" 144 | 145 | def test_returns_updated_list(self): 146 | """Test return value.""" 147 | assert filters.add([1, 2], 3) == [1, 2, 3] 148 | 149 | 150 | class TestCut: 151 | """All tests for cut function.""" 152 | 153 | def test_returns_updated_string(self): 154 | """Test return value.""" 155 | assert filters.cut('Hello world', ['world']) == 'Hello ' 156 | 157 | def test_returns_updated_multi(self): 158 | """Test return value.""" 159 | assert filters.cut( 160 | 'Well hello world', ['hello', 'world']) == 'Well ' 161 | 162 | def test_returns_updated_multispace(self): 163 | """Test return value.""" 164 | assert filters.cut( 165 | 'String with spaces', [' ']) == 'Stringwithspaces' 166 | 167 | 168 | class TestAddSlashes: 169 | """All tests for add slashes function.""" 170 | 171 | def test_returns_updated_basic(self): 172 | """Test return value.""" 173 | res = filters.addslashes("I'm using Flask!") 174 | assert res == "I\\'m using Flask!" 175 | 176 | def test_returns_updated_empty(self): 177 | """Test return value.""" 178 | res = filters.addslashes("Using Flask!") 179 | assert res == "Using Flask!" 180 | 181 | def test_returns_updated_complex(self): 182 | """Test return value.""" 183 | res = filters.addslashes("I'm u's'i'n'g Flask!") 184 | assert res == "I\\'m u\\'s\\'i\\'n\\'g Flask!" 185 | 186 | 187 | class TestDefaultVal: 188 | """All tests for default val function.""" 189 | 190 | def test_returns_default(self): 191 | """Test return value.""" 192 | assert filters.default(False, 'default') == 'default' 193 | 194 | def test_returns_original(self): 195 | """Test return value.""" 196 | assert filters.default(1, 'default') == 1 197 | 198 | 199 | class TestDefaultIfNoneVal: 200 | """All tests for default if none function.""" 201 | 202 | def test_returns_default(self): 203 | """Test return value.""" 204 | assert filters.default_if_none(None, 'default') == 'default' 205 | 206 | def test_returns_original(self): 207 | """Test return value.""" 208 | assert filters.default_if_none(1, 'default') == 1 209 | 210 | 211 | class TestGetDigit: 212 | """All tests for get digit function.""" 213 | 214 | def test_returns_index_empty(self): 215 | """Test return value.""" 216 | assert filters.get_digit(123456789, 0) == 123456789 217 | 218 | def test_returns_index_end(self): 219 | """Test return value.""" 220 | assert filters.get_digit(123456789, 1) == 9 221 | 222 | def test_returns_index_mid(self): 223 | """Test return value.""" 224 | assert filters.get_digit(123456789, 5) == 5 225 | 226 | def test_returns_index_beg(self): 227 | """Test return value.""" 228 | assert filters.get_digit(123456789, 9) == 1 229 | 230 | 231 | class TestLengthIs: 232 | """All tests for length is function.""" 233 | 234 | def test_returns_false(self): 235 | """Test return value.""" 236 | assert not filters.length_is('three', 4) 237 | 238 | def test_returns_true(self): 239 | """Test return value.""" 240 | assert filters.length_is('one', 3) 241 | 242 | 243 | class TestIsUrl: 244 | """All tests for is url function.""" 245 | 246 | def test_returns_urls_true(self): 247 | """Test return value.""" 248 | assert filters.is_url('http://foo.bar') 249 | assert filters.is_url('https://foo.bar') 250 | 251 | def test_returns_urls_false(self): 252 | """Test return value.""" 253 | assert not filters.is_url('//foo.bar') 254 | 255 | 256 | class TestLJust: 257 | """All tests for ljust function.""" 258 | 259 | def test_returns_lpadding(self): 260 | """Test return value.""" 261 | assert filters.ljust('Flask', 10) == 'Flask ' 262 | 263 | 264 | class TestRJust: 265 | """All tests for rjust function.""" 266 | 267 | def test_returns_rpadding(self): 268 | """Test return value.""" 269 | assert filters.rjust('Flask', 10) == ' Flask' 270 | 271 | 272 | class TestMakeList: 273 | """All tests for make list function.""" 274 | 275 | def test_list2list(self): 276 | """Test return value.""" 277 | assert filters.make_list([1, 2]) == [1, 2] 278 | 279 | def test_ints_not_coerced(self): 280 | """Test return value.""" 281 | assert filters.make_list( 282 | '12', coerce_numbers=False) == ['1', '2'] 283 | 284 | def test_ints_coerced(self): 285 | """Test return value.""" 286 | assert filters.make_list('12') == [1, 2] 287 | 288 | def test_dict(self): 289 | """Test return value.""" 290 | assert filters.make_list({'foo': 'bar'}) == [('foo', 'bar')] 291 | 292 | def test_list(self): 293 | """Test return value.""" 294 | assert filters.make_list([1, 2]) == [1, 2] 295 | 296 | def test_str(self): 297 | """Test return value.""" 298 | assert filters.make_list('abc') == ['a', 'b', 'c'] 299 | 300 | 301 | class TestPhone2Numeric: 302 | """All tests for phone2numeric function.""" 303 | 304 | def test_basic(self): 305 | """Test return value.""" 306 | assert filters.phone2numeric('1800-COLLeCT') == '1800-2655328' 307 | 308 | def test_1_thru_9(self): 309 | """Test return value.""" 310 | assert filters.phone2numeric('1800-ADGJMPTX') == '1800-23456789' 311 | 312 | 313 | class TestSlugify: 314 | """All tests for slugify function.""" 315 | 316 | def test_slugify_plain(self): 317 | """Test return value.""" 318 | assert filters.slugify('My news title!') == 'my-news-title' 319 | 320 | def test_slugify_complex(self): 321 | """Test return value.""" 322 | res = filters.slugify('I am an OBFUsc@@Ted URL!!! Foo bar') 323 | expected = 'i-am-an-obfusc--ted-url----foo-bar' 324 | assert res == expected 325 | 326 | 327 | class TestPagetitle: 328 | """All tests for pagetitle function.""" 329 | 330 | def test_title_plain(self): 331 | """Test return value.""" 332 | assert filters.pagetitle('/foo/bar/bam') == ' > foo > bar > bam' 333 | 334 | def test_title_removefirst(self): 335 | """Test return value.""" 336 | res = filters.pagetitle('/foo/bar/bam', divider=' | ') 337 | expected = ' | foo | bar | bam' 338 | assert res == expected 339 | 340 | def test_title_divider(self): 341 | """Test return value.""" 342 | res = filters.pagetitle('/foo/bar/bam', remove_first=True) 343 | assert res == 'foo > bar > bam' 344 | 345 | 346 | class TestGreet: 347 | """All tests for greet function.""" 348 | 349 | def test_greet(self): 350 | """Test return value.""" 351 | assert filters.greet('Chris') == 'Hello, Chris!' 352 | 353 | def test_greet_override(self): 354 | """Test return value.""" 355 | assert filters.greet( 356 | 'Chris', greeting='Bonjour' == 'Bonjour, Chris!') 357 | 358 | 359 | class TestIsList: 360 | """All tests for islist function.""" 361 | 362 | def test_islist(self): 363 | """Test return value.""" 364 | assert filters.islist([1, 2, 3]) 365 | 366 | def test_notislist(self): 367 | """Test return value.""" 368 | assert not filters.islist('Foo') 369 | assert not filters.islist({'foo': 'bar'}) 370 | assert not filters.islist(1) 371 | assert not filters.islist(1.0) 372 | 373 | 374 | class TestSql2dict: 375 | """All tests for sql2dict function.""" 376 | 377 | def test_none(self): 378 | """Test return value.""" 379 | assert filters.sql2dict(None) == [] 380 | 381 | def test_set(self): 382 | """Test return value.""" 383 | mm = MockClass() 384 | mm.__dict__ = {'foo': 'bar'} 385 | assert filters.sql2dict([mm]) == [{'foo': 'bar'}] 386 | -------------------------------------------------------------------------------- /tests/test_layout.py: -------------------------------------------------------------------------------- 1 | """Tests for 'layout' filters.""" 2 | 3 | from flask_extras.filters import layout 4 | 5 | 6 | class TestBs3Col: 7 | """All tests for bs3 col function.""" 8 | 9 | def test_returns_right_width(self): 10 | """Test the return value for a valid type.""" 11 | assert layout.bs3_cols(1) == 12 12 | assert layout.bs3_cols(2) == 6 13 | assert layout.bs3_cols(3) == 4 14 | assert layout.bs3_cols(4) == 3 15 | assert layout.bs3_cols(5) == 2 16 | assert layout.bs3_cols(6) == 2 17 | 18 | def test_returns_right_width_bad_data(self): 19 | """Test the return value for an invalid type.""" 20 | assert layout.bs3_cols(None) == 12 21 | assert layout.bs3_cols('foo') == 12 22 | assert layout.bs3_cols(dict()) == 12 23 | -------------------------------------------------------------------------------- /tests/test_munging.py: -------------------------------------------------------------------------------- 1 | """Test munging filters.""" 2 | 3 | from flask_extras.filters import munging 4 | 5 | import pytest 6 | 7 | 8 | class TestFilterVals: 9 | """All tests for filter_vals function.""" 10 | 11 | def test_title_returns_invalid_first(self): 12 | """Test function.""" 13 | assert munging.filter_vals({}, None) == {} 14 | 15 | def test_title_returns_invalid_second(self): 16 | """Test function.""" 17 | assert munging.filter_vals(None, []) is None 18 | 19 | def test_title_returns_invalid_both(self): 20 | """Test function.""" 21 | assert munging.filter_vals(None, None) is None 22 | 23 | def test_title_returns_valid_empty(self): 24 | """Test function.""" 25 | assert munging.filter_vals(dict(), []) == {} 26 | 27 | def test_title_returns_valid_filtered_empty(self): 28 | """Test function.""" 29 | assert munging.filter_vals(dict(foo='bar'), ['bar']) == {} 30 | 31 | def test_title_returns_valid_filtered(self): 32 | """Test function.""" 33 | assert munging.filter_vals( 34 | dict(foo='bar', bar='foo'), ['bar']) == dict(bar='foo') 35 | 36 | def test_title_returns_valid_filtered_invalid_val(self): 37 | """Test function.""" 38 | d = dict(foo='bar', bar='foo') 39 | assert munging.filter_vals(d, ['baz']) == d 40 | 41 | 42 | class TestFilterKeys: 43 | """All tests for filter_keys function.""" 44 | 45 | def test_title_returns_invalid_first(self): 46 | """Test function.""" 47 | assert munging.filter_keys({}, None) == {} 48 | 49 | def test_title_returns_invalid_second(self): 50 | """Test function.""" 51 | assert munging.filter_keys(None, []) is None 52 | 53 | def test_title_returns_invalid_both(self): 54 | """Test function.""" 55 | assert munging.filter_keys(None, None) is None 56 | 57 | def test_title_returns_valid_empty(self): 58 | """Test function.""" 59 | assert munging.filter_keys(dict(), []) == {} 60 | 61 | def test_title_returns_valid_filtered_empty(self): 62 | """Test function.""" 63 | assert munging.filter_keys(dict(foo='bar'), ['foo']) == {} 64 | 65 | def test_title_returns_valid_filtered(self): 66 | """Test function.""" 67 | assert munging.filter_keys( 68 | dict(foo='bar', bar='foo'), ['bar']) == dict(foo='bar') 69 | 70 | def test_title_returns_valid_filtered_invalid_val(self): 71 | """Test function.""" 72 | d = dict(foo='bar', bar='foo') 73 | assert munging.filter_keys(d, ['baz']) == d 74 | 75 | 76 | class TestFilterList: 77 | """All tests for filter_list function.""" 78 | 79 | def test_title_returns_invalid_first(self): 80 | """Test function.""" 81 | assert munging.filter_list([], None) == [] 82 | 83 | def test_title_returns_invalid_second(self): 84 | """Test function.""" 85 | assert munging.filter_list(None, []) is None 86 | 87 | def test_title_returns_invalid_both(self): 88 | """Test function.""" 89 | assert munging.filter_list(None, None) is None 90 | 91 | def test_title_returns_invalid_dict(self): 92 | """Test function.""" 93 | assert munging.filter_list(dict(), []) == dict() 94 | 95 | def test_title_returns_valid_filtered_empty(self): 96 | """Test function.""" 97 | assert munging.filter_list([], ['foo']) == [] 98 | 99 | def test_title_returns_valid_filtered(self): 100 | """Test function.""" 101 | assert munging.filter_list(['foo', 'bar'], ['bar']) == ['foo'] 102 | 103 | def test_title_returns_valid_filtered_invalid_val(self): 104 | """Test function.""" 105 | assert munging.filter_list(['foo', 'bar'], ['baz']) == ['foo', 'bar'] 106 | 107 | 108 | class TestGroupBy: 109 | """All tests for group_by function.""" 110 | 111 | def _get_obj(self, name): 112 | """Data for tests.""" 113 | class ObjClass(object): 114 | def __init__(self, name=None): 115 | if name is not None: 116 | self.name = name 117 | return ObjClass(name=name) 118 | 119 | def test_returns_no_objs_noname(self): 120 | """Test function.""" 121 | objs = [None for _ in range(4)] 122 | res = munging.group_by(objs, attr=None) 123 | assert res.keys() == ['__unlabeled'] 124 | assert len(res['__unlabeled']) == 4 125 | 126 | def test_returns_no_objs_with_name(self): 127 | """Test function.""" 128 | objs = [None for _ in range(4)] 129 | res = munging.group_by(objs, attr='invalid-attr') 130 | assert res.keys() == ['__unlabeled'] 131 | assert len(res['__unlabeled']) == 4 132 | 133 | def test_returns_objs_nogroup_noname(self): 134 | """Test function.""" 135 | objs = [self._get_obj(name) for name in ['foo1']] 136 | res = munging.group_by(objs, attr=None) 137 | assert res.keys() == ['__unlabeled'] 138 | assert len(res['__unlabeled']) == 1 139 | 140 | def test_returns_objs_nogroup_fallback(self): 141 | """Test function.""" 142 | objs = [self._get_obj(name) for name in ['foo1']] 143 | res = munging.group_by(objs, attr=None, fallback='somegroup') 144 | assert res.keys() == ['somegroup'] 145 | assert len(res['somegroup']) == 1 146 | 147 | def test_returns_objs_nogroup(self): 148 | """Test function.""" 149 | objs = [self._get_obj(None)] 150 | res = munging.group_by(objs, attr='name') 151 | assert res.keys() == ['__unlabeled'] 152 | assert len(res['__unlabeled']) == 1 153 | 154 | def test_returns_objs_group_custom_group(self): 155 | """Test function.""" 156 | objs = [self._get_obj(name) for name in ['foo1', 'foo2']] 157 | groups = [('group1', ('foo1', 'foo2'))] 158 | res = munging.group_by(objs, groups=groups, attr='name') 159 | assert res.keys() == ['group1', '__unlabeled'] 160 | assert len(res['group1']) == 2 161 | 162 | def test_returns_objs_group_custom_group_with_one_unlabeled(self): 163 | """Test function.""" 164 | objs = [self._get_obj(name) for name in ['foo1', 'foo2', 'foo3']] 165 | groups = [('group1', ('foo1', 'foo2'))] 166 | res = munging.group_by(objs, groups=groups, attr='name') 167 | assert res.keys() == ['group1', '__unlabeled'] 168 | assert len(res['group1']) == 2 169 | assert len(res['__unlabeled']) == 1 170 | 171 | def test_returns_objs_group_custom_group_with_one_unlabeled_complex(self): 172 | """Test function.""" 173 | names = ['foo{}'.format(i) for i in range(1, 11)] 174 | objs = [self._get_obj(name) for name in names] 175 | groups = [ 176 | ('group1', ('foo1', 'foo2', 'foo3')), 177 | ('group2', ('foo4', 'foo5', 'foo6')), 178 | ('group3', ('foo7', 'foo8', 'foo9')), 179 | ] 180 | res = munging.group_by(objs, groups=groups, attr='name') 181 | for key in res.keys(): 182 | assert key in ['group1', 'group2', 'group3', '__unlabeled'] 183 | assert len(res.keys()) == 4 184 | assert len(res['group1']) == 3 185 | assert len(res['group2']) == 3 186 | assert len(res['group3']) == 3 187 | assert len(res['__unlabeled']) == 1 188 | 189 | def test_returns_objs_group_custom_group_with_order_preserved(self): 190 | """Test function.""" 191 | names = ['foo{}'.format(i) for i in range(1, 10)] 192 | objs = [self._get_obj(name) for name in names] 193 | groups = [ 194 | ('group1', ('foo2', 'foo1', 'foo3')), 195 | ('group2', ('foo5', 'foo4', 'foo6')), 196 | ('group3', ('foo7', 'foo9', 'foo8')), 197 | ] 198 | res = munging.group_by(objs, groups=groups, attr='name') 199 | for key in res.keys(): 200 | assert key in ['group1', 'group2', 'group3', '__unlabeled'] 201 | for group in groups: 202 | label, items = group 203 | for i, item in enumerate(items): 204 | obj_label = getattr(res[label][i], 'name') 205 | assert item == obj_label 206 | 207 | 208 | class TestSortDictKeysFromReflist: 209 | """All tests for sort_dict_keys_from_reflist function.""" 210 | 211 | def test_sort_dict_keys_from_reflist(self): 212 | """Test function.""" 213 | data = dict(foo=1, bar=2, baz=3, quux=4) 214 | ref = ['quux', 'baz', 'foo', 'bar'] 215 | expected = [('quux', 4), ('baz', 3), ('foo', 1), ('bar', 2)] 216 | assert munging.sort_dict_keys_from_reflist(data, ref) == expected 217 | 218 | def test_sort_dict_keys_from_reflist_nested(self): 219 | """Test function.""" 220 | data = dict(foo=dict(inner1=1, inner2=2), bar=2, baz=3, quux=4) 221 | ref = ['quux', 'baz', 'foo', 'bar'] 222 | expected = [ 223 | ('quux', 4), ('baz', 3), 224 | ('foo', {'inner1': 1, 'inner2': 2}), ('bar', 2)] 225 | assert munging.sort_dict_keys_from_reflist(data, ref) == expected 226 | 227 | def test_sort_dict_keys_from_reflist_none(self): 228 | """Test function.""" 229 | data = dict(foo=None, bar=2, baz=3, quux=4) 230 | ref = ['quux', 'baz', 'foo', 'bar'] 231 | expected = [('quux', 4), ('baz', 3), ('foo', None), ('bar', 2)] 232 | assert munging.sort_dict_keys_from_reflist(data, ref) == expected 233 | 234 | def test_sort_dict_keys_from_reflist_missing_val(self): 235 | """Test function.""" 236 | data = dict(foo=1, bar=2, baz=3, quux=4) 237 | ref = ['quux', 'baz', 'foo'] 238 | expected = [('quux', 4), ('baz', 3), ('foo', 1)] 239 | assert munging.sort_dict_keys_from_reflist(data, ref) == expected 240 | 241 | 242 | class TestSortDictValsFromReflist: 243 | """All tests for sort_dict_vals_from_reflist function.""" 244 | 245 | def test_sort_dict_vals_from_reflist(self): 246 | """Test function.""" 247 | data = dict(foo=1, bar=2, baz=3, quux=4) 248 | ref = [4, 3, 1, 2] 249 | expected = [('quux', 4), ('baz', 3), ('foo', 1), ('bar', 2)] 250 | assert munging.sort_dict_vals_from_reflist(data, ref) == expected 251 | 252 | def test_sort_dict_vals_from_reflist_nested(self): 253 | """Test function.""" 254 | data = dict(foo=dict(inner1=1, inner2=2), bar=2, baz=3, quux=4) 255 | ref = [4, 3, {'inner1': 1, 'inner2': 2}, 2] 256 | expected = [ 257 | ('quux', 4), ('baz', 3), 258 | ('foo', {'inner1': 1, 'inner2': 2}), ('bar', 2)] 259 | assert munging.sort_dict_vals_from_reflist(data, ref) == expected 260 | 261 | def test_sort_dict_vals_from_reflist_none(self): 262 | """Test function.""" 263 | data = dict(foo=None, bar=2, baz=3, quux=4) 264 | ref = [4, 3, None, 2] 265 | expected = [('quux', 4), ('baz', 3), ('foo', None), ('bar', 2)] 266 | assert munging.sort_dict_vals_from_reflist(data, ref) == expected 267 | 268 | def test_sort_dict_vals_from_reflist_missing_val(self): 269 | """Test function.""" 270 | data = dict(foo=1, bar=2, baz=3, quux=4) 271 | ref = [4, 3, 1] 272 | expected = [('quux', 4), ('baz', 3), ('foo', 1)] 273 | assert munging.sort_dict_vals_from_reflist(data, ref) == expected 274 | -------------------------------------------------------------------------------- /tests/test_random.py: -------------------------------------------------------------------------------- 1 | """Tests for 'random' filters.""" 2 | 3 | import re 4 | 5 | from flask_extras.filters import random 6 | 7 | 8 | class TestRandomChoice: 9 | """All tests for random choice function.""" 10 | 11 | def test_choice_returns_str(self): 12 | """Test the return value for a valid type.""" 13 | assert isinstance(random.rand_choice([0, 1, 2, 3]), int) 14 | 15 | 16 | class TestRandomNameTitle: 17 | """All tests for random title function.""" 18 | 19 | def test_name_returns_str(self): 20 | """Test the return value for a valid type.""" 21 | assert isinstance(random.rand_name_title('Chris'), str) 22 | 23 | def test_name_returns_spaced_name(self): 24 | """Test the return value for a valid value length.""" 25 | assert len(random.rand_name_title('Chris').split()) == 2 26 | 27 | 28 | class TestRandomColor: 29 | """All tests for random color function.""" 30 | 31 | def test_returns_str(self): 32 | """Test the return value for a valid type.""" 33 | assert isinstance(random.rand_color(), str) 34 | 35 | def test_returns_rgba_format(self): 36 | """Test the return value for a valid string format.""" 37 | re_rgba = r'rgba\([0-9{0,3}]+, [0-9{0,3}]+, [0-9{0,3}]+, [0-9{0,3}]+\)' 38 | assert re.match(re_rgba, random.rand_color(alpha=10)) is not None 39 | -------------------------------------------------------------------------------- /tests/test_validators_network.py: -------------------------------------------------------------------------------- 1 | """Test WTForm validators.""" 2 | 3 | from collections import namedtuple 4 | 5 | import pytest 6 | 7 | from flask_extras.forms.validators import network 8 | 9 | 10 | class FakeCls(object): 11 | pass 12 | 13 | 14 | Field = namedtuple('Field', 'data') 15 | 16 | 17 | def test_is_ip_valid(): 18 | assert network.is_ip('192.168.1.0') 19 | assert network.is_ip('0.0.0.0') 20 | assert network.is_ip('255.255.255.255') 21 | 22 | 23 | def test_is_ip_invalid(): 24 | assert not network.is_ip('foo.x.y-baz.com') 25 | assert not network.is_ip('https://www.rad.com') 26 | 27 | 28 | def test_is_hostname_valid(): 29 | assert network.is_hostname('foo.x.y-baz.com') 30 | assert network.is_hostname('https://www.rad.com') 31 | assert network.is_hostname('https://localhost:8080') 32 | assert network.is_hostname('somehost/') 33 | 34 | 35 | def test_is_hostname_invalid_ips(): 36 | assert not network.is_hostname('192.168.1.0') 37 | assert not network.is_hostname('0.0.0.0') 38 | assert not network.is_hostname('255.255.255.255') 39 | 40 | 41 | def test_is_hostname_invalid_ends_hyphen(): 42 | assert not network.is_hostname('www.foo.com-') 43 | 44 | 45 | def test_is_hostname_invalid_underscore(): 46 | assert not network.is_hostname('www.bar.foo_.com') 47 | 48 | 49 | def test_valid_hosts_invalid_octet(): 50 | with pytest.raises(ValueError): 51 | network.valid_hosts(FakeCls(), Field(data='192.3.120.1111')) 52 | 53 | 54 | def test_valid_hosts_valid_hostname_single(): 55 | # Just ensuring it doesn't raise a ValueError 56 | network.valid_hosts(FakeCls(), Field(data='www.foo.com')) 57 | 58 | 59 | def test_valid_hosts_valid_hostname_list(): 60 | # Just ensuring it doesn't raise a ValueError 61 | network.valid_hosts(FakeCls(), Field(data='www.foo.com,bar.baz.rad')) 62 | 63 | 64 | def test_valid_hosts_valid_hostname_ip_mix(): 65 | # Just ensuring it doesn't raise a ValueError 66 | network.valid_hosts(FakeCls(), Field(data='0.0.0.0, www.foo.com')) 67 | 68 | 69 | def test_valid_hosts_valid_single(): 70 | # Just ensuring it doesn't raise a ValueError 71 | network.valid_hosts(FakeCls(), Field(data='0.0.0.0')) 72 | network.valid_hosts(FakeCls(), Field(data='192.168.0.1')) 73 | 74 | 75 | def test_valid_hosts_valid_list(): 76 | # Just ensuring it doesn't raise a ValueError 77 | network.valid_hosts(FakeCls(), Field(data='192.168.0.1,10.3.20.112')) 78 | 79 | 80 | def test_valid_hosts_valid_range(): 81 | # Just ensuring it doesn't raise a ValueError 82 | network.valid_hosts(FakeCls(), Field(data='192.168.0.1-192.168.10.0')) 83 | 84 | 85 | def test_valid_hosts_valid_hyphen_no_ip(): 86 | # Just ensuring it doesn't raise a ValueError 87 | network.valid_hosts(FakeCls(), Field(data='http://www.foo-bar.x.com')) 88 | 89 | 90 | def test_valid_hosts_valid_range_mixed(): 91 | # Just ensuring it doesn't raise a ValueError 92 | network.valid_hosts(FakeCls(), Field( 93 | data='192.168.0.1,http://www.foo.com')) 94 | 95 | 96 | def test_valid_hosts_valid_hyphenated_hostname_ip_mixed(): 97 | # Just ensuring it doesn't raise a ValueError 98 | network.valid_hosts(FakeCls(), Field( 99 | data='http://www.foo-bar.x.com,192.168.0.1')) 100 | 101 | 102 | def test_valid_hosts_valid_hyphenated_hostname_ip_mixed_range(): 103 | # Just ensuring it doesn't raise a ValueError 104 | network.valid_hosts(FakeCls(), Field( 105 | data='http://www.foo-bar.x.com,192.168.0.1-192.168.10.4')) 106 | -------------------------------------------------------------------------------- /tests/test_validators_serialization.py: -------------------------------------------------------------------------------- 1 | """Test WTForm validators.""" 2 | 3 | from collections import namedtuple 4 | 5 | import pytest 6 | 7 | from flask_extras.forms.validators import serialization 8 | 9 | 10 | class FakeCls(object): 11 | pass 12 | 13 | 14 | Field = namedtuple('Field', 'data') 15 | 16 | 17 | def test_valid_json_valid(): 18 | # Just ensuring it doesn't raise a ValueError 19 | serialization.valid_json(FakeCls(), Field(data='{"foo": "bar"}')) 20 | 21 | 22 | def test_valid_json_invalid(): 23 | with pytest.raises(ValueError): 24 | serialization.valid_json(FakeCls(), Field(data='asdASDp')) 25 | 26 | 27 | def test_valid_json_invalid_dict_ish(): 28 | with pytest.raises(ValueError): 29 | serialization.valid_json(FakeCls(), Field(data='{foo: bar}')) 30 | 31 | 32 | def test_valid_json_invalid_none(): 33 | with pytest.raises(TypeError): 34 | serialization.valid_json(FakeCls(), Field(data=None)) 35 | -------------------------------------------------------------------------------- /tests/test_wizard.py: -------------------------------------------------------------------------------- 1 | """Test form wizard.""" 2 | 3 | import json 4 | 5 | import pytest 6 | 7 | from flask import ( 8 | flash, 9 | jsonify, 10 | render_template, 11 | request, 12 | session, 13 | ) 14 | from flask.ext.wtf import FlaskForm 15 | from wtforms import ( 16 | IntegerField, 17 | StringField, 18 | validators, 19 | ) 20 | from flask_extras.forms.wizard import MultiStepWizard 21 | 22 | from conftest import app 23 | 24 | 25 | TESTING_SESSION_KEY = 'fakename' 26 | 27 | 28 | class MultiStepTest1(FlaskForm): 29 | field1 = StringField(validators=[validators.DataRequired()],) 30 | field2 = IntegerField(validators=[validators.DataRequired()],) 31 | 32 | 33 | class MultiStepTest2(FlaskForm): 34 | field3 = StringField(validators=[validators.DataRequired()],) 35 | field4 = IntegerField(validators=[validators.DataRequired()],) 36 | 37 | 38 | class MyCoolForm(MultiStepWizard): 39 | __forms__ = [ 40 | MultiStepTest1, 41 | MultiStepTest2, 42 | ] 43 | 44 | 45 | @pytest.fixture(scope='module') 46 | def ctx(request): 47 | with app.test_request_context() as req_ctx: 48 | yield req_ctx 49 | if TESTING_SESSION_KEY in session: 50 | del session[TESTING_SESSION_KEY] 51 | if 'MyCoolForm' in session: 52 | del session['MyCoolForm'] 53 | 54 | 55 | @app.route('/wizard', methods=['GET', 'POST']) 56 | def wizard_valid_route(): 57 | form = MyCoolForm() 58 | curr_step = request.args.get('curr_step') 59 | form_kwargs = dict(session_key=TESTING_SESSION_KEY) 60 | combine = 'combine' in request.args 61 | if curr_step is not None: 62 | form_kwargs.update(curr_step=curr_step) 63 | form = MyCoolForm(**form_kwargs) 64 | msg, data = None, None 65 | if request.method == 'POST': 66 | data = form.data 67 | if form.validate_on_submit(): 68 | if form.is_complete(): 69 | data = form.alldata(combine_fields=combine, flush_after=True) 70 | msg = 'Form validated AND COMPLETE! data = {}'.format(data) 71 | else: 72 | msg = ('Great job, but not done yet' 73 | ' ({} steps remain!).'.format(form.remaining)) 74 | else: 75 | msg = 'Invalid' 76 | return jsonify(dict(data=data, msg=msg)) 77 | 78 | 79 | @app.route('/wizard-bad', methods=['GET', 'POST']) 80 | def wizard_invalid_route(): 81 | form = MultiStepWizard() 82 | return jsonify(form.data) 83 | 84 | 85 | def test_wizard_basic_init_nodata(client): 86 | app, test = client 87 | with pytest.raises(AssertionError): 88 | res = test.get('/wizard-bad') 89 | assert res.status_code == 500 90 | 91 | 92 | def test_wizard_basic_init_get(client): 93 | app, test = client 94 | res = test.get('/wizard') 95 | assert res.status_code == 200 96 | data = json.loads(res.data) 97 | assert data['data'] is None 98 | 99 | 100 | def test_wizard_basic_init_post(client): 101 | app, test = client 102 | res = test.post('/wizard', data=dict(field1='Foo', field2=2)) 103 | assert res.status_code == 200 104 | data = json.loads(res.data) 105 | assert data['data'] == dict(field1='Foo', field2=2) 106 | 107 | 108 | def test_wizard_basic_post_jumpstep(client): 109 | app, test = client 110 | res = test.post('/wizard?curr_step=2', 111 | data=dict(field3='Foo', field4=4)) 112 | assert res.status_code == 200 113 | data = json.loads(res.data) 114 | assert data['data'] == dict(field3='Foo', field4=4) 115 | 116 | 117 | def test_wizard_post_fullsteps(ctx, client): 118 | app, test = client 119 | res1 = test.post('/wizard?curr_step=1', 120 | data=dict(field1='Foo', field2=2)) 121 | res2 = test.post('/wizard?curr_step=2', 122 | data=dict(field3='Foo', field4=4)) 123 | assert res1.status_code == 200 124 | assert res2.status_code == 200 125 | data = json.loads(res2.data) 126 | assert data['data'] == dict(field3='Foo', field4=4) 127 | 128 | 129 | def test_wizard_post_bad_fieldtype(ctx, client): 130 | app, test = client 131 | res1 = test.post('/wizard?curr_step=1', 132 | data=dict(field1=1, field2=2)) 133 | res2 = test.post('/wizard?curr_step=2', 134 | data=dict(field3='Foo', field4='Foo')) 135 | assert res1.status_code == 200 136 | assert res2.status_code == 200 137 | data = json.loads(res2.data) 138 | assert data['data']['field4'] is None 139 | assert data['msg'] == 'Invalid' 140 | 141 | 142 | def test_form_is_complete(client): 143 | form = MyCoolForm() 144 | assert not form.is_complete() 145 | 146 | 147 | def test_form_setfields(client): 148 | form = MyCoolForm() 149 | assert len(form._unbound_fields) == 1 150 | assert len(form.__forms__) == 2 151 | # Confirm it's obfuscated for non-local access. 152 | assert not hasattr(form, '__forms') 153 | 154 | 155 | def test_form_get_data(client): 156 | app, test = client 157 | form = MyCoolForm() 158 | assert form.data == dict(field1=None, field2=None) 159 | 160 | 161 | def test_form_get_alldata(client): 162 | form = MyCoolForm() 163 | assert form.alldata() == dict( 164 | MultiStepTest1=None, 165 | MultiStepTest2=None, 166 | ) 167 | 168 | 169 | def test_form_get_alldata_combined_none(client): 170 | form = MyCoolForm() 171 | assert form.alldata(combine_fields=True) == dict() 172 | 173 | 174 | def test_form_populate_forms(client): 175 | form = MyCoolForm() 176 | assert len(form.forms) == 2 177 | 178 | 179 | def test_form_populate_forms_once_only(client): 180 | form = MyCoolForm() 181 | form._populate_forms() 182 | form._populate_forms() 183 | assert len(form.forms) == 2 184 | 185 | 186 | def test_form_active(client): 187 | form = MyCoolForm() 188 | assert form.active_name == 'MultiStepTest1' 189 | assert isinstance(form.active_form, MultiStepTest1) 190 | 191 | 192 | def test_form_step_attrs(client): 193 | form = MyCoolForm() 194 | assert form.step == 1 195 | assert form.total_steps == 2 196 | assert form.remaining == 2 197 | assert form.steps == [1, 2] 198 | 199 | 200 | def test_form_flush(client): 201 | form = MyCoolForm() 202 | assert 'MyCoolForm' in session 203 | form.flush() 204 | assert 'MyCoolForm' not in session 205 | 206 | 207 | def test_form_validate_on_submit(client): 208 | form = MyCoolForm() 209 | assert not form.validate_on_submit() 210 | 211 | 212 | def test_form_len_override(client): 213 | form = MyCoolForm() 214 | assert len(form) == 2 215 | 216 | 217 | def test_form_iter_override(client): 218 | form = MyCoolForm() 219 | assert iter(form) 220 | 221 | 222 | def test_form_getitem_override(client): 223 | form = MyCoolForm() 224 | assert isinstance(form['field1'], StringField) 225 | assert form['field1'].name == 'field1' 226 | form.validate_on_submit() 227 | assert form['field1'].data is None 228 | 229 | 230 | def test_form_wtform_contains(client): 231 | form = MyCoolForm() 232 | assert 'field1' in form 233 | assert 'field2' in form 234 | assert 'field3' not in form 235 | assert 'field4' not in form 236 | 237 | 238 | def test_errors(client): 239 | form = MyCoolForm() 240 | assert form.errors == dict() 241 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist=True 3 | envlist=py{27} 4 | 5 | [testenv] 6 | usedevelop=True 7 | deps=pytest 8 | commands=pytest tests 9 | 10 | [flake8] 11 | max-line-length=80 12 | max-complexity=10 13 | exclude=tests 14 | -------------------------------------------------------------------------------- /wiki/old_setup.md: -------------------------------------------------------------------------------- 1 | ## Older setup 2 | 3 | ~~Since the nature of macros and filters makes it harder to import as a standard package, the best way to use this project is as a git submodule. This can be done easily, just use `git submodule add https://github.com/christabor/flask_extras.git` inside your current git project. This allows easy updates.~~ 4 | 5 | This project has been converted a proper python package. You can use it easily in the same way as before, but without the headache of submodules. 6 | 7 | ## Testing 8 | 9 | Run `nosetests .` 10 | 11 | ## Registering filters 12 | It's easy. All filters are registered at once, using the following command: 13 | 14 | ```python 15 | from flask_extras.filters import config as filter_conf 16 | 17 | filter_conf.config_flask_filters(app) 18 | ``` 19 | 20 | ## Using macros 21 | 22 | The best way is to add the templates to your jinja instance, rather than having to move folders around after cloning the module. 23 | 24 | This can be done using the following 25 | 26 | ### As a submodule: 27 | 28 | ```python 29 | import os 30 | import jinja2 31 | 32 | extra_folders = jinja2.ChoiceLoader([ 33 | app.jinja_loader, 34 | jinja2.FileSystemLoader('{}/flask_extras/macros/'.format(os.getcwd())), 35 | ]) 36 | app.jinja_loader = extra_folders 37 | ``` 38 | 39 | ### As a package: 40 | 41 | ```python 42 | import os 43 | import jinja2 44 | 45 | from flask_extras import macros as extra_macros 46 | 47 | extra_folders = jinja2.ChoiceLoader([ 48 | app.jinja_loader, 49 | jinja2.FileSystemLoader(os.path.dirname(extra_macros.__file__)), 50 | ]) 51 | app.jinja_loader = extra_folders 52 | ``` 53 | 54 | Which will load the default `templates` folder, and the new macros. 55 | 56 | Now, just import them like any other macro! 57 | --------------------------------------------------------------------------------