Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #5472 --Added OpenLayers-based widgets in contrib.gis

Largely inspired from django-floppyforms. Designed to not depend
on OpenLayers at code level.
  • Loading branch information...
commit b16b72d415808073da0418de93bf32f71ead959d 1 parent d4d1145
Claude Paroz authored March 16, 2013
13  django/contrib/gis/db/models/fields.py
@@ -44,6 +44,7 @@ class GeometryField(Field):
44 44
 
45 45
     # The OpenGIS Geometry name.
46 46
     geom_type = 'GEOMETRY'
  47
+    form_class = forms.GeometryField
47 48
 
48 49
     # Geodetic units.
49 50
     geodetic_units = ('Decimal Degree', 'degree')
@@ -201,11 +202,14 @@ def db_type(self, connection):
201 202
         return connection.ops.geo_db_type(self)
202 203
 
203 204
     def formfield(self, **kwargs):
204  
-        defaults = {'form_class' : forms.GeometryField,
  205
+        defaults = {'form_class' : self.form_class,
205 206
                     'geom_type' : self.geom_type,
206 207
                     'srid' : self.srid,
207 208
                     }
208 209
         defaults.update(kwargs)
  210
+        if (self.dim > 2 and not 'widget' in kwargs and
  211
+                not getattr(defaults['form_class'].widget, 'supports_3d', False)):
  212
+            defaults['widget'] = forms.Textarea
209 213
         return super(GeometryField, self).formfield(**defaults)
210 214
 
211 215
     def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False):
@@ -267,28 +271,35 @@ def get_placeholder(self, value, connection):
267 271
 # The OpenGIS Geometry Type Fields
268 272
 class PointField(GeometryField):
269 273
     geom_type = 'POINT'
  274
+    form_class = forms.PointField
270 275
     description = _("Point")
271 276
 
272 277
 class LineStringField(GeometryField):
273 278
     geom_type = 'LINESTRING'
  279
+    form_class = forms.LineStringField
274 280
     description = _("Line string")
275 281
 
276 282
 class PolygonField(GeometryField):
277 283
     geom_type = 'POLYGON'
  284
+    form_class = forms.PolygonField
278 285
     description = _("Polygon")
279 286
 
280 287
 class MultiPointField(GeometryField):
281 288
     geom_type = 'MULTIPOINT'
  289
+    form_class = forms.MultiPointField
282 290
     description = _("Multi-point")
283 291
 
284 292
 class MultiLineStringField(GeometryField):
285 293
     geom_type = 'MULTILINESTRING'
  294
+    form_class = forms.MultiLineStringField
286 295
     description = _("Multi-line string")
287 296
 
288 297
 class MultiPolygonField(GeometryField):
289 298
     geom_type = 'MULTIPOLYGON'
  299
+    form_class = forms.MultiPolygonField
290 300
     description = _("Multi polygon")
291 301
 
292 302
 class GeometryCollectionField(GeometryField):
293 303
     geom_type = 'GEOMETRYCOLLECTION'
  304
+    form_class = forms.GeometryCollectionField
294 305
     description = _("Geometry collection")
5  django/contrib/gis/forms/__init__.py
... ...
@@ -1,2 +1,5 @@
1 1
 from django.forms import *
2  
-from django.contrib.gis.forms.fields import GeometryField
  2
+from .fields import (GeometryField, GeometryCollectionField, PointField,
  3
+    MultiPointField, LineStringField, MultiLineStringField, PolygonField,
  4
+    MultiPolygonField)
  5
+from .widgets import BaseGeometryWidget, OpenLayersWidget, OSMWidget
35  django/contrib/gis/forms/fields.py
@@ -9,6 +9,7 @@
9 9
 # While this couples the geographic forms to the GEOS library,
10 10
 # it decouples from database (by not importing SpatialBackend).
11 11
 from django.contrib.gis.geos import GEOSException, GEOSGeometry, fromstr
  12
+from .widgets import OpenLayersWidget
12 13
 
13 14
 
14 15
 class GeometryField(forms.Field):
@@ -17,7 +18,8 @@ class GeometryField(forms.Field):
17 18
     accepted by GEOSGeometry is accepted by this form.  By default,
18 19
     this includes WKT, HEXEWKB, WKB (in a buffer), and GeoJSON.
19 20
     """
20  
-    widget = forms.Textarea
  21
+    widget = OpenLayersWidget
  22
+    geom_type = 'GEOMETRY'
21 23
 
22 24
     default_error_messages = {
23 25
         'required' : _('No geometry value provided.'),
@@ -31,12 +33,13 @@ def __init__(self, **kwargs):
31 33
         # Pop out attributes from the database field, or use sensible
32 34
         # defaults (e.g., allow None).
33 35
         self.srid = kwargs.pop('srid', None)
34  
-        self.geom_type = kwargs.pop('geom_type', 'GEOMETRY')
  36
+        self.geom_type = kwargs.pop('geom_type', self.geom_type)
35 37
         if 'null' in kwargs:
36 38
             kwargs.pop('null', True)
37 39
             warnings.warn("Passing 'null' keyword argument to GeometryField is deprecated.",
38 40
                 DeprecationWarning, stacklevel=2)
39 41
         super(GeometryField, self).__init__(**kwargs)
  42
+        self.widget.attrs['geom_type'] = self.geom_type
40 43
 
41 44
     def to_python(self, value):
42 45
         """
@@ -98,3 +101,31 @@ def _has_changed(self, initial, data):
98 101
         else:
99 102
             # Check for change of state of existence
100 103
             return bool(initial) != bool(data)
  104
+
  105
+
  106
+class GeometryCollectionField(GeometryField):
  107
+    geom_type = 'GEOMETRYCOLLECTION'
  108
+
  109
+
  110
+class PointField(GeometryField):
  111
+    geom_type = 'POINT'
  112
+
  113
+
  114
+class MultiPointField(GeometryField):
  115
+    geom_type = 'MULTIPOINT'
  116
+
  117
+
  118
+class LineStringField(GeometryField):
  119
+    geom_type = 'LINESTRING'
  120
+
  121
+
  122
+class MultiLineStringField(GeometryField):
  123
+    geom_type = 'MULTILINESTRING'
  124
+
  125
+
  126
+class PolygonField(GeometryField):
  127
+    geom_type = 'POLYGON'
  128
+
  129
+
  130
+class MultiPolygonField(GeometryField):
  131
+    geom_type = 'MULTIPOLYGON'
112  django/contrib/gis/forms/widgets.py
... ...
@@ -0,0 +1,112 @@
  1
+from __future__ import unicode_literals
  2
+
  3
+import logging
  4
+
  5
+from django.conf import settings
  6
+from django.contrib.gis import gdal
  7
+from django.contrib.gis.geos import GEOSGeometry, GEOSException
  8
+from django.forms.widgets import Widget
  9
+from django.template import loader
  10
+from django.utils import six
  11
+from django.utils import translation
  12
+
  13
+logger = logging.getLogger('django.contrib.gis')
  14
+
  15
+
  16
+class BaseGeometryWidget(Widget):
  17
+    """
  18
+    The base class for rich geometry widgets.
  19
+    Renders a map using the WKT of the geometry.
  20
+    """
  21
+    geom_type = 'GEOMETRY'
  22
+    map_srid = 4326
  23
+    map_width = 600
  24
+    map_height = 400
  25
+    display_wkt = False
  26
+
  27
+    supports_3d = False
  28
+    template_name = ''  # set on subclasses
  29
+
  30
+    def __init__(self, attrs=None):
  31
+        self.attrs = {}
  32
+        for key in ('geom_type', 'map_srid', 'map_width', 'map_height', 'display_wkt'):
  33
+            self.attrs[key] = getattr(self, key)
  34
+        if attrs:
  35
+            self.attrs.update(attrs)
  36
+
  37
+    def render(self, name, value, attrs=None):
  38
+        # If a string reaches here (via a validation error on another
  39
+        # field) then just reconstruct the Geometry.
  40
+        if isinstance(value, six.string_types):
  41
+            try:
  42
+                value = GEOSGeometry(value)
  43
+            except (GEOSException, ValueError) as err:
  44
+                logger.error(
  45
+                    "Error creating geometry from value '%s' (%s)" % (
  46
+                    value, err)
  47
+                )
  48
+                value = None
  49
+
  50
+        wkt = ''
  51
+        if value:
  52
+            # Check that srid of value and map match
  53
+            if value.srid != self.map_srid:
  54
+                try:
  55
+                    ogr = value.ogr
  56
+                    ogr.transform(self.map_srid)
  57
+                    wkt = ogr.wkt
  58
+                except gdal.OGRException as err:
  59
+                    logger.error(
  60
+                        "Error transforming geometry from srid '%s' to srid '%s' (%s)" % (
  61
+                        value.srid, self.map_srid, err)
  62
+                    )
  63
+            else:
  64
+                wkt = value.wkt
  65
+
  66
+        context = self.build_attrs(attrs,
  67
+            name=name,
  68
+            module='geodjango_%s' % name.replace('-','_'),  # JS-safe
  69
+            wkt=wkt,
  70
+            geom_type=gdal.OGRGeomType(self.attrs['geom_type']),
  71
+            STATIC_URL=settings.STATIC_URL,
  72
+            LANGUAGE_BIDI=translation.get_language_bidi(),
  73
+        )
  74
+        return loader.render_to_string(self.template_name, context)
  75
+
  76
+
  77
+class OpenLayersWidget(BaseGeometryWidget):
  78
+    template_name = 'gis/openlayers.html'
  79
+    class Media:
  80
+        js = (
  81
+            'http://openlayers.org/api/2.11/OpenLayers.js',
  82
+            'gis/js/OLMapWidget.js',
  83
+        )
  84
+
  85
+
  86
+class OSMWidget(BaseGeometryWidget):
  87
+    """
  88
+    An OpenLayers/OpenStreetMap-based widget.
  89
+    """
  90
+    template_name = 'gis/openlayers-osm.html'
  91
+    default_lon = 5
  92
+    default_lat = 47
  93
+
  94
+    class Media:
  95
+        js = (
  96
+            'http://openlayers.org/api/2.11/OpenLayers.js',
  97
+            'http://www.openstreetmap.org/openlayers/OpenStreetMap.js',
  98
+            'gis/js/OLMapWidget.js',
  99
+        )
  100
+
  101
+    @property
  102
+    def map_srid(self):
  103
+        # Use the official spherical mercator projection SRID on versions
  104
+        # of GDAL that support it; otherwise, fallback to 900913.
  105
+        if gdal.HAS_GDAL and gdal.GDAL_VERSION >= (1, 7):
  106
+            return 3857
  107
+        else:
  108
+            return 900913
  109
+
  110
+    def render(self, name, value, attrs=None):
  111
+        return super(self, OSMWidget).render(name, value,
  112
+            {'default_lon': self.default_lon, 'default_lat': self.default_lat})
371  django/contrib/gis/static/gis/js/OLMapWidget.js
... ...
@@ -0,0 +1,371 @@
  1
+(function() {
  2
+/**
  3
+ * Transforms an array of features to a single feature with the merged
  4
+ * geometry of geom_type
  5
+ */
  6
+OpenLayers.Util.properFeatures = function(features, geom_type) {
  7
+    if (features.constructor == Array) {
  8
+        var geoms = [];
  9
+        for (var i=0; i<features.length; i++) {
  10
+            geoms.push(features[i].geometry);
  11
+        }
  12
+        var geom = new geom_type(geoms);
  13
+        features = new OpenLayers.Feature.Vector(geom);
  14
+    }
  15
+    return features;
  16
+}
  17
+
  18
+/**
  19
+ * @requires OpenLayers/Format/WKT.js
  20
+ */
  21
+
  22
+/**
  23
+ * Class: OpenLayers.Format.DjangoWKT
  24
+ * Class for reading Well-Known Text, with workarounds to successfully parse
  25
+ * geometries and collections as returnes by django.contrib.gis.geos.
  26
+ *
  27
+ * Inherits from:
  28
+ *  - <OpenLayers.Format.WKT>
  29
+ */
  30
+
  31
+OpenLayers.Format.DjangoWKT = OpenLayers.Class(OpenLayers.Format.WKT, {
  32
+    initialize: function(options) {
  33
+        OpenLayers.Format.WKT.prototype.initialize.apply(this, [options]);
  34
+        this.regExes.justComma = /\s*,\s*/;
  35
+    },
  36
+
  37
+    parse: {
  38
+        'point': function(str) {
  39
+            var coords = OpenLayers.String.trim(str).split(this.regExes.spaces);
  40
+            return new OpenLayers.Feature.Vector(
  41
+                new OpenLayers.Geometry.Point(coords[0], coords[1])
  42
+            );
  43
+        },
  44
+
  45
+        'multipoint': function(str) {
  46
+            var point;
  47
+            var points = OpenLayers.String.trim(str).split(this.regExes.justComma);
  48
+            var components = [];
  49
+            for(var i=0, len=points.length; i<len; ++i) {
  50
+                point = points[i].replace(this.regExes.trimParens, '$1');
  51
+                components.push(this.parse.point.apply(this, [point]).geometry);
  52
+            }
  53
+            return new OpenLayers.Feature.Vector(
  54
+                new OpenLayers.Geometry.MultiPoint(components)
  55
+            );
  56
+        },
  57
+
  58
+        'linestring': function(str) {
  59
+            var points = OpenLayers.String.trim(str).split(',');
  60
+            var components = [];
  61
+            for(var i=0, len=points.length; i<len; ++i) {
  62
+                components.push(this.parse.point.apply(this, [points[i]]).geometry);
  63
+            }
  64
+            return new OpenLayers.Feature.Vector(
  65
+                new OpenLayers.Geometry.LineString(components)
  66
+            );
  67
+        },
  68
+
  69
+        'multilinestring': function(str) {
  70
+            var line;
  71
+            var lines = OpenLayers.String.trim(str).split(this.regExes.parenComma);
  72
+            var components = [];
  73
+            for(var i=0, len=lines.length; i<len; ++i) {
  74
+                line = lines[i].replace(this.regExes.trimParens, '$1');
  75
+                components.push(this.parse.linestring.apply(this, [line]).geometry);
  76
+            }
  77
+            return new OpenLayers.Feature.Vector(
  78
+                new OpenLayers.Geometry.MultiLineString(components)
  79
+            );
  80
+        },
  81
+
  82
+        'polygon': function(str) {
  83
+            var ring, linestring, linearring;
  84
+            var rings = OpenLayers.String.trim(str).split(this.regExes.parenComma);
  85
+            var components = [];
  86
+            for(var i=0, len=rings.length; i<len; ++i) {
  87
+                ring = rings[i].replace(this.regExes.trimParens, '$1');
  88
+                linestring = this.parse.linestring.apply(this, [ring]).geometry;
  89
+                linearring = new OpenLayers.Geometry.LinearRing(linestring.components);
  90
+                components.push(linearring);
  91
+            }
  92
+            return new OpenLayers.Feature.Vector(
  93
+                new OpenLayers.Geometry.Polygon(components)
  94
+            );
  95
+        },
  96
+
  97
+        'multipolygon': function(str) {
  98
+            var polygon;
  99
+            var polygons = OpenLayers.String.trim(str).split(this.regExes.doubleParenComma);
  100
+            var components = [];
  101
+            for(var i=0, len=polygons.length; i<len; ++i) {
  102
+                polygon = polygons[i].replace(this.regExes.trimParens, '$1');
  103
+                components.push(this.parse.polygon.apply(this, [polygon]).geometry);
  104
+            }
  105
+            return new OpenLayers.Feature.Vector(
  106
+                new OpenLayers.Geometry.MultiPolygon(components)
  107
+            );
  108
+        },
  109
+
  110
+        'geometrycollection': function(str) {
  111
+            // separate components of the collection with |
  112
+            str = str.replace(/,\s*([A-Za-z])/g, '|$1');
  113
+            var wktArray = OpenLayers.String.trim(str).split('|');
  114
+            var components = [];
  115
+            for(var i=0, len=wktArray.length; i<len; ++i) {
  116
+                components.push(OpenLayers.Format.WKT.prototype.read.apply(this,[wktArray[i]]));
  117
+            }
  118
+            return components;
  119
+        }
  120
+    },
  121
+
  122
+    extractGeometry: function(geometry) {
  123
+        var type = geometry.CLASS_NAME.split('.')[2].toLowerCase();
  124
+        if (!this.extract[type]) {
  125
+            return null;
  126
+        }
  127
+        if (this.internalProjection && this.externalProjection) {
  128
+            geometry = geometry.clone();
  129
+            geometry.transform(this.internalProjection, this.externalProjection);
  130
+        }
  131
+        var wktType = type == 'collection' ? 'GEOMETRYCOLLECTION' : type.toUpperCase();
  132
+        var data = wktType + '(' + this.extract[type].apply(this, [geometry]) + ')';
  133
+        return data;
  134
+    },
  135
+
  136
+    /**
  137
+     * Patched write: successfully writes WKT for geometries and
  138
+     * geometrycollections.
  139
+     */
  140
+    write: function(features) {
  141
+        var collection, geometry, type, data, isCollection;
  142
+        isCollection = features.geometry.CLASS_NAME == "OpenLayers.Geometry.Collection";
  143
+        var pieces = [];
  144
+        if (isCollection) {
  145
+            collection = features.geometry.components;
  146
+            pieces.push('GEOMETRYCOLLECTION(');
  147
+            for (var i=0, len=collection.length; i<len; ++i) {
  148
+                if (i>0) {
  149
+                    pieces.push(',');
  150
+                }
  151
+                pieces.push(this.extractGeometry(collection[i]));
  152
+            }
  153
+            pieces.push(')');
  154
+        } else {
  155
+            pieces.push(this.extractGeometry(features.geometry));
  156
+        }
  157
+        return pieces.join('');
  158
+    },
  159
+
  160
+    CLASS_NAME: "OpenLayers.Format.DjangoWKT"
  161
+});
  162
+
  163
+function MapWidget(options) {
  164
+    this.map = null;
  165
+    this.controls = null;
  166
+    this.panel = null;
  167
+    this.layers = {};
  168
+    this.wkt_f = new OpenLayers.Format.DjangoWKT();
  169
+
  170
+    // Mapping from OGRGeomType name to OpenLayers.Geometry name
  171
+    if (options['geom_name'] == 'Unknown') options['geom_type'] = OpenLayers.Geometry;
  172
+    else if (options['geom_name'] == 'GeometryCollection') options['geom_type'] = OpenLayers.Geometry.Collection;
  173
+    else options['geom_type'] = eval('OpenLayers.Geometry' + options['geom_name']);
  174
+
  175
+    // Default options
  176
+    this.options = {
  177
+        color: 'ee9900',
  178
+        default_lat: 0,
  179
+        default_lon: 0,
  180
+        default_zoom: 4,
  181
+        is_collection: options['geom_type'] instanceof OpenLayers.Geometry.Collection,
  182
+        layerswitcher: false,
  183
+        map_options: {},
  184
+        map_srid: 4326,
  185
+        modifiable: true,
  186
+        mouse_position: false,
  187
+        opacity: 0.4,
  188
+        point_zoom: 12,
  189
+        scale_text: false,
  190
+        scrollable: true
  191
+    };
  192
+
  193
+    // Altering using user-provied options
  194
+    for (var property in options) {
  195
+        if (options.hasOwnProperty(property)) {
  196
+            this.options[property] = options[property];
  197
+        }
  198
+    }
  199
+
  200
+    this.map = new OpenLayers.Map(this.options.map_id, this.options.map_options);
  201
+    if (this.options.base_layer) this.layers.base = this.options.base_layer;
  202
+    else this.layers.base = new OpenLayers.Layer.WMS('OpenLayers WMS', 'http://vmap0.tiles.osgeo.org/wms/vmap0', {layers: 'basic'});
  203
+    this.map.addLayer(this.layers.base);
  204
+
  205
+    var defaults_style = {
  206
+        'fillColor': '#' + this.options.color,
  207
+        'fillOpacity': this.options.opacity,
  208
+        'strokeColor': '#' + this.options.color,
  209
+    };
  210
+    if (this.options.geom_name == 'LineString') {
  211
+        defaults_style['strokeWidth'] = 3;
  212
+    }
  213
+    var styleMap = new OpenLayers.StyleMap({'default': OpenLayers.Util.applyDefaults(defaults_style, OpenLayers.Feature.Vector.style['default'])});
  214
+    this.layers.vector = new OpenLayers.Layer.Vector(" " + this.options.name, {styleMap: styleMap});
  215
+    this.map.addLayer(this.layers.vector);
  216
+    wkt = document.getElementById(this.options.id).value;
  217
+    if (wkt) {
  218
+        var feat = OpenLayers.Util.properFeatures(this.read_wkt(wkt), this.options.geom_type);
  219
+        this.write_wkt(feat);
  220
+        if (this.options.is_collection) {
  221
+            for (var i=0; i<this.num_geom; i++) {
  222
+                this.layers.vector.addFeatures([new OpenLayers.Feature.Vector(feat.geometry.components[i].clone())]);
  223
+            }
  224
+        } else {
  225
+            this.layers.vector.addFeatures([feat]);
  226
+        }
  227
+        this.map.zoomToExtent(feat.geometry.getBounds());
  228
+        if (this.options.geom_name == 'Point') {
  229
+            this.map.zoomTo(this.options.point_zoom);
  230
+        }
  231
+    } else {
  232
+        this.map.setCenter(this.defaultCenter(), this.options.default_zoom);
  233
+    }
  234
+    this.layers.vector.events.on({'featuremodified': this.modify_wkt, scope: this});
  235
+    this.layers.vector.events.on({'featureadded': this.add_wkt, scope: this});
  236
+
  237
+    this.getControls(this.layers.vector);
  238
+    this.panel.addControls(this.controls);
  239
+    this.map.addControl(this.panel);
  240
+    this.addSelectControl();
  241
+
  242
+    if (this.options.mouse_position) {
  243
+        this.map.addControl(new OpenLayers.Control.MousePosition());
  244
+    }
  245
+    if (this.options.scale_text) {
  246
+        this.map.addControl(new OpenLayers.Control.Scale());
  247
+    }
  248
+    if (this.options.layerswitcher) {
  249
+        this.map.addControl(new OpenLayers.Control.LayerSwitcher());
  250
+    }
  251
+    if (!this.options.scrollable) {
  252
+        this.map.getControlsByClass('OpenLayers.Control.Navigation')[0].disableZoomWheel();
  253
+    }
  254
+    if (wkt) {
  255
+        if (this.options.modifiable) {
  256
+            this.enableEditing();
  257
+        }
  258
+    } else {
  259
+        this.enableDrawing();
  260
+    }
  261
+}
  262
+
  263
+MapWidget.prototype.get_ewkt = function(feat) {
  264
+    return "SRID=" + this.options.map_srid + ";" + this.wkt_f.write(feat);
  265
+};
  266
+
  267
+MapWidget.prototype.read_wkt = function(wkt) {
  268
+    var prefix = 'SRID=' + this.options.map_srid + ';'
  269
+    if (wkt.indexOf(prefix) === 0) {
  270
+        wkt = wkt.slice(prefix.length);
  271
+    }
  272
+    return this.wkt_f.read(wkt);
  273
+};
  274
+
  275
+MapWidget.prototype.write_wkt = function(feat) {
  276
+    feat = OpenLayers.Util.properFeatures(feat, this.options.geom_type);
  277
+    if (this.options.is_collection) {
  278
+        this.num_geom = feat.geometry.components.length;
  279
+    } else {
  280
+        this.num_geom = 1;
  281
+    }
  282
+    document.getElementById(this.options.id).value = this.get_ewkt(feat);
  283
+};
  284
+
  285
+MapWidget.prototype.add_wkt = function(event) {
  286
+    if (this.options.is_collection) {
  287
+        var feat = new OpenLayers.Feature.Vector(new this.options.geom_type());
  288
+        for (var i=0; i<this.layers.vector.features.length; i++) {
  289
+            feat.geometry.addComponents([this.layers.vector.features[i].geometry]);
  290
+        }
  291
+        this.write_wkt(feat);
  292
+    } else {
  293
+        if (this.layers.vector.features.length > 1) {
  294
+            old_feats = [this.layers.vector.features[0]];
  295
+            this.layers.vector.removeFeatures(old_feats);
  296
+            this.layers.vector.destroyFeatures(old_feats);
  297
+        }
  298
+        this.write_wkt(event.feature);
  299
+    }
  300
+};
  301
+
  302
+MapWidget.prototype.modify_wkt = function(event) {
  303
+    if (this.options.is_collection) {
  304
+        if (this.options.geom_name == 'MultiPoint') {
  305
+            this.add_wkt(event);
  306
+            return;
  307
+        } else {
  308
+            var feat = new OpenLayers.Feature.Vector(new this.options.geom_type());
  309
+            for (var i=0; i<this.num_geom; i++) {
  310
+                feat.geometry.addComponents([this.layers.vector.features[i].geometry]);
  311
+            }
  312
+            this.write_wkt(feat);
  313
+        }
  314
+    } else {
  315
+        this.write_wkt(event.feature);
  316
+    }
  317
+};
  318
+
  319
+MapWidget.prototype.deleteFeatures = function() {
  320
+    this.layers.vector.removeFeatures(this.layers.vector.features);
  321
+    this.layers.vector.destroyFeatures();
  322
+};
  323
+
  324
+MapWidget.prototype.clearFeatures = function() {
  325
+    this.deleteFeatures();
  326
+    document.getElementById(this.options.id).value = '';
  327
+    this.map.setCenter(this.defaultCenter(), this.options.default_zoom);
  328
+};
  329
+
  330
+MapWidget.prototype.defaultCenter = function() {
  331
+    var center = new OpenLayers.LonLat(this.options.default_lon, this.options.default_lat);
  332
+    if (this.options.map_srid) {
  333
+        return center.transform(new OpenLayers.Projection("EPSG:4326"), this.map.getProjectionObject());
  334
+    }
  335
+    return center;
  336
+};
  337
+
  338
+MapWidget.prototype.addSelectControl = function() {
  339
+    var select = new OpenLayers.Control.SelectFeature(this.layers.vector, {'toggle': true, 'clickout': true});
  340
+    this.map.addControl(select);
  341
+    select.activate();
  342
+};
  343
+
  344
+MapWidget.prototype.enableDrawing = function () {
  345
+    this.map.getControlsByClass('OpenLayers.Control.DrawFeature')[0].activate();
  346
+};
  347
+
  348
+MapWidget.prototype.enableEditing = function () {
  349
+    this.map.getControlsByClass('OpenLayers.Control.ModifyFeature')[0].activate();
  350
+};
  351
+
  352
+MapWidget.prototype.getControls = function(layer) {
  353
+    this.panel = new OpenLayers.Control.Panel({'displayClass': 'olControlEditingToolbar'});
  354
+    this.controls = [new OpenLayers.Control.Navigation()];
  355
+    if (!this.options.modifiable && layer.features.length)
  356
+        return;
  357
+    if (this.options.geom_name == 'LineString' || this.options.geom_name == 'Unknown') {
  358
+        this.controls.push(new OpenLayers.Control.DrawFeature(layer, OpenLayers.Handler.Path, {'displayClass': 'olControlDrawFeaturePath'}));
  359
+    }
  360
+    if (this.options.geom_name == 'Polygon' || this.options.geom_name == 'Unknown') {
  361
+        this.controls.push(new OpenLayers.Control.DrawFeature(layer, OpenLayers.Handler.Polygon, {'displayClass': 'olControlDrawFeaturePolygon'}));
  362
+    }
  363
+    if (this.options.geom_name == 'Point' || this.options.geom_name == 'Unknown') {
  364
+        this.controls.push(new OpenLayers.Control.DrawFeature(layer, OpenLayers.Handler.Point, {'displayClass': 'olControlDrawFeaturePoint'}));
  365
+    }
  366
+    if (this.options.modifiable) {
  367
+        this.controls.push(new OpenLayers.Control.ModifyFeature(layer, {'displayClass': 'olControlModifyFeature'}));
  368
+    }
  369
+};
  370
+window.MapWidget = MapWidget;
  371
+})();
17  django/contrib/gis/templates/gis/openlayers-osm.html
... ...
@@ -0,0 +1,17 @@
  1
+{% extends "gis/openlayers.html" %}
  2
+{% load l10n %}
  3
+
  4
+{% block map_options %}var map_options = {
  5
+    maxExtend: new OpenLayers.Bounds(-20037508,-20037508,20037508,20037508),
  6
+    maxResolution: 156543.0339,
  7
+    numZoomLevels: 20,
  8
+    units: 'm'
  9
+};{% endblock %}
  10
+
  11
+{% block options %}{{ block.super }}
  12
+options['scale_text'] = true;
  13
+options['mouse_position'] = true;
  14
+options['default_lon'] = {{ default_lon|unlocalize }};
  15
+options['default_lat'] = {{ default_lat|unlocalize }};
  16
+options['base_layer'] = new OpenLayers.Layer.OSM.Mapnik("OpenStreetMap (Mapnik)");
  17
+{% endblock %}
34  django/contrib/gis/templates/gis/openlayers.html
... ...
@@ -0,0 +1,34 @@
  1
+<style type="text/css">{% block map_css %}
  2
+    #{{ id }}_map { width: {{ map_width }}px; height: {{ map_height }}px; }
  3
+    #{{ id }}_map .aligned label { float: inherit; }
  4
+    #{{ id }}_div_map { position: relative; vertical-align: top; float: {{ LANGUAGE_BIDI|yesno:"right,left" }}; }
  5
+    {% if not display_wkt %}#{{ id }} { display: none; }{% endif %}
  6
+    .olControlEditingToolbar .olControlModifyFeatureItemActive {
  7
+        background-image: url("{{ STATIC_URL }}admin/img/gis/move_vertex_on.png");
  8
+        background-repeat: no-repeat;
  9
+    }
  10
+    .olControlEditingToolbar .olControlModifyFeatureItemInactive {
  11
+        background-image: url("{{ STATIC_URL }}admin/img/gis/move_vertex_off.png");
  12
+        background-repeat: no-repeat;
  13
+    }{% endblock %}
  14
+</style>
  15
+
  16
+<div id="{{ id }}_div_map">
  17
+    <div id="{{ id }}_map"></div>
  18
+    <span class="clear_features"><a href="javascript:{{ module }}.clearFeatures()">Delete all Features</a></span>
  19
+    {% if display_wkt %}<p> WKT debugging window:</p>{% endif %}
  20
+    <textarea id="{{ id }}" class="vWKTField required" cols="150" rows="10" name="{{ name }}">{{ wkt }}</textarea>
  21
+    <script type="text/javascript">
  22
+        {% block map_options %}var map_options = {};{% endblock %}
  23
+        {% block options %}var options = {
  24
+            geom_name: '{{ geom_type }}',
  25
+            id: '{{ id }}',
  26
+            map_id: '{{ id }}_map',
  27
+            map_options: map_options,
  28
+            map_srid: {{ map_srid }},
  29
+            name: '{{ name }}'
  30
+        };
  31
+        {% endblock %}
  32
+        var {{ module }} = new MapWidget(options);
  33
+    </script>
  34
+</div>
189  django/contrib/gis/tests/test_geoforms.py
... ...
@@ -1,24 +1,25 @@
1 1
 from django.forms import ValidationError
2 2
 from django.contrib.gis.gdal import HAS_GDAL
3 3
 from django.contrib.gis.tests.utils import HAS_SPATIALREFSYS
  4
+from django.test import SimpleTestCase
4 5
 from django.utils import six
5  
-from django.utils import unittest
  6
+from django.utils.unittest import skipUnless
6 7
 
7 8
 
8 9
 if HAS_SPATIALREFSYS:
9 10
     from django.contrib.gis import forms
10 11
     from django.contrib.gis.geos import GEOSGeometry
11 12
 
12  
-@unittest.skipUnless(HAS_GDAL and HAS_SPATIALREFSYS, "GeometryFieldTest needs gdal support and a spatial database")
13  
-class GeometryFieldTest(unittest.TestCase):
  13
+@skipUnless(HAS_GDAL and HAS_SPATIALREFSYS, "GeometryFieldTest needs gdal support and a spatial database")
  14
+class GeometryFieldTest(SimpleTestCase):
14 15
 
15  
-    def test00_init(self):
  16
+    def test_init(self):
16 17
         "Testing GeometryField initialization with defaults."
17 18
         fld = forms.GeometryField()
18 19
         for bad_default in ('blah', 3, 'FoO', None, 0):
19 20
             self.assertRaises(ValidationError, fld.clean, bad_default)
20 21
 
21  
-    def test01_srid(self):
  22
+    def test_srid(self):
22 23
         "Testing GeometryField with a SRID set."
23 24
         # Input that doesn't specify the SRID is assumed to be in the SRID
24 25
         # of the input field.
@@ -34,7 +35,7 @@ def test01_srid(self):
34 35
         cleaned_geom = fld.clean('SRID=4326;POINT (-95.363151 29.763374)')
35 36
         self.assertTrue(xform_geom.equals_exact(cleaned_geom, tol))
36 37
 
37  
-    def test02_null(self):
  38
+    def test_null(self):
38 39
         "Testing GeometryField's handling of null (None) geometries."
39 40
         # Form fields, by default, are required (`required=True`)
40 41
         fld = forms.GeometryField()
@@ -46,7 +47,7 @@ def test02_null(self):
46 47
         fld = forms.GeometryField(required=False)
47 48
         self.assertIsNone(fld.clean(None))
48 49
 
49  
-    def test03_geom_type(self):
  50
+    def test_geom_type(self):
50 51
         "Testing GeometryField's handling of different geometry types."
51 52
         # By default, all geometry types are allowed.
52 53
         fld = forms.GeometryField()
@@ -60,7 +61,7 @@ def test03_geom_type(self):
60 61
         # but rejected by `clean`
61 62
         self.assertRaises(forms.ValidationError, pnt_fld.clean, 'LINESTRING(0 0, 1 1)')
62 63
 
63  
-    def test04_to_python(self):
  64
+    def test_to_python(self):
64 65
         """
65 66
         Testing to_python returns a correct GEOSGeometry object or
66 67
         a ValidationError
@@ -74,13 +75,169 @@ def test04_to_python(self):
74 75
             self.assertRaises(forms.ValidationError, fld.to_python, wkt)
75 76
 
76 77
 
77  
-def suite():
78  
-    s = unittest.TestSuite()
79  
-    s.addTest(unittest.makeSuite(GeometryFieldTest))
80  
-    return s
  78
+@skipUnless(HAS_GDAL and HAS_SPATIALREFSYS,
  79
+    "SpecializedFieldTest needs gdal support and a spatial database")
  80
+class SpecializedFieldTest(SimpleTestCase):
  81
+    def setUp(self):
  82
+        self.geometries = {
  83
+            'point': GEOSGeometry("SRID=4326;POINT(9.052734375 42.451171875)"),
  84
+            'multipoint': GEOSGeometry("SRID=4326;MULTIPOINT("
  85
+                                       "(13.18634033203125 14.504356384277344),"
  86
+                                       "(13.207969665527 14.490966796875),"
  87
+                                       "(13.177070617675 14.454917907714))"),
  88
+            'linestring': GEOSGeometry("SRID=4326;LINESTRING("
  89
+                                       "-8.26171875 -0.52734375,"
  90
+                                       "-7.734375 4.21875,"
  91
+                                       "6.85546875 3.779296875,"
  92
+                                       "5.44921875 -3.515625)"),
  93
+            'multilinestring': GEOSGeometry("SRID=4326;MULTILINESTRING("
  94
+                                            "(-16.435546875 -2.98828125,"
  95
+                                            "-17.2265625 2.98828125,"
  96
+                                            "-0.703125 3.515625,"
  97
+                                            "-1.494140625 -3.33984375),"
  98
+                                            "(-8.0859375 -5.9765625,"
  99
+                                            "8.525390625 -8.7890625,"
  100
+                                            "12.392578125 -0.87890625,"
  101
+                                            "10.01953125 7.646484375))"),
  102
+            'polygon': GEOSGeometry("SRID=4326;POLYGON("
  103
+                                    "(-1.669921875 6.240234375,"
  104
+                                    "-3.8671875 -0.615234375,"
  105
+                                    "5.9765625 -3.955078125,"
  106
+                                    "18.193359375 3.955078125,"
  107
+                                    "9.84375 9.4921875,"
  108
+                                    "-1.669921875 6.240234375))"),
  109
+            'multipolygon': GEOSGeometry("SRID=4326;MULTIPOLYGON("
  110
+                                         "((-17.578125 13.095703125,"
  111
+                                         "-17.2265625 10.8984375,"
  112
+                                         "-13.974609375 10.1953125,"
  113
+                                         "-13.359375 12.744140625,"
  114
+                                         "-15.732421875 13.7109375,"
  115
+                                         "-17.578125 13.095703125)),"
  116
+                                         "((-8.525390625 5.537109375,"
  117
+                                         "-8.876953125 2.548828125,"
  118
+                                         "-5.888671875 1.93359375,"
  119
+                                         "-5.09765625 4.21875,"
  120
+                                         "-6.064453125 6.240234375,"
  121
+                                         "-8.525390625 5.537109375)))"),
  122
+            'geometrycollection': GEOSGeometry("SRID=4326;GEOMETRYCOLLECTION("
  123
+                                               "POINT(5.625 -0.263671875),"
  124
+                                               "POINT(6.767578125 -3.603515625),"
  125
+                                               "POINT(8.525390625 0.087890625),"
  126
+                                               "POINT(8.0859375 -2.13134765625),"
  127
+                                               "LINESTRING("
  128
+                                               "6.273193359375 -1.175537109375,"
  129
+                                               "5.77880859375 -1.812744140625,"
  130
+                                               "7.27294921875 -2.230224609375,"
  131
+                                               "7.657470703125 -1.25244140625))"),
  132
+        }
81 133
 
82  
-def run(verbosity=2):
83  
-    unittest.TextTestRunner(verbosity=verbosity).run(suite())
  134
+    def assertMapWidget(self, form_instance):
  135
+        """
  136
+        Make sure the MapWidget js is passed in the form media and a MapWidget
  137
+        is actually created
  138
+        """
  139
+        self.assertTrue(form_instance.is_valid())
  140
+        rendered = form_instance.as_p()
  141
+        self.assertIn('new MapWidget(options);', rendered)
  142
+        self.assertIn('gis/js/OLMapWidget.js', str(form_instance.media))
  143
+
  144
+    def assertTextarea(self, geom, rendered):
  145
+        """Makes sure the wkt and a textarea are in the content"""
  146
+        
  147
+        self.assertIn('<textarea ', rendered)
  148
+        self.assertIn('required', rendered)
  149
+        self.assertIn(geom.wkt, rendered)
  150
+
  151
+    def test_pointfield(self):
  152
+        class PointForm(forms.Form):
  153
+            p = forms.PointField()
  154
+
  155
+        geom = self.geometries['point']
  156
+        form = PointForm(data={'p': geom})
  157
+        self.assertTextarea(geom, form.as_p())
  158
+        self.assertMapWidget(form)
  159
+        self.assertFalse(PointForm().is_valid())
  160
+        invalid = PointForm(data={'p': 'some invalid geom'})
  161
+        self.assertFalse(invalid.is_valid())
  162
+        self.assertTrue('Invalid geometry value' in str(invalid.errors))
  163
+
  164
+        for invalid in [geom for key, geom in self.geometries.items() if key!='point']:
  165
+            self.assertFalse(PointForm(data={'p': invalid.wkt}).is_valid())
  166
+
  167
+    def test_multipointfield(self):
  168
+        class PointForm(forms.Form):
  169
+            p = forms.MultiPointField()
  170
+
  171
+        geom = self.geometries['multipoint']
  172
+        form = PointForm(data={'p': geom})
  173
+        self.assertTextarea(geom, form.as_p())
  174
+        self.assertMapWidget(form)
  175
+        self.assertFalse(PointForm().is_valid())
  176
+
  177
+        for invalid in [geom for key, geom in self.geometries.items() if key!='multipoint']:
  178
+            self.assertFalse(PointForm(data={'p': invalid.wkt}).is_valid())
  179
+
  180
+    def test_linestringfield(self):
  181
+        class LineStringForm(forms.Form):
  182
+            l = forms.LineStringField()
  183
+
  184
+        geom = self.geometries['linestring']
  185
+        form = LineStringForm(data={'l': geom})
  186
+        self.assertTextarea(geom, form.as_p())
  187
+        self.assertMapWidget(form)
  188
+        self.assertFalse(LineStringForm().is_valid())
  189
+
  190
+        for invalid in [geom for key, geom in self.geometries.items() if key!='linestring']:
  191
+            self.assertFalse(LineStringForm(data={'p': invalid.wkt}).is_valid())
  192
+
  193
+    def test_multilinestringfield(self):
  194
+        class LineStringForm(forms.Form):
  195
+            l = forms.MultiLineStringField()
  196
+
  197
+        geom = self.geometries['multilinestring']
  198
+        form = LineStringForm(data={'l': geom})
  199
+        self.assertTextarea(geom, form.as_p())
  200
+        self.assertMapWidget(form)
  201
+        self.assertFalse(LineStringForm().is_valid())
  202
+
  203
+        for invalid in [geom for key, geom in self.geometries.items() if key!='multilinestring']:
  204
+            self.assertFalse(LineStringForm(data={'p': invalid.wkt}).is_valid())
  205
+
  206
+    def test_polygonfield(self):
  207
+        class PolygonForm(forms.Form):
  208
+            p = forms.PolygonField()
  209
+
  210
+        geom = self.geometries['polygon']
  211
+        form = PolygonForm(data={'p': geom})
  212
+        self.assertTextarea(geom, form.as_p())
  213
+        self.assertMapWidget(form)
  214
+        self.assertFalse(PolygonForm().is_valid())
  215
+
  216
+        for invalid in [geom for key, geom in self.geometries.items() if key!='polygon']:
  217
+            self.assertFalse(PolygonForm(data={'p': invalid.wkt}).is_valid())
  218
+
  219
+    def test_multipolygonfield(self):
  220
+        class PolygonForm(forms.Form):
  221
+            p = forms.MultiPolygonField()
  222
+
  223
+        geom = self.geometries['multipolygon']
  224
+        form = PolygonForm(data={'p': geom})
  225
+        self.assertTextarea(geom, form.as_p())
  226
+        self.assertMapWidget(form)
  227
+        self.assertFalse(PolygonForm().is_valid())
  228
+
  229
+        for invalid in [geom for key, geom in self.geometries.items() if key!='multipolygon']:
  230
+            self.assertFalse(PolygonForm(data={'p': invalid.wkt}).is_valid())
  231
+
  232
+    def test_geometrycollectionfield(self):
  233
+        class GeometryForm(forms.Form):
  234
+            g = forms.GeometryCollectionField()
  235
+
  236
+        geom = self.geometries['geometrycollection']
  237
+        form = GeometryForm(data={'g': geom})
  238
+        self.assertTextarea(geom, form.as_p())
  239
+        self.assertMapWidget(form)
  240
+        self.assertFalse(GeometryForm().is_valid())
84 241
 
85  
-if __name__=="__main__":
86  
-    run()
  242
+        for invalid in [geom for key, geom in self.geometries.items() if key!='geometrycollection']:
  243
+            self.assertFalse(GeometryForm(data={'g': invalid.wkt}).is_valid())
165  docs/ref/contrib/gis/forms-api.txt
... ...
@@ -0,0 +1,165 @@
  1
+.. _ref-gis-forms-api:
  2
+
  3
+===================
  4
+GeoDjango Forms API
  5
+===================
  6
+
  7
+.. module:: django.contrib.gis.forms
  8
+   :synopsis: GeoDjango forms API.
  9
+
  10
+.. versionadded:: 1.6
  11
+
  12
+GeoDjango provides some specialized form fields and widgets in order to visually 
  13
+display and edit geolocalized data on a map. By default, they use
  14
+`OpenLayers`_-powered maps, with a base WMS layer provided by `Metacarta`_.
  15
+
  16
+.. _OpenLayers: http://openlayers.org/
  17
+.. _Metacarta: http://metacarta.com/
  18
+
  19
+Field arguments
  20
+===============
  21
+In addition to the regular :ref:`form field arguments <core-field-arguments>`,
  22
+GeoDjango form fields take the following optional arguments.
  23
+
  24
+``srid``
  25
+~~~~~~~~
  26
+
  27
+.. attribute:: Field.srid
  28
+
  29
+    This is the SRID code that the field value should be transformed to. For
  30
+    example, if the map widget SRID is different from the SRID more generally
  31
+    used by your application or database, the field will automatically convert
  32
+    input values into that SRID.
  33
+
  34
+``geom_type``
  35
+~~~~~~~~~~~~~
  36
+
  37
+.. attribute:: Field.geom_type
  38
+
  39
+    You generally shouldn't have to set or change that attribute which should
  40
+    be setup depending on the field class. It matches the OpenGIS standard
  41
+    geometry name.
  42
+
  43
+Form field classes
  44
+==================
  45
+
  46
+``GeometryField``
  47
+~~~~~~~~~~~~~~~~~
  48
+
  49
+.. class:: GeometryField
  50
+
  51
+``PointField``
  52
+~~~~~~~~~~~~~~
  53
+
  54
+.. class:: PointField
  55
+
  56
+``LineStringField``
  57
+~~~~~~~~~~~~~~~~~~~
  58
+
  59
+.. class:: LineStringField
  60
+
  61
+``PolygonField``
  62
+~~~~~~~~~~~~~~~~
  63
+
  64
+.. class:: PolygonField
  65
+
  66
+``MultiPointField``
  67
+~~~~~~~~~~~~~~~~~~~
  68
+
  69
+.. class:: MultiPointField
  70
+
  71
+``MultiLineStringField``
  72
+~~~~~~~~~~~~~~~~~~~~~~~~
  73
+
  74
+.. class:: MultiLineStringField
  75
+
  76
+``MultiPolygonField``
  77
+~~~~~~~~~~~~~~~~~~~~~
  78
+
  79
+.. class:: MultiPolygonField
  80
+
  81
+``GeometryCollectionField``
  82
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
  83
+
  84
+.. class:: GeometryCollectionField
  85
+
  86
+Form widgets
  87
+============
  88
+
  89
+.. module:: django.contrib.gis.widgets
  90
+   :synopsis: GeoDjango widgets API.
  91
+
  92
+GeoDjango form widgets allow you to display and edit geographic data on a
  93
+visual map.
  94
+Note that none of the currently available widgets supports 3D geometries, hence
  95
+geometry fields will fallback using a simple ``Textarea`` widget for such data.
  96
+
  97
+Widget attributes
  98
+~~~~~~~~~~~~~~~~~
  99
+
  100
+GeoDjango widgets are template-based, so their attributes are mostly different
  101
+from other Django widget attributes.
  102
+
  103
+
  104
+.. attribute:: BaseGeometryWidget.geom_type
  105
+
  106
+    The OpenGIS geometry type, generally set by the form field.
  107
+
  108
+.. attribute:: BaseGeometryWidget.map_height
  109
+.. attribute:: BaseGeometryWidget.map_width
  110
+
  111
+    Height and width of the widget map (default is 400x600).
  112
+
  113
+.. attribute:: BaseGeometryWidget.map_srid
  114
+
  115
+    SRID code used by the map (default is 4326).
  116
+
  117
+.. attribute:: BaseGeometryWidget.display_wkt
  118
+
  119
+    Boolean value specifying if a textarea input showing the WKT representation
  120
+    of the current geometry is visible, mainly for debugging purposes (default
  121
+    is ``False``).
  122
+
  123
+.. attribute:: BaseGeometryWidget.supports_3d
  124
+
  125
+    Indicates if the widget supports edition of 3D data (default is ``False``).
  126
+
  127
+.. attribute:: BaseGeometryWidget.template_name
  128
+
  129
+    The template used to render the map widget.
  130
+
  131
+You can pass widget attributes in the same manner that for any other Django
  132
+widget. For example::
  133
+
  134
+    from django.contrib.gis import forms
  135
+
  136
+    class MyGeoForm(forms.Form):
  137
+        point = forms.PointField(widget=
  138
+            forms.OSMWidget(attrs={'map_width': 800, 'map_height': 500}))
  139
+
  140
+Widget classes
  141
+~~~~~~~~~~~~~~
  142
+
  143
+``BaseGeometryWidget``
  144
+
  145
+.. class:: BaseGeometryWidget
  146
+
  147
+    This is an abstract base widget containing the logic needed by subclasses.
  148
+    You cannot directly use this widget for a geometry field.
  149
+    Note that the rendering of GeoDjango widgets is based on a template,
  150
+    identified by the :attr:`template_name` class attribute.
  151
+
  152
+``OpenLayersWidget``
  153
+
  154
+.. class:: OpenLayersWidget
  155
+
  156
+    This is the default widget used by all GeoDjango form fields.
  157
+    ``template_name`` is ``gis/openlayers.html``.
  158
+
  159
+``OSMWidget``
  160
+
  161
+.. class:: OSMWidget
  162
+
  163
+    This widget uses an OpenStreetMap base layer (Mapnik) to display geographic
  164
+    objects on.
  165
+    ``template_name`` is ``gis/openlayers-osm.html``.
1  docs/ref/contrib/gis/index.txt
@@ -18,6 +18,7 @@ of spatially enabled data.
18 18
    install/index
19 19
    model-api
20 20
    db-api
  21
+   forms-api
21 22
    geoquerysets
22 23
    measure
23 24
    geos
2  docs/ref/forms/fields.txt
@@ -30,6 +30,8 @@ exception or returns the clean value::
30 30
     ...
31 31
     ValidationError: [u'Enter a valid email address.']
32 32
 
  33
+.. _core-field-arguments:
  34
+
33 35
 Core field arguments
34 36
 --------------------
35 37
 
7  docs/releases/1.6.txt
@@ -114,6 +114,13 @@ Django 1.6 adds support for savepoints in SQLite, with some :ref:`limitations
114 114
 A new :class:`django.db.models.BinaryField` model field allows to store raw
115