forked from orbit-oss/flask
Merge branch 'master' of github.com:mitsuhiko/flask
This commit is contained in:
commit
5fd81f702c
20 changed files with 163 additions and 58 deletions
4
CHANGES
4
CHANGES
|
|
@ -21,6 +21,10 @@ Version 1.0
|
|||
- Added :attr:`flask.Flask.config_class`.
|
||||
- Added :meth:`flask.config.Config.get_namespace`.
|
||||
|
||||
- Added ``TEMPLATES_AUTO_RELOAD`` config key. If disabled the
|
||||
templates will be reloaded only if the application is running in
|
||||
debug mode. For higher performance it’s possible to disable that.
|
||||
|
||||
Version 0.10.2
|
||||
--------------
|
||||
|
||||
|
|
|
|||
|
|
@ -202,3 +202,18 @@ you can use relative redirects by prefixing the endpoint with a dot only::
|
|||
|
||||
This will link to ``admin.index`` for instance in case the current request
|
||||
was dispatched to any other admin blueprint endpoint.
|
||||
|
||||
Error Handlers
|
||||
--------------
|
||||
|
||||
Blueprints support the errorhandler decorator just like the :class:`Flask`
|
||||
application object, so it is easy to make Blueprint-specific custom error
|
||||
pages.
|
||||
|
||||
Here is an example for a "404 Page Not Found" exception::
|
||||
|
||||
@simple_page.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
return render_template('pages/404.html')
|
||||
|
||||
More information on error handling see :ref:`errorpages`.
|
||||
|
|
|
|||
|
|
@ -174,6 +174,12 @@ The following configuration values are used internally by Flask:
|
|||
if they are not requested by an
|
||||
XMLHttpRequest object (controlled by
|
||||
the ``X-Requested-With`` header)
|
||||
``TEMPLATES_AUTO_RELOAD`` Flask checks if template was modified each
|
||||
time it is requested and reloads it if
|
||||
necessary. But disk I/O is costly and it may
|
||||
be viable to disable this feature by setting
|
||||
this key to ``False``. This option does not
|
||||
affect debug mode.
|
||||
================================= =========================================
|
||||
|
||||
.. admonition:: More on ``SERVER_NAME``
|
||||
|
|
@ -222,6 +228,9 @@ The following configuration values are used internally by Flask:
|
|||
.. versionadded:: 1.0
|
||||
``SESSION_REFRESH_EACH_REQUEST``
|
||||
|
||||
.. versionadded:: 1.0
|
||||
``TEMPLATES_AUTO_RELOAD``
|
||||
|
||||
Configuring from Files
|
||||
----------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Installation
|
||||
============
|
||||
|
||||
Flask depends on two external libraries, `Werkzeug
|
||||
Flask depends on some external libraries, like `Werkzeug
|
||||
<http://werkzeug.pocoo.org/>`_ and `Jinja2 <http://jinja.pocoo.org/2/>`_.
|
||||
Werkzeug is a toolkit for WSGI, the standard Python interface between web
|
||||
applications and a variety of servers for both development and deployment.
|
||||
|
|
@ -13,7 +13,7 @@ So how do you get all that on your computer quickly? There are many ways you
|
|||
could do that, but the most kick-ass method is virtualenv, so let's have a look
|
||||
at that first.
|
||||
|
||||
You will need Python 2.6 or higher to get started, so be sure to have an
|
||||
You will need Python 2.6 or newer to get started, so be sure to have an
|
||||
up-to-date Python 2.x installation. For using Flask with Python 3 have a
|
||||
look at :ref:`python3-support`.
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ folder within::
|
|||
$ cd myproject
|
||||
$ virtualenv venv
|
||||
New python executable in venv/bin/python
|
||||
Installing distribute............done.
|
||||
Installing setuptools, pip............done.
|
||||
|
||||
Now, whenever you want to work on a project, you only have to activate the
|
||||
corresponding environment. On OS X and Linux, do the following::
|
||||
|
|
@ -113,9 +113,9 @@ Get the git checkout in a new virtualenv and run in development mode::
|
|||
$ git clone http://github.com/mitsuhiko/flask.git
|
||||
Initialized empty Git repository in ~/dev/flask/.git/
|
||||
$ cd flask
|
||||
$ virtualenv venv --distribute
|
||||
$ virtualenv venv
|
||||
New python executable in venv/bin/python
|
||||
Installing distribute............done.
|
||||
Installing setuptools, pip............done.
|
||||
$ . venv/bin/activate
|
||||
$ python setup.py develop
|
||||
...
|
||||
|
|
@ -129,45 +129,53 @@ To just get the development version without git, do this instead::
|
|||
|
||||
$ mkdir flask
|
||||
$ cd flask
|
||||
$ virtualenv venv --distribute
|
||||
$ virtualenv venv
|
||||
$ . venv/bin/activate
|
||||
New python executable in venv/bin/python
|
||||
Installing distribute............done.
|
||||
Installing setuptools, pip............done.
|
||||
$ pip install Flask==dev
|
||||
...
|
||||
Finished processing dependencies for Flask==dev
|
||||
|
||||
.. _windows-easy-install:
|
||||
|
||||
`pip` and `distribute` on Windows
|
||||
-----------------------------------
|
||||
`pip` and `setuptools` on Windows
|
||||
---------------------------------
|
||||
|
||||
On Windows, installation of `easy_install` is a little bit trickier, but still
|
||||
quite easy. The easiest way to do it is to download the
|
||||
`distribute_setup.py`_ file and run it. The easiest way to run the file is to
|
||||
open your downloads folder and double-click on the file.
|
||||
Sometimes getting the standard "Python packaging tools" like *pip*, *setuptools*
|
||||
and *virtualenv* can be a little trickier, but nothing very hard. The two crucial
|
||||
packages you will need are setuptools and pip - these will let you install
|
||||
anything else (like virtualenv). Fortunately there are two "bootstrap scripts"
|
||||
you can run to install either.
|
||||
|
||||
Next, add the `easy_install` command and other Python scripts to the
|
||||
command search path, by adding your Python installation's Scripts folder
|
||||
to the `PATH` environment variable. To do that, right-click on the
|
||||
"Computer" icon on the Desktop or in the Start menu, and choose "Properties".
|
||||
Then click on "Advanced System settings" (in Windows XP, click on the
|
||||
"Advanced" tab instead). Then click on the "Environment variables" button.
|
||||
Finally, double-click on the "Path" variable in the "System variables" section,
|
||||
and add the path of your Python interpreter's Scripts folder. Be sure to
|
||||
delimit it from existing values with a semicolon. Assuming you are using
|
||||
Python 2.7 on the default path, add the following value::
|
||||
If you don't currently have either, then `get-pip.py` will install both for you
|
||||
(you won't need to run ez_setup.py).
|
||||
|
||||
`get-pip.py`_
|
||||
|
||||
;C:\Python27\Scripts
|
||||
To install the latest setuptools, you can use its bootstrap file:
|
||||
|
||||
And you are done! To check that it worked, open the Command Prompt and execute
|
||||
``easy_install``. If you have User Account Control enabled on Windows Vista or
|
||||
Windows 7, it should prompt you for administrator privileges.
|
||||
`ez_setup.py`_
|
||||
|
||||
Now that you have ``easy_install``, you can use it to install ``pip``::
|
||||
Either should be double-clickable once you download them. If you already have pip,
|
||||
you can upgrade them by running::
|
||||
|
||||
> easy_install pip
|
||||
> pip install --upgrade pip setuptools
|
||||
|
||||
Most often, once you pull up a command prompt you want to be able to type ``pip``
|
||||
and ``python`` which will run those things, but this might not automatically happen
|
||||
on Windows, because it doesn't know where those executables are (give either a try!).
|
||||
|
||||
.. _distribute_setup.py: http://python-distribute.org/distribute_setup.py
|
||||
To fix this, you should be able to navigate to your Python install directory
|
||||
(e.g ``C:\Python27``), then go to ``Tools``, then ``Scripts``; then find the
|
||||
``win_add2path.py`` file and run that. Open a **new** Command Prompt and
|
||||
check that you can now just type ``python`` to bring up the interpreter.
|
||||
|
||||
Finally, to install `virtualenv`_, you can simply run::
|
||||
|
||||
> pip install virtualenv
|
||||
|
||||
Then you can be off on your way following the installation instructions above.
|
||||
|
||||
.. _get-pip.py: https://raw.github.com/pypa/pip/master/contrib/get-pip.py
|
||||
.. _ez_setup.py: https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py
|
||||
|
|
|
|||
|
|
@ -185,6 +185,8 @@ you basically only need the engine::
|
|||
Then you can either declare the tables in your code like in the examples
|
||||
above, or automatically load them::
|
||||
|
||||
from sqlalchemy import Table
|
||||
|
||||
users = Table('users', metadata, autoload=True)
|
||||
|
||||
To insert data you can use the `insert` method. We have to get a
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
Unicode in Flask
|
||||
================
|
||||
|
||||
Flask like Jinja2 and Werkzeug is totally Unicode based when it comes to
|
||||
Flask, like Jinja2 and Werkzeug, is totally Unicode based when it comes to
|
||||
text. Not only these libraries, also the majority of web related Python
|
||||
libraries that deal with text. If you don't know Unicode so far, you
|
||||
should probably read `The Absolute Minimum Every Software Developer
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@ drop table if exists entries;
|
|||
create table entries (
|
||||
id integer primary key autoincrement,
|
||||
title text not null,
|
||||
text text not null
|
||||
'text' text not null
|
||||
);
|
||||
|
|
|
|||
12
flask/app.py
12
flask/app.py
|
|
@ -305,6 +305,7 @@ class Flask(_PackageBoundObject):
|
|||
'JSON_AS_ASCII': True,
|
||||
'JSON_SORT_KEYS': True,
|
||||
'JSONIFY_PRETTYPRINT_REGULAR': True,
|
||||
'TEMPLATES_AUTO_RELOAD': True,
|
||||
})
|
||||
|
||||
#: The rule object to use for URL rules created. This is used by
|
||||
|
|
@ -655,10 +656,16 @@ class Flask(_PackageBoundObject):
|
|||
this function to customize the behavior.
|
||||
|
||||
.. versionadded:: 0.5
|
||||
.. versionchanged:: 1.0
|
||||
``Environment.auto_reload`` set in accordance with
|
||||
``TEMPLATES_AUTO_RELOAD`` configuration option.
|
||||
"""
|
||||
options = dict(self.jinja_options)
|
||||
if 'autoescape' not in options:
|
||||
options['autoescape'] = self.select_jinja_autoescape
|
||||
if 'auto_reload' not in options:
|
||||
options['auto_reload'] = self.debug \
|
||||
or self.config['TEMPLATES_AUTO_RELOAD']
|
||||
rv = Environment(self, **options)
|
||||
rv.globals.update(
|
||||
url_for=url_for,
|
||||
|
|
@ -1070,6 +1077,11 @@ class Flask(_PackageBoundObject):
|
|||
The first `None` refers to the active blueprint. If the error
|
||||
handler should be application wide `None` shall be used.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
Use :meth:`register_error_handler` instead of modifying
|
||||
:attr:`error_handler_spec` directly, for application wide error
|
||||
handlers.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
One can now additionally also register custom exception types
|
||||
that do not necessarily have to be a subclass of the
|
||||
|
|
|
|||
|
|
@ -399,3 +399,14 @@ class Blueprint(_PackageBoundObject):
|
|||
self.name, code_or_exception, f))
|
||||
return f
|
||||
return decorator
|
||||
|
||||
def register_error_handler(self, code_or_exception, f):
|
||||
"""Non-decorator version of the :meth:`errorhandler` error attach
|
||||
function, akin to the :meth:`~flask.Flask.register_error_handler`
|
||||
application-wide function of the :class:`~flask.Flask` object but
|
||||
for error handlers limited to this blueprint.
|
||||
|
||||
.. versionadded:: 0.11
|
||||
"""
|
||||
self.record_once(lambda s: s.app._register_error_handler(
|
||||
self.name, code_or_exception, f))
|
||||
|
|
|
|||
|
|
@ -199,16 +199,16 @@ def url_for(endpoint, **values):
|
|||
For more information, head over to the :ref:`Quickstart <url-building>`.
|
||||
|
||||
To integrate applications, :class:`Flask` has a hook to intercept URL build
|
||||
errors through :attr:`Flask.build_error_handler`. The `url_for` function
|
||||
results in a :exc:`~werkzeug.routing.BuildError` when the current app does
|
||||
not have a URL for the given endpoint and values. When it does, the
|
||||
:data:`~flask.current_app` calls its :attr:`~Flask.build_error_handler` if
|
||||
errors through :attr:`Flask.url_build_error_handlers`. The `url_for`
|
||||
function results in a :exc:`~werkzeug.routing.BuildError` when the current
|
||||
app does not have a URL for the given endpoint and values. When it does, the
|
||||
:data:`~flask.current_app` calls its :attr:`~Flask.url_build_error_handlers` if
|
||||
it is not `None`, which can return a string to use as the result of
|
||||
`url_for` (instead of `url_for`'s default to raise the
|
||||
:exc:`~werkzeug.routing.BuildError` exception) or re-raise the exception.
|
||||
An example::
|
||||
|
||||
def external_url_handler(error, endpoint, **values):
|
||||
def external_url_handler(error, endpoint, values):
|
||||
"Looks up an external URL when `url_for` cannot build a URL."
|
||||
# This is an example of hooking the build_error_handler.
|
||||
# Here, lookup_url is some utility function you've built
|
||||
|
|
@ -225,10 +225,10 @@ def url_for(endpoint, **values):
|
|||
# url_for will use this result, instead of raising BuildError.
|
||||
return url
|
||||
|
||||
app.build_error_handler = external_url_handler
|
||||
app.url_build_error_handlers.append(external_url_handler)
|
||||
|
||||
Here, `error` is the instance of :exc:`~werkzeug.routing.BuildError`, and
|
||||
`endpoint` and `**values` are the arguments passed into `url_for`. Note
|
||||
`endpoint` and `values` are the arguments passed into `url_for`. Note
|
||||
that this is for building URLs outside the current application, and not for
|
||||
handling 404 NotFound errors.
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ except ImportError:
|
|||
from itsdangerous import json as _json
|
||||
|
||||
|
||||
# figure out if simplejson escapes slashes. This behavior was changed
|
||||
# Figure out if simplejson escapes slashes. This behavior was changed
|
||||
# from one version to another without reason.
|
||||
_slash_escape = '\\/' not in _json.dumps('/')
|
||||
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ class SessionInterface(object):
|
|||
|
||||
def get_cookie_path(self, app):
|
||||
"""Returns the path for which the cookie should be valid. The
|
||||
default implementation uses the value from the SESSION_COOKIE_PATH``
|
||||
default implementation uses the value from the ``SESSION_COOKIE_PATH``
|
||||
config var if it's set, and falls back to ``APPLICATION_ROOT`` or
|
||||
uses ``/`` if it's `None`.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -37,12 +37,12 @@ except ImportError:
|
|||
temporarily_connected_to = connected_to = _fail
|
||||
del _fail
|
||||
|
||||
# the namespace for code signals. If you are not flask code, do
|
||||
# The namespace for code signals. If you are not flask code, do
|
||||
# not put signals in here. Create your own namespace instead.
|
||||
_signals = Namespace()
|
||||
|
||||
|
||||
# core signals. For usage examples grep the sourcecode or consult
|
||||
# Core signals. For usage examples grep the sourcecode or consult
|
||||
# the API documentation in docs/api.rst as well as docs/signals.rst
|
||||
template_rendered = _signals.signal('template-rendered')
|
||||
request_started = _signals.signal('request-started')
|
||||
|
|
|
|||
|
|
@ -296,6 +296,39 @@ class BlueprintTestCase(FlaskTestCase):
|
|||
self.assert_equal(c.get('/backend-no').data, b'backend says no')
|
||||
self.assert_equal(c.get('/what-is-a-sideend').data, b'application itself says no')
|
||||
|
||||
def test_blueprint_specific_user_error_handling(self):
|
||||
class MyDecoratorException(Exception):
|
||||
pass
|
||||
class MyFunctionException(Exception):
|
||||
pass
|
||||
|
||||
blue = flask.Blueprint('blue', __name__)
|
||||
|
||||
@blue.errorhandler(MyDecoratorException)
|
||||
def my_decorator_exception_handler(e):
|
||||
self.assert_true(isinstance(e, MyDecoratorException))
|
||||
return 'boom'
|
||||
|
||||
def my_function_exception_handler(e):
|
||||
self.assert_true(isinstance(e, MyFunctionException))
|
||||
return 'bam'
|
||||
blue.register_error_handler(MyFunctionException, my_function_exception_handler)
|
||||
|
||||
@blue.route('/decorator')
|
||||
def blue_deco_test():
|
||||
raise MyDecoratorException()
|
||||
@blue.route('/function')
|
||||
def blue_func_test():
|
||||
raise MyFunctionException()
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
app.register_blueprint(blue)
|
||||
|
||||
c = app.test_client()
|
||||
|
||||
self.assert_equal(c.get('/decorator').data, b'boom')
|
||||
self.assert_equal(c.get('/function').data, b'bam')
|
||||
|
||||
def test_blueprint_url_definitions(self):
|
||||
bp = flask.Blueprint('test', __name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -125,7 +125,9 @@ class ExtImportHookTestCase(FlaskTestCase):
|
|||
next = tb.tb_next.tb_next
|
||||
if not PY2:
|
||||
next = next.tb_next
|
||||
self.assert_in('flask_broken/__init__.py', next.tb_frame.f_code.co_filename)
|
||||
|
||||
import os.path
|
||||
self.assert_in(os.path.join('flask_broken', '__init__.py'), next.tb_frame.f_code.co_filename)
|
||||
|
||||
|
||||
def suite():
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ class SendfileTestCase(FlaskTestCase):
|
|||
app = flask.Flask(__name__)
|
||||
with catch_warnings() as captured:
|
||||
with app.test_request_context():
|
||||
f = open(os.path.join(app.root_path, 'static/index.html'))
|
||||
f = open(os.path.join(app.root_path, 'static/index.html'), mode='rb')
|
||||
rv = flask.send_file(f)
|
||||
rv.direct_passthrough = False
|
||||
with app.open_resource('static/index.html') as f:
|
||||
|
|
|
|||
|
|
@ -295,6 +295,14 @@ class TemplatingTestCase(FlaskTestCase):
|
|||
rv = app.test_client().get('/')
|
||||
self.assert_equal(rv.data, b'<h1>Jameson</h1>')
|
||||
|
||||
def test_templates_auto_reload(self):
|
||||
app = flask.Flask(__name__)
|
||||
self.assert_true(app.config['TEMPLATES_AUTO_RELOAD'])
|
||||
self.assert_true(app.jinja_env.auto_reload)
|
||||
app = flask.Flask(__name__)
|
||||
app.config['TEMPLATES_AUTO_RELOAD'] = False
|
||||
self.assert_false(app.jinja_env.auto_reload)
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ class View(object):
|
|||
#: view function is created the result is automatically decorated.
|
||||
#:
|
||||
#: .. versionadded:: 0.8
|
||||
decorators = []
|
||||
decorators = ()
|
||||
|
||||
def dispatch_request(self):
|
||||
"""Subclasses have to override this method to implement the
|
||||
|
|
@ -89,7 +89,7 @@ class View(object):
|
|||
for decorator in cls.decorators:
|
||||
view = decorator(view)
|
||||
|
||||
# we attach the view class to the view function for two reasons:
|
||||
# We attach the view class to the view function for two reasons:
|
||||
# first of all it allows us to easily figure out what class-based
|
||||
# view this thing came from, secondly it's also used for instantiating
|
||||
# the view class so you can actually replace it with something else
|
||||
|
|
@ -111,7 +111,7 @@ class MethodViewType(type):
|
|||
for key in d:
|
||||
if key in http_method_funcs:
|
||||
methods.add(key.upper())
|
||||
# if we have no method at all in there we don't want to
|
||||
# If we have no method at all in there we don't want to
|
||||
# add a method list. (This is for instance the case for
|
||||
# the baseclass or another subclass of a base method view
|
||||
# that does not introduce new methods).
|
||||
|
|
@ -141,8 +141,8 @@ class MethodView(with_metaclass(MethodViewType, View)):
|
|||
"""
|
||||
def dispatch_request(self, *args, **kwargs):
|
||||
meth = getattr(self, request.method.lower(), None)
|
||||
# if the request method is HEAD and we don't have a handler for it
|
||||
# retry with GET
|
||||
# If the request method is HEAD and we don't have a handler for it
|
||||
# retry with GET.
|
||||
if meth is None and request.method == 'HEAD':
|
||||
meth = getattr(self, 'get', None)
|
||||
assert meth is not None, 'Unimplemented method %r' % request.method
|
||||
|
|
|
|||
|
|
@ -40,25 +40,25 @@ class Request(RequestBase):
|
|||
specific ones.
|
||||
"""
|
||||
|
||||
#: the internal URL rule that matched the request. This can be
|
||||
#: The internal URL rule that matched the request. This can be
|
||||
#: useful to inspect which methods are allowed for the URL from
|
||||
#: a before/after handler (``request.url_rule.methods``) etc.
|
||||
#:
|
||||
#: .. versionadded:: 0.6
|
||||
url_rule = None
|
||||
|
||||
#: a dict of view arguments that matched the request. If an exception
|
||||
#: A dict of view arguments that matched the request. If an exception
|
||||
#: happened when matching, this will be `None`.
|
||||
view_args = None
|
||||
|
||||
#: if matching the URL failed, this is the exception that will be
|
||||
#: If matching the URL failed, this is the exception that will be
|
||||
#: raised / was raised as part of the request handling. This is
|
||||
#: usually a :exc:`~werkzeug.exceptions.NotFound` exception or
|
||||
#: something similar.
|
||||
routing_exception = None
|
||||
|
||||
# switched by the request context until 1.0 to opt in deprecated
|
||||
# module functionality
|
||||
# Switched by the request context until 1.0 to opt in deprecated
|
||||
# module functionality.
|
||||
_is_old_module = False
|
||||
|
||||
@property
|
||||
|
|
@ -179,7 +179,7 @@ class Request(RequestBase):
|
|||
def _load_form_data(self):
|
||||
RequestBase._load_form_data(self)
|
||||
|
||||
# in debug mode we're replacing the files multidict with an ad-hoc
|
||||
# In debug mode we're replacing the files multidict with an ad-hoc
|
||||
# subclass that raises a different error for key errors.
|
||||
ctx = _request_ctx_stack.top
|
||||
if ctx is not None and ctx.app.debug and \
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ def parse_changelog():
|
|||
match = re.search('^Version\s+(.*)', line.strip())
|
||||
if match is None:
|
||||
continue
|
||||
length = len(match.group(1))
|
||||
version = match.group(1).strip()
|
||||
if lineiter.next().count('-') != len(match.group(0)):
|
||||
continue
|
||||
|
|
@ -60,6 +59,7 @@ def parse_date(string):
|
|||
|
||||
def set_filename_version(filename, version_number, pattern):
|
||||
changed = []
|
||||
|
||||
def inject_version(match):
|
||||
before, old, after = match.groups()
|
||||
changed.append(True)
|
||||
|
|
@ -133,7 +133,8 @@ def main():
|
|||
if version in tags:
|
||||
fail('Version "%s" is already tagged', version)
|
||||
if release_date.date() != date.today():
|
||||
fail('Release date is not today (%s != %s)')
|
||||
fail('Release date is not today (%s != %s)',
|
||||
release_date.date(), date.today())
|
||||
|
||||
if not git_is_clean():
|
||||
fail('You have uncommitted changes in git')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue