[1.6.x] Fixed #20998 -- Allow custom (de)serialization for GIS widgets

Thanks Mathieu Leplatre for the report and the initial patch.
Backport of 102f26c92 from master.
This commit is contained in:
Claude Paroz 2013-08-30 10:48:36 +02:00
parent 64383e8349
commit 4e3794dd1f
4 changed files with 53 additions and 23 deletions

View File

@ -22,51 +22,54 @@ class BaseGeometryWidget(Widget):
map_srid = 4326 map_srid = 4326
map_width = 600 map_width = 600
map_height = 400 map_height = 400
display_wkt = False display_raw = False
supports_3d = False supports_3d = False
template_name = '' # set on subclasses template_name = '' # set on subclasses
def __init__(self, attrs=None): def __init__(self, attrs=None):
self.attrs = {} self.attrs = {}
for key in ('geom_type', 'map_srid', 'map_width', 'map_height', 'display_wkt'): for key in ('geom_type', 'map_srid', 'map_width', 'map_height', 'display_raw'):
self.attrs[key] = getattr(self, key) self.attrs[key] = getattr(self, key)
if attrs: if attrs:
self.attrs.update(attrs) self.attrs.update(attrs)
def serialize(self, value):
return value.wkt if value else ''
def deserialize(self, value):
try:
return GEOSGeometry(value)
except (GEOSException, ValueError) as err:
logger.error(
"Error creating geometry from value '%s' (%s)" % (
value, err)
)
return None
def render(self, name, value, attrs=None): def render(self, name, value, attrs=None):
# If a string reaches here (via a validation error on another # If a string reaches here (via a validation error on another
# field) then just reconstruct the Geometry. # field) then just reconstruct the Geometry.
if isinstance(value, six.string_types): if isinstance(value, six.string_types):
try: value = self.deserialize(value)
value = GEOSGeometry(value)
except (GEOSException, ValueError) as err:
logger.error(
"Error creating geometry from value '%s' (%s)" % (
value, err)
)
value = None
wkt = ''
if value: if value:
# Check that srid of value and map match # Check that srid of value and map match
if value.srid != self.map_srid: if value.srid != self.map_srid:
try: try:
ogr = value.ogr ogr = value.ogr
ogr.transform(self.map_srid) ogr.transform(self.map_srid)
wkt = ogr.wkt value = ogr
except gdal.OGRException as err: except gdal.OGRException as err:
logger.error( logger.error(
"Error transforming geometry from srid '%s' to srid '%s' (%s)" % ( "Error transforming geometry from srid '%s' to srid '%s' (%s)" % (
value.srid, self.map_srid, err) value.srid, self.map_srid, err)
) )
else:
wkt = value.wkt
context = self.build_attrs(attrs, context = self.build_attrs(attrs,
name=name, name=name,
module='geodjango_%s' % name.replace('-','_'), # JS-safe module='geodjango_%s' % name.replace('-','_'), # JS-safe
wkt=wkt, serialized=self.serialize(value),
geom_type=gdal.OGRGeomType(self.attrs['geom_type']), geom_type=gdal.OGRGeomType(self.attrs['geom_type']),
STATIC_URL=settings.STATIC_URL, STATIC_URL=settings.STATIC_URL,
LANGUAGE_BIDI=translation.get_language_bidi(), LANGUAGE_BIDI=translation.get_language_bidi(),

View File

@ -2,7 +2,7 @@
#{{ id }}_map { width: {{ map_width }}px; height: {{ map_height }}px; } #{{ id }}_map { width: {{ map_width }}px; height: {{ map_height }}px; }
#{{ id }}_map .aligned label { float: inherit; } #{{ id }}_map .aligned label { float: inherit; }
#{{ id }}_div_map { position: relative; vertical-align: top; float: {{ LANGUAGE_BIDI|yesno:"right,left" }}; } #{{ id }}_div_map { position: relative; vertical-align: top; float: {{ LANGUAGE_BIDI|yesno:"right,left" }}; }
{% if not display_wkt %}#{{ id }} { display: none; }{% endif %} {% if not display_raw %}#{{ id }} { display: none; }{% endif %}
.olControlEditingToolbar .olControlModifyFeatureItemActive { .olControlEditingToolbar .olControlModifyFeatureItemActive {
background-image: url("{{ STATIC_URL }}admin/img/gis/move_vertex_on.png"); background-image: url("{{ STATIC_URL }}admin/img/gis/move_vertex_on.png");
background-repeat: no-repeat; background-repeat: no-repeat;
@ -16,8 +16,8 @@
<div id="{{ id }}_div_map"> <div id="{{ id }}_div_map">
<div id="{{ id }}_map"></div> <div id="{{ id }}_map"></div>
<span class="clear_features"><a href="javascript:{{ module }}.clearFeatures()">Delete all Features</a></span> <span class="clear_features"><a href="javascript:{{ module }}.clearFeatures()">Delete all Features</a></span>
{% if display_wkt %}<p> WKT debugging window:</p>{% endif %} {% if display_raw %}<p>Debugging window (serialized value):</p>{% endif %}
<textarea id="{{ id }}" class="vWKTField required" cols="150" rows="10" name="{{ name }}">{{ wkt }}</textarea> <textarea id="{{ id }}" class="vSerializedField required" cols="150" rows="10" name="{{ name }}">{{ serialized }}</textarea>
<script type="text/javascript"> <script type="text/javascript">
{% block map_options %}var map_options = {};{% endblock %} {% block map_options %}var map_options = {};{% endblock %}
{% block options %}var options = { {% block options %}var options = {

View File

@ -3,9 +3,9 @@ from django.contrib.gis.gdal import HAS_GDAL
from django.contrib.gis.tests.utils import HAS_SPATIALREFSYS from django.contrib.gis.tests.utils import HAS_SPATIALREFSYS
from django.test import SimpleTestCase from django.test import SimpleTestCase
from django.utils import six from django.utils import six
from django.utils.html import escape
from django.utils.unittest import skipUnless from django.utils.unittest import skipUnless
if HAS_SPATIALREFSYS: if HAS_SPATIALREFSYS:
from django.contrib.gis import forms from django.contrib.gis import forms
from django.contrib.gis.geos import GEOSGeometry from django.contrib.gis.geos import GEOSGeometry
@ -241,3 +241,30 @@ class SpecializedFieldTest(SimpleTestCase):
for invalid in [geom for key, geom in self.geometries.items() if key!='geometrycollection']: for invalid in [geom for key, geom in self.geometries.items() if key!='geometrycollection']:
self.assertFalse(GeometryForm(data={'g': invalid.wkt}).is_valid()) self.assertFalse(GeometryForm(data={'g': invalid.wkt}).is_valid())
@skipUnless(HAS_GDAL and HAS_SPATIALREFSYS,
"CustomGeometryWidgetTest needs gdal support and a spatial database")
class CustomGeometryWidgetTest(SimpleTestCase):
class CustomGeometryWidget(forms.BaseGeometryWidget):
template_name = 'gis/openlayers.html'
deserialize_called = 0
def serialize(self, value):
return value.json if value else ''
def deserialize(self, value):
self.deserialize_called += 1
return GEOSGeometry(value)
def test_custom_serialization_widget(self):
class PointForm(forms.Form):
p = forms.PointField(widget=self.CustomGeometryWidget)
point = GEOSGeometry("SRID=4326;POINT(9.052734375 42.451171875)")
form = PointForm(data={'p': point})
self.assertIn(escape(point.json), form.as_p())
self.CustomGeometryWidget.called = 0
widget = form.fields['p'].widget
# Force deserialize use due to a string value
self.assertIn(escape(point.json), widget.render('p', point.json))
self.assertEqual(widget.deserialize_called, 1)

View File

@ -114,11 +114,11 @@ from other Django widget attributes.
SRID code used by the map (default is 4326). SRID code used by the map (default is 4326).
.. attribute:: BaseGeometryWidget.display_wkt .. attribute:: BaseGeometryWidget.display_raw
Boolean value specifying if a textarea input showing the WKT representation Boolean value specifying if a textarea input showing the serialized
of the current geometry is visible, mainly for debugging purposes (default representation of the current geometry is visible, mainly for debugging
is ``False``). purposes (default is ``False``).
.. attribute:: BaseGeometryWidget.supports_3d .. attribute:: BaseGeometryWidget.supports_3d