pyramid_crud - CRUD pages for Pyramid¶
This software is a framework with the attempt to replicate a behavior similar to Django’s Generic Views and Admin Pages.
It aims to provide a simple yet configurable interface to get a CRUD (Create, Read, Update, Delete) interface on persisted data.
This library is an unofficial extension to Pyramid. This is not likely to change unless the libraries dependencies are decoupled as described in A Word on Dependencies.
Note
This library is in an early phase and contributions are welcome that fix bugs or add missing features. Just please make sure to keep it as clean as possible. Also always take a look at how Django achieves the desired functionality (if present), because they have some good ideas on keeping the code clean and readable.
Table of Contents¶
Introduction¶
Installation¶
You can install pyramid_crud
using pip:
pip install pyramid_crud
Or you can fetch the current sources and install it manually:
git clone https://github.com/Javex/pyramid_crud
cd pyramid_crud
python setup.py install
A Word on Dependencies¶
This library currently relies on certain other libraries. Therefore, it only supports a certain use-case (or at least only integrates well there). Thus, if your application stack differs from the libraries listed below, make sure to read this section to see which parts can be changed and which cannot.
The Mako integration is very loose, allowing for arbitrary templates to be used as long as they are registered properly with Pyramid.
WTForms on the other hand is more tightly integrated. It should be easily possible to write an adapter that replicates the WTForms interface and allows integration with other form libraries but this library was not designed for it. However, I am happy to accept pull requests that change this behavior to allow arbitrary form libraries as long as the code stays clean and the interface does not require major changes. Finally, there is no requirement for you to use WTForms in the rest of your application: You can simply rely on WTForms only for this library. As long as you don’t deviate from the default mechanisms you will not even have to concern yourself with WTForms at all.
SQLAlchemy is also very tightly bound to the library. Both the form and the view part rely on SQLAlchemy and its interface. However, seeing as SQLAlchemy is basically the go-to ORM outside of Django, I don’t see a need except if NoSQL databases are desired.
Pyramid is, of course, at the core of this library and there are currently no plans to decouple it to allow arbitrary frameworks the usage of this library. Again, I accept pull requests for this, but I find it much more likely that a split into a new library that provides this functionality independent of a web framework and separate integration into different frameworks is the way to go if this is desired. If you want to work on something like this, please contact me, so we can coordinate on this.
QuickStart¶
For this quickstart we assume you already have an application with models that you want to enable CRUD for.
First you have to include pyramid_crud
in your .ini
file:
pyramid.includes =
...
pyramid_crud
...
from pyramid_crud.forms import CSRFModelForm
from pyramid_crud.views import CRUDView
from .models import MyModel
class MyModelForm(CSRFModelForm):
class Meta:
model = MyModel
class MyModelView(CRUDView):
form = MyModelForm
url_path = '/mymodel'
That gets you started: We create a form and a set of views for our form. Now
start your application and visit the application on the path /mymodel
. You
should see a list of present instances and also buttons to delete them and add
new instances. Finally, you can also click the first columns element to edit
an item. Go ahead, play around with it. Afterwards, you can head to
Usage and start configuring the associated parts to behave the way you
need it to.
Usage¶
After you have read the QuickStart you can now head into the special configuration. This chapter is split into four sections. Views are the center of configuration. Here you define which form to use, how templates are located and under which route it should be available. Views also needs Forms. These define the associated model and configure which fields are displayed. Finally, if the default templates don’t suit you (e.g. you want to integrate your own style or you don’t use Mako), you can visit the Templates section to see how to change the default templates.
There are also some Examples that show possible applications. You can refer there to see how your goal can be realized in pratice. To change the global behavior of the library refer to Configuration. This is useful if the default global application behavior does not suit you.
Configuration¶
There are several global settings with which you can configure the behavior of
this library. All settings use the prefix crud.
Static View URL Prefix¶
The application needs to serve static assets to display the default templates
properly (specifically, it uses Bootstrap). These assets need their own
prefix to avoid routing conflicts with your other static files. Thus, this
setting allows you to define a custom prefix. By default, it is
/static/crud
which should be fine for most applications (as static
is a
very common name, you can have all your CSS and JS files under this). However,
if this does not fit your use case, use this setting to change it.
If this is None
, no additional static view will be registered. This is
useful if you roll your own theme anyway (see Create a Complete Theme) and you set up
your own static views for it.
Config File Setting Name |
---|
crud.static_url_prefix |
Views¶
Add a New View¶
Configuration¶
The main configuration of the library is done on the view. By subclassing
CRUDView
for each new view you can create an individual configuration
that turns your model & form into a fully accessible CRUD interface. The
available confiugration parameters are described on the class:
-
class
pyramid_crud.views.
CRUDView
(request)[source]¶ The base class for all views. Subclassing directly from this gets you a new view configuration for a single model & form. If you specify
__abstract__
on it, the class will not be configured at all and you can use it as your own base class.Note
Configuration is done by Pyramid the moment you call
pyramid.config.Configurator.scan()
in a way similar to what thepyramid.view.view_config
decorator does. If you want to completely disable this behavior, set view_configurator_class toNone
. Then no route configuration will be done and you have to set up views and routes yourself. This is an advanced technique not recommended for beginners.The following attributes can be defined to override behavior of the view:
- Form
- Mandatory argument that specifies the form class for which this view should be created. This must be a form as described in Forms.
- url_path
Mandatory arguments if the default view_configurator_class is used. It determines the base path under which this view should be available.
So for example, if this is
/myitems
then the list view will be reached under the/myitems
path whereas the new view will be under/myitems/new
.How and if this parameter is used depends entirely on the implementation of the configurator but it is recommended to keep this parameter for custom implementations as well.
- dbsession
- Return the current SQLAlchemy session. By default this
expects a
dbsession
attribute on therequest
object. It is mandatory that you either attach the attribute using an event or override this attribute (you can use aproperty
if you like).
- list_display
A tuple if items which should be displayed on the list view. By default a single column of the models
__str__
method is used. There are several possibilities of what you might specify here (the options will be tried in this order):A string representing an attribute or callable on the model. If this attribute is callable, it will be called and get no additional arguments (the first argument will already be
self
, the model instance).For example, with a normal field on the model:
class Model(Base): id = Column(Integer, primary_key=True, info={'label': 'ID'}) class View(CRUDView): list_display = ('id',)
In this example there will be a single column in the list view. Its title will be “ID” and its value will be the value of the
id
field in the database.Similarly, with a callable:
class Model(Base): id = Column(Integer, primary_key=True) def id_plus_one(self): return self.id + 1 id_plus_one.info = {'label': 'ID+1'} class View(CRUDView): list_display = ('id_plus_one',)
A generic callable function. This function will be called with a single argument: The instance of the model. For example:
class Model(Base): id = Column(Integer, primary_key=True) def id_plus_one(obj): return obj.id + 1 id_plus_one.info = {'label': 'ID+1'} class View(CRUDView): list_display = (id_plus_one,)
A string representing a method on the view. This will behave in the same way as for the function callable above except that it must be a string. For example:
class Model(Base): id = Column(Integer, primary_key=True) class View(CRUDView): list_display = ('id_plus_one',) def id_plus_one(self, obj): return obj.id + 1 id_plus_one.info = {'label': 'ID+1'}
Some additional notes on the way this attribute behaves:
- Some additional configuration is possible on each attribute, regardless of how it is specified. For information on this see The Info Dictionary.
- A class
columnn-<attr-name>
is placed on each on each of the <th> fields in the column heading to allow application of CSS attributes, e.g. to set the width of a column. - If the attribute
info
cannot be found on the attribute (at the class level, not instance level), default value is determined as the column heading. If name of the column is__str__
then the name of the model class is fetched. If it is directly callable (in case of a generic callable function), then the name of the function is used. In all other cases the provided string is used. To make for a prettier format, it additionally replaces any underscores by spaces and captializes each word.
- list_display_links
Specify which of the displayed columns should be turned into links that open the edit view of that instance. By default, the first column is used.
This should be any kind of iterable, preferrably a tuple or set for performance reasons.
Example:
class MyView(CRUDView): list_display = ('column1', 'column2', 'column3') list_display_links = ('column1', 'column3')
This configuration will turn the columns
column1
andcolumn3
into links.
- actions:
- An optional list of action callables or view method names for the dropdown menu. See Adding Actions to Forms for details on how to use it.
- theme
- A theme is just a collection of template files inside a directory and
this is the name of that directory. The recommended way is to use
asset specification to unambigously identify the package. By default
the bootstrap template is used and so this is set to
pyramid_crud:templates/mako/bootstrap
. If you want to roll your own theme, you can overwrite this. But if you only want to copy a single template and modify it, you should check out Templates.
- template_ext
- Which file extension to use for templates. By default,
Mako templates are used and so the extension is
.mako
but any renderer that is recognized by pramid can be used.
- template_*
You can specify any name here, e.g.
template_list
and theCRUDView.get_template_for()
method will use this when calling it withlist
as the action parameter. This is useful for overwriting specific templates but keeping the default behavior for the rest.Note
The name “ext” for an action is thus not allowed (as
template_ext
is another configuration). Just don’t define an action with that name.This way is also impossible for templates in subdirectories, for example
fieldsets/horizontal.mako
since a slash (“/”) cannot be used on a path. Currently the only way is to overwriteCRUDView.get_template_for()
.
- view_configurator_class
- A class that configures all views and routes for this view class. The
default implementation is
ViewConfigurator
which covers basic route & view configuration. However, if you need more advanced functionalities like, for example, permissions, you can change this parameter. See the documentation onViewConfigurator
for details on how to achieve that.
There are also some attributes which you can access. All of them are available on the instance, but only some are also available on the class (in this case, it is noted on the attribute).
- routes
A dictionary mapping action names to routes. Action names are such as
list
oredit
and they all have unique route names that can be given torequest.route_url
. You can use it like this:url = request.route_url(view.routes["list"])
This will return a URL to the list view.
The routes dictionary is populated by the view_configurator_class.
This can be accessed at the class and instance level.
- request
- The current request, an instance of
pyramid.request.Request
.
View & Route Setup¶
Setting up views and routes is delegated to a special configurator class that creates a route & view for each available view, i.e. list, edit, new and delete. Since you often need to change the routes and views to match your needs, you can subclass this and start overwriting its behavior. The interface is very simple:
Note
There is a slight overhead to configuring views like this because it requires the creation of an additional class. However, approaches like configuring parameters directly on the view are inflexible and setting awkward callables (in theory the most pythonic way) look ugly. Thus, this method is both flexible and easy to read.
-
class
pyramid_crud.views.
ViewConfigurator
(config, view_class)[source]¶ The standard implementation of the view configuration. It performs the most basic configuration of routes and views without any extra functionality.
This is sufficient in many cases, but there are several applications where you might want to completely or partially change this behavior. Any time you want to pass additional arguments to
pyramid.config.Configurator.add_route()
orpyramid.config.Configurator.add_view()
you can just subclass this and override the specific methods.All the public methods must always be implemented according to their documentation or the configuration of views and routes will fail. If you are unsure, you can take a look at the default implementation. It is just a very thin wrapper around the above mentioned methods.
During instantiation the arguments
config
representing an instance ofpyramid.config.Configurator
andview_class
being your subclassed view class are given to the instance and stored under these values as its attributes.From the
view_class
parameter you can access the complete configuration as documented onCRUDView
.config
should then be used to add routes and views and possibly other configuration you might need.
-
ViewConfigurator.
configure_list_view
()[source]¶ Configure the “list” view by setting its route and view. This method must call
add_view
to configure the view andadd_route
to connect a route to it. Afterwards, it must return the name of the configured route that links route and view. This will then be stored in the view’sroute
dictionary under the “list” key.def configure_list_view(self): self.config.add_view('myview-list', renderer='list.mako',) self.config.add_route('myview-list', self.view_class.url_path) return 'myview-list'
This does a few things:
- It sets up the view under the alias
myview-list
with the templatelist.mako
. Note that the default configuration uses a theme and absolute paths while this configures a template that needs to be inmako.directories
. - It connects the alias to the configured route via the url_path configuration parameter (the list view is just the base route in this case, but that is totally up to you).
- It returns this alias from the function so that it can be stored in
the
routes
dictionary on the view.
- It sets up the view under the alias
-
ViewConfigurator.
configure_edit_view
()[source]¶ This method behaves exactly like
ViewConfigurator.configure_list_view()
except it must configure the edit view, i.e. the view for editing existing objects. It must return the name of the route as well that will then be stored under the “edit” key.
-
ViewConfigurator.
configure_new_view
()[source]¶ This method behaves exactly like
ViewConfigurator.configure_list_view()
except it must configure the new view, i.e. the view for adding new objects. It must return the name of the route as well that will then be stored under the “new” key.
There are also some helper methods available.
The Info Dictionary¶
Each object can have an optional info dictionary attached (and in most cases you will want one). The idea is based on the idea of WTForms-Alchemy’s Form Customization and actually just extends it. Several attributes used by this library support inclusion of extra information in this dict. The following options can be set and/or read and some are automatically defined if you do not provide a value. The follwoing values are available:
- label
- This is taken over from WTForms-Alchemy but is used in more places. Instead of being just used as the label on a form, it is also used as a column heading in the list view. Each object should have one, but some functions set it (for example, the column header function associated with list_display provides a default). For specific behavior on this regarding different views you should consult the associated documentation. While you should normally set it, not setting it will invent some hopefully nice-looking strings for the default usage (basically list and edit views).
- description
- Used on form fields to describe a field more in-depth than a label can. This text may be arbitrarily long. It is not displayed on all templates (see Fieldset Templates).
- css_class
- A css class which should be set on this element’s context. Currently this
is only used for the list view where the
th
element gets this class so you can style your table based on individual columns. See the documentation on list_display for more info. - bool
- This value is not always set, but when it is set, it indicates if this item is a boolean type. Currently this is only set for the list headings and there it is unused but can be adapted by custom templates.
- func
- This is only used with actions and defines the callable which executes an
action. It is part of the dict returned by
_all_actions
on the view.
API¶
The classes, methods and attributes described here are normally not used directly by the user of the library and are just here for the sake of completeness.
CRUDView
¶
The following methods refer to specific views:
-
CRUDView.
list
()[source]¶ List all items for a Model. This is the default view that can be overridden by subclasses to change its behavior.
Returns: A dict with a single key items
that is a query which when iterating over yields all items to be listed.
-
CRUDView.
edit
()[source]¶ The default view for editing an item. It loads the configured form and model. In edit mode (i.e. with an already existing object) it requires a matchdict mapping primary key names to their values. This has to be ensured during route configuring by setting the correct pattern. The default implementation takes correctly care of this.
Returns: In case of a GET request a dict with the key form
denoting the configured form instance with data from an optional model loaded and a keyis_new
which is a boolean flag indicating whether the actual action isnew
oredit
(allowing for templates to display “New Item” or “Edit Item”).In case of a POST request, either the same dict is returned or an instance of
HTTPFound
which indicates success in saving the item to the database.Raises: ValueError – In case of an invalid, missing or unmatched action. The most likely reason for this is the missing button of a form, e.g. by the name save
. By default the following actions are supported:save
,save_close
,save_new
and additionally anything that starts withadd_
ordelete_
(these two are for internal form handling and inline deletes/adds).
Addtionally, the following helper methods are used internally during several sections of the library:
-
CRUDView.
redirect
(route_name=None, *args, **kw)[source]¶ Convenience function to create a redirect.
Parameters: route_name – The name of the route for which to create a URL. If this is None
, the current route is used.All additional arguments and keyword arguments are passed to
pyramid.request.Request.route_url()
.Returns: An instance of pyramid.httpexceptions.HTTPFound
suitable to be returned from a view to create a redirect.
-
classmethod
CRUDView.
get_template_for
(action)[source]¶ Return the name of the template to be used. By default this uses the template in the folder
theme
with the nameaction + template_ext
, so for example in the default case for a list view: “pyramid_crud:templates/mako/bootstrap/list.mako”.This method basically just appends the given action to a base path and appends the file extension. As such, it is perfectly fine, to define relative paths here:
view.get_template_for('fieldsets/horizontal')
You can also change single templates by statically defining
action_template
on the view class whereaction
is replaced by a specific action, e.g.list
. So say, for example, you want to only change the default list template and keep the others. In that case, you would specifylist_template = "templates/my_crud_list.mako"
and the list template would be loaded from there (while still loading all other templates from their default location).Parameters: action – The action, e.g. list
oredit
.
-
CRUDView.
_get_request_pks
()[source]¶ Get an ordered dictionary of primary key names matching to their value, fetched from the request’s matchdict (not the model!).
Parameters: names – An iterable of names which are to be fetched from the matchdict. Returns: An OrderedDict
of the givennames
as keys with their corresponding value.Raises: ValueError – When only some primary keys are set (it is allowed to have all or none of them set)
-
CRUDView.
_get_route_pks
(obj)[source]¶ Get a dictionary mapping primary key names to values based on the model (contrary to
_get_request_pks()
which bases them on the request).Parameters: obj – An instance of the model. Returns: A dict with primary key names as keys and their values on the object instance as the values.
-
CRUDView.
_edit_route
(obj)[source]¶ Get a route for the edit action based on an objects primary keys.
Parameters: obj – The instance of a model on which the routes values should be based. Returns: A URL which can be used as the routing URL for redirects or displaying the URL on the page.
ViewConfigurator
¶
In addition to the methods described above, the default implementation has a few helper methods. These are not required in any case since they are only called by the above methods. However, since these methods are used to factor out common tedious work, you might either use or override them and possibly not even touch the default implementations above.
-
ViewConfigurator.
_configure_view
(action, route_action=None, *args, **kw)[source]¶ Configure a view via
pyramid.config.Configurator.add_view()
while passing any additional arguments to it.Parameters: - action – The name of the attribute on the view class that
represents the action. For example, in the default
implementation the
list
action corresponds toCRUDView.list()
. If you rename them, e.g. by naming thelist
action “_my_list”, this would have to be_my_list
regardless of the name of the action. - route_action – An optional parameter that is used as the name
base for the route. If this is missing, it will
take the same value as
action
. In the default implementation it is used to distinguish betweennew
andedit
which use the same action, view and template but different route names.
Overriding this method allows you to change the view configuration for all configured views at once, i.e. you don’t have to change the public methods at all. Just look at their default implementation to see the parameters they use.
- action – The name of the attribute on the view class that
represents the action. For example, in the default
implementation the
-
ViewConfigurator.
_configure_route
(action, suffix, *args, **kw)[source]¶ Set up a route via
pyramid.config.Configurator.add_route()
while passing all addtional arguments through to it.Parameters: - action – The action upon which to base the route name. It must
be the same as
route_action
on_configure_view()
. - suffix – The suffix to be used for the actual path. It is
appended to the
url_path
directly. This may be empty (as is the case for the default list view) but must always be explicitly specified. The result of this will be passed toadd_route
and so may (and often will) include parameters such as/{id}
.
Overriding this method can be done in the same manner as described for
_configure_view()
.Warning
Some methods on the view require primary keys of the object in question in the
matchdict
of the request. To guarantee this, the routes have to be correctly set up, i.e. each route that requires this primary key (or keys, depending on the model) has to have a pattern where each primary key name appears once. The default implementation takes care of this via_get_route_pks()
, but if you change things you have to ensure this yourself.Which methods require which values is documented on the respective views of
CRUDView
.- action – The action upon which to base the route name. It must
be the same as
-
ViewConfigurator.
_get_route_name
(action)[source]¶ Get a name for a route of a specific action. The default implementation provides the fully quallyfied name of the view plus the action, e.g.
mypackage.views.MyView.list
(in this case, the action is “list” for the class “MyView” in the module “mypackage.views”).Note
In theory this implementation is ambigous, because you could very well have two classes with the same name in the same module. However, this would be a very awkward implementation and is not recommended anyway. If you really choose to do such a thing, you should probably find a better way of naming your routes.
-
ViewConfigurator.
_get_route_pks
()[source]¶ Get a string representing all primary keys for a route suitable to be given as suffix to
_configure_route()
. Some examples will probably best describe the default behavior.In the case of a model with a single primary key
id
, the result is the very simple string{id}
. If you add this to a route, the primary key of the object will be in thematchdict
under the keyid
.If you have a model with multiple primary keys, say composite foreign keys, called
model1_id
andmodel2_id
then the result would be{model1_id},{model2_id}
. The order is not important on this one as pyramids routing system will fully take care of it.Note
If you have some kind of setup where one of the primary keys may contain a comma, this implementation is likely to fail and you have to change it. However, in most cases you will not have a comma and this should be fine.
Forms¶
Add a New Form¶
Configuration¶
To configure a normal form, you subclass the ModelForm
. On this
subclass there are several options you can/must override. The mandatory options
are listed first, followed by a list of optional configuration parameters.
Finally, you can of course always override the methods on the form.
-
class
pyramid_crud.forms.
ModelForm
(formdata=None, obj=None, *args, **kw)[source]¶ Base-class for all regular forms.
The following configuration options are available on this form in addition to the full behavior described for WTForms-Alchemy
Note
While this class can easily be the base for each form you want to configure, it is strongly recommended to use the
CSRFModelForm
instead. It is almost no different than this form except for a newcsrf_token
field. Thus it should never hurt to subclass it instead of this form.- Meta
- This is the only mandatory argument. It is directly taken over from WTForms-Alchemy so you should check out their documentation on this class as it will provide you with a complete overview of what’s possible here.
- inlines
- A list of forms to use as inline forms. See Inline Forms / One-To-Many.
- fieldsets
Optionally define fieldsets to group your form into categories. It requires a list of dictionaries and in each dictionary, the following attributes can/must be set:
title
: A title to use for the fieldset. This is required but may be the empty string (then no title is displayed).fields
: A list of field names that should be displayed together in a fieldset. This is required.template
: The name of the fieldset template to load. This must be the name of a file in thefieldsets
directory of the current theme without a file extension. It defaults tohorizontal
which uses bootstraps horizontal forms for each fieldset. See Fieldset Templates for details on available templates.
- title
- Set the title of your form. By default this returns the class name of the model. It is used in different places such as the title of the page.
- title_plural:
- The plural title. By default it is the title with an “s” appended, however, you somtimes might want to override it because “Childs” just looks stupid ;-)
- name:
- The name of this form. By default it uses the lowercase model class name. This is used internally und you normally do not need to change it.
- get_dbsession:
- Unfortunately, you have to define this
classmethod
on the form to get support for the unique validator. It is documented in Unique Validator. This is a limitation we soon hope to overcome.
-
fieldsets
¶ See inline documentation for ModelForm
-
get_fieldsets
()¶ Get a list of all configured fieldsets, setting defaults where they are missing.
-
populate_obj
(obj)[source]¶ Populates the attributes of the passed obj with data from the form’s fields.
Note: This is a destructive operation; Any attribute with the same name as a field will be overridden. Use with caution.
-
populate_obj_inline
(obj)[source]¶ Populate all inline objects. It takes the usual
obj
argument that is the parent of the inline fields. From these all other values are derived and finally the objects are updated.Note
Right now this assumes the relationship operation is a
append
, thus for example set collections won’t work right now.
-
primary_keys
¶ Get a list of pairs
name, value
of primary key names and their values on the current object.
-
process
(formdata=None, obj=None, **kwargs)[source]¶ Take form, object data, and keyword arg input and have the fields process them.
Parameters: - formdata – Used to pass data coming from the enduser, usually request.POST or equivalent.
- obj – If formdata is empty or not provided, this object is checked for attributes matching form field names, which will be used for field values.
- data – If provided, must be a dictionary of data. This is only used if formdata is empty or not provided and obj does not contain an attribute named the same as the field.
- **kwargs – If formdata is empty or not provided and obj does not contain an attribute named the same as a field, form will assign the value of a matching keyword argument to the field, if one exists.
-
process_inline
(formdata=None, obj=None, **kwargs)[source]¶ Process all inline fields. This sets the global attribute
inline_fields
which is a dict-like object that contains as keys the name of all defined inline fields and as values a pair ofinline, inline_forms
whereinline
is the inline which the name refers to andinline_forms
is the list of form instances associated with this inline.
-
validate
()[source]¶ Validates the form by calling validate on each field, passing any extra Form.validate_<fieldname> validators to the field validator.
-
validate_inline
()[source]¶ Validate all inline forms. Implicitly called by
validate()
.This will also fill the
form.errors
dict with additional error messages based on invalid inline fields using the same naming pattern used for naming inline fields for display and form submission, i.e.inlinename_index_fieldname
.Thus, if errors exist on an inline field, they can be fetched from the global errors dict the same way regular errors are present in it.
Fieldset Templates¶
You can configure custom fieldset templates on the fieldsets configuration parameter by setting the “template” key for a fieldset. The following fieldsets are available:
- horizontal
- A typical horizontal display that renders each form field in its own row with a label before the field.
- grid
- A grid display that renders the field first and then displays the label. All fields are next to each other and line breaks only happen at the edge of the screen. This is a good template for a fieldset that consists only of checkboxes. This will not display the “description” of a field.
Inline Forms / One-To-Many¶
-
class
pyramid_crud.forms.
BaseInLine
(formdata=None, obj=None, *args, **kw)[source]¶ Base-class for all inline forms. You normally don’t subclass from this directly unless you want to create a new inline type. However, all inline types share the attributes inherited by this template.
Inline forms are forms that are not intended to be displayed by themselves but instead are added to the inlines attribute of a normal form. They will then be displayed inside the normal form while editing, allowing for multiple instance to be added, deleted or modified at the same time. They are heavily inspired by Django’s inline forms.
An inline form is configurable with the following attributes, additionally to any attribute provided by WTForms-Alchemy
- Meta
- This is the standard WTForms-Alchemy attribute to configure the model. Check out their documentation for specific details.
- relationship_name
The name of the other side of the relationship. Determined automatically, unless there are multiple relationships between the models in which case this must be overridden by the subclass.
For example: If this is the child form to be inlined, the other side might be called
children
and this might be calledparent
(or it might not even exist, there is no need for a bidrectional relationship). The correct value would then bechildren
notparent
.- extra
- How many empty fields to display in which new objects can be added. Pay
attention that often fields require intputs and thus extra field may
often not be left empty. This is an intentional restriction to allow
client-side validation without javascript. So only specify this if you
are sure that items will always be added (note, however, that the extra
attribute is not used to enforce a minimum number of members in the
database). Defaults to
0
. - is_extra
- A boolean indicating whether this instance is an extra field or a persisted database field. Set during parent’s processing.
-
fieldsets
¶ See inline documentation for ModelForm
-
get_fieldsets
()¶ Get a list of all configured fieldsets, setting defaults where they are missing.
-
classmethod
pks_from_formdata
(formdata, index)[source]¶ Get a list of primary key values in the order of the primary keys on the model. The returned value is suitable to be passed to
sqlalchemy.orm.query.Query.get()
.Parameters: - formdata – A
webob.multidict.MultiDict
that contains all parameters that were passed to the form. - index (int) – The index of the element for which the primary key is
desired. From this, the correct field name to get from
fromdata
is determined.
Returns: A tuple of primary keys that uniquely identify the object in the database. The order is based on the order of primary keys in the table as reported by SQLAlchemy.
Return type: - formdata – A
-
populate_obj
(obj)¶ Populates the attributes of the passed obj with data from the form’s fields.
Note: This is a destructive operation; Any attribute with the same name as a field will be overridden. Use with caution.
-
primary_keys
¶ Get a list of pairs
name, value
of primary key names and their values on the current object.
-
process
(formdata=None, obj=None, data=None, **kwargs)¶ Take form, object data, and keyword arg input and have the fields process them.
Parameters: - formdata – Used to pass data coming from the enduser, usually request.POST or equivalent.
- obj – If formdata is empty or not provided, this object is checked for attributes matching form field names, which will be used for field values.
- data – If provided, must be a dictionary of data. This is only used if formdata is empty or not provided and obj does not contain an attribute named the same as the field.
- **kwargs – If formdata is empty or not provided and obj does not contain an attribute named the same as a field, form will assign the value of a matching keyword argument to the field, if one exists.
-
validate
()¶ Validates the form by calling validate on each field, passing any extra Form.validate_<fieldname> validators to the field validator.
-
class
pyramid_crud.forms.
TabularInLine
(formdata=None, obj=None, *args, **kw)[source]¶ A base class for a tabular inline display. Each row is displayed in a table row with the field labels being displayed in the table head. This is basically a list view of the fields only that you can edit and delete them and even insert new ones.
Many-To-One & One-To-One¶
The opposite of the One-To-Many pattern is the Many-To-One. One-To-One looks the same from the “One” side, just that the “parent” does not have many “children” but one “child”.
Both relationships are possible without any further configuration. They are automatically detected and work right away. Currently, this feature only has limited use as you cannot directly create a parent form here. Instead the parent object has to exist. Then you can go back and select it in the child’s edit form.
Note
When using a model with a child as an inline, this automatic detection will not display the parent item in the inline form.
Extra Forms¶
CSRF¶
The CSRF Forms are special forms to protect you against CSRF attacks. There are
two different types: The CSRFForm
is the base for any form that
wants to enable CSRF and is not limited to the usage within the scope of this
library (it is not integrated with the rest of the system, it only implements
a WTForms form that takes a pyramid.request.Request
as the
csrf_context
). The CSRFModelForm
on the other hand is
integrated with the rest of the library and should be used to protect a form
against CSRF attacks while still maintaining the complete functionality of the
ModelForm
.
-
class
pyramid_crud.forms.
CSRFForm
(formdata=None, obj=None, prefix=u'', csrf_context=None, **kwargs)[source]¶ Base class from which new CSRF-protected forms are derived. Only use this if you want to create a form without the extra model-functionality, i.e. is normal form.
If you want to create a CSRF-protected model form use
CSRFModelForm
.-
generate_csrf_token
(csrf_context)[source]¶ Create a CSRF token from the given context (which is actually just a
pyramid.request.Request
instance). This is automatically called during__init__
.
-
-
class
pyramid_crud.forms.
CSRFModelForm
(formdata=None, obj=None, *args, **kw)[source]¶ A form that adds a CSRF token to the form. Derive from this class for security critical operations (read: you want it most of the time and it doesn’t hurt).
Do not derive from this for inline stuff and other composite forms: Only the main form should use this as you only need one token per request.
All configuration is done exactly in the same way as with the
ModelForm
except for one difference: An additionalcsrf_context
argument is required. The pre-configured views and templates already know how to utilize this field and work fine with and without it.
Fields¶
The library defined some special fields. Normally, there is no need to be concerned with them as they are used internally. However, they might provide useful features to a developer.
-
class
pyramid_crud.fields.
MultiCheckboxField
(label=None, validators=None, coerce=<type 'unicode'>, choices=None, **kwargs)[source]¶ A multiple-select, except displays a list of checkboxes.
Iterating the field will produce subfields, allowing custom rendering of the enclosed checkbox fields.
Example for displaying this field:
class MyForm(Form): items = MultiCheckboxField(choices=[('1', 'Label')] form = MyForm() for item in form.items: str(item) # the actual field to be displayed, likely in template
If you don’t iterate, it produces an unordered list be default (if
str
is called onform.items
, not each item individually).And with formdata it might look like this:
# Definition same as above formdata = MultiDict() formdata.add('items', '1') form = MyForm(formdata) assert form.items.data == ['1']
As you can see, a list is produces instead of a scalar value which allows multiple fields with the same name.
-
class
pyramid_crud.fields.
MultiHiddenField
(label=None, validators=None, coerce=<type 'unicode'>, choices=None, **kwargs)[source]¶ A field that represents a list of hidden input fields the same way as
MultiCheckboxField
andwtforms.fields.SelectMultipleField
.
-
class
pyramid_crud.fields.
SelectField
(label=None, validators=None, coerce=<type 'unicode'>, choices=None, **kwargs)[source]¶ Same as
wtforms.fields.SelectField
with a custom validation message and the requirement thatdata
evaluates toTrue
(for the purpose of having an empty field that is not allowed).
Templates¶
Changing a Single Template¶
Changing a single template is as simple as copying it from the default theme
and adjusting it to your needs. For example, say you want to change the
list.mako
template. You copy it over and start editing.
cp /path/to/pyramid_crud/templates/mako/bootstrap/list.mako my_library/templates/crud/list.mako
After you are done editing the template, you need to configure your view to load it:
class MyCRUDView(CRUDView):
...
template_list = 'crud/list.mako'
This is all assuming the crud
directory can be looked up (in the example
above, you would need my_library/templates
to be in mako.directories
).
For an explanation of each template and some additional details, see Create a Complete Theme.
Create a Complete Theme¶
The default theme uses Bootstrap which looks nice but also not really unique and does not integrate at all with your own application look. Thus, you might want to roll your own look for all views. This is easily possible with the theme configuration parameter.
Note
This is an advanced technique. A lot of knowledge about the rest of the library is assumed throughout this section and so it is recommended that you make yourself familiar with the rest of the documentation before taking on own themes.
It is perfectly okay to create your application with the default theme first and change it afterwards to your custom theme. This way you familiarize yourself with the library and have it much easier understanding what is going on here.
The best way to roll your own template is to copy the default template from
pyramid_crud/templates/mako/bootstrap
. Let’s say you want to create your
own theme by the name my_crud_theme
. First you copy over the theme folder:
cp -a /path/to/pyramid_crud/templates/mako/bootstrap my_library/templates/my_crud_theme
Now you can directly enable your theme by configuring the
theme variable with the setting my_crud_theme
(this is
assuming that this folder is in your mako.directories
path). With this, you
should already have your new template enabled.
Note
If you want to configure a default template, just create your own
intermediate base class that defines the theme
parameter. This isn’t a
very pythonic solution but it works and is very flexible.
Now you should fire up your text editor and take a look at the files in your new theme folder. Here is a description of the files used:
Note
Each file receives the usual request
and view
parameters pyramid
passes in by default.
- base.mako
Contains the basic template with style sheets, flash messages and everything else that each template needs. You will define your own look here.
If you already have your own template, you don’t need this file and can delete it. You then have two options for configuring a custom base template:
- You can statically set the path in each
<%include
statement in the inheriting templates or - You can define template_base on the view and set it to the path of your own base template. It will then take this path as the base for all your templates and the regular base file is not needed anymore.
If you roll your own base, pay attention to the flash messages and their queue names: They are currently statically configured and so you have to read these queues or won’t see any messages at all.
Also pay attention to the blocks used by inherting templates and either change them or define them in your base (e.g.
head
andheading
).- You can statically set the path in each
- list.mako
A simple list view. It gets two arguments: The
items
parameter is a query that you can iterate over to get the object instances for each row. Theaction_form
parameter is a form instance with the following fields:- action
- A select list where you can choose an action and execute it on multiple items at once (see Adding Actions to Forms).
- items
- A field that has one checkbox field for each item in the
items
iterable. If you iterate over it, you get a single field that renders to a checkbox. In the default implementation,zip()
is used to provide each loop iteration with a single checkbox field and the corresponding item. - csrf_token
- A CSRF token field. This is required and must be displayed somewhere in the form or the validation will fail.
- submit
- A submit button that sends the form to execute the actions on the selected items.
- edit.mako
The view of a single item being edited. In the default implementation, this loads a fieldset for each configured fieldset on the form and then loads an inline template for each configured inline on the form. It receives the following parameters:
- form
- A form representing the item being edited. It is an instance of your
subclassed
ModelForm
. Look at the documentation for Forms for more information on supported methods (make sure to also checkout linked documentation from there). - is_new
- A boolean representing whether this is a new item or not.
- delete_confirm.mako
This template is invoked after the delete action was called and displays an intermediate view to make sure the user really wants to delete the selected items. It gets the following arguments:
- form
- The same form that the list view got as
action_form
- items
- The list of items to be deleted.
- edit_inline/*.mako
Any file in this folder is considered an inline template to be included. The following parameters are given during inclusion:
- inline
- The class that is inlined (not an instance!). It is the subclass you
made from base of
BaseInLine
. - items
- Instances of the above
inline
parameter, each being a form to be displayed inlined.
- fieldsets/*.mako
This file is used by the
edit.mako
template for each fieldset that should be rendered. It gets a singlefieldset
argument which is a dict with the following keys (note that it also keeps globals of the parent):- title
- The title of this fieldset, usually displayed in a
<legend>
tag. - fields
- A list of field names on the form. Use these to retrieve the correct field from the form instance. This is used instead of iterating over the form so you can group the fields into fieldsets.
You often don’t need to edit all of the files if you don’t use them. For
example, the grid
fieldset is just a special case and can often go unused
(you can delete it if you never use it on any fieldset). You can also often
keep the default template if you like the way they do things and just style
them by creating your own stylesheet using the same classes bootstrap does.
Keeping Some Templates from the Default Library¶
Sometimes you might want to change the complete look and overwrite most of the
templates but keep some of them from the old library. You could just keep the
original copy you made above but that is not a good idea because you might miss
out on updates to the templates. You can abuse the template_*
setting for
this, as it works both ways: Just set it to the path of the template you want
to keep. For example, to keep the delete_confirm
template but overwrite
everything else, configure your view like this:
class MyCRUDView(CRUDView):
...
template_delete_confirm = 'pyramid_crud:templates/mako/bootstrap/delete_confirm.mako'
Note how this is the full asset specification of the template because it is not
in any of the directories configured with mako.directories
. Also note, that
you cannot do this with templates in subdirectories (see
template_* for an explanation and solution).
Supporting Different Template Engines¶
Supporting another template engine is very simple. Assuming you already use them in the rest of the application, you have them set up anyway. Once you have a theme for this engine, you can just set it to the file extension of this theme.
Let’s say, for example, you have created a Chameleon theme with all file
names ending in .pt
. If you have this renderer enabled properly, it will
automatically be chosen correctly, if you give pyramid a path to a file ending
with .pt
. Thus, in addition to configuring your theme (see above), you just
configure the template_ext parameter to .pt
and
are good to go. This is what your view might look like:
class MyCRUDView(CRUDView)
...
theme = 'templates/my_chameleon_theme'
template_ext = '.pt'
Now assuming the lookup is correctly configured, this will fetch the templates using the correct renderer.
Adding Actions to Forms¶
Similar to Django’s Admin actions, pyramid_crud also provides a way to configure specific actions.
Introduction¶
What are actions?¶
An action in the context of this library is something you perform on a list of items that might change their state (or perform anything else, really). A good example would be publishing multiple articles at once or activating multiple users.
How do you configure actions?¶
Actions are configured by setting the actions parameter on your view. Possible values here are strings or callables. If a string is provided, a method of the same name is looked up on the view and used as the callable.
Each callable gets two arguments: The view and the query which selects the
items for which the actions should be performed. Note that a query is used
instead of a list of items so that you can refine it or directly perform
actions on it. If you need a list, call .all
on it or iterate over it.
So how do I create an action exactly?¶
Let’s work by example and take the same example Django does so we can directly see similarities and differences. Here’s our definition with which we start:
class Article(Base):
id = Column(Integer, primary_key=True)
title = Column(Text)
body = Column(Text)
status = Column(Enum('p', 'd', 'w'))
def __unicode__(self):
return self.title
Now that we have our model, let’s make a method to publish multiple articles at once:
def make_published(view, query):
query.update({'status': 'p'})
return True, None
Notice, how we don’t pass in the request as it can be accessed with
view.request
. The view is an instance of your subclassed
CRUDView
. The query is an instance of
Query
. Additionally, you can see that
we return a pair here. The first value indicates success of the operation, the
latter value is an optional response (see Returning Values From Actions for a detailed
explanation).
Now you might want to have a nicer title than ‘Make Published’ (this title is
assigned by default, replacing underscores with spaces and calling
str.title()
on the result). To achieve a custom title (that will appear
in the list of items), assign a label to its info dict:
def make_published(view, query):
query.update({'status': 'p'})
return True, None
make_published.info = {'label': "Mark selected stories as published"}
And how do I add it to a view?¶
That’s easy. Here is a full configuration based on the model above:
class ArticleForm(ModelForm):
class Meta:
model = Article
class ArticleView(CRUDView):
Form = ArticleForm
url_path = '/articles'
actions = [make_published]
See how we added the actions configuration directive? We gave it a list (with one item) of actions that should be available on this model.
And that’s it, now you have an additional action available at your disposal. Read on for some more information, including advanced techniques, differences and what’s missing in comparison to Django.
Advanced Techniques¶
Handling Errors¶
To handle exceptions, wrap your code in a try-except-else
clause.
You can then handle any exception and possibly log error
messages and flash a message to the user. This allows you to shield the user
from any application crashes and gives you the ability to examine the log for
the cause of the error.
Nonetheless, you can still raise exceptions and they will be passed through in which case the section on Returning Values From Actions does not really apply (as no value is returned).
An example of an implementation that shields to user from exceptions might look like this:
def make_published(view, query):
try:
query.update({'status': 'p'})
except:
log.error("An error oucurred:\n%s" % format_traceback())
self.request.session.flash("An error happened while publishing "
"the article(s)")
return False, None
else:
return True, None
This will inform the user of any failure and log the exact exception so you can investigate the problem. Note that with a perfect implementation, you would probably want to explicitly catch all possible exceptions and not use a catch-all. However, since this implementation doesn’t just ignore and instead log the exception, it is not too bad to have a catch all here.
Returning Values From Actions¶
As already noted above, it is recommended to wrap your code in
try-except-else
blocks and return the status as a boolean. The reason for
this is to allow explicit changes in application behavior based on the result
of your execution.
You always have to return a pair of (success, response)
to indicate how you
would like to proceed.
success
must be a boolean value. If it is False
it indicates that the
action was not successful. In this case the redirect is raised which means
it is considered an exception. Any optional transaction (e.g. pyramid_tm)
will see this exception and abort the transaction. Afterwards the page is
redirected. The response
value is not used in this case, so it should
always be None
.
If success
is True
, it is assumed that the action was successful. In
this case the redirect is returned and the transaction is committed. Note
that this is a fine distinction between success and failure and the user does
not see a difference (except error messages you might give out).
However, in the case of a successful response, you might also want to change
the returned value into something else (maybe redirect somewhere else or return
a whole new response). This can be done by setting the response
paramater
which can really be anything that is allowed to be returned from a view.
So for example if you wanted to direct to a completely different page, you
could return an instance of
HTTPFound
that achieves this. On
the other hand, you maybe want to create an intermediate response. In that
case, you just need to return an instance of
Response
. You could create this by calling
render_to_response
if you want
to render an intermediary view from a template. This is the technique the
delete action uses.
Note
The more complex it gets, the more likely it is that a redirect to an actual view is much better than manually rendering or building your response. This allows you to factor out the code from your action into a separate view but has the drawback of an additional redirect and the need to keep all the formdata alive (e.g. in the session).
Actions as Methods on the View¶
Instead of having an external function, you can add your action directly to the view (in most cases the recommended way). For this, you just create a method on the view instead of a function:
class ArticleView(CRUDView):
...
def make_published(self, query):
try:
query.update({'status': 'p'})
except:
return False, None
else:
return True, None
make_published = {'info': "Mark selected stories as published"}
Note how we renamed view
to self
because as a method the view reference
is now actually the own instance.
Instead of providing the action as a callable, you now use a string instead:
class ArticleView(CRUDView):
actions = ['make_published']
This will look up the action as a method on the view and call it in the same manner.
Currently Unspported Features from Django¶
- Site-wide actions: Currently it is not possible to add actions that are globally available. However, you can work around that by creating a custom subclass and modifiying the action list in the children during runtime, however, this is an unspported as of now and you might face some issues with mutability.
- Disabling actions: This is currently not supported at all.
- Runtime disabling/enabling of actions: While unspported, this is possible by
overriding the
_all_actions
atribute. In the default implementation it behaves like a property but caches its result (using Pyramid’s reify decoartor). Take a look at the default implementation to see the format of the returned value.
Help Topics¶
Transition From Django¶
If you are coming from Django you are probably wondering which features are offered here and to which Django features they correspond. To help you with this, here are some guidelines on getting used to this system.
ModelAdmin
Configuration Options¶
Django as a myriad of configuration options on its ModelAdmin. Not all features are supported here and they are distributed differently (due to the nature of this library), so here is some guidance on getting around.
First of all, you have to know that we split things a bit more up around here because we are already based on other libraries. Roughly speaking though we can define some equivalents:
- Django’s
ModelAdmin
is closest toviews.CRUDView
. Here we perform basic configuration actions, set routes and so on. However, contrary to Django not all configuration is performed here. And instead of a registration of theModelAdmin
with theModel
, we define aForm
on it that in turn links to the model. - The
Form
subclassesforms.ModelForm
. There is no equivalent in Django, as it creates the form automatically from the relation between the model and the admin class. However, because of the way the integration works (the form is actually created by WTForms-Alchemy) we need this form. This is the place where you configure behavior on actual form instances. - The
Model
in turn is very close to that of Django. But since our systems are not so closely integrated, the coupling is much lower (which can be good or bad). The models are mostly just SQLAlchemy models with some additional configuration done by WTForms-Alchemy.
Now that you know how the parts are constructed take a look at the following
table. It lists each Django option on ModelAdmin
and provides the
equivalent in either this library, WTForms-Alchemy or SQLAlchemy. If a behavior
is not supported here yet, it is denoted by “NYI” (not yet implemented).
Django | Us |
---|---|
actions |
Adding Actions to Forms |
actions_on_top |
NYI |
actions_on_bottomg |
NYI |
actions_selection_count |
NYI |
date_hierarchy |
NYI |
exclude |
Form.Meta.exclude |
fields |
Form.Meta.only |
fieldsets |
ModelForm.fieldsets |
filter_horizontal |
NYI |
filter_vertical |
NYI |
form |
CRUDView.Form |
formfield_overrides |
Adding/overriding fields |
inlines |
ModelForm.inlines |
list_display |
CRUDView.list_display |
list_display_links |
CRUDView.list_display_links |
list_editable |
NYI |
list_filter |
NYI |
list_max_show_all |
NYI |
list_per_page |
NYI |
list_select_related |
NYI |
ordering |
NYI |
paginator |
NYI |
prepoulated_fields |
NYI |
preserve_filters |
NYI |
radio_fields |
NYI |
raw_id_fields |
NYI |
readonly_fields |
NYI |
save_as |
NYI |
save_on_top |
NYI |
search_fields |
NYI |
add_form_template |
NYI |
change_form_template |
NYI |
change_list_template |
NYI |
delete_confirmation_template |
NYI |
delete_selected_confirmation_template |
NYI |
object_history_template |
NYI |
save_model |
NYI |
delete_model |
NYI |
save_formset |
NYI |
get_ordering |
NYI |
get_search_results |
NYI |
save_related |
NYI |
get_readonly_fields |
NYI |
get_prepopulated_fields |
NYI |
get_list_display |
NYI |
get_list_display_links |
NYI |
get_fieldsets |
NYI |
get_list_filter |
NYI |
get_inline_instances |
NYI |
get_urls |
NYI |
get_form |
NYI |
get_formsets |
NYI |
formfield_for_foreignkey |
NYI |
formfield_for_manytomany |
NYI |
formfield_for_choice_field |
NYI |
get_changelist |
NYI |
get_changelist_form |
NYI |
get_changelist_formset |
NYI |
has_add_permission |
NYI |
has_change_permission |
NYI |
has_delete_permission |
NYI |
get_queryset |
NYI |
message_user |
NYI |
get_paginator |
NYI |
add_view |
NYI |
change_view |
NYI |
changelist_view |
NYI |
delete_view |
NYI |
history_view |
NYI |
Media |
NYI |
FAQ¶
Examples¶
Development¶
Building Documentation¶
To build the documentation you first need to install all documentation dependencies:
pip install -r docs_require.txt
Then you can build the documentation:
python setup.py build_sphinx
Running Tests¶
Before you can run tests, you need to install the requirements. These consist of the requirements to create docs (for doctests) and pytest:
pip install -r tests_require.txt
Note
mock
is an unnecessary requirement for users of python 3.3 and above,
but it is included in the above file unconditionally.
Now you can run your tests with:
python setup.py test
If you need more control over which tests are executed, you can also execute pytest and doctest directly:
py.test tests/
make -C docs/ doctest
Note
Our tests are also run against templates. However, as they are not python files, the test suite automatically compiles them into a temporary directory. This directory should never be checked into GitHub and also be removed before installing the library (it does not hurt, it just pollutes the directory).
Running tests against templates also is included in coverage (the reason
why we need an accessible template module directory). The coverage values
are reported by Travis CI to coveralls. However, since the code for this is
not on GitHub, you cannot see which lines were missted online. Instead, you
need to run those tests locally and get coverage output with coverage
html
after you have run the tests.