Skip to content

Commit

Permalink
Merge pull request #28 from makinacorpus/add_leaflet_form_widget_and_…
Browse files Browse the repository at this point in the history
…admin

Add leaflet form widget and admin
  • Loading branch information
leplatrem committed Sep 3, 2013
2 parents da38c7c + fb054fd commit 1fa80ac
Show file tree
Hide file tree
Showing 22 changed files with 3,715 additions and 9 deletions.
12 changes: 12 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ env:
- DJANGO_VERSION=1.5.1

install:
# GeoDjango dependencies
- sudo apt-get install -y python-software-properties
- sudo apt-add-repository -y ppa:ubuntugis/ppa
- sudo apt-get update > /dev/null
- sudo apt-get install -y libgdal-dev libproj-dev libgeos-dev libspatialite-dev
- wget http://pysqlite.googlecode.com/files/pysqlite-2.6.3.tar.gz
- tar -zxvf pysqlite-2.6.3.tar.gz
- cd pysqlite-2.6.3
- sed -i "s/define=SQLITE_OMIT_LOAD_EXTENSION//g" setup.cfg
- python setup.py install
- cd ..

# This is a dependency of our Django test script
- pip install argparse --use-mirrors

Expand Down
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
include README.rst CHANGES LICENSE
recursive-include leaflet/locale *.mo
recursive-include leaflet/templates *.html
recursive-include leaflet/templates *.html *.js
recursive-include leaflet/static *.js
recursive-include leaflet/static *.css
recursive-include leaflet/static *.png
84 changes: 84 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,90 @@ To include all plugins configured in ``LEAFLET_CONFIG['PLUGINS']``, use::
{% leaflet_css plugins="ALL" %}


Forms
=====

With Django>=1.6, a Leaflet widget is provided to edit geometry fields.
In previous versions, it falls back to simple text areas.

It embeds *Leaflet.draw* in version *0.2.1dev*.


In Adminsite
------------

::

from leaflet.admin import LeafletGeoAdmin

from .models import WeatherStation


admin.site.register(WeatherStation, LeafletGeoAdmin)


In forms
--------

::

from django import forms

from leaflet.forms.fields import PointField


class WeatherStationForm(forms.ModelForm):
geom = PointField()

class Meta:
model = WeatherStation
fields = ('name', 'geom')


The related template would look like this:

::

<html>
<head>
{% leaflet_js plugins="forms" %}
{% leaflet_css plugins="forms" %}
</head>
<body>
<h1>Edit {{ object }}</h1>
<form action="POST">
{{ form }}
<input type="submit"/>
</form>
</body>
</html>


Plugins
-------

It's possible to add extras JS/CSS or auto-include *forms* plugins
everywhere: ::

LEAFLET_CONFIG = {
'PLUGINS': {
'auto-include': True
}
}

( *It will be merged over default minimal set required for edition* )


Details
-------

* It relies on global settings for map initialization.
* It works with local map projections. But SRID is specified globally
through ``LEAFLET_CONFIG['SRID']`` as described below.
* Javascript component for de/serializing fields value is pluggable.
* Javascript component for Leaflet.draw behaviour initialization is pluggable.


Advanced usage
==============

Expand Down
21 changes: 17 additions & 4 deletions leaflet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,25 +70,38 @@
SRID = None


DEFAULT_CENTER = app_settings.get('DEFAULT_CENTER', None)
DEFAULT_CENTER = app_settings['DEFAULT_CENTER']
if DEFAULT_CENTER is not None and not (isinstance(DEFAULT_CENTER, (list, tuple)) and len(DEFAULT_CENTER) == 2):
raise ImproperlyConfigured("LEAFLET_CONFIG['DEFAULT_CENTER'] must be an list/tuple with two elements - (lon, lat)")


DEFAULT_ZOOM = app_settings.get("DEFAULT_ZOOM", None)
DEFAULT_ZOOM = app_settings['DEFAULT_ZOOM']
if DEFAULT_ZOOM is not None and not (isinstance(DEFAULT_ZOOM, int) and (1 <= DEFAULT_ZOOM <= 24)):
raise ImproperlyConfigured("LEAFLET_CONFIG['DEFAULT_ZOOM'] must be an int between 1 and 24.")


PLUGINS = app_settings.get("PLUGINS", None)
if PLUGINS is not None and not (isinstance(PLUGINS, dict) and all(map(lambda el: isinstance(el, dict), PLUGINS.itervalues()))):
PLUGINS = app_settings['PLUGINS']
if not (isinstance(PLUGINS, dict) and all([isinstance(el, dict) for el in PLUGINS.values()])):
error_msg = """LEAFLET_CONFIG['PLUGINS'] must be dict of dicts in the format:
{ '[plugin_name]': { 'js': '[path-to-js]', 'css': '[path-to-css]' } } .)"""
raise ImproperlyConfigured(error_msg)

PLUGIN_ALL = 'ALL'
PLUGINS_DEFAULT = '__default__'

# Add plugins required for forms (not auto-included)
PLUGIN_FORMS = 'forms'
_forms_js = ['leaflet/leaflet.js',
'leaflet/draw/leaflet.draw.js',
'leaflet/leaflet.extras.js',
'leaflet/leaflet.forms.js']
_forms_css = ['leaflet/leaflet.css',
'leaflet/draw/leaflet.draw.css']
_forms_plugins = PLUGINS.setdefault(PLUGIN_FORMS, {})
_forms_plugins.setdefault('js', []).extend(_forms_js)
_forms_plugins.setdefault('css', []).extend(_forms_css)
_forms_plugins.setdefault('auto-include', False)
PLUGINS[PLUGIN_FORMS] = _forms_plugins

# Take advantage of plugin system for Leaflet.MiniMap
if app_settings.get('MINIMAP'):
Expand Down
41 changes: 41 additions & 0 deletions leaflet/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from django.contrib.admin import ModelAdmin
from django.contrib.gis.db import models

from .forms.widgets import LeafletWidget


class LeafletGeoAdmin(ModelAdmin):
widget = LeafletWidget
map_template = 'leaflet/admin/widget.html'
modifiable = True
map_width = '100%'
map_height = '400px'
display_raw = False

def formfield_for_dbfield(self, db_field, **kwargs):
"""
Overloaded from ModelAdmin to set Leaflet widget
in form field init params.
"""
if isinstance(db_field, models.GeometryField) and (db_field.dim < 3 or
self.widget.supports_3d):
kwargs.pop('request', None) # unsupported for form field
# Setting the widget with the newly defined widget.
kwargs['widget'] = self._get_map_widget(db_field)
return db_field.formfield(**kwargs)
else:
return super(LeafletGeoAdmin, self).formfield_for_dbfield(db_field, **kwargs)

def _get_map_widget(self, db_field):
"""
Overriden LeafletWidget with LeafletGeoAdmin params.
"""
class LeafletMap(self.widget):
template_name = self.map_template
include_media = True
geom_type = db_field.geom_type
modifiable = self.modifiable,
map_width = self.map_width
map_height = self.map_height
display_raw = self.display_raw
return LeafletMap
Empty file added leaflet/forms/__init__.py
Empty file.
50 changes: 50 additions & 0 deletions leaflet/forms/fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import django

if django.VERSION >= (1, 6, 0):
from django.contrib.gis.forms.fields import GeometryField
else:
from django.contrib.gis.forms.fields import GeometryField as BaseField

class GeometryField(BaseField): # noqa
geom_type = 'GEOMETRY'

def __init__(self, *args, **kwargs):
kwargs['geom_type'] = self.geom_type
super(GeometryField, self).__init__(*args, **kwargs)
self.widget.attrs['geom_type'] = self.geom_type


from .widgets import LeafletWidget


class LeafletGeometryField(GeometryField):
widget = LeafletWidget
geom_type = 'GEOMETRY'


class GeometryCollectionField(LeafletGeometryField):
geom_type = 'GEOMETRYCOLLECTION'


class PointField(LeafletGeometryField):
geom_type = 'POINT'


class MultiPointField(LeafletGeometryField):
geom_type = 'MULTIPOINT'


class LineStringField(LeafletGeometryField):
geom_type = 'LINESTRING'


class MultiLineStringField(LeafletGeometryField):
geom_type = 'MULTILINESTRING'


class PolygonField(LeafletGeometryField):
geom_type = 'POLYGON'


class MultiPolygonField(LeafletGeometryField):
geom_type = 'MULTIPOLYGON'
56 changes: 56 additions & 0 deletions leaflet/forms/widgets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from django import forms
try:
from django.contrib.gis.forms.widgets import BaseGeometryWidget
except ImportError:
from django.forms.widgets import Textarea

class BaseGeometryWidget(Textarea):
geom_type = 'GEOMETRY'
display_raw = False

from .. import PLUGINS, PLUGIN_FORMS


class LeafletWidget(BaseGeometryWidget):
template_name = 'leaflet/widget.html'
map_srid = 4326
map_width = None
map_height = None
modifiable = True
supports_3d = False
include_media = False

@property
def media(self):
if not self.include_media:
return forms.Media()

js = PLUGINS[PLUGIN_FORMS]['js']
css = PLUGINS[PLUGIN_FORMS]['css']
return forms.Media(js=js, css={'screen': css})

def serialize(self, value):
return value.geojson if value else ''

def deserialize(self, value):
value = super(LeafletWidget, self).deserialize(value)
if value:
value.srid = self.map_srid
return value

def render(self, name, value, attrs=None):
assert self.map_srid == 4326, 'Leaflet vectors should be decimal degrees.'

attrs = attrs or {}

# In BaseGeometryWidget, geom_type is set using gdal, and fails with generic.
# See https://code.djangoproject.com/ticket/21021
if self.geom_type == 'GEOMETRY':
attrs['geom_type'] = 'Geometry'

attrs.update(id_map=attrs.get('id', name) + '_map',
id_map_callback=attrs.get('id', name) + '_map_callback',
modifiable=self.modifiable,
geometry_field_class=attrs.get('geometry_field_class', 'L.GeometryField'),
field_store_class=attrs.get('field_store_class', 'L.FieldStore'))
return super(LeafletWidget, self).render(name, value, attrs)
Binary file modified leaflet/locale/fr/LC_MESSAGES/django.mo
Binary file not shown.
Loading

0 comments on commit 1fa80ac

Please sign in to comment.