├── .coveragerc
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── CHANGES
├── LICENSE
├── MANIFEST.in
├── README.rst
├── djgeojson
├── __init__.py
├── fields.py
├── http.py
├── models.py
├── nogeos.py
├── serializers.py
├── templatetags
│ ├── __init__.py
│ └── geojson_tags.py
├── tests.py
└── views.py
├── docs
├── Makefile
├── advanced.rst
├── conf.py
├── index.rst
├── installation.rst
├── make.bat
├── models.rst
└── views.rst
├── quicktest.py
├── setup.py
└── tox.ini
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | source = djgeojson
3 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Python application
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | tests_python:
7 | name: Test on Python ${{ matrix.python_version }}
8 | runs-on: ubuntu-latest
9 | strategy:
10 | matrix:
11 | python_version:
12 | - '3.9'
13 | - '3.10'
14 | - '3.11'
15 | - '3.12'
16 | - '3.13'
17 | env:
18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
19 |
20 | steps:
21 | - uses: actions/checkout@v4
22 |
23 | - name: Set up Python ${{ matrix.python_version }}
24 | uses: actions/setup-python@v5
25 | with:
26 | python-version: ${{ matrix.python_version }}
27 |
28 | - name: Cache pip
29 | uses: actions/cache@v4
30 | with:
31 | # This path is specific to Ubuntu
32 | path: ~/.cache/pip
33 | key: ${{ runner.os }}-pip-${{ matrix.python_version }}
34 |
35 | - name: Install dependencies
36 | run: |
37 | sudo apt-get update -qq
38 | sudo apt-get install -y libproj-dev libgeos-dev gdal-bin libgdal-dev libsqlite3-mod-spatialite
39 | python -m pip install --upgrade pip
40 | python -m pip install --upgrade 'tox>=4.0.0rc3'
41 |
42 | - name: Run tox targets for ${{ matrix.python_version }}
43 | run: tox run -f py$(echo ${{ matrix.python_version }} | tr -d .)
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/django,intellij+iml,python
3 |
4 | ### Django ###
5 | *.log
6 | *.pot
7 | *.pyc
8 | __pycache__/
9 | local_settings.py
10 | db.sqlite3
11 | media
12 |
13 | ### Intellij+iml ###
14 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
15 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
16 |
17 | # User-specific stuff:
18 | .idea/**/workspace.xml
19 | .idea/**/tasks.xml
20 |
21 | # Sensitive or high-churn files:
22 | .idea/**/dataSources/
23 | .idea/**/dataSources.ids
24 | .idea/**/dataSources.xml
25 | .idea/**/dataSources.local.xml
26 | .idea/**/sqlDataSources.xml
27 | .idea/**/dynamic.xml
28 | .idea/**/uiDesigner.xml
29 |
30 | # Gradle:
31 | .idea/**/gradle.xml
32 | .idea/**/libraries
33 |
34 | # Mongo Explorer plugin:
35 | .idea/**/mongoSettings.xml
36 |
37 | ## File-based project format:
38 | *.iws
39 |
40 | ## Plugin-specific files:
41 |
42 | # IntelliJ
43 | /out/
44 |
45 | # mpeltonen/sbt-idea plugin
46 | .idea_modules/
47 |
48 | # JIRA plugin
49 | atlassian-ide-plugin.xml
50 |
51 | # Crashlytics plugin (for Android Studio and IntelliJ)
52 | com_crashlytics_export_strings.xml
53 | crashlytics.properties
54 | crashlytics-build.properties
55 | fabric.properties
56 |
57 | ### Intellij+iml Patch ###
58 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
59 |
60 | *.iml
61 | modules.xml
62 | .idea/misc.xml
63 | *.ipr
64 |
65 | ### Python ###
66 | # Byte-compiled / optimized / DLL files
67 | *.py[cod]
68 | *$py.class
69 |
70 | # C extensions
71 | *.so
72 |
73 | # Distribution / packaging
74 | .Python
75 | env/
76 | build/
77 | develop-eggs/
78 | dist/
79 | downloads/
80 | eggs/
81 | .eggs/
82 | lib/
83 | lib64/
84 | parts/
85 | sdist/
86 | var/
87 | wheels/
88 | *.egg-info/
89 | .installed.cfg
90 | *.egg
91 |
92 | # PyInstaller
93 | # Usually these files are written by a python script from a template
94 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
95 | *.manifest
96 | *.spec
97 |
98 | # Installer logs
99 | pip-log.txt
100 | pip-delete-this-directory.txt
101 |
102 | # Unit test / coverage reports
103 | htmlcov/
104 | .tox/
105 | .coverage
106 | .coverage.*
107 | .cache
108 | nosetests.xml
109 | coverage.xml
110 | *,cover
111 | .hypothesis/
112 |
113 | # Translations
114 | *.mo
115 |
116 | # Django stuff:
117 |
118 | # Flask stuff:
119 | instance/
120 | .webassets-cache
121 |
122 | # Scrapy stuff:
123 | .scrapy
124 |
125 | # Sphinx documentation
126 | docs/_build/
127 |
128 | # PyBuilder
129 | target/
130 |
131 | # Jupyter Notebook
132 | .ipynb_checkpoints
133 |
134 | # pyenv
135 | .python-version
136 |
137 | # celery beat schedule file
138 | celerybeat-schedule
139 |
140 | # dotenv
141 | .env
142 |
143 | # virtualenv
144 | .venv
145 | venv/
146 | ENV/
147 |
148 | # Spyder project settings
149 | .spyderproject
150 |
151 | # Rope project settings
152 | .ropeproject
153 |
154 | # End of https://www.gitignore.io/api/django,intellij+iml,python
155 |
156 | /.idea
--------------------------------------------------------------------------------
/CHANGES:
--------------------------------------------------------------------------------
1 | =========
2 | CHANGELOG
3 | =========
4 |
5 | 4.2.1 (unreleased)
6 | ==================
7 |
8 | -
9 |
10 | 4.2.0 (2025-10-03)
11 | ==================
12 |
13 | - Drop Python 3.8, Django 4.0, and Django 4.1.
14 | - Support Python 3.13, Django 5.1 and Django 5.2.
15 |
16 | 4.1.0 (2024-05-13)
17 | ==================
18 |
19 | - Drop Python 3.7, Django 3.2. Support Python 3.11 and 3.12, Django 4.2 and 5.0. #123
20 |
21 | 4.0.0 (2022-10-17)
22 | ==================
23 |
24 | - Drop support for Python < 3.7 / Django < 3.2
25 | - Remove jsonfield dependency in favor of django.db.models.JSONField
26 | WARNING : This can break your migration. jsonfield used to create a text column for your json data when running 'makemigrations'. However, django.db.models.JSONField creates a json column using database-backend specific column types (ie jsonb on postgresql). As such your model field that used to be a text column is now a jsonb column.
27 |
28 | 3.2.1 (2022-02-21)
29 | ==================
30 |
31 | - Set default initial value to form field #116
32 |
33 | 3.2.0 (2021-04-15)
34 | ==================
35 |
36 | - Support Django 3.2.
37 |
38 | 3.1.0 (2020-10-27)
39 | ==================
40 |
41 | - Drop support for Python 2 and Django < 2.2
42 | - Fix warnings in Django 3.1
43 | - Use explicit srid in get_queryset #106
44 | - Add support for crs_type using the view options #107
45 |
46 | 3.0.0 (2020-03-02)
47 | ===================
48 |
49 | - Fix with_modelname option after first iteration
50 | - Drop Django 1.11 support
51 | - jsonfield 3.0 support, drop 2.x support
52 |
53 | 2.12.0 (2018-12-05)
54 | ===================
55 |
56 | - Change media type to 'application/geo+json'
57 | - Make with_modelname option available in views
58 | - Drop Django 1.8 support.
59 | - Fixed unnecessary GDAL dependency. (#100, thanks @coredumperror)
60 |
61 | 2.11.0 (2017-12-05)
62 | ===================
63 |
64 | - Django 2.0 support.
65 | - Fix setup.py encoding error.
66 |
67 | 2.10.0 (2017-02-28)
68 | ===================
69 |
70 | - python 3.5 support.
71 |
72 |
73 | 2.9.1 (2016-08-22)
74 | ==================
75 |
76 | ** Bug fixes **
77 |
78 | - Remove deprecation warnings with Django 1.8 (#78, thanks @AchilleAsh)
79 |
80 |
81 | 2.9.0 (2016-02-08)
82 | ==================
83 |
84 | ** New features **
85 |
86 | - handle natural keys in views (#74, thanks Achille Ash!)
87 |
88 | ** Bug fixes **
89 |
90 | - Add Django 1.9 compatibility (#69, thanks Julien Le Sech!)
91 | - Fix imports in view.py to work without GEOS (#62, thanks Kevin Cooper!)
92 |
93 |
94 | 2.8.1 (2015-06-17)
95 | ==================
96 |
97 | ** Bug fixes**
98 |
99 | - Fixed detection of GEOS (thanks Kevin Cooper!)
100 |
101 | 2.8.0 (2015-04-17)
102 | ==================
103 |
104 | ** New features **
105 |
106 | - Support GeoJSON specification for named crs (thanks Alvin Lindstam)
107 |
108 | ** Bug fixes **
109 |
110 | - Add python 3.2 compatibility (thanks Nikolay Korotkiy, fixes #55)
111 | - Fix tests on Django >= 1.7 (thanks Manel Clos)
112 |
113 |
114 | 2.7.0 (2015-02-21)
115 | ==================
116 |
117 | ** New features **
118 |
119 | - Add a with_modelname option to serializer
120 |
121 | ** Bug fixes **
122 |
123 | - change 'fields' to 'properties' in code example
124 | - Adds a warning for "Module version, as defined in PEP-0396
125 |
126 |
127 | 2.6.0 (2014-07-21)
128 | ==================
129 |
130 | ** New features **
131 |
132 | - Django GeoJSON fields without libgeos installed (thanks Florent Lebreton)
133 | - Properties can be a tuple (fixes #34)
134 |
135 |
136 | 2.5.0 (2014-06-03)
137 | ==================
138 |
139 | ** New features **
140 |
141 | - Add vector tiles view
142 | - Improved `geojsonfeature` template tag (fixes #15, #16)
143 | - Add various GeoJSON fields, for each geometry type
144 |
145 | ** Bug fixes **
146 |
147 | - Fix (de)serializers not being usable from command-line (fixes #28)
148 | - Fix import attempt for *django-leaflet* (fixes #27), by Seyi Ogunyemi
149 | - Fix missed SRID after copying a geometry in ``_handle_geom``, by Biel Frontera
150 |
151 | ** Internal changes **
152 |
153 | - Specify django-leaflet minimal version for GeoJSON model field
154 | - Got rid of shapely for deserialization
155 |
156 |
157 | 2.4.0 (2014-03-22)
158 | ==================
159 |
160 | - Add GeoJSON fields
161 |
162 |
163 | 2.3.0 (2014-02-08)
164 | ==================
165 |
166 | - Python 3 support (thanks @amarandon)
167 | - Add bbox at feature level (thanks @7wonders)
168 |
169 |
170 | 2.2.0 (2013-12-18)
171 | ==================
172 |
173 | - Deserialization: add ability to specify model name as option (thanks @Vross)
174 | - Deserialization: look-up ``ìd`` value in properties whe missing at feature level: (thanks @Vross)
175 |
176 |
177 | 2.1.1 (2013-08-21)
178 | ==================
179 |
180 | - Set default SRID to 4326 in generic GeoJSON views.
181 |
182 |
183 | 2.1.0 (2013-08-19)
184 | ==================
185 |
186 | - Serialize reversed relations using natural keys (fixes #8)
187 | - Support empty geometries (None or NULL in Db)
188 | - Fix serializing property in upper class
189 |
190 | 2.0.1 (2013-07-10)
191 | ==================
192 |
193 | - Fix usage of simplify.
194 | - Expose ``force2d`` option in view
195 | - Allow to have ``pk`` or ``id`` in properties if explicitly listed
196 |
197 | 2.0.0 (2013-07-09)
198 | ==================
199 |
200 | - Complete rewrite using @jeffkistler and @glenrobertson code
201 | - CRS is added to GeoJSON ouput by default
202 | - Ability to build ``pk`` dynamically by passing a lambda
203 | - Ability to provide a ``bbox``
204 | - Ability to use Django natural keys
205 | - Support of *ValuesQuerySet*
206 | - Support of *ForeignKey* and *ManyToMany*
207 | - Added ``force2d`` option
208 |
209 | ** Backwards incompatible changes **
210 |
211 | - Geom field is not guessed automatically : Use ``geometry_field`` option, default is ``'geom'``.
212 | - no more ``pk`` in properties : use feature ``id``, or list ``pk`` in properties explicitly.
213 | - ``fields`` list in ``GeoJSONLayer`` was renamed ``properties``.
214 |
215 | 1.2.0 (2013-05-22)
216 | ==================
217 |
218 | - Shapely is now optional (used for deserialization only)
219 | - Add Django to requirements
220 | - Skip a step in GeoJSON conversion of geometries (fixes #6)
221 |
222 |
223 | 1.1.0 (2013-03-06)
224 | ==================
225 |
226 | - Django 1.5 support
227 | - Fix template tag geojsonfeature on empty geometries
228 |
229 | 1.0.0 (2012-08-03)
230 | ==================
231 |
232 | - Initial working version.
233 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.rst CHANGES LICENSE
2 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ==============
2 | django-geojson
3 | ==============
4 |
5 | See the `documentation `_ for more information.
6 |
7 | *django-geojson* is a set of tools to manipulate GeoJSON with Django >= 3.2:
8 |
9 | * (De)Serializer for (Geo)Django objects, querysets and lists
10 | * Base views to serve GeoJSON map layers from models
11 | * GeoJSON model and form fields to avoid spatial database backends
12 | (compatible with *django-leaflet* for map widgets)
13 |
14 |
15 | .. image:: https://readthedocs.org/projects/django-geojson/badge/?version=latest
16 | :target: http://django-geojson.readthedocs.io/en/latest/?badge=latest
17 | :alt: Documentation Status
18 |
19 | .. image:: https://img.shields.io/pypi/v/django-geojson.svg
20 | :target: https://pypi.python.org/pypi/django-geojson
21 |
22 | .. image:: https://img.shields.io/pypi/dm/django-geojson.svg
23 | :target: https://pypi.python.org/pypi/django-geojson
24 |
25 | .. image:: https://travis-ci.org/makinacorpus/django-geojson.png?branch=master
26 | :target: https://travis-ci.org/makinacorpus/django-geojson
27 |
28 | .. image:: https://coveralls.io/repos/makinacorpus/django-geojson/badge.png?branch=master
29 | :target: https://coveralls.io/r/makinacorpus/django-geojson
30 |
31 |
32 | =======
33 | AUTHORS
34 | =======
35 |
36 | * Mathieu Leplatre
37 | * Glen Robertson author of django-geojson-tiles at: https://github.com/glenrobertson/django-geojson-tiles/
38 | * @jeffkistler's author of geojson serializer at: https://gist.github.com/967274
39 | * Ben Welsh and Lukasz Dziedzia for `quick test script `_
40 | * Florent Lebreton http://github.com/fle
41 | * Julien Le Sech http://www.idreammicro.com
42 | * Kevin Cooper @kevcooper
43 | * Achille Ash @AchilleAsh
44 | * Yann Fouillat (alias Gagaro)
45 |
46 | Version 1.X:
47 |
48 | * Daniel Sokolowski, serializer snippet
49 | * ozzmo, python 2.6 compatibility
50 |
51 | |makinacom|_
52 |
53 | .. |makinacom| image:: http://depot.makina-corpus.org/public/logo.gif
54 | .. _makinacom: http://www.makina-corpus.com
55 |
56 | =======
57 | LICENSE
58 | =======
59 |
60 | Lesser GNU Public License
61 |
--------------------------------------------------------------------------------
/djgeojson/__init__.py:
--------------------------------------------------------------------------------
1 | GEOJSON_DEFAULT_SRID = 4326
2 |
--------------------------------------------------------------------------------
/djgeojson/fields.py:
--------------------------------------------------------------------------------
1 | from django.core.exceptions import ValidationError
2 | from django.db.models import JSONField
3 | from django.forms.fields import InvalidJSONInput
4 | from django.forms.fields import JSONField as JSONFormField
5 | from django.forms.widgets import HiddenInput
6 | from django.utils.translation import gettext_lazy as _
7 |
8 | try:
9 | from leaflet.forms.widgets import LeafletWidget
10 | HAS_LEAFLET = True
11 | except ImportError:
12 | HAS_LEAFLET = False
13 |
14 |
15 | class GeoJSONValidator(object):
16 | def __init__(self, geom_type):
17 | self.geom_type = geom_type
18 |
19 | def __call__(self, value):
20 | err_msg = None
21 | geom_type = value.get('type') or ''
22 | if self.geom_type == 'GEOMETRY':
23 | is_geometry = geom_type in (
24 | "Point", "MultiPoint", "LineString", "MultiLineString",
25 | "Polygon", "MultiPolygon", "GeometryCollection"
26 | )
27 | if not is_geometry:
28 | err_msg = '%s is not a valid GeoJSON geometry type' % geom_type
29 | else:
30 | if self.geom_type.lower() != geom_type.lower():
31 | err_msg = '%s does not match geometry type' % geom_type
32 |
33 | if err_msg:
34 | raise ValidationError(err_msg)
35 |
36 |
37 | class GeoJSONFormField(JSONFormField):
38 | widget = LeafletWidget if HAS_LEAFLET else HiddenInput
39 |
40 | def __init__(self, *args, **kwargs):
41 | if not HAS_LEAFLET:
42 | import warnings
43 | warnings.warn('`django-leaflet` is not available.')
44 | geom_type = kwargs.pop('geom_type')
45 | kwargs.setdefault('validators', [GeoJSONValidator(geom_type)])
46 | kwargs.setdefault('initial', InvalidJSONInput(''))
47 | super(GeoJSONFormField, self).__init__(*args, **kwargs)
48 |
49 |
50 | class GeoJSONField(JSONField):
51 | description = _("Geometry as GeoJSON")
52 | form_class = GeoJSONFormField
53 | dim = 2
54 | geom_type = 'GEOMETRY'
55 |
56 | def formfield(self, **kwargs):
57 | kwargs.setdefault('geom_type', self.geom_type)
58 | kwargs.setdefault('form_class', self.form_class)
59 | return super(GeoJSONField, self).formfield(**kwargs)
60 |
61 |
62 | class GeometryField(GeoJSONField):
63 | pass
64 |
65 |
66 | class GeometryCollectionField(GeometryField):
67 | geom_type = 'GEOMETRYCOLLECTION'
68 |
69 |
70 | class PointField(GeometryField):
71 | geom_type = 'POINT'
72 |
73 |
74 | class MultiPointField(GeometryField):
75 | geom_type = 'MULTIPOINT'
76 |
77 |
78 | class LineStringField(GeometryField):
79 | geom_type = 'LINESTRING'
80 |
81 |
82 | class MultiLineStringField(GeometryField):
83 | geom_type = 'MULTILINESTRING'
84 |
85 |
86 | class PolygonField(GeometryField):
87 | geom_type = 'POLYGON'
88 |
89 |
90 | class MultiPolygonField(GeoJSONField):
91 | geom_type = 'MULTIPOLYGON'
92 |
--------------------------------------------------------------------------------
/djgeojson/http.py:
--------------------------------------------------------------------------------
1 | import warnings
2 |
3 | from django.http import HttpResponse
4 |
5 |
6 | class HttpGeoJSONResponse(HttpResponse):
7 | def __init__(self, **kwargs):
8 | kwargs['content_type'] = 'application/geo+json'
9 | super(HttpGeoJSONResponse, self).__init__(**kwargs)
10 |
11 |
12 | class HttpJSONResponse(HttpGeoJSONResponse):
13 | def __init__(self, **kwargs):
14 | warnings.warn("The 'HttpJSONResponse' class was renamed to 'HttpGeoJSONResponse'",
15 | DeprecationWarning)
16 | super(HttpJSONResponse, self).__init__(**kwargs)
17 |
--------------------------------------------------------------------------------
/djgeojson/models.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/makinacorpus/django-geojson/9134370e43d30f78baf44fa0c847ffbe7ae3db33/djgeojson/models.py
--------------------------------------------------------------------------------
/djgeojson/nogeos.py:
--------------------------------------------------------------------------------
1 | class GEOSGeometry(object):
2 | """
3 | A simple mock of django.contrib.gis.geos.GEOSGeometry to make
4 | django-geojson work without libgeos
5 | """
6 | def __init__(self, geo_input, srid=None):
7 | self.srid = srid
8 | self.geojson = geo_input
9 | return
10 |
11 |
12 | class Polygon(GEOSGeometry):
13 | pass
14 |
15 |
16 | class WKBWriter(object):
17 | pass
18 |
--------------------------------------------------------------------------------
/djgeojson/serializers.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | This code mainly comes from @glenrobertson's django-geoson-tiles at:
4 | https://github.com/glenrobertson/django-geojson-tiles/
5 |
6 | Itself, adapted from @jeffkistler's geojson serializer at: https://gist.github.com/967274
7 | """
8 | import json
9 | import logging
10 | from contextlib import contextmanager
11 | from io import StringIO # NOQA
12 |
13 | import django
14 | from django.db.models.base import Model
15 |
16 | try:
17 | from django.db.models.query import QuerySet, ValuesQuerySet
18 | except ImportError:
19 | from django.db.models.query import QuerySet
20 | ValuesQuerySet = None
21 |
22 | from django.core.exceptions import ImproperlyConfigured
23 | from django.core.serializers.base import (
24 | DeserializationError,
25 | SerializationError,
26 | )
27 | from django.core.serializers.json import DjangoJSONEncoder
28 | from django.core.serializers.python import Deserializer as PythonDeserializer
29 | from django.core.serializers.python import Serializer as PythonSerializer
30 | from django.forms.models import model_to_dict
31 | from django.utils.encoding import smart_str
32 |
33 | try:
34 | from django.contrib.gis.db.models.fields import GeometryField
35 | from django.contrib.gis.geos import GEOSGeometry, WKBWriter
36 | except (ImportError, ImproperlyConfigured):
37 | from .nogeos import WKBWriter
38 | from .nogeos import GEOSGeometry
39 | from .fields import GeometryField
40 |
41 | from . import GEOJSON_DEFAULT_SRID
42 | from .fields import GeoJSONField
43 |
44 | if django.VERSION >= (5, 2):
45 | get_model = PythonDeserializer._get_model_from_node
46 | else:
47 | from django.core.serializers.python import _get_model as get_model
48 |
49 | logger = logging.getLogger(__name__)
50 |
51 |
52 | def hasattr_lazy(obj, name):
53 | if isinstance(obj, dict):
54 | return name in obj
55 | return name in dir(obj)
56 |
57 |
58 | def get_field_remote_field(field):
59 | """ For Django 1.8/2.0 compatibility """
60 | if django.VERSION < (1, 9):
61 | return field.rel
62 | else:
63 | return field.remote_field
64 |
65 |
66 | class DjangoGeoJSONEncoder(DjangoJSONEncoder):
67 |
68 | def default(self, o):
69 | if isinstance(o, GEOSGeometry):
70 | return json.loads(o.geojson)
71 | else:
72 | return super(DjangoGeoJSONEncoder, self).default(o)
73 |
74 |
75 | @contextmanager
76 | def json_encoder_with_precision(precision, JSONEncoderClass):
77 | """
78 | Context manager to set float precision during json encoding
79 | """
80 | needs_class_hack = not hasattr(json.encoder, 'FLOAT_REPR')
81 | try:
82 | if precision is not None:
83 | def float_repr(o):
84 | return format(o, '.%sf' % precision)
85 | if not needs_class_hack:
86 | # python is not 3.5
87 | original_float_repr = json.encoder.FLOAT_REPR
88 | json.encoder.FLOAT_REPR = float_repr
89 | else:
90 | # HACK to allow usage of float precision in python 3.5
91 | # Needed because python 3.5 removes the global FLOAT_REPR hack
92 |
93 | class JSONEncoderClass(JSONEncoderClass):
94 | FLOAT_REPR = float.__repr__
95 |
96 | # Follows code copied from cPython Lib/json/encoder.py
97 | # The only change is that in floatstr _repr=float.__repr__ is replaced
98 |
99 | def iterencode(self, o, _one_shot=False):
100 | """Encode the given object and yield each string
101 | representation as available.
102 |
103 | For example::
104 |
105 | for chunk in JSONEncoder().iterencode(bigobject):
106 | mysocket.write(chunk)
107 |
108 | """
109 | if self.check_circular:
110 | markers = {}
111 | else:
112 | markers = None
113 | if self.ensure_ascii:
114 | _encoder = json.encoder.encode_basestring_ascii
115 | else:
116 | _encoder = json.encoder.encode_basestring
117 |
118 | def floatstr(o, allow_nan=self.allow_nan, _repr=float_repr, _inf=json.encoder.INFINITY, _neginf=-json.encoder.INFINITY):
119 | # Check for specials. Note that this type of test is processor
120 | # and/or platform-specific, so do tests which don't depend on the
121 | # internals.
122 |
123 | if o != o:
124 | text = 'NaN'
125 | elif o == _inf:
126 | text = 'Infinity'
127 | elif o == _neginf:
128 | text = '-Infinity'
129 | else:
130 | return _repr(o)
131 |
132 | if not allow_nan:
133 | raise ValueError(
134 | "Out of range float values are not JSON compliant: " +
135 | repr(o))
136 |
137 | return text
138 |
139 | if (_one_shot and json.encoder.c_make_encoder is not None and self.indent is None):
140 | _iterencode = json.encoder.c_make_encoder(
141 | markers, self.default, _encoder, self.indent,
142 | self.key_separator, self.item_separator, self.sort_keys,
143 | self.skipkeys, self.allow_nan)
144 | else:
145 | _iterencode = json.encoder._make_iterencode(
146 | markers, self.default, _encoder, self.indent, floatstr,
147 | self.key_separator, self.item_separator, self.sort_keys,
148 | self.skipkeys, _one_shot)
149 | return _iterencode(o, 0)
150 | yield JSONEncoderClass
151 | finally:
152 | if precision is not None:
153 | if not needs_class_hack:
154 | # restore FLOAT_REPR
155 | json.encoder.FLOAT_REPR = original_float_repr
156 |
157 |
158 | class Serializer(PythonSerializer):
159 |
160 | internal_use_only = False
161 |
162 | def start_serialization(self):
163 | self.feature_collection = {"type": "FeatureCollection", "features": []}
164 | if self.crs is not False:
165 | self.feature_collection["crs"] = self.get_crs()
166 |
167 | bbox = self.options.pop('bbox', None)
168 | if bbox:
169 | self.feature_collection["bbox"] = bbox
170 |
171 | self._current = None
172 |
173 | def get_crs(self):
174 | crs = {}
175 | crs_type = self.options.pop('crs_type', None)
176 | properties = {}
177 | if crs_type == "name":
178 | # todo: GeoJSON Spec: OGC CRS URNs such as "urn:ogc:def:crs:OGC:1.3:CRS84" shall be preferred over legacy identifiers such as "EPSG:4326":
179 | properties["name"] = "EPSG:%s" % (str(self.srid))
180 | else: # preserve default behaviour
181 | crs_type = "link"
182 | properties["href"] = "http://spatialreference.org/ref/epsg/%s/" % (str(self.srid))
183 | properties["type"] = "proj4"
184 | crs["type"] = crs_type
185 | crs["properties"] = properties
186 | return crs
187 |
188 | def start_object(self, obj):
189 | self._current = {"type": "Feature", "properties": {}}
190 |
191 | # Try to determine the primary key from the obj
192 | # self.primary_key can be a function (callable on obj), or a string
193 | # if self.primary_key is not set, use obj.pk if obj is a Model
194 | # otherwise the primary key will not be used
195 | primary_key = None
196 | if self.primary_key and hasattr(self.primary_key, '__call__'):
197 | primary_key = self.primary_key(obj)
198 | elif self.primary_key and isinstance(self.primary_key, str):
199 | if isinstance(obj, Model):
200 | primary_key = getattr(obj, self.primary_key)
201 | else:
202 | primary_key = obj[self.primary_key]
203 | elif isinstance(obj, Model):
204 | primary_key = obj.pk
205 |
206 | if primary_key:
207 | self._current['id'] = primary_key
208 |
209 | def end_object(self, obj):
210 | # Add extra properties from dynamic attributes
211 | extras = []
212 | if isinstance(self.properties, dict):
213 | extras = [field for field, name in self.properties.items()
214 | if name not in self._current['properties']]
215 | elif isinstance(self.properties, (list, tuple)):
216 | extras = [field for field in self.properties
217 | if field not in self._current['properties']]
218 |
219 | for field in extras:
220 | if hasattr_lazy(obj, field):
221 | self.handle_field(obj, field)
222 |
223 | # Add extra-info for deserializing
224 | with_modelname = self.options.get('with_modelname', True)
225 | if hasattr(obj, '_meta') and with_modelname:
226 | self._current['properties']['model'] = smart_str(obj._meta)
227 |
228 | # If geometry not in model fields, may be a dynamic attribute
229 | if 'geometry' not in self._current:
230 | if hasattr_lazy(obj, self.geometry_field):
231 | geometry = getattr(obj, self.geometry_field)
232 | self._handle_geom(geometry)
233 | else:
234 | logger.warn("No GeometryField found in object")
235 |
236 | self.feature_collection["features"].append(self._current)
237 | self._current = None
238 |
239 | def end_serialization(self):
240 | self.options.pop('stream', None)
241 | self.options.pop('properties', None)
242 | self.options.pop('primary_key', None)
243 | self.options.pop('geometry_field', None)
244 | self.options.pop('use_natural_keys', None)
245 | self.options.pop('crs', None)
246 | self.options.pop('srid', None)
247 | self.options.pop('force2d', None)
248 | self.options.pop('simplify', None)
249 | self.options.pop('bbox', None)
250 | self.options.pop('bbox_auto', None)
251 | self.options.pop('with_modelname', None)
252 |
253 | # Optional float precision control
254 | precision = self.options.pop('precision', None)
255 |
256 | with json_encoder_with_precision(precision, DjangoGeoJSONEncoder) as cls:
257 | json.dump(self.feature_collection, self.stream, cls=cls, **self.options)
258 |
259 | def _handle_geom(self, value):
260 | """ Geometry processing (in place), depending on options """
261 | if value is None:
262 | geometry = None
263 | elif isinstance(value, dict) and 'type' in value:
264 | geometry = value
265 | else:
266 | if isinstance(value, GEOSGeometry):
267 | geometry = value
268 | else:
269 | try:
270 | # this will handle string representations (e.g. ewkt, bwkt)
271 | geometry = GEOSGeometry(value)
272 | except ValueError:
273 | # if the geometry couldn't be parsed.
274 | # we can't generate valid geojson
275 | error_msg = 'The field ["%s", "%s"] could not be parsed as a valid geometry' % (
276 | self.geometry_field, value
277 | )
278 | raise SerializationError(error_msg)
279 |
280 | # Optional force 2D
281 | if self.options.get('force2d'):
282 | wkb_w = WKBWriter()
283 | wkb_w.outdim = 2
284 | geometry = GEOSGeometry(wkb_w.write(geometry), srid=geometry.srid)
285 | # Optional geometry simplification
286 | simplify = self.options.get('simplify')
287 | if simplify is not None:
288 | geometry = geometry.simplify(tolerance=simplify, preserve_topology=True)
289 | # Optional geometry reprojection
290 | if geometry.srid and geometry.srid != self.srid:
291 | geometry.transform(self.srid)
292 | # Optional bbox
293 | if self.options.get('bbox_auto'):
294 | self._current['bbox'] = geometry.extent
295 |
296 | self._current['geometry'] = geometry
297 |
298 | def handle_field(self, obj, field_name):
299 | if isinstance(obj, Model):
300 | value = getattr(obj, field_name)
301 | elif isinstance(obj, dict):
302 | value = obj[field_name]
303 | else:
304 | # Only supports dicts and models, not lists (e.g. values_list)
305 | return
306 |
307 | if field_name == self.geometry_field:
308 | self._handle_geom(value)
309 |
310 | elif self.properties and field_name in self.properties:
311 | # set the field name to the key's value mapping in self.properties
312 | if isinstance(self.properties, dict):
313 | property_name = self.properties[field_name]
314 | self._current['properties'][property_name] = value
315 | else:
316 | self._current['properties'][field_name] = value
317 |
318 | elif not self.properties:
319 | self._current['properties'][field_name] = value
320 |
321 | def getvalue(self):
322 | if callable(getattr(self.stream, 'getvalue', None)):
323 | return self.stream.getvalue()
324 |
325 | def handle_fk_field(self, obj, field):
326 | related = getattr(obj, field.name)
327 | if related is not None:
328 | if self.use_natural_keys and hasattr(related, 'natural_key'):
329 | related = related.natural_key()
330 | else:
331 | if get_field_remote_field(field).field_name == related._meta.pk.name:
332 | # Related to remote object via primary key
333 | related = related._get_pk_val()
334 | else:
335 | # Related to remote object via other field
336 | related = smart_str(getattr(related, get_field_remote_field(field).field_name), strings_only=True)
337 | self._current['properties'][field.name] = related
338 |
339 | def handle_m2m_field(self, obj, field):
340 | if get_field_remote_field(field).through._meta.auto_created:
341 | remote_field = get_field_remote_field(field)
342 | if django.VERSION < (1, 9): # Django 1.8/2.0 compatibility
343 | remote_model = remote_field.to
344 | else:
345 | remote_model = remote_field.related_model
346 | if self.use_natural_keys and hasattr(remote_model, 'natural_key'):
347 | def m2m_value(value):
348 | return value.natural_key()
349 | else:
350 | def m2m_value(value):
351 | return smart_str(value._get_pk_val(), strings_only=True)
352 | self._current['properties'][field.name] = [m2m_value(related)
353 | for related in getattr(obj, field.name).iterator()]
354 |
355 | def handle_reverse_field(self, obj, field, field_name):
356 | if self.use_natural_keys and hasattr(field.model, 'natural_key'):
357 | def reverse_value(value):
358 | return value.natural_key()
359 | else:
360 | def reverse_value(value):
361 | return smart_str(value._get_pk_val(), strings_only=True)
362 | values = [reverse_value(related) for related in getattr(obj, field_name).iterator()]
363 | self._current['properties'][field_name] = values
364 |
365 | def serialize_object_list(self, objects):
366 | if len(objects) == 0:
367 | return
368 |
369 | # Transform to list of dicts instead of objects
370 | if not isinstance(objects[0], dict):
371 | values = []
372 | for obj in objects:
373 | objdict = model_to_dict(obj)
374 | # In case geometry is not a DB field
375 | if self.geometry_field not in objdict:
376 | objdict[self.geometry_field] = getattr(obj, self.geometry_field)
377 | if self.properties:
378 | extras = [f for f in self.properties if hasattr(obj, f)]
379 | for field_name in extras:
380 | objdict[field_name] = getattr(obj, field_name)
381 | values.append(objdict)
382 | objects = values
383 |
384 | self.serialize_values_queryset(objects)
385 |
386 | def serialize_values_queryset(self, queryset):
387 | for obj in queryset:
388 | self.start_object(obj)
389 |
390 | # handle the geometry field
391 | self.handle_field(obj, self.geometry_field)
392 |
393 | for field_name in obj:
394 | if field_name not in obj:
395 | continue
396 | if self.properties is None or field_name in self.properties:
397 | self.handle_field(obj, field_name)
398 |
399 | self.end_object(obj)
400 |
401 | def serialize_queryset(self, queryset):
402 | opts = queryset.model._meta
403 | local_fields = opts.local_fields
404 | many_to_many_fields = opts.many_to_many
405 | reversed_fields = [obj.field for obj in get_all_related_objects(opts)]
406 | reversed_fields += [obj.field for obj in get_all_related_many_to_many_objects(opts)]
407 |
408 | # populate each queryset obj as a feature
409 | for obj in queryset:
410 | self.start_object(obj)
411 |
412 | # handle the geometry field
413 | self.handle_field(obj, self.geometry_field)
414 |
415 | # handle the property fields
416 | for field in local_fields:
417 | # don't include the pk in the properties
418 | # as it is in the id of the feature
419 | # except if explicitly listed in properties
420 | if field.name == opts.pk.name and \
421 | (self.properties is None or 'id' not in self.properties):
422 | continue
423 | # ignore other geometries
424 | if isinstance(field, GeometryField):
425 | continue
426 |
427 | if field.serialize or field.primary_key:
428 | if get_field_remote_field(field) is None:
429 | if self.properties is None or field.attname in self.properties:
430 | self.handle_field(obj, field.name)
431 | else:
432 | if self.properties is None or field.attname[:-3] in self.properties:
433 | self.handle_fk_field(obj, field)
434 |
435 | for field in many_to_many_fields:
436 | if field.serialize:
437 | if self.properties is None or field.attname in self.properties:
438 | self.handle_m2m_field(obj, field)
439 |
440 | for field in reversed_fields:
441 | if field.serialize:
442 | field_name = get_field_remote_field(field).related_name or opts.object_name.lower()
443 | if self.properties is None or field_name in self.properties:
444 | self.handle_reverse_field(obj, field, field_name)
445 | self.end_object(obj)
446 |
447 | def serialize(self, queryset, **options):
448 | """
449 | Serialize a queryset.
450 | """
451 | self.options = options
452 |
453 | self.stream = options.get("stream", StringIO())
454 | self.primary_key = options.get("primary_key", None)
455 | self.properties = options.get("properties")
456 | self.geometry_field = options.get("geometry_field", "geom")
457 | self.use_natural_keys = options.get("use_natural_keys", False)
458 | self.bbox = options.get("bbox", None)
459 | self.bbox_auto = options.get("bbox_auto", None)
460 | self.srid = options.get("srid", GEOJSON_DEFAULT_SRID)
461 | self.crs = options.get("crs", True)
462 | self.crs_type = options.get("crs_type", 'name')
463 |
464 | self.start_serialization()
465 |
466 | if ValuesQuerySet is not None and isinstance(queryset, ValuesQuerySet):
467 | self.serialize_values_queryset(queryset)
468 |
469 | elif isinstance(queryset, list):
470 | self.serialize_object_list(queryset)
471 |
472 | elif isinstance(queryset, QuerySet):
473 | self.serialize_queryset(queryset)
474 |
475 | self.end_serialization()
476 | return self.getvalue()
477 |
478 |
479 | def Deserializer(stream_or_string, **options):
480 | """
481 | Deserialize a stream or string of JSON data.
482 | """
483 |
484 | geometry_field = options.get("geometry_field", "geom")
485 |
486 | def FeatureToPython(dictobj):
487 | properties = dictobj['properties']
488 | model_name = options.get("model_name") or properties.pop('model')
489 | # Deserialize concrete fields only (bypass dynamic properties)
490 | model = get_model(model_name)
491 | field_names = [f.name for f in model._meta.fields]
492 | fields = {}
493 | for k, v in properties.items():
494 | if k in field_names:
495 | fields[k] = v
496 | obj = {
497 | "model": model_name,
498 | "pk": dictobj.get('id') or properties.get('id'),
499 | "fields": fields
500 | }
501 | if isinstance(model._meta.get_field(geometry_field), GeoJSONField):
502 | obj['fields'][geometry_field] = dictobj['geometry']
503 | else:
504 | shape = GEOSGeometry(json.dumps(dictobj['geometry']))
505 | obj['fields'][geometry_field] = shape.wkt
506 | return obj
507 |
508 | if isinstance(stream_or_string, str):
509 | stream = StringIO(stream_or_string)
510 | else:
511 | stream = stream_or_string
512 | try:
513 | collection = json.load(stream)
514 | objects = [FeatureToPython(f) for f in collection['features']]
515 | for obj in PythonDeserializer(objects, **options):
516 | yield obj
517 | except GeneratorExit:
518 | raise
519 | except Exception as e:
520 | # Map to deserializer error
521 | raise DeserializationError(repr(e))
522 |
523 |
524 | def get_all_related_objects(opts):
525 | """
526 | Django 1.8 changed meta api, see
527 | https://docs.djangoproject.com/en/1.8/ref/models/meta/#migrating-old-meta-api
528 | https://code.djangoproject.com/ticket/12663
529 | https://github.com/django/django/pull/3848
530 | Initially from Django REST Framework:
531 | https://github.com/tomchristie/django-rest-framework/blob/3.3.2/rest_framework/compat.py
532 | :param opts: Options instance
533 | :return: list of relations except many-to-many ones
534 | """
535 | if django.VERSION < (1, 8):
536 | return opts.get_all_related_objects()
537 | else:
538 | return [r for r in opts.related_objects if not r.field.many_to_many]
539 |
540 |
541 | def get_all_related_many_to_many_objects(opts):
542 | """
543 | Django 1.8 changed meta api, see docstr in get_all_related_objects()
544 |
545 | :param opts: Options instance
546 | :return: list of many-to-many relations
547 | """
548 | if django.VERSION < (1, 8):
549 | return opts.get_all_related_many_to_many_objects()
550 | else:
551 | return [r for r in opts.related_objects if r.field.many_to_many]
552 |
--------------------------------------------------------------------------------
/djgeojson/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/makinacorpus/django-geojson/9134370e43d30f78baf44fa0c847ffbe7ae3db33/djgeojson/templatetags/__init__.py
--------------------------------------------------------------------------------
/djgeojson/templatetags/geojson_tags.py:
--------------------------------------------------------------------------------
1 | import json
2 | import re
3 |
4 | from django import template
5 | from django.core.exceptions import ImproperlyConfigured
6 |
7 | try:
8 | from django.contrib.gis.db.models.fields import GeometryField
9 | from django.contrib.gis.geos import GEOSGeometry
10 | except (ImportError, ImproperlyConfigured):
11 | from ..nogeos import GEOSGeometry
12 | from ..fields import GeometryField
13 |
14 | from .. import GEOJSON_DEFAULT_SRID
15 | from ..serializers import DjangoGeoJSONEncoder, Serializer
16 |
17 | register = template.Library()
18 |
19 |
20 | @register.filter
21 | def geojsonfeature(source, params=''):
22 | """
23 | :params: A string with the following optional tokens:
24 | "properties:field:srid"
25 | """
26 | parse = re.search(r'(?P((\w+)(,\w+)*)?)(:(?P(\w+)?))?(:(?P(\d+)?))?', params)
27 | if parse:
28 | parse = parse.groupdict()
29 | else:
30 | parse = {}
31 |
32 | geometry_field = parse.get('field') or 'geom'
33 | properties = parse.get('properties', '').split(',')
34 | srid = parse.get('srid') or GEOJSON_DEFAULT_SRID
35 |
36 | if source is None or isinstance(source, str):
37 | return 'null'
38 |
39 | if isinstance(source, (GEOSGeometry, GeometryField)):
40 | encoder = DjangoGeoJSONEncoder()
41 | if source.srid != srid:
42 | source.transform(srid)
43 | feature = {"type": "Feature", "properties": {}}
44 | feature['geometry'] = encoder.default(source)
45 | return json.dumps(feature)
46 |
47 | serializer = Serializer()
48 |
49 | if not hasattr(source, '__iter__'):
50 | source = [source]
51 |
52 | return serializer.serialize(source, properties=properties,
53 | geometry_field=geometry_field, srid=srid)
54 |
--------------------------------------------------------------------------------
/djgeojson/tests.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | import django
4 | from django.conf import settings
5 | from django.contrib.gis.db import models
6 | from django.contrib.gis.geos import GeometryCollection, LineString, Point
7 | from django.core import serializers
8 | from django.core.exceptions import SuspiciousOperation, ValidationError
9 | from django.forms import HiddenInput
10 | from django.test import TestCase
11 | from django.utils.encoding import smart_str
12 |
13 | from .fields import GeoJSONField, GeoJSONFormField, GeoJSONValidator
14 | from .serializers import Serializer
15 | from .templatetags.geojson_tags import geojsonfeature
16 | from .views import GeoJSONLayerView, TiledGeoJSONLayerView
17 |
18 | settings.SERIALIZATION_MODULES = {'geojson': 'djgeojson.serializers'}
19 |
20 |
21 | class PictureMixin(object):
22 |
23 | @property
24 | def picture(self):
25 | return 'image.png'
26 |
27 |
28 | class Country(models.Model):
29 | label = models.CharField(max_length=20)
30 | geom = models.PolygonField(spatial_index=False, srid=4326)
31 |
32 | if django.VERSION < (1, 9):
33 | objects = models.GeoManager()
34 |
35 | def natural_key(self):
36 | return self.label
37 |
38 |
39 | class Route(PictureMixin, models.Model):
40 | name = models.CharField(max_length=20)
41 | geom = models.LineStringField(spatial_index=False, srid=4326)
42 | countries = models.ManyToManyField(Country)
43 |
44 | def natural_key(self):
45 | return self.name
46 |
47 | @property
48 | def upper_name(self):
49 | return self.name.upper()
50 |
51 | if django.VERSION < (1, 9):
52 | objects = models.GeoManager()
53 |
54 |
55 | class Sign(models.Model):
56 | label = models.CharField(max_length=20)
57 | route = models.ForeignKey(Route, related_name='signs', on_delete=models.PROTECT)
58 |
59 | def natural_key(self):
60 | return self.label
61 |
62 | @property
63 | def geom(self):
64 | return self.route.geom.centroid
65 |
66 |
67 | class GeoJsonDeSerializerTest(TestCase):
68 |
69 | def test_basic(self):
70 | input_geojson = """
71 | {"type": "FeatureCollection",
72 | "features": [
73 | { "type": "Feature",
74 | "properties": {"model": "djgeojson.route", "name": "green", "upper_name": "RED"},
75 | "id": 1,
76 | "geometry": {
77 | "type": "LineString",
78 | "coordinates": [
79 | [0.0, 0.0],
80 | [1.0, 1.0]
81 | ]
82 | }
83 | },
84 | { "type": "Feature",
85 | "properties": {"model": "djgeojson.route", "name": "blue"},
86 | "id": 2,
87 | "geometry": {
88 | "type": "LineString",
89 | "coordinates": [
90 | [0.0, 0.0],
91 | [1.0, 1.0]
92 | ]
93 | }
94 | }
95 | ]}"""
96 |
97 | # Deserialize into a list of objects
98 | objects = list(serializers.deserialize('geojson', input_geojson))
99 |
100 | # Were three objects deserialized?
101 | self.assertEqual(len(objects), 2)
102 |
103 | # Did the objects deserialize correctly?
104 | self.assertEqual(objects[1].object.name, "blue")
105 | self.assertEqual(objects[0].object.upper_name, "GREEN")
106 | self.assertEqual(objects[0].object.geom,
107 | LineString((0.0, 0.0), (1.0, 1.0), srid=objects[0].object.geom.srid))
108 |
109 | def test_with_model_name_passed_as_argument(self):
110 | input_geojson = """
111 | {"type": "FeatureCollection",
112 | "features": [
113 | { "type": "Feature",
114 | "properties": {"name": "bleh"},
115 | "id": 24,
116 | "geometry": {
117 | "type": "LineString",
118 | "coordinates": [
119 | [1, 2],
120 | [42, 3]
121 | ]
122 | }
123 | }
124 | ]}"""
125 |
126 | my_object = list(serializers.deserialize(
127 | 'geojson', input_geojson, model_name='djgeojson.route'))[0].object
128 |
129 | self.assertEqual(my_object.name, "bleh")
130 |
131 |
132 | class GeoJsonSerializerTest(TestCase):
133 |
134 | def test_basic(self):
135 | # Stuff to serialize
136 | route1 = Route.objects.create(
137 | name='green', geom="LINESTRING (0 0, 1 1)")
138 | route2 = Route.objects.create(
139 | name='blue', geom="LINESTRING (0 0, 1 1)")
140 | route3 = Route.objects.create(name='red', geom="LINESTRING (0 0, 1 1)")
141 |
142 | actual_geojson = json.loads(serializers.serialize(
143 | 'geojson', Route.objects.all(), properties=['name']))
144 | self.assertEqual(
145 | actual_geojson, {"crs": {"type": "link", "properties": {"href": "http://spatialreference.org/ref/epsg/4326/", "type": "proj4"}}, "type": "FeatureCollection", "features": [{"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"model": "djgeojson.route", "name": "green"}, "id": route1.pk}, {"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"model": "djgeojson.route", "name": "blue"}, "id": route2.pk}, {"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"model": "djgeojson.route", "name": "red"}, "id": route3.pk}]})
146 | actual_geojson_with_prop = json.loads(
147 | serializers.serialize(
148 | 'geojson', Route.objects.all(),
149 | properties=['name', 'upper_name', 'picture']))
150 | self.assertEqual(actual_geojson_with_prop,
151 | {"crs": {"type": "link", "properties": {"href": "http://spatialreference.org/ref/epsg/4326/", "type": "proj4"}}, "type": "FeatureCollection", "features": [{"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"picture": "image.png", "model": "djgeojson.route", "upper_name": "GREEN", "name": "green"}, "id": route1.pk}, {"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"picture": "image.png", "model": "djgeojson.route", "upper_name": "BLUE", "name": "blue"}, "id": route2.pk}, {"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"picture": "image.png", "model": "djgeojson.route", "upper_name": "RED", "name": "red"}, "id": route3.pk}]})
152 |
153 | def test_precision(self):
154 | serializer = Serializer()
155 | features = json.loads(serializer.serialize(
156 | [{'geom': 'SRID=2154;POINT (1 1)'}], precision=2, crs=False))
157 | self.assertEqual(
158 | features, {"type": "FeatureCollection", "features": [{"geometry": {"type": "Point", "coordinates": [-1.36, -5.98]}, "type": "Feature", "properties": {}}]})
159 |
160 | def test_simplify(self):
161 | serializer = Serializer()
162 | features = json.loads(serializer.serialize(
163 | [{'geom': 'SRID=4326;LINESTRING (1 1, 1.5 1, 2 3, 3 3)'}], simplify=0.5, crs=False))
164 | self.assertEqual(
165 | features, {"type": "FeatureCollection", "features": [{"geometry": {"type": "LineString", "coordinates": [[1.0, 1.0], [2.0, 3.0], [3.0, 3.0]]}, "type": "Feature", "properties": {}}]})
166 |
167 | def test_force2d(self):
168 | serializer = Serializer()
169 | features2d = json.loads(serializer.serialize(
170 | [{'geom': 'SRID=4326;POINT Z (1 2 3)'}],
171 | force2d=True, crs=False))
172 | self.assertEqual(
173 | features2d, {"type": "FeatureCollection", "features": [{"geometry": {"type": "Point", "coordinates": [1.0, 2.0]}, "type": "Feature", "properties": {}}]})
174 |
175 | def test_named_crs(self):
176 | serializer = Serializer()
177 | features = json.loads(serializer.serialize(
178 | [{'geom': 'SRID=4326;POINT (1 2)'}],
179 | crs_type="name"))
180 | self.assertEqual(
181 | features['crs'], {"type": "name", "properties": {"name": "EPSG:4326"}})
182 |
183 | def test_misspelled_named_crs(self):
184 | serializer = Serializer()
185 | features = json.loads(serializer.serialize(
186 | [{'geom': 'SRID=4326;POINT (1 2)'}],
187 | crs_type="named"))
188 | self.assertEqual(
189 | features['crs'], {"type": "link", "properties": {"href": "http://spatialreference.org/ref/epsg/4326/", "type": "proj4"}})
190 |
191 | def test_pk_property(self):
192 | route = Route.objects.create(name='red', geom="LINESTRING (0 0, 1 1)")
193 | serializer = Serializer()
194 | features2d = json.loads(serializer.serialize(
195 | Route.objects.all(), properties=['id'], crs=False))
196 | self.assertEqual(
197 | features2d, {"type": "FeatureCollection", "features": [{"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"model": "djgeojson.route", "id": route.pk}, "id": route.pk}]})
198 |
199 | def test_geometry_property(self):
200 | class Basket(models.Model):
201 |
202 | @property
203 | def geom(self):
204 | return GeometryCollection(LineString((3, 4, 5), (6, 7, 8)), Point(1, 2, 3), srid=4326)
205 |
206 | serializer = Serializer()
207 | features = json.loads(
208 | serializer.serialize([Basket()], crs=False, force2d=True))
209 | expected_content = {"type": "FeatureCollection", "features": [{"geometry": {"type": "GeometryCollection", "geometries": [{"type": "LineString", "coordinates": [[3.0, 4.0], [6.0, 7.0]]}, {"type": "Point", "coordinates": [1.0, 2.0]}]}, "type": "Feature", "properties": {"id": None}}]}
210 | self.assertEqual(features, expected_content)
211 |
212 | def test_none_geometry(self):
213 | class Empty(models.Model):
214 | geom = None
215 | serializer = Serializer()
216 | features = json.loads(serializer.serialize([Empty()], crs=False))
217 | self.assertEqual(
218 | features, {
219 | "type": "FeatureCollection",
220 | "features": [{
221 | "geometry": None,
222 | "type": "Feature",
223 | "properties": {"id": None}}]
224 | })
225 |
226 | def test_bbox_auto(self):
227 | serializer = Serializer()
228 | features = json.loads(serializer.serialize([{'geom': 'SRID=4326;LINESTRING (1 1, 3 3)'}],
229 | bbox_auto=True, crs=False))
230 | self.assertEqual(
231 | features, {
232 | "type": "FeatureCollection",
233 | "features": [{
234 | "geometry": {"type": "LineString", "coordinates": [[1.0, 1.0], [3.0, 3.0]]},
235 | "type": "Feature",
236 | "properties": {},
237 | "bbox": [1.0, 1.0, 3.0, 3.0]
238 | }]
239 | })
240 |
241 |
242 | class ForeignKeyTest(TestCase):
243 |
244 | def setUp(self):
245 | self.route = Route.objects.create(
246 | name='green', geom="LINESTRING (0 0, 1 1)")
247 | Sign(label='A', route=self.route).save()
248 |
249 | def test_serialize_foreign(self):
250 | serializer = Serializer()
251 | features = json.loads(serializer.serialize(Sign.objects.all(), properties=['route']))
252 | self.assertEqual(
253 | features, {"crs": {"type": "link", "properties": {"href": "http://spatialreference.org/ref/epsg/4326/", "type": "proj4"}}, "type": "FeatureCollection", "features": [{"geometry": {"type": "Point", "coordinates": [0.5, 0.5]}, "type": "Feature", "properties": {"route": 1, "model": "djgeojson.sign"}, "id": self.route.pk}]})
254 |
255 | def test_serialize_foreign_natural(self):
256 | serializer = Serializer()
257 | features = json.loads(serializer.serialize(
258 | Sign.objects.all(), use_natural_keys=True, properties=['route']))
259 | self.assertEqual(
260 | features, {"crs": {"type": "link", "properties": {"href": "http://spatialreference.org/ref/epsg/4326/", "type": "proj4"}}, "type": "FeatureCollection", "features": [{"geometry": {"type": "Point", "coordinates": [0.5, 0.5]}, "type": "Feature", "properties": {"route": "green", "model": "djgeojson.sign"}, "id": self.route.pk}]})
261 |
262 |
263 | class ManyToManyTest(TestCase):
264 |
265 | def setUp(self):
266 | country1 = Country(label='C1', geom="POLYGON ((0 0,1 1,0 2,0 0))")
267 | country1.save()
268 | country2 = Country(label='C2', geom="POLYGON ((0 0,1 1,0 2,0 0))")
269 | country2.save()
270 |
271 | self.route1 = Route.objects.create(
272 | name='green', geom="LINESTRING (0 0, 1 1)")
273 | self.route2 = Route.objects.create(
274 | name='blue', geom="LINESTRING (0 0, 1 1)")
275 | self.route2.countries.add(country1)
276 | self.route3 = Route.objects.create(
277 | name='red', geom="LINESTRING (0 0, 1 1)")
278 | self.route3.countries.add(country1)
279 | self.route3.countries.add(country2)
280 |
281 | def test_serialize_manytomany(self):
282 | serializer = Serializer()
283 | features = json.loads(serializer.serialize(
284 | Route.objects.all(), properties=['countries']))
285 | self.assertEqual(
286 | features, {"crs": {"type": "link", "properties": {"href": "http://spatialreference.org/ref/epsg/4326/", "type": "proj4"}}, "type": "FeatureCollection", "features": [{"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"model": "djgeojson.route", "countries": []}, "id": self.route1.pk}, {"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"model": "djgeojson.route", "countries": [1]}, "id": self.route2.pk}, {"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"model": "djgeojson.route", "countries": [1, 2]}, "id": self.route3.pk}]})
287 |
288 | def test_serialize_manytomany_natural(self):
289 | serializer = Serializer()
290 | features = json.loads(serializer.serialize(
291 | Route.objects.all(), use_natural_keys=True, properties=['countries']))
292 | self.assertEqual(
293 | features, {"crs": {"type": "link", "properties": {"href": "http://spatialreference.org/ref/epsg/4326/", "type": "proj4"}}, "type": "FeatureCollection", "features": [{"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"model": "djgeojson.route", "countries": []}, "id": self.route1.pk}, {"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"model": "djgeojson.route", "countries": ["C1"]}, "id": self.route2.pk}, {"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"model": "djgeojson.route", "countries": ["C1", "C2"]}, "id": self.route3.pk}]})
294 |
295 |
296 | class ReverseForeignkeyTest(TestCase):
297 |
298 | def setUp(self):
299 | self.route = Route(name='green', geom="LINESTRING (0 0, 1 1)")
300 | self.route.save()
301 | self.sign1 = Sign.objects.create(label='A', route=self.route)
302 | self.sign2 = Sign.objects.create(label='B', route=self.route)
303 | self.sign3 = Sign.objects.create(label='C', route=self.route)
304 |
305 | def test_relation_set(self):
306 | self.assertEqual(len(self.route.signs.all()), 3)
307 |
308 | def test_serialize_reverse(self):
309 | serializer = Serializer()
310 | features = json.loads(serializer.serialize(
311 | Route.objects.all(), properties=['signs']))
312 | self.assertEqual(
313 | features, {
314 | "crs": {
315 | "type": "link", "properties": {
316 | "href": "http://spatialreference.org/ref/epsg/4326/",
317 | "type": "proj4"
318 | }
319 | },
320 | "type": "FeatureCollection",
321 | "features": [{
322 | "geometry": {
323 | "type": "LineString",
324 | "coordinates": [[0.0, 0.0], [1.0, 1.0]]
325 | },
326 | "type": "Feature",
327 | "properties": {
328 | "model": "djgeojson.route",
329 | "signs": [
330 | self.sign1.pk,
331 | self.sign2.pk,
332 | self.sign3.pk]},
333 | "id": self.route.pk
334 | }]
335 | })
336 |
337 | def test_serialize_reverse_natural(self):
338 | serializer = Serializer()
339 | features = json.loads(serializer.serialize(
340 | Route.objects.all(), use_natural_keys=True, properties=['signs']))
341 | self.assertEqual(
342 | features, {
343 | "crs": {
344 | "type": "link",
345 | "properties": {
346 | "href": "http://spatialreference.org/ref/epsg/4326/",
347 | "type": "proj4"
348 | }
349 | },
350 | "type": "FeatureCollection",
351 | "features": [{
352 | "geometry": {
353 | "type": "LineString",
354 | "coordinates": [[0.0, 0.0], [1.0, 1.0]]},
355 | "type": "Feature",
356 | "properties": {
357 | "model": "djgeojson.route",
358 | "signs": ["A", "B", "C"]},
359 | "id": self.route.pk
360 | }]
361 | })
362 |
363 |
364 | class GeoJsonTemplateTagTest(TestCase):
365 |
366 | def setUp(self):
367 | self.route1 = Route.objects.create(name='green',
368 | geom="LINESTRING (0 0, 1 1)")
369 | self.route2 = Route.objects.create(name='blue',
370 | geom="LINESTRING (0 0, 1 1)")
371 | self.route3 = Route.objects.create(name='red',
372 | geom="LINESTRING (0 0, 1 1)")
373 |
374 | def test_templatetag_renders_single_object(self):
375 | feature = json.loads(geojsonfeature(self.route1))
376 | self.assertEqual(
377 | feature, {
378 | "crs": {
379 | "type": "link",
380 | "properties": {
381 | "href": "http://spatialreference.org/ref/epsg/4326/",
382 | "type": "proj4"
383 | }
384 | },
385 | "type": "FeatureCollection",
386 | "features": [{
387 | "geometry": {
388 | "type": "LineString",
389 | "coordinates": [[0.0, 0.0], [1.0, 1.0]]},
390 | "type": "Feature", "properties": {}}]
391 | })
392 |
393 | def test_templatetag_renders_queryset(self):
394 | feature = json.loads(geojsonfeature(Route.objects.all()))
395 | self.assertEqual(
396 | feature, {
397 | "crs": {
398 | "type": "link", "properties": {
399 | "href": "http://spatialreference.org/ref/epsg/4326/",
400 | "type": "proj4"
401 | }
402 | },
403 | "type": "FeatureCollection",
404 | "features": [
405 | {
406 | "geometry": {
407 | "type": "LineString",
408 | "coordinates": [[0.0, 0.0], [1.0, 1.0]]
409 | },
410 | "type": "Feature",
411 | "properties": {
412 | "model": "djgeojson.route"
413 | },
414 | "id": self.route1.pk
415 | },
416 | {
417 | "geometry": {
418 | "type": "LineString",
419 | "coordinates": [[0.0, 0.0], [1.0, 1.0]]
420 | },
421 | "type": "Feature",
422 | "properties": {"model": "djgeojson.route"},
423 | "id": self.route2.pk
424 | },
425 | {
426 | "geometry": {"type": "LineString",
427 | "coordinates": [[0.0, 0.0], [1.0, 1.0]]},
428 | "type": "Feature",
429 | "properties": {"model": "djgeojson.route"},
430 | "id": self.route3.pk
431 | }
432 | ]
433 | })
434 |
435 | def test_template_renders_geometry(self):
436 | feature = json.loads(geojsonfeature(self.route1.geom))
437 | self.assertEqual(
438 | feature, {
439 | "geometry": {"type": "LineString",
440 | "coordinates": [[0.0, 0.0], [1.0, 1.0]]},
441 | "type": "Feature", "properties": {}
442 | })
443 |
444 | def test_property_can_be_specified(self):
445 | features = json.loads(geojsonfeature(self.route1,
446 | "name"))
447 | feature = features['features'][0]
448 | self.assertEqual(feature['properties']['name'],
449 | self.route1.name)
450 |
451 | def test_several_properties_can_be_specified(self):
452 | features = json.loads(geojsonfeature(self.route1,
453 | "name,id"))
454 | feature = features['features'][0]
455 | self.assertEqual(feature['properties'],
456 | {'name': self.route1.name,
457 | 'id': self.route1.id})
458 |
459 | def test_srid_can_be_specified(self):
460 | feature = json.loads(geojsonfeature(self.route1.geom, "::2154"))
461 | self.assertAlmostEqual(feature['geometry']['coordinates'][0][0], 253531.1305237495)
462 | self.assertAlmostEqual(feature['geometry']['coordinates'][0][1], 909838.9305578759)
463 | self.assertAlmostEqual(feature['geometry']['coordinates'][1][0], 406035.7627716485)
464 | self.assertAlmostEqual(feature['geometry']['coordinates'][1][1], 1052023.2925472297)
465 |
466 | def test_geom_field_name_can_be_specified(self):
467 | features = json.loads(geojsonfeature(self.route1, ":geom"))
468 | feature = features['features'][0]
469 | self.assertEqual(feature['geometry']['coordinates'],
470 | [[0.0, 0.0], [1.0, 1.0]])
471 |
472 | def test_geom_field_raises_attributeerror_if_unknown(self):
473 | self.assertRaises(AttributeError, geojsonfeature, self.route1, ":geo")
474 |
475 |
476 | class ViewsTest(TestCase):
477 |
478 | def setUp(self):
479 | self.route = Route.objects.create(
480 | name='green', geom="LINESTRING (0 0, 1 1)")
481 | Sign(label='A', route=self.route).save()
482 |
483 | def test_view_default_options(self):
484 | view = GeoJSONLayerView(model=Route)
485 | view.object_list = []
486 | response = view.render_to_response(context={})
487 | geojson = json.loads(smart_str(response.content))
488 | self.assertEqual(geojson['features'][0]['geometry']['coordinates'],
489 | [[0.0, 0.0], [1.0, 1.0]])
490 |
491 | def test_view_can_control_properties(self):
492 | class FullGeoJSON(GeoJSONLayerView):
493 | properties = ['name']
494 | view = FullGeoJSON(model=Route)
495 | view.object_list = []
496 | response = view.render_to_response(context={})
497 | geojson = json.loads(smart_str(response.content))
498 | self.assertEqual(geojson['features'][0]['properties']['name'],
499 | 'green')
500 |
501 | def test_view_foreign(self):
502 | class FullGeoJSON(GeoJSONLayerView):
503 | properties = ['label', 'route']
504 | view = FullGeoJSON(model=Sign)
505 | view.object_list = []
506 | response = view.render_to_response(context={})
507 | geojson = json.loads(smart_str(response.content))
508 | self.assertEqual(geojson['features'][0]['properties']['route'],
509 | 1)
510 |
511 | def test_view_foreign_natural(self):
512 | class FullGeoJSON(GeoJSONLayerView):
513 | properties = ['label', 'route']
514 | use_natural_keys = True
515 | view = FullGeoJSON(model=Sign)
516 | view.object_list = []
517 | response = view.render_to_response(context={})
518 | geojson = json.loads(smart_str(response.content))
519 | self.assertEqual(geojson['features'][0]['properties']['route'],
520 | 'green')
521 |
522 |
523 | class TileEnvelopTest(TestCase):
524 | def setUp(self):
525 | self.view = TiledGeoJSONLayerView()
526 |
527 | def test_raises_error_if_not_spherical_mercator(self):
528 | self.view.tile_srid = 2154
529 | self.assertRaises(AssertionError, self.view.tile_coord, 0, 0, 0)
530 |
531 | def test_origin_is_north_west_for_tile_0(self):
532 | self.assertEqual((-180.0, 85.0511287798066),
533 | self.view.tile_coord(0, 0, 0))
534 |
535 | def test_origin_is_center_for_middle_tile(self):
536 | self.assertEqual((0, 0), self.view.tile_coord(8, 8, 4))
537 |
538 |
539 | class TiledGeoJSONViewTest(TestCase):
540 | def setUp(self):
541 | self.view = TiledGeoJSONLayerView(model=Route)
542 | self.view.args = []
543 | self.r1 = Route.objects.create(geom=LineString((0, 1), (10, 1)))
544 | self.r2 = Route.objects.create(geom=LineString((0, -1), (-10, -1)))
545 |
546 | def test_view_with_kwargs(self):
547 | self.view.kwargs = {'z': 4,
548 | 'x': 8,
549 | 'y': 7}
550 | response = self.view.render_to_response(context={})
551 | geojson = json.loads(smart_str(response.content))
552 | self.assertEqual(geojson['features'][0]['geometry']['coordinates'], [[0.0, 1.0], [10.0, 1.0]])
553 |
554 | def test_view_with_kwargs_wrong_type_z(self):
555 | self.view.kwargs = {'z': 'a',
556 | 'x': 8,
557 | 'y': 7}
558 | self.assertRaises(SuspiciousOperation,
559 | self.view.render_to_response,
560 | context={})
561 |
562 | def test_view_with_kwargs_wrong_type_x(self):
563 | self.view.kwargs = {'z': 1,
564 | 'x': 'a',
565 | 'y': 7}
566 | self.assertRaises(SuspiciousOperation,
567 | self.view.render_to_response,
568 | context={})
569 |
570 | def test_view_with_kwargs_wrong_type_y(self):
571 | self.view.kwargs = {'z': 4,
572 | 'x': 8,
573 | 'y': 'a'}
574 | self.assertRaises(SuspiciousOperation,
575 | self.view.render_to_response,
576 | context={})
577 |
578 | def test_view_with_kwargs_no_z(self):
579 | self.view.kwargs = {'x': 8,
580 | 'y': 7}
581 | self.assertRaises(SuspiciousOperation,
582 | self.view.render_to_response,
583 | context={})
584 |
585 | def test_view_with_kwargs_no_x(self):
586 | self.view.kwargs = {'z': 8,
587 | 'y': 7}
588 | self.assertRaises(SuspiciousOperation,
589 | self.view.render_to_response,
590 | context={})
591 |
592 | def test_view_with_kwargs_no_y(self):
593 | self.view.kwargs = {'x': 8,
594 | 'z': 7}
595 | self.assertRaises(SuspiciousOperation,
596 | self.view.render_to_response,
597 | context={})
598 |
599 | def test_view_is_serialized_as_geojson(self):
600 | self.view.args = [4, 8, 7]
601 | response = self.view.render_to_response(context={})
602 | geojson = json.loads(smart_str(response.content))
603 | self.assertEqual(geojson['features'][0]['geometry']['coordinates'],
604 | [[0.0, 1.0], [10.0, 1.0]])
605 |
606 | def test_view_trims_to_geometries_boundaries(self):
607 | self.view.args = [8, 128, 127]
608 | response = self.view.render_to_response(context={})
609 | geojson = json.loads(smart_str(response.content))
610 | self.assertEqual(geojson['features'][0]['geometry']['coordinates'],
611 | [[0.0, 1.0], [1.40625, 1.0]])
612 |
613 | def test_geometries_trim_can_be_disabled(self):
614 | self.view.args = [8, 128, 127]
615 | self.view.trim_to_boundary = False
616 | response = self.view.render_to_response(context={})
617 | geojson = json.loads(smart_str(response.content))
618 | self.assertEqual(geojson['features'][0]['geometry']['coordinates'],
619 | [[0.0, 1.0], [10.0, 1.0]])
620 |
621 | def test_tile_extent_is_provided_in_collection(self):
622 | self.view.args = [8, 128, 127]
623 | response = self.view.render_to_response(context={})
624 | geojson = json.loads(smart_str(response.content))
625 | self.assertEqual(geojson['bbox'],
626 | [0.0, 0.0, 1.40625, 1.4061088354351565])
627 |
628 | def test_url_parameters_are_converted_to_int(self):
629 | self.view.args = ['0', '0', '0']
630 | self.assertEqual(2, len(self.view.get_queryset()))
631 |
632 | def test_zoom_0_queryset_contains_all(self):
633 | self.view.args = [0, 0, 0]
634 | self.assertEqual(2, len(self.view.get_queryset()))
635 |
636 | def test_zoom_4_filters_by_tile_extent(self):
637 | self.view.args = [4, 8, 7]
638 | self.assertEqual([self.r1], list(self.view.get_queryset()))
639 |
640 | def test_some_tiles_have_empty_queryset(self):
641 | self.view.args = [4, 6, 8]
642 | self.assertEqual(0, len(self.view.get_queryset()))
643 |
644 | def test_simplification_depends_on_zoom_level(self):
645 | self.view.simplifications = {6: 100}
646 | self.view.args = [6, 8, 4]
647 | self.view.get_queryset()
648 | self.assertEqual(self.view.simplify, 100)
649 |
650 | def test_simplification_is_default_if_not_specified(self):
651 | self.view.simplifications = {}
652 | self.view.args = [0, 8, 4]
653 | self.view.get_queryset()
654 | self.assertEqual(self.view.simplify, None)
655 |
656 | def test_simplification_takes_the_closest_upper_level(self):
657 | self.view.simplifications = {3: 100, 6: 200}
658 | self.view.args = [4, 8, 4]
659 | self.view.get_queryset()
660 | self.assertEqual(self.view.simplify, 200)
661 |
662 |
663 | class FixedSridPoint(models.Model):
664 |
665 | geom = models.PointField(srid=28992)
666 |
667 |
668 | class TiledGeoJSONViewFixedSridTest(TestCase):
669 | def setUp(self):
670 | self.view = TiledGeoJSONLayerView(model=FixedSridPoint)
671 | self.view.args = []
672 | self.p1 = FixedSridPoint.objects.create(geom=Point(253286, 531490))
673 | self.p2 = FixedSridPoint.objects.create(geom=Point(253442, 532897))
674 |
675 | def test_within_viewport(self):
676 | self.view.args = [12, 2125, 1338]
677 | response = self.view.render_to_response(context={})
678 | geojson = json.loads(smart_str(response.content))
679 | self.assertEqual(len(geojson['features']), 2)
680 | self.assertAlmostEqual(geojson['features'][0]['geometry']['coordinates'][0], 6.843321961076886)
681 | self.assertAlmostEqual(geojson['features'][0]['geometry']['coordinates'][1], 52.76181518632031)
682 | self.assertAlmostEqual(geojson['features'][1]['geometry']['coordinates'][0], 6.846053240233331)
683 | self.assertAlmostEqual(geojson['features'][1]['geometry']['coordinates'][1], 52.77442791046052)
684 |
685 |
686 | class Address(models.Model):
687 | geom = GeoJSONField()
688 |
689 |
690 | class ModelFieldTest(TestCase):
691 | def setUp(self):
692 | self.address = Address()
693 | self.address.geom = {'type': 'Point', 'coordinates': [0, 0]}
694 | self.address.save()
695 |
696 | def test_models_can_have_geojson_fields(self):
697 | saved = Address.objects.get(id=self.address.id)
698 | if isinstance(saved.geom, dict):
699 | self.assertDictEqual(saved.geom, self.address.geom)
700 | else:
701 | # Django 1.8 !
702 | self.assertEqual(json.loads(saved.geom.geojson), self.address.geom)
703 |
704 | def test_default_form_field_is_geojsonfield(self):
705 | field = self.address._meta.get_field('geom').formfield()
706 | self.assertTrue(isinstance(field, GeoJSONFormField))
707 |
708 | def test_default_form_field_widget_is_hidden(self):
709 | field = self.address._meta.get_field('geom').formfield()
710 | self.assertTrue(isinstance(field.widget, HiddenInput))
711 |
712 | def test_default_form_field_has_geojson_validator(self):
713 | field = self.address._meta.get_field('geom').formfield()
714 | validator = field.validators[0]
715 | self.assertTrue(isinstance(validator, GeoJSONValidator))
716 |
717 | def test_form_field_raises_if_invalid_type(self):
718 | field = self.address._meta.get_field('geom').formfield()
719 | self.assertRaises(ValidationError, field.clean,
720 | {'type': 'FeatureCollection', 'foo': 'bar'})
721 |
722 | def test_form_field_raises_if_type_missing(self):
723 | field = self.address._meta.get_field('geom').formfield()
724 | self.assertRaises(ValidationError, field.clean,
725 | {'foo': 'bar'})
726 |
727 | def test_field_can_be_serialized(self):
728 | serializer = Serializer()
729 | geojson = serializer.serialize(Address.objects.all(), crs=False)
730 | features = json.loads(geojson)
731 | self.assertEqual(
732 | features, {
733 | 'type': u'FeatureCollection',
734 | 'features': [{
735 | 'id': self.address.id,
736 | 'type': 'Feature',
737 | 'geometry': {'type': 'Point', 'coordinates': [0, 0]},
738 | 'properties': {
739 | 'model': 'djgeojson.address'
740 | }
741 | }]
742 | })
743 |
744 | def test_field_can_be_deserialized(self):
745 | input_geojson = """
746 | {"type": "FeatureCollection",
747 | "features": [
748 | { "type": "Feature",
749 | "properties": {"model": "djgeojson.address"},
750 | "id": 1,
751 | "geometry": {
752 | "type": "Point",
753 | "coordinates": [0.0, 0.0]
754 | }
755 | }
756 | ]}"""
757 | objects = list(serializers.deserialize('geojson', input_geojson))
758 | self.assertEqual(objects[0].object.geom,
759 | {'type': 'Point', 'coordinates': [0, 0]})
760 |
761 | def test_model_can_be_omitted(self):
762 | serializer = Serializer()
763 | geojson = serializer.serialize(Address.objects.all(),
764 | with_modelname=False)
765 | features = json.loads(geojson)
766 | self.assertEqual(
767 | features, {
768 | "crs": {
769 | "type": "link",
770 | "properties": {
771 | "href": "http://spatialreference.org/ref/epsg/4326/",
772 | "type": "proj4"
773 | }
774 | },
775 | 'type': 'FeatureCollection',
776 | 'features': [{
777 | 'id': self.address.id,
778 | 'type': 'Feature',
779 | 'geometry': {'type': 'Point', 'coordinates': [0, 0]},
780 | 'properties': {}
781 | }]
782 | })
783 |
784 |
785 | class GeoJSONValidatorTest(TestCase):
786 | def test_validator_raises_if_missing_type(self):
787 | validator = GeoJSONValidator('GEOMETRY')
788 | self.assertRaises(ValidationError, validator, {'foo': 'bar'})
789 |
790 | def test_validator_raises_if_type_is_wrong(self):
791 | validator = GeoJSONValidator('GEOMETRY')
792 | self.assertRaises(ValidationError, validator,
793 | {'type': 'FeatureCollection',
794 | 'features': []})
795 |
796 | def test_validator_succeeds_if_type_matches(self):
797 | validator = GeoJSONValidator('POINT')
798 | self.assertIsNone(validator({'type': 'Point', 'coords': [0, 0]}))
799 |
800 | def test_validator_succeeds_if_type_is_generic(self):
801 | validator = GeoJSONValidator('GEOMETRY')
802 | self.assertIsNone(validator({'type': 'Point', 'coords': [0, 0]}))
803 | self.assertIsNone(validator({'type': 'LineString', 'coords': [0, 0]}))
804 | self.assertIsNone(validator({'type': 'Polygon', 'coords': [0, 0]}))
805 |
806 | def test_validator_fails_if_type_does_not_match(self):
807 | validator = GeoJSONValidator('POINT')
808 | self.assertRaises(ValidationError, validator,
809 | {'type': 'LineString', 'coords': [0, 0]})
810 |
--------------------------------------------------------------------------------
/djgeojson/views.py:
--------------------------------------------------------------------------------
1 | import math
2 |
3 | import django
4 | from django.core.exceptions import ImproperlyConfigured
5 |
6 | try:
7 | from django.contrib.gis.db.models.functions import Intersection
8 | except (ImportError, ImproperlyConfigured):
9 | Intersection = None
10 | from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
11 | from django.utils.decorators import method_decorator
12 | from django.views.decorators.gzip import gzip_page
13 | from django.views.generic import ListView
14 |
15 | try:
16 | from django.contrib.gis.geos import Polygon
17 | except (ImportError, ImproperlyConfigured):
18 | try:
19 | from django.contrib.gis.geos.geometry import Polygon
20 | except (ImportError, ImproperlyConfigured):
21 | from .nogeos import Polygon
22 |
23 | try:
24 | from django.contrib.gis.db.models import PointField
25 | except (ImportError, ImproperlyConfigured):
26 | from .fields import PointField
27 |
28 | from . import GEOJSON_DEFAULT_SRID
29 | from .http import HttpGeoJSONResponse
30 | from .serializers import Serializer as GeoJSONSerializer
31 |
32 |
33 | class GeoJSONResponseMixin(object):
34 | """
35 | A mixin that can be used to render a GeoJSON response.
36 | """
37 | response_class = HttpGeoJSONResponse
38 | """ Select fields for properties """
39 | properties = []
40 | """ Limit float precision """
41 | precision = None
42 | """ Simplify geometries """
43 | simplify = None
44 | """ Change projection of geometries """
45 | srid = GEOJSON_DEFAULT_SRID
46 | """ Geometry field to serialize """
47 | geometry_field = 'geom'
48 | """ Force 2D """
49 | force2d = False
50 | """ bbox """
51 | bbox = None
52 | """ bbox auto """
53 | bbox_auto = False
54 |
55 | use_natural_keys = False
56 |
57 | with_modelname = True
58 |
59 | crs_type = 'name'
60 |
61 | def render_to_response(self, context, **response_kwargs):
62 | """
63 | Returns a JSON response, transforming 'context' to make the payload.
64 | """
65 | serializer = GeoJSONSerializer()
66 | response = self.response_class(**response_kwargs)
67 | queryset = self.get_queryset()
68 |
69 | options = dict(properties=self.properties,
70 | precision=self.precision,
71 | simplify=self.simplify,
72 | srid=self.srid,
73 | geometry_field=self.geometry_field,
74 | force2d=self.force2d,
75 | bbox=self.bbox,
76 | bbox_auto=self.bbox_auto,
77 | use_natural_keys=self.use_natural_keys,
78 | with_modelname=self.with_modelname,
79 | crs_type=self.crs_type)
80 | serializer.serialize(queryset, stream=response, ensure_ascii=False,
81 | **options)
82 | return response
83 |
84 |
85 | class GeoJSONLayerView(GeoJSONResponseMixin, ListView):
86 | """
87 | A generic view to serve a model as a layer.
88 | """
89 | @method_decorator(gzip_page)
90 | def dispatch(self, *args, **kwargs):
91 | return super(GeoJSONLayerView, self).dispatch(*args, **kwargs)
92 |
93 |
94 | class TiledGeoJSONLayerView(GeoJSONLayerView):
95 | width = 256
96 | height = 256
97 | tile_srid = 3857
98 | trim_to_boundary = True
99 | """Simplify geometries by zoom level (dict )"""
100 | simplifications = None
101 |
102 | def tile_coord(self, xtile, ytile, zoom):
103 | """
104 | This returns the NW-corner of the square. Use the function
105 | with xtile+1 and/or ytile+1 to get the other corners.
106 | With xtile+0.5 & ytile+0.5 it will return the center of the tile.
107 | http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_numbers_to_lon..2Flat._2
108 | """
109 | assert self.tile_srid == 3857, 'Custom tile projection not supported yet'
110 | n = 2.0 ** zoom
111 | lon_deg = xtile / n * 360.0 - 180.0
112 | lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))
113 | lat_deg = math.degrees(lat_rad)
114 | return (lon_deg, lat_deg)
115 |
116 | def _parse_args(self):
117 | try:
118 | return [int(v) for v in (self.args[0], self.args[1], self.args[2])]
119 | except (ValueError, IndexError):
120 | try:
121 | return [int(v) for v in (self.kwargs['z'],
122 | self.kwargs['x'],
123 | self.kwargs['y'])]
124 | except (ValueError, TypeError, KeyError):
125 | # Raise suspicious, Django will return ``400 Bad Request``.
126 | error_msg = "Tile (z, x, y) parameters could not be processed."
127 | raise SuspiciousOperation(error_msg)
128 |
129 | def get_queryset(self):
130 | """
131 | Inspired by Glen Roberton's django-geojson-tiles view
132 | """
133 | self.z, self.x, self.y = self._parse_args()
134 | nw = self.tile_coord(self.x, self.y, self.z)
135 | se = self.tile_coord(self.x + 1, self.y + 1, self.z)
136 | bbox = Polygon((nw, (se[0], nw[1]),
137 | se, (nw[0], se[1]), nw))
138 | bbox.srid = self.srid
139 | qs = super(TiledGeoJSONLayerView, self).get_queryset()
140 | qs = qs.filter(**{
141 | '%s__intersects' % self.geometry_field: bbox
142 | })
143 | self.bbox = bbox.extent
144 |
145 | # Simplification dict by zoom level
146 | simplifications = self.simplifications or {}
147 | z = self.z
148 | self.simplify = simplifications.get(z)
149 | while self.simplify is None and z < 32:
150 | z += 1
151 | self.simplify = simplifications.get(z)
152 |
153 | # Won't trim point geometries to a boundary
154 | model_field = qs.model._meta.get_field(self.geometry_field)
155 | self.trim_to_boundary = (self.trim_to_boundary and
156 | not isinstance(model_field, PointField) and
157 | Intersection is not None)
158 | if self.trim_to_boundary:
159 | if django.VERSION < (1, 9):
160 | qs = qs.intersection(bbox)
161 | else:
162 | qs = qs.annotate(intersection=Intersection(self.geometry_field, bbox))
163 | self.geometry_field = 'intersection'
164 |
165 | return qs
166 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # User-friendly check for sphinx-build
11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
13 | endif
14 |
15 | # Internal variables.
16 | PAPEROPT_a4 = -D latex_paper_size=a4
17 | PAPEROPT_letter = -D latex_paper_size=letter
18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
19 | # the i18n builder cannot share the environment and doctrees with the others
20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
21 |
22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
23 |
24 | help:
25 | @echo "Please use \`make ' where is one of"
26 | @echo " html to make standalone HTML files"
27 | @echo " dirhtml to make HTML files named index.html in directories"
28 | @echo " singlehtml to make a single large HTML file"
29 | @echo " pickle to make pickle files"
30 | @echo " json to make JSON files"
31 | @echo " htmlhelp to make HTML files and a HTML help project"
32 | @echo " qthelp to make HTML files and a qthelp project"
33 | @echo " devhelp to make HTML files and a Devhelp project"
34 | @echo " epub to make an epub"
35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
36 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
38 | @echo " text to make text files"
39 | @echo " man to make manual pages"
40 | @echo " texinfo to make Texinfo files"
41 | @echo " info to make Texinfo files and run them through makeinfo"
42 | @echo " gettext to make PO message catalogs"
43 | @echo " changes to make an overview of all changed/added/deprecated items"
44 | @echo " xml to make Docutils-native XML files"
45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
46 | @echo " linkcheck to check all external links for integrity"
47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
48 |
49 | clean:
50 | rm -rf $(BUILDDIR)/*
51 |
52 | html:
53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
54 | @echo
55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
56 |
57 | dirhtml:
58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
59 | @echo
60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
61 |
62 | singlehtml:
63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
64 | @echo
65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
66 |
67 | pickle:
68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
69 | @echo
70 | @echo "Build finished; now you can process the pickle files."
71 |
72 | json:
73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
74 | @echo
75 | @echo "Build finished; now you can process the JSON files."
76 |
77 | htmlhelp:
78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
79 | @echo
80 | @echo "Build finished; now you can run HTML Help Workshop with the" \
81 | ".hhp project file in $(BUILDDIR)/htmlhelp."
82 |
83 | qthelp:
84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
85 | @echo
86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-geojson.qhcp"
89 | @echo "To view the help file:"
90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-geojson.qhc"
91 |
92 | devhelp:
93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
94 | @echo
95 | @echo "Build finished."
96 | @echo "To view the help file:"
97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-geojson"
98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-geojson"
99 | @echo "# devhelp"
100 |
101 | epub:
102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
103 | @echo
104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
105 |
106 | latex:
107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
108 | @echo
109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
111 | "(use \`make latexpdf' here to do that automatically)."
112 |
113 | latexpdf:
114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
115 | @echo "Running LaTeX files through pdflatex..."
116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
118 |
119 | latexpdfja:
120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
121 | @echo "Running LaTeX files through platex and dvipdfmx..."
122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
124 |
125 | text:
126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
127 | @echo
128 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
129 |
130 | man:
131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
132 | @echo
133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
134 |
135 | texinfo:
136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
137 | @echo
138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
139 | @echo "Run \`make' in that directory to run these through makeinfo" \
140 | "(use \`make info' here to do that automatically)."
141 |
142 | info:
143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
144 | @echo "Running Texinfo files through makeinfo..."
145 | make -C $(BUILDDIR)/texinfo info
146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
147 |
148 | gettext:
149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
150 | @echo
151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
152 |
153 | changes:
154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
155 | @echo
156 | @echo "The overview file is in $(BUILDDIR)/changes."
157 |
158 | linkcheck:
159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
160 | @echo
161 | @echo "Link check complete; look for any errors in the above output " \
162 | "or in $(BUILDDIR)/linkcheck/output.txt."
163 |
164 | doctest:
165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
166 | @echo "Testing of doctests in the sources finished, look at the " \
167 | "results in $(BUILDDIR)/doctest/output.txt."
168 |
169 | xml:
170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
171 | @echo
172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
173 |
174 | pseudoxml:
175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
176 | @echo
177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
178 |
--------------------------------------------------------------------------------
/docs/advanced.rst:
--------------------------------------------------------------------------------
1 | Advanced usage
2 | ==============
3 |
4 | Low-level serializer
5 | --------------------
6 |
7 | ::
8 |
9 | from djgeojson.serializers import Serializer as GeoJSONSerializer
10 |
11 | GeoJSONSerializer().serialize(Restaurants.objects.all(), use_natural_keys=True, with_modelname=False)
12 |
13 |
14 |
15 | Low-level deserializer
16 | ----------------------
17 |
18 | ::
19 |
20 | from djgeojson.serializers import Serializer as GeoJSONSerializer
21 |
22 | GeoJSONSerializer().deserialize('geojson', my_geojson)
23 |
24 | You can optionally specify the model name directly in the parameters:
25 |
26 | ::
27 |
28 | GeoJSONSerializer().deserialize('geojson', my_geojson, model_name=my_model_name)
29 |
30 |
31 |
32 |
33 | Dump GIS models, or fixtures
34 | ----------------------------
35 |
36 | Register the serializer in your project :
37 |
38 | ::
39 |
40 | # settings.py
41 |
42 | SERIALIZATION_MODULES = {
43 | 'geojson' : 'djgeojson.serializers'
44 | }
45 |
46 | Command-line ``dumpdata`` can export files, viewable in GIS software like QGis :
47 |
48 | ::
49 |
50 | python manage.py dumpdata --format=geojson yourapp.Model > export.geojson
51 |
52 | Works with ``loaddata`` as well, which can now import GeoJSON files.
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | #
2 | # django-geojson documentation build configuration file, created by
3 | # sphinx-quickstart on Tue Feb 28 13:44:49 2017.
4 | #
5 | # This file is execfile()d with the current directory set to its
6 | # containing dir.
7 | #
8 | # Note that not all possible configuration values are present in this
9 | # autogenerated file.
10 | #
11 | # All configuration values have a default; values that are commented out
12 | # serve to show the default.
13 |
14 | import os
15 | import sys
16 |
17 | # If extensions (or modules to document with autodoc) are in another directory,
18 | # add these directories to sys.path here. If the directory is relative to the
19 | # documentation root, use os.path.abspath to make it absolute, like shown here.
20 | #sys.path.insert(0, os.path.abspath('.'))
21 |
22 | # -- General configuration ------------------------------------------------
23 |
24 | # If your documentation needs a minimal Sphinx version, state it here.
25 | #needs_sphinx = '1.0'
26 |
27 | # Add any Sphinx extension module names here, as strings. They can be
28 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
29 | # ones.
30 | extensions = []
31 |
32 | # Add any paths that contain templates here, relative to this directory.
33 | templates_path = ['_templates']
34 |
35 | # The suffix of source filenames.
36 | source_suffix = '.rst'
37 |
38 | # The encoding of source files.
39 | #source_encoding = 'utf-8-sig'
40 |
41 | # The master toctree document.
42 | master_doc = 'index'
43 |
44 | # General information about the project.
45 | project = u'django-geojson'
46 | copyright = u'2017, Makina Corpus'
47 |
48 | # The version info for the project you're documenting, acts as replacement for
49 | # |version| and |release|, also used in various other places throughout the
50 | # built documents.
51 | #
52 | # The short X.Y version.
53 | version = '2.10'
54 | # The full version, including alpha/beta/rc tags.
55 | release = '2.10'
56 |
57 | # The language for content autogenerated by Sphinx. Refer to documentation
58 | # for a list of supported languages.
59 | #language = None
60 |
61 | # There are two options for replacing |today|: either, you set today to some
62 | # non-false value, then it is used:
63 | #today = ''
64 | # Else, today_fmt is used as the format for a strftime call.
65 | #today_fmt = '%B %d, %Y'
66 |
67 | # List of patterns, relative to source directory, that match files and
68 | # directories to ignore when looking for source files.
69 | exclude_patterns = ['_build']
70 |
71 | # The reST default role (used for this markup: `text`) to use for all
72 | # documents.
73 | #default_role = None
74 |
75 | # If true, '()' will be appended to :func: etc. cross-reference text.
76 | #add_function_parentheses = True
77 |
78 | # If true, the current module name will be prepended to all description
79 | # unit titles (such as .. function::).
80 | #add_module_names = True
81 |
82 | # If true, sectionauthor and moduleauthor directives will be shown in the
83 | # output. They are ignored by default.
84 | #show_authors = False
85 |
86 | # The name of the Pygments (syntax highlighting) style to use.
87 | pygments_style = 'sphinx'
88 |
89 | # A list of ignored prefixes for module index sorting.
90 | #modindex_common_prefix = []
91 |
92 | # If true, keep warnings as "system message" paragraphs in the built documents.
93 | #keep_warnings = False
94 |
95 |
96 | # -- Options for HTML output ----------------------------------------------
97 |
98 | # The theme to use for HTML and HTML Help pages. See the documentation for
99 | # a list of builtin themes.
100 | html_theme = 'default'
101 |
102 | # Theme options are theme-specific and customize the look and feel of a theme
103 | # further. For a list of options available for each theme, see the
104 | # documentation.
105 | #html_theme_options = {}
106 |
107 | # Add any paths that contain custom themes here, relative to this directory.
108 | #html_theme_path = []
109 |
110 | # The name for this set of Sphinx documents. If None, it defaults to
111 | # " v documentation".
112 | #html_title = None
113 |
114 | # A shorter title for the navigation bar. Default is the same as html_title.
115 | #html_short_title = None
116 |
117 | # The name of an image file (relative to this directory) to place at the top
118 | # of the sidebar.
119 | #html_logo = None
120 |
121 | # The name of an image file (within the static path) to use as favicon of the
122 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
123 | # pixels large.
124 | #html_favicon = None
125 |
126 | # Add any paths that contain custom static files (such as style sheets) here,
127 | # relative to this directory. They are copied after the builtin static files,
128 | # so a file named "default.css" will overwrite the builtin "default.css".
129 | html_static_path = ['_static']
130 |
131 | # Add any extra paths that contain custom files (such as robots.txt or
132 | # .htaccess) here, relative to this directory. These files are copied
133 | # directly to the root of the documentation.
134 | #html_extra_path = []
135 |
136 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
137 | # using the given strftime format.
138 | #html_last_updated_fmt = '%b %d, %Y'
139 |
140 | # If true, SmartyPants will be used to convert quotes and dashes to
141 | # typographically correct entities.
142 | #html_use_smartypants = True
143 |
144 | # Custom sidebar templates, maps document names to template names.
145 | #html_sidebars = {}
146 |
147 | # Additional templates that should be rendered to pages, maps page names to
148 | # template names.
149 | #html_additional_pages = {}
150 |
151 | # If false, no module index is generated.
152 | #html_domain_indices = True
153 |
154 | # If false, no index is generated.
155 | #html_use_index = True
156 |
157 | # If true, the index is split into individual pages for each letter.
158 | #html_split_index = False
159 |
160 | # If true, links to the reST sources are added to the pages.
161 | #html_show_sourcelink = True
162 |
163 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
164 | #html_show_sphinx = True
165 |
166 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
167 | #html_show_copyright = True
168 |
169 | # If true, an OpenSearch description file will be output, and all pages will
170 | # contain a tag referring to it. The value of this option must be the
171 | # base URL from which the finished HTML is served.
172 | #html_use_opensearch = ''
173 |
174 | # This is the file name suffix for HTML files (e.g. ".xhtml").
175 | #html_file_suffix = None
176 |
177 | # Output file base name for HTML help builder.
178 | htmlhelp_basename = 'django-geojsondoc'
179 |
180 |
181 | # -- Options for LaTeX output ---------------------------------------------
182 |
183 | latex_elements = {
184 | # The paper size ('letterpaper' or 'a4paper').
185 | #'papersize': 'letterpaper',
186 |
187 | # The font size ('10pt', '11pt' or '12pt').
188 | #'pointsize': '10pt',
189 |
190 | # Additional stuff for the LaTeX preamble.
191 | #'preamble': '',
192 | }
193 |
194 | # Grouping the document tree into LaTeX files. List of tuples
195 | # (source start file, target name, title,
196 | # author, documentclass [howto, manual, or own class]).
197 | latex_documents = [
198 | ('index', 'django-geojson.tex', u'django-geojson Documentation',
199 | u'Makina Corpus', 'manual'),
200 | ]
201 |
202 | # The name of an image file (relative to this directory) to place at the top of
203 | # the title page.
204 | #latex_logo = None
205 |
206 | # For "manual" documents, if this is true, then toplevel headings are parts,
207 | # not chapters.
208 | #latex_use_parts = False
209 |
210 | # If true, show page references after internal links.
211 | #latex_show_pagerefs = False
212 |
213 | # If true, show URL addresses after external links.
214 | #latex_show_urls = False
215 |
216 | # Documents to append as an appendix to all manuals.
217 | #latex_appendices = []
218 |
219 | # If false, no module index is generated.
220 | #latex_domain_indices = True
221 |
222 |
223 | # -- Options for manual page output ---------------------------------------
224 |
225 | # One entry per manual page. List of tuples
226 | # (source start file, name, description, authors, manual section).
227 | man_pages = [
228 | ('index', 'django-geojson', u'django-geojson Documentation',
229 | [u'Makina Corpus'], 1)
230 | ]
231 |
232 | # If true, show URL addresses after external links.
233 | #man_show_urls = False
234 |
235 |
236 | # -- Options for Texinfo output -------------------------------------------
237 |
238 | # Grouping the document tree into Texinfo files. List of tuples
239 | # (source start file, target name, title, author,
240 | # dir menu entry, description, category)
241 | texinfo_documents = [
242 | ('index', 'django-geojson', u'django-geojson Documentation',
243 | u'Makina Corpus', 'django-geojson', 'One line description of project.',
244 | 'Miscellaneous'),
245 | ]
246 |
247 | # Documents to append as an appendix to all manuals.
248 | #texinfo_appendices = []
249 |
250 | # If false, no module index is generated.
251 | #texinfo_domain_indices = True
252 |
253 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
254 | #texinfo_show_urls = 'footnote'
255 |
256 | # If true, do not generate a @detailmenu in the "Top" node's menu.
257 | #texinfo_no_detailmenu = False
258 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. django-geojson documentation master file, created by
2 | sphinx-quickstart on Tue Feb 28 13:44:49 2017.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to django-geojson's documentation!
7 | ==========================================
8 |
9 | Contents:
10 |
11 | .. toctree::
12 | :maxdepth: 2
13 |
14 | installation
15 | views
16 | models
17 | advanced
18 |
19 | Indices and tables
20 | ==================
21 |
22 | * :ref:`genindex`
23 | * :ref:`modindex`
24 | * :ref:`search`
25 |
26 |
--------------------------------------------------------------------------------
/docs/installation.rst:
--------------------------------------------------------------------------------
1 | Installation
2 | ============
3 |
4 | ::
5 |
6 | pip install django-geojson
7 |
8 |
9 | This package has **optional** `extra dependencies `_.
10 |
11 |
12 | If you need GeoJSON fields with map widgets :
13 |
14 | ::
15 |
16 | pip install "django-geojson [field]"
17 |
18 | Configuration
19 | -------------
20 |
21 | Add ``djgeojson`` to your applications :
22 |
23 | ::
24 |
25 | # settings.py
26 |
27 | INSTALLED_APPS += (
28 | 'djgeojson',
29 | )
30 |
31 | *(not required for views)*
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | REM Command file for Sphinx documentation
4 |
5 | if "%SPHINXBUILD%" == "" (
6 | set SPHINXBUILD=sphinx-build
7 | )
8 | set BUILDDIR=_build
9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
10 | set I18NSPHINXOPTS=%SPHINXOPTS% .
11 | if NOT "%PAPER%" == "" (
12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
14 | )
15 |
16 | if "%1" == "" goto help
17 |
18 | if "%1" == "help" (
19 | :help
20 | echo.Please use `make ^` where ^ is one of
21 | echo. html to make standalone HTML files
22 | echo. dirhtml to make HTML files named index.html in directories
23 | echo. singlehtml to make a single large HTML file
24 | echo. pickle to make pickle files
25 | echo. json to make JSON files
26 | echo. htmlhelp to make HTML files and a HTML help project
27 | echo. qthelp to make HTML files and a qthelp project
28 | echo. devhelp to make HTML files and a Devhelp project
29 | echo. epub to make an epub
30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
31 | echo. text to make text files
32 | echo. man to make manual pages
33 | echo. texinfo to make Texinfo files
34 | echo. gettext to make PO message catalogs
35 | echo. changes to make an overview over all changed/added/deprecated items
36 | echo. xml to make Docutils-native XML files
37 | echo. pseudoxml to make pseudoxml-XML files for display purposes
38 | echo. linkcheck to check all external links for integrity
39 | echo. doctest to run all doctests embedded in the documentation if enabled
40 | goto end
41 | )
42 |
43 | if "%1" == "clean" (
44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
45 | del /q /s %BUILDDIR%\*
46 | goto end
47 | )
48 |
49 |
50 | %SPHINXBUILD% 2> nul
51 | if errorlevel 9009 (
52 | echo.
53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
54 | echo.installed, then set the SPHINXBUILD environment variable to point
55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
56 | echo.may add the Sphinx directory to PATH.
57 | echo.
58 | echo.If you don't have Sphinx installed, grab it from
59 | echo.http://sphinx-doc.org/
60 | exit /b 1
61 | )
62 |
63 | if "%1" == "html" (
64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
65 | if errorlevel 1 exit /b 1
66 | echo.
67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html.
68 | goto end
69 | )
70 |
71 | if "%1" == "dirhtml" (
72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
73 | if errorlevel 1 exit /b 1
74 | echo.
75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
76 | goto end
77 | )
78 |
79 | if "%1" == "singlehtml" (
80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
81 | if errorlevel 1 exit /b 1
82 | echo.
83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
84 | goto end
85 | )
86 |
87 | if "%1" == "pickle" (
88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
89 | if errorlevel 1 exit /b 1
90 | echo.
91 | echo.Build finished; now you can process the pickle files.
92 | goto end
93 | )
94 |
95 | if "%1" == "json" (
96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
97 | if errorlevel 1 exit /b 1
98 | echo.
99 | echo.Build finished; now you can process the JSON files.
100 | goto end
101 | )
102 |
103 | if "%1" == "htmlhelp" (
104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
105 | if errorlevel 1 exit /b 1
106 | echo.
107 | echo.Build finished; now you can run HTML Help Workshop with the ^
108 | .hhp project file in %BUILDDIR%/htmlhelp.
109 | goto end
110 | )
111 |
112 | if "%1" == "qthelp" (
113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
114 | if errorlevel 1 exit /b 1
115 | echo.
116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^
117 | .qhcp project file in %BUILDDIR%/qthelp, like this:
118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-geojson.qhcp
119 | echo.To view the help file:
120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-geojson.ghc
121 | goto end
122 | )
123 |
124 | if "%1" == "devhelp" (
125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
126 | if errorlevel 1 exit /b 1
127 | echo.
128 | echo.Build finished.
129 | goto end
130 | )
131 |
132 | if "%1" == "epub" (
133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
134 | if errorlevel 1 exit /b 1
135 | echo.
136 | echo.Build finished. The epub file is in %BUILDDIR%/epub.
137 | goto end
138 | )
139 |
140 | if "%1" == "latex" (
141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
142 | if errorlevel 1 exit /b 1
143 | echo.
144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
145 | goto end
146 | )
147 |
148 | if "%1" == "latexpdf" (
149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
150 | cd %BUILDDIR%/latex
151 | make all-pdf
152 | cd %BUILDDIR%/..
153 | echo.
154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex.
155 | goto end
156 | )
157 |
158 | if "%1" == "latexpdfja" (
159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
160 | cd %BUILDDIR%/latex
161 | make all-pdf-ja
162 | cd %BUILDDIR%/..
163 | echo.
164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex.
165 | goto end
166 | )
167 |
168 | if "%1" == "text" (
169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
170 | if errorlevel 1 exit /b 1
171 | echo.
172 | echo.Build finished. The text files are in %BUILDDIR%/text.
173 | goto end
174 | )
175 |
176 | if "%1" == "man" (
177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
178 | if errorlevel 1 exit /b 1
179 | echo.
180 | echo.Build finished. The manual pages are in %BUILDDIR%/man.
181 | goto end
182 | )
183 |
184 | if "%1" == "texinfo" (
185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
186 | if errorlevel 1 exit /b 1
187 | echo.
188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
189 | goto end
190 | )
191 |
192 | if "%1" == "gettext" (
193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
194 | if errorlevel 1 exit /b 1
195 | echo.
196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
197 | goto end
198 | )
199 |
200 | if "%1" == "changes" (
201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
202 | if errorlevel 1 exit /b 1
203 | echo.
204 | echo.The overview file is in %BUILDDIR%/changes.
205 | goto end
206 | )
207 |
208 | if "%1" == "linkcheck" (
209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
210 | if errorlevel 1 exit /b 1
211 | echo.
212 | echo.Link check complete; look for any errors in the above output ^
213 | or in %BUILDDIR%/linkcheck/output.txt.
214 | goto end
215 | )
216 |
217 | if "%1" == "doctest" (
218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
219 | if errorlevel 1 exit /b 1
220 | echo.
221 | echo.Testing of doctests in the sources finished, look at the ^
222 | results in %BUILDDIR%/doctest/output.txt.
223 | goto end
224 | )
225 |
226 | if "%1" == "xml" (
227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
228 | if errorlevel 1 exit /b 1
229 | echo.
230 | echo.Build finished. The XML files are in %BUILDDIR%/xml.
231 | goto end
232 | )
233 |
234 | if "%1" == "pseudoxml" (
235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
236 | if errorlevel 1 exit /b 1
237 | echo.
238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
239 | goto end
240 | )
241 |
242 | :end
243 |
--------------------------------------------------------------------------------
/docs/models.rst:
--------------------------------------------------------------------------------
1 | Model and forms fields
2 | ======================
3 |
4 | GeoJSON fields are based on Django JSONField.
5 | See :doc:`installation` to install extra dependencies.
6 |
7 | They are useful to avoid usual GIS stacks (GEOS, GDAL, PostGIS...)
8 | for very simple use-cases (no spatial operation yet).
9 |
10 | ::
11 |
12 | from djgeojson.fields import PointField
13 |
14 | class Address(models.Model):
15 | geom = PointField()
16 |
17 | address = Address()
18 | address.geom = {'type': 'Point', 'coordinates': [0, 0]}
19 | address.save()
20 |
21 |
22 | Form widgets are rendered with Leaflet maps automatically if
23 | `django-leaflet `_
24 | is available.
25 |
26 | All geometry types are supported and respectively validated :
27 | `GeometryField`, `PointField`, `MultiPointField`, `LineStringField`,
28 | `MultiLineStringField`, `PolygonField`, `MultiPolygonField`,
29 | `GeometryCollectionField`.
30 |
--------------------------------------------------------------------------------
/docs/views.rst:
--------------------------------------------------------------------------------
1 | Views
2 | =====
3 |
4 | GeoJSON layer view
5 | ------------------
6 |
7 | Very useful for web mapping :
8 |
9 | ::
10 |
11 | # urls.py
12 | from djgeojson.views import GeoJSONLayerView
13 | ...
14 | url(r'^data.geojson$', GeoJSONLayerView.as_view(model=MushroomSpot), name='data'),
15 |
16 |
17 | Consume the vector layer as usual, for example, with Leaflet loaded with jQuery Ajax:
18 |
19 | ::
20 |
21 | # Leaflet JS
22 | var layer = L.geoJson();
23 | map.addLayer(layer);
24 | $.getJSON("{% url 'data' %}", function (data) {
25 | layer.addData(data);
26 | });
27 |
28 |
29 | Inherit generic views **only** if you need a reusable set of options :
30 |
31 | ::
32 |
33 | # views.py
34 | from djgeojson.views import GeoJSONLayerView
35 |
36 | class MapLayer(GeoJSONLayerView):
37 | # Options
38 | precision = 4 # float
39 | simplify = 0.5 # generalization
40 |
41 |
42 | # urls.py
43 | from .views import MapLayer, MeetingLayer
44 | ...
45 | url(r'^mushrooms.geojson$', MapLayer.as_view(model=MushroomSpot, properties=('name',)), name='mushrooms')
46 |
47 | Most common use-cases of reusable options are: low-fi precision, common list of fields between several views, etc.
48 |
49 | Options are :
50 |
51 | * **properties** : ``list`` of properties names, or ``dict`` for mapping field names and properties
52 | * **simplify** : generalization of geometries (See ``simplify()``)
53 | * **precision** : number of digit after comma
54 | * **geometry_field** : name of geometry field (*default*: ``geom``)
55 | * **srid** : projection (*default*: 4326, for WGS84)
56 | * **bbox** : Allows you to set your own bounding box on feature collection level
57 | * **bbox_auto** : True/False (default false). Will automatically generate a bounding box on a per feature level.
58 | * **use_natural_keys** : serialize natural keys instead of primary keys (*default*: ``False``)
59 | * **with_modelname** : add the app and model name to the properties. (*default*: ``True``)
60 | * **crs_type** : add the type of crs generated, options: ``name`` and ``link`` (*default*: ``name``)
61 |
62 | Tiled GeoJSON layer view
63 | ------------------------
64 |
65 | Vectorial tiles can help display a great number of objects on the map,
66 | with `reasonnable performance `_.
67 |
68 | ::
69 |
70 | # urls.py
71 | from djgeojson.views import TiledGeoJSONLayerView
72 | ...
73 |
74 | url(r'^data/(?P\d+)/(?P\d+)/(?P\d+).geojson$',
75 | TiledGeoJSONLayerView.as_view(model=MushroomSpot), name='data'),
76 |
77 |
78 | Consume the vector tiles using `Leaflet TileLayer GeoJSON `_, `Polymaps `_, `OpenLayers 3 `_ or `d3.js `_ for example.
79 |
80 | Options are :
81 |
82 | * **trim_to_boundary** : if ``True`` geometries are trimmed to the tile boundary
83 | * **simplifications** : a dict of simplification values by zoom level
84 |
85 |
86 |
87 | GeoJSON template filter
88 | -----------------------
89 |
90 | Mainly useful to dump features in HTML output and bypass AJAX call :
91 |
92 | ::
93 |
94 | // Leaflet JS
95 | var raw_data = '{{ object_list|geojsonfeature|safe|escapejs }}';
96 | var data = JSON.parse(raw_data);
97 | L.geoJSON(data).addTo(map);
98 |
99 | Will work either for a model, a geometry field or a queryset.
100 |
101 | ::
102 |
103 | {% load geojson_tags %}
104 |
105 | var feature = {{ object|geojsonfeature|safe }};
106 |
107 | var geom = {{ object.geom|geojsonfeature|safe }};
108 |
109 | var collection = {{ object_list|geojsonfeature|safe }};
110 |
111 |
112 | Properties and custom geometry field name can be provided.
113 |
114 | ::
115 |
116 | {{ object|geojsonfeature:"name,age" }}
117 | {{ object|geojsonfeature:"name,age:the_geom" }}
118 | {{ object|geojsonfeature:":geofield" }}
119 |
--------------------------------------------------------------------------------
/quicktest.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import os
3 | import sys
4 |
5 | import django
6 | from django.conf import settings
7 | from django.test.runner import DiscoverRunner
8 |
9 |
10 | class QuickDjangoTest:
11 | """
12 | A quick way to run the Django test suite without a fully-configured project.
13 |
14 | Example usage:
15 |
16 | >>> QuickDjangoTest('app1', 'app2')
17 |
18 | Based on a script published by Lukasz Dziedzia at:
19 | http://stackoverflow.com/questions/3841725/how-to-launch-tests-for-django-reusable-app
20 | """
21 | DIRNAME = os.path.dirname(__file__)
22 | INSTALLED_APPS = (
23 | 'django.contrib.auth',
24 | 'django.contrib.contenttypes',
25 | 'django.contrib.sessions',
26 | 'django.contrib.admin',
27 | 'django.contrib.messages',
28 | )
29 |
30 | def __init__(self, *args, **kwargs):
31 | self.apps = args
32 | self.run_tests()
33 |
34 | def run_tests(self):
35 | """
36 | Fire up the Django test suite developed for version 1.2
37 | """
38 | settings.configure(
39 | DATABASES={
40 | 'default': {
41 | 'ENGINE': 'django.contrib.gis.db.backends.spatialite',
42 | 'NAME': os.path.join(self.DIRNAME, 'database.db'),
43 | }
44 | },
45 | INSTALLED_APPS=self.INSTALLED_APPS + self.apps,
46 | SPATIALITE_LIBRARY_PATH=os.getenv('SPATIALITE_LIBRARY_PATH', 'mod_spatialite'),
47 | TEMPLATES=[
48 | {
49 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
50 | 'DIRS': [],
51 | 'APP_DIRS': True,
52 | 'OPTIONS': {
53 | "context_processors": [
54 | "django.contrib.auth.context_processors.auth",
55 | "django.contrib.messages.context_processors.messages",
56 | "django.template.context_processors.request",
57 | ]
58 | },
59 | },
60 | ],
61 | MIDDLEWARE=[
62 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
63 | 'django.contrib.messages.middleware.MessageMiddleware',
64 | 'django.contrib.sessions.middleware.SessionMiddleware',
65 | ],
66 | SECRET_KEY="not-secret",
67 | # django.VERSION >= (3, 2)
68 | DEFAULT_AUTO_FIELD="django.db.models.BigAutoField",
69 | )
70 |
71 | django.setup()
72 |
73 | # Workaround for incompatibility between SQLite 3.36+ and SpatiaLite 5,
74 | # as used on GitHub Actions. This change monkey patches
75 | # prepare_database() to avoid a call to InitSpatialMetaDataFull(). See:
76 | # https://code.djangoproject.com/ticket/32935
77 | # https://groups.google.com/g/spatialite-users/c/SnNZt4AGm_o
78 | from django.contrib.gis.db.backends.spatialite.base import (
79 | DatabaseWrapper,
80 | )
81 |
82 | def prepare_database(self):
83 | super(DatabaseWrapper, self).prepare_database()
84 | with self.cursor() as cursor:
85 | cursor.execute("PRAGMA table_info(geometry_columns);")
86 | if cursor.fetchall() == []:
87 | cursor.execute("SELECT InitSpatialMetaData(1)")
88 |
89 | DatabaseWrapper.prepare_database = prepare_database
90 |
91 | failures = DiscoverRunner().run_tests(self.apps, verbosity=1)
92 | if failures: # pragma: no cover
93 | sys.exit(failures)
94 |
95 | if __name__ == '__main__':
96 | """
97 | What do when the user hits this file from the shell.
98 |
99 | Example usage:
100 |
101 | $ python quicktest.py app1 app2
102 |
103 | """
104 | parser = argparse.ArgumentParser(
105 | usage="[args]",
106 | description="Run Django tests on the provided applications."
107 | )
108 | parser.add_argument('apps', nargs='+', type=str)
109 | args = parser.parse_args()
110 | QuickDjangoTest(*args.apps)
111 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | from io import open
3 |
4 | from setuptools import find_packages, setup
5 |
6 | here = os.path.abspath(os.path.dirname(__file__))
7 |
8 | setup(
9 | name='django-geojson',
10 | version='4.2.1dev',
11 | author='Mathieu Leplatre',
12 | author_email='mathieu.leplatre@makina-corpus.com',
13 | url='https://github.com/makinacorpus/django-geojson',
14 | download_url="http://pypi.python.org/pypi/django-geojson/",
15 | description="Serve vectorial map layers with Django",
16 | long_description=open(os.path.join(here, 'README.rst')).read() + '\n\n' +
17 | open(os.path.join(here, 'CHANGES'), encoding='utf-8').read(),
18 | license='LPGL, see LICENSE file.',
19 | install_requires=[
20 | 'Django>=4.2',
21 | ],
22 | extras_require={
23 | 'field': ['django-leaflet>=0.12'],
24 | 'docs': ['sphinx', 'sphinx-autobuild'],
25 | },
26 | packages=find_packages(),
27 | python_requires=">=3.8",
28 | include_package_data=True,
29 | zip_safe=False,
30 | classifiers=['Topic :: Utilities',
31 | 'Natural Language :: English',
32 | 'Operating System :: OS Independent',
33 | 'Intended Audience :: Developers',
34 | 'Environment :: Web Environment',
35 | 'Framework :: Django',
36 | 'Development Status :: 5 - Production/Stable',
37 | 'Programming Language :: Python :: 3.9',
38 | 'Programming Language :: Python :: 3.10',
39 | 'Programming Language :: Python :: 3.11',
40 | 'Programming Language :: Python :: 3.12',
41 | 'Programming Language :: Python :: 3.13',
42 | ],
43 | )
44 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | skipsdist = True
3 | envlist =
4 | flake8
5 | isort
6 | py39-django{40,41,42}
7 | py310-django{40,41,42,50,51,52}
8 | py311-django{41,42,50,51,52}
9 | py312-django{42,50,51,52}
10 | py313-django{51,52}
11 |
12 | [testenv]
13 | setenv =
14 | PYTHONPATH = {toxinidir}:{toxinidir}
15 | deps =
16 | coverage
17 | django40: Django>=4.0,<4.1
18 | django41: Django>=4.1,<4.2
19 | django42: Django>=4.2,<5.0
20 | django50: Django>=5.0,<5.1
21 | django51: Django>=5.1,<5.2
22 | django52: Django>=5.2a1,<5.3
23 | commands =
24 | python \
25 | -W error::DeprecationWarning \
26 | -W error::PendingDeprecationWarning \
27 | -W ignore:::site \
28 | -W ignore:::distutils \
29 | -m coverage run \
30 | {toxinidir}/quicktest.py djgeojson
31 |
32 | [testenv:docs]
33 | basepython = python
34 | changedir = docs
35 | deps =
36 | sphinx
37 | sphinx_rtd_theme
38 | Django>=5.1,<5.2
39 | commands =
40 | sphinx-build -W -b html -d build/doctrees . build/html
41 |
42 | [testenv:flake8]
43 | usedevelop = false
44 | basepython = python3.13
45 | deps = flake8
46 | commands = flake8 --ignore=E501,W504 djgeojson
47 |
48 | [testenv:isort]
49 | usedevelop = false
50 | basepython = python3.13
51 | deps = isort
52 | commands = isort .
53 |
54 | [isort]
55 | skip_gitignore = true
56 | include_trailing_comma = true
57 | multi_line_output = 3
58 | line_length = 80
59 |
--------------------------------------------------------------------------------