Commit e364662e authored by Petrus J.v.Rensburg's avatar Petrus J.v.Rensburg

Update 'Adding a new model backend' page.

parent 0c3e9406
Adding new model backend
========================
Adding a new model backend
==========================
If you want to implement new database backend to use with model views, follow steps found in this guideline.
Flask-Admin makes a few assumptions about the database models that it works with. If you want to implement your own
database backend, and still have Flask-Admin's model views work as expected, then you should take note of the following:
There are few assumptions about models:
1. Each model must have one field which acts as a `primary key` to uniquely identify instances of that model.
However, there are no restriction on the data type or the field name of the `primary key` field.
2. Models must make their data accessible as python properties.
1. Model has "primary key" - value which uniquely identifies
one model in a data store. There's no restriction on the
data type or field name.
2. Model has readable python properties
3. It is possible to get list of models (optionally - sorted,
filtered, etc) from data store
4. It is possible to get one model by its primary key
If that is the case, then you can implement your own database backend by extending the `BaseModelView` class,
and implementing the set of scaffolding methods listed below.
Extending BaseModelView
-------------------------
Steps to add new model backend:
1. Create new class and derive it from :class:`~flask.ext.admin.model.BaseModelView`::
Start off by defining a new class, which derives from from :class:`~flask.ext.admin.model.BaseModelView`::
class MyDbModel(BaseModelView):
pass
By default, all model views accept model class and it
will be stored as ``self.model``.
2. Implement following scaffolding methods:
- :meth:`~flask.ext.admin.model.BaseModelView.get_pk_value`
This method will return primary key value from
the model. For example, in SQLAlchemy backend,
it gets primary key from the model using :meth:`~flask.ext.admin.contrib.sqla.ModelView.scaffold_pk`, caches it
and returns actual value from the model when requested.
For example::
class MyDbModel(BaseModelView):
def get_pk_value(self, model):
return self.model.id
- :meth:`~flask.ext.admin.model.BaseModelView.scaffold_list_columns`
Returns list of columns to be displayed in a list view.
For example::
class MyDbModel(BaseModelView):
def scaffold_list_columns(self):
columns = []
for p in dir(self.model):
attr = getattr(self.model)
if isinstance(attr, MyDbColumn):
columns.append(p)
This class inherits BaseModelView's `__init__` method, which accepts a model class as first argument. The model
class is stored as the attribute ``self.model`` so that other methods may access it.
return columns
Now, implement the following scaffolding methods for the new class:
- :meth:`~flask.ext.admin.model.BaseModelView.scaffold_sortable_columns`
1. :meth:`~flask.ext.admin.model.BaseModelView.get_pk_value`
Returns dictionary of sortable columns. Key in a dictionary is field name. Value - implementation
specific, value that will be used by you backend implementation to do actual sort operation.
This method returns a primary key value from
the model instance. In the SQLAlchemy backend, it gets the primary key from the model
using :meth:`~flask.ext.admin.contrib.sqla.ModelView.scaffold_pk`, caches it
and then returns the value from the model whenever requested.
For example, in SQLAlchemy backend it is possible to
sort by foreign key. If there's a field `user` and
it is foreign key for a `Users` table which has a name
field, key will be `user` and value will be `Users.name`.
If your backend does not support sorting, return
`None` or empty dictionary.
- :meth:`~flask.ext.admin.model.BaseModelView.init_search`
Initialize search functionality. If your backend supports
full-text search, do initializations and return `True`.
If your backend does not support full-text search, return
`False`.
For example, SQLAlchemy backend reads value of the `self.searchable_columns` and verifies if all fields are of
text type, if they're local to the current model (if not,
it will add a join, etc) and caches this information for
future use.
- :meth:`~flask.ext.admin.model.BaseModelView.is_valid_filter`
Verify if provided object is a valid filter.
Each model backend should have its own set of
filter implementations. It is not possible to use
filters from SQLAlchemy models in non-SQLAlchemy backend.
This also means that different backends might have
different set of available filters.
Filter is a class derived from :class:`~flask.ext.admin.model.filters.BaseFilter` which implements at least two methods:
1. :meth:`~flask.ext.admin.model.filters.BaseFilter.apply`
2. :meth:`~flask.ext.admin.model.filters.BaseFilter.operation`
`apply` method accepts two parameters: `query` object and a value from the client. Here you will add
filtering logic for this filter type.
Lets take SQLAlchemy model backend as an example.
All SQLAlchemy filters derive from :class:`~flask.ext.admin.contrib.sqla.filters.BaseSQLAFilter` class.
Each filter implements one simple filter SQL operation
(like, not like, greater, etc) and accepts column as
input parameter.
Whenever model view wants to apply a filter to a query
object, it will call `apply` method in a filter class
with a query and value. Filter will then apply
real filter operation.
For example::
For example::
class MyBaseFilter(BaseFilter):
def __init__(self, column, name, options=None, data_type=None):
super(MyBaseFilter, self).__init__(name, options, data_type)
class MyDbModel(BaseModelView):
def get_pk_value(self, model):
return self.model.id
self.column = column
2. :meth:`~flask.ext.admin.model.BaseModelView.scaffold_list_columns`
class MyEqualFilter(MyBaseFilter):
def apply(self, query, value):
return query.filter(self.column == value)
Returns a list of columns to be displayed in a list view. For example::
def operation(self):
return gettext('equals')
class MyDbModel(BaseModelView):
def scaffold_list_columns(self):
columns = []
# You can validate values. If value is not valid,
# return `False`, so filter will be ignored.
def validate(self, value):
return True
for p in dir(self.model):
attr = getattr(self.model)
if isinstance(attr, MyDbColumn):
columns.append(p)
# You can "clean" values before they will be
# passed to the your data access layer
def clean(self, value):
return value
return columns
- :meth:`~flask.ext.admin.model.BaseModelView.scaffold_filters`
3. :meth:`~flask.ext.admin.model.BaseModelView.scaffold_sortable_columns`
Return list of filter objects for one model field.
Returns a dictionary of sortable columns. The keys in the dictionary should correspond to the model's
field names. The values should be those variables that will be used for sorting.
This method will be called once for each entry in the
`self.column_filters` setting.
For example, in the SQLAlchemy backend it is possible to sort by a foreign key field. So, if there is a
field named `user`, which is a foreign key for the `Users` table, and the `Users` table also has a name
field, then the key will be `user` and value will be `Users.name`.
If your backend does not know how to generate filters
for the provided field, it should return `None`.
If your backend does not support sorting, return
`None` or an empty dictionary.
For example::
4. :meth:`~flask.ext.admin.model.BaseModelView.init_search`
class MyDbModel(BaseModelView):
def scaffold_filters(self, name):
attr = getattr(self.model, name)
Initialize search functionality. If your backend supports
full-text search, do initializations and return `True`.
If your backend does not support full-text search, return
`False`.
if isinstance(attr, MyDbTextField):
return [MyEqualFilter(name, name)]
For example, SQLAlchemy backend reads value of the `self.searchable_columns` and verifies if all fields are of
text type, if they're local to the current model (if not,
it will add a join, etc) and caches this information for
future use.
- :meth:`~flask.ext.admin.model.BaseModelView.scaffold_form`
5. :meth:`~flask.ext.admin.model.BaseModelView.scaffold_form`
Generate `WTForms` form class from the model.
......@@ -169,9 +90,9 @@ Steps to add new model backend:
# Do something
return MyForm
- :meth:`~flask.ext.admin.model.BaseModelView.get_list`
6. :meth:`~flask.ext.admin.model.BaseModelView.get_list`
This method should return list of models with paging,
This method should return list of model instances with paging,
sorting, etc applied.
For SQLAlchemy backend it looks like:
......@@ -199,22 +120,97 @@ Steps to add new model backend:
6. Return count, list as a tuple
- :meth:`~flask.ext.admin.model.BaseModelView.get_one`
7. :meth:`~flask.ext.admin.model.BaseModelView.get_one`
Return a model instance by its primary key.
8. :meth:`~flask.ext.admin.model.BaseModelView.create_model`
Create a new instance of the model from the `Form` object.
9. :meth:`~flask.ext.admin.model.BaseModelView.update_model`
Update the model instance with data from the form.
Return one model by its primary key.
10. :meth:`~flask.ext.admin.model.BaseModelView.delete_model`
- :meth:`~flask.ext.admin.model.BaseModelView.create_model`
Delete the specified model instance from the data store.
Create new model from the `Form` object.
11. :meth:`~flask.ext.admin.model.BaseModelView.is_valid_filter`
- :meth:`~flask.ext.admin.model.BaseModelView.update_model`
Verify whether the given object is a valid filter.
Update provided model with the data from the form.
12. :meth:`~flask.ext.admin.model.BaseModelView.scaffold_filters`
Return a list of filter objects for one model field.
This method will be called once for each entry in the
`self.column_filters` setting.
If your backend does not know how to generate filters
for the provided field, it should return `None`.
For example::
class MyDbModel(BaseModelView):
def scaffold_filters(self, name):
attr = getattr(self.model, name)
- :meth:`~flask.ext.admin.model.BaseModelView.delete_model`
if isinstance(attr, MyDbTextField):
return [MyEqualFilter(name, name)]
Implementing filters
--------------------
Each model backend should have its own set of filter implementations. It is not possible to use the
filters from SQLAlchemy models in a non-SQLAlchemy backend.
This also means that different backends might have different set of available filters.
The filter is a class derived from :class:`~flask.ext.admin.model.filters.BaseFilter` which implements at least two methods:
1. :meth:`~flask.ext.admin.model.filters.BaseFilter.apply`
2. :meth:`~flask.ext.admin.model.filters.BaseFilter.operation`
`apply` method accepts two parameters: `query` object and a value from the client. Here you can add
filtering logic for the filter type.
Lets take SQLAlchemy model backend as an example:
All SQLAlchemy filters derive from :class:`~flask.ext.admin.contrib.sqla.filters.BaseSQLAFilter` class.
Each filter implements one simple filter SQL operation (like, not like, greater, etc) and accepts a column as
input parameter.
Whenever model view wants to apply a filter to a query
object, it will call `apply` method in a filter class
with a query and value. Filter will then apply
real filter operation.
For example::
class MyBaseFilter(BaseFilter):
def __init__(self, column, name, options=None, data_type=None):
super(MyBaseFilter, self).__init__(name, options, data_type)
self.column = column
class MyEqualFilter(MyBaseFilter):
def apply(self, query, value):
return query.filter(self.column == value)
def operation(self):
return gettext('equals')
# You can validate values. If value is not valid,
# return `False`, so filter will be ignored.
def validate(self, value):
return True
# You can "clean" values before they will be
# passed to the your data access layer
def clean(self, value):
return value
Delete provided model from the data store.
Feel free ask questions if you have problem adding new model backend.
Also, it is good idea to take a look on SQLAlchemy model backend to
see how it works in different circumstances.
Feel free ask questions if you have problems adding a new model backend.
Also, if you get stuck, try taking a look at the SQLAlchemy model backend and use it as a reference.
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment