I have started to work on ticket #5472 [1]. I'd like to move it
forward, I'm the author of django-geoportail [0] and I'd like to make
it easier to write such applications and to have a nicer widget API
for geographic fields. I'm writing on this list to get some feedback
on what I've got so far. The implementation of my approach is in my
github fork, the patch can be seen here:
https://github.com/brutasse/django/compare/5472-map-widgets
There is a patch based on olWidget attached to the ticket but I'm not
basing my work on it since I'm much more familiar with the existing
admin widgets than with olWidget.
My patch implements rich form widgets using the django form and
widgets libraries. The default widget is the same as the GeoModelAdmin
widget. Here is a summary of the changes :
- Geometry fields have their geometry widgets in
django.contrib.gis.forms.widgets. The default widget is no longer a
Textarea, which means that ModelForms will automatically pick up the
appropriate widgets.
- django.contrib.gis.admin.GeoModelAdmin is no longer necessary,
django.contrib.admin.ModelAdmin will display map widgets. I've removed
contrib.gis.admin from my fork to make sure nothing depends on it but
it will more likely follow the usual deprecation path.
- additionally to the default widget, there is a Google Map widget
and an OpenStreetMap widget. It's really easy to write a custom
widget.
A bit of work is required if you want to override the field's default
widget. Let's say I have a simple model :
class Foo(models.Model):
point = models.PointField()
If I want to create a ModelForm with the Google Maps widget, I have to do :
from django.contrib.gis import forms
class GMapPointWidget(forms.GMapWidget, forms.PointWidget):
pass
class FooForm(forms.ModelForm):
point = forms.PointField(widget=GMapPointWidget())
class Meta:
model = Foo
There are two kinds of widgets:
- widgets that are related to map providers : the default
GeometryWidget and its subclasses OSMWidget and GMapWidget
- widgets that are related to geometry types, such as PointWidget,
PolygonWidget, etc.
With the current implementation you need to tell django "I want a map
from provider X" and "I want to draw a point", hence the double
inheritance. This is also more or less the case with the
get_map_widget() method of the admin class, it's a factory that
generates a different widget depending on the type of the field.
As for the differences in the admin, now the customization needs to be
done on a per-widget basis instead of a per-model basis. If you want
to customize Foo, you need to attach the custom ModelForm to the admin
class:
from django.contrib import admin
from django.contrib.gis import forms
class GMapPointWidget(forms.GMapWidget, forms.PointWidget):
width = 800
height = 500
layerswitcher = False
# etc etc, these are the options of GeoModelAdmin that you all know
class FooForm(forms.ModelForm):
point = forms.PointField(widget=GMapPointWidget())
class Meta:
model = Foo
class FooAdmin(admin.ModelAdmin):
form = FooForm
So now there is greater control of the different widgets but
customization is slightly more verbose. I think this is still an
improvement since no extra work is required to enable rich widgets in
the admin and the forms library can be used just like standard django
forms.
Widget classes also accept the 'attrs' argument instead of using class
attributes. For instance:
other_point = forms.PointField(widget=GMapPointWidget(attrs={'map_width':
200, 'map_height': 100}))
This can avoid having a lot of subclasses when you only need to
override a couple of attributes.
Read-only widgets are also easy to render, the 'modifiable = False'
class attribute disables editing. I have added a 'color' and 'opacity'
attributes to customize the color of the feature drawn on the map.
Color is 'rgb' or 'rrggbb', opacity varies from 0 to 1 and is useful
mostly for polygons. For django-geoportail I wrote a template library
for read-only widgets but I'm not sure it's the right approach anyway.
I'd like to hear your comments, criticisms and suggestions to see if
this can be considered for inclusion in Django. The feature freeze for
1.3 is in 10 days, if a patch everyone's happy with is ready until
then that would be awesome. I havent' written any test yet, I'm not
sure to what extend this can be tested since there is probably more
javascript code than python code, tests would basically check template
rendering (I'll write some later). Docs on API usage and writing
custom widgets will follow as soon as I get positive feedback…
Greetings,
Bruno
[0] http://packages.python.org/django-geoportail/
[1] http://code.djangoproject.com/ticket/5472
On Fri, Nov 19, 2010 at 2:17 AM, Bruno Renié <bub...@gmail.com> wrote:
> Hi everyone,
>
> … <snip> …
I've made a few improvements since my previous email. The
GeoModelAdmin admin class is back since it makes the customization of
the admin much simpler (OSMGeoAdmin is still here too). It also
removes the need to deprecate it while existing application may still
use the get_map_widget() hack.
I also wrote some documentation for the forms library:
https://github.com/brutasse/django/blob/5472-map-widgets/docs/ref/contrib/gis/forms.txt
Changes since the initial proposal:
* ModelForms are actually simpler to customize than I previously
stated. I learned about the 'widgets' meta attribute added in Django
1.2. With my previous "Foo" model example:
from django.contrib.gis import forms
class GMapPointWidget(forms.GMapWidget, forms.PointWidget):
pass
class FooForm(forms.ModelForm):
class Meta:
model = Foo
widgets = {'point': GMapPointWidget}
Same number of lines, but simpler lines :-)
* As for the admin, it is now much simpler with the GeoModelAdmin.
Instead of declaring a form and passing it to the admin class, we can
just set the base widget as a class attribute and get_map_widget()
acts as a factory that returns a widget for the right geometry. With
the FooAdmin example from my initial email:
from django.contrib.gis import admin, forms
class FooAdmin(admin.GeoModelAdmin):
widget = forms.GMapWidget
map_width = 800
map_height = 500
layerswitcher = False
So, no widget declaration and no ModelForm declaration. This is the
way GeoModelAdmin currently works so again, backwards compatibility is
not a problem anymore. There is a bit of redundancy since map
attributes can be set on a widget *and* on an admin class but there
are use cases for both methods.
I have a few extra bullet points.
* extra_js and openlayers_url are not used anymore on the admin class,
should it be documented somewhere or should I still support it and
raise a DeprecationWarning?
* I added support for editing GeometryFields and it works perfectly,
however OpenLayers has some issues with WKT serialization of
GeometryCollections (http://trac.osgeo.org/openlayers/ticket/2706,
http://trac.osgeo.org/openlayers/ticket/2240). Hopefully it will be
fixed in the next OL release, but for now GeometryCollections are not
editable.
* OpenLayers 2.10 breaks MultiPointFields, I can add/save them but
they are not properly parsed and re-displayed on an edit form. It just
works with OpenLayers 2.9. Can anyone confirm this?
* Additionnaly to #9806 (geometry fields), I integrated the fix for
#11634, a minor issue with projections and default_lon / default_lat.
* I deleted the templates in the gis/admin folder, is there a reason
I'm not aware of to keep them?
* ADMIN_MEDIA_PREFIX is used in the widget rendering (for some icons),
it'll need to be replaced by STATIC_URL + 'admin/' when the
deprecation happens.
* I'm not sure the docs explain well the concept of "base widgets" and
"field widgets" and the double inheritance thing when defining custom
widget. If anyone is able to explain it better than I do… (I'm not a
native English speaker as you can imagine :)
I think that's all for today! Sorry for the long email again.
Regards,
Bruno
Hey Bruno,
Thanks for working on this stuff! I'm the author of olwidget and just
wanted to throw my 2c in. I totally support the work you're doing, and
don't currently have time to contribute too much.
I'll just point out some of the biggest areas of difficulty that I've
faced with olwidget:
1. The quality of the out-of-the-box openlayers editing leaves a bit to
be desired (I haven't explored the google maps alternatives much). Some
basic usability stuff like undo and redo are missing; and the toolbar
buttons aren't very obvious or accessible. It's totally non-functional
on touchscreen devices that lack "drag". I don't think olwidget solved
this super well, but I think it did go a few clicks beyond the vanilla
OL way (at least it's got undo and redo).
2. When maps enter user-space, it becomes especially critical that they
have a high degree of customizability. Things like colors, icons, base
layer choices, etc. can be both usability issues and political
statements. But that customizability can present a lot of complexity
and duplication in the Django implementation, as Django is operating as
a proxy to javascript. I'm not sure where the best balance is, but it
would be nice to figure out some way to allow the complexity to be
implemented in javascript directly, to avoid having too many layers of
gloves between the developer and the map.
Even a detail like "default zoom" can be tricky to get right: users
expect a map to be zoomed to the maximum extent if there are geometries,
or a default zoom if there are none, unless the existing geometry is a
single point (which has zero extent).
3. A lot of my development work in olwidget in the last year was to
implement code that allows a single map to edit geometries from multiple
fields (with different fields on different layers), which was something
many olwidget users had requested. This turns out to be fairly tricky
to do, and adds a lot of complexity.... so I don't know if you want to
go there.
4. Final note: maps are for displaying as well as editing. It's possible
that display-oriented maps are out of scope for editable map widgets,
but if you take the time to implement a reasonable way for people to
customize the look and feel of a map, it'd be great to be able to reuse
that in a class for displaying maps.
I'm still going to be maintaining olwidget for a while yet, but if an
implementation in trunk reaches appropriate stability, I may retire it.
Or if someone wants to make a competing or complementary integration of
olwidget into trunk, you have my support (but not a lot of my time right
now unfortunately).
best,
Charlie
Hi Charlie, thanks for joining the discussion!
> Thanks for working on this stuff! I'm the author of olwidget and just
> wanted to throw my 2c in. I totally support the work you're doing, and
> don't currently have time to contribute too much.
>
> I'll just point out some of the biggest areas of difficulty that I've
> faced with olwidget:
>
> 1. The quality of the out-of-the-box openlayers editing leaves a bit to
> be desired (I haven't explored the google maps alternatives much). Some
> basic usability stuff like undo and redo are missing; and the toolbar
> buttons aren't very obvious or accessible. It's totally non-functional
> on touchscreen devices that lack "drag". I don't think olwidget solved
> this super well, but I think it did go a few clicks beyond the vanilla
> OL way (at least it's got undo and redo).
>
> 2. When maps enter user-space, it becomes especially critical that they
> have a high degree of customizability. Things like colors, icons, base
> layer choices, etc. can be both usability issues and political
> statements. But that customizability can present a lot of complexity
> and duplication in the Django implementation, as Django is operating as
> a proxy to javascript. I'm not sure where the best balance is, but it
> would be nice to figure out some way to allow the complexity to be
> implemented in javascript directly, to avoid having too many layers of
> gloves between the developer and the map.
Indeed, that's an area that could be improved quite a lot. The big
advantage of having a template-based map widget is that you can extend
and override different blocks but a lot of work needs to be done to
identify the areas that would need to be customized in the base
template. Right now you can easily add some code *after* all the
rendering is done but regarding the parameters needed to instantiate
the map (projection, default values, colors, layers, controls…), I
can't find an obvious way to specify those parameters in the template
without requiring the end developer to rewrite the whole thing.
Ideally it would be awesome to define a custom widget only by setting
the template_name attribute on the template class and delegate
everything rendering-related to the template. The main difficulty is
to find the right template structure...
> Even a detail like "default zoom" can be tricky to get right: users
> expect a map to be zoomed to the maximum extent if there are geometries,
> or a default zoom if there are none, unless the existing geometry is a
> single point (which has zero extent).
I think the current situation is fine: we have default_zoom and
point_zoom. I can imagine use cases for dynamic values though...
> 3. A lot of my development work in olwidget in the last year was to
> implement code that allows a single map to edit geometries from multiple
> fields (with different fields on different layers), which was something
> many olwidget users had requested. This turns out to be fairly tricky
> to do, and adds a lot of complexity.... so I don't know if you want to
> go there.
My approach was to start with simpler things first :). More seriously,
the way django forms tend to work is to provide one widget per field,
leaving the more specific cases to 3rd party apps / snippets.
> 4. Final note: maps are for displaying as well as editing. It's possible
> that display-oriented maps are out of scope for editable map widgets,
> but if you take the time to implement a reasonable way for people to
> customize the look and feel of a map, it'd be great to be able to reuse
> that in a class for displaying maps.
This is tricky because there is no equivalent for this in the Django
codebase... With the current implementation you could define a widget
with modifiable=False and possibly other display-related options and
render it as a form without any submit button (and passing an initial
value coming from a geographic model).
Or you could have a template tag that renders a widget ({% map_widget
mymodel.geo_field %}) but again for this to be as generic as possible,
you'd need countless arguments and options and a good way to allow
extensibility.
> I'm still going to be maintaining olwidget for a while yet, but if an
> implementation in trunk reaches appropriate stability, I may retire it.
> Or if someone wants to make a competing or complementary integration of
> olwidget into trunk, you have my support (but not a lot of my time right
> now unfortunately).
For now I don't feel like making the port myself, I think olWidget
makes a lot of sense if basic widgets are added to trunk. We'll see
how things evolve :)
Cheers,
Bruno