Import djangorestframework-gis_0.9.5.orig.tar.gz
Brian May
8 years ago
0 | sudo: false | |
0 | 1 | language: python |
2 | cache: pip | |
1 | 3 | |
2 | 4 | services: |
3 | 5 | - postgresql |
5 | 7 | python: |
6 | 8 | - "3.4" |
7 | 9 | - "3.3" |
10 | - "3.2" | |
8 | 11 | - "2.7" |
9 | 12 | - "2.6" |
10 | 13 | |
11 | 14 | env: |
12 | - DJANGO="django==1.7.4" | |
13 | - DJANGO="django==1.6.10" | |
15 | - DJANGO="django==1.8.4" | |
16 | - DJANGO="django==1.7.10" | |
17 | - DJANGO="django==1.6.11" | |
14 | 18 | - DJANGO="django==1.5.12" |
15 | 19 | |
16 | 20 | matrix: |
17 | 21 | exclude: |
18 | 22 | - python: "2.6" |
19 | env: DJANGO="django==1.7.4" | |
23 | env: DJANGO="django==1.7.10" | |
24 | - python: "2.6" | |
25 | env: DJANGO="django==1.8.4" | |
20 | 26 | |
21 | 27 | branches: |
22 | 28 | only: |
23 | 29 | - master |
24 | - drf-3.0 | |
25 | 30 | |
26 | 31 | # command to install requirements |
27 | 32 | install: |
28 | 33 | - if [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install ordereddict; fi |
29 | 34 | - pip install $DJANGO |
30 | 35 | - pip install -r requirements-test.txt |
31 | - python setup.py -q install | |
36 | - python setup.py -q develop | |
32 | 37 | |
33 | 38 | before_script: |
34 | 39 | - createdb django_restframework_gis |
35 | 40 | - psql -U postgres -d django_restframework_gis -c "CREATE EXTENSION postgis;" |
36 | - psql -U postgres -d django_restframework_gis -c "CREATE EXTENSION postgis_topology;" | |
37 | 41 | |
38 | 42 | # command to run tests, e.g. python setup.py test |
39 | 43 | script: |
40 | - coverage run --source=rest_framework_gis,django_restframework_gis_tests runtests.py | |
44 | - coverage run --source=rest_framework_gis runtests.py | |
41 | 45 | |
42 | 46 | after_success: |
43 | coveralls⏎ | |
47 | coveralls |
7 | 7 | Christoph Heer https://github.com/jarus |
8 | 8 | Federico Capoano https://github.com/nemesisdesign/ |
9 | 9 | Shanto https://github.com/Shanto |
10 | Eric Theise https://github.com/erictheise |
0 | Changelog | |
1 | ========= | |
2 | ||
3 | Version 0.9.5 [2015-10-12] | |
4 | -------------------------- | |
5 | ||
6 | - `#71 <https://github.com/djangonauts/django-rest-framework-gis/pull/71>`_: added possibility to override GeoJSON properties in ``GeoFeatureModelSerializer`` | |
7 | - `52e15a5 <https://github.com/djangonauts/django-rest-framework-gis/commit/52e15a5>`_: Added default ``page_size_query_param`` in ``GeoJsonPagination`` | |
8 | ||
9 | Version 0.9.4 [2015-09-08] | |
10 | -------------------------- | |
11 | ||
12 | - `#68 <https://github.com/djangonauts/django-rest-framework-gis/issues/68>`_: ensure not having drf-gis in ``INSTALLED_APPS`` works anyway | |
13 | - `#76 <https://github.com/djangonauts/django-rest-framework-gis/issues/76>`_: avoid pickle errors in ``GeoJsonDict`` | |
14 | - `#75 <https://github.com/djangonauts/django-rest-framework-gis/pull/75>`_: return ``GEOSGeometry`` instead of geojson property | |
15 | ||
16 | Version 0.9.3 [2015-07-22] | |
17 | -------------------------- | |
18 | ||
19 | - `04fd1bf <https://github.com/djangonauts/django-rest-framework-gis/commit/04fd1bf>`_: Added ``GeoJsonPagination`` | |
20 | - `fe47d86 <https://github.com/djangonauts/django-rest-framework-gis/commit/fe47d86>`_: Improved ``ValidationError`` message of ``GeometryField`` | |
21 | - `a3ddd3d <https://github.com/djangonauts/django-rest-framework-gis/commit/a3ddd3d>`_: **Improved serialization performance between 25% and 29%** | |
22 | - `fb6ed36 <https://github.com/djangonauts/django-rest-framework-gis/commit/fb6ed36>`_: ``GeoModelSerializer`` deprecated because obsolete | |
23 | - `#66 <https://github.com/djangonauts/django-rest-framework-gis/pull/66>`_: geometry now allows ``None`` values according to the **GeoJSON spec** | |
24 | - `#67 <https://github.com/djangonauts/django-rest-framework-gis/pull/67>`_: discern ``False`` or empty string values from ``None`` in ``GeoFeatureModelSerializer`` | |
25 | ||
26 | Version 0.9.2 [2015-07-15] | |
27 | -------------------------- | |
28 | ||
29 | - `#59 <https://github.com/djangonauts/django-rest-framework-gis/pull/59>`_: Added GeometrySerializerMethodField | |
30 | - `3fa2354 <https://github.com/djangonauts/django-rest-framework-gis/commit/3fa2354>`_: removed broken/obsolete/untested code | |
31 | ||
32 | Version 0.9.1 [2015-06-28] | |
33 | -------------------------- | |
34 | ||
35 | - `#63 <https://github.com/djangonauts/django-rest-framework-gis/issues/63>`_: added compatibility with python 3.2 and updated compatibility table in README | |
36 | - `#60 <https://github.com/djangonauts/django-rest-framework-gis/pull/60>`_: ensure GeoJSON is rendered correctly in browsable API when using python 2 | |
37 | - `#62 <https://github.com/djangonauts/django-rest-framework-gis/issues/62>`_: updated django-rest-framework requirement to 3.1.3 | |
38 | ||
39 | Version 0.9 [2015-05-31] | |
40 | ------------------------ | |
41 | ||
42 | - `#55 <https://github.com/djangonauts/django-rest-framework-gis/pull/55>`_: Fixed exception in ``DistanceToPointFilter`` in case of invalid point | |
43 | - `#58 <https://github.com/djangonauts/django-rest-framework-gis/pull/58>`_: Fixed handling of ``None`` values in ``GeoFeatureModelSerializer`` to avoid problems with ``FileField`` and ``ImageField`` | |
44 | - `#57 <https://github.com/djangonauts/django-rest-framework-gis/pull/57>`_: Added support for GeoJSON Bounding Boxes in ``GeoFeatureModelSerializer`` | |
45 | ||
46 | Version 0.8.2 [2015-04-29] | |
47 | -------------------------- | |
48 | ||
49 | - `#53 <https://github.com/djangonauts/django-rest-framework-gis/pull/53>`_: Added support for PATCH requests in ``GeoFeatureModelSerializer`` | |
50 | ||
51 | Version 0.8.1 [2015-03-25] | |
52 | -------------------------- | |
53 | ||
54 | - Added compatibility with django-rest-framework 3.1.x | |
55 | - Added compatibility with django 1.8 (RC1) | |
56 | ||
57 | Version 0.8 [2015-03-03] | |
58 | ------------------------ | |
59 | ||
60 | - Added compatibility with django-rest-framework 3.x | |
61 | ||
62 | Version 0.7 [2014-10-03] | |
63 | ------------------------ | |
64 | ||
65 | - upgraded development status classifer to Beta | |
66 | - avoid empty string in textarea widget if value is None | |
67 | - allow field definition in GeoFeatureModelSerializer to be list | |
68 | ||
69 | Version 0.6 [2014-09-24] | |
70 | ------------------------ | |
71 | ||
72 | - Added compatibility to django-rest-framework 2.4.3 | |
73 | ||
74 | Version 0.5 [2014-09-07] | |
75 | ------------------------ | |
76 | ||
77 | - added TMSTileFilter | |
78 | - added DistanceToPointFilter | |
79 | - renamed InBBOXFilter to InBBoxFilter | |
80 | - added compatibility with DRF 2.4.0 | |
81 | ||
82 | Version 0.4 [2014-08-25] | |
83 | ------------------------ | |
84 | ||
85 | - python3 compatibility | |
86 | - improved DRF browsable API HTML widget (textarea instead of text input) | |
87 | ||
88 | Version 0.3 [2014-07-07] | |
89 | ------------------------ | |
90 | ||
91 | - added compatibility with DRF 2.3.14 | |
92 | ||
93 | Version 0.2 [2014-03-18] | |
94 | ------------------------ | |
95 | ||
96 | - geofilter support | |
97 | - README in restructured text for pypi | |
98 | - updated python package info | |
99 | ||
100 | Version 0.1 [2013-12-30] | |
101 | ------------------------ | |
102 | ||
103 | - first release |
0 | Contributing | |
1 | ============ | |
2 | ||
3 | Thanks for your interest! We love contributions, so please feel free to fix bugs, improve things, provide documentation. Just `follow the | |
4 | guidelines <https://github.com/djangonauts/django-rest-framework-gis#contributing>`_ and submit a PR. |
0 | include AUTHORS LICENSE README.md | |
0 | include AUTHORS LICENSE README.rst requirements.txt | |
1 | 1 | recursive-include tests * |
2 | 2 | recursive-exclude * *.pyc *.swp |
0 | 0 | django-rest-framework-gis |
1 | 1 | ========================= |
2 | 2 | |
3 | |Build Status| |Coverage Status| |Code Health| |Requirements Status| |PyPI version| |PyPI downloads| | |
3 | |Build Status| |Coverage Status| |Requirements Status| |PyPI version| |PyPI downloads| | |
4 | 4 | |
5 | 5 | Geographic add-ons for Django Rest Framework - `Mailing |
6 | 6 | List <http://bit.ly/1M4sLTp>`__. |
19 | 19 | |
20 | 20 | pip install https://github.com/djangonauts/django-rest-framework-gis/tarball/master |
21 | 21 | |
22 | Setup | |
23 | ----- | |
24 | ||
25 | Add ``rest_framework_gis`` in ``settings.INSTALLED_APPS``, after ``rest_framework``: | |
26 | ||
27 | .. code-block:: python | |
28 | ||
29 | INSTALLED_APPS = [ | |
30 | # ... | |
31 | 'rest_framework', | |
32 | 'rest_framework_gis', | |
33 | # ... | |
34 | ] | |
35 | ||
22 | 36 | Compatibility with DRF, Django and Python |
23 | 37 | ----------------------------------------- |
24 | 38 | |
25 | =============== ============================ ==================== ================== | |
39 | =============== ============================ ==================== ================================== | |
26 | 40 | DRF-gis version DRF version Django version Python version |
27 | **0.8** **3.0.4** **1.5.x** to **1.7** **2.6** to **3.4** | |
28 | **0.7** **2.4.3** **1.5.x** to **1.7** **2.6** to **3.4** | |
29 | **0.6** **2.4.3** **1.5.x** to **1.7** **2.6** to **3.4** | |
30 | **0.5** from **2.3.14** to **2.4.2** **1.5.x** to **1.7** **2.6** to **3.4** | |
31 | **0.4** from **2.3.14** to **2.4.2** **1.5.x** to **1.7** **2.6** to **3.4** | |
41 | **0.9.5** **3.1.X** to **3.2.X** **1.5.x** to **1.8** **2.6** to **3.4** | |
42 | **0.9.4** **3.1.X** to **3.2.X** **1.5.x** to **1.8** **2.6** to **3.4** | |
43 | **0.9.3** **3.1.X** **1.5.x** to **1.8** **2.6** to **3.4** | |
44 | **0.9.2** **3.1.X** **1.5.x** to **1.8** **2.6** to **3.4** | |
45 | **0.9.1** **3.1.X** **1.5.x** to **1.8** **2.6** to **3.4** | |
46 | **0.9** **3.1.X** **1.5.x** to **1.8** **2.6**, **2.7**, **3.3**, **3.4** | |
47 | **0.9** **3.1.X** **1.5.x** to **1.8** **2.6**, **2.7**, **3.3**, **3.4** | |
48 | **0.9** **3.1.X** **1.5.x** to **1.8** **2.6**, **2.7**, **3.3**, **3.4** | |
49 | **0.8.2** **3.0.4** to **3.1.1** **1.5.x** to **1.8** **2.6**, **2.7**, **3.3**, **3.4** | |
50 | **0.8.1** **3.0.4** to **3.1.1** **1.5.x** to **1.8** **2.6**, **2.7**, **3.3**, **3.4** | |
51 | **0.8** **3.0.4** **1.5.x** to **1.7** **2.6**, **2.7**, **3.3**, **3.4** | |
52 | **0.7** **2.4.3** **1.5.x** to **1.7** **2.6**, **2.7**, **3.3**, **3.4** | |
53 | **0.6** **2.4.3** **1.5.x** to **1.7** **2.6**, **2.7**, **3.3**, **3.4** | |
54 | **0.5** from **2.3.14** to **2.4.2** **1.5.x** to **1.7** **2.6**, **2.7**, **3.3**, **3.4** | |
55 | **0.4** from **2.3.14** to **2.4.2** **1.5.x** to **1.7** **2.6**, **2.7**, **3.3**, **3.4** | |
32 | 56 | **0.3** from **2.3.14** to **2.4.2** **1.5.x**, **1.6.x** **2.6**, **2.7** |
33 | 57 | **0.2** from **2.2.2** to **2.3.13** **1.5.x**, **1.6.x** **2.6**, **2.7** |
34 | =============== ============================ ==================== ================== | |
58 | =============== ============================ ==================== ================================== | |
35 | 59 | |
36 | 60 | Fields |
37 | 61 | ------ |
38 | 62 | |
39 | Provides a GeometryField, which is a subclass of Django Rest Framework | |
63 | GeometryField | |
64 | ~~~~~~~~~~~~~ | |
65 | ||
66 | Provides a ``GeometryField``, which is a subclass of Django Rest Framework | |
40 | 67 | (from now on **DRF**) ``WritableField``. This field handles GeoDjango |
41 | 68 | geometry fields, providing custom ``to_native`` and ``from_native`` |
42 | 69 | methods for GeoJSON input/output. |
43 | 70 | |
71 | **New in 0.9.3:** there is no need to define this field explicitly in your serializer, | |
72 | it's mapped automatically during initialization in ``rest_framework_gis.apps.AppConfig.ready()``. | |
73 | ||
74 | GeometrySerializerMethodField | |
75 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
76 | ||
77 | Provides a ``GeometrySerializerMethodField``, which is a subclass of DRF | |
78 | ``SerializerMethodField`` and handles values which are computed with a serializer | |
79 | method and are used as a ``geo_field``. `See example below <https://github.com/djangonauts/django-rest-framework-gis#using-geometryserializermethodfield-as-geo_field>`__. | |
80 | ||
44 | 81 | Serializers |
45 | 82 | ----------- |
46 | 83 | |
47 | GeoModelSerializer | |
48 | ~~~~~~~~~~~~~~~~~~ | |
84 | GeoModelSerializer (DEPRECATED) | |
85 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
86 | ||
87 | **Deprecated, will be removed in 1.0**: Using this serializer is not needed anymore since 0.9.3 if you add | |
88 | ``rest_framework_gis`` in ``settings.INSTALLED_APPS`` | |
49 | 89 | |
50 | 90 | Provides a ``GeoModelSerializer``, which is a sublass of DRF |
51 | 91 | ``ModelSerializer``. This serializer updates the field\_mapping |
95 | 135 | GeoFeatureModelSerializer |
96 | 136 | ~~~~~~~~~~~~~~~~~~~~~~~~~ |
97 | 137 | |
98 | ``GeoFeatureModelSerializer`` is a subclass of ``GeoModelSerializer`` | |
138 | ``GeoFeatureModelSerializer`` is a subclass of ``rest_framework.ModelSerializer`` | |
99 | 139 | which will output data in a format that is **GeoJSON** compatible. Using |
100 | 140 | the above example, the ``GeoFeatureModelSerializer`` will output: |
101 | 141 | |
119 | 159 | |
120 | 160 | If you are serializing an object list, ``GeoFeatureModelSerializer`` |
121 | 161 | will create a ``FeatureCollection``: |
122 | ||
123 | (**NOTE:** This currenty does not work with the default pagination | |
124 | serializer) | |
125 | 162 | |
126 | 163 | .. code-block:: javascript |
127 | 164 | |
160 | 197 | } |
161 | 198 | } |
162 | 199 | |
163 | ``GeoFeatureModelSerializer`` requires you to define a **``geo_field``** | |
200 | Specifying the geometry field: "geo_field" | |
201 | ########################################## | |
202 | ||
203 | ``GeoFeatureModelSerializer`` requires you to define a ``geo_field`` | |
164 | 204 | to be serialized as the "geometry". For example: |
165 | 205 | |
166 | 206 | .. code-block:: python |
178 | 218 | # as with a ModelSerializer. |
179 | 219 | fields = ('id', 'address', 'city', 'state') |
180 | 220 | |
221 | Using GeometrySerializerMethodField as "geo_field" | |
222 | ################################################## | |
223 | ||
224 | ``geo_field`` may also be an instance of ``GeometrySerializerMethodField``. | |
225 | In this case you can compute its value during serialization. For example: | |
226 | ||
227 | .. code-block:: python | |
228 | ||
229 | from django.contrib.gis.geos import Point | |
230 | from rest_framework_gis.serializers import GeoFeatureModelSerializer, GeometrySerializerMethodField | |
231 | ||
232 | class LocationSerializer(GeoFeatureModelSerializer): | |
233 | """ A class to serialize locations as GeoJSON compatible data """ | |
234 | ||
235 | # a field which contains a geometry value and can be used as geo_field | |
236 | other_point = GeometrySerializerMethodField() | |
237 | ||
238 | def get_other_point(self, obj): | |
239 | return Point(obj.point.lat / 2, obj.point.lon / 2) | |
240 | ||
241 | class Meta: | |
242 | model = Location | |
243 | geo_field = 'other_point' | |
244 | ||
245 | Serializer for ``geo_field`` may also return ``None`` value, which will translate to ``null`` value for geojson ``geometry`` field. | |
246 | ||
247 | Specifying the ID: "id_field" | |
248 | ############################# | |
249 | ||
181 | 250 | The primary key of the model (usually the "id" attribute) is |
182 | 251 | automatically put outside the "properties" object (before "type") unless |
183 | **``id_field``** is set to False: | |
252 | ``id_field`` is set to False: | |
184 | 253 | |
185 | 254 | .. code-block:: python |
186 | 255 | |
194 | 263 | id_field = False |
195 | 264 | fields = ('id', 'address', 'city', 'state') |
196 | 265 | |
197 | You could also set the **``id_field``** to some other unique field in | |
266 | You could also set the ``id_field`` to some other unique field in | |
198 | 267 | your model, like **"slug"**: |
199 | 268 | |
200 | 269 | .. code-block:: python |
205 | 274 | |
206 | 275 | class Meta: |
207 | 276 | model = Location |
208 | geo_field = "point" | |
209 | id_field = "slug" | |
277 | geo_field = 'point' | |
278 | id_field = 'slug' | |
210 | 279 | fields = ('slug', 'address', 'city', 'state') |
280 | ||
281 | Bounding Box: "auto_bbox" and "bbox_geo_field" | |
282 | ############################################## | |
283 | ||
284 | The GeoJSON specification allows a feature to contain a | |
285 | `boundingbox of a feature <http://geojson.org/geojson-spec.html#geojson-objects>`__. | |
286 | ``GeoFeatureModelSerializer`` allows two different ways to fill this property. The first | |
287 | is using the ``geo_field`` to calculate the bounding box of a feature. This only allows | |
288 | read access for a REST client and can be achieved using ``auto_bbox``. Example: | |
289 | ||
290 | .. code-block:: python | |
291 | ||
292 | from rest_framework_gis.serializers import GeoFeatureModelSerializer | |
293 | ||
294 | class LocationSerializer(GeoFeatureModelSerializer): | |
295 | class Meta: | |
296 | model = Location | |
297 | geo_field = 'geometry' | |
298 | auto_bbox = True | |
299 | ||
300 | ||
301 | The second approach uses the ``bbox_geo_field`` to specify an addional | |
302 | GeometryField of the model which will be used to calculate the bounding box. This allows | |
303 | boundingboxes differ from the exact extent of a features geometry. Additionally this | |
304 | enables read and write access for the REST client. Bounding boxes send from the client will | |
305 | be saved as Polygons. Example: | |
306 | ||
307 | .. code-block:: python | |
308 | ||
309 | from rest_framework_gis.serializers import GeoFeatureModelSerializer | |
310 | ||
311 | class LocationSerializer(GeoFeatureModelSerializer): | |
312 | ||
313 | class Meta: | |
314 | model = BoxedLocation | |
315 | geo_field = 'geometry' | |
316 | bbox_geo_field = 'bbox_geometry' | |
317 | ||
318 | ||
319 | Custom GeoJSON properties source | |
320 | ################################ | |
321 | ||
322 | In GeoJSON each feature can have a ``properties`` member containing the | |
323 | attributes of the feature. By default this field is filled with the | |
324 | attributes from your Django model, excluding the id, geometry and bounding | |
325 | box fields. It's possible to override this behaviour and implement a custom | |
326 | source for the ``properties`` member. | |
327 | ||
328 | The following example shows how to use a PostgreSQL HStore field as a source for | |
329 | the ``properties`` member: | |
330 | ||
331 | .. code-block:: python | |
332 | ||
333 | # models.py | |
334 | class Link(models.Model): | |
335 | """ | |
336 | Metadata is stored in a PostgreSQL HStore field, which allows us to | |
337 | store arbitrary key-value pairs with a link record. | |
338 | """ | |
339 | metadata = HStoreField(blank=True, null=True, default={}) | |
340 | geo = models.LineStringField() | |
341 | objects = models.GeoManager() | |
342 | ||
343 | # serializers.py | |
344 | class NetworkGeoSerializer(GeoFeatureModelSerializer): | |
345 | class Meta: | |
346 | model = models.Link | |
347 | geo_field = 'geo' | |
348 | auto_bbox = True | |
349 | ||
350 | def get_properties(self, instance, fields): | |
351 | # This is a PostgreSQL HStore field, which django maps to a dict | |
352 | return instance.metadata | |
353 | ||
354 | def unformat_geojson(self, feature): | |
355 | attrs = { | |
356 | self.Meta.geo_field: feature["geometry"], | |
357 | "metadata": feature["properties"] | |
358 | } | |
359 | ||
360 | if self.Meta.bbox_geo_field and "bbox" in feature: | |
361 | attrs[self.Meta.bbox_geo_field] = Polygon.from_bbox(feature["bbox"]) | |
362 | ||
363 | return attrs | |
364 | ||
365 | When the serializer renders GeoJSON, it calls the method | |
366 | ``get_properties`` for each object in the database. This function | |
367 | should return a dictionary containing the attributes for the feature. In the | |
368 | case of a HStore field, this function is easily implemented. | |
369 | ||
370 | The reverse is also required: mapping a GeoJSON formatted structure to | |
371 | attributes of your model. This task is done by ``unformat_geojson``. It should | |
372 | return a dictionary with your model attributes as keys, and the corresponding | |
373 | values retrieved from the GeoJSON feature data. | |
374 | ||
375 | Pagination | |
376 | ---------- | |
377 | ||
378 | We provide a ``GeoJsonPagination`` class. | |
379 | ||
380 | GeoJsonPagination | |
381 | ~~~~~~~~~~~~~~~~~ | |
382 | ||
383 | Based on ``rest_framework.pagination.PageNumberPagination``. | |
384 | ||
385 | Code example: | |
386 | ||
387 | .. code-block:: python | |
388 | ||
389 | from rest_framework_gis.pagination import GeoJsonPagination | |
390 | # --- other omitted imports --- # | |
391 | ||
392 | class GeojsonLocationList(generics.ListCreateAPIView): | |
393 | # -- other omitted view attributes --- # | |
394 | pagination_class = GeoJsonPagination | |
395 | ||
396 | Example result response (cut to one element only instead of 10): | |
397 | ||
398 | .. code-block:: javascript | |
399 | ||
400 | { | |
401 | "type": "FeatureCollection", | |
402 | "count": 25, | |
403 | "next": "http://localhost:8000/geojson/?page=2", | |
404 | "previous": null, | |
405 | "features": [ | |
406 | { | |
407 | "type": "Feature", | |
408 | "geometry": { | |
409 | "type": "Point", | |
410 | "coordinates": [ | |
411 | 42.0, | |
412 | 50.0 | |
413 | ] | |
414 | }, | |
415 | "properties": { | |
416 | "name": "test" | |
417 | } | |
418 | } | |
419 | ] | |
420 | } | |
421 | ||
211 | 422 | |
212 | 423 | Filters |
213 | 424 | ------- |
401 | 612 | 8. Document your changes |
402 | 613 | 9. Send pull request |
403 | 614 | |
404 | .. |Build Status| image:: https://travis-ci.org/djangonauts/django-rest-framework-gis.png?branch=master | |
615 | .. |Build Status| image:: https://travis-ci.org/djangonauts/django-rest-framework-gis.svg?branch=master | |
405 | 616 | :target: https://travis-ci.org/djangonauts/django-rest-framework-gis |
406 | .. |Coverage Status| image:: https://coveralls.io/repos/djangonauts/django-rest-framework-gis/badge.png | |
617 | .. |Coverage Status| image:: https://coveralls.io/repos/djangonauts/django-rest-framework-gis/badge.svg | |
407 | 618 | :target: https://coveralls.io/r/djangonauts/django-rest-framework-gis |
408 | .. |Code Health| image:: https://landscape.io/github/djangonauts/django-rest-framework-gis/master/landscape.png | |
409 | :target: https://landscape.io/github/djangonauts/django-rest-framework-gis/master | |
410 | .. |Requirements Status| image:: https://requires.io/github/djangonauts/django-rest-framework-gis/requirements.png?branch=master | |
619 | .. |Requirements Status| image:: https://requires.io/github/djangonauts/django-rest-framework-gis/requirements.svg?branch=master | |
411 | 620 | :target: https://requires.io/github/djangonauts/django-rest-framework-gis/requirements/?branch=master |
412 | .. |PyPI version| image:: https://badge.fury.io/py/djangorestframework-gis.png | |
621 | .. |PyPI version| image:: https://badge.fury.io/py/djangorestframework-gis.svg | |
413 | 622 | :target: http://badge.fury.io/py/djangorestframework-gis |
414 | .. |PyPI downloads| image:: https://pypip.in/d/djangorestframework-gis/badge.png | |
623 | .. |PyPI downloads| image:: https://img.shields.io/pypi/dm/djangorestframework-gis.svg | |
415 | 624 | :target: http://badge.fury.io/py/djangorestframework-gis |
0 | Average of 5 measurements for python2 and python3. | |
1 | ||
2 | Launch the performance test with:: | |
3 | ||
4 | cd tests | |
5 | django test --keepdb django_restframework_gis_tests.test_performance | |
6 | ||
7 | For more information regarding how the measurement is performed read the code in | |
8 | `test_performance.py <https://github.com/djangonauts/django-rest-framework-gis/blob/master/tests/django_restframework_gis_tests/test_performance.py>`__. | |
9 | ||
10 | 0.9.2 | |
11 | ===== | |
12 | ||
13 | - **py2**: 6.304474401472 | |
14 | - **py3**: 6.952443096001661 | |
15 | ||
16 | 0.9.3 | |
17 | ===== | |
18 | ||
19 | - **py2**: 4.462462759018 (**29.2%** improvement) | |
20 | - **py3**: 5.217188624000118 (**25%** improvement) | |
21 | ||
22 | 0.9.4 | |
23 | ===== | |
24 | ||
25 | - **py2**: 4.227859210966 | |
26 | - **py3**: 5.00467809599977 | |
27 | ||
28 | 0.9.5 | |
29 | ===== | |
30 | ||
31 | - **py2**: 4.193417596816 | |
32 | - **py3**: 4.89978777600045 |
0 | 0 | psycopg2 |
1 | 1 | djangorestframework>=3.0.4 |
2 | coverage | |
2 | coverage==3.7.1 # rq.filter: >=3,<4 | |
3 | 3 | coveralls |
4 | 4 | django-filter |
5 | contexttimer |
0 | VERSION = (0, 8, 0, 'final') | |
0 | VERSION = (0, 9, 5, 'final') | |
1 | 1 | __version__ = VERSION # alias |
2 | 2 | |
3 | 3 | |
11 | 11 | if VERSION[3] != 'final': |
12 | 12 | version = '%s %s' % (version, VERSION[3]) |
13 | 13 | return version |
14 | ||
15 | ||
16 | default_app_config = 'rest_framework_gis.apps.AppConfig' | |
17 | ||
18 | # maintain support for django 1.5 and 1.6 | |
19 | # TODO: remove in version 1.0 | |
20 | try: | |
21 | import os | |
22 | import django | |
23 | ||
24 | if os.environ.get('DJANGO_SETTINGS_MODULE'): | |
25 | from django.conf import settings | |
26 | from .apps import AppConfig | |
27 | ||
28 | if 'rest_framework_gis' not in settings.INSTALLED_APPS: | |
29 | import warnings | |
30 | warnings.simplefilter('always', DeprecationWarning) | |
31 | warnings.warn('\nGeoModelSerializer is deprecated, ' | |
32 | 'add "rest_framework_gis" to settings.INSTALLED_APPS and use ' | |
33 | '"rest_framework.ModelSerializer" instead', | |
34 | DeprecationWarning) | |
35 | ||
36 | if django.get_version() < '1.7' or 'rest_framework_gis' not in settings.INSTALLED_APPS: | |
37 | import rest_framework_gis | |
38 | AppConfig('rest_framework_gis', rest_framework_gis).ready() | |
39 | except ImportError: | |
40 | pass |
0 | try: | |
1 | from django.apps import AppConfig as BaseConfig | |
2 | except ImportError: # pragma: nocover | |
3 | # django <= 1.6 | |
4 | class BaseConfig(object): | |
5 | def __init__(self, *args): | |
6 | pass | |
7 | ||
8 | ||
9 | class AppConfig(BaseConfig): | |
10 | name = 'rest_framework_gis' | |
11 | ||
12 | def ready(self): | |
13 | """ | |
14 | update Django Rest Framework serializer mappings | |
15 | """ | |
16 | from django.contrib.gis.db import models | |
17 | from rest_framework.serializers import ModelSerializer | |
18 | from .fields import GeometryField | |
19 | ||
20 | try: | |
21 | # drf 3.0 | |
22 | field_mapping = ModelSerializer._field_mapping.mapping | |
23 | except AttributeError: | |
24 | # drf 3.1 | |
25 | field_mapping = ModelSerializer.serializer_field_mapping | |
26 | ||
27 | # map GeoDjango fields to drf-gis GeometryField | |
28 | field_mapping.update({ | |
29 | models.GeometryField: GeometryField, | |
30 | models.PointField: GeometryField, | |
31 | models.LineStringField: GeometryField, | |
32 | models.PolygonField: GeometryField, | |
33 | models.MultiPointField: GeometryField, | |
34 | models.MultiLineStringField: GeometryField, | |
35 | models.MultiPolygonField: GeometryField, | |
36 | models.GeometryCollectionField: GeometryField | |
37 | }) |
0 | # rest_framework_gis/fields.py | |
1 | ||
2 | try: | |
3 | import simplejson as json | |
4 | except ImportError: | |
5 | import json | |
0 | import json | |
6 | 1 | |
7 | 2 | from django.contrib.gis.geos import GEOSGeometry, GEOSException |
8 | 3 | from django.contrib.gis.gdal import OGRException |
9 | 4 | from django.core.exceptions import ValidationError |
10 | 5 | from django.utils.translation import ugettext_lazy as _ |
11 | from rest_framework.fields import Field | |
6 | from rest_framework.fields import Field, SerializerMethodField | |
7 | ||
8 | from .utils import OrderedDict | |
9 | ||
10 | ||
11 | __all__ = ['GeometryField', 'GeometrySerializerMethodField'] | |
12 | 12 | |
13 | 13 | |
14 | 14 | class GeometryField(Field): |
24 | 24 | def to_representation(self, value): |
25 | 25 | if isinstance(value, dict) or value is None: |
26 | 26 | return value |
27 | ||
28 | # Get GeoDjango geojson serialization and then convert it _back_ to | |
29 | # a Python object | |
30 | return json.loads(GEOSGeometry(value).geojson) | |
27 | # we expect value to be a GEOSGeometry instance | |
28 | return GeoJsonDict(( | |
29 | ('type', value.geom_type), | |
30 | ('coordinates', value.coords), | |
31 | )) | |
31 | 32 | |
32 | 33 | def to_internal_value(self, value): |
33 | 34 | if value == '' or value is None: |
34 | 35 | return value |
35 | ||
36 | if isinstance(value, GEOSGeometry): | |
37 | # value already has the correct representation | |
38 | return value | |
36 | 39 | if isinstance(value, dict): |
37 | 40 | value = json.dumps(value) |
38 | ||
39 | 41 | try: |
40 | return GEOSGeometry(value).geojson | |
42 | return GEOSGeometry(value) | |
41 | 43 | except (ValueError, GEOSException, OGRException, TypeError): |
42 | raise ValidationError(_('Invalid format: string or unicode input unrecognized as WKT EWKT, and HEXEWKB.')) | |
44 | raise ValidationError(_('Invalid format: string or unicode input unrecognized as GeoJSON, WKT EWKT or HEXEWKB.')) | |
43 | 45 | |
44 | 46 | def validate_empty_values(self, data): |
45 | if data in [u'']: | |
47 | if data == '': | |
46 | 48 | self.fail('required') |
47 | 49 | return super(GeometryField, self).validate_empty_values(data) |
50 | ||
51 | ||
52 | class GeometrySerializerMethodField(SerializerMethodField): | |
53 | def to_representation(self, value): | |
54 | value = super(GeometrySerializerMethodField, self).to_representation(value) | |
55 | if value is not None: | |
56 | # we expect value to be a GEOSGeometry instance | |
57 | return GeoJsonDict(( | |
58 | ('type', value.geom_type), | |
59 | ('coordinates', value.coords), | |
60 | )) | |
61 | else: | |
62 | return None | |
63 | ||
64 | ||
65 | class GeoJsonDict(OrderedDict): | |
66 | """ | |
67 | Used for serializing GIS values to GeoJSON values | |
68 | TODO: remove this when support for python 2.6/2.7 will be dropped | |
69 | """ | |
70 | ||
71 | def __str__(self): | |
72 | """ | |
73 | Avoid displaying strings like | |
74 | ``{ 'type': u'Point', 'coordinates': [12, 32] }`` | |
75 | in DRF browsable UI inputs (python 2.6/2.7) | |
76 | see: https://github.com/djangonauts/django-rest-framework-gis/pull/60 | |
77 | """ | |
78 | return json.dumps(self) |
0 | from math import cos, pi | |
1 | ||
0 | 2 | from django.db.models import Q |
1 | 3 | from django.core.exceptions import ImproperlyConfigured |
2 | 4 | from django.contrib.gis.db import models |
5 | 7 | |
6 | 8 | from rest_framework.filters import BaseFilterBackend |
7 | 9 | from rest_framework.exceptions import ParseError |
8 | from math import cos, pi | |
9 | 10 | |
10 | 11 | from .tilenames import tile_edges |
11 | 12 | |
12 | 13 | try: |
13 | 14 | import django_filters |
14 | except ImportError: | |
15 | except ImportError: # pragma: no cover | |
15 | 16 | raise ImproperlyConfigured( |
16 | 17 | 'restframework-gis filters depend on package "django-filter" ' |
17 | 18 | 'which is missing. Install with "pip install django-filter".' |
18 | 19 | ) |
20 | ||
21 | try: # pragma: no cover | |
22 | # django >= 1.8 | |
23 | from django.contrib.gis.db.models.lookups import gis_lookups | |
24 | except ImportError: # pragma: no cover | |
25 | # django <= 1.7 | |
26 | gis_lookups = models.sql.query.ALL_TERMS | |
19 | 27 | |
20 | 28 | |
21 | 29 | __all__ = [ |
32 | 40 | bbox_param = 'in_bbox' # The URL query parameter which contains the bbox. |
33 | 41 | |
34 | 42 | def get_filter_bbox(self, request): |
35 | bbox_string = request.QUERY_PARAMS.get(self.bbox_param, None) | |
43 | bbox_string = request.query_params.get(self.bbox_param, None) | |
36 | 44 | if not bbox_string: |
37 | 45 | return None |
38 | 46 | |
39 | 47 | try: |
40 | 48 | p1x, p1y, p2x, p2y = (float(n) for n in bbox_string.split(',')) |
41 | 49 | except ValueError: |
42 | raise ParseError("Not valid bbox string in parameter %s." | |
43 | % self.bbox_param) | |
50 | raise ParseError('Invalid bbox string supplied for parameter {0}'.format(self.bbox_param)) | |
44 | 51 | |
45 | 52 | x = Polygon.from_bbox((p1x, p1y, p2x, p2y)) |
46 | 53 | return x |
77 | 84 | |
78 | 85 | def __new__(cls, *args, **kwargs): |
79 | 86 | cls.filter_overrides.update(cls.GEOFILTER_FOR_DBFIELD_DEFAULTS) |
80 | cls.LOOKUP_TYPES = sorted(models.sql.query.ALL_TERMS) | |
87 | cls.LOOKUP_TYPES = sorted(gis_lookups) | |
81 | 88 | return super(GeoFilterSet, cls).__new__(cls) |
82 | 89 | |
83 | 90 | |
84 | 91 | class TMSTileFilter(InBBoxFilter): |
85 | tile_param = 'tile' # The URL query paramater which contains the tile address | |
92 | tile_param = 'tile' # The URL query paramater which contains the tile address | |
86 | 93 | |
87 | 94 | def get_filter_bbox(self, request): |
88 | tile_string = request.QUERY_PARAMS.get(self.tile_param, None) | |
95 | tile_string = request.query_params.get(self.tile_param, None) | |
89 | 96 | if not tile_string: |
90 | 97 | return None |
91 | 98 | |
92 | 99 | try: |
93 | 100 | z, x, y = (int(n) for n in tile_string.split('/')) |
94 | 101 | except ValueError: |
95 | raise ParseError("Not valid tile string in parameter %s." | |
96 | % self.tile_param) | |
102 | raise ParseError('Invalid tile string supplied for parameter {0}'.format(self.tile_param)) | |
97 | 103 | |
98 | 104 | bbox = Polygon.from_bbox(tile_edges(x, y, z)) |
99 | 105 | return bbox |
101 | 107 | |
102 | 108 | class DistanceToPointFilter(BaseFilterBackend): |
103 | 109 | dist_param = 'dist' |
104 | point_param = 'point' # The URL query parameter which contains the | |
110 | point_param = 'point' # The URL query parameter which contains the | |
105 | 111 | |
106 | 112 | def get_filter_point(self, request): |
107 | point_string = request.QUERY_PARAMS.get(self.point_param, None) | |
113 | point_string = request.query_params.get(self.point_param, None) | |
108 | 114 | if not point_string: |
109 | 115 | return None |
110 | 116 | |
111 | 117 | try: |
112 | (x,y) = (float(n) for n in point_string.split(',')) | |
118 | (x, y) = (float(n) for n in point_string.split(',')) | |
113 | 119 | except ValueError: |
114 | raise ParseError("Not valid geometry string in parameter %s." | |
115 | % self.point_string) | |
120 | raise ParseError('Invalid geometry string supplied for parameter {0}'.format(self.point_param)) | |
116 | 121 | |
117 | p = Point(x,y) | |
122 | p = Point(x, y) | |
118 | 123 | return p |
119 | 124 | |
120 | 125 | def dist_to_deg(self, distance, latitude): |
121 | 126 | """ |
122 | 127 | distance = distance in meters |
123 | latitude = latitude in degrees | |
128 | latitude = latitude in degrees | |
124 | 129 | |
125 | 130 | at the equator, the distance of one degree is equal in latitude and longitude. |
126 | 131 | at higher latitudes, a degree longitude is shorter in length, proportional to cos(latitude) |
127 | 132 | http://en.wikipedia.org/wiki/Decimal_degrees |
128 | 133 | |
129 | 134 | This function is part of a distance filter where the database 'distance' is in degrees. |
130 | There's no good single-valued answer to this problem. | |
131 | The distance/ degree is quite constant N/S around the earth (latitude), | |
135 | There's no good single-valued answer to this problem. | |
136 | The distance/ degree is quite constant N/S around the earth (latitude), | |
132 | 137 | but varies over a huge range E/W (longitude). |
133 | 138 | |
134 | Split the difference: I'm going to average the the degrees latitude and degrees longitude | |
135 | corresponding to the given distance. At high latitudes, this will be too short N/S | |
136 | and too long E/W. It splits the errors between the two axes. | |
139 | Split the difference: I'm going to average the the degrees latitude and degrees longitude | |
140 | corresponding to the given distance. At high latitudes, this will be too short N/S | |
141 | and too long E/W. It splits the errors between the two axes. | |
137 | 142 | |
138 | 143 | Errors are < 25 percent for latitudes < 60 degrees N/S. |
139 | 144 | """ |
140 | 145 | # d * (180 / pi) / earthRadius ==> degrees longitude |
141 | 146 | # (degrees longitude) / cos(latitude) ==> degrees latitude |
142 | lat = latitude if latitude >= 0 else -1*latitude | |
143 | rad2deg = 180/pi | |
147 | lat = latitude if latitude >= 0 else -1 * latitude | |
148 | rad2deg = 180 / pi | |
144 | 149 | earthRadius = 6378160.0 |
145 | 150 | latitudeCorrection = 0.5 * (1 + cos(lat * pi / 180)) |
146 | 151 | return (distance / (earthRadius * latitudeCorrection) * rad2deg) |
147 | ||
152 | ||
148 | 153 | def filter_queryset(self, request, queryset, view): |
149 | 154 | filter_field = getattr(view, 'distance_filter_field', None) |
150 | 155 | convert_distance_input = getattr(view, 'distance_filter_convert_meters', False) |
158 | 163 | return queryset |
159 | 164 | |
160 | 165 | # distance in meters |
161 | dist_string = request.QUERY_PARAMS.get(self.dist_param, 1000) | |
166 | dist_string = request.query_params.get(self.dist_param, 1000) | |
162 | 167 | try: |
163 | 168 | dist = float(dist_string) |
164 | 169 | except ValueError: |
165 | raise ParseError("Not valid distance string in parameter %s." | |
166 | % self.dist_param) | |
170 | raise ParseError('Invalid distance string supplied for parameter {0}'.format(self.dist_param)) | |
167 | 171 | |
168 | if (convert_distance_input): | |
172 | if (convert_distance_input): | |
169 | 173 | # Warning: assumes that the point is (lon,lat) |
170 | 174 | dist = self.dist_to_deg(dist, point[1]) |
171 | ||
175 | ||
172 | 176 | return queryset.filter(Q(**{'%s__%s' % (filter_field, geoDjango_filter): (point, dist)})) |
0 | from rest_framework import pagination | |
1 | from rest_framework.response import Response | |
2 | ||
3 | from .utils import OrderedDict | |
4 | ||
5 | ||
6 | class GeoJsonPagination(pagination.PageNumberPagination): | |
7 | """ | |
8 | A geoJSON implementation of a pagination serializer. | |
9 | """ | |
10 | page_size_query_param = 'page_size' | |
11 | ||
12 | def get_paginated_response(self, data): | |
13 | return Response(OrderedDict([ | |
14 | ('type', 'FeatureCollection'), | |
15 | ('count', self.page.paginator.count), | |
16 | ('next', self.get_next_link()), | |
17 | ('previous', self.get_previous_link()), | |
18 | ('features', data['features']) | |
19 | ])) |
0 | 0 | from django.core.exceptions import ImproperlyConfigured |
1 | from django.contrib.gis.db.models.fields import GeometryField as django_GeometryField | |
1 | from django.contrib.gis.geos import Polygon | |
2 | 2 | |
3 | 3 | from rest_framework.serializers import ModelSerializer, ListSerializer, LIST_SERIALIZER_KWARGS |
4 | from rest_framework.utils.field_mapping import ClassLookupDict | |
5 | 4 | |
6 | try: | |
7 | from collections import OrderedDict | |
8 | # python 2.6 | |
9 | except ImportError: | |
10 | from ordereddict import OrderedDict | |
11 | ||
12 | from .fields import GeometryField | |
13 | ||
14 | # map drf-gis GeometryField to GeoDjango Geometry Field | |
15 | _geo_field_mapping = ModelSerializer._field_mapping.mapping | |
16 | _geo_field_mapping.update({ | |
17 | django_GeometryField: GeometryField | |
18 | }) | |
5 | from .fields import GeometryField, GeometrySerializerMethodField # noqa | |
6 | from .utils import OrderedDict | |
19 | 7 | |
20 | 8 | |
21 | 9 | class GeoModelSerializer(ModelSerializer): |
22 | 10 | """ |
23 | A subclass of DFR ModelSerializer that adds support | |
24 | for GeoDjango fields to be serialized as GeoJSON | |
25 | compatible data | |
11 | Deprecated, will be removed in django-rest-framework-gis 1.0 | |
26 | 12 | """ |
27 | _field_mapping = ClassLookupDict(_geo_field_mapping) | |
28 | 13 | |
29 | 14 | |
30 | 15 | class GeoFeatureModelListSerializer(ListSerializer): |
42 | 27 | )) |
43 | 28 | |
44 | 29 | |
45 | class GeoFeatureModelSerializer(GeoModelSerializer): | |
30 | class GeoFeatureModelSerializer(ModelSerializer): | |
46 | 31 | """ |
47 | A subclass of GeoModelSerializer | |
32 | A subclass of ModelSerializer | |
48 | 33 | that outputs geojson-ready data as |
49 | 34 | features and feature collections |
50 | 35 | """ |
63 | 48 | def __init__(self, *args, **kwargs): |
64 | 49 | super(GeoFeatureModelSerializer, self).__init__(*args, **kwargs) |
65 | 50 | self.Meta.id_field = getattr(self.Meta, 'id_field', self.Meta.model._meta.pk.name) |
66 | if self.Meta.geo_field is None: | |
51 | if not hasattr(self.Meta, 'geo_field') or not self.Meta.geo_field: | |
67 | 52 | raise ImproperlyConfigured("You must define a 'geo_field'.") |
68 | # make sure geo_field is included in fields | |
69 | if hasattr(self.Meta, 'exclude'): | |
70 | if self.Meta.geo_field in self.Meta.exclude: | |
71 | raise ImproperlyConfigured("You cannot exclude your 'geo_field'.") | |
72 | if hasattr(self.Meta, 'fields'): | |
73 | if self.Meta.geo_field not in self.Meta.fields: | |
74 | if type(self.Meta.fields) is tuple: | |
75 | additional_fields = (self.Meta.geo_field, ) | |
76 | else: | |
77 | additional_fields = [self.Meta.geo_field, ] | |
78 | self.Meta.fields += additional_fields | |
53 | ||
54 | def check_excludes(field_name, field_role): | |
55 | """make sure the field is not excluded""" | |
56 | if hasattr(self.Meta, 'exclude') and field_name in self.Meta.exclude: | |
57 | raise ImproperlyConfigured("You cannot exclude your '{0}'.".format(field_role)) | |
58 | ||
59 | def add_to_fields(field_name): | |
60 | """Make sure the field is included in the fields""" | |
61 | if hasattr(self.Meta, 'fields'): | |
62 | if field_name not in self.Meta.fields: | |
63 | if type(self.Meta.fields) is tuple: | |
64 | additional_fields = (field_name,) | |
65 | else: | |
66 | additional_fields = [field_name] | |
67 | self.Meta.fields += additional_fields | |
68 | ||
69 | check_excludes(self.Meta.geo_field, 'geo_field') | |
70 | add_to_fields(self.Meta.geo_field) | |
71 | ||
72 | self.Meta.bbox_geo_field = getattr(self.Meta, 'bbox_geo_field', None) | |
73 | if self.Meta.bbox_geo_field: | |
74 | check_excludes(self.Meta.bbox_geo_field, 'bbox_geo_field') | |
75 | add_to_fields(self.Meta.bbox_geo_field) | |
76 | ||
77 | self.Meta.auto_bbox = getattr(self.Meta, 'auto_bbox', False) | |
78 | if self.Meta.bbox_geo_field and self.Meta.auto_bbox: | |
79 | raise ImproperlyConfigured( | |
80 | "You must eiher define a 'bbox_geo_field' or 'auto_bbox', but you can not set both" | |
81 | ) | |
79 | 82 | |
80 | 83 | def to_representation(self, instance): |
81 | 84 | """ |
82 | 85 | Serialize objects -> primitives. |
83 | 86 | """ |
84 | ret = OrderedDict() | |
85 | fields = [field for field in self.fields.values() if not field.write_only] | |
87 | # prepare OrderedDict geojson structure | |
88 | feature = OrderedDict() | |
89 | # the list of fields that will be processed by get_properties | |
90 | # we will remove fields that have been already processed | |
91 | # to increase performance on large numbers | |
92 | fields = list(self.fields.values()) | |
86 | 93 | |
87 | # geo structure | |
88 | if self.Meta.id_field is not False: | |
89 | ret["id"] = "" | |
90 | ret["type"] = "Feature" | |
91 | ret["geometry"] = {} | |
92 | ret["properties"] = OrderedDict() | |
94 | # optional id attribute | |
95 | if self.Meta.id_field: | |
96 | field = self.fields[self.Meta.id_field] | |
97 | value = field.get_attribute(instance) | |
98 | feature["id"] = field.to_representation(value) | |
99 | fields.remove(field) | |
100 | ||
101 | # required type attribute | |
102 | # must be "Feature" according to GeoJSON spec | |
103 | feature["type"] = "Feature" | |
104 | ||
105 | # required geometry attribute | |
106 | # MUST be present in output according to GeoJSON spec | |
107 | field = self.fields[self.Meta.geo_field] | |
108 | geo_value = field.get_attribute(instance) | |
109 | feature["geometry"] = field.to_representation(geo_value) | |
110 | fields.remove(field) | |
111 | # Bounding Box | |
112 | # if auto_bbox feature is enabled | |
113 | # bbox will be determined automatically automatically | |
114 | if self.Meta.auto_bbox and geo_value: | |
115 | feature["bbox"] = geo_value.extent | |
116 | # otherwise it can be determined via another field | |
117 | elif self.Meta.bbox_geo_field: | |
118 | field = self.fields[self.Meta.bbox_geo_field] | |
119 | value = field.get_attribute(instance) | |
120 | feature["bbox"] = value.extent if hasattr(value, 'extent') else None | |
121 | fields.remove(field) | |
122 | ||
123 | # GeoJSON properties | |
124 | feature["properties"] = self.get_properties(instance, fields) | |
125 | ||
126 | return feature | |
127 | ||
128 | def get_properties(self, instance, fields): | |
129 | """ | |
130 | Get the feature metadata which will be used for the GeoJSON | |
131 | "properties" key. | |
132 | ||
133 | By default it returns all serializer fields excluding those used for | |
134 | the ID, the geometry and the bounding box. | |
135 | ||
136 | :param instance: The current Django model instance | |
137 | :param fields: The list of fields to process (fields already processed have been removed) | |
138 | :return: OrderedDict containing the properties of the current feature | |
139 | :rtype: OrderedDict | |
140 | """ | |
141 | properties = OrderedDict() | |
93 | 142 | |
94 | 143 | for field in fields: |
95 | field_name = field.field_name | |
96 | if field.read_only and instance is None: | |
144 | if field.write_only: | |
97 | 145 | continue |
98 | 146 | value = field.get_attribute(instance) |
99 | if value: | |
100 | value = field.to_representation(value) | |
101 | if self.Meta.id_field is not False and field_name == self.Meta.id_field: | |
102 | ret["id"] = value | |
103 | elif field_name == self.Meta.geo_field: | |
104 | ret["geometry"] = value | |
105 | elif not getattr(field, 'write_only', False): | |
106 | ret["properties"][field_name] = value | |
147 | properties[field.field_name] = field.to_representation(value) | |
107 | 148 | |
108 | return ret | |
149 | return properties | |
109 | 150 | |
110 | 151 | def to_internal_value(self, data): |
111 | 152 | """ |
112 | 153 | Override the parent method to first remove the GeoJSON formatting |
113 | 154 | """ |
114 | if 'features' in data: | |
115 | _unformatted_data = [] | |
116 | features = data['features'] | |
117 | for feature in features: | |
118 | _dict = feature["properties"] | |
119 | geom = { self.Meta.geo_field: feature["geometry"] } | |
120 | _dict.update(geom) | |
121 | _unformatted_data.append(_dict) | |
122 | elif 'properties' in data: | |
123 | _dict = data["properties"] | |
124 | geom = { self.Meta.geo_field: data["geometry"] } | |
125 | _dict.update(geom) | |
126 | _unformatted_data = _dict | |
127 | else: | |
128 | _unformatted_data = data | |
155 | if 'properties' in data: | |
156 | data = self.unformat_geojson(data) | |
157 | return super(GeoFeatureModelSerializer, self).to_internal_value(data) | |
129 | 158 | |
130 | return super(GeoFeatureModelSerializer, self).to_internal_value(_unformatted_data) | |
159 | def unformat_geojson(self, feature): | |
160 | """ | |
161 | This function should return a dictionary containing keys which maps | |
162 | to serializer fields. | |
163 | ||
164 | Remember that GeoJSON contains a key "properties" which contains the | |
165 | feature metadata. This should be flattened to make sure this | |
166 | metadata is stored in the right serializer fields. | |
167 | ||
168 | :param feature: The dictionary containing the feature data directly | |
169 | from the GeoJSON data. | |
170 | :return: A new dictionary which maps the GeoJSON values to | |
171 | serializer fields | |
172 | """ | |
173 | attrs = feature["properties"] | |
174 | ||
175 | if 'geometry' in feature: | |
176 | attrs[self.Meta.geo_field] = feature['geometry'] | |
177 | ||
178 | if self.Meta.bbox_geo_field and 'bbox' in feature: | |
179 | attrs[self.Meta.bbox_geo_field] = Polygon.from_bbox(feature['bbox']) | |
180 | ||
181 | return attrs |
1 | 1 | # ------------------------------------------------------- |
2 | 2 | # Translates between lat/long and the slippy-map tile |
3 | 3 | # numbering scheme |
4 | # | |
4 | # | |
5 | 5 | # http://wiki.openstreetmap.org/index.php/Slippy_map_tilenames |
6 | # | |
6 | # | |
7 | 7 | # Written by Oliver White, 2007 |
8 | 8 | # This file is public-domain |
9 | 9 | # ------------------------------------------------------- |
11 | 11 | |
12 | 12 | |
13 | 13 | def num_tiles(z): |
14 | return(math_pow(2,z)) | |
14 | return(math_pow(2, z)) | |
15 | 15 | |
16 | 16 | |
17 | def lat_edges(y,z): | |
17 | def lat_edges(y, z): | |
18 | 18 | n = num_tiles(z) |
19 | 19 | unit = 1 / n |
20 | 20 | relY1 = y * unit |
21 | 21 | relY2 = relY1 + unit |
22 | 22 | lat1 = mercator_to_lat(pi * (1 - 2 * relY1)) |
23 | 23 | lat2 = mercator_to_lat(pi * (1 - 2 * relY2)) |
24 | return(lat1,lat2) | |
24 | return(lat1, lat2) | |
25 | 25 | |
26 | 26 | |
27 | def lon_edges(x,z): | |
27 | def lon_edges(x, z): | |
28 | 28 | n = num_tiles(z) |
29 | 29 | unit = 360 / n |
30 | 30 | lon1 = -180 + x * unit |
31 | 31 | lon2 = lon1 + unit |
32 | return(lon1,lon2) | |
32 | return(lon1, lon2) | |
33 | 33 | |
34 | 34 | |
35 | def tile_edges(x,y,z): | |
36 | lat1,lat2 = lat_edges(y,z) | |
37 | lon1,lon2 = lon_edges(x,z) | |
38 | return((lon1, lat2, lon2, lat1)) # w, s, e, n | |
35 | def tile_edges(x, y, z): | |
36 | lat1, lat2 = lat_edges(y, z) | |
37 | lon1, lon2 = lon_edges(x, z) | |
38 | return((lon1, lat2, lon2, lat1)) # w, s, e, n | |
39 | 39 | |
40 | 40 | |
41 | 41 | def mercator_to_lat(mercatorY): |
0 | try: | |
1 | from collections import OrderedDict # noqa | |
2 | # python 2.6 | |
3 | except ImportError: # pragma: no cover | |
4 | from ordereddict import OrderedDict # noqa |
0 | 0 | #!/usr/bin/env python |
1 | 1 | # -*- coding: utf-8 -*- |
2 | 2 | |
3 | import os, sys | |
3 | import os | |
4 | import sys | |
5 | ||
4 | 6 | sys.path.insert(0, "tests") |
5 | 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") |
6 | 8 |
0 | #!/usr/bin/env python | |
0 | 1 | import sys |
1 | 2 | import os |
2 | 3 | from setuptools import setup, find_packages |
3 | from setuptools.command.test import test | |
4 | 4 | from rest_framework_gis import get_version |
5 | 5 | |
6 | 6 | |
19 | 19 | |
20 | 20 | |
21 | 21 | if sys.argv[-1] == 'publish': |
22 | os.system("python setup.py sdist upload") | |
22 | os.system("python setup.py sdist bdist_wheel upload -s") | |
23 | 23 | args = {'version': get_version()} |
24 | 24 | print("You probably want to also tag the version now:") |
25 | 25 | print(" git tag -a %(version)s -m 'version %(version)s'" % args) |
32 | 32 | version=get_version(), |
33 | 33 | license='BSD', |
34 | 34 | author='Douglas Meehan', |
35 | author_email='dmeehan@gmail.com', | |
35 | author_email='django-rest-framework-gis@googlegroups.com', | |
36 | 36 | description='Geographic add-ons for Django Rest Framework', |
37 | 37 | url='https://github.com/djangonauts/django-rest-framework-gis', |
38 | 38 | download_url='https://github.com/djangonauts/django-rest-framework-gis/releases', |
1 | 1 | from django.utils.text import slugify |
2 | 2 | |
3 | 3 | |
4 | __all__ = ['Location'] | |
4 | __all__ = [ | |
5 | 'Location', | |
6 | 'LocatedFile', | |
7 | 'BoxedLocation' | |
8 | ] | |
5 | 9 | |
6 | 10 | |
7 | class Location(models.Model): | |
11 | class BaseModel(models.Model): | |
8 | 12 | name = models.CharField(max_length=32) |
9 | 13 | slug = models.SlugField(max_length=128, unique=True, blank=True) |
10 | 14 | geometry = models.GeometryField() |
11 | ||
12 | 15 | objects = models.GeoManager() |
13 | ||
16 | ||
17 | class Meta: | |
18 | abstract = True | |
19 | ||
14 | 20 | def __unicode__(self): |
15 | 21 | return self.name |
16 | ||
22 | ||
17 | 23 | def _generate_slug(self): |
18 | 24 | if self.slug == '' or self.slug is None: |
19 | 25 | try: |
21 | 27 | except NameError: |
22 | 28 | name = self.name |
23 | 29 | self.slug = slugify(name) |
24 | ||
30 | ||
25 | 31 | def clean(self): |
26 | 32 | self._generate_slug() |
27 | ||
33 | ||
28 | 34 | def save(self, *args, **kwargs): |
29 | 35 | self._generate_slug() |
30 | super(Location, self).save(*args, **kwargs) | |
36 | super(BaseModel, self).save(*args, **kwargs) | |
37 | ||
38 | ||
39 | class Location(BaseModel): | |
40 | pass | |
41 | ||
42 | ||
43 | class LocatedFile(BaseModel): | |
44 | file = models.FileField(upload_to='located_files', blank=True, null=True) | |
45 | ||
46 | ||
47 | class BoxedLocation(BaseModel): | |
48 | bbox_geometry = models.PolygonField() |
0 | from django.contrib.gis.geos import Point | |
1 | ||
0 | 2 | from rest_framework import pagination, serializers |
1 | 3 | from rest_framework_gis import serializers as gis_serializers |
2 | 4 | |
9 | 11 | 'LocationGeoFeatureSerializer', |
10 | 12 | 'LocationGeoFeatureSlugSerializer', |
11 | 13 | 'LocationGeoFeatureFalseIDSerializer', |
12 | 'PaginatedLocationGeoFeatureSerializer', | |
14 | 'LocatedFileGeoFeatureSerializer', | |
15 | 'BoxedLocationGeoFeatureSerializer', | |
16 | 'LocationGeoFeatureBboxSerializer', | |
17 | 'LocationGeoFeatureMethodSerializer', | |
18 | 'NoneGeoFeatureMethodSerializer', | |
13 | 19 | ] |
14 | 20 | |
15 | 21 | |
16 | class LocationGeoSerializer(gis_serializers.GeoModelSerializer): | |
22 | class LocationGeoSerializer(serializers.ModelSerializer): | |
17 | 23 | """ location geo serializer """ |
18 | ||
19 | 24 | details = serializers.HyperlinkedIdentityField(view_name='api_location_details') |
20 | 25 | |
21 | 26 | class Meta: |
22 | 27 | model = Location |
23 | geo_field = 'geometry' | |
24 | 28 | |
25 | 29 | |
26 | class PaginatedLocationGeoSerializer(pagination.PaginationSerializer): | |
27 | ||
28 | class Meta: | |
29 | object_serializer_class = LocationGeoSerializer | |
30 | class PaginatedLocationGeoSerializer(pagination.PageNumberPagination): | |
31 | page_size_query_param = 'limit' | |
32 | page_size = 40 | |
33 | max_page_size = 10000 | |
30 | 34 | |
31 | 35 | |
32 | 36 | class LocationGeoFeatureSerializer(gis_serializers.GeoFeatureModelSerializer): |
33 | 37 | """ location geo serializer """ |
34 | ||
35 | 38 | details = serializers.HyperlinkedIdentityField(view_name='api_geojson_location_details') |
36 | 39 | fancy_name = serializers.SerializerMethodField() |
37 | 40 | |
38 | 41 | def get_fancy_name(self, obj): |
39 | return u'Kool %s' % obj.name | |
42 | return 'Kool %s' % obj.name | |
40 | 43 | |
41 | 44 | class Meta: |
42 | 45 | model = Location |
45 | 48 | |
46 | 49 | class LocationGeoFeatureSlugSerializer(LocationGeoFeatureSerializer): |
47 | 50 | """ use slug as id attribute """ |
48 | ||
49 | 51 | class Meta: |
50 | 52 | model = Location |
51 | 53 | geo_field = 'geometry' |
52 | 54 | id_field = 'slug' |
53 | fields = ['name', 'slug'] | |
55 | fields = ('name', 'slug') | |
54 | 56 | |
55 | 57 | |
56 | 58 | class LocationGeoFeatureFalseIDSerializer(LocationGeoFeatureSerializer): |
57 | 59 | """ id attribute set as False """ |
58 | ||
59 | 60 | class Meta: |
60 | 61 | model = Location |
61 | 62 | geo_field = 'geometry' |
62 | 63 | id_field = False |
63 | 64 | |
64 | 65 | |
65 | class PaginatedLocationGeoFeatureSerializer(pagination.PaginationSerializer): | |
66 | class LocatedFileGeoFeatureSerializer(gis_serializers.GeoFeatureModelSerializer): | |
67 | """ located file geo serializer """ | |
68 | details = serializers.HyperlinkedIdentityField(view_name='api_geojson_located_file_details') | |
69 | fancy_name = serializers.SerializerMethodField() | |
70 | file = serializers.FileField(allow_empty_file=True) | |
71 | ||
72 | def get_fancy_name(self, obj): | |
73 | return 'Nice %s' % obj.name | |
66 | 74 | |
67 | 75 | class Meta: |
68 | object_serializer_class = LocationGeoFeatureSerializer | |
76 | model = Location | |
77 | geo_field = 'geometry' | |
78 | ||
79 | ||
80 | class BoxedLocationGeoFeatureSerializer(gis_serializers.GeoFeatureModelSerializer): | |
81 | """ location geo serializer """ | |
82 | details = serializers.HyperlinkedIdentityField(view_name='api_geojson_boxedlocation_details') | |
83 | ||
84 | class Meta: | |
85 | model = BoxedLocation | |
86 | geo_field = 'geometry' | |
87 | bbox_geo_field = 'bbox_geometry' | |
88 | fields = ['name', 'details', 'id'] | |
89 | ||
90 | ||
91 | class LocationGeoFeatureBboxSerializer(gis_serializers.GeoFeatureModelSerializer): | |
92 | class Meta: | |
93 | model = Location | |
94 | geo_field = 'geometry' | |
95 | auto_bbox = True | |
96 | ||
97 | ||
98 | class LocationGeoFeatureMethodSerializer(gis_serializers.GeoFeatureModelSerializer): | |
99 | new_geometry = gis_serializers.GeometrySerializerMethodField() | |
100 | ||
101 | def get_new_geometry(self, obj): | |
102 | if obj.name.startswith('hidden'): | |
103 | return Point(0., 0.) | |
104 | else: | |
105 | return obj.geometry | |
106 | ||
107 | class Meta: | |
108 | model = Location | |
109 | geo_field = 'new_geometry' | |
110 | ||
111 | ||
112 | class NoneGeoFeatureMethodSerializer(gis_serializers.GeoFeatureModelSerializer): | |
113 | new_geometry = gis_serializers.GeometrySerializerMethodField() | |
114 | ||
115 | def get_new_geometry(self, obj): | |
116 | return None | |
117 | ||
118 | class Meta: | |
119 | model = Location | |
120 | geo_field = 'new_geometry' | |
121 | fields = ['name', 'slug', 'id'] |
0 | import json | |
1 | ||
2 | from django.test import TestCase | |
3 | from django.core.urlresolvers import reverse | |
4 | from django.core.exceptions import ImproperlyConfigured | |
5 | ||
6 | from rest_framework_gis import serializers as gis_serializers | |
7 | ||
8 | from .models import BoxedLocation, Location | |
9 | from .serializers import LocationGeoSerializer | |
10 | ||
11 | ||
12 | class TestRestFrameworkGisBBox(TestCase): | |
13 | """ | |
14 | unit tests for bbox support in restframework_gis | |
15 | """ | |
16 | def setUp(self): | |
17 | self.geojson_boxedlocation_list_url = reverse('api_geojson_boxedlocation_list') | |
18 | self.geojson_location_bbox_list_url = reverse('api_geojson_location_bbox_list') | |
19 | ||
20 | def _create_locations(self): | |
21 | self.bl1 = BoxedLocation.objects.create(id=1, name='l1', slug='l1', geometry='POINT (13.007 42.423)', | |
22 | bbox_geometry='POLYGON((12.997 42.413,12.997 42.433,13.017 42.433,13.017 42.413,12.997 42.413))') | |
23 | self.bl2 = BoxedLocation.objects.create(id=2, name='l2', slug='l2', geometry='POINT (12.007 43.423)', | |
24 | bbox_geometry='POLYGON((11.997 43.413,11.997 43.433,12.017 43.433,12.017 43.413,11.997 43.413))') | |
25 | self.l1 = Location.objects.create(id=1, name='l1', slug='l1', | |
26 | geometry='POLYGON((12.997 42.413,12.997 42.433,13.017 42.433,13.017 42.413,12.997 42.413))') | |
27 | ||
28 | def test_list(self): | |
29 | self._create_locations() | |
30 | response = self.client.get(self.geojson_boxedlocation_list_url) | |
31 | self.assertEqual(response.status_code, 200) | |
32 | self.assertEqual(len(response.data['features']), 2) | |
33 | for feature in response.data['features']: | |
34 | self.assertIn('bbox', feature) | |
35 | fid = feature['id'] | |
36 | if fid==1: | |
37 | self.assertEqual(feature['bbox'], self.bl1.bbox_geometry.extent) | |
38 | elif fid==2: | |
39 | self.assertEqual(feature['bbox'], self.bl2.bbox_geometry.extent) | |
40 | else: | |
41 | self.fail("Unexpected id: {0}".format(fid)) | |
42 | BoxedLocation.objects.all().delete() | |
43 | ||
44 | def test_post_location_list_geojson(self): | |
45 | self.assertEqual(BoxedLocation.objects.count(), 0) | |
46 | data = { | |
47 | "properties": { | |
48 | "name": "geojson input test", | |
49 | }, | |
50 | "geometry": { | |
51 | "type": "Point", | |
52 | "coordinates": [ | |
53 | 12.49, | |
54 | 41.89 | |
55 | ] | |
56 | }, | |
57 | "bbox": [11.0, 40.0, 13.0, 42.0] | |
58 | } | |
59 | response = self.client.post(self.geojson_boxedlocation_list_url, data=json.dumps(data), content_type='application/json') | |
60 | self.assertEqual(response.status_code, 201) | |
61 | self.assertEqual(BoxedLocation.objects.count(), 1) | |
62 | self.assertEqual(BoxedLocation.objects.all()[0].bbox_geometry.extent, (11.0,40.0,13.0,42.0)) | |
63 | ||
64 | def test_get_autogenerated_location_bbox_geojson(self): | |
65 | self._create_locations() | |
66 | response = self.client.get(self.geojson_location_bbox_list_url) | |
67 | self.assertEqual(response.status_code, 200) | |
68 | self.assertEqual(len(response.data['features']), 1) | |
69 | self.assertEqual(response.data['features'][0]['bbox'], self.l1.geometry.extent) | |
70 | ||
71 | def test_bbox_improperly_configured(self): | |
72 | self._create_locations() | |
73 | class LocationGeoFeatureSerializer(gis_serializers.GeoFeatureModelSerializer): | |
74 | class Meta: | |
75 | model = Location | |
76 | geo_field = 'geometry' | |
77 | bbox_geo_field = 'geometry' | |
78 | auto_bbox = True | |
79 | with self.assertRaises(ImproperlyConfigured): | |
80 | LocationGeoFeatureSerializer(instance=self.l1) |
0 | """ | |
1 | unit tests for restframework_gis | |
2 | """ | |
3 | ||
4 | try: | |
5 | import simplejson as json | |
6 | except ImportError: | |
7 | import json | |
8 | ||
9 | ||
0 | import json | |
10 | 1 | import urllib |
2 | ||
11 | 3 | from django.test import TestCase |
12 | 4 | from django.contrib.gis.geos import GEOSGeometry, Polygon |
13 | 5 | from django.core.urlresolvers import reverse |
15 | 7 | from .models import Location |
16 | 8 | |
17 | 9 | |
18 | class TestRestFrameworkGis(TestCase): | |
19 | ||
10 | class TestRestFrameworkGisFilters(TestCase): | |
11 | """ | |
12 | unit tests for filters feature in restframework_gis | |
13 | """ | |
20 | 14 | def setUp(self): |
21 | 15 | self.location_contained_in_bbox_list_url = reverse('api_geojson_location_list_contained_in_bbox_filter') |
22 | 16 | self.location_overlaps_bbox_list_url = reverse('api_geojson_location_list_overlaps_bbox_filter') |
133 | 127 | self.assertEqual(Location.objects.count(), 0) |
134 | 128 | |
135 | 129 | # Filter parameters |
136 | distance = 5000 #meters | |
130 | distance = 5000 # meters | |
137 | 131 | point_inside_ggpark = [-122.49034881591797, 37.76949349270407] |
138 | 132 | point_on_golden_gate_bridge = [-122.47894, 37.8199] |
139 | 133 | point_on_alcatraz = [-122.4222, 37.82667] |
140 | 134 | point_on_treasure_island = [-122.3692, 37.8244] |
141 | 135 | point_on_angel_island = [-122.4326, 37.86091] |
136 | ||
137 | url_params = '?dist=%0.4f&point=hello&format=json' % (distance) | |
138 | response = self.client.get('%s%s' % (self.location_within_distance_of_point_list_url, url_params)) | |
139 | self.assertEqual(response.status_code, 400) | |
142 | 140 | |
143 | 141 | url_params = '?dist=%0.4f&point=%0.4f,%0.4f&format=json' % (distance, point_on_alcatraz[0], point_on_alcatraz[1]) |
144 | 142 | |
369 | 367 | |
370 | 368 | url_params = "?contains_properly=%s" % quoted_param |
371 | 369 | |
372 | response = self.client.get(self.geojson_contained_in_geometry + url_params) | |
370 | response = self.client.get('{0}{1}'.format(self.geojson_contained_in_geometry, url_params)) | |
373 | 371 | self.assertEqual(len(response.data), 1) |
374 | 372 | |
375 | 373 | geometry_response = GEOSGeometry(json.dumps(response.data[0]['geometry'])) |
380 | 378 | # try without any param, should return both |
381 | 379 | response = self.client.get(self.geojson_contained_in_geometry) |
382 | 380 | self.assertEqual(len(response.data), 2) |
381 | ||
382 | def test_inBBOXFilter_filtering_none(self): | |
383 | url_params = '?in_bbox=&format=json' | |
384 | response = self.client.get(self.location_contained_in_bbox_list_url + url_params) | |
385 | self.assertDictEqual(response.data, {'type':'FeatureCollection','features':[]}) | |
386 | ||
387 | def test_inBBOXFilter_ValueError(self): | |
388 | url_params = '?in_bbox=0&format=json' | |
389 | response = self.client.get(self.location_contained_in_bbox_list_url + url_params) | |
390 | self.assertEqual(response.data['detail'], 'Invalid bbox string supplied for parameter in_bbox') | |
391 | ||
392 | def test_inBBOXFilter_filter_field_none(self): | |
393 | from .views import GeojsonLocationContainedInBBoxList as view | |
394 | original_value = view.bbox_filter_field | |
395 | view.bbox_filter_field = None | |
396 | url_params = '?in_bbox=0,0,0,0&format=json' | |
397 | response = self.client.get(self.location_contained_in_bbox_list_url + url_params) | |
398 | self.assertDictEqual(response.data, {'type':'FeatureCollection','features':[]}) | |
399 | view.bbox_filter_field = original_value | |
400 | ||
401 | def test_TileFilter_filtering_none(self): | |
402 | url_params = '?tile=&format=json' | |
403 | response = self.client.get(self.location_contained_in_tile_list_url + url_params) | |
404 | self.assertEqual(response.data, {'type':'FeatureCollection','features':[]}) | |
405 | ||
406 | def test_TileFilter_ValueError(self): | |
407 | url_params = '?tile=1/0&format=json' | |
408 | response = self.client.get(self.location_contained_in_tile_list_url + url_params) | |
409 | self.assertEqual(response.data['detail'], 'Invalid tile string supplied for parameter tile') | |
410 | ||
411 | def test_DistanceToPointFilter_filtering_none(self): | |
412 | url_params = '?dist=%0.4f&point=&format=json' % 5000 | |
413 | response = self.client.get('%s%s' % (self.location_within_distance_of_point_list_url, url_params)) | |
414 | self.assertDictEqual(response.data, {'type':'FeatureCollection','features':[]}) | |
415 | ||
416 | def test_DistanceToPointFilter_filter_field_none(self): | |
417 | from .views import GeojsonLocationWithinDistanceOfPointList as view | |
418 | original_value = view.distance_filter_field | |
419 | view.distance_filter_field = None | |
420 | url_params = '?dist=%0.4f&point=&format=json' % 5000 | |
421 | response = self.client.get('%s%s' % (self.location_within_distance_of_point_list_url, url_params)) | |
422 | self.assertDictEqual(response.data, {'type':'FeatureCollection','features':[]}) | |
423 | view.distance_filter_field = original_value | |
424 | ||
425 | def test_DistanceToPointFilter_ValueError_point(self): | |
426 | url_params = '?dist=500.0&point=hello&format=json' | |
427 | response = self.client.get('%s%s' % (self.location_within_distance_of_point_list_url, url_params)) | |
428 | self.assertEqual(response.data['detail'], 'Invalid geometry string supplied for parameter point') | |
429 | ||
430 | def test_DistanceToPointFilter_ValueError_distance(self): | |
431 | url_params = '?dist=wrong&point=12.0,42.0&format=json' | |
432 | response = self.client.get('%s%s' % (self.location_within_distance_of_point_list_url, url_params)) | |
433 | self.assertEqual(response.data['detail'], 'Invalid distance string supplied for parameter dist') |
0 | import sys | |
1 | from django.conf import settings | |
2 | ||
3 | # this test must be run explicitly | |
4 | # either by calling: | |
5 | # django test --keepdb django_restframework_gis_tests.test_performance | |
6 | # or by setting ``settings.TEST_PERFORMANCE`` to ``True`` | |
7 | if 'django_restframework_gis_tests.test_performance' in sys.argv or settings.TEST_PERFORMANCE: | |
8 | from django.test import TestCase | |
9 | from django.core.urlresolvers import reverse | |
10 | from rest_framework.renderers import JSONRenderer | |
11 | from rest_framework_gis import serializers as gis_serializers | |
12 | from contexttimer import Timer | |
13 | from .models import Location | |
14 | ||
15 | ||
16 | class TestRestFrameworkGisPerformance(TestCase): | |
17 | NUMBER_OF_LOCATIONS = 10000 | |
18 | ||
19 | def _create_data(self): | |
20 | """ creates a bunch of gis models instances """ | |
21 | locations = [] | |
22 | name = 'l{0}' | |
23 | slug = 'l{0}' | |
24 | wkt = 'POINT (13.{0}125000020002 42.{0}565179379999)' | |
25 | for n in range(1, self.NUMBER_OF_LOCATIONS): | |
26 | locations.append(Location(name=name.format(n), | |
27 | slug=slug.format(n), | |
28 | geometry=wkt.format(n))) | |
29 | Location.objects.bulk_create(locations) | |
30 | ||
31 | def test_geojson_performance(self): | |
32 | class PerfSerializer(gis_serializers.GeoFeatureModelSerializer): | |
33 | class Meta: | |
34 | model = Location | |
35 | geo_field = 'geometry' | |
36 | # create data | |
37 | self._create_data() | |
38 | # initialize serializer | |
39 | serializer = PerfSerializer(Location.objects.all(), many=True) | |
40 | with Timer() as t: | |
41 | JSONRenderer().render(serializer.data) | |
42 | # print results | |
43 | msg = 'GeoJSON rendering of {0} objects '\ | |
44 | 'completed in {1}'.format(self.NUMBER_OF_LOCATIONS, t.elapsed) | |
45 | print('\n\033[95m{0}\033[0m'.format(msg)) |
1 | 1 | unit tests for restframework_gis |
2 | 2 | """ |
3 | 3 | |
4 | try: | |
5 | import simplejson as json | |
6 | except ImportError: | |
7 | import json | |
8 | ||
9 | ||
10 | 4 | import urllib |
11 | 5 | import sys |
6 | import json | |
7 | ||
12 | 8 | from django.test import TestCase |
13 | from django.contrib.gis.geos import GEOSGeometry, Polygon | |
9 | from django.contrib.gis.geos import GEOSGeometry, Polygon, Point | |
14 | 10 | from django.core.urlresolvers import reverse |
15 | ||
16 | from .models import Location | |
11 | from django.core.exceptions import ImproperlyConfigured | |
12 | ||
13 | from rest_framework_gis import serializers as gis_serializers | |
14 | ||
15 | from .models import Location, LocatedFile | |
16 | from .serializers import LocationGeoSerializer | |
17 | 17 | |
18 | 18 | |
19 | 19 | class TestRestFrameworkGis(TestCase): |
20 | ||
21 | 20 | def setUp(self): |
22 | 21 | self.location_list_url = reverse('api_location_list') |
23 | 22 | self.geojson_location_list_url = reverse('api_geojson_location_list') |
24 | self.geos_error_message = 'Invalid format: string or unicode input unrecognized as WKT EWKT, and HEXEWKB.' | |
23 | self.geos_error_message = 'Invalid format: string or unicode input unrecognized as GeoJSON, WKT EWKT or HEXEWKB.' | |
25 | 24 | |
26 | 25 | def _create_locations(self): |
27 | 26 | self.l1 = Location.objects.create(id=1, name='l1', slug='l1', geometry='POINT (13.0078125000020002 42.4234565179379999)') |
33 | 32 | |
34 | 33 | def test_post_location_list_geojson(self): |
35 | 34 | self.assertEqual(Location.objects.count(), 0) |
36 | ||
37 | 35 | data = { |
38 | 36 | "name": "geojson input test", |
39 | 37 | "geometry": { |
49 | 47 | ] |
50 | 48 | } |
51 | 49 | } |
52 | ||
53 | response = self.client.post(self.location_list_url, data=json.dumps(data), content_type='application/json') | |
54 | self.assertEqual(response.status_code, 201) | |
55 | self.assertEqual(Location.objects.count(), 1) | |
56 | ||
50 | response = self.client.post(self.location_list_url, data=json.dumps(data), content_type='application/json') | |
51 | self.assertEqual(response.status_code, 201) | |
52 | self.assertEqual(Location.objects.count(), 1) | |
57 | 53 | data = { |
58 | 54 | "name": "geojson input test2", |
59 | 55 | "geometry": { |
71 | 67 | def test_post_location_list_geojson_as_multipartformdata(self): |
72 | 68 | """ emulate sending geojson string in webform """ |
73 | 69 | self.assertEqual(Location.objects.count(), 0) |
74 | ||
75 | 70 | data = { |
76 | 71 | "name": "geojson input test", |
77 | 72 | "geometry": json.dumps({ |
87 | 82 | ] |
88 | 83 | }) |
89 | 84 | } |
90 | ||
91 | 85 | response = self.client.post(self.location_list_url, data) |
92 | 86 | self.assertEqual(response.status_code, 201) |
93 | 87 | self.assertEqual(Location.objects.count(), 1) |
94 | 88 | |
95 | 89 | def test_post_HTML_browsable_api(self): |
96 | 90 | self.assertEqual(Location.objects.count(), 0) |
97 | ||
98 | 91 | data = { |
99 | 92 | "name": "geojson input test2", |
100 | 93 | "slug": "prova", |
114 | 107 | response = self.client.post(self.location_list_url, data, HTTP_ACCEPT='text/html') |
115 | 108 | self.assertEqual(response.status_code, 201) |
116 | 109 | self.assertEqual(Location.objects.count(), 1) |
117 | ||
118 | 110 | location = Location.objects.all()[0] |
119 | 111 | self.assertEqual(location.name, 'geojson input test2') |
120 | 112 | self.assertEqual(location.slug, 'prova') |
121 | 113 | |
122 | 114 | def test_post_location_list_WKT(self): |
123 | 115 | self.assertEqual(Location.objects.count(), 0) |
124 | ||
125 | 116 | data = { |
126 | 117 | 'name': 'WKT input test', |
127 | 118 | 'geometry': 'POINT (12.492324113849 41.890307434153)' |
130 | 121 | self.assertEqual(response.status_code, 201) |
131 | 122 | self.assertEqual(Location.objects.count(), 1) |
132 | 123 | |
124 | def test_post_location_list_EWKT(self): | |
125 | self.assertEqual(Location.objects.count(), 0) | |
126 | data = { | |
127 | 'name': 'EWKT input test', | |
128 | 'geometry': 'SRID=28992;POINT(221160 600204)' | |
129 | } | |
130 | response = self.client.post(self.location_list_url, data) | |
131 | expected_coords = (6.381495826183805, 53.384066927384985) | |
132 | self.assertEqual(response.status_code, 201) | |
133 | self.assertEqual(Location.objects.count(), 1) | |
134 | self.assertEquals( | |
135 | Location.objects.get(name='EWKT input test').geometry.coords, | |
136 | expected_coords | |
137 | ) | |
138 | ||
133 | 139 | def test_post_location_list_WKT_as_json(self): |
134 | 140 | self.assertEqual(Location.objects.count(), 0) |
135 | ||
136 | 141 | data = { |
137 | 142 | 'name': 'WKT input test', |
138 | 143 | 'geometry': 'POINT (12.492324113849 41.890307434153)' |
144 | 149 | def test_post_location_list_empty_geometry(self): |
145 | 150 | data = { 'name': 'empty input test' } |
146 | 151 | response = self.client.post(self.location_list_url, data) |
147 | self.assertEqual(response.data['geometry'][0], u'This field is required.') | |
148 | ||
152 | self.assertEqual(response.data['geometry'][0], 'This field is required.') | |
149 | 153 | data = { 'name': 'empty input test', 'geometry': '' } |
150 | 154 | response = self.client.post(self.location_list_url, data) |
151 | self.assertEqual(response.data['geometry'][0], u'This field is required.') | |
152 | ||
155 | self.assertEqual(response.data['geometry'][0], 'This field is required.') | |
153 | 156 | data = { 'name': 'empty input test' } |
154 | 157 | response = self.client.post(self.location_list_url, data=json.dumps(data), content_type='application/json') |
155 | self.assertEqual(response.data['geometry'][0], u'This field is required.') | |
156 | ||
158 | self.assertEqual(response.data['geometry'][0], 'This field is required.') | |
157 | 159 | data = { 'name': 'empty input test', 'geometry': '' } |
158 | 160 | response = self.client.post(self.location_list_url, data=json.dumps(data), content_type='application/json') |
159 | self.assertEqual(response.data['geometry'][0], u'This field is required.') | |
161 | self.assertEqual(response.data['geometry'][0], 'This field is required.') | |
160 | 162 | |
161 | 163 | def test_post_location_list_invalid_WKT(self): |
162 | 164 | data = { |
167 | 169 | self.assertEqual(response.status_code, 400) |
168 | 170 | self.assertEqual(Location.objects.count(), 0) |
169 | 171 | self.assertEqual(response.data['geometry'][0], self.geos_error_message) |
170 | ||
171 | 172 | # repeat as multipart form data |
172 | 173 | response = self.client.post(self.location_list_url, data) |
173 | 174 | self.assertEqual(response.data['geometry'][0], self.geos_error_message) |
174 | ||
175 | 175 | data = { |
176 | 176 | 'name': 'I AM MODERATELY WRONG', |
177 | 177 | 'geometry': 'POINT (12.492324113849, 41.890307434153)' |
178 | 178 | } |
179 | 179 | response = self.client.post(self.location_list_url, data=json.dumps(data), content_type='application/json') |
180 | 180 | self.assertEqual(response.data['geometry'][0], self.geos_error_message) |
181 | ||
182 | 181 | # repeat as multipart form data |
183 | 182 | response = self.client.post(self.location_list_url, data) |
184 | 183 | self.assertEqual(response.data['geometry'][0], self.geos_error_message) |
198 | 197 | } |
199 | 198 | response = self.client.post(self.location_list_url, data=json.dumps(data), content_type='application/json') |
200 | 199 | self.assertEqual(response.data['geometry'][0], self.geos_error_message) |
201 | ||
202 | 200 | data = { |
203 | 201 | "name": "very wrong", |
204 | 202 | "geometry": ['a', 'b', 'c'] |
205 | 203 | } |
206 | 204 | response = self.client.post(self.location_list_url, data=json.dumps(data), content_type='application/json') |
207 | 205 | self.assertEqual(response.data['geometry'][0], self.geos_error_message) |
208 | ||
209 | 206 | data = { |
210 | 207 | "name": "very wrong", |
211 | 208 | "geometry": False |
212 | 209 | } |
213 | 210 | response = self.client.post(self.location_list_url, data=json.dumps(data), content_type='application/json') |
214 | 211 | self.assertEqual(response.data['geometry'][0], self.geos_error_message) |
215 | ||
216 | 212 | data = { |
217 | 213 | "name": "very wrong", |
218 | 214 | "geometry": { "value": { "nested": ["yo"] } } |
223 | 219 | def test_geojson_format(self): |
224 | 220 | """ test geojson format """ |
225 | 221 | location = Location.objects.create(name='geojson test', geometry='POINT (135.0 45.0)') |
226 | ||
227 | 222 | url = reverse('api_geojson_location_details', args=[location.id]) |
228 | 223 | expected = { |
229 | 224 | 'id': location.id, |
247 | 242 | self.assertCountEqual(json.dumps(response.data), json.dumps(expected)) |
248 | 243 | else: |
249 | 244 | self.assertItemsEqual(json.dumps(response.data), json.dumps(expected)) |
250 | ||
251 | 245 | response = self.client.get(url, HTTP_ACCEPT='text/html') |
252 | 246 | self.assertContains(response, "Kool geojson test") |
253 | 247 | |
254 | 248 | def test_geojson_id_attribute(self): |
255 | 249 | location = Location.objects.create(name='geojson test', geometry='POINT (10.1 10.1)') |
256 | ||
257 | 250 | url = reverse('api_geojson_location_details', args=[location.id]) |
258 | 251 | response = self.client.get(url) |
259 | 252 | self.assertEqual(response.data['id'], location.id) |
260 | 253 | |
261 | 254 | def test_geojson_id_attribute_slug(self): |
262 | 255 | location = Location.objects.create(name='geojson test', geometry='POINT (10.1 10.1)') |
263 | ||
264 | 256 | url = reverse('api_geojson_location_slug_details', args=[location.slug]) |
265 | 257 | response = self.client.get(url) |
266 | 258 | self.assertEqual(response.data['id'], location.slug) |
267 | 259 | |
268 | 260 | def test_geojson_false_id_attribute_slug(self): |
269 | 261 | location = Location.objects.create(name='geojson test', geometry='POINT (10.1 10.1)') |
270 | ||
271 | 262 | url = reverse('api_geojson_location_falseid_details', args=[location.id]) |
272 | 263 | response = self.client.get(url) |
273 | 264 | with self.assertRaises(KeyError): |
274 | 265 | response.data['id'] |
275 | 266 | |
267 | def test_geojson_filefield_attribute(self): | |
268 | located_file = LocatedFile.objects.create(name='geojson filefield test', geometry='POINT (10.1 10.1)') | |
269 | url = reverse('api_geojson_located_file_details', args=[located_file.id]) | |
270 | response = self.client.get(url) | |
271 | self.assertEqual(response.data['properties']['file'], None) | |
272 | ||
276 | 273 | def test_post_geojson_location_list(self): |
277 | 274 | self.assertEqual(Location.objects.count(), 0) |
278 | ||
279 | 275 | data = { |
280 | 276 | "type": "Feature", |
281 | 277 | "properties": { |
290 | 286 | ] |
291 | 287 | } |
292 | 288 | } |
293 | ||
294 | 289 | response = self.client.post(self.geojson_location_list_url, data=json.dumps(data), content_type='application/json') |
295 | 290 | self.assertEqual(response.status_code, 201) |
296 | 291 | self.assertEqual(Location.objects.count(), 1) |
297 | ||
298 | 292 | url = reverse('api_geojson_location_details', args=[Location.objects.order_by('-id')[0].id]) |
299 | 293 | response = self.client.get(url) |
300 | 294 | self.assertEqual(response.data['properties']['name'], "point?") |
304 | 298 | |
305 | 299 | def test_post_geojson_location_list_HTML(self): |
306 | 300 | self.assertEqual(Location.objects.count(), 0) |
307 | ||
308 | 301 | data = { |
309 | 302 | "type": "Feature", |
310 | 303 | "properties": { |
319 | 312 | ] |
320 | 313 | } |
321 | 314 | } |
322 | ||
323 | 315 | response = self.client.post(self.geojson_location_list_url, data=json.dumps(data), content_type='application/json', HTTP_ACCEPT='text/html') |
324 | 316 | self.assertEqual(response.status_code, 201) |
325 | 317 | self.assertEqual(Location.objects.count(), 1) |
326 | ||
327 | 318 | url = reverse('api_geojson_location_details', args=[Location.objects.order_by('-id')[0].id]) |
328 | 319 | response = self.client.get(url) |
329 | 320 | self.assertEqual(response.data['properties']['name'], "point?") |
345 | 336 | ] |
346 | 337 | } |
347 | 338 | } |
348 | ||
349 | 339 | response = self.client.post(self.geojson_location_list_url, data=json.dumps(data), content_type='application/json') |
350 | 340 | self.assertEqual(response.status_code, 400) |
351 | 341 | self.assertEqual(Location.objects.count(), 0) |
352 | 342 | self.assertEqual(response.data['name'][0], "This field is required.") |
353 | ||
354 | 343 | data = { |
355 | 344 | "type": "Feature", |
356 | 345 | "properties": { |
368 | 357 | |
369 | 358 | def test_post_geojson_location_list_WKT(self): |
370 | 359 | self.assertEqual(Location.objects.count(), 0) |
371 | ||
372 | 360 | data = { |
373 | 361 | "type": "Feature", |
374 | 362 | "properties": { |
376 | 364 | }, |
377 | 365 | "geometry": "POINT (10.1 10.1)" |
378 | 366 | } |
379 | ||
380 | 367 | response = self.client.post(self.geojson_location_list_url, data=json.dumps(data), content_type='application/json') |
381 | 368 | self.assertEqual(response.status_code, 201) |
382 | 369 | self.assertEqual(Location.objects.count(), 1) |
383 | ||
384 | 370 | url = reverse('api_geojson_location_details', args=[Location.objects.order_by('-id')[0].id]) |
385 | 371 | response = self.client.get(url) |
386 | 372 | self.assertEqual(response.data['properties']['name'], "point?") |
389 | 375 | |
390 | 376 | def test_geofeatured_model_serializer_compatible_with_geomodel_serializer(self): |
391 | 377 | self.assertEqual(Location.objects.count(), 0) |
392 | ||
393 | 378 | data = { |
394 | 379 | "name": "geojson input test", |
395 | 380 | "geometry": { |
405 | 390 | ] |
406 | 391 | } |
407 | 392 | } |
408 | ||
409 | 393 | response = self.client.post(self.geojson_location_list_url, data=json.dumps(data), content_type='application/json') |
410 | 394 | self.assertEqual(response.status_code, 201) |
411 | 395 | self.assertEqual(Location.objects.count(), 1) |
413 | 397 | def test_geofeatured_model_post_as_multipartformdata(self): |
414 | 398 | """ emulate sending geojson string in webform """ |
415 | 399 | self.assertEqual(Location.objects.count(), 0) |
416 | ||
417 | 400 | data = { |
418 | 401 | "name": "geojson input test", |
419 | 402 | "geometry": json.dumps({ |
424 | 407 | ] |
425 | 408 | }) |
426 | 409 | } |
427 | ||
428 | 410 | response = self.client.post(self.location_list_url, data) |
429 | 411 | self.assertEqual(response.status_code, 201) |
430 | 412 | self.assertEqual(Location.objects.count(), 1) |
433 | 415 | def test_HTML_browsable_geojson_location_list(self): |
434 | 416 | response = self.client.get(self.geojson_location_list_url, HTTP_ACCEPT='text/html') |
435 | 417 | self.assertEqual(response.status_code, 200) |
436 | ||
437 | 418 | self._create_locations() |
438 | ||
439 | 419 | response = self.client.get(self.geojson_location_list_url, HTTP_ACCEPT='text/html') |
440 | 420 | self.assertContains(response, 'l1') |
441 | 421 | self.assertContains(response, 'l2') |
442 | 422 | |
443 | 423 | def test_post_geojson_location_list_HTML_web_form(self): |
444 | 424 | self.assertEqual(Location.objects.count(), 0) |
445 | ||
446 | 425 | data = { |
447 | 426 | "name": "HTML test", |
448 | 427 | "geometry": json.dumps({ |
453 | 432 | ] |
454 | 433 | }) |
455 | 434 | } |
456 | ||
457 | 435 | response = self.client.post(self.geojson_location_list_url, data, HTTP_ACCEPT='text/html') |
458 | 436 | self.assertEqual(response.status_code, 201) |
459 | 437 | self.assertEqual(Location.objects.count(), 1) |
460 | ||
461 | 438 | location = Location.objects.all()[0] |
462 | 439 | self.assertEqual(location.name, "HTML test") |
463 | 440 | self.assertEqual(location.geometry.geom_type, "Point") |
464 | 441 | |
465 | 442 | def test_post_geojson_location_list_HTML_web_form_WKT(self): |
466 | 443 | self.assertEqual(Location.objects.count(), 0) |
467 | ||
468 | 444 | data = { |
469 | 445 | "name": "HTML test WKT", |
470 | 446 | "geometry": "POINT (10.1 10.1)" |
471 | 447 | } |
472 | ||
473 | 448 | response = self.client.post(self.geojson_location_list_url, data, HTTP_ACCEPT='text/html') |
474 | #print response.content | |
475 | self.assertEqual(response.status_code, 201) | |
476 | self.assertEqual(Location.objects.count(), 1) | |
477 | ||
449 | self.assertEqual(response.status_code, 201) | |
450 | self.assertEqual(Location.objects.count(), 1) | |
478 | 451 | location = Location.objects.all()[0] |
479 | 452 | self.assertEqual(location.name, "HTML test WKT") |
480 | 453 | |
484 | 457 | self.assertContains(response, '<textarea name="geometry"') |
485 | 458 | self.assertContains(response, '"type": "Point"') |
486 | 459 | self.assertContains(response, '"coordinates": [') |
487 | # textarea input should contain valid GeoJSON indented for readability | |
488 | # doesn't seem possible with DRF 3.0 | |
489 | # let's wait for DRF 3.1 | |
490 | #self.assertNotContains(response, 'u'type': u'Point', u'coordinates':') | |
460 | # TODO: remove this when python 2 will be deprecated | |
461 | self.assertNotContains(response, 'u'type': u'Point', u'coordinates':') | |
462 | ||
463 | def test_patch_geojson_location(self): | |
464 | location = Location.objects.create(name='geojson patch test', geometry='POINT (135.0 45.0)') | |
465 | url = reverse('api_geojson_location_details', args=[location.id]) | |
466 | data = { | |
467 | "properties": { | |
468 | "name":"geojson successful patch test" | |
469 | }, | |
470 | "geometry": { | |
471 | "type": "Point", | |
472 | "coordinates": [10.1, 10.1] | |
473 | } | |
474 | } | |
475 | response = self.client.generic('PATCH', url, json.dumps(data), content_type='application/json') | |
476 | self.assertEqual(response.status_code, 200) | |
477 | location_reloaded = Location.objects.get(pk=location.id) | |
478 | self.assertEquals(location_reloaded.name, 'geojson successful patch test') | |
479 | self.assertEquals(location_reloaded.geometry, Point(10.1, 10.1)) | |
480 | ||
481 | def test_patch_geojson_location_wo_changing_geometry(self): | |
482 | location = Location.objects.create(name='geojson patch test', geometry='POINT (135.0 45.0)') | |
483 | url = reverse('api_geojson_location_details', args=[location.id]) | |
484 | data = { | |
485 | "properties": { | |
486 | "name":"geojson successful patch test" | |
487 | } | |
488 | } | |
489 | response = self.client.generic('PATCH', url, json.dumps(data), content_type='application/json') | |
490 | self.assertEqual(response.status_code, 200) | |
491 | location_reloaded = Location.objects.get(pk=location.id) | |
492 | self.assertEquals(location_reloaded.name, 'geojson successful patch test') | |
493 | self.assertEquals(location_reloaded.geometry, Point(135.0, 45.0)) | |
494 | ||
495 | def test_geometry_serializer_method_field(self): | |
496 | location = Location.objects.create(name='geometry serializer method test', geometry='POINT (135.0 45.0)') | |
497 | location_loaded = Location.objects.get(pk=location.id) | |
498 | self.assertEqual(location_loaded.name, 'geometry serializer method test') | |
499 | self.assertEqual(location_loaded.geometry, Point(135.0, 45.0)) | |
500 | url = reverse('api_geojson_location_details_hidden', args=[location.id]) | |
501 | data = { | |
502 | "properties": { | |
503 | "name":"hidden geometry" | |
504 | } | |
505 | } | |
506 | response = self.client.generic('PATCH', url, json.dumps(data), content_type='application/json') | |
507 | self.assertEqual(response.status_code, 200) | |
508 | self.assertEqual(response.data['properties']['name'], 'hidden geometry') | |
509 | self.assertEqual(response.data['geometry']['type'], 'Point') | |
510 | self.assertEqual(response.data['geometry']['coordinates'], (0.0, 0.0)) | |
511 | ||
512 | def test_geometry_serializer_method_field_none(self): | |
513 | location = Location.objects.create(name='None value', geometry='POINT (135.0 45.0)') | |
514 | location_loaded = Location.objects.get(pk=location.id) | |
515 | self.assertEqual(location_loaded.geometry, Point(135.0, 45.0)) | |
516 | url = reverse('api_geojson_location_details_none', args=[location.id]) | |
517 | response = self.client.generic('GET', url, content_type='application/json') | |
518 | self.assertEqual(response.status_code, 200) | |
519 | self.assertEqual(response.data['properties']['name'], 'None value') | |
520 | self.assertEqual(response.data['geometry'], None) | |
521 | ||
522 | def test_filterset(self): | |
523 | from rest_framework_gis.filterset import GeoFilterSet | |
524 | ||
525 | def test_geometry_field_to_representation_none(self): | |
526 | self._create_locations() | |
527 | f = LocationGeoSerializer(instance=self.l1).fields['geometry'] | |
528 | self.assertIsNone(f.to_representation(None)) | |
529 | ||
530 | def test_geometry_field_to_internal_value_none(self): | |
531 | self._create_locations() | |
532 | f = LocationGeoSerializer(instance=self.l1).fields['geometry'] | |
533 | self.assertIsNone(f.to_internal_value(None)) | |
534 | ||
535 | def test_no_geo_field_improperly_configured(self): | |
536 | class LocationGeoFeatureSerializer(gis_serializers.GeoFeatureModelSerializer): | |
537 | class Meta: | |
538 | model = Location | |
539 | with self.assertRaises(ImproperlyConfigured): | |
540 | LocationGeoFeatureSerializer() | |
541 | ||
542 | def test_exclude_geo_field_improperly_configured(self): | |
543 | self._create_locations() | |
544 | class LocationGeoFeatureSerializer(gis_serializers.GeoFeatureModelSerializer): | |
545 | class Meta: | |
546 | model = Location | |
547 | geo_field = 'geometry' | |
548 | exclude = ('geometry', ) | |
549 | with self.assertRaises(ImproperlyConfigured): | |
550 | LocationGeoFeatureSerializer(instance=self.l1) | |
551 | ||
552 | def test_geojson_pagination(self): | |
553 | self._create_locations() | |
554 | response = self.client.get(self.geojson_location_list_url) | |
555 | self.assertEqual(response.data['type'], 'FeatureCollection') | |
556 | self.assertEqual(len(response.data['features']), 2) | |
557 | response = self.client.get('{0}?page_size=1'.format(self.geojson_location_list_url)) | |
558 | self.assertEqual(response.data['type'], 'FeatureCollection') | |
559 | self.assertEqual(len(response.data['features']), 1) | |
560 | self.assertIn('next', response.data) | |
561 | self.assertIn('previous', response.data) | |
562 | ||
563 | def test_pickle(self): | |
564 | import pickle | |
565 | from rest_framework_gis.fields import GeoJsonDict | |
566 | geometry = GEOSGeometry('POINT (30 10)') | |
567 | geojsondict = GeoJsonDict(( | |
568 | ('type', geometry.geom_type), | |
569 | ('coordinates', geometry.coords), | |
570 | )) | |
571 | pickled = pickle.dumps(geojsondict) | |
572 | restored = pickle.loads(pickled) | |
573 | self.assertEqual(restored, geojsondict) |
3 | 3 | urlpatterns = patterns('django_restframework_gis_tests.views', |
4 | 4 | url(r'^$', 'location_list', name='api_location_list'), |
5 | 5 | url(r'^(?P<pk>[0-9]+)/$', 'location_details', name='api_location_details'), |
6 | ||
6 | ||
7 | 7 | # geojson |
8 | 8 | url(r'^geojson/$', 'geojson_location_list', name='api_geojson_location_list'), |
9 | 9 | url(r'^geojson/(?P<pk>[0-9]+)/$', 'geojson_location_details', name='api_geojson_location_details'), |
10 | url(r'^geojson_hidden/(?P<pk>[0-9]+)/$', 'geojson_location_details_hidden', name='api_geojson_location_details_hidden'), | |
11 | url(r'^geojson_none/(?P<pk>[0-9]+)/$', 'geojson_location_details_none', name='api_geojson_location_details_none'), | |
10 | 12 | url(r'^geojson/(?P<slug>[-\w]+)/$', 'geojson_location_slug_details', name='api_geojson_location_slug_details'), |
11 | 13 | url(r'^geojson-falseid/(?P<pk>[0-9]+)/$', 'geojson_location_falseid_details', name='api_geojson_location_falseid_details'), |
14 | ||
15 | # file | |
16 | url(r'^geojson-file/(?P<pk>[0-9]+)/$', 'geojson_located_file_details', name='api_geojson_located_file_details'), | |
17 | ||
18 | # geojson with bbox with its own geometry field | |
19 | url(r'^geojson-with-bbox/$', 'geojson_boxedlocation_list', name='api_geojson_boxedlocation_list'), | |
20 | url(r'^geojson-with-bbox/(?P<pk>[0-9]+)/$', 'geojson_boxedlocation_details', name='api_geojson_boxedlocation_details'), | |
21 | ||
22 | # geojson with bbox with autogenerated bbox | |
23 | url(r'^geojson-with-auto-bbox/$', 'geojson_location_bbox_list', name='api_geojson_location_bbox_list'), | |
12 | 24 | |
13 | 25 | # Filters |
14 | 26 | url(r'^filters/contained_in_bbox$', 'geojson_location_contained_in_bbox_list', name='api_geojson_location_list_contained_in_bbox_filter'), |
0 | 0 | from rest_framework import generics |
1 | 1 | from rest_framework.filters import DjangoFilterBackend |
2 | from rest_framework_gis.filters import * | |
3 | from rest_framework_gis.pagination import GeoJsonPagination | |
2 | 4 | |
3 | 5 | from .models import * |
4 | 6 | from .serializers import * |
5 | from rest_framework_gis.filters import * | |
6 | 7 | |
7 | 8 | |
8 | 9 | class LocationList(generics.ListCreateAPIView): |
9 | 10 | model = Location |
10 | 11 | serializer_class = LocationGeoSerializer |
11 | 12 | queryset = Location.objects.all() |
12 | pagination_serializer_class = PaginatedLocationGeoSerializer | |
13 | paginate_by_param = 'limit' | |
14 | paginate_by = 40 | |
15 | ||
13 | pagination_class = PaginatedLocationGeoSerializer | |
14 | ||
16 | 15 | location_list = LocationList.as_view() |
17 | ||
18 | ||
16 | ||
17 | ||
19 | 18 | class LocationDetails(generics.RetrieveUpdateDestroyAPIView): |
20 | 19 | model = Location |
21 | 20 | serializer_class = LocationGeoSerializer |
28 | 27 | model = Location |
29 | 28 | serializer_class = LocationGeoFeatureSerializer |
30 | 29 | queryset = Location.objects.all() |
31 | pagination_serializer_class = PaginatedLocationGeoFeatureSerializer | |
32 | paginate_by_param = 'limit' | |
33 | paginate_by = 40 | |
34 | ||
30 | pagination_class = GeoJsonPagination | |
31 | ||
35 | 32 | geojson_location_list = GeojsonLocationList.as_view() |
36 | 33 | |
37 | 34 | |
92 | 89 | geojson_location_details = GeojsonLocationDetails.as_view() |
93 | 90 | |
94 | 91 | |
92 | class GeojsonLocationDetailsHidden(generics.RetrieveUpdateDestroyAPIView): | |
93 | model = Location | |
94 | serializer_class = LocationGeoFeatureMethodSerializer | |
95 | queryset = Location.objects.all() | |
96 | ||
97 | geojson_location_details_hidden = GeojsonLocationDetailsHidden.as_view() | |
98 | ||
99 | ||
100 | class GeojsonLocationDetailsNone(generics.RetrieveUpdateDestroyAPIView): | |
101 | model = Location | |
102 | serializer_class = NoneGeoFeatureMethodSerializer | |
103 | queryset = Location.objects.all() | |
104 | ||
105 | geojson_location_details_none = GeojsonLocationDetailsNone.as_view() | |
106 | ||
107 | ||
95 | 108 | class GeojsonLocationSlugDetails(generics.RetrieveUpdateDestroyAPIView): |
96 | 109 | model = Location |
97 | 110 | lookup_field = 'slug' |
115 | 128 | class Meta: |
116 | 129 | model = Location |
117 | 130 | |
118 | ||
119 | 131 | class GeojsonLocationContainedInGeometry(generics.ListAPIView): |
120 | 132 | queryset = Location.objects.all() |
121 | 133 | serializer_class = LocationGeoSerializer |
124 | 136 | filter_backends = (DjangoFilterBackend,) |
125 | 137 | |
126 | 138 | geojson_contained_in_geometry = GeojsonLocationContainedInGeometry.as_view() |
139 | ||
140 | ||
141 | class GeojsonLocatedFileDetails(generics.RetrieveUpdateDestroyAPIView): | |
142 | model = LocatedFile | |
143 | serializer_class = LocatedFileGeoFeatureSerializer | |
144 | queryset = LocatedFile.objects.all() | |
145 | ||
146 | geojson_located_file_details = GeojsonLocatedFileDetails.as_view() | |
147 | ||
148 | ||
149 | class GeojsonBoxedLocationDetails(generics.RetrieveUpdateDestroyAPIView): | |
150 | model = BoxedLocation | |
151 | serializer_class = BoxedLocationGeoFeatureSerializer | |
152 | queryset = BoxedLocation.objects.all() | |
153 | ||
154 | geojson_boxedlocation_details = GeojsonBoxedLocationDetails.as_view() | |
155 | ||
156 | ||
157 | class GeojsonBoxedLocationList(generics.ListCreateAPIView): | |
158 | model = BoxedLocation | |
159 | serializer_class = BoxedLocationGeoFeatureSerializer | |
160 | queryset = BoxedLocation.objects.all() | |
161 | ||
162 | geojson_boxedlocation_list = GeojsonBoxedLocationList.as_view() | |
163 | ||
164 | ||
165 | class GeojsonLocationBboxList(generics.ListCreateAPIView): | |
166 | model = Location | |
167 | serializer_class = LocationGeoFeatureBboxSerializer | |
168 | queryset = Location.objects.all() | |
169 | ||
170 | geojson_location_bbox_list = GeojsonLocationBboxList.as_view() |
0 | from settings import * | |
0 | # RENAME THIS FILE TO local_settings.py IF YOU NEED TO CUSTOMIZE SOME SETTINGS | |
1 | # BUT DO NOT COMMIT | |
1 | 2 | |
2 | # RENAME THIS FILE local_settings.py IF YOU NEED TO CUSTOMIZE SOME SETTINGS | |
3 | # BUT DO NOT COMMIT | |
3 | TEST_PERFORMANCE = True | |
4 | 4 | |
5 | 5 | #DATABASES = { |
6 | 6 | # 'default': { |
20 | 20 | # 'django.contrib.messages', |
21 | 21 | # 'django.contrib.staticfiles', |
22 | 22 | # 'django.contrib.gis', |
23 | # | |
23 | # | |
24 | 24 | # # geodjango widgets |
25 | 25 | # 'olwidget', |
26 | # | |
26 | # | |
27 | 27 | # # admin |
28 | 28 | # #'grappelli', |
29 | 29 | # 'django.contrib.admin', |
30 | # | |
30 | # | |
31 | 31 | # # rest framework |
32 | 32 | # 'rest_framework', |
33 | 33 | # 'rest_framework_gis', |
34 | # | |
34 | # | |
35 | 35 | # # test app |
36 | 36 | # 'django_restframework_gis_tests' |
37 | #)⏎ | |
37 | #) |
0 | 0 | import os |
1 | ||
2 | TEST_PERFORMANCE = False | |
1 | 3 | |
2 | 4 | DEBUG = True |
3 | 5 | TEMPLATE_DEBUG = DEBUG |
31 | 33 | 'django_restframework_gis_tests' |
32 | 34 | ) |
33 | 35 | |
36 | MIDDLEWARE_CLASSES = [ | |
37 | 'django.middleware.common.CommonMiddleware', | |
38 | 'django.middleware.csrf.CsrfViewMiddleware', | |
39 | ] | |
40 | ||
34 | 41 | ROOT_URLCONF = 'urls' |
35 | 42 | |
36 | 43 | TIME_ZONE = 'Europe/Rome' |