Welcome to c2cgeoform’s documentation!¶
Prerequisites¶
The following system packages must be installed on your system:
python-virtualenv
libpq-dev
(header files for PostgreSQL)gettext
On Windows, you should install make
using Cygwin (and put the bin
folder into the path). For Python, please use Python >= 3.x.
You need to install PostgreSQL and PostGIS. On Ubuntu, the packages
postgresql-server-dev-9.3
and python-dev
are required.
User guide¶
Creating a c2cgeoform project¶
This page describes how to create a c2cgeoform project. A c2cgeoform project is basically a Pyramid project with c2cgeoform enabled in the project.
Install c2cgeoform¶
git clone git@github.com:camptocamp/c2cgeoform.git
cd c2cgeoform
make build
On Windows, you should use the https way to clone the repository:
git clone https://github.com:camptocamp/c2cgeoform.git
Create a Pyramid project using c2cgeoform scaffold¶
Note that if PYTHONPATH does not exists as an environment variable, template files (*_tmpl) are not rendered in new project folder.
export PYTHONPATH=$PYTHONPATH
.build/venv/bin/pcreate -s c2cgeoform ../c2cgeoform_project
Initialize a git repository¶
Make your new project folder a git repository.
cd ../c2cgeoform_project
git init
git add .
git commit -m 'Initial commit'
Install the project and its dependencies¶
make build
Set up database¶
First of all you need to have a PostGIS database for the project. Create the database:
sudo -u postgres psql -c "CREATE USER \"www-data\" WITH PASSWORD 'www-data';"
sudo -u postgres createdb c2cgeoform_project
sudo -u postgres psql -d c2cgeoform_project -c 'CREATE EXTENSION postgis;'
sudo -u postgres psql -c 'GRANT ALL ON DATABASE c2cgeoform_project TO "www-data";'
When you do have a Postgres role and a PostGIS database edit the
development.ini
and production.ini
files and set
sqlachemy.url
appropriately. For example:
sqlalchemy.url = postgresql://www-data:www-data@localhost:5432/c2cgeoform_project
Now create the tables:
make initdb
Note that this will launch the python script
c2cgeoform_project/scripts/initializedb.py
. You will have to
customize this thereafter.
Run the development server¶
You are now ready to run the application:
make serve
Visit the following URLs to verify that the application works correctly: http://localhost:6543/excavations/new and http://localhost:6543/excavations.
Defining the model for a form¶
The underlying schema for a c2cgeoform
form is defined as a SQLAlchemy
model. A simple definition is shown below:
from sqlalchemy import (Column, Integer, Text)
import deform
from uuid import uuid4
from c2cgeoform.models import Base
class Comment(Base):
__tablename__ = 'comments'
__colanderalchemy_config__ = {
'title': 'A very simple form'
}
id = Column(Integer, primary_key=True, info={
'colanderalchemy': {
'widget': deform.widget.HiddenWidget()
}})
hash = Column(Text, unique=True, default=lambda: str(uuid4(), info={
'colanderalchemy': {
'widget': HiddenWidget()
}})
name = Column(Text, nullable=False, info={
'colanderalchemy': {
'title': 'Name'
}})
comment = Column(Text, nullable=True, info={
'colanderalchemy': {
'title': 'Comment',
'widget': deform.widget.TextAreaWidget(rows=3),
}})
This SQLAlchemy model is enriched with properties for ColanderAlchemy, for example to set a title for a field, use a specific Deform widget or use a Colander validator.
In general, every SQLAlchemy model can be used as schema for a form. The only requirements are:
- The model class must contain exactly one primary key column. Tables with composite primary keys are not supported.
A more complex example for a model can be found here. For more information on how to define the model, please refer to the SQLAlchemy, ColanderAlchemy, Colander and Deform documentations.
Create the views for your model¶
There is already a views class created in your project by the scaffold,
see file views/excavation.py
. Let’s have a look on that file content.
To ease creation of views classes, c2cgeoform comes with an abstract class that contains base methods to display grids, render forms and save data. This is why ExcavationViews extends AbstractViews for a specific SQLAlchemy model and colander schema:
@view_defaults(match_param='table=excavations')
class ExcavationViews(AbstractViews):
_model = Excavation
_base_schema = base_schema
Also note the @view_defaults
which says that all the views declared in this
class will only apply when the route parameter named table
will be equal to
"excavation"
. The routes given by c2cgeoform have the following form:
- c2cgeoform_index:
{table}
- c2cgeoform_grid:
{table}/grid.json
- c2cgeoform_item:
{table}/{{id}}
- c2cgeoform_item_duplicate:
{table}/{{id}}/duplicate
Those routes are registered in the pyramid config by the routes
module (see
the routes.py
file situated at the root of the generated project).
register_models(config, [
('excavations', Excavation)
To select records through urls, we also need a unique field, this is given by:
_id_field = 'hash'
And to show the table records grid we need a definition per column:
_list_fields = [
_list_field('reference_number'),
_list_field('request_date'),
...
]
Finally we need a method for each view, for a typical use case, we could have 6 views:
index
: Return HTML page with the grid.grid
: Return records as JSON for the grid.edit
: Show create or edit form for the specified record.duplicate
: Show duplication form for the specified record.delete
: Delete the specified record.save
: Save new record or modifications to existing record.
In a typical use case, those views will only call the super class method with the same name.
Configure the grid¶
Grid columns can be configured using the _list_fields
property of the views
class, which is an ordered list of ListField
objects, one for each column.
The ListField
constructor take some parameters:
model
: the SQLAlchemy mapper (required if attr is an attribute name).attr
: the model attribute name to use or an SQLAlchemy InstrumentedAttribute.key
: an identifier for the column, default toattribute.key
.label
: text for the column header, default to colanderalchemy title for the field.renderer
: callable that takes an entity of the SQLAlchemy mapper and returns a string value.sort_column
: AnIntrumentedAttribute
to use insort_by
.filter_column
: AnIntrumentedAttribute
to filter with.visible
: a boolean for the initial visible state of this column.
Every time the table index page asks for data from the grid view, the
AbstractView
will create a default query using AbstractViews._base_query
method.
If you use columns coming from relationships, this might result in sending one
request to the database for each relationship and each record.
In such cases, you should override the _base_query
method to use eager
loading for those relationships, for example:
def _base_query(self):
return self._request.dbsession.query(Excavation).distinct(). \
join('situations'). \
options(subqueryload('situations'))
Note that you also need to join
the relationships you use for sorting and filtering.
Understanding the schemas¶
ColanderAlchemy allows creating Colander schemas directly from SQLAlchemy model classes.
Additionally, c2cgeoform provides its own classes with extended features. A basic use case schema creation will look like:
from model import MyClass
schema = GeoFormSchemaNode(MyClass)
See the following API to understand what is going on behind the scene.
-
class
c2cgeoform.schema.
GeoFormSchemaNode
(*args, **kw)¶ An SQLAlchemySchemaNode with deferred request and dbsession properties. This will allow defining schemas that requires the request and dbsession at module-scope.
Example usage:
schema = GeoFormSchemaNode(MyModel) def create_form(request, dbsession): return Form( schema = schema.bind( request=request, dbsession=request.dbsession), ... )
-
add_unique_validator
(column, column_id)¶ Adds an unique validator on this schema instance.
- column
- SQLAlchemy ColumnProperty that should be unique.
- column_id
- SQLAlchemy MapperProperty that is used to recognize the entity, basically the primary key ColumnProperty.
-
-
class
c2cgeoform.schema.
GeoFormManyToManySchemaNode
(class_, includes=None, *args, **kw)¶ A GeoFormSchemaNode that properly handles many to many relationships.
- includes:
- Default to primary key name(s) only.
-
objectify
(dict_, context=None)¶ Method override that returns the existing ORM class instance instead of creating a new one.
Configure the widgets¶
All Deform widgets can be used with c2cgeoform
. See the Deform examples
and widgets API reference for detailed description about available options.
Additionally, c2cgeoform provides some extra widgets:
-
class
c2cgeoform.ext.deform_ext.
FileUploadWidget
(tmpstore, get_url=None, **kw)¶ Extension of
deform.widget.FileUploadWidget
to be used in a model class that extends themodels.FileData
mixin class.Note that, contrary to
deform.widget.FileUploadWidget
, this extension is not meant to be used with thedeform.FileData
Colander type. Instead it works with thecolander.Mapping
type, which is whatcolanderalchemy
uses for an SQLAlchemy model class.Note also that it is required to set
unknown
to'preserve'
in the__colanderalchemy_config__
dictionary.Example usage
from c2cgeoform import models from c2cgeoform.ext import deform_ext class Photo(models.FileData, Base): __tablename__ = 'photo' __colanderalchemy_config__ = { 'title': _('Photo'), 'unknown': 'preserve', 'widget': deform_ext.FileUploadWidget(file_upload_temp_store) } permission_id = Column(Integer, ForeignKey('excavations.id'))
Attributes/Arguments
- get_url (optional)
A callback function function(request, id) -> string which returns the URL to get the file. Example usage:
'widget': deform_ext.FileUploadWidget( _file_upload_temp_store, get_url=lambda request, id: request.route_url('file', id=id) )
-
class
c2cgeoform.ext.deform_ext.
MapWidget
(**kw)¶ A Deform widget that fits with GeoAlchemy 2 geometry columns and shows an OpenLayers 3 map which allows to draw and modify geometries.
Example usage
geom = Column( geoalchemy2.Geometry('POLYGON', 4326, management=True), info={ 'colanderalchemy': { 'typ': colander_ext.Geometry( 'POLYGON', srid=4326, map_srid=3857), 'widget': deform_ext.MapWidget( base_layer='new ol.layer.Tile({ source: new ol.source.OSM() })', center=[829170, 5933942], zoom=7, fit_max_zoom=14 ) }})
To customize the map, the template file map.pt has to be overwritten.
Attributes/Arguments
- base_layer (str, optional):
- Javascript code returning the map base layer.
- center ([x, y], optional):
- Initial center when no geometry is given.
- zoom (int, optional):
- Initial zoom when no geometry is given.
- fit_max_zoom (int, optional):
- Maximum zoom when fitting to given geometry.
-
class
c2cgeoform.ext.deform_ext.
RecaptchaWidget
(**kw)¶ A Deform widget for Google reCaptcha.
In c2cgeoform this widget can be used by setting the show_captcha flag when calling register_schema().
Example usage:
register_schema( 'comment', model.Comment, show_confirmation=False, show_captcha=True, recaptcha_public_key=settings.get('recaptcha_public_key'), recaptcha_private_key=settings.get('recaptcha_private_key'))
Attributes/arguments
- public_key (required)
- The Google reCaptcha site key.
- private_key (required)
- The Google reCaptcha secret key.
-
class
c2cgeoform.ext.deform_ext.
RelationCheckBoxListWidget
(model, id_field='id', label_field='label', order_by=None, **kw)¶ Extension of the widget
``deform.widget.CheckboxChoiceWidget
which loads the values from the database using a SQLAlchemy model.For n:m relations the widget can be used like so:
situations = relationship( "Situation", secondary=situation_for_permission, cascade="save-update,merge,refresh-expire", info={ 'colanderalchemy': { 'title': _('Situations'), 'widget': RelationCheckBoxListWidget( Situation, 'id', 'name', order_by='name', edit_url=lambda request, value: request.route_url( 'c2cgeoform_item', table='situations', id=value ) ), 'includes': ['id'], 'validator': manytomany_validator } })
Attributes/Arguments
- model (required)
- The SQLAlchemy model that is used to generate the list of values.
- id_field
- The property of the model that is used as value.
Default:
id
. - label_field
- The property of the model that is used as label.
Default:
label
. - order_by
- The property of the model that is used for the
order_by
clause of the SQL query. Default:None
. - edit_url (optionnal)
- a function taking request and value as parameter and returning an url to the correponding resource.
For further attributes, please refer to the documentation of
deform.widget.Select2Widget
in the deform documentation: <http://deform.readthedocs.org/en/latest/api.html>
-
class
c2cgeoform.ext.deform_ext.
RelationRadioChoiceWidget
(model, id_field='id', label_field='label', order_by=None, **kw)¶ Extension of the widget
``deform.widget.RadioChoiceWidget
which loads the values from the database using a SQLAlchemy model.Example usage
districtId = Column(Integer, ForeignKey('district.id'), info={ 'colanderalchemy': { 'title': 'District', 'widget': deform_ext.RelationRadioChoiceWidget( District, 'id', 'name', order_by='name' ) }})
The values of this field are filled with entries of model
District
, whereas propertyid
is used as value andname
as label.Attributes/Arguments
- model (required)
- The SQLAlchemy model that is used to generate the list of values.
- id_field
- The property of the model that is used as value.
Default:
id
. - label_field
- The property of the model that is used as label.
Default:
label
. - order_by
- The property of the model that is used for the
order_by
clause of the SQL query. Default:None
.
For further attributes, please refer to the documentation of
deform.widget.RadioChoiceWidget
in the deform documentation: <http://deform.readthedocs.org/en/latest/api.html>
-
class
c2cgeoform.ext.deform_ext.
RelationSearchWidget
(url, **kw)¶ A Deform widget to select an item via a search field. This widget is similar to the
RelationSelectWidget
, but instead of a select-box a Twitter Typeahead search field is shown.Example usage:
address_id = Column(Integer, ForeignKey('address.id'), info={ 'colanderalchemy': { 'title': _('Address'), 'widget': deform_ext.RelationSearchWidget( url=lambda request: request.route_url('addresses'), model=Address, min_length=1, id_field='id', label_field='label' ) }})
The user is responsible for providing a web-service at the given URL. The web service should expect requests of the form
?term=<search_terms>
. And it should return responses of this form:[{"id": 0, "label": "foo"}, {"id": 1, "label": "bar"}]
The name of the id and label keys are configurable. See below.
Attributes/arguments
- url (required)
- The search service URL, or a function that takes a request a return the search service URL.
- model (required)
- The SQLAlchemy model class associated to the linked table.
- min_length
- The minimum character length needed before suggestions start getting
rendered. Default:
1
. - id_field
- The name of the “id” property in JSON responses. Default:
"id"
. - label_field
- The name of the “label” property in JSON responses.
Default:
"label"
. - limit
- The maximum number of suggestions. Default: 8.
-
class
c2cgeoform.ext.deform_ext.
RelationSelect2Widget
(model, id_field='id', label_field='label', default_value=None, order_by=None, **kw)¶ Extension of the widget
``deform.widget.Select2Widget
which loads the values from the database using a SQLAlchemy model.Example usage
districtId = Column(Integer, ForeignKey('district.id'), info={ 'colanderalchemy': { 'title': 'District', 'widget': deform_ext.RelationSelect2Widget( District, 'id', 'name', order_by='name', default_value=('', _('- Select -')) ) }})
The values of this
<select>
field are filled with entries of modelDistrict
, whereas propertyid
is used as value andname
as label.For n:m relations the widget can be used like so:
situations = relationship( "Situation", secondary=situation_for_permission, cascade="save-update,merge,refresh-expire", info={ 'colanderalchemy': { 'title': _('Situations'), 'widget': RelationSelect2Widget( Situation, 'id', 'name', order_by='name', multiple=True ), 'includes': ['id'], 'validator': manytomany_validator } })
Attributes/Arguments
- model (required)
- The SQLAlchemy model that is used to generate the list of values.
- id_field
- The property of the model that is used as value.
Default:
id
. - label_field
- The property of the model that is used as label.
Default:
label
. - order_by
- The property of the model that is used for the
order_by
clause of the SQL query. Default:None
. - default_value
- A default value that is added add the beginning of the list of values
that were loaded from the database.
For example:
default_value=('', _('- Select -'))
Default:None
. - multiple
- Allow to select multiple values. Requires a n:m relationship.
Default:
False
.
For further attributes, please refer to the documentation of
deform.widget.Select2Widget
in the deform documentation: <http://deform.readthedocs.org/en/latest/api.html>
-
class
c2cgeoform.ext.deform_ext.
RelationSelectMapWidget
(url, label_field='label', **kw)¶ A Deform widget to select an item on a map. From the idea, this widget is similar to the
RelationSelectWidget
, but instead of a select-box a map is shown.Example usage
bus_stop = Column(Integer, ForeignKey('bus_stops.id'), info={ 'colanderalchemy': { 'title': 'Bus stop', 'widget': deform_ext.RelationSelectMapWidget( label_field='name', url='/bus_stops' ) }})
The user is responsible for providing a web-service under the given URL, which returns a list of features as GeoJSON. The features must contain the two properties specified with id_field and label_field. The geometries are expected to use the CRS EPSG:4326.
To customize the map, the template file map_select.pt has to be overwritten.
Attributes/Arguments
- url (required)
The URL to the web-service which returns the GeoJSON features or a callback function function(request) -> string which returns the URL to the web-service. Example usage:
'widget': deform_ext.RelationSelectMapWidget( label_field='name', url=lambda request: request.route_url('bus_stops') )
- label_field
- The property of the GeoJSON features that is used as label.
Default:
label
.
-
class
c2cgeoform.ext.deform_ext.
RelationSelectWidget
(model, id_field='id', label_field='label', default_value=None, order_by=None, **kw)¶ Extension of the widget
``deform.widget.SelectWidget
which loads the values from the database using a SQLAlchemy model.Example usage
districtId = Column(Integer, ForeignKey('district.id'), info={ 'colanderalchemy': { 'title': 'District', 'widget': deform_ext.RelationSelectWidget( District, 'id', 'name', order_by='name', default_value=('', _('- Select -')) ) }})
The values of this
<select>
field are filled with entries of modelDistrict
, whereas propertyid
is used as value andname
as label.For n:m relations the widget can be used like so:
situations = relationship( "Situation", secondary=situation_for_permission, cascade="save-update,merge,refresh-expire", info={ 'colanderalchemy': { 'title': _('Situations'), 'widget': RelationSelectWidget( Situation, 'id', 'name', order_by='name', multiple=True ), 'includes': ['id'], 'validator': manytomany_validator } })
Attributes/Arguments
- model (required)
- The SQLAlchemy model that is used to generate the list of values.
- id_field
- The property of the model that is used as value.
Default:
id
. - label_field
- The property of the model that is used as label.
Default:
label
. - order_by
- The property of the model that is used for the
order_by
clause of the SQL query. Default:None
. - default_value
- A default value that is added add the beginning of the list of values
that were loaded from the database.
For example:
default_value=('', _('- Select -'))
Default:None
. - multiple
- Allow to select multiple values. Requires a n:m relationship.
Default:
False
.
For further attributes, please refer to the documentation of
deform.widget.SelectWidget
in the deform documentation: <http://deform.readthedocs.org/en/latest/api.html>
Using custom templates¶
c2cgeoform distinguishes two types of templates: views templates and widget templates. - Views templates are used directly by Pyramid and provide the site structure. - Widgets templates are used by Deform to render the forms.
Default views templates¶
The default c2cgeoform views templates are located in the templates
folder and use jinja2 syntax.
c2cgeoform comes with partial templates that are included in views templates of your project.
Overriding widgets templates globally¶
Deform widget templates are located in the templates/widgets
folder and
use the chameleon syntax.
At rendering time, Deform will search folders for the templates in order they
appear in Form renderer search_path
property. c2cgeoform configure it to:
default_search_paths = (
resource_filename('c2cgeoform', 'templates/widgets'),
resource_filename('deform', 'templates'))
But you can add you own widgets folder, in your package __init__.py
file
before including c2cgeoform
using:
import c2cgeoform
search_paths = (
(resource_filename(__name__, 'templates/widgets'),) +
c2cgeoform.default_search_paths
)
c2cgeoform.default_search_paths = search_paths
To overwrite globally the Deform templates or the templates coming from
c2cgeoform
(like the map widget), you just need to copy the template to your application
templates/widgets
folder.
Use a custom template for a form or a specific widget in a form¶
Both the form main template and widget templates can be changed locally for a
given model by giving a template
property to the Widget
.
base_schema = GeoFormSchemaNode(
Comment,
widget=FormWidget(template='comment'))
Note that it is possible to create a layout for the form fields without completely
overriding the form template by giving a fields_template
to the form schema.
base_schema = GeoFormSchemaNode(
Comment,
widget=FormWidget(fields_template='comment_fields'))
Here is the default one: https://github.com/camptocamp/c2cgeoform/blob/master/c2cgeoform/templates/widgets/mapping_fields.pt
Writing tests¶
Internationalization¶
Developer guide¶
This page describes how to set up the development environment for working on c2cgeoform. It is for developers working on c2cgeoform itself, not for developers working on c2cgeoform-based applications.
Note that c2cgeoform is a framework with a Pyramid scaffold used to create c2cgeoform-based applications. This scaffold produce a fully functional c2cgeoform-base project: the c2cgeoform_demo project.
When running code checks and tests, these jobs are first run on the c2cgeoform framework itself. Then the c2cgeoform_demo project is generated in .build folder. Finally, the checks and tests are launched in this project.
Note that you should never alter the c2cgeoform_demo project itself but the c2cgeoform scaffold and regenerate the c2cgeoform_demo project.
Clone the project¶
git clone git@github.com:camptocamp/c2cgeoform.git
cd c2cgeoform
Run the checks¶
make check
Run the tests¶
Create the tests database:
sudo -u postgres psql -c "CREATE USER \"www-data\" WITH PASSWORD 'www-data';"
export DATABASE=c2cgeoform_demo_tests
sudo -u postgres psql -d postgres -c "CREATE DATABASE $DATABASE OWNER \"www-data\";"
sudo -u postgres psql -d $DATABASE -c "CREATE EXTENSION postgis;"
Run the framework and demo tests:
make test
Serve the c2cgeoform_demo project¶
You need to create a PostGIS database. For example:
export DATABASE=c2cgeoform_demo
sudo -u postgres psql -d postgres -c "CREATE DATABASE $DATABASE OWNER \"www-data\";"
sudo -u postgres psql -d $DATABASE -c "CREATE EXTENSION postgis;"
make initdb
Run the development server:
make serve
You can now open the demo project in your favorite browser: http://localhost:6543/
And there you go, you’re ready to develop, make changes in c2cgeoform, run checks and tests in c2cgeoform. And finally see the results in c2cgeoform demo application.
Deploy the c2cgeoform_demo on demo server¶
Prepare the demo project:
# open a ssh connection with the GMF 2.3 server
ssh -A geomapfish-demo.camptocamp.com
# clone the c2cgeoform repository
cd /var/www/vhosts/geomapfish-demo/private
git clone git@github.com:camptocamp/c2cgeoform.git
# generate the c2cgeoform_demo project with mod_wsgi related files
APACHE_ENTRY_POINT=c2cgeoform make modwsgi
Create the database as to serve the development version, see: Serve the c2cgeoform_demo project
Include the demo project in Apache virtual host configuration:
echo "IncludeOptional $PWD/.build/c2cgeoform_demo/.build/apache.conf" > /var/www/vhosts/geomapfish-demo/conf/c2cgeoform_demo.conf
sudo apache2ctl configtest
If everything goes fine, restart apache:
sudo apache2ctl graceful
You can now open the demo project in your favorite browser: https://geomapfish-demo.camptocamp.com/c2cgeoform/