├── .github ├── dependabot.yml └── workflows │ ├── automerge.yml │ ├── ci.yml │ ├── lint.yml │ └── pypi.yml ├── .gitignore ├── AUTHORS ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.rst ├── boundaries ├── __init__.py ├── admin.py ├── base_views.py ├── kml.py ├── locale │ └── en │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── analyzeshapefiles.py │ │ ├── compute_intersections.py │ │ └── loadshapefiles.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20141129_1402.py │ ├── 0003_auto_20150528_1338.py │ ├── 0004_auto_20150921_1607.py │ ├── 0005_auto_20150925_0338.py │ ├── 0006_switch_to_django_jsonfield.py │ ├── 0007_auto_20180325_1421.py │ ├── 0008_alter_boundary_extent_alter_boundary_metadata_and_more.py │ ├── 0009_alter_boundaryset_slug.py │ └── __init__.py ├── models.py ├── static │ └── js │ │ └── jsonformatter.js ├── templates │ └── boundaries │ │ ├── apibrowser.html │ │ └── apidoc.html ├── tests │ ├── __init__.py │ ├── definitions │ │ ├── no_data_sources │ │ │ └── definition.py │ │ ├── no_features │ │ │ └── definition.py │ │ ├── polygons │ │ │ ├── definition.py │ │ │ ├── test_poly.dbf │ │ │ ├── test_poly.prj │ │ │ ├── test_poly.shp │ │ │ └── test_poly.shx │ │ └── srid │ │ │ └── definition.py │ ├── fixtures │ │ ├── bad.zip │ │ ├── bar_definition.py │ │ ├── empty.zip │ │ ├── empty │ │ │ ├── empty.txt │ │ │ └── empty.zip │ │ ├── flat.zip │ │ ├── foo.dbf │ │ ├── foo.prj │ │ ├── foo.shp │ │ ├── foo.shx │ │ ├── foo_definition.py │ │ ├── multiple.zip │ │ ├── multiple │ │ │ ├── bar.dbf │ │ │ ├── bar.prj │ │ │ ├── bar.shp │ │ │ ├── bar.shx │ │ │ ├── dir.zip │ │ │ │ ├── foo.dbf │ │ │ │ ├── foo.prj │ │ │ │ ├── foo.shp │ │ │ │ └── foo.shx │ │ │ ├── empty.zip │ │ │ ├── flat.zip │ │ │ ├── foo.dbf │ │ │ ├── foo.prj │ │ │ ├── foo.shp │ │ │ ├── foo.shx │ │ │ └── nested.zip │ │ ├── nested.zip │ │ └── nested │ │ │ └── dir.zip │ │ │ ├── foo.dbf │ │ │ ├── foo.prj │ │ │ ├── foo.shp │ │ │ └── foo.shx │ ├── test.py │ ├── test_boundary.py │ ├── test_boundary_detail.py │ ├── test_boundary_geo_detail.py │ ├── test_boundary_list.py │ ├── test_boundary_list_filter.py │ ├── test_boundary_list_geo.py │ ├── test_boundary_list_geo_filter.py │ ├── test_boundary_list_set.py │ ├── test_boundary_list_set_filter.py │ ├── test_boundary_list_set_geo.py │ ├── test_boundary_list_set_geo_filter.py │ ├── test_boundary_set.py │ ├── test_boundary_set_detail.py │ ├── test_boundary_set_list.py │ ├── test_boundary_set_list_filter.py │ ├── test_compute_intersections.py │ ├── test_definition.py │ ├── test_feature.py │ ├── test_geometry.py │ ├── test_loadshapefiles.py │ └── test_titlecase.py ├── titlecase.py ├── urls.py └── views.py ├── definition.example.py ├── pyproject.toml ├── runtests.py ├── settings.py └── setup.cfg /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | # The pull_request_target workflow trigger is dangerous. Do not add unrelated logic to this workflow. 2 | # https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ 3 | # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target 4 | name: Auto-merge 5 | on: pull_request_target 6 | permissions: 7 | pull-requests: write # to approve the PR 8 | contents: write # to merge the PR 9 | jobs: 10 | dependabot: 11 | if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} 12 | runs-on: ubuntu-latest 13 | steps: 14 | - id: dependabot-metadata 15 | uses: dependabot/fetch-metadata@v2 16 | with: 17 | github-token: ${{ secrets.GITHUB_TOKEN }} 18 | - if: ${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' || steps.dependabot-metadata.outputs.package-ecosystem == 'github_actions' }} 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | run: gh pr review --approve ${{ github.event.pull_request.html_url }} 22 | - if: ${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' || steps.dependabot-metadata.outputs.package-ecosystem == 'github_actions' }} 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | run: gh pr merge --auto --squash ${{ github.event.pull_request.html_url }} 26 | precommit: 27 | if: ${{ github.event.pull_request.user.login == 'pre-commit-ci[bot]' }} 28 | runs-on: ubuntu-latest 29 | steps: 30 | - env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | run: gh pr review --approve ${{ github.event.pull_request.html_url }} 33 | - env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | run: gh pr merge --auto --squash ${{ github.event.pull_request.html_url }} 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | python-version: [3.8, 3.9, '3.10', '3.11', '3.12'] 10 | django-version: ['Django>=3.2,<4', 'Django>=4.2,<5'] 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-python@v5 14 | with: 15 | python-version: ${{ matrix.python-version }} 16 | cache: pip 17 | cache-dependency-path: setup.cfg 18 | # https://docs.djangoproject.com/en/4.2/ref/contrib/gis/install/geolibs/ 19 | - run: | 20 | sudo apt update 21 | sudo apt install binutils libproj-dev gdal-bin 22 | - run: pip install .[test] '${{ matrix.django-version }}' psycopg2-binary 23 | - env: 24 | PORT: ${{ job.services.postgres.ports[5432] }} 25 | DJANGO_SETTINGS_MODULE: settings 26 | run: env PYTHONPATH=$PYTHONPATH:$PWD django-admin migrate --noinput 27 | - env: 28 | PORT: ${{ job.services.postgres.ports[5432] }} 29 | run: coverage run --source=boundaries runtests.py 30 | - env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | run: coveralls --service=github 33 | services: 34 | postgres: 35 | image: postgis/postgis:15-3.4 36 | env: 37 | POSTGRES_PASSWORD: postgres 38 | options: >- 39 | --health-cmd pg_isready 40 | --health-interval 10s 41 | --health-timeout 5s 42 | --health-retries 5 43 | ports: 44 | - 5432/tcp 45 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - uses: actions/setup-python@v5 10 | with: 11 | python-version: '3.10' 12 | - run: pip install --upgrade check-manifest flake8 isort 13 | - run: flake8 . 14 | - run: isort . 15 | - run: check-manifest --ignore-bad-ideas '*.mo' 16 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | on: push 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | permissions: 7 | id-token: write 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-python@v5 11 | with: 12 | python-version: '3.10' 13 | - run: pip install --upgrade build 14 | - run: python -m build --sdist --wheel 15 | - name: Publish to TestPyPI 16 | uses: pypa/gh-action-pypi-publish@release/v1 17 | with: 18 | repository-url: https://test.pypi.org/legacy/ 19 | skip-existing: true 20 | - name: Publish to PyPI 21 | if: startsWith(github.ref, 'refs/tags') 22 | uses: pypa/gh-action-pypi-publish@release/v1 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | *_cleaned_* 4 | /build 5 | /dist 6 | /.coverage 7 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Christopher Groskopf (@onyxfish) 2 | Ryan Nagle (@ryannagle) 3 | Ryan Mark (@ryanmark) 4 | Joe Germuska (@joegermuska) 5 | Brian Boyer (@brianboyer) 6 | Anders Eriksen (@anderseri) 7 | Ben Welsh (@palewire) 8 | Shane Shifflett (@shaneshifflett) 9 | James McKinney (@mckinneyjames) 10 | James Turk (@jamesturk) 11 | Michael Mulley (@michaelmulley) 12 | Joshua Tauberer (@joshdata) 13 | Paul Tagliamonte (@paultag) 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.10.2 (2024-06-26) 4 | 5 | * Replace n-dashes and m-dashes in boundary set slugs with hyphens, in management commands. 6 | 7 | ## 0.10.1 (2024-06-26) 8 | 9 | * Make boundary set slug editable. 10 | 11 | ## 0.10.0 (2024-06-26) 12 | 13 | * Replace n-dashes and m-dashes in boundary set slugs with hyphens. 14 | * Add support for Django 3.2 and 4.2. 15 | * Drop support for Python <= 3.7. 16 | 17 | ## 0.9.4 (2019-01-09) 18 | 19 | * Add support for Django 2.1. 20 | 21 | ## 0.9.3 (2018-08-04) 22 | 23 | * `analyzeshapefiles` outputs boundary sets in alphabetical order by name/slug. 24 | 25 | ## 0.9.2 (2018-03-25) 26 | 27 | * Support shapefiles with date field values. 28 | * Add support for Django 2.0. 29 | * Drop support for Django <= 1.10. 30 | 31 | ## 0.9.1 (2017-02-23) 32 | 33 | * Fix packaging (was omitting data files during install). 34 | 35 | ## 0.9.0 (2017-02-23) 36 | 37 | * Use Django 1.9's `JSONField` to migrate to PostgreSQL 9.4's `jsonb` datatype. 38 | * Drop support for Django <= 1.8. 39 | * Drop support for PostgreSQL <= 9.3. 40 | 41 | ## 0.8.1 (2017-02-23) 42 | 43 | * Add `analyzeshapefiles` command to report the number of features to be loaded, along with names and identifiers. 44 | * Fix `compute_intersections` for Django 1.10. 45 | * Fix packaging for recent Django. 46 | * Remove South migrations. 47 | 48 | ## 0.8.0 (2017-02-13) 49 | 50 | * Add support for Django 1.9 and 1.10. 51 | * Drop support for Django <= 1.7 and Python 2.6 and 3.3. 52 | 53 | ## 0.7.5 (2016-02-25) 54 | 55 | * Allow `$` and `.` in JSONP callback. The callback validation can be [further improved](http://tav.espians.com/sanitising-jsonp-callback-identifiers-for-security.html). 56 | 57 | ## 0.7.4 (2015-10-05) 58 | 59 | * Add `boundaries.migrations` to package. 60 | 61 | ## 0.7.3 (2015-10-05) 62 | 63 | * Fix `SourceFileLoader` being unavailable prior to Python 3.3. 64 | 65 | ## 0.7.2 (2015-10-05) 66 | 67 | * Drop support for undocumented `metadata` key in definition files. 68 | 69 | ## 0.7.1 (2015-09-25) 70 | 71 | * Add `start_date` and `end_date` to boundaries. [#31](https://github.com/opennorth/represent-boundaries/pull/31) (@mileswwatkins) 72 | * Increase length of `external_id` to 255 characters. [#32](https://github.com/opennorth/represent-boundaries/pull/32) (@evz) 73 | * Add `blank=True` to `JSONField` for Django 1.9. [#33](https://github.com/opennorth/represent-boundaries/pull/33) (@jamesturk) 74 | 75 | ## 0.7.0 (2015-04-24) 76 | 77 | * Fix definition file loader to use an importer instead of `eval` [#30](https://github.com/opennorth/represent-boundaries/pull/30) (@paultag) 78 | * If you were using `re` or other modules imported by `boundaries/__init__.py` in your definition files without importing them in your definition files, you must now import them in your definition files. 79 | 80 | ## 0.6.5 (2015-01-30) 81 | 82 | * Relax `jsonfield` version requirements. 83 | * Fix assignment of default `srs` in `Feature` class, which was breaking Heroku static assets. 84 | 85 | ## 0.6.4 (2015-01-06) 86 | 87 | * Fix regression in slugless definition files. 88 | 89 | ## 0.6.3 (2014-12-29) 90 | 91 | * Eliminate Django 1.7 deprecation warning. 92 | 93 | ## 0.6.2 (2014-11-29) 94 | 95 | * Set default value on `jsonfield` fields. 96 | 97 | ## 0.6.1 (2014-11-29) 98 | 99 | * Use `jsonfield` instead of `django-jsonfield`. 100 | 101 | ## 0.6.0 (2014-10-19) 102 | 103 | * Support shapefiles with binary field names. 104 | * Recurse directories and ZIP files in `loadshapefiles`. 105 | * `loadshapefiles` will not create a boundary set if no shapefiles are found. 106 | 107 | Identified quirks: 108 | 109 | * If a shapefile has `_cleaned_` in its name, it will not be loaded, unless created by Represent Boundaries. 110 | 111 | ## 0.5.1 (2014-09-12) 112 | 113 | * Fix regression with `loadshapefiles` skip logic. 114 | 115 | ## 0.5.0 (2014-08-28) 116 | 117 | * Remove the `--database` (`-u`) option from the `loadshapefiles` management command, which would only specify the database in which to find the `spatial_ref_sys` table. 118 | * Make non-integer `offset` error message consistent with non-integer `limit` error message. 119 | * `format=wkt` and `format=kml` no longer error in Django 1.7. 120 | * I18n support. 121 | * Add tests. 122 | 123 | Identified quirks: 124 | 125 | * The `shape`, `simple_shape` and `centroid` endpoints ignore the `pretty` parameter. 126 | 127 | ## 0.4.0 (2014-08-01) 128 | 129 | * Add `start_date` and `end_date` to boundary sets. [#21](https://github.com/opennorth/represent-boundaries/pull/21) (@jamesturk) 130 | * Remove API throttle, as this is the responsibility of a proxy. [#22](https://github.com/opennorth/represent-boundaries/pull/22) (@jamesturk) 131 | 132 | ## 0.3.2 (2014-07-01) 133 | 134 | * Add templates and static files to package. 135 | 136 | ## 0.3.1 (2014-07-01) 137 | 138 | * Python 3 compatibility: Fix writing ZIP file contents. 139 | 140 | ## 0.3 (2014-06-27) 141 | 142 | * Django 1.7 compatibility. 143 | * If the `contains` parameter is an invalid latitude and longitude pair, return the invalid pair in the error message. 144 | 145 | ## 0.2 (2014-03-26) 146 | 147 | * Python 3 compatibility. [#14](https://github.com/opennorth/represent-boundaries/pull/14) (@jamesturk) 148 | * Fix various small bugs and encoding issues and add help text. 149 | * API 150 | * Add CORS support. 151 | * New API throttle. 152 | * If a request is made with invalid filters, return a 400 error instead of a 500 error. 153 | * JSON 154 | * Add `extent` to the detail of boundary sets. 155 | * Add `external_id` to the list of boundaries. 156 | * Add `extent`, `centroid` and `extra` to the detail of boundaries. 157 | * Loading shapefiles 158 | * Calculate the geographic extent of boundary sets and boundaries. 159 | * Re-load a boundary set if the `last_updated` field in its definition is more recent than in the database, without having to set the `--reload` switch. 160 | * If two boundaries have the same slug, and the `--merge` option is set to `union` or `combine`, union their geometries or combine their geometries into a MultiPolygon. 161 | * Follow symbolic links when walking the shapefiles directory tree. 162 | * If `DEBUG = True`, prompt the user about the risk of high memory consumption. [#15](https://github.com/opennorth/represent-boundaries/pull/15) (@jamesturk) 163 | * Log an error if a shapefile contains no layers. 164 | * Add an example definition file. 165 | * Definition files 166 | * New `name` field so that a boundary set's slug and name can differ. 167 | * New `is_valid_func` field so that features can be excluded when loading a shapefile. 168 | * New `extra` field to add free-form metadata. 169 | * ZIP files 170 | * If the `--clean` switch is set, convert 3D features to 2D when loading shapefiles from ZIP files. 171 | * Clean up temporary files created by uncompressing ZIP files. 172 | * Support ZIP files containing directories. 173 | * Management commands 174 | * Add a `compute_intersections` management command to report overlapping boundaries from a pair of boundary sets. 175 | * Remove the `startshapedefinitions` management command. 176 | 177 | ## 0.1 (2013-09-14) 178 | 179 | This first release is a [significant refactoring](https://github.com/opennorth/represent-boundaries/commit/db2cdaa381ecde423dd68962d79811925092d4da) of [django-boundaryservice](https://github.com/newsapps/django-boundaryservice) from [this commit](https://github.com/newsapps/django-boundaryservice/commit/67e79d47d49eab444681309328dbe6554b953d69). Minor changes may not be logged. 180 | 181 | * Don't `SELECT` geometries when retrieving boundary sets from the database. 182 | * Fix various small bugs and encoding issues and improve error messages. 183 | * API 184 | * Use plural endpoints `boundary-sets` and `boundaries` instead of `boundary-set` and `boundary`. 185 | * Move boundary detail endpoint from `boundaries//` to `boundaries///`. 186 | * Remove some fields from list endpoints, remove geospatial fields from detail endpoints, and add geospatial endpoints. 187 | * Add a `touches` boundary filter. 188 | * Change the semantics of the `intersects` boundary filter from "intersects" to "covers or overlaps". 189 | * If the parameter `format=apibrowser` is present, display a HTML version of the JSON response. 190 | * Support `format=kml` and `format=wkt`. 191 | * JSON 192 | * Rename `name` to `name_plural`, `singular` to `name_singular`, and `boundaries` to `boundaries_url` on boundary sets. 193 | * Move `boundaries_url` under `related` on boundary sets. 194 | * Change `boundaries_url` from a list of boundary detail URLs to a boundary list URL. 195 | * Add `licence_url` to the detail of boundary sets. 196 | * Remove `slug`, `resource_uri`, `count` and `metadata_fields` from the detail of boundary sets. 197 | * Rename `kind` to `boundary_set_name` and `set` to `boundary_set_url` on boundaries. 198 | * Move `boundary_set_url` under `related` on boundaries. 199 | * Add `shape_url`, `simple_shape_url`, `centroid_url` and `boundaries_url` under `related` to the detail of boundaries. 200 | * Remove `slug`, `resource_uri` and `centroid` from the detail of boundaries. 201 | * Loading shapefiles 202 | * Allow multiple `definition.py` files anywhere in the shapefiles directory tree, instead of a single `definitions.py` file. 203 | * Use EPSG:4326 (WGS 84, Google Maps) instead of EPSG:4269 (NAD 83, US Census) by default. 204 | * Add a `--reload` switch to re-load shapefiles that have already been loaded. 205 | * Remove the `--clear` switch. 206 | * Make the simplification tolerance configurable. 207 | * Definition files 208 | * Rename `ider` to `id_func`, `namer` to `name_func`, and `href` to `source_url`. 209 | * New `slug_func` to set a custom slug. 210 | * New `licence_url` field to link to a data license. 211 | * If `singular`, `id_func` or `slug_func` are not set, use sensible defaults. 212 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Chicago Tribune, Christopher Groskopf, Ryan Nagle, Michael 2 | Mulley, Open North Inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.md 2 | recursive-include boundaries/locale * 3 | recursive-include boundaries/static * 4 | recursive-include boundaries/templates * 5 | recursive-include boundaries/tests *.dbf 6 | recursive-include boundaries/tests *.prj 7 | recursive-include boundaries/tests *.py 8 | recursive-include boundaries/tests *.shp 9 | recursive-include boundaries/tests *.shx 10 | recursive-include boundaries/tests *.txt 11 | recursive-include boundaries/tests *.zip 12 | exclude *.py 13 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Represent Boundaries 2 | ==================== 3 | 4 | |PyPI version| |Build Status| |Coverage Status| 5 | 6 | Represent Boundaries is a web API to geographic areas, like electoral 7 | districts. It allows you to easily find the areas that cover your users' 8 | locations to display location-based information, like profiles of 9 | electoral candidates. 10 | 11 | It's a Django app that's easy to integrate into an existing project or 12 | to deploy on its own. It uses a simple file format to control how data 13 | is loaded into the API, and it provides a command-line tool to easily 14 | manage data. 15 | 16 | Notable uses include: 17 | 18 | - `Represent `__ helps people find the 19 | elected officials and electoral districts for any Canadian address or 20 | postal code, at any level of government. 21 | - `OpenStates.org `__ 22 | allows anyone to discover more about lawmaking in their state and 23 | uses Represent Boundaries to help them find their state legislators. 24 | - `GovTrack.us `__ helps 25 | track the activities of the United States Congress and uses Represent 26 | Boundaries to help people find their members of Congress. 27 | - `ANCFinder.org `__ helps Washington, DC 28 | residents discover and participate in their Advisory Neighborhood 29 | Commissions. 30 | 31 | Public instances include: 32 | 33 | - `represent.opennorth.ca `__ for 34 | Canada: `source 35 | code `__ and `data 36 | files `__ 37 | - `gis.govtrack.us `__ for 38 | the US: `source code `__ 39 | 40 | Documentation 41 | ------------- 42 | 43 | - `Installation `__ 44 | - `Add data to the API `__ 45 | - `Use the API `__ 46 | - `Update data in the API `__ 47 | - `Read the API 48 | reference `__ 49 | 50 | Testing 51 | ------- 52 | 53 | :: 54 | 55 | createdb represent_boundaries_test 56 | psql represent_boundaries_test -c 'CREATE EXTENSION postgis;' 57 | env DJANGO_SETTINGS_MODULE=settings django-admin migrate --noinput 58 | python runtests.py 59 | 60 | Release process 61 | --------------- 62 | 63 | - Run `env PYTHONPATH=. DJANGO_SETTINGS_MODULE=settings django-admin makemigrations` 64 | - Run `env PYTHONPATH=. DJANGO_SETTINGS_MODULE=settings django-admin makemessages -l en && django-admin compilemessages` 65 | - Update the version number in `setup.py` and `loadshapefiles.py` 66 | - Update the release date in `CHANGELOG.md` 67 | - Tag the release: `git tag -a x.x.x -m 'x.x.x release.'` 68 | - Push the tag: `git push --follow-tags` 69 | 70 | Acknowledgements 71 | ---------------- 72 | 73 | Represent Boundaries is based on the Chicago Tribune's 74 | `django-boundaryservice `__. 75 | 76 | Released under the MIT license 77 | 78 | .. |PyPI version| image:: https://badge.fury.io/py/represent-boundaries.svg 79 | :target: https://badge.fury.io/py/represent-boundaries 80 | .. |Build Status| image:: https://github.com/opennorth/represent-boundaries/actions/workflows/ci.yml/badge.svg 81 | :target: https://github.com/opennorth/represent-boundaries/actions/workflows/ci.yml 82 | .. |Coverage Status| image:: https://coveralls.io/repos/opennorth/represent-boundaries/badge.png?branch=master 83 | :target: https://coveralls.io/r/opennorth/represent-boundaries 84 | -------------------------------------------------------------------------------- /boundaries/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import re 4 | import sys 5 | 6 | from django.utils.translation import gettext as _ 7 | 8 | log = logging.getLogger(__name__) 9 | registry = {} 10 | _basepath = '.' 11 | 12 | 13 | def register(slug, **kwargs): 14 | """ 15 | Adds a definition file to the list during the loadshapefiles management 16 | command. Called by definition files. 17 | """ 18 | kwargs['file'] = os.path.join(_basepath, kwargs.get('file', '')) 19 | if slug in registry: 20 | log.warning(_('Multiple definitions of %(slug)s found.') % {'slug': slug}) 21 | registry[slug] = kwargs 22 | 23 | 24 | definition_file_re = re.compile(r'definitions?\.py\Z') 25 | 26 | 27 | def autodiscover(base_dir): 28 | """ 29 | Walks the directory tree, loading definition files. Definition files are any 30 | files ending in "definition.py" or "definitions.py". 31 | """ 32 | global _basepath 33 | for (dirpath, dirnames, filenames) in os.walk(base_dir, followlinks=True): 34 | _basepath = dirpath 35 | for filename in filenames: 36 | if definition_file_re.search(filename): 37 | import_file(os.path.join(dirpath, filename)) 38 | 39 | 40 | def attr(name): 41 | return lambda f: f.get(name) 42 | 43 | 44 | def _clean_string(s): 45 | if re.search(r'[A-Z]', s) and not re.search(r'[a-z]', s): 46 | # WE'RE IN UPPERCASE 47 | from boundaries.titlecase import titlecase 48 | s = titlecase(s) 49 | s = re.sub(r'(?u)\s', ' ', s) 50 | s = re.sub(r'( ?-- ?| - )', '—', s) 51 | return s 52 | 53 | 54 | def clean_attr(name): 55 | attr_getter = attr(name) 56 | return lambda f: _clean_string(attr_getter(f)) 57 | 58 | 59 | def dashed_attr(name): 60 | # Replaces all hyphens with em dashes 61 | attr_getter = clean_attr(name) 62 | return lambda f: attr_getter(f).replace('-', '—') 63 | 64 | 65 | def import_file(path): 66 | module = ':definition-py:' 67 | # This module name has two benefits: 68 | # 1. Using a top-level module name avoid issues when importing a definition 69 | # file at a path like `path/to/definition.py`, which would otherwise 70 | # issue warnings about its parent modules not being found in Python 2.7. 71 | # 2. We remove the module name from `sys.modules` in order to reuse the 72 | # module name for another definition file. Using an invalid module name 73 | # makes it unlikely that this would interfere with third-party code. 74 | # 75 | # The module object is returned, but this return value is unused by this 76 | # package. 77 | 78 | """ 79 | If we're in Python 3, we'll use the PEP 302 import loader. 80 | """ 81 | import importlib.machinery 82 | loader = importlib.machinery.SourceFileLoader(module, path) 83 | obj = loader.load_module() 84 | sys.modules.pop(module) 85 | return obj 86 | 87 | """ 88 | If we're in Python 2, we'll use the `imp` module. 89 | """ 90 | import imp 91 | obj = imp.load_source(module, path) 92 | sys.modules.pop(module) 93 | return obj 94 | -------------------------------------------------------------------------------- /boundaries/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib.gis import admin 2 | 3 | from boundaries.models import Boundary, BoundarySet 4 | 5 | 6 | @admin.register(BoundarySet) 7 | class BoundarySetAdmin(admin.ModelAdmin): 8 | list_filter = ('authority', 'domain') 9 | 10 | 11 | @admin.register(Boundary) 12 | class BoundaryAdmin(admin.OSMGeoAdmin): 13 | list_display = ('name', 'external_id', 'set') 14 | list_display_links = ('name', 'external_id') 15 | list_filter = ('set',) 16 | -------------------------------------------------------------------------------- /boundaries/kml.py: -------------------------------------------------------------------------------- 1 | from xml.sax.saxutils import escape 2 | 3 | 4 | def generate_placemark(name, geom): 5 | return f"{escape(name)}{geom.kml}" 6 | 7 | 8 | def generate_kml_document(placemarks): 9 | return """ 10 | 11 | 12 | %s 13 | 14 | """ % "\n".join(placemarks) 15 | -------------------------------------------------------------------------------- /boundaries/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /boundaries/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2024-06-26 15:15-0500\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: boundaries/__init__.py:20 21 | #, python-format 22 | msgid "Multiple definitions of %(slug)s found." 23 | msgstr "" 24 | 25 | #: boundaries/base_views.py:140 boundaries/base_views.py:209 26 | msgid "Invalid filter value" 27 | msgstr "" 28 | 29 | #: boundaries/base_views.py:183 30 | #, python-format 31 | msgid "Invalid latitude,longitude '%(value)s' provided." 32 | msgstr "" 33 | 34 | #: boundaries/base_views.py:214 35 | #, python-format 36 | msgid "" 37 | "Spatial-list queries cannot return more than %(expected)d resources; this " 38 | "query would return %(actual)s. Please filter your query." 39 | msgstr "" 40 | 41 | #: boundaries/base_views.py:368 42 | #, python-format 43 | msgid "Invalid limit '%(value)s' provided. Please provide a positive integer." 44 | msgstr "" 45 | 46 | #: boundaries/base_views.py:373 47 | #, python-format 48 | msgid "" 49 | "Invalid limit '%(value)s' provided. Please provide a positive integer >= 0." 50 | msgstr "" 51 | 52 | #: boundaries/base_views.py:401 53 | #, python-format 54 | msgid "Invalid offset '%(value)s' provided. Please provide a positive integer." 55 | msgstr "" 56 | 57 | #: boundaries/base_views.py:406 58 | #, python-format 59 | msgid "" 60 | "Invalid offset '%(value)s' provided. Please provide a positive integer >= 0." 61 | msgstr "" 62 | 63 | #: boundaries/management/commands/analyzeshapefiles.py:16 64 | msgid "" 65 | "Reports the number of features to be loaded, along with names and " 66 | "identifiers." 67 | msgstr "" 68 | 69 | #: boundaries/management/commands/analyzeshapefiles.py:25 70 | #: boundaries/management/commands/loadshapefiles.py:40 71 | msgid "Load shapefiles from this directory." 72 | msgstr "" 73 | 74 | #: boundaries/management/commands/analyzeshapefiles.py:45 75 | #: boundaries/management/commands/loadshapefiles.py:118 76 | msgid "No shapefiles found." 77 | msgstr "" 78 | 79 | #: boundaries/management/commands/compute_intersections.py:12 80 | msgid "" 81 | "Create a report of the area of intersection of every pair of boundaries from " 82 | "two boundary sets specified by their slug." 83 | msgstr "" 84 | 85 | #: boundaries/management/commands/compute_intersections.py:24 86 | msgid "Choose an output format: csv, json." 87 | msgstr "" 88 | 89 | #: boundaries/management/commands/compute_intersections.py:32 90 | msgid "Includes the original shapefile metadata in the output." 91 | msgstr "" 92 | 93 | #: boundaries/management/commands/loadshapefiles.py:23 94 | msgid "Import boundaries described by shapefiles." 95 | msgstr "" 96 | 97 | #: boundaries/management/commands/loadshapefiles.py:32 98 | msgid "Reload boundary sets that have already been imported." 99 | msgstr "" 100 | 101 | #: boundaries/management/commands/loadshapefiles.py:48 102 | msgid "Don't load these boundary set slugs (comma-delimited)." 103 | msgstr "" 104 | 105 | #: boundaries/management/commands/loadshapefiles.py:56 106 | msgid "Only load these boundary set slugs (comma-delimited)." 107 | msgstr "" 108 | 109 | #: boundaries/management/commands/loadshapefiles.py:64 110 | msgid "Clean shapefiles first with ogr2ogr." 111 | msgstr "" 112 | 113 | #: boundaries/management/commands/loadshapefiles.py:73 114 | msgid "" 115 | "Merge strategy when there are duplicate slugs, either \"combine\" (extend " 116 | "the MultiPolygon) or \"union\" (union the geometries)." 117 | msgstr "" 118 | 119 | #: boundaries/management/commands/loadshapefiles.py:83 120 | msgid "DEBUG is True. This can cause memory usage to balloon. Continue? [y/n]" 121 | msgstr "" 122 | 123 | #: boundaries/management/commands/loadshapefiles.py:103 124 | #, python-format 125 | msgid "Processing %(slug)s." 126 | msgstr "" 127 | 128 | #: boundaries/management/commands/loadshapefiles.py:125 129 | #, python-format 130 | msgid "Skipping %(slug)s." 131 | msgstr "" 132 | 133 | #: boundaries/management/commands/loadshapefiles.py:165 134 | #, python-format 135 | msgid "Loading %(slug)s from %(source)s" 136 | msgstr "" 137 | 138 | #: boundaries/management/commands/loadshapefiles.py:180 139 | #, python-format 140 | msgid "%(slug)s..." 141 | msgstr "" 142 | 143 | #: boundaries/management/commands/loadshapefiles.py:189 144 | #, python-format 145 | msgid "%(slug)s count: %(count)i" 146 | msgstr "" 147 | 148 | #: boundaries/management/commands/loadshapefiles.py:202 149 | #, python-format 150 | msgid "The merge strategy '%(value)s' must be 'combine' or 'union'." 151 | msgstr "" 152 | 153 | #: boundaries/management/commands/loadshapefiles.py:256 154 | #, python-format 155 | msgid "The path must be a shapefile, a ZIP file, or a directory: %(value)s." 156 | msgstr "" 157 | 158 | #: boundaries/models.py:48 159 | msgid "The boundary set's unique identifier, used as a path component in URLs." 160 | msgstr "" 161 | 162 | #: boundaries/models.py:53 163 | msgid "The plural name of the boundary set." 164 | msgstr "" 165 | 166 | #: boundaries/models.py:57 167 | msgid "A generic singular name for a boundary in the set." 168 | msgstr "" 169 | 170 | #: boundaries/models.py:61 171 | msgid "The entity responsible for publishing the data." 172 | msgstr "" 173 | 174 | #: boundaries/models.py:65 175 | msgid "The geographic area covered by the boundary set." 176 | msgstr "" 177 | 178 | #: boundaries/models.py:68 179 | msgid "The most recent date on which the data was updated." 180 | msgstr "" 181 | 182 | #: boundaries/models.py:72 183 | msgid "A URL to the source of the data." 184 | msgstr "" 185 | 186 | #: boundaries/models.py:77 187 | msgid "" 188 | "Free-form text notes, often used to describe changes that were made to the " 189 | "original source data." 190 | msgstr "" 191 | 192 | #: boundaries/models.py:82 193 | msgid "A URL to the licence under which the data is made available." 194 | msgstr "" 195 | 196 | #: boundaries/models.py:87 197 | msgid "" 198 | "The set's boundaries' bounding box as a list like [xmin, ymin, xmax, ymax] " 199 | "in EPSG:4326." 200 | msgstr "" 201 | 202 | #: boundaries/models.py:92 203 | msgid "The date from which the set's boundaries are in effect." 204 | msgstr "" 205 | 206 | #: boundaries/models.py:97 207 | msgid "The date until which the set's boundaries are in effect." 208 | msgstr "" 209 | 210 | #: boundaries/models.py:102 211 | msgid "Any additional metadata." 212 | msgstr "" 213 | 214 | #: boundaries/models.py:126 215 | msgid "boundary set" 216 | msgstr "" 217 | 218 | #: boundaries/models.py:127 219 | msgid "boundary sets" 220 | msgstr "" 221 | 222 | #: boundaries/models.py:182 223 | msgid "The set to which the boundary belongs." 224 | msgstr "" 225 | 226 | #: boundaries/models.py:186 227 | msgid "A generic singular name for the boundary." 228 | msgstr "" 229 | 230 | #: boundaries/models.py:191 231 | msgid "" 232 | "The boundary's unique identifier within the set, used as a path component in " 233 | "URLs." 234 | msgstr "" 235 | 236 | #: boundaries/models.py:195 237 | msgid "An identifier of the boundary, which should be unique within the set." 238 | msgstr "" 239 | 240 | #: boundaries/models.py:200 241 | msgid "The name of the boundary." 242 | msgstr "" 243 | 244 | #: boundaries/models.py:206 245 | msgid "The attributes of the boundary from the shapefile, as a dictionary." 246 | msgstr "" 247 | 248 | #: boundaries/models.py:209 249 | msgid "The geometry of the boundary in EPSG:4326." 250 | msgstr "" 251 | 252 | #: boundaries/models.py:212 253 | msgid "The simplified geometry of the boundary in EPSG:4326." 254 | msgstr "" 255 | 256 | #: boundaries/models.py:216 257 | msgid "The centroid of the boundary in EPSG:4326." 258 | msgstr "" 259 | 260 | #: boundaries/models.py:221 261 | msgid "" 262 | "The bounding box of the boundary as a list like [xmin, ymin, xmax, ymax] in " 263 | "EPSG:4326." 264 | msgstr "" 265 | 266 | #: boundaries/models.py:227 267 | msgid "" 268 | "The point at which to place a label for the boundary in EPSG:4326, used by " 269 | "represent-maps." 270 | msgstr "" 271 | 272 | #: boundaries/models.py:232 273 | msgid "The date from which the boundary is in effect." 274 | msgstr "" 275 | 276 | #: boundaries/models.py:237 277 | msgid "The date until which the boundary is in effect." 278 | msgstr "" 279 | 280 | #: boundaries/models.py:247 281 | msgid "boundary" 282 | msgstr "" 283 | 284 | #: boundaries/models.py:248 285 | msgid "boundaries" 286 | msgstr "" 287 | 288 | #: boundaries/models.py:397 289 | #, python-format 290 | msgid "The geometry is a %(value)s but must be a Polygon or a MultiPolygon." 291 | msgstr "" 292 | 293 | #: boundaries/templates/boundaries/apibrowser.html:5 294 | msgid "API Browser" 295 | msgstr "" 296 | 297 | #: boundaries/views.py:51 298 | msgid "Invalid value for intersects filter" 299 | msgstr "" 300 | 301 | #: boundaries/views.py:61 302 | msgid "Invalid value for touches filter" 303 | msgstr "" 304 | -------------------------------------------------------------------------------- /boundaries/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/management/__init__.py -------------------------------------------------------------------------------- /boundaries/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/management/commands/__init__.py -------------------------------------------------------------------------------- /boundaries/management/commands/analyzeshapefiles.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from collections import OrderedDict 3 | from shutil import rmtree 4 | 5 | from django.core.management.base import BaseCommand 6 | from django.utils.translation import gettext as _ 7 | 8 | import boundaries 9 | from boundaries.management.commands.loadshapefiles import create_data_sources 10 | from boundaries.models import Definition, Feature, app_settings, slugify 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | class Command(BaseCommand): 16 | help = _('Reports the number of features to be loaded, along with names and identifiers.') 17 | 18 | def add_arguments(self, parser): 19 | parser.add_argument( 20 | '-d', 21 | '--data-dir', 22 | action='store', 23 | dest='data_dir', 24 | default=app_settings.SHAPEFILES_DIR, 25 | help=_('Load shapefiles from this directory.'), 26 | ) 27 | 28 | def handle(self, *args, **options): 29 | boundaries.autodiscover(options['data_dir']) 30 | 31 | for slug in sorted(boundaries.registry): 32 | name = slug 33 | slug = slugify(slug) 34 | definition = boundaries.registry[name] 35 | 36 | # Backwards-compatibility with having the name, instead of the slug, 37 | # as the first argument to `boundaries.register`. 38 | definition.setdefault('name', name) 39 | definition = Definition(definition) 40 | 41 | data_sources, tmpdirs = create_data_sources(definition['file'], encoding=definition['encoding']) 42 | 43 | try: 44 | if not data_sources: 45 | log.warning(_('No shapefiles found.')) 46 | else: 47 | features = OrderedDict() 48 | 49 | for data_source in data_sources: 50 | features[slug] = [] 51 | 52 | layer = data_source[0] 53 | for feature in layer: 54 | feature = Feature(feature, definition) 55 | if feature.is_valid(): 56 | features[slug].append((feature.id, feature.name)) 57 | 58 | for slug, features in features.items(): 59 | print('\n%s: %d' % (slug, len(features))) 60 | for properties in sorted(features): 61 | print('%s: %s' % properties) 62 | finally: 63 | for tmpdir in tmpdirs: 64 | rmtree(tmpdir) 65 | -------------------------------------------------------------------------------- /boundaries/management/commands/compute_intersections.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | from django.core.management.base import BaseCommand 5 | from django.utils.translation import gettext as _ 6 | 7 | from boundaries.models import BoundarySet 8 | 9 | 10 | class Command(BaseCommand): 11 | help = _( 12 | 'Create a report of the area of intersection of every pair of boundaries from two boundary sets ' 13 | 'specified by their slug.' 14 | ) 15 | 16 | def add_arguments(self, parser): 17 | parser.add_argument('slug', nargs=2) 18 | parser.add_argument( 19 | '-f', 20 | '--format', 21 | action='store', 22 | dest='format', 23 | default='csv', 24 | help=_('Choose an output format: csv, json.'), 25 | ) 26 | parser.add_argument( 27 | '-m', 28 | '--metadata', 29 | action='store_true', 30 | dest='include_metadata', 31 | default=False, 32 | help=_('Includes the original shapefile metadata in the output.'), 33 | ) 34 | 35 | def handle(self, *args, **options): 36 | bset_a = BoundarySet.objects.get(slug=options['slug'][0]) 37 | bset_b = BoundarySet.objects.get(slug=options['slug'][1]) 38 | 39 | if options["format"] == "csv": 40 | print(bset_a.slug, "area_1", bset_b.slug, "area_2", "area_intersection", "pct_of_1", "pct_of_2") 41 | elif options["format"] == "json": 42 | output = [] 43 | 44 | # For each boundary in the first set... 45 | for a_slug in bset_a.boundaries.order_by("slug").values_list('slug', flat=True): 46 | a_bdry = bset_a.boundaries.get(slug=a_slug) 47 | a_area = a_bdry.shape.area 48 | 49 | # Find each intersecting boundary in the second set... 50 | for b_bdry in bset_b.boundaries\ 51 | .filter(shape__intersects=a_bdry.shape): 52 | 53 | try: 54 | geometry = a_bdry.shape.intersection(b_bdry.shape) 55 | except Exception as e: 56 | sys.stderr.write(f"{a_slug}/{b_bdry.slug}: {e}\n") 57 | continue 58 | 59 | int_area = geometry.area 60 | if geometry.empty: 61 | continue 62 | 63 | b_area = b_bdry.shape.area 64 | 65 | # Skip overlaps that are less than .1% of the area of either of the shapes. 66 | # These are probably not true overlaps. 67 | if int_area / a_area < .001 or int_area / b_area < .001: 68 | continue 69 | 70 | if options["format"] == "csv": 71 | print(a_slug, a_area, b_bdry.slug, b_area, int_area, int_area / a_area, int_area / b_area) 72 | elif options["format"] == "json": 73 | output.append({ 74 | "area": int_area, 75 | bset_a.slug: { 76 | "id": a_bdry.external_id, 77 | "name": a_bdry.name, 78 | "slug": a_slug, 79 | "centroid": tuple(a_bdry.centroid), 80 | "extent": a_bdry.extent, 81 | "area": a_area, 82 | "ratio": int_area / a_area, 83 | }, 84 | bset_b.slug: { 85 | "id": b_bdry.external_id, 86 | "name": b_bdry.name, 87 | "slug": b_bdry.slug, 88 | "centroid": tuple(b_bdry.centroid), 89 | "extent": b_bdry.extent, 90 | "area": b_area, 91 | "ratio": int_area / b_area, 92 | }, 93 | }) 94 | if options["include_metadata"]: 95 | output[-1][bset_a.slug]["metadata"] = a_bdry.metadata 96 | output[-1][bset_b.slug]["metadata"] = b_bdry.metadata 97 | 98 | if options["format"] == "json": 99 | print(json.dumps(output, sort_keys=True, indent=2)) 100 | -------------------------------------------------------------------------------- /boundaries/management/commands/loadshapefiles.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import os.path 4 | import subprocess 5 | from contextlib import closing 6 | from shutil import rmtree 7 | from tempfile import mkdtemp 8 | from zipfile import ZipFile 9 | 10 | from django.conf import settings 11 | from django.contrib.gis.gdal import DataSource, SpatialReference 12 | from django.core.management.base import BaseCommand 13 | from django.db import transaction 14 | from django.utils.translation import gettext as _ 15 | 16 | import boundaries 17 | from boundaries.models import Boundary, BoundarySet, Definition, Feature, app_settings, slugify 18 | 19 | log = logging.getLogger(__name__) 20 | 21 | 22 | class Command(BaseCommand): 23 | help = _('Import boundaries described by shapefiles.') 24 | 25 | def add_arguments(self, parser): 26 | parser.add_argument( 27 | '-r', 28 | '--reload', 29 | action='store_true', 30 | dest='reload', 31 | default=False, 32 | help=_('Reload boundary sets that have already been imported.'), 33 | ) 34 | parser.add_argument( 35 | '-d', 36 | '--data-dir', 37 | action='store', 38 | dest='data_dir', 39 | default=app_settings.SHAPEFILES_DIR, 40 | help=_('Load shapefiles from this directory.'), 41 | ) 42 | parser.add_argument( 43 | '-e', 44 | '--except', 45 | action='store', 46 | dest='except', 47 | default='', 48 | help=_("Don't load these boundary set slugs (comma-delimited)."), 49 | ) 50 | parser.add_argument( 51 | '-o', 52 | '--only', 53 | action='store', 54 | dest='only', 55 | default='', 56 | help=_('Only load these boundary set slugs (comma-delimited).'), 57 | ) 58 | parser.add_argument( 59 | '-c', 60 | '--clean', 61 | action='store_true', 62 | dest='clean', 63 | default=False, 64 | help=_('Clean shapefiles first with ogr2ogr.'), 65 | ) 66 | parser.add_argument( 67 | '-m', 68 | '--merge', 69 | action='store', 70 | dest='merge', 71 | default=None, 72 | help=_( 73 | 'Merge strategy when there are duplicate slugs, ' 74 | 'either "combine" (extend the MultiPolygon) or "union" (union the geometries).' 75 | ), 76 | ) 77 | 78 | def get_version(self): 79 | return '0.10.2' 80 | 81 | def handle(self, *args, **options): 82 | if settings.DEBUG: 83 | print(_('DEBUG is True. This can cause memory usage to balloon. Continue? [y/n]')) 84 | if input().lower() != 'y': 85 | return 86 | 87 | boundaries.autodiscover(options['data_dir']) 88 | 89 | if options['only']: 90 | whitelist = set(options['only'].split(',')) 91 | else: 92 | whitelist = set() 93 | if options['except']: 94 | blacklist = set(options['except'].split(',')) 95 | else: 96 | blacklist = set() 97 | 98 | for slug, definition in boundaries.registry.items(): 99 | name = slug 100 | slug = slugify(slug) 101 | 102 | if self.loadable(slug, definition['last_updated'], whitelist, blacklist, options['reload']): 103 | log.info(_('Processing %(slug)s.') % {'slug': slug}) 104 | 105 | # Backwards-compatibility with having the name, instead of the slug, 106 | # as the first argument to `boundaries.register`. 107 | definition.setdefault('name', name) 108 | definition = Definition(definition) 109 | 110 | data_sources, tmpdirs = create_data_sources( 111 | definition['file'], 112 | encoding=definition['encoding'], 113 | convert_3d_to_2d=options['clean'], 114 | ) 115 | 116 | try: 117 | if not data_sources: 118 | log.warning(_('No shapefiles found.')) 119 | else: 120 | self.load_boundary_set(slug, definition, data_sources, options) 121 | finally: 122 | for tmpdir in tmpdirs: 123 | rmtree(tmpdir) 124 | else: 125 | log.debug(_('Skipping %(slug)s.') % {'slug': slug}) 126 | 127 | def loadable(self, slug, last_updated, whitelist=[], blacklist=[], reload_existing=False): 128 | """ 129 | Allows through boundary sets that are in the whitelist (if set) and are 130 | not in the blacklist. Unless the `reload_existing` argument is True, it 131 | further limits to those that don't exist or are out-of-date. 132 | """ 133 | if whitelist and slug not in whitelist or slug in blacklist: 134 | return False 135 | elif reload_existing: 136 | return True 137 | else: 138 | try: 139 | return BoundarySet.objects.get(slug=slug).last_updated < last_updated 140 | except BoundarySet.DoesNotExist: 141 | return True 142 | 143 | @transaction.atomic 144 | def load_boundary_set(self, slug, definition, data_sources, options): 145 | BoundarySet.objects.filter(slug=slug).delete() # also deletes boundaries 146 | 147 | boundary_set = BoundarySet.objects.create( 148 | slug=slug, 149 | last_updated=definition['last_updated'], 150 | name=definition['name'], 151 | singular=definition['singular'], 152 | domain=definition['domain'], 153 | authority=definition['authority'], 154 | source_url=definition['source_url'], 155 | licence_url=definition['licence_url'], 156 | start_date=definition['start_date'], 157 | end_date=definition['end_date'], 158 | notes=definition['notes'], 159 | extra=definition['extra'], 160 | ) 161 | 162 | boundary_set.extent = [None, None, None, None] # [xmin, ymin, xmax, ymax] 163 | 164 | for data_source in data_sources: 165 | log.info(_('Loading %(slug)s from %(source)s') % {'slug': slug, 'source': data_source.name}) 166 | 167 | layer = data_source[0] 168 | layer.source = data_source # to trace the layer back to its source 169 | 170 | if definition.get('srid'): 171 | srs = SpatialReference(definition['srid']) 172 | else: 173 | srs = layer.srs 174 | 175 | for feature in layer: 176 | feature = Feature(feature, definition, srs, boundary_set) 177 | feature.layer = layer # to trace the feature back to its source 178 | 179 | if feature.is_valid(): 180 | log.info(_('%(slug)s...') % {'slug': feature.slug}) 181 | 182 | boundary = self.load_boundary(feature, options['merge']) 183 | boundary_set.extend(boundary.extent) 184 | 185 | if None not in boundary_set.extent: # unless there are no features 186 | boundary_set.save() 187 | 188 | log.info( 189 | _('%(slug)s count: %(count)i') % {'slug': slug, 'count': Boundary.objects.filter(set=boundary_set).count()} 190 | ) 191 | 192 | def load_boundary(self, feature, merge_strategy=None): 193 | if merge_strategy: 194 | try: 195 | boundary = Boundary.objects.get(set=feature.boundary_set, slug=feature.slug) 196 | if merge_strategy == 'combine': 197 | boundary.merge(feature.geometry) 198 | elif merge_strategy == 'union': 199 | boundary.unary_union(feature.geometry) 200 | else: 201 | raise ValueError( 202 | _("The merge strategy '%(value)s' must be 'combine' or 'union'.") % {'value': merge_strategy} 203 | ) 204 | boundary.centroid = boundary.shape.centroid 205 | boundary.extent = boundary.shape.extent 206 | boundary.save() 207 | return boundary 208 | except Boundary.DoesNotExist: 209 | return feature.create_boundary() 210 | else: 211 | return feature.create_boundary() 212 | 213 | 214 | def create_data_sources(path, encoding='ascii', convert_3d_to_2d=False, zipfile=None): 215 | """ 216 | If the path is to a shapefile, returns a DataSource for the shapefile. If 217 | the path is to a directory or ZIP file, returns DataSources for shapefiles 218 | in the directory or ZIP file. 219 | """ 220 | 221 | def create_data_source(path): 222 | if convert_3d_to_2d and '_cleaned_' not in path: 223 | source = path 224 | path = path.replace('.shp', '._cleaned_.shp') 225 | args = ['ogr2ogr', '-f', 'ESRI Shapefile', path, source, '-nlt', 'POLYGON'] 226 | if os.path.exists(path): 227 | args.append('-overwrite') 228 | subprocess.call(args) 229 | 230 | return DataSource(path, encoding=encoding) 231 | 232 | def create_data_sources_from_zip(path): 233 | """ 234 | Decompresses a ZIP file into a temporary directory and returns the data 235 | sources it contains, along with all temporary directories created. 236 | """ 237 | 238 | tmpdir = mkdtemp() 239 | 240 | with closing(ZipFile(path)) as z: 241 | z.extractall(tmpdir) 242 | 243 | data_sources, tmpdirs = create_data_sources(tmpdir, encoding, convert_3d_to_2d, path) 244 | 245 | tmpdirs.insert(0, tmpdir) 246 | 247 | return data_sources, tmpdirs 248 | 249 | if os.path.isfile(path): 250 | if path.endswith('.shp'): 251 | return [create_data_source(path)], [] 252 | elif path.endswith('.zip'): 253 | return create_data_sources_from_zip(path) 254 | else: 255 | raise ValueError( 256 | _("The path must be a shapefile, a ZIP file, or a directory: %(value)s.") % {'value': path} 257 | ) 258 | 259 | data_sources = [] 260 | tmpdirs = [] 261 | 262 | for (dirpath, dirnames, filenames) in os.walk(path, followlinks=True): 263 | dirnames.sort() # force a constant order 264 | for basename in sorted(filenames): 265 | filename = os.path.join(dirpath, basename) 266 | if filename.endswith('.shp'): 267 | if '_cleaned_' not in filename: # don't load the cleaned copy twice 268 | data_source = create_data_source(filename) 269 | if zipfile: 270 | data_source.zipfile = zipfile # to trace the data source back to its ZIP file 271 | data_sources.append(data_source) 272 | elif filename.endswith('.zip'): 273 | _data_sources, _tmpdirs = create_data_sources_from_zip(filename) 274 | data_sources += _data_sources 275 | tmpdirs += _tmpdirs 276 | 277 | return data_sources, tmpdirs 278 | -------------------------------------------------------------------------------- /boundaries/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | import django.contrib.gis.db.models.fields 2 | from django.db import migrations, models 3 | 4 | 5 | class JSONField(models.TextField): 6 | """Mocks jsonfield 0.92's column-type behaviour""" 7 | def db_type(self, connection): 8 | if connection.vendor == 'postgresql' and connection.pg_version >= 90300: 9 | return 'json' 10 | else: 11 | return super().db_type(connection) 12 | 13 | class Migration(migrations.Migration): 14 | 15 | dependencies = [ 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='Boundary', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('set_name', models.CharField(max_length=100, help_text='A generic singular name for the boundary.')), 24 | ('slug', models.SlugField(max_length=200, help_text="The boundary's unique identifier within the set, used as a path component in URLs.")), 25 | ('external_id', models.CharField(max_length=64, help_text='An identifier of the boundary, which should be unique within the set.')), 26 | ('name', models.CharField(db_index=True, max_length=192, help_text='The name of the boundary.')), 27 | ('metadata', JSONField(default=dict, help_text='The attributes of the boundary from the shapefile, as a dictionary.', blank=True)), 28 | ('shape', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326, help_text='The geometry of the boundary in EPSG:4326.')), 29 | ('simple_shape', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326, help_text='The simplified geometry of the boundary in EPSG:4326.')), 30 | ('centroid', django.contrib.gis.db.models.fields.PointField(srid=4326, help_text='The centroid of the boundary in EPSG:4326.', null=True)), 31 | ('extent', JSONField(blank=True, help_text='The bounding box of the boundary as a list like [xmin, ymin, xmax, ymax] in EPSG:4326.', null=True)), 32 | ('label_point', django.contrib.gis.db.models.fields.PointField(spatial_index=False, srid=4326, blank=True, help_text='The point at which to place a label for the boundary in EPSG:4326, used by represent-maps.', null=True)), 33 | ], 34 | options={ 35 | 'verbose_name_plural': 'boundaries', 36 | 'verbose_name': 'boundary', 37 | }, 38 | bases=(models.Model,), 39 | ), 40 | migrations.CreateModel( 41 | name='BoundarySet', 42 | fields=[ 43 | ('slug', models.SlugField(primary_key=True, help_text="The boundary set's unique identifier, used as a path component in URLs.", serialize=False, max_length=200, editable=False)), 44 | ('name', models.CharField(max_length=100, help_text='The plural name of the boundary set.', unique=True)), 45 | ('singular', models.CharField(max_length=100, help_text='A generic singular name for a boundary in the set.')), 46 | ('authority', models.CharField(max_length=256, help_text='The entity responsible for publishing the data.')), 47 | ('domain', models.CharField(max_length=256, help_text='The geographic area covered by the boundary set.')), 48 | ('last_updated', models.DateField(help_text='The most recent date on which the data was updated.')), 49 | ('source_url', models.URLField(help_text='A URL to the source of the data.', blank=True)), 50 | ('notes', models.TextField(help_text='Free-form text notes, often used to describe changes that were made to the original source data.', blank=True)), 51 | ('licence_url', models.URLField(help_text='A URL to the licence under which the data is made available.', blank=True)), 52 | ('extent', JSONField(blank=True, help_text="The set's boundaries' bounding box as a list like [xmin, ymin, xmax, ymax] in EPSG:4326.", null=True)), 53 | ('start_date', models.DateField(blank=True, help_text="The date from which the set's boundaries are in effect.", null=True)), 54 | ('end_date', models.DateField(blank=True, help_text="The date until which the set's boundaries are in effect.", null=True)), 55 | ('extra', JSONField(blank=True, help_text='Any additional metadata.', null=True)), 56 | ], 57 | options={ 58 | 'ordering': ('name',), 59 | 'verbose_name_plural': 'boundary sets', 60 | 'verbose_name': 'boundary set', 61 | }, 62 | bases=(models.Model,), 63 | ), 64 | migrations.AddField( 65 | model_name='boundary', 66 | name='set', 67 | field=models.ForeignKey(related_name='boundaries', to='boundaries.BoundarySet', on_delete=models.CASCADE, help_text='The set to which the boundary belongs.'), 68 | preserve_default=True, 69 | ), 70 | migrations.AlterUniqueTogether( 71 | name='boundary', 72 | unique_together={('slug', 'set')}, 73 | ), 74 | ] 75 | -------------------------------------------------------------------------------- /boundaries/migrations/0002_auto_20141129_1402.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class JSONField(models.TextField): 5 | """Mocks jsonfield 0.92's column-type behaviour""" 6 | def db_type(self, connection): 7 | if connection.vendor == 'postgresql' and connection.pg_version >= 90300: 8 | return 'json' 9 | else: 10 | return super().db_type(connection) 11 | 12 | def set_defaults(apps, schema_editor): 13 | Boundary = apps.get_model("boundaries", "Boundary") 14 | for boundary in Boundary.objects.all(): 15 | if boundary.metadata is None: 16 | boundary.metadata = {} 17 | boundary.save() 18 | 19 | 20 | class Migration(migrations.Migration): 21 | 22 | dependencies = [ 23 | ('boundaries', '0001_initial'), 24 | ] 25 | 26 | operations = [ 27 | migrations.RunPython(set_defaults), 28 | migrations.AlterField( 29 | model_name='boundary', 30 | name='metadata', 31 | field=JSONField(default={}, help_text='The attributes of the boundary from the shapefile, as a dictionary.'), 32 | ), 33 | migrations.AlterField( 34 | model_name='boundaryset', 35 | name='extra', 36 | field=JSONField(default={}, help_text='Any additional metadata.'), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /boundaries/migrations/0003_auto_20150528_1338.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class Migration(migrations.Migration): 5 | 6 | dependencies = [ 7 | ('boundaries', '0002_auto_20141129_1402'), 8 | ] 9 | 10 | operations = [ 11 | migrations.AddField( 12 | model_name='boundary', 13 | name='end_date', 14 | field=models.DateField(help_text='The date until which the boundary is in effect.', blank=True, null=True), 15 | preserve_default=True, 16 | ), 17 | migrations.AddField( 18 | model_name='boundary', 19 | name='start_date', 20 | field=models.DateField(help_text='The date from which the boundary is in effect.', blank=True, null=True), 21 | preserve_default=True, 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /boundaries/migrations/0004_auto_20150921_1607.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class Migration(migrations.Migration): 5 | 6 | dependencies = [ 7 | ('boundaries', '0003_auto_20150528_1338'), 8 | ] 9 | 10 | operations = [ 11 | migrations.AlterField( 12 | model_name='boundary', 13 | name='external_id', 14 | field=models.CharField(max_length=255, help_text='An identifier of the boundary, which should be unique within the set.'), 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /boundaries/migrations/0005_auto_20150925_0338.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9a1 on 2015-09-25 03:38 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class JSONField(models.TextField): 7 | """Mocks jsonfield 0.92's column-type behaviour""" 8 | def db_type(self, connection): 9 | if connection.vendor == 'postgresql' and connection.pg_version >= 90300: 10 | return 'json' 11 | else: 12 | return super().db_type(connection) 13 | 14 | 15 | class Migration(migrations.Migration): 16 | 17 | dependencies = [ 18 | ('boundaries', '0004_auto_20150921_1607'), 19 | ] 20 | 21 | operations = [ 22 | migrations.AlterField( 23 | model_name='boundary', 24 | name='metadata', 25 | field=JSONField(blank=True, default={}, help_text='The attributes of the boundary from the shapefile, as a dictionary.'), 26 | ), 27 | migrations.AlterField( 28 | model_name='boundaryset', 29 | name='extra', 30 | field=JSONField(blank=True, default={}, help_text='Any additional metadata.'), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /boundaries/migrations/0006_switch_to_django_jsonfield.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.5 on 2017-02-23 13:52 2 | 3 | import django.contrib.postgres.fields.jsonb 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('boundaries', '0005_auto_20150925_0338'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='boundary', 16 | name='extent', 17 | field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The bounding box of the boundary as a list like [xmin, ymin, xmax, ymax] in EPSG:4326.', null=True), 18 | ), 19 | migrations.AlterField( 20 | model_name='boundary', 21 | name='metadata', 22 | field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default={}, help_text='The attributes of the boundary from the shapefile, as a dictionary.'), 23 | ), 24 | migrations.AlterField( 25 | model_name='boundaryset', 26 | name='extent', 27 | field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text="The set's boundaries' bounding box as a list like [xmin, ymin, xmax, ymax] in EPSG:4326.", null=True), 28 | ), 29 | migrations.AlterField( 30 | model_name='boundaryset', 31 | name='extra', 32 | field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default={}, help_text='Any additional metadata.'), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /boundaries/migrations/0007_auto_20180325_1421.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.3 on 2018-03-25 14:21 2 | 3 | import django.contrib.postgres.fields.jsonb 4 | import django.core.serializers.json 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('boundaries', '0006_switch_to_django_jsonfield'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='boundary', 17 | name='metadata', 18 | field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default={}, encoder=django.core.serializers.json.DjangoJSONEncoder, help_text='The attributes of the boundary from the shapefile, as a dictionary.'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /boundaries/migrations/0008_alter_boundary_extent_alter_boundary_metadata_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.8 on 2023-12-10 02:19 2 | 3 | import django.core.serializers.json 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('boundaries', '0007_auto_20180325_1421'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='boundary', 16 | name='extent', 17 | field=models.JSONField(blank=True, help_text='The bounding box of the boundary as a list like [xmin, ymin, xmax, ymax] in EPSG:4326.', null=True), 18 | ), 19 | migrations.AlterField( 20 | model_name='boundary', 21 | name='metadata', 22 | field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder, help_text='The attributes of the boundary from the shapefile, as a dictionary.'), 23 | ), 24 | migrations.AlterField( 25 | model_name='boundaryset', 26 | name='extent', 27 | field=models.JSONField(blank=True, help_text="The set's boundaries' bounding box as a list like [xmin, ymin, xmax, ymax] in EPSG:4326.", null=True), 28 | ), 29 | migrations.AlterField( 30 | model_name='boundaryset', 31 | name='extra', 32 | field=models.JSONField(blank=True, default=dict, help_text='Any additional metadata.'), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /boundaries/migrations/0009_alter_boundaryset_slug.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2024-06-26 14:06 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('boundaries', '0008_alter_boundary_extent_alter_boundary_metadata_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='boundaryset', 15 | name='slug', 16 | field=models.SlugField(help_text="The boundary set's unique identifier, used as a path component in URLs.", max_length=200, primary_key=True, serialize=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /boundaries/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/migrations/__init__.py -------------------------------------------------------------------------------- /boundaries/static/js/jsonformatter.js: -------------------------------------------------------------------------------- 1 | function escape(string) { 2 | /* Escape HTML-unsafe values */ 3 | if (typeof(string) != 'string') { 4 | return ''; 5 | } 6 | return string.replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g, '/'); 7 | } 8 | function formatJSON(content) { 9 | /* Takes a string of raw JSON, and returns escaped, syntax-highlighted 10 | HTML, with links in tags. */ 11 | var lines = content.split('\n'); 12 | var l; 13 | var r = []; 14 | var structure = /^(\s*)([{}\[\]])(,?)\s*$/; 15 | for (var i = 0; i < lines.length; i++) { 16 | l = lines[i]; 17 | var smatch = l.match(structure); 18 | if (smatch) { 19 | r.push(smatch[1] + '' + smatch[2] + '' + smatch[3]); 20 | } 21 | else { 22 | var match = l.match(/^(\s+)("[^"]+"): (.+?)(,?)\s*$/); 23 | if (!match) { 24 | r.push(l); 25 | } 26 | else { 27 | var val = match[1] + '' + escape(match[2]) 28 | + ': '; 29 | if (structure.test(match[3])) { 30 | val += '' + escape(match[3]) + ''; 31 | } 32 | else { 33 | val += ''; 34 | if ( 35 | (/url"$/.test(match[2]) || match[2] === '"next"' || match[2] == '"previous"') 36 | && /^"(h|\/)/.test(match[3])) { 37 | var url = match[3].substr(1, match[3].length - 2); 38 | url = url.replace(/[?&]format=apibrowser$/, ''); 39 | url = url.replace(/([?&])format=apibrowser&/, '$1'); 40 | val += '"' + escape(url) + '"'; 51 | } 52 | else { 53 | val += escape(match[3]).replace(/\\u2014/g, '—'); 54 | } 55 | val += ''; 56 | } 57 | r.push(val + match[4]); 58 | } 59 | } 60 | } 61 | return r.join('\n'); 62 | } 63 | -------------------------------------------------------------------------------- /boundaries/templates/boundaries/apibrowser.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | 5 | {% trans 'API Browser' %} 6 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /boundaries/templates/boundaries/apidoc.html: -------------------------------------------------------------------------------- 1 |

BoundarySet

2 | 3 |

A BoundarySet is a group of geographic regions, like districts or wards.

4 | 5 |
6 |
/boundary-sets
7 |
Get metadata for all BoundarySets.
8 | 9 |
/boundary-sets/{{boundaryset.slug|urlencode}}
10 |
Get metadata for one BoundarySet, the one named {{boundaryset.slug|urlencode}}.
11 |
12 | 13 |

The metadata fields of a BoundarySet are largely user-supplied strings.

14 | 15 | 16 | {% for name, field in models.BoundarySet %} 17 | 18 | 19 | 20 | 21 | {% endfor %} 22 |
{{name}}{{field.help_text}} {% if field.null %}(Optional.){% endif %}
23 | 24 |

Boundary

25 | 26 |

A Boundary is a single district. It is stored as a MultiPolygon internally, which means the district may be made up of multiple geographically disjoint parts. All coordinates are in the WGS 84 (EPSG:4326) spatial reference system.

27 | 28 |
29 |
/boundaries
30 |
Get metadata for all Boundary records, in all BoundarySets.
31 | 32 |
/boundaries/{{boundaryset.slug|urlencode}}
33 |
Get metadata for all Boundary records in a particular BoundarySet, here the {{boundaryset.slug}} BoundarySet.
34 | 35 |
/boundaries/{{boundaryset.slug|urlencode}}/{{boundary.slug|urlencode}}
36 |
Get metadata for a particular Boundary — here the {{boundary.slug}} Boundary in the {{boundaryset.slug}} BoundarySet.
37 | 38 |
/boundaries/{{boundaryset.slug|urlencode}}/?contains={{boundary.centroid.coords.1|floatformat:-3}},{{boundary.centroid.coords.0|floatformat:-3}}
39 |
Find Boundaries that contain a given lat/lng coordiate.
40 | 41 |
/boundaries/{{boundaryset.slug|urlencode}}/?touches={{boundaryset.slug|urlencode}}/{{boundary.slug|urlencode}}
42 |
Find Boundaries in a particular BoundarySet that touch a given Boundary (named by the BoundarySet and Boundary slugs). Leave out the BoundarySet in the path part of the URL (see the intersects example next) to search across all BoundarySets.
43 | 44 |
/boundaries/?intersects={{boundaryset.slug|urlencode}}/{{boundary.slug|urlencode}}
45 |
Find Boundaries across the whole database that intersects a given Boundary (named by the BoundarySet and Boundary slugs). Specify a BoundarySet slug in the path part of the URL (see the touches example above) to search in a particular BoundarySet.
46 | 47 |
/boundaries/{{boundaryset.slug|urlencode}}/{{boundary.slug|urlencode}}/shape?format=wkt
48 |
Get the actual shape of a Boundary — here in WTK format for the {{boundary.slug}} Boundary in the {{boundaryset.slug}} BoundarySet. You will always get back a MultiPolygon. You may also specify format=json or format=kml.
49 | 50 |
/boundaries/{{boundaryset.slug|urlencode}}/{{boundary.slug|urlencode}}/simple_shape?format=wkt
51 |
Same as above but retrieves simplified geometry. The degree of simplification depends on site settings but the default is to eliminate features smaller than 0.0002 of a degree.
52 |
53 | 54 |

The metadata fields of a Boundary are:

55 | 56 | 57 | {% for name, field in models.Boundary %} 58 | 59 | 60 | 61 | 62 | {% endfor %} 63 |
{{name}}{{field.help_text}} {% if field.null %}(Optional.){% endif %}
64 | -------------------------------------------------------------------------------- /boundaries/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | from copy import deepcopy 4 | from urllib.parse import parse_qsl, unquote_plus, urlparse 5 | 6 | from django.conf import settings 7 | from django.contrib.gis.gdal import OGRGeometry 8 | from django.contrib.gis.geos import GEOSGeometry 9 | from django.test import TestCase 10 | 11 | from boundaries.models import Boundary, app_settings 12 | 13 | jsonp_re = re.compile(r'\Aabcdefghijklmnopqrstuvwxyz\.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\$_\((.+)\);\Z', re.DOTALL) 14 | pretty_re = re.compile(r'\n ') 15 | 16 | 17 | if not hasattr(TestCase, 'assertCountEqual'): # Python < 3.2 18 | TestCase.assertCountEqual = TestCase.assertItemsEqual 19 | 20 | 21 | class FeatureProxy(dict): 22 | def __init__(self, fields): 23 | self.update(fields) 24 | 25 | @property 26 | def fields(self): 27 | return self.keys() 28 | 29 | @property 30 | def geom(self): 31 | return OGRGeometry('MULTIPOLYGON (((0 0,0.0001 0.0001,0 5,5 5,0 0)))') 32 | 33 | 34 | class BoundariesTestCase(TestCase): 35 | def assertTupleAlmostEqual(self, actual, expected): 36 | self.assertTrue(isinstance(actual, tuple)) 37 | self.assertEqual(len(actual), len(expected)) 38 | for i, value in enumerate(expected): 39 | self.assertAlmostEqual(actual[i], expected[i]) 40 | 41 | 42 | class ViewTestCase(TestCase): 43 | non_integers = ('', '1.0', '0b1', '0o1', '0x1') # '01' is okay 44 | 45 | def assertResponse(self, response, content_type='application/json; charset=utf-8'): 46 | self.assertEqual(response.status_code, 200) 47 | self.assertEqual(response['Content-Type'], content_type) 48 | if app_settings.ALLOW_ORIGIN and 'application/json' in response['Content-Type']: 49 | self.assertEqual(response['Access-Control-Allow-Origin'], '*') 50 | else: 51 | self.assertNotIn('Access-Control-Allow-Origin', response) 52 | 53 | def assertNotFound(self, response): 54 | self.assertEqual(response.status_code, 404) 55 | self.assertIn(response['Content-Type'], ('text/html', 'text/html; charset=utf-8')) # different versions of Django 56 | self.assertNotIn('Access-Control-Allow-Origin', response) 57 | 58 | def assertError(self, response): 59 | self.assertEqual(response.status_code, 400) 60 | self.assertEqual(response['Content-Type'], 'text/plain') 61 | self.assertNotIn('Access-Control-Allow-Origin', response) 62 | 63 | def assertForbidden(self, response): 64 | self.assertEqual(response.status_code, 403) 65 | self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') 66 | self.assertNotIn('Access-Control-Allow-Origin', response) 67 | 68 | def assertJSONEqual(self, actual, expected): 69 | if isinstance(actual, str): 70 | actual = json.loads(actual) 71 | else: # It's a response. 72 | actual = load_response(actual) 73 | if isinstance(expected, str): 74 | expected = json.loads(expected) 75 | else: 76 | expected = deepcopy(expected) 77 | self.assertCountEqual(comparable(actual), comparable(expected)) 78 | 79 | 80 | class URL: 81 | 82 | """ 83 | https://stackoverflow.com/questions/5371992/comparing-two-urls-in-python 84 | """ 85 | 86 | def __init__(self, url): 87 | if isinstance(url, str): 88 | parsed = urlparse(url) 89 | self.parsed = parsed._replace(query=frozenset(parse_qsl(parsed.query)), path=unquote_plus(parsed.path)) 90 | else: # It's already a URL. 91 | self.parsed = url.parsed 92 | 93 | def __eq__(self, other): 94 | return self.parsed == other.parsed 95 | 96 | def __hash__(self): 97 | return hash(self.parsed) 98 | 99 | def __str__(self): 100 | return self.parsed.geturl() 101 | 102 | 103 | def comparable(o): 104 | """ 105 | The order of URL query parameters may differ, so make URLs into URL objects, 106 | which ignore query parameter ordering. 107 | """ 108 | 109 | if isinstance(o, dict): 110 | for k, v in o.items(): 111 | if v is None: 112 | o[k] = None 113 | elif k.endswith('url') or k in ('next', 'previous'): 114 | o[k] = URL(v) 115 | else: 116 | o[k] = comparable(v) 117 | elif isinstance(o, list): 118 | o = [comparable(v) for v in o] 119 | return o 120 | 121 | 122 | def load_response(response): 123 | return json.loads(response.content.decode('utf-8')) 124 | 125 | 126 | class ViewsTests: 127 | 128 | def test_get(self): 129 | response = self.client.get(self.url) 130 | self.assertResponse(response) 131 | self.assertEqual(load_response(response), self.json) 132 | 133 | def test_allow_origin(self): 134 | app_settings.ALLOW_ORIGIN, _ = None, app_settings.ALLOW_ORIGIN 135 | 136 | response = self.client.get(self.url) 137 | self.assertResponse(response) 138 | self.assertEqual(load_response(response), self.json) 139 | 140 | app_settings.ALLOW_ORIGIN = _ 141 | 142 | def test_jsonp(self): 143 | response = self.client.get(self.url, {'callback': 'abcdefghijklmnopqrstuvwxyz.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890`~!@#$%^&*()-_=+[{]}\\|;:\'",<>/?'}) 144 | self.assertResponse(response) 145 | content = response.content.decode('utf-8') 146 | self.assertJSONEqual(content[66:-2], self.json) 147 | self.assertRegex(content, jsonp_re) 148 | 149 | def test_apibrowser(self): 150 | response = self.client.get(self.url, {'format': 'apibrowser', 'limit': 20}) 151 | self.assertResponse(response, content_type='text/html; charset=utf-8') 152 | 153 | 154 | class PrettyTests: 155 | 156 | def test_pretty(self): 157 | response = self.client.get(self.url, {'pretty': 1}) 158 | self.assertResponse(response) 159 | self.assertEqual(load_response(response), self.json) 160 | self.assertRegex(response.content.decode('utf-8'), pretty_re) 161 | 162 | def test_jsonp_and_pretty(self): 163 | response = self.client.get(self.url, {'callback': 'abcdefghijklmnopqrstuvwxyz.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890`~!@#$%^&*()-_=+[{]}\\|;:\'",<>/?', 'pretty': 1}) 164 | self.assertResponse(response) 165 | content = response.content.decode('utf-8') 166 | self.assertJSONEqual(content[66:-2], self.json) 167 | self.assertRegex(content, jsonp_re) 168 | self.assertRegex(response.content.decode('utf-8'), pretty_re) 169 | 170 | 171 | class PaginationTests: 172 | 173 | def test_limit_is_set(self): 174 | response = self.client.get(self.url, {'limit': 10}) 175 | self.assertResponse(response) 176 | data = deepcopy(self.json) 177 | data['meta']['limit'] = 10 178 | self.assertEqual(load_response(response), data) 179 | 180 | def test_offset_is_set(self): 181 | response = self.client.get(self.url, {'offset': 10}) 182 | self.assertResponse(response) 183 | data = deepcopy(self.json) 184 | data['meta']['offset'] = 10 185 | self.assertEqual(load_response(response), data) 186 | 187 | def test_limit_is_set_to_maximum_if_zero(self): 188 | response = self.client.get(self.url, {'limit': 0}) 189 | self.assertResponse(response) 190 | data = deepcopy(self.json) 191 | data['meta']['limit'] = 1000 192 | self.assertEqual(load_response(response), data) 193 | 194 | def test_limit_is_set_to_maximum_if_greater_than_maximum(self): 195 | response = self.client.get(self.url, {'limit': 2000}) 196 | self.assertResponse(response) 197 | data = deepcopy(self.json) 198 | data['meta']['limit'] = 1000 199 | self.assertEqual(load_response(response), data) 200 | 201 | def test_api_limit_per_page(self): 202 | settings.API_LIMIT_PER_PAGE, _ = 1, getattr(settings, 'API_LIMIT_PER_PAGE', 20) 203 | 204 | response = self.client.get(self.url) 205 | self.assertResponse(response) 206 | data = deepcopy(self.json) 207 | data['meta']['limit'] = 1 208 | self.assertEqual(load_response(response), data) 209 | 210 | settings.API_LIMIT_PER_PAGE = _ 211 | 212 | def test_limit_must_be_an_integer(self): 213 | for value in self.non_integers: 214 | response = self.client.get(self.url, {'limit': value}) 215 | self.assertError(response) 216 | self.assertEqual(response.content, ("Invalid limit '%s' provided. Please provide a positive integer." % value).encode('ascii')) 217 | 218 | def test_offset_must_be_an_integer(self): 219 | for value in self.non_integers: 220 | response = self.client.get(self.url, {'offset': value}) 221 | self.assertError(response) 222 | self.assertEqual(response.content, ("Invalid offset '%s' provided. Please provide a positive integer." % value).encode('ascii')) 223 | 224 | def test_limit_must_be_non_negative(self): 225 | response = self.client.get(self.url, {'limit': -1}) 226 | self.assertError(response) 227 | self.assertEqual(response.content, b"Invalid limit '-1' provided. Please provide a positive integer >= 0.") 228 | 229 | def test_offset_must_be_non_negative(self): 230 | response = self.client.get(self.url, {'offset': -1}) 231 | self.assertError(response) 232 | self.assertEqual(response.content, b"Invalid offset '-1' provided. Please provide a positive integer >= 0.") 233 | 234 | 235 | class BoundaryListTests: 236 | 237 | def test_omits_meta_if_too_many_items_match(self): 238 | app_settings.MAX_GEO_LIST_RESULTS, _ = 0, app_settings.MAX_GEO_LIST_RESULTS 239 | 240 | geom = GEOSGeometry('MULTIPOLYGON(((0 0,0 5,5 5,0 0)))') 241 | Boundary.objects.create(slug='foo', set_id='inc', shape=geom, simple_shape=geom) 242 | 243 | response = self.client.get(self.url) 244 | self.assertResponse(response) 245 | self.assertJSONEqual(response, '{"objects": [{"url": "/boundaries/inc/foo/", "boundary_set_name": "", "external_id": "", "name": "", "related": {"boundary_set_url": "/boundary-sets/inc/"}}], "meta": {"next": null, "total_count": 1, "previous": null, "limit": 20, "offset": 0}}') 246 | 247 | app_settings.MAX_GEO_LIST_RESULTS = _ 248 | 249 | 250 | class GeoListTests: 251 | 252 | def test_must_not_match_too_many_items(self): 253 | app_settings.MAX_GEO_LIST_RESULTS, _ = 0, app_settings.MAX_GEO_LIST_RESULTS 254 | 255 | response = self.client.get(self.url) 256 | self.assertForbidden(response) 257 | self.assertEqual(response.content, b'Spatial-list queries cannot return more than 0 resources; this query would return 1. Please filter your query.') 258 | 259 | app_settings.MAX_GEO_LIST_RESULTS = _ 260 | 261 | 262 | class GeoTests: 263 | 264 | def test_wkt(self): 265 | response = self.client.get(self.url, {'format': 'wkt'}) 266 | self.assertResponse(response, content_type='text/plain') 267 | self.assertRegex(str(response.content), r'MULTIPOLYGON \(\(\(0(\.0+)? 0(\.0+)?, 0(\.0+)? 5(\.0+)?, 5(\.0+)? 5(\.0+)?, 0(\.0+)? 0(\.0+)?\)\)\)') 268 | 269 | def test_kml(self): 270 | response = self.client.get(self.url, {'format': 'kml'}) 271 | self.assertResponse(response, content_type='application/vnd.google-earth.kml+xml') 272 | self.assertEqual(response.content, b'\n\n\n0.0,0.0,0 0.0,5.0,0 5.0,5.0,0 0.0,0.0,0\n\n') 273 | self.assertEqual(response['Content-Disposition'], 'attachment; filename="shape.kml"') 274 | 275 | def test_invalid(self): 276 | self.assertRaises(NotImplementedError, self.client.get, self.url, {'format': 'invalid'}) 277 | -------------------------------------------------------------------------------- /boundaries/tests/definitions/no_data_sources/definition.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | import boundaries 4 | 5 | boundaries.register( 6 | 'Empty', 7 | last_updated=date(2000, 1, 1), 8 | name_func=boundaries.attr('id'), 9 | file='../../fixtures/empty.zip', 10 | ) 11 | -------------------------------------------------------------------------------- /boundaries/tests/definitions/no_features/definition.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | import boundaries 4 | 5 | boundaries.register( 6 | 'Districts', 7 | last_updated=date(2000, 1, 1), 8 | name_func=boundaries.attr('id'), 9 | file='../../fixtures/foo.shp', 10 | ) 11 | -------------------------------------------------------------------------------- /boundaries/tests/definitions/polygons/definition.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | import boundaries 4 | 5 | boundaries.register( 6 | 'Polygons', 7 | last_updated=date(2000, 1, 1), 8 | name_func=boundaries.attr('str'), 9 | ) 10 | -------------------------------------------------------------------------------- /boundaries/tests/definitions/polygons/test_poly.dbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/definitions/polygons/test_poly.dbf -------------------------------------------------------------------------------- /boundaries/tests/definitions/polygons/test_poly.prj: -------------------------------------------------------------------------------- 1 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] -------------------------------------------------------------------------------- /boundaries/tests/definitions/polygons/test_poly.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/definitions/polygons/test_poly.shp -------------------------------------------------------------------------------- /boundaries/tests/definitions/polygons/test_poly.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/definitions/polygons/test_poly.shx -------------------------------------------------------------------------------- /boundaries/tests/definitions/srid/definition.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | import boundaries 4 | 5 | boundaries.register( 6 | 'Wards', 7 | last_updated=date(2000, 1, 1), 8 | name_func=boundaries.attr('id'), 9 | file='../../fixtures/foo.shp', 10 | srid=4326, 11 | ) 12 | -------------------------------------------------------------------------------- /boundaries/tests/fixtures/bad.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/fixtures/bad.zip -------------------------------------------------------------------------------- /boundaries/tests/fixtures/bar_definition.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | import boundaries 4 | 5 | boundaries.register( 6 | 'Districts', 7 | last_updated=date(2000, 1, 1), 8 | name_func=boundaries.attr('id'), 9 | file='foo.shp', 10 | ) 11 | -------------------------------------------------------------------------------- /boundaries/tests/fixtures/empty.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/fixtures/empty.zip -------------------------------------------------------------------------------- /boundaries/tests/fixtures/empty/empty.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/fixtures/empty/empty.txt -------------------------------------------------------------------------------- /boundaries/tests/fixtures/empty/empty.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/fixtures/empty/empty.zip -------------------------------------------------------------------------------- /boundaries/tests/fixtures/flat.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/fixtures/flat.zip -------------------------------------------------------------------------------- /boundaries/tests/fixtures/foo.dbf: -------------------------------------------------------------------------------- 1 | _A idN 2 | -------------------------------------------------------------------------------- /boundaries/tests/fixtures/foo.prj: -------------------------------------------------------------------------------- 1 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] -------------------------------------------------------------------------------- /boundaries/tests/fixtures/foo.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/fixtures/foo.shp -------------------------------------------------------------------------------- /boundaries/tests/fixtures/foo.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/fixtures/foo.shx -------------------------------------------------------------------------------- /boundaries/tests/fixtures/foo_definition.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | import boundaries 4 | 5 | boundaries.register( 6 | 'Districts', 7 | last_updated=date(2000, 1, 1), 8 | name_func=boundaries.attr('id'), 9 | file='foo.shp', 10 | ) 11 | -------------------------------------------------------------------------------- /boundaries/tests/fixtures/multiple.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/fixtures/multiple.zip -------------------------------------------------------------------------------- /boundaries/tests/fixtures/multiple/bar.dbf: -------------------------------------------------------------------------------- 1 | _A idN 2 | -------------------------------------------------------------------------------- /boundaries/tests/fixtures/multiple/bar.prj: -------------------------------------------------------------------------------- 1 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] -------------------------------------------------------------------------------- /boundaries/tests/fixtures/multiple/bar.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/fixtures/multiple/bar.shp -------------------------------------------------------------------------------- /boundaries/tests/fixtures/multiple/bar.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/fixtures/multiple/bar.shx -------------------------------------------------------------------------------- /boundaries/tests/fixtures/multiple/dir.zip/foo.dbf: -------------------------------------------------------------------------------- 1 | _A idN 2 | -------------------------------------------------------------------------------- /boundaries/tests/fixtures/multiple/dir.zip/foo.prj: -------------------------------------------------------------------------------- 1 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] -------------------------------------------------------------------------------- /boundaries/tests/fixtures/multiple/dir.zip/foo.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/fixtures/multiple/dir.zip/foo.shp -------------------------------------------------------------------------------- /boundaries/tests/fixtures/multiple/dir.zip/foo.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/fixtures/multiple/dir.zip/foo.shx -------------------------------------------------------------------------------- /boundaries/tests/fixtures/multiple/empty.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/fixtures/multiple/empty.zip -------------------------------------------------------------------------------- /boundaries/tests/fixtures/multiple/flat.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/fixtures/multiple/flat.zip -------------------------------------------------------------------------------- /boundaries/tests/fixtures/multiple/foo.dbf: -------------------------------------------------------------------------------- 1 | _A idN 2 | -------------------------------------------------------------------------------- /boundaries/tests/fixtures/multiple/foo.prj: -------------------------------------------------------------------------------- 1 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] -------------------------------------------------------------------------------- /boundaries/tests/fixtures/multiple/foo.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/fixtures/multiple/foo.shp -------------------------------------------------------------------------------- /boundaries/tests/fixtures/multiple/foo.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/fixtures/multiple/foo.shx -------------------------------------------------------------------------------- /boundaries/tests/fixtures/multiple/nested.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/fixtures/multiple/nested.zip -------------------------------------------------------------------------------- /boundaries/tests/fixtures/nested.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/fixtures/nested.zip -------------------------------------------------------------------------------- /boundaries/tests/fixtures/nested/dir.zip/foo.dbf: -------------------------------------------------------------------------------- 1 | _A idN 2 | -------------------------------------------------------------------------------- /boundaries/tests/fixtures/nested/dir.zip/foo.prj: -------------------------------------------------------------------------------- 1 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] -------------------------------------------------------------------------------- /boundaries/tests/fixtures/nested/dir.zip/foo.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/fixtures/nested/dir.zip/foo.shp -------------------------------------------------------------------------------- /boundaries/tests/fixtures/nested/dir.zip/foo.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennorth/represent-boundaries/564083d5dac961b95fc68767264bb86e46aae0f4/boundaries/tests/fixtures/nested/dir.zip/foo.shx -------------------------------------------------------------------------------- /boundaries/tests/test.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from django.test import TestCase 4 | from testfixtures import LogCapture 5 | 6 | import boundaries 7 | 8 | 9 | class BoundariesTestCase(TestCase): 10 | maxDiff = None 11 | 12 | def test_register(self): 13 | boundaries.registry = {} 14 | boundaries._basepath = '.' 15 | boundaries.register('foo', file='bar', last_updated=date(2000, 1, 1)) 16 | self.assertEqual(boundaries.registry, {'foo': {'file': './bar', 'last_updated': date(2000, 1, 1)}}) 17 | 18 | def test_autodiscover(self): 19 | # Uses bar_definition.py and foo_definition.py fixtures. 20 | boundaries.registry = {} 21 | boundaries._basepath = '.' 22 | with LogCapture() as logcapture: 23 | boundaries.autodiscover('./boundaries/tests/fixtures') 24 | self.assertEqual(len(boundaries.registry), 1) 25 | self.assertEqual(boundaries.registry['Districts']['file'], './boundaries/tests/fixtures/foo.shp') 26 | self.assertEqual(boundaries.registry['Districts']['last_updated'], date(2000, 1, 1)) 27 | 28 | logcapture.check(('boundaries', 'WARNING', 'Multiple definitions of Districts found.')) 29 | 30 | def test_attr(self): 31 | self.assertEqual(boundaries.attr('foo')({'foo': 'bar'}), 'bar') 32 | self.assertEqual(boundaries.attr('foo')({}), None) # not the case for clean_attr and dashed_attr 33 | 34 | def test_clean_attr(self): 35 | self.assertEqual(boundaries.clean_attr('foo')({'foo': 'Foo --\tBar\r--Baz--\nBzz--Abc - Xyz'}), 'Foo—Bar—Baz—Bzz—Abc—Xyz') 36 | self.assertEqual(boundaries.clean_attr('foo')({'foo': 'FOO --\tBAR\r--BAZ--\nBZZ--ABC - XYZ'}), 'Foo—Bar—Baz—Bzz—Abc—Xyz') 37 | 38 | def test_dashed_attr(self): 39 | self.assertEqual(boundaries.dashed_attr('foo')({'foo': 'Foo --\tBar\r--Baz--\nBzz--Abc - Xyz-Inc'}), 'Foo—Bar—Baz—Bzz—Abc—Xyz—Inc') 40 | self.assertEqual(boundaries.dashed_attr('foo')({'foo': 'FOO --\tBAR\r--BAZ--\nBZZ--ABC - XYZ-INC'}), 'Foo—Bar—Baz—Bzz—Abc—Xyz—Inc') 41 | -------------------------------------------------------------------------------- /boundaries/tests/test_boundary.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from django.contrib.gis.gdal import OGRGeometry 4 | from django.contrib.gis.geos import GEOSGeometry, Point 5 | from django.test import TestCase 6 | 7 | from boundaries.models import Boundary, BoundarySet, Geometry 8 | 9 | 10 | class BoundaryTestCase(TestCase): 11 | maxDiff = None 12 | 13 | def test___str__(self): 14 | self.assertEqual(str(Boundary(set_name='Foo', name='Bar')), 'Bar (Foo)') 15 | 16 | def test_get_absolute_url(self): 17 | self.assertEqual(Boundary(set_id='foo', slug='bar').get_absolute_url(), '/boundaries/foo/bar/') 18 | 19 | def test_boundary_set(self): 20 | self.assertEqual(Boundary(set=BoundarySet(slug='foo')).boundary_set, 'foo') 21 | 22 | def test_boundary_set_name(self): 23 | self.assertEqual(Boundary(set_name='Foo').boundary_set_name, 'Foo') 24 | 25 | def test_get_dicts(self): 26 | boundaries = [ 27 | ('bar', 'foo', 'Bar', 'Foo', 1), 28 | ('bzz', 'baz', 'Bzz', 'Baz', 2), 29 | ] 30 | self.assertEqual(Boundary.get_dicts(boundaries), [ 31 | { 32 | 'url': '/boundaries/foo/bar/', 33 | 'name': 'Bar', 34 | 'related': { 35 | 'boundary_set_url': '/boundary-sets/foo/', 36 | }, 37 | 'boundary_set_name': 'Foo', 38 | 'external_id': 1, 39 | }, 40 | { 41 | 'url': '/boundaries/baz/bzz/', 42 | 'name': 'Bzz', 43 | 'related': { 44 | 'boundary_set_url': '/boundary-sets/baz/', 45 | }, 46 | 'boundary_set_name': 'Baz', 47 | 'external_id': 2, 48 | }, 49 | ]) 50 | 51 | def test_as_dict(self): 52 | self.assertEqual(Boundary( 53 | set_id='foo', 54 | slug='bar', 55 | set_name='Foo', 56 | name='Bar', 57 | metadata={ 58 | 'baz': 'bzz', 59 | }, 60 | external_id=1, 61 | extent=[0, 0, 1, 1], 62 | centroid=Point(0, 1), 63 | start_date=date(2000, 1, 1), 64 | end_date=date(2010, 1, 1), 65 | ).as_dict(), { 66 | 'related': { 67 | 'boundary_set_url': '/boundary-sets/foo/', 68 | 'shape_url': '/boundaries/foo/bar/shape', 69 | 'simple_shape_url': '/boundaries/foo/bar/simple_shape', 70 | 'centroid_url': '/boundaries/foo/bar/centroid', 71 | 'boundaries_url': '/boundaries/foo/', 72 | }, 73 | 'boundary_set_name': 'Foo', 74 | 'name': 'Bar', 75 | 'metadata': { 76 | 'baz': 'bzz', 77 | }, 78 | 'external_id': 1, 79 | 'extent': [0, 0, 1, 1], 80 | 'centroid': { 81 | 'type': 'Point', 82 | 'coordinates': (0.0, 1.0), 83 | }, 84 | 'start_date': '2000-01-01', 85 | 'end_date': '2010-01-01', 86 | }) 87 | 88 | self.assertEqual(Boundary( 89 | set_id='foo', 90 | slug='bar', 91 | ).as_dict(), { 92 | 'related': { 93 | 'boundary_set_url': '/boundary-sets/foo/', 94 | 'shape_url': '/boundaries/foo/bar/shape', 95 | 'simple_shape_url': '/boundaries/foo/bar/simple_shape', 96 | 'centroid_url': '/boundaries/foo/bar/centroid', 97 | 'boundaries_url': '/boundaries/foo/', 98 | }, 99 | 'boundary_set_name': '', 100 | 'name': '', 101 | 'metadata': {}, 102 | 'external_id': '', 103 | 'extent': None, 104 | 'centroid': None, 105 | 'start_date': None, 106 | 'end_date': None, 107 | }) 108 | 109 | def test_prepare_queryset_for_get_dicts(self): 110 | BoundarySet.objects.create(slug='foo', last_updated=date(2000, 1, 1)) 111 | 112 | geom = GEOSGeometry('MULTIPOLYGON(((0 0,0 5,5 5,0 0)))') 113 | Boundary.objects.create( 114 | slug='bar', 115 | set=BoundarySet(slug='foo'), 116 | name='Bar', 117 | set_name='Foo', 118 | external_id=1, 119 | shape=geom, 120 | simple_shape=geom, 121 | ) 122 | # Coerce the django.contrib.gis.db.models.query.GeoValuesListQuerySet. 123 | self.assertEqual(list(Boundary.prepare_queryset_for_get_dicts(Boundary.objects)), [ 124 | ('bar', 'foo', 'Bar', 'Foo', '1'), 125 | ]) 126 | 127 | def test_merge(self): 128 | boundary = Boundary(shape='MULTIPOLYGON (((0 0,0 5,2.5 5.0001,5 5,0 0)))', simple_shape='MULTIPOLYGON (((0 0,0 5,5 5,0 0)))') 129 | boundary.merge(Geometry(OGRGeometry('MULTIPOLYGON (((0 0,5 0,5.0001 2.5,5 5,0 0)))'))) 130 | 131 | self.assertEqual(boundary.shape.ogr.wkt, 'MULTIPOLYGON (((0 0,0 5,2.5 5.0001,5 5,0 0)),((0 0,5 0,5.0001 2.5,5 5,0 0)))') 132 | self.assertEqual(boundary.simple_shape.ogr.wkt, 'MULTIPOLYGON (((0 0,0 5,5 5,0 0)),((0 0,5 0,5 5,0 0)))') 133 | 134 | def test_unary_union(self): 135 | boundary = Boundary(shape='MULTIPOLYGON (((0 0,0 5,2.5 5.0001,5 5,0 0)))') 136 | boundary.unary_union(Geometry(OGRGeometry('MULTIPOLYGON (((0 0,5 0,5 5,0 0)))'))) 137 | 138 | self.assertEqual( 139 | boundary.shape.ogr.difference(OGRGeometry('MULTIPOLYGON (((5 5,5 0,0 0,0 5,2.5 5.0001,5 5)))')).wkt, 140 | 'POLYGON EMPTY', 141 | ) 142 | self.assertEqual( 143 | boundary.simple_shape.ogr.difference(OGRGeometry('MULTIPOLYGON (((5 5,5 0,0 0,0 5,5 5)))')).wkt, 144 | 'POLYGON EMPTY', 145 | ) 146 | -------------------------------------------------------------------------------- /boundaries/tests/test_boundary_detail.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from django.contrib.gis.geos import GEOSGeometry 4 | 5 | from boundaries.models import Boundary, BoundarySet 6 | from boundaries.tests import PrettyTests, ViewsTests, ViewTestCase 7 | 8 | 9 | class BoundaryDetailTestCase(ViewTestCase, ViewsTests, PrettyTests): 10 | maxDiff = None 11 | 12 | url = '/boundaries/inc/foo/' 13 | json = { 14 | 'name': '', 15 | 'related': { 16 | 'boundary_set_url': '/boundary-sets/inc/', 17 | 'simple_shape_url': '/boundaries/inc/foo/simple_shape', 18 | 'boundaries_url': '/boundaries/inc/', 19 | 'shape_url': '/boundaries/inc/foo/shape', 20 | 'centroid_url': '/boundaries/inc/foo/centroid', 21 | }, 22 | 'boundary_set_name': '', 23 | 'centroid': None, 24 | 'extent': None, 25 | 'external_id': '', 26 | 'start_date': None, 27 | 'end_date': None, 28 | 'metadata': {}, 29 | } 30 | 31 | def setUp(self): 32 | BoundarySet.objects.create(slug='inc', last_updated=date(2000, 1, 1)) 33 | 34 | geom = GEOSGeometry('MULTIPOLYGON(((0 0,0 5,5 5,0 0)))') 35 | Boundary.objects.create(slug='foo', set_id='inc', shape=geom, simple_shape=geom) 36 | 37 | def test_404(self): 38 | response = self.client.get('/boundaries/inc/nonexistent/') 39 | self.assertNotFound(response) 40 | 41 | def test_404_on_boundary_set(self): 42 | response = self.client.get('/boundaries/nonexistent/bar/') 43 | self.assertNotFound(response) 44 | -------------------------------------------------------------------------------- /boundaries/tests/test_boundary_geo_detail.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from django.contrib.gis.geos import GEOSGeometry 4 | 5 | from boundaries.models import Boundary, BoundarySet 6 | from boundaries.tests import GeoTests, ViewsTests, ViewTestCase 7 | 8 | 9 | class BoundaryGeoDetailTestCase(ViewTestCase, ViewsTests, GeoTests): 10 | maxDiff = None 11 | 12 | url = '/boundaries/inc/foo/shape' 13 | json = { 14 | 'type': 'MultiPolygon', 15 | 'coordinates': [[[[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [0.0, 0.0]]]], 16 | } 17 | 18 | def setUp(self): 19 | BoundarySet.objects.create(slug='inc', last_updated=date(2000, 1, 1)) 20 | 21 | geom = GEOSGeometry('MULTIPOLYGON(((0 0,0 5,5 5,0 0)))') 22 | Boundary.objects.create(slug='foo', set_id='inc', shape=geom, simple_shape=geom) 23 | -------------------------------------------------------------------------------- /boundaries/tests/test_boundary_list.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from django.contrib.gis.geos import GEOSGeometry 4 | 5 | from boundaries.models import Boundary, BoundarySet 6 | from boundaries.tests import BoundaryListTests, PaginationTests, PrettyTests, ViewsTests, ViewTestCase 7 | 8 | 9 | class BoundaryListTestCase(ViewTestCase, ViewsTests, PrettyTests, PaginationTests, BoundaryListTests): 10 | 11 | """ 12 | Compare to BoundarySetListTestCase (/boundary-sets/) and BoundaryListSetTestCase (/boundaries/inc/) 13 | """ 14 | 15 | maxDiff = None 16 | 17 | url = '/boundaries/' 18 | json = { 19 | 'objects': [], 20 | 'meta': { 21 | 'next': None, 22 | 'total_count': 0, 23 | 'previous': None, 24 | 'limit': 20, 25 | 'offset': 0, 26 | }, 27 | } 28 | 29 | def setUp(self): 30 | BoundarySet.objects.create(slug='inc', last_updated=date(2000, 1, 1)) 31 | 32 | def test_pagination(self): 33 | geom = GEOSGeometry('MULTIPOLYGON(((0 0,0 5,5 5,0 0)))') 34 | Boundary.objects.create(slug='foo', set_id='inc', shape=geom, simple_shape=geom) 35 | Boundary.objects.create(slug='bar', set_id='inc', shape=geom, simple_shape=geom) 36 | Boundary.objects.create(slug='baz', set_id='inc', shape=geom, simple_shape=geom) 37 | 38 | response = self.client.get(self.url, {'limit': 1}) 39 | self.assertResponse(response) 40 | self.assertJSONEqual(response, '{"objects": [{"url": "/boundaries/inc/foo/", "boundary_set_name": "", "external_id": "", "name": "", "related": {"boundary_set_url": "/boundary-sets/inc/"}}], "meta": {"total_count": 3, "related": {"centroids_url": "/boundaries/centroid?limit=1", "simple_shapes_url": "/boundaries/simple_shape?limit=1", "shapes_url": "/boundaries/shape?limit=1"}, "next": "/boundaries/?limit=1&offset=1", "limit": 1, "offset": 0, "previous": null}}') 41 | 42 | response = self.client.get(self.url, {'limit': 1, 'offset': 1}) 43 | self.assertResponse(response) 44 | self.assertJSONEqual(response, '{"objects": [{"url": "/boundaries/inc/bar/", "boundary_set_name": "", "external_id": "", "name": "", "related": {"boundary_set_url": "/boundary-sets/inc/"}}], "meta": {"total_count": 3, "related": {"centroids_url": "/boundaries/centroid?limit=1&offset=1", "simple_shapes_url": "/boundaries/simple_shape?limit=1&offset=1", "shapes_url": "/boundaries/shape?limit=1&offset=1"}, "next": "/boundaries/?limit=1&offset=2", "limit": 1, "offset": 1, "previous": "/boundaries/?limit=1&offset=0"}}') 45 | 46 | response = self.client.get(self.url, {'limit': 1, 'offset': 2}) 47 | self.assertResponse(response) 48 | self.assertJSONEqual(response, '{"objects": [{"url": "/boundaries/inc/baz/", "boundary_set_name": "", "external_id": "", "name": "", "related": {"boundary_set_url": "/boundary-sets/inc/"}}], "meta": {"total_count": 3, "related": {"centroids_url": "/boundaries/centroid?limit=1&offset=2", "simple_shapes_url": "/boundaries/simple_shape?limit=1&offset=2", "shapes_url": "/boundaries/shape?limit=1&offset=2"}, "next": null, "limit": 1, "offset": 2, "previous": "/boundaries/?limit=1&offset=1"}}') 49 | -------------------------------------------------------------------------------- /boundaries/tests/test_boundary_list_filter.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from django.contrib.gis.geos import GEOSGeometry 4 | 5 | from boundaries.models import Boundary, BoundarySet 6 | from boundaries.tests import ViewTestCase 7 | 8 | 9 | class BoundaryListFilterTestCase(ViewTestCase): 10 | 11 | """ 12 | Compare to BoundaryListGeoFilterTestCase (/boundaries/shape) and BoundaryListSetFilterTestCase (/boundaries/inc/) 13 | and BoundaryListSetGeoFilterTestCase (/boundaries/inc/shape) 14 | """ 15 | 16 | maxDiff = None 17 | 18 | url = '/boundaries/' 19 | 20 | def setUp(self): 21 | BoundarySet.objects.create(name='inc', last_updated=date(2000, 1, 1)) 22 | BoundarySet.objects.create(name='abc', last_updated=date(2000, 1, 1)) 23 | BoundarySet.objects.create(name='xyz', last_updated=date(2000, 1, 1)) 24 | 25 | geom = GEOSGeometry('MULTIPOLYGON(((0 0,0 5,5 5,0 0)))') 26 | Boundary.objects.create(slug='foo', set_id='inc', shape=geom, simple_shape=geom, name='Foo', external_id=1) 27 | geom = GEOSGeometry('MULTIPOLYGON(((1 2,1 4,3 4,1 2)))') # coverlaps 28 | Boundary.objects.create(slug='bar', set_id='abc', shape=geom, simple_shape=geom, name='Bar', external_id=2) 29 | geom = GEOSGeometry('MULTIPOLYGON(((0 0,5 0,5 5,0 0)))') # touches 30 | Boundary.objects.create(slug='baz', set_id='xyz', shape=geom, simple_shape=geom) 31 | 32 | def test_filter_name(self): 33 | response = self.client.get(self.url, {'name': 'Foo'}) 34 | self.assertResponse(response) 35 | self.assertJSONEqual(response, '{"objects": [{"url": "/boundaries/inc/foo/", "boundary_set_name": "", "external_id": "1", "name": "Foo", "related": {"boundary_set_url": "/boundary-sets/inc/"}}], "meta": {"total_count": 1, "related": {"centroids_url": "/boundaries/centroid?name=Foo", "simple_shapes_url": "/boundaries/simple_shape?name=Foo", "shapes_url": "/boundaries/shape?name=Foo"}, "next": null, "limit": 20, "offset": 0, "previous": null}}') 36 | 37 | def test_filter_external_id(self): 38 | response = self.client.get(self.url, {'external_id': '2'}) 39 | self.assertResponse(response) 40 | self.assertJSONEqual(response, '{"objects": [{"url": "/boundaries/abc/bar/", "boundary_set_name": "", "external_id": "2", "name": "Bar", "related": {"boundary_set_url": "/boundary-sets/abc/"}}], "meta": {"total_count": 1, "related": {"centroids_url": "/boundaries/centroid?external_id=2", "simple_shapes_url": "/boundaries/simple_shape?external_id=2", "shapes_url": "/boundaries/shape?external_id=2"}, "next": null, "limit": 20, "offset": 0, "previous": null}}') 41 | 42 | def test_filter_type(self): 43 | response = self.client.get(self.url, {'name__istartswith': 'f'}) 44 | self.assertResponse(response) 45 | self.assertJSONEqual(response, '{"objects": [{"url": "/boundaries/inc/foo/", "boundary_set_name": "", "external_id": "1", "name": "Foo", "related": {"boundary_set_url": "/boundary-sets/inc/"}}], "meta": {"total_count": 1, "related": {"centroids_url": "/boundaries/centroid?name__istartswith=f", "simple_shapes_url": "/boundaries/simple_shape?name__istartswith=f", "shapes_url": "/boundaries/shape?name__istartswith=f"}, "next": null, "limit": 20, "offset": 0, "previous": null}}') 46 | 47 | def test_ignore_non_filter_field(self): 48 | response = self.client.get(self.url, {'slug': 'foo'}) 49 | self.assertResponse(response) 50 | self.assertJSONEqual(response, '{"objects": [' 51 | '{"url": "/boundaries/inc/foo/", "boundary_set_name": "", "external_id": "1", "name": "Foo", "related": {"boundary_set_url": "/boundary-sets/inc/"}}, ' 52 | '{"url": "/boundaries/abc/bar/", "boundary_set_name": "", "external_id": "2", "name": "Bar", "related": {"boundary_set_url": "/boundary-sets/abc/"}}, ' 53 | '{"url": "/boundaries/xyz/baz/", "boundary_set_name": "", "external_id": "", "name": "", "related": {"boundary_set_url": "/boundary-sets/xyz/"}}], ' 54 | '"meta": {"total_count": 3, "related": {"centroids_url": "/boundaries/centroid?slug=foo", "simple_shapes_url": "/boundaries/simple_shape?slug=foo", "shapes_url": "/boundaries/shape?slug=foo"}, "next": null, "limit": 20, "offset": 0, "previous": null}}') 55 | 56 | def test_ignore_non_filter_type(self): 57 | response = self.client.get(self.url, {'name__search': 'Foo'}) 58 | self.assertResponse(response) 59 | self.assertJSONEqual(response, '{"objects": [' 60 | '{"url": "/boundaries/inc/foo/", "boundary_set_name": "", "external_id": "1", "name": "Foo", "related": {"boundary_set_url": "/boundary-sets/inc/"}}, ' 61 | '{"url": "/boundaries/abc/bar/", "boundary_set_name": "", "external_id": "2", "name": "Bar", "related": {"boundary_set_url": "/boundary-sets/abc/"}}, ' 62 | '{"url": "/boundaries/xyz/baz/", "boundary_set_name": "", "external_id": "", "name": "", "related": {"boundary_set_url": "/boundary-sets/xyz/"}}], ' 63 | '"meta": {"total_count": 3, "related": {"centroids_url": "/boundaries/centroid?name__search=Foo", "simple_shapes_url": "/boundaries/simple_shape?name__search=Foo", "shapes_url": "/boundaries/shape?name__search=Foo"}, "next": null, "limit": 20, "offset": 0, "previous": null}}') 64 | 65 | def test_filter_value_must_be_valid(self): 66 | response = self.client.get(self.url, {'name__isnull': 'none'}) 67 | self.assertError(response) 68 | self.assertEqual(response.content, b'Invalid filter value') 69 | 70 | def test_filter_intersects(self): 71 | response = self.client.get(self.url, {'intersects': 'abc/bar'}) 72 | self.assertResponse(response) 73 | self.assertJSONEqual(response, '{"meta": {"total_count": 2, "related": {"centroids_url": "/boundaries/centroid?intersects=abc%2Fbar", "simple_shapes_url": "/boundaries/simple_shape?intersects=abc%2Fbar", "shapes_url": "/boundaries/shape?intersects=abc%2Fbar"}, "next": null, "limit": 20, "offset": 0, "previous": null}, "objects": [{"url": "/boundaries/inc/foo/", "boundary_set_name": "", "external_id": "1", "name": "Foo", "related": {"boundary_set_url": "/boundary-sets/inc/"}}, {"url": "/boundaries/abc/bar/", "boundary_set_name": "", "external_id": "2", "name": "Bar", "related": {"boundary_set_url": "/boundary-sets/abc/"}}]}') 74 | 75 | def test_filter_intersects_404(self): 76 | response = self.client.get(self.url, {'intersects': 'inc/nonexistent'}) 77 | self.assertNotFound(response) 78 | 79 | def test_filter_intersects_error(self): 80 | response = self.client.get(self.url, {'intersects': ''}) 81 | self.assertError(response) 82 | self.assertEqual(response.content, b'Invalid value for intersects filter') 83 | 84 | def test_filter_touches(self): 85 | response = self.client.get(self.url, {'touches': 'xyz/baz'}) 86 | self.assertResponse(response) 87 | self.assertJSONEqual(response, '{"meta": {"total_count": 1, "related": {"centroids_url": "/boundaries/centroid?touches=xyz%2Fbaz", "simple_shapes_url": "/boundaries/simple_shape?touches=xyz%2Fbaz", "shapes_url": "/boundaries/shape?touches=xyz%2Fbaz"}, "next": null, "limit": 20, "offset": 0, "previous": null}, "objects": [{"url": "/boundaries/inc/foo/", "boundary_set_name": "", "external_id": "1", "name": "Foo", "related": {"boundary_set_url": "/boundary-sets/inc/"}}]}') 88 | 89 | def test_filter_touches_404(self): 90 | response = self.client.get(self.url, {'touches': 'inc/nonexistent'}) 91 | self.assertNotFound(response) 92 | 93 | def test_filter_touches_error(self): 94 | response = self.client.get(self.url, {'touches': ''}) 95 | self.assertError(response) 96 | self.assertEqual(response.content, b'Invalid value for touches filter') 97 | 98 | def test_contains(self): 99 | response = self.client.get(self.url, {'contains': '1,4'}) 100 | self.assertResponse(response) 101 | self.assertJSONEqual(response, '{"meta": {"total_count": 1, "related": {"centroids_url": "/boundaries/centroid?contains=1%2C4", "simple_shapes_url": "/boundaries/simple_shape?contains=1%2C4", "shapes_url": "/boundaries/shape?contains=1%2C4"}, "next": null, "limit": 20, "offset": 0, "previous": null}, "objects": [{"url": "/boundaries/xyz/baz/", "boundary_set_name": "", "external_id": "", "name": "", "related": {"boundary_set_url": "/boundary-sets/xyz/"}}]}') 102 | 103 | def test_contains_error(self): 104 | response = self.client.get(self.url, {'contains': ''}) 105 | self.assertError(response) 106 | self.assertEqual(response.content, b"""Invalid latitude,longitude '' provided.""") 107 | 108 | def test_near(self): 109 | pass # @note This filter is undocumented. 110 | 111 | def test_sets(self): 112 | response = self.client.get(self.url, {'sets': 'inc,abc'}) 113 | self.assertResponse(response) 114 | self.assertJSONEqual(response, '{"meta": {"total_count": 2, "related": {"centroids_url": "/boundaries/centroid?sets=inc%2Cabc", "simple_shapes_url": "/boundaries/simple_shape?sets=inc%2Cabc", "shapes_url": "/boundaries/shape?sets=inc%2Cabc"}, "next": null, "limit": 20, "offset": 0, "previous": null}, "objects": [{"url": "/boundaries/inc/foo/", "boundary_set_name": "", "external_id": "1", "name": "Foo", "related": {"boundary_set_url": "/boundary-sets/inc/"}}, {"url": "/boundaries/abc/bar/", "boundary_set_name": "", "external_id": "2", "name": "Bar", "related": {"boundary_set_url": "/boundary-sets/abc/"}}]}') 115 | -------------------------------------------------------------------------------- /boundaries/tests/test_boundary_list_geo.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from django.contrib.gis.geos import GEOSGeometry 4 | 5 | from boundaries.models import Boundary, BoundarySet 6 | from boundaries.tests import GeoListTests, GeoTests, ViewsTests, ViewTestCase 7 | 8 | 9 | class BoundaryListGeoTestCase(ViewTestCase, ViewsTests, GeoListTests, GeoTests): 10 | 11 | """ 12 | Compare to BoundaryListSetGeoTestCase (/boundaries/inc/shape) 13 | """ 14 | 15 | maxDiff = None 16 | 17 | url = '/boundaries/shape' 18 | json = { 19 | 'objects': [ 20 | { 21 | 'name': '', 22 | 'shape': { 23 | 'type': 'MultiPolygon', 24 | 'coordinates': [[[[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [0.0, 0.0]]]], 25 | }, 26 | }, 27 | ], 28 | } 29 | 30 | def setUp(self): 31 | BoundarySet.objects.create(slug='inc', last_updated=date(2000, 1, 1)) 32 | 33 | geom = GEOSGeometry('MULTIPOLYGON(((0 0,0 5,5 5,0 0)))') 34 | Boundary.objects.create(slug='foo', set_id='inc', shape=geom, simple_shape=geom) 35 | -------------------------------------------------------------------------------- /boundaries/tests/test_boundary_list_geo_filter.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from django.contrib.gis.geos import GEOSGeometry 4 | 5 | from boundaries.models import Boundary, BoundarySet 6 | from boundaries.tests import ViewTestCase 7 | 8 | 9 | class BoundaryListGeoFilterTestCase(ViewTestCase): 10 | 11 | """ 12 | Compare to BoundaryListFilterTestCase (/boundaries/) and BoundaryListSetFilterTestCase (/boundaries/inc/) 13 | and BoundaryListSetGeoFilterTestCase (/boundaries/inc/shape) 14 | """ 15 | 16 | maxDiff = None 17 | 18 | url = '/boundaries/shape' 19 | 20 | def setUp(self): 21 | BoundarySet.objects.create(name='inc', last_updated=date(2000, 1, 1)) 22 | BoundarySet.objects.create(name='abc', last_updated=date(2000, 1, 1)) 23 | BoundarySet.objects.create(name='xyz', last_updated=date(2000, 1, 1)) 24 | 25 | geom = GEOSGeometry('MULTIPOLYGON(((0 0,0 5,5 5,0 0)))') 26 | Boundary.objects.create(slug='foo', set_id='inc', shape=geom, simple_shape=geom, name='Foo', external_id=1) 27 | geom = GEOSGeometry('MULTIPOLYGON(((1 2,1 4,3 4,1 2)))') # coverlaps 28 | Boundary.objects.create(slug='bar', set_id='abc', shape=geom, simple_shape=geom, name='Bar', external_id=2) 29 | geom = GEOSGeometry('MULTIPOLYGON(((0 0,5 0,5 5,0 0)))') # touches 30 | Boundary.objects.create(slug='baz', set_id='xyz', shape=geom, simple_shape=geom) 31 | 32 | def test_filter_name(self): 33 | response = self.client.get(self.url, {'name': 'Foo'}) 34 | self.assertResponse(response) 35 | self.assertJSONEqual(response, '{"objects": [{"shape": {"type": "MultiPolygon", "coordinates": [[[[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [0.0, 0.0]]]]}, "name": "Foo"}]}') 36 | 37 | def test_filter_external_id(self): 38 | response = self.client.get(self.url, {'external_id': '2'}) 39 | self.assertResponse(response) 40 | self.assertJSONEqual(response, '{"objects": [{"shape": {"type": "MultiPolygon", "coordinates": [[[[1.0, 2.0], [1.0, 4.0], [3.0, 4.0], [1.0, 2.0]]]]}, "name": "Bar"}]}') 41 | 42 | def test_filter_type(self): 43 | response = self.client.get(self.url, {'name__istartswith': 'f'}) 44 | self.assertResponse(response) 45 | self.assertJSONEqual(response, '{"objects": [{"shape": {"type": "MultiPolygon", "coordinates": [[[[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [0.0, 0.0]]]]}, "name": "Foo"}]}') 46 | 47 | def test_ignore_non_filter_field(self): 48 | response = self.client.get(self.url, {'slug': 'foo'}) 49 | self.assertResponse(response) 50 | self.assertJSONEqual(response, '{"objects": [' 51 | '{"shape": {"type": "MultiPolygon", "coordinates": [[[[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [0.0, 0.0]]]]}, "name": "Foo"}, ' 52 | '{"shape": {"type": "MultiPolygon", "coordinates": [[[[1.0, 2.0], [1.0, 4.0], [3.0, 4.0], [1.0, 2.0]]]]}, "name": "Bar"}, ' 53 | '{"shape": {"type": "MultiPolygon", "coordinates": [[[[0.0, 0.0], [5.0, 0.0], [5.0, 5.0], [0.0, 0.0]]]]}, "name": ""}]}') 54 | 55 | def test_ignore_non_filter_type(self): 56 | response = self.client.get(self.url, {'name__search': 'Foo'}) 57 | self.assertResponse(response) 58 | self.assertJSONEqual(response, '{"objects": [' 59 | '{"shape": {"type": "MultiPolygon", "coordinates": [[[[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [0.0, 0.0]]]]}, "name": "Foo"}, ' 60 | '{"shape": {"type": "MultiPolygon", "coordinates": [[[[1.0, 2.0], [1.0, 4.0], [3.0, 4.0], [1.0, 2.0]]]]}, "name": "Bar"}, ' 61 | '{"shape": {"type": "MultiPolygon", "coordinates": [[[[0.0, 0.0], [5.0, 0.0], [5.0, 5.0], [0.0, 0.0]]]]}, "name": ""}]}') 62 | 63 | def test_filter_value_must_be_valid(self): 64 | response = self.client.get(self.url, {'name__isnull': 'none'}) 65 | self.assertError(response) 66 | self.assertEqual(response.content, b'Invalid filter value') 67 | 68 | def test_filter_intersects(self): 69 | response = self.client.get(self.url, {'intersects': 'abc/bar'}) 70 | self.assertResponse(response) 71 | self.assertJSONEqual(response, '{"objects": [{"shape": {"type": "MultiPolygon", "coordinates": [[[[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [0.0, 0.0]]]]}, "name": "Foo"}, {"shape": {"type": "MultiPolygon", "coordinates": [[[[1.0, 2.0], [1.0, 4.0], [3.0, 4.0], [1.0, 2.0]]]]}, "name": "Bar"}]}') 72 | 73 | def test_filter_intersects_404(self): 74 | response = self.client.get(self.url, {'intersects': 'inc/nonexistent'}) 75 | self.assertNotFound(response) 76 | 77 | def test_filter_intersects_error(self): 78 | response = self.client.get(self.url, {'intersects': ''}) 79 | self.assertError(response) 80 | self.assertEqual(response.content, b'Invalid value for intersects filter') 81 | 82 | def test_filter_touches(self): 83 | response = self.client.get(self.url, {'touches': 'xyz/baz'}) 84 | self.assertResponse(response) 85 | self.assertJSONEqual(response, '{"objects": [{"shape": {"type": "MultiPolygon", "coordinates": [[[[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [0.0, 0.0]]]]}, "name": "Foo"}]}') 86 | 87 | def test_filter_touches_404(self): 88 | response = self.client.get(self.url, {'touches': 'inc/nonexistent'}) 89 | self.assertNotFound(response) 90 | 91 | def test_filter_touches_error(self): 92 | response = self.client.get(self.url, {'touches': ''}) 93 | self.assertError(response) 94 | self.assertEqual(response.content, b'Invalid value for touches filter') 95 | 96 | def test_contains(self): 97 | response = self.client.get(self.url, {'contains': '1,4'}) 98 | self.assertResponse(response) 99 | self.assertJSONEqual(response, '{"objects": [{"shape": {"type": "MultiPolygon", "coordinates": [[[[0.0, 0.0], [5.0, 0.0], [5.0, 5.0], [0.0, 0.0]]]]}, "name": ""}]}') 100 | 101 | def test_contains_error(self): 102 | response = self.client.get(self.url, {'contains': ''}) 103 | self.assertError(response) 104 | self.assertEqual(response.content, b"""Invalid latitude,longitude '' provided.""") 105 | 106 | def test_near(self): 107 | pass # @note This filter is undocumented. 108 | 109 | def test_sets(self): 110 | response = self.client.get(self.url, {'sets': 'inc,abc'}) 111 | self.assertResponse(response) 112 | self.assertJSONEqual(response, '{"objects": [{"shape": {"type": "MultiPolygon", "coordinates": [[[[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [0.0, 0.0]]]]}, "name": "Foo"}, {"shape": {"type": "MultiPolygon", "coordinates": [[[[1.0, 2.0], [1.0, 4.0], [3.0, 4.0], [1.0, 2.0]]]]}, "name": "Bar"}]}') 113 | -------------------------------------------------------------------------------- /boundaries/tests/test_boundary_list_set.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from django.contrib.gis.geos import GEOSGeometry 4 | 5 | from boundaries.models import Boundary, BoundarySet 6 | from boundaries.tests import BoundaryListTests, PaginationTests, PrettyTests, ViewsTests, ViewTestCase 7 | 8 | 9 | class BoundaryListSetTestCase(ViewTestCase, ViewsTests, PrettyTests, PaginationTests, BoundaryListTests): 10 | 11 | """ 12 | Compare to BoundarySetListTestCase (/boundary-sets/) and BoundaryListTestCase (/boundaries/) 13 | """ 14 | 15 | maxDiff = None 16 | 17 | url = '/boundaries/inc/' 18 | json = { 19 | 'objects': [], 20 | 'meta': { 21 | 'next': None, 22 | 'total_count': 0, 23 | 'previous': None, 24 | 'limit': 20, 25 | 'offset': 0, 26 | }, 27 | } 28 | 29 | def setUp(self): 30 | BoundarySet.objects.create(slug='inc', last_updated=date(2000, 1, 1)) 31 | 32 | def test_pagination(self): 33 | geom = GEOSGeometry('MULTIPOLYGON(((0 0,0 5,5 5,0 0)))') 34 | Boundary.objects.create(slug='foo', set_id='inc', shape=geom, simple_shape=geom) 35 | Boundary.objects.create(slug='bar', set_id='inc', shape=geom, simple_shape=geom) 36 | Boundary.objects.create(slug='baz', set_id='inc', shape=geom, simple_shape=geom) 37 | 38 | response = self.client.get(self.url, {'limit': 1}) 39 | self.assertResponse(response) 40 | self.assertJSONEqual(response, '{"objects": [{"url": "/boundaries/inc/baz/", "boundary_set_name": "", "external_id": "", "name": "", "related": {"boundary_set_url": "/boundary-sets/inc/"}}], "meta": {"total_count": 3, "related": {"centroids_url": "/boundaries/inc/centroid?limit=1", "simple_shapes_url": "/boundaries/inc/simple_shape?limit=1", "shapes_url": "/boundaries/inc/shape?limit=1"}, "next": "/boundaries/inc/?limit=1&offset=1", "limit": 1, "offset": 0, "previous": null}}') 41 | 42 | response = self.client.get(self.url, {'limit': 1, 'offset': 1}) 43 | self.assertResponse(response) 44 | self.assertJSONEqual(response, '{"objects": [{"url": "/boundaries/inc/bar/", "boundary_set_name": "", "external_id": "", "name": "", "related": {"boundary_set_url": "/boundary-sets/inc/"}}], "meta": {"total_count": 3, "related": {"centroids_url": "/boundaries/inc/centroid?limit=1&offset=1", "simple_shapes_url": "/boundaries/inc/simple_shape?limit=1&offset=1", "shapes_url": "/boundaries/inc/shape?limit=1&offset=1"}, "next": "/boundaries/inc/?limit=1&offset=2", "limit": 1, "offset": 1, "previous": "/boundaries/inc/?limit=1&offset=0"}}') 45 | 46 | response = self.client.get(self.url, {'limit': 1, 'offset': 2}) 47 | self.assertResponse(response) 48 | self.assertJSONEqual(response, '{"objects": [{"url": "/boundaries/inc/foo/", "boundary_set_name": "", "external_id": "", "name": "", "related": {"boundary_set_url": "/boundary-sets/inc/"}}], "meta": {"total_count": 3, "related": {"centroids_url": "/boundaries/inc/centroid?limit=1&offset=2", "simple_shapes_url": "/boundaries/inc/simple_shape?limit=1&offset=2", "shapes_url": "/boundaries/inc/shape?limit=1&offset=2"}, "next": null, "limit": 1, "offset": 2, "previous": "/boundaries/inc/?limit=1&offset=1"}}') 49 | 50 | def test_404_on_boundary_set(self): 51 | response = self.client.get('/boundaries/nonexistent/') 52 | self.assertNotFound(response) 53 | -------------------------------------------------------------------------------- /boundaries/tests/test_boundary_list_set_filter.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from django.contrib.gis.geos import GEOSGeometry 4 | 5 | from boundaries.models import Boundary, BoundarySet 6 | from boundaries.tests import ViewTestCase 7 | 8 | 9 | class BoundaryListSetFilterTestCase(ViewTestCase): 10 | 11 | """ 12 | Compare to BoundaryListFilterTestCase (/boundaries/) and BoundaryListGeoFilterTestCase (/boundaries/shape) 13 | and BoundaryListSetGeoFilterTestCase (/boundaries/inc/shape) 14 | """ 15 | 16 | maxDiff = None 17 | 18 | url = '/boundaries/inc/' 19 | 20 | def setUp(self): 21 | BoundarySet.objects.create(name='inc', last_updated=date(2000, 1, 1)) 22 | BoundarySet.objects.create(name='abc', last_updated=date(2000, 1, 1)) 23 | BoundarySet.objects.create(name='xyz', last_updated=date(2000, 1, 1)) 24 | 25 | geom = GEOSGeometry('MULTIPOLYGON(((0 0,0 5,5 5,0 0)))') 26 | Boundary.objects.create(slug='foo', set_id='inc', shape=geom, simple_shape=geom, name='Foo', external_id=1) 27 | geom = GEOSGeometry('MULTIPOLYGON(((1 2,1 4,3 4,1 2)))') # coverlaps 28 | Boundary.objects.create(slug='bar', set_id='inc', shape=geom, simple_shape=geom, name='Bar', external_id=2) 29 | geom = GEOSGeometry('MULTIPOLYGON(((0 0,5 0,5 5,0 0)))') # touches 30 | Boundary.objects.create(slug='baz', set_id='inc', shape=geom, simple_shape=geom) 31 | 32 | # Boundaries that should not match. 33 | geom = GEOSGeometry('MULTIPOLYGON(((1 2,1 4,3 4,1 2)))') 34 | Boundary.objects.create(slug='bar', set_id='abc', shape=geom, simple_shape=geom, name='Bar', external_id=2) 35 | geom = GEOSGeometry('MULTIPOLYGON(((0 0,5 0,5 5,0 0)))') 36 | Boundary.objects.create(slug='baz', set_id='xyz', shape=geom, simple_shape=geom) 37 | 38 | def test_filter_name(self): 39 | response = self.client.get(self.url, {'name': 'Foo'}) 40 | self.assertResponse(response) 41 | self.assertJSONEqual(response, '{"objects": [{"url": "/boundaries/inc/foo/", "boundary_set_name": "", "external_id": "1", "name": "Foo", "related": {"boundary_set_url": "/boundary-sets/inc/"}}], "meta": {"total_count": 1, "related": {"centroids_url": "/boundaries/inc/centroid?name=Foo", "simple_shapes_url": "/boundaries/inc/simple_shape?name=Foo", "shapes_url": "/boundaries/inc/shape?name=Foo"}, "next": null, "limit": 20, "offset": 0, "previous": null}}') 42 | 43 | def test_filter_external_id(self): 44 | response = self.client.get(self.url, {'external_id': '2'}) 45 | self.assertResponse(response) 46 | self.assertJSONEqual(response, '{"objects": [{"url": "/boundaries/inc/bar/", "boundary_set_name": "", "external_id": "2", "name": "Bar", "related": {"boundary_set_url": "/boundary-sets/inc/"}}], "meta": {"total_count": 1, "related": {"centroids_url": "/boundaries/inc/centroid?external_id=2", "simple_shapes_url": "/boundaries/inc/simple_shape?external_id=2", "shapes_url": "/boundaries/inc/shape?external_id=2"}, "next": null, "limit": 20, "offset": 0, "previous": null}}') 47 | 48 | def test_filter_type(self): 49 | response = self.client.get(self.url, {'name__istartswith': 'f'}) 50 | self.assertResponse(response) 51 | self.assertJSONEqual(response, '{"objects": [{"url": "/boundaries/inc/foo/", "boundary_set_name": "", "external_id": "1", "name": "Foo", "related": {"boundary_set_url": "/boundary-sets/inc/"}}], "meta": {"total_count": 1, "related": {"centroids_url": "/boundaries/inc/centroid?name__istartswith=f", "simple_shapes_url": "/boundaries/inc/simple_shape?name__istartswith=f", "shapes_url": "/boundaries/inc/shape?name__istartswith=f"}, "next": null, "limit": 20, "offset": 0, "previous": null}}') 52 | 53 | def test_ignore_non_filter_field(self): 54 | response = self.client.get(self.url, {'slug': 'foo'}) 55 | self.assertResponse(response) 56 | self.assertJSONEqual(response, '{"objects": [' 57 | '{"url": "/boundaries/inc/baz/", "boundary_set_name": "", "external_id": "", "name": "", "related": {"boundary_set_url": "/boundary-sets/inc/"}}, ' 58 | '{"url": "/boundaries/inc/bar/", "boundary_set_name": "", "external_id": "2", "name": "Bar", "related": {"boundary_set_url": "/boundary-sets/inc/"}}, ' 59 | '{"url": "/boundaries/inc/foo/", "boundary_set_name": "", "external_id": "1", "name": "Foo", "related": {"boundary_set_url": "/boundary-sets/inc/"}}], ' 60 | '"meta": {"total_count": 3, "related": {"centroids_url": "/boundaries/inc/centroid?slug=foo", "simple_shapes_url": "/boundaries/inc/simple_shape?slug=foo", "shapes_url": "/boundaries/inc/shape?slug=foo"}, "next": null, "limit": 20, "offset": 0, "previous": null}}') 61 | 62 | def test_ignore_non_filter_type(self): 63 | response = self.client.get(self.url, {'name__search': 'Foo'}) 64 | self.assertResponse(response) 65 | self.assertJSONEqual(response, '{"objects": [' 66 | '{"url": "/boundaries/inc/baz/", "boundary_set_name": "", "external_id": "", "name": "", "related": {"boundary_set_url": "/boundary-sets/inc/"}}, ' 67 | '{"url": "/boundaries/inc/bar/", "boundary_set_name": "", "external_id": "2", "name": "Bar", "related": {"boundary_set_url": "/boundary-sets/inc/"}}, ' 68 | '{"url": "/boundaries/inc/foo/", "boundary_set_name": "", "external_id": "1", "name": "Foo", "related": {"boundary_set_url": "/boundary-sets/inc/"}}], ' 69 | '"meta": {"total_count": 3, "related": {"centroids_url": "/boundaries/inc/centroid?name__search=Foo", "simple_shapes_url": "/boundaries/inc/simple_shape?name__search=Foo", "shapes_url": "/boundaries/inc/shape?name__search=Foo"}, "next": null, "limit": 20, "offset": 0, "previous": null}}') 70 | 71 | def test_filter_value_must_be_valid(self): 72 | response = self.client.get(self.url, {'name__isnull': 'none'}) 73 | self.assertError(response) 74 | self.assertEqual(response.content, b'Invalid filter value') 75 | 76 | def test_filter_intersects(self): 77 | response = self.client.get(self.url, {'intersects': 'inc/bar'}) 78 | self.assertResponse(response) 79 | self.assertJSONEqual(response, '{"meta": {"total_count": 2, "related": {"centroids_url": "/boundaries/inc/centroid?intersects=inc%2Fbar", "simple_shapes_url": "/boundaries/inc/simple_shape?intersects=inc%2Fbar", "shapes_url": "/boundaries/inc/shape?intersects=inc%2Fbar"}, "next": null, "limit": 20, "offset": 0, "previous": null}, "objects": [{"url": "/boundaries/inc/bar/", "boundary_set_name": "", "external_id": "2", "name": "Bar", "related": {"boundary_set_url": "/boundary-sets/inc/"}}, {"url": "/boundaries/inc/foo/", "boundary_set_name": "", "external_id": "1", "name": "Foo", "related": {"boundary_set_url": "/boundary-sets/inc/"}}]}') 80 | 81 | def test_filter_intersects_404(self): 82 | response = self.client.get(self.url, {'intersects': 'inc/nonexistent'}) 83 | self.assertNotFound(response) 84 | 85 | def test_filter_intersects_error(self): 86 | response = self.client.get(self.url, {'intersects': ''}) 87 | self.assertError(response) 88 | self.assertEqual(response.content, b'Invalid value for intersects filter') 89 | 90 | def test_filter_touches(self): 91 | response = self.client.get(self.url, {'touches': 'inc/baz'}) 92 | self.assertResponse(response) 93 | self.assertJSONEqual(response, '{"meta": {"total_count": 1, "related": {"centroids_url": "/boundaries/inc/centroid?touches=inc%2Fbaz", "simple_shapes_url": "/boundaries/inc/simple_shape?touches=inc%2Fbaz", "shapes_url": "/boundaries/inc/shape?touches=inc%2Fbaz"}, "next": null, "limit": 20, "offset": 0, "previous": null}, "objects": [{"url": "/boundaries/inc/foo/", "boundary_set_name": "", "external_id": "1", "name": "Foo", "related": {"boundary_set_url": "/boundary-sets/inc/"}}]}') 94 | 95 | def test_filter_touches_404(self): 96 | response = self.client.get(self.url, {'touches': 'inc/nonexistent'}) 97 | self.assertNotFound(response) 98 | 99 | def test_filter_touches_error(self): 100 | response = self.client.get(self.url, {'touches': ''}) 101 | self.assertError(response) 102 | self.assertEqual(response.content, b'Invalid value for touches filter') 103 | 104 | def test_contains(self): 105 | response = self.client.get(self.url, {'contains': '1,4'}) 106 | self.assertResponse(response) 107 | self.assertJSONEqual(response, '{"meta": {"total_count": 1, "related": {"centroids_url": "/boundaries/inc/centroid?contains=1%2C4", "simple_shapes_url": "/boundaries/inc/simple_shape?contains=1%2C4", "shapes_url": "/boundaries/inc/shape?contains=1%2C4"}, "next": null, "limit": 20, "offset": 0, "previous": null}, "objects": [{"url": "/boundaries/inc/baz/", "boundary_set_name": "", "external_id": "", "name": "", "related": {"boundary_set_url": "/boundary-sets/inc/"}}]}') 108 | 109 | def test_contains_error(self): 110 | response = self.client.get(self.url, {'contains': ''}) 111 | self.assertError(response) 112 | self.assertEqual(response.content, b"""Invalid latitude,longitude '' provided.""") 113 | 114 | def test_near(self): 115 | pass # @note This filter is undocumented. 116 | -------------------------------------------------------------------------------- /boundaries/tests/test_boundary_list_set_geo.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from django.contrib.gis.geos import GEOSGeometry 4 | 5 | from boundaries.models import Boundary, BoundarySet 6 | from boundaries.tests import GeoListTests, GeoTests, ViewsTests, ViewTestCase 7 | 8 | 9 | class BoundaryListSetGeoTestCase(ViewTestCase, ViewsTests, GeoListTests, GeoTests): 10 | 11 | """ 12 | Compare to BoundaryListGeoTestCase (/boundaries/shape) 13 | """ 14 | 15 | maxDiff = None 16 | 17 | url = '/boundaries/inc/shape' 18 | json = { 19 | 'objects': [ 20 | { 21 | 'name': '', 22 | 'shape': { 23 | 'type': 'MultiPolygon', 24 | 'coordinates': [[[[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [0.0, 0.0]]]], 25 | }, 26 | }, 27 | ], 28 | } 29 | 30 | def setUp(self): 31 | BoundarySet.objects.create(slug='inc', last_updated=date(2000, 1, 1)) 32 | 33 | geom = GEOSGeometry('MULTIPOLYGON(((0 0,0 5,5 5,0 0)))') 34 | Boundary.objects.create(slug='foo', set_id='inc', shape=geom, simple_shape=geom) 35 | -------------------------------------------------------------------------------- /boundaries/tests/test_boundary_list_set_geo_filter.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from django.contrib.gis.geos import GEOSGeometry 4 | 5 | from boundaries.models import Boundary, BoundarySet 6 | from boundaries.tests import ViewTestCase 7 | 8 | 9 | class BoundaryListSetGeoFilterTestCase(ViewTestCase): 10 | 11 | """ 12 | Compare to BoundaryListFilterTestCase (/boundaries/) and BoundaryListGeoFilterTestCase (/boundaries/shape) 13 | and BoundaryListSetFilterTestCase (/boundaries/inc/) 14 | """ 15 | 16 | maxDiff = None 17 | 18 | url = '/boundaries/inc/shape' 19 | 20 | def setUp(self): 21 | BoundarySet.objects.create(name='inc', last_updated=date(2000, 1, 1)) 22 | BoundarySet.objects.create(name='abc', last_updated=date(2000, 1, 1)) 23 | BoundarySet.objects.create(name='xyz', last_updated=date(2000, 1, 1)) 24 | 25 | geom = GEOSGeometry('MULTIPOLYGON(((0 0,0 5,5 5,0 0)))') 26 | Boundary.objects.create(slug='foo', set_id='inc', shape=geom, simple_shape=geom, name='Foo', external_id=1) 27 | geom = GEOSGeometry('MULTIPOLYGON(((1 2,1 4,3 4,1 2)))') # coverlaps 28 | Boundary.objects.create(slug='bar', set_id='inc', shape=geom, simple_shape=geom, name='Bar', external_id=2) 29 | geom = GEOSGeometry('MULTIPOLYGON(((0 0,5 0,5 5,0 0)))') # touches 30 | Boundary.objects.create(slug='baz', set_id='inc', shape=geom, simple_shape=geom) 31 | 32 | # Boundaries that should not match. 33 | geom = GEOSGeometry('MULTIPOLYGON(((1 2,1 4,3 4,1 2)))') 34 | Boundary.objects.create(slug='bar', set_id='abc', shape=geom, simple_shape=geom, name='Bar', external_id=2) 35 | geom = GEOSGeometry('MULTIPOLYGON(((0 0,5 0,5 5,0 0)))') 36 | Boundary.objects.create(slug='baz', set_id='xyz', shape=geom, simple_shape=geom) 37 | 38 | def test_filter_name(self): 39 | response = self.client.get(self.url, {'name': 'Foo'}) 40 | self.assertResponse(response) 41 | self.assertJSONEqual(response, '{"objects": [{"shape": {"type": "MultiPolygon", "coordinates": [[[[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [0.0, 0.0]]]]}, "name": "Foo"}]}') 42 | 43 | def test_filter_external_id(self): 44 | response = self.client.get(self.url, {'external_id': '2'}) 45 | self.assertResponse(response) 46 | self.assertJSONEqual(response, '{"objects": [{"shape": {"type": "MultiPolygon", "coordinates": [[[[1.0, 2.0], [1.0, 4.0], [3.0, 4.0], [1.0, 2.0]]]]}, "name": "Bar"}]}') 47 | 48 | def test_filter_type(self): 49 | response = self.client.get(self.url, {'name__istartswith': 'f'}) 50 | self.assertResponse(response) 51 | self.assertJSONEqual(response, '{"objects": [{"shape": {"type": "MultiPolygon", "coordinates": [[[[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [0.0, 0.0]]]]}, "name": "Foo"}]}') 52 | 53 | def test_ignore_non_filter_field(self): 54 | response = self.client.get(self.url, {'slug': 'foo'}) 55 | self.assertResponse(response) 56 | self.assertJSONEqual(response, '{"objects": [' 57 | '{"shape": {"type": "MultiPolygon", "coordinates": [[[[0.0, 0.0], [5.0, 0.0], [5.0, 5.0], [0.0, 0.0]]]]}, "name": ""}, ' 58 | '{"shape": {"type": "MultiPolygon", "coordinates": [[[[1.0, 2.0], [1.0, 4.0], [3.0, 4.0], [1.0, 2.0]]]]}, "name": "Bar"}, ' 59 | '{"shape": {"type": "MultiPolygon", "coordinates": [[[[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [0.0, 0.0]]]]}, "name": "Foo"}]}') 60 | 61 | def test_ignore_non_filter_type(self): 62 | response = self.client.get(self.url, {'name__search': 'Foo'}) 63 | self.assertResponse(response) 64 | self.assertJSONEqual(response, '{"objects": [' 65 | '{"shape": {"type": "MultiPolygon", "coordinates": [[[[0.0, 0.0], [5.0, 0.0], [5.0, 5.0], [0.0, 0.0]]]]}, "name": ""}, ' 66 | '{"shape": {"type": "MultiPolygon", "coordinates": [[[[1.0, 2.0], [1.0, 4.0], [3.0, 4.0], [1.0, 2.0]]]]}, "name": "Bar"}, ' 67 | '{"shape": {"type": "MultiPolygon", "coordinates": [[[[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [0.0, 0.0]]]]}, "name": "Foo"}]}') 68 | 69 | def test_filter_value_must_be_valid(self): 70 | response = self.client.get(self.url, {'name__isnull': 'none'}) 71 | self.assertError(response) 72 | self.assertEqual(response.content, b'Invalid filter value') 73 | 74 | def test_filter_intersects(self): 75 | response = self.client.get(self.url, {'intersects': 'abc/bar'}) 76 | self.assertResponse(response) 77 | self.assertJSONEqual(response, '{"objects": [{"shape": {"type": "MultiPolygon", "coordinates": [[[[1.0, 2.0], [1.0, 4.0], [3.0, 4.0], [1.0, 2.0]]]]}, "name": "Bar"}, {"shape": {"type": "MultiPolygon", "coordinates": [[[[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [0.0, 0.0]]]]}, "name": "Foo"}]}') 78 | 79 | def test_filter_intersects_404(self): 80 | response = self.client.get(self.url, {'intersects': 'inc/nonexistent'}) 81 | self.assertNotFound(response) 82 | 83 | def test_filter_intersects_error(self): 84 | response = self.client.get(self.url, {'intersects': ''}) 85 | self.assertError(response) 86 | self.assertEqual(response.content, b'Invalid value for intersects filter') 87 | 88 | def test_filter_touches(self): 89 | response = self.client.get(self.url, {'touches': 'xyz/baz'}) 90 | self.assertResponse(response) 91 | self.assertJSONEqual(response, '{"objects": [{"shape": {"type": "MultiPolygon", "coordinates": [[[[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [0.0, 0.0]]]]}, "name": "Foo"}]}') 92 | 93 | def test_filter_touches_404(self): 94 | response = self.client.get(self.url, {'touches': 'inc/nonexistent'}) 95 | self.assertNotFound(response) 96 | 97 | def test_filter_touches_error(self): 98 | response = self.client.get(self.url, {'touches': ''}) 99 | self.assertError(response) 100 | self.assertEqual(response.content, b'Invalid value for touches filter') 101 | 102 | def test_contains(self): 103 | response = self.client.get(self.url, {'contains': '1,4'}) 104 | self.assertResponse(response) 105 | self.assertJSONEqual(response, '{"objects": [{"shape": {"type": "MultiPolygon", "coordinates": [[[[0.0, 0.0], [5.0, 0.0], [5.0, 5.0], [0.0, 0.0]]]]}, "name": ""}]}') 106 | 107 | def test_contains_error(self): 108 | response = self.client.get(self.url, {'contains': ''}) 109 | self.assertError(response) 110 | self.assertEqual(response.content, b"""Invalid latitude,longitude '' provided.""") 111 | 112 | def test_near(self): 113 | pass # @note This filter is undocumented. 114 | -------------------------------------------------------------------------------- /boundaries/tests/test_boundary_set.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from django.test import TestCase 4 | 5 | from boundaries.models import BoundarySet 6 | 7 | 8 | class BoundarySetTestCase(TestCase): 9 | maxDiff = None 10 | 11 | def test_save_should_set_default_slug(self): 12 | boundary_set = BoundarySet.objects.create(name='Foo Bar', last_updated=date(2000, 1, 1)) 13 | self.assertEqual(boundary_set.slug, 'foo-bar') 14 | 15 | def test_save_should_not_overwrite_slug(self): 16 | boundary_set = BoundarySet.objects.create(name='Foo Bar', last_updated=date(2000, 1, 1), slug='baz') 17 | self.assertEqual(boundary_set.slug, 'baz') 18 | 19 | def test___str__(self): 20 | self.assertEqual(str(BoundarySet(name='Foo Bar')), 'Foo Bar') 21 | 22 | def test_get_dicts(self): 23 | sets = [ 24 | BoundarySet(name='Foo', slug='foo', domain='Fooland'), 25 | BoundarySet(name='Bar', slug='bar', domain='Barland'), 26 | ] 27 | self.assertEqual(BoundarySet.get_dicts(sets), [ 28 | { 29 | 'url': '/boundary-sets/foo/', 30 | 'related': { 31 | 'boundaries_url': '/boundaries/foo/', 32 | }, 33 | 'name': 'Foo', 34 | 'domain': 'Fooland', 35 | }, 36 | { 37 | 'url': '/boundary-sets/bar/', 38 | 'related': { 39 | 'boundaries_url': '/boundaries/bar/', 40 | }, 41 | 'name': 'Bar', 42 | 'domain': 'Barland', 43 | }, 44 | ]) 45 | 46 | def test_as_dict(self): 47 | self.assertEqual(BoundarySet( 48 | slug='foo', 49 | name='Foo', 50 | singular='Foe', 51 | authority='King', 52 | domain='Fooland', 53 | source_url='http://example.com/', 54 | notes='Noted', 55 | licence_url='http://example.com/licence', 56 | last_updated=date(2000, 1, 1), 57 | extent=[0, 0, 1, 1], 58 | start_date=date(2000, 1, 1), 59 | end_date=date(2010, 1, 1), 60 | extra={ 61 | 'bar': 'baz', 62 | }, 63 | ).as_dict(), { 64 | 'related': { 65 | 'boundaries_url': '/boundaries/foo/', 66 | }, 67 | 'name_plural': 'Foo', 68 | 'name_singular': 'Foe', 69 | 'authority': 'King', 70 | 'domain': 'Fooland', 71 | 'source_url': 'http://example.com/', 72 | 'notes': 'Noted', 73 | 'licence_url': 'http://example.com/licence', 74 | 'last_updated': '2000-01-01', 75 | 'extent': [0, 0, 1, 1], 76 | 'start_date': '2000-01-01', 77 | 'end_date': '2010-01-01', 78 | 'extra': { 79 | 'bar': 'baz', 80 | }, 81 | }) 82 | 83 | self.assertEqual(BoundarySet( 84 | slug='foo', 85 | ).as_dict(), { 86 | 'related': { 87 | 'boundaries_url': '/boundaries/foo/', 88 | }, 89 | 'name_plural': '', 90 | 'name_singular': '', 91 | 'authority': '', 92 | 'domain': '', 93 | 'source_url': '', 94 | 'notes': '', 95 | 'licence_url': '', 96 | 'last_updated': None, 97 | 'extent': None, 98 | 'start_date': None, 99 | 'end_date': None, 100 | 'extra': {}, 101 | }) 102 | 103 | def test_extend(self): 104 | boundary_set = BoundarySet.objects.create(name='Foo Bar', last_updated=date(2000, 1, 1)) 105 | boundary_set.extent = [None, None, None, None] 106 | boundary_set.extend((0.0, 0.0, 1.0, 1.0)) 107 | self.assertEqual(boundary_set.extent, [0.0, 0.0, 1.0, 1.0]) 108 | boundary_set.extend((0.25, 0.25, 0.75, 0.75)) 109 | self.assertEqual(boundary_set.extent, [0.0, 0.0, 1.0, 1.0]) 110 | boundary_set.extend((-1.0, -1.0, 2.0, 2.0)) 111 | self.assertEqual(boundary_set.extent, [-1.0, -1.0, 2.0, 2.0]) 112 | -------------------------------------------------------------------------------- /boundaries/tests/test_boundary_set_detail.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from boundaries.models import BoundarySet 4 | from boundaries.tests import PrettyTests, ViewsTests, ViewTestCase 5 | 6 | 7 | class BoundarySetDetailTestCase(ViewTestCase, ViewsTests, PrettyTests): 8 | maxDiff = None 9 | 10 | url = '/boundary-sets/inc/' 11 | json = { 12 | 'domain': '', 13 | 'licence_url': '', 14 | 'end_date': None, 15 | 'name_singular': '', 16 | 'extra': {}, 17 | 'notes': '', 18 | 'authority': '', 19 | 'source_url': '', 20 | 'name_plural': '', 21 | 'extent': None, 22 | 'last_updated': '2000-01-01', 23 | 'start_date': None, 24 | 'related': { 25 | 'boundaries_url': '/boundaries/inc/' 26 | }, 27 | } 28 | 29 | def setUp(self): 30 | BoundarySet.objects.create(slug='inc', last_updated=date(2000, 1, 1)) 31 | 32 | def test_404(self): 33 | response = self.client.get('/boundary-sets/nonexistent/') 34 | self.assertNotFound(response) 35 | -------------------------------------------------------------------------------- /boundaries/tests/test_boundary_set_list.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from boundaries.models import BoundarySet 4 | from boundaries.tests import PaginationTests, PrettyTests, ViewsTests, ViewTestCase 5 | 6 | 7 | class BoundarySetListTestCase(ViewTestCase, ViewsTests, PrettyTests, PaginationTests): 8 | 9 | """ 10 | Compare to BoundaryListTestCase (/boundaries/) and BoundaryListSetTestCase (/boundaries/inc/) 11 | """ 12 | 13 | maxDiff = None 14 | 15 | url = '/boundary-sets/' 16 | json = { 17 | 'objects': [], 18 | 'meta': { 19 | 'next': None, 20 | 'total_count': 0, 21 | 'previous': None, 22 | 'limit': 20, 23 | 'offset': 0, 24 | }, 25 | } 26 | 27 | def test_pagination(self): 28 | BoundarySet.objects.create(name='Foo', last_updated=date(2000, 1, 1)) 29 | BoundarySet.objects.create(name='Bar', last_updated=date(2000, 1, 1)) 30 | BoundarySet.objects.create(name='Baz', last_updated=date(2000, 1, 1)) 31 | 32 | response = self.client.get(self.url, {'limit': 1}) 33 | self.assertResponse(response) 34 | self.assertJSONEqual(response, '{"objects": [{"url": "/boundary-sets/bar/", "domain": "", "name": "Bar", "related": {"boundaries_url": "/boundaries/bar/"}}], "meta": {"next": "/boundary-sets/?limit=1&offset=1", "total_count": 3, "previous": null, "limit": 1, "offset": 0}}') 35 | 36 | response = self.client.get(self.url, {'limit': 1, 'offset': 1}) 37 | self.assertResponse(response) 38 | self.assertJSONEqual(response, '{"objects": [{"url": "/boundary-sets/baz/", "domain": "", "name": "Baz", "related": {"boundaries_url": "/boundaries/baz/"}}], "meta": {"next": "/boundary-sets/?limit=1&offset=2", "total_count": 3, "previous": "/boundary-sets/?limit=1&offset=0", "limit": 1, "offset": 1}}') 39 | 40 | response = self.client.get(self.url, {'limit': 1, 'offset': 2}) 41 | self.assertResponse(response) 42 | self.assertJSONEqual(response, '{"objects": [{"url": "/boundary-sets/foo/", "domain": "", "name": "Foo", "related": {"boundaries_url": "/boundaries/foo/"}}], "meta": {"next": null, "total_count": 3, "previous": "/boundary-sets/?limit=1&offset=1", "limit": 1, "offset": 2}}') 43 | -------------------------------------------------------------------------------- /boundaries/tests/test_boundary_set_list_filter.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from boundaries.models import BoundarySet 4 | from boundaries.tests import ViewTestCase 5 | 6 | 7 | class BoundarySetListFilterTestCase(ViewTestCase): 8 | maxDiff = None 9 | 10 | url = '/boundary-sets/' 11 | 12 | def setUp(self): 13 | BoundarySet.objects.create(name='Foo', last_updated=date(2000, 1, 1), domain='Fooland', authority='King') 14 | BoundarySet.objects.create(name='Bar', last_updated=date(2000, 1, 1), domain='Barland', authority='Queen') 15 | BoundarySet.objects.create(name='Baz', last_updated=date(2000, 1, 1)) 16 | 17 | def test_filter_name(self): 18 | response = self.client.get(self.url, {'name': 'Foo'}) 19 | self.assertResponse(response) 20 | self.assertJSONEqual(response, '{"objects": [{"url": "/boundary-sets/foo/", "domain": "Fooland", "name": "Foo", "related": {"boundaries_url": "/boundaries/foo/"}}], "meta": {"next": null, "total_count": 1, "previous": null, "limit": 20, "offset": 0}}') 21 | 22 | def test_filter_domain(self): 23 | response = self.client.get(self.url, {'domain': 'Barland'}) 24 | self.assertResponse(response) 25 | self.assertJSONEqual(response, '{"objects": [{"url": "/boundary-sets/bar/", "domain": "Barland", "name": "Bar", "related": {"boundaries_url": "/boundaries/bar/"}}], "meta": {"next": null, "total_count": 1, "previous": null, "limit": 20, "offset": 0}}') 26 | 27 | def test_filter_type(self): 28 | response = self.client.get(self.url, {'name__istartswith': 'f'}) 29 | self.assertResponse(response) 30 | self.assertJSONEqual(response, '{"objects": [{"url": "/boundary-sets/foo/", "domain": "Fooland", "name": "Foo", "related": {"boundaries_url": "/boundaries/foo/"}}], "meta": {"next": null, "total_count": 1, "previous": null, "limit": 20, "offset": 0}}') 31 | 32 | def test_ignore_non_filter_field(self): 33 | response = self.client.get(self.url, {'authority': 'King'}) 34 | self.assertResponse(response) 35 | self.assertJSONEqual(response, '{"objects": [' 36 | '{"url": "/boundary-sets/bar/", "domain": "Barland", "name": "Bar", "related": {"boundaries_url": "/boundaries/bar/"}}, ' 37 | '{"url": "/boundary-sets/baz/", "domain": "", "name": "Baz", "related": {"boundaries_url": "/boundaries/baz/"}}, ' 38 | '{"url": "/boundary-sets/foo/", "domain": "Fooland", "name": "Foo", "related": {"boundaries_url": "/boundaries/foo/"}}], ' 39 | '"meta": {"next": null, "total_count": 3, "previous": null, "limit": 20, "offset": 0}}') 40 | 41 | def test_ignore_non_filter_type(self): 42 | response = self.client.get(self.url, {'name__search': 'Foo'}) 43 | self.assertResponse(response) 44 | self.assertJSONEqual(response, '{"objects": [' 45 | '{"url": "/boundary-sets/bar/", "domain": "Barland", "name": "Bar", "related": {"boundaries_url": "/boundaries/bar/"}}, ' 46 | '{"url": "/boundary-sets/baz/", "domain": "", "name": "Baz", "related": {"boundaries_url": "/boundaries/baz/"}}, ' 47 | '{"url": "/boundary-sets/foo/", "domain": "Fooland", "name": "Foo", "related": {"boundaries_url": "/boundaries/foo/"}}], ' 48 | '"meta": {"next": null, "total_count": 3, "previous": null, "limit": 20, "offset": 0}}') 49 | 50 | def test_filter_value_must_be_valid(self): 51 | response = self.client.get(self.url, {'name__isnull': 'none'}) 52 | self.assertError(response) 53 | self.assertEqual(response.content, b'Invalid filter value') 54 | -------------------------------------------------------------------------------- /boundaries/tests/test_compute_intersections.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | 4 | class ComputeIntersectionsTestCase(TestCase): 5 | 6 | def test_command(self): 7 | pass # @todo 8 | -------------------------------------------------------------------------------- /boundaries/tests/test_definition.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from django.test import TestCase 4 | 5 | from boundaries.models import Definition 6 | 7 | if not hasattr(TestCase, 'assertCountEqual'): # Python < 3.2 8 | TestCase.assertCountEqual = TestCase.assertItemsEqual 9 | 10 | 11 | class DefinitionTestCase(TestCase): 12 | maxDiff = None 13 | 14 | def test_str(self): 15 | definition = Definition({ 16 | 'name': 'Test', 17 | 'name_func': lambda feature: '', 18 | }) 19 | 20 | self.assertEqual(str(definition), 'Test') 21 | 22 | def test_defaults(self): 23 | definition = Definition({ 24 | 'name': '', 25 | 'name_func': lambda feature: 'Test', 26 | }) 27 | 28 | self.assertCountEqual(definition.dictionary.keys(), [ 29 | 'name', 30 | 'name_func', 31 | 'encoding', 32 | 'domain', 33 | 'authority', 34 | 'source_url', 35 | 'licence_url', 36 | 'start_date', 37 | 'end_date', 38 | 'notes', 39 | 'extra', 40 | 'id_func', 41 | 'slug_func', 42 | 'is_valid_func', 43 | 'label_point_func', 44 | ]) 45 | 46 | self.assertEqual(definition['name'], '') 47 | self.assertEqual(definition['name_func']({}), 'Test') 48 | self.assertEqual(definition['encoding'], 'ascii') 49 | self.assertEqual(definition['domain'], '') 50 | self.assertEqual(definition['authority'], '') 51 | self.assertEqual(definition['source_url'], '') 52 | self.assertEqual(definition['licence_url'], '') 53 | self.assertEqual(definition['start_date'], None) 54 | self.assertEqual(definition['end_date'], None) 55 | self.assertEqual(definition['notes'], '') 56 | self.assertEqual(definition['extra'], {}) 57 | self.assertEqual(definition['id_func']({}), '') 58 | self.assertEqual(definition['slug_func']({}), 'Test') 59 | self.assertEqual(definition['is_valid_func']({}), True) 60 | self.assertEqual(definition['label_point_func']({}), None) 61 | 62 | def test_overrides(self): 63 | definition = Definition({ 64 | 'name': 'Federal', 65 | 'name_func': lambda feature: 'Name', 66 | 'encoding': 'iso-8859-1', 67 | 'domain': 'Canada', 68 | 'authority': 'Her Majesty the Queen in Right of Canada', 69 | 'source_url': 'http://data.gc.ca/data/en/dataset/48f10fb9-78a2-43a9-92ab-354c28d30674', 70 | 'licence_url': 'http://data.gc.ca/eng/open-government-licence-canada', 71 | 'start_date': date(2000, 1, 1), 72 | 'end_date': date(2010, 1, 1), 73 | 'notes': 'Notes', 74 | 'extra': {'id': 'ocd-division/country:ca'}, 75 | 'id_func': lambda feature: 'ID', 76 | 'slug_func': lambda feature: 'Slug', 77 | 'is_valid_func': lambda feature: False, 78 | 'label_point_func': lambda feature: '', 79 | }) 80 | 81 | self.assertCountEqual(definition.dictionary.keys(), [ 82 | 'name', 83 | 'name_func', 84 | 'encoding', 85 | 'domain', 86 | 'authority', 87 | 'source_url', 88 | 'licence_url', 89 | 'start_date', 90 | 'end_date', 91 | 'notes', 92 | 'extra', 93 | 'id_func', 94 | 'slug_func', 95 | 'is_valid_func', 96 | 'label_point_func', 97 | ]) 98 | 99 | self.assertEqual(definition['name'], 'Federal') 100 | self.assertEqual(definition['name_func']({}), 'Name') 101 | self.assertEqual(definition['encoding'], 'iso-8859-1') 102 | self.assertEqual(definition['domain'], 'Canada') 103 | self.assertEqual(definition['authority'], 'Her Majesty the Queen in Right of Canada') 104 | self.assertEqual(definition['source_url'], 'http://data.gc.ca/data/en/dataset/48f10fb9-78a2-43a9-92ab-354c28d30674') 105 | self.assertEqual(definition['licence_url'], 'http://data.gc.ca/eng/open-government-licence-canada') 106 | self.assertEqual(definition['start_date'], date(2000, 1, 1)) 107 | self.assertEqual(definition['end_date'], date(2010, 1, 1)) 108 | self.assertEqual(definition['notes'], 'Notes') 109 | self.assertEqual(definition['extra'], {'id': 'ocd-division/country:ca'}) 110 | self.assertEqual(definition['id_func']({}), 'ID') 111 | self.assertEqual(definition['slug_func']({}), 'Slug') 112 | self.assertEqual(definition['is_valid_func']({}), False) 113 | self.assertEqual(definition['label_point_func']({}), '') 114 | 115 | def test_singular_default(self): 116 | definition = Definition({ 117 | 'name': 'Districts', 118 | 'name_func': lambda feature: None, 119 | }) 120 | self.assertEqual(definition['singular'], 'District') 121 | 122 | def test_singular_override(self): 123 | definition = Definition({ 124 | 'name': 'Districts', 125 | 'name_func': lambda feature: None, 126 | 'singular': 'Singular', 127 | }) 128 | self.assertEqual(definition['singular'], 'Singular') 129 | 130 | def test_extra_default(self): 131 | definition = Definition({ 132 | 'name': '', 133 | 'name_func': lambda feature: None, 134 | 'extra': {'id': 'ocd-division/country:ca'}, 135 | }) 136 | self.assertEqual(definition['extra'], {'id': 'ocd-division/country:ca'}) 137 | 138 | def test_get(self): 139 | definition = Definition({ 140 | 'name': '', 141 | 'name_func': lambda feature: None, 142 | }) 143 | self.assertEqual(definition.get('name'), '') 144 | self.assertEqual(definition.get('nonexistent', 'default'), 'default') 145 | -------------------------------------------------------------------------------- /boundaries/tests/test_feature.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from django.contrib.gis.gdal import SpatialReference 4 | from django.contrib.gis.geos import Point 5 | 6 | from boundaries import attr, clean_attr 7 | from boundaries.models import BoundarySet, Definition, Feature 8 | from boundaries.tests import BoundariesTestCase, FeatureProxy 9 | 10 | 11 | class FeatureTestCase(BoundariesTestCase): 12 | 13 | def setUp(self): 14 | self.definition = definition = Definition({ 15 | 'last_updated': date(2000, 1, 1), 16 | 'encoding': 'utf-8', 17 | 'name': 'Districts', 18 | 'name_func': clean_attr('Name'), 19 | 'id_func': attr('ID'), 20 | 'slug_func': attr('Code'), 21 | 'is_valid_func': lambda f: f.get('ID') == '1', 22 | 'label_point_func': lambda f: Point(0, 1), 23 | }) 24 | 25 | self.fields = { 26 | 'Name': 'VALID', 27 | 'ID': '1', 28 | 'Code': '\tFoo—Bar–Baz \r Bzz\n', # m-dash, n-dash 29 | } 30 | 31 | self.boundary_set = BoundarySet( 32 | last_updated=definition['last_updated'], 33 | name=definition['name'], 34 | singular=definition['singular'], 35 | ) 36 | 37 | self.feature = Feature(FeatureProxy(self.fields), definition) 38 | 39 | self.other = Feature(FeatureProxy({ 40 | 'Name': 'INVALID', 41 | 'ID': 100, 42 | 'Code': 3, 43 | }), definition, SpatialReference(4269), self.boundary_set) 44 | 45 | def test_init(self): 46 | self.assertEqual(self.feature.boundary_set, None) 47 | self.assertEqual(self.other.boundary_set, self.boundary_set) 48 | 49 | def test_str(self): 50 | self.assertEqual(str(self.feature), 'Valid') 51 | self.assertEqual(str(self.other), 'Invalid') 52 | 53 | def test_get(self): 54 | self.assertEqual(self.feature.get('Name'), 'VALID') 55 | 56 | def test_is_valid(self): 57 | self.assertTrue(self.feature.is_valid()) 58 | self.assertFalse(self.other.is_valid()) 59 | 60 | def test_name(self): 61 | self.assertEqual(self.feature.name, 'Valid') 62 | self.assertEqual(self.other.name, 'Invalid') 63 | 64 | def test_id(self): 65 | self.assertEqual(self.feature.id, '1') 66 | self.assertEqual(self.other.id, '100') 67 | 68 | def test_slug(self): 69 | self.assertEqual(self.feature.slug, 'foo-bar-baz-bzz') 70 | self.assertEqual(self.other.slug, '3') 71 | 72 | def test_label_point(self): 73 | self.assertEqual(self.feature.label_point, Point(0, 1)) 74 | 75 | def test_metadata(self): 76 | self.assertEqual(self.feature.metadata, self.fields) 77 | 78 | def test_boundary_set(self): 79 | self.feature.boundary_set = self.boundary_set 80 | 81 | self.assertEqual(self.feature.boundary_set, self.boundary_set) 82 | 83 | self.feature.boundary_set = None 84 | 85 | def test_create_boundary(self): 86 | self.feature.boundary_set = self.boundary_set 87 | 88 | self.boundary_set.save() 89 | boundary = self.feature.create_boundary() 90 | self.assertEqual(boundary.set, self.boundary_set) 91 | self.assertEqual(boundary.set_name, 'District') 92 | self.assertEqual(boundary.external_id, '1') 93 | self.assertEqual(boundary.name, 'Valid') 94 | self.assertEqual(boundary.slug, 'foo-bar-baz-bzz') 95 | self.assertEqual(boundary.metadata, self.fields) 96 | self.assertEqual(boundary.shape.ogr.wkt, 'MULTIPOLYGON (((0 0,0.0001 0.0001,0 5,5 5,0 0)))') 97 | self.assertEqual(boundary.simple_shape.ogr.wkt, 'MULTIPOLYGON (((0 0,0 5,5 5,0 0)))') 98 | self.assertRegex(boundary.centroid.ogr.wkt, r'\APOINT \(1\.6667 3\.3333666666666\d+\)\Z') 99 | self.assertTupleAlmostEqual(boundary.extent, (0.0, 0.0, 5.0, 5.0)) 100 | self.assertEqual(boundary.label_point, Point(0, 1, srid=4326)) 101 | self.assertEqual(boundary.start_date, None) 102 | self.assertEqual(boundary.end_date, None) 103 | 104 | self.feature.boundary_set = None 105 | -------------------------------------------------------------------------------- /boundaries/tests/test_geometry.py: -------------------------------------------------------------------------------- 1 | from django.contrib.gis.gdal import OGRGeometry, SpatialReference 2 | from django.test import TestCase 3 | 4 | from boundaries.models import Geometry 5 | 6 | 7 | class GeometryTestCase(TestCase): 8 | maxDiff = None 9 | 10 | def test_str(self): 11 | wkt = 'MULTIPOLYGON (((0 0,0 5,5 5,0 0)))' 12 | self.assertEqual(str(Geometry(OGRGeometry(wkt))), wkt) 13 | 14 | def test_init_with_ogrgeometry(self): 15 | geometry = OGRGeometry('MULTIPOLYGON (((0 0,0 5,5 5,0 0)))') 16 | self.assertEqual(Geometry(geometry).geometry, geometry) 17 | 18 | def test_init_with_geometry(self): 19 | geometry = OGRGeometry('MULTIPOLYGON (((0 0,0 5,5 5,0 0)))') 20 | self.assertEqual(Geometry(Geometry(geometry)).geometry, geometry) 21 | 22 | def test_transform_polygon(self): 23 | geometry = Geometry(OGRGeometry('POLYGON ((0 0,0 5,5 5,0 0))')).transform(SpatialReference(26917)) 24 | self.assertIsInstance(geometry, Geometry) 25 | self.assertEqual(geometry.geometry.geom_name, 'MULTIPOLYGON') 26 | self.assertRegex(geometry.wkt, r'MULTIPOLYGON \(\(\(-85.488743884\d+ 0.0,-85.488743884\d+ 0.000045096\d+,-85.488699089\d+ 0.000045096\d+,-85.488743884\d+ 0.0\)\)\)') 27 | 28 | def test_transform_multipolygon(self): 29 | geometry = Geometry(OGRGeometry('MULTIPOLYGON (((0 0,0 5,5 5,0 0)))')).transform(SpatialReference(26917)) 30 | self.assertIsInstance(geometry, Geometry) 31 | self.assertEqual(geometry.geometry.geom_name, 'MULTIPOLYGON') 32 | self.assertRegex(geometry.wkt, r'MULTIPOLYGON \(\(\(-85.488743884\d+ 0.0,-85.488743884\d+ 0.000045096\d+,-85.488699089\d+ 0.000045096\d+,-85.488743884\d+ 0.0\)\)\)') 33 | 34 | def test_transform_nonpolygon(self): 35 | self.assertRaisesRegex(ValueError, r'\AThe geometry is a Point but must be a Polygon or a MultiPolygon\.\Z', Geometry(OGRGeometry('POINT (0 0)')).transform, SpatialReference(26917)) 36 | 37 | def test_simplify(self): 38 | geometry = Geometry(OGRGeometry('MULTIPOLYGON (((0 0,0.0001 0.0001,0 5,5 5,0 0)))')).simplify() 39 | self.assertIsInstance(geometry, Geometry) 40 | self.assertEqual(geometry.geometry.geom_name, 'MULTIPOLYGON') 41 | self.assertEqual(geometry.wkt, 'MULTIPOLYGON (((0 0,0 5,5 5,0 0)))') 42 | 43 | def test_unary_union(self): 44 | geometry = Geometry(OGRGeometry('MULTIPOLYGON (((0 0,0 5,5 5,0 0)),((0 0,5 0,5 5,0 0)))')).unary_union() 45 | self.assertIsInstance(geometry, Geometry) 46 | self.assertEqual(geometry.geometry.geom_name, 'MULTIPOLYGON') 47 | self.assertEqual( 48 | geometry.geometry.difference(OGRGeometry('MULTIPOLYGON (((0 0,0 5,5 5,5 0,0 0)))')).wkt, 49 | 'POLYGON EMPTY', 50 | ) 51 | 52 | def test_merge_with_ogrgeometry(self): 53 | other = OGRGeometry('MULTIPOLYGON (((5 0,5 3,2 0,5 0)))') 54 | geometry = Geometry(OGRGeometry('MULTIPOLYGON (((0 0,0 5,5 5,0 0)))')).merge(other) 55 | self.assertIsInstance(geometry, Geometry) 56 | self.assertEqual(geometry.geometry.geom_name, 'MULTIPOLYGON') 57 | self.assertEqual(geometry.wkt, 'MULTIPOLYGON (((0 0,0 5,5 5,0 0)),((5 0,5 3,2 0,5 0)))') 58 | 59 | def test_merge_with_geometry(self): 60 | other = Geometry(OGRGeometry('MULTIPOLYGON (((5 0,5 3,2 0,5 0)))')) 61 | geometry = Geometry(OGRGeometry('MULTIPOLYGON (((0 0,0 5,5 5,0 0)))')).merge(other) 62 | self.assertIsInstance(geometry, Geometry) 63 | self.assertEqual(geometry.geometry.geom_name, 'MULTIPOLYGON') 64 | self.assertEqual(geometry.wkt, 'MULTIPOLYGON (((0 0,0 5,5 5,0 0)),((5 0,5 3,2 0,5 0)))') 65 | 66 | def test_wkt(self): 67 | geometry = Geometry(OGRGeometry('MULTIPOLYGON (((0 0,0 5,5 5,0 0)))')) 68 | self.assertEqual(geometry.wkt, 'MULTIPOLYGON (((0 0,0 5,5 5,0 0)))') 69 | 70 | def test_centroid(self): 71 | geometry = Geometry(OGRGeometry('MULTIPOLYGON (((0 0,0 5,5 5,0 0)))')) 72 | self.assertRegex(geometry.centroid.ogr.wkt, r'\APOINT \(1\.6666666666666+7 3\.33333333333333+\)\Z') 73 | 74 | def test_extent(self): 75 | geometry = Geometry(OGRGeometry('MULTIPOLYGON (((0 0,0 5,5 5,0 0)))')) 76 | self.assertEqual(geometry.extent, (0.0, 0.0, 5.0, 5.0)) 77 | -------------------------------------------------------------------------------- /boundaries/tests/test_loadshapefiles.py: -------------------------------------------------------------------------------- 1 | import errno 2 | import os 3 | import os.path 4 | import traceback 5 | from datetime import date 6 | from zipfile import BadZipfile 7 | 8 | from django.contrib.gis.gdal import OGRGeometry 9 | from django.core.management import call_command 10 | from django.test import TestCase 11 | from testfixtures import LogCapture 12 | 13 | import boundaries 14 | from boundaries.management.commands.loadshapefiles import Command, create_data_sources 15 | from boundaries.models import BoundarySet, Definition, Feature 16 | from boundaries.tests import BoundariesTestCase, FeatureProxy 17 | 18 | 19 | def fixture(basename): 20 | return os.path.join(os.path.dirname(__file__), 'fixtures', basename) 21 | 22 | 23 | class LoadShapefilesTestCase(TestCase): # @todo This only ensures there's no gross error. Need more tests. 24 | 25 | def setUp(self): 26 | boundaries.registry = {} 27 | boundaries._basepath = '.' 28 | 29 | def test_loadshapefiles(self): 30 | with LogCapture() as logcapture: 31 | try: 32 | call_command('loadshapefiles', data_dir='boundaries/tests/definitions/polygons') 33 | except Exception as e: 34 | if not hasattr(e, 'errno') or e.errno != errno.ENOENT: 35 | self.fail(f'Exception {type(e).__name__} raised: {e} {traceback.format_exc()}') 36 | logcapture.check( 37 | ('boundaries.management.commands.loadshapefiles', 'INFO', 'Processing polygons.'), 38 | ('boundaries.management.commands.loadshapefiles', 'INFO', 'Loading polygons from boundaries/tests/definitions/polygons/test_poly.shp'), 39 | ('boundaries.management.commands.loadshapefiles', 'INFO', '1...'), 40 | ('boundaries.management.commands.loadshapefiles', 'INFO', '2...'), 41 | ('boundaries.management.commands.loadshapefiles', 'INFO', '3...'), 42 | ('boundaries.management.commands.loadshapefiles', 'INFO', 'polygons count: 3'), 43 | ) 44 | 45 | def test_no_features(self): 46 | with LogCapture() as logcapture: 47 | try: 48 | call_command('loadshapefiles', data_dir='boundaries/tests/definitions/no_features') 49 | except Exception as e: 50 | if not hasattr(e, 'errno') or e.errno != errno.ENOENT: 51 | self.fail(f'Exception {type(e).__name__} raised: {e} {traceback.format_exc()}') 52 | logcapture.check( 53 | ('boundaries.management.commands.loadshapefiles', 'INFO', 'Processing districts.'), 54 | ('boundaries.management.commands.loadshapefiles', 'INFO', 'Loading districts from boundaries/tests/definitions/no_features/../../fixtures/foo.shp'), 55 | ('boundaries.management.commands.loadshapefiles', 'INFO', 'districts count: 0'), 56 | ) 57 | 58 | def test_srid(self): 59 | with LogCapture() as logcapture: 60 | try: 61 | call_command('loadshapefiles', data_dir='boundaries/tests/definitions/srid') 62 | except Exception as e: 63 | if not hasattr(e, 'errno') or e.errno != errno.ENOENT: 64 | self.fail(f'Exception {type(e).__name__} raised: {e} {traceback.format_exc()}') 65 | logcapture.check( 66 | ('boundaries.management.commands.loadshapefiles', 'INFO', 'Processing wards.'), 67 | ('boundaries.management.commands.loadshapefiles', 'INFO', 'Loading wards from boundaries/tests/definitions/srid/../../fixtures/foo.shp'), 68 | ('boundaries.management.commands.loadshapefiles', 'INFO', 'wards count: 0'), 69 | ) 70 | 71 | def test_clean(self): 72 | with LogCapture() as logcapture: 73 | try: 74 | call_command('loadshapefiles', data_dir='boundaries/tests/definitions/no_features', clean=True) 75 | logcapture.check( 76 | ('boundaries.management.commands.loadshapefiles', 'INFO', 'Processing districts.'), 77 | ('boundaries.management.commands.loadshapefiles', 'INFO', 'Loading districts from boundaries/tests/definitions/no_features/../../fixtures/foo._cleaned_.shp'), 78 | ('boundaries.management.commands.loadshapefiles', 'INFO', 'districts count: 0'), 79 | ) 80 | except Exception as e: 81 | if not hasattr(e, 'errno') or e.errno != errno.ENOENT: 82 | self.fail(f'Exception {type(e).__name__} raised: {e} {traceback.format_exc()}') 83 | else: 84 | logcapture.check(('boundaries.management.commands.loadshapefiles', 'INFO', 'Processing districts.')) 85 | 86 | def test_only(self): 87 | with LogCapture() as logcapture: 88 | call_command('loadshapefiles', data_dir='boundaries/tests/definitions/no_features', only='unknown') 89 | logcapture.check(('boundaries.management.commands.loadshapefiles', 'DEBUG', 'Skipping districts.')) 90 | 91 | def test_except(self): 92 | with LogCapture() as logcapture: 93 | call_command('loadshapefiles', data_dir='boundaries/tests/definitions/no_features', **{'except': 'districts'}) 94 | logcapture.check(('boundaries.management.commands.loadshapefiles', 'DEBUG', 'Skipping districts.')) 95 | 96 | def test_no_data_sources(self): 97 | with LogCapture() as logcapture: 98 | call_command('loadshapefiles', data_dir='boundaries/tests/definitions/no_data_sources') 99 | logcapture.check( 100 | ('boundaries.management.commands.loadshapefiles', 'INFO', 'Processing empty.'), 101 | ('boundaries.management.commands.loadshapefiles', 'WARNING', 'No shapefiles found.'), 102 | ) 103 | 104 | def test_get_version(self): 105 | try: 106 | Command().get_version() 107 | except Exception as e: 108 | self.fail(f'Exception {type(e).__name__} raised: {e} {traceback.format_exc()}') 109 | 110 | 111 | class LoadableTestCase(TestCase): 112 | 113 | def test_whitelist(self): 114 | self.assertTrue(Command().loadable('foo', date(2000, 1, 1), whitelist={'foo'})) 115 | self.assertFalse(Command().loadable('bar', date(2000, 1, 1), whitelist={'foo'})) 116 | 117 | def test_blacklist(self): 118 | self.assertFalse(Command().loadable('foo', date(2000, 1, 1), blacklist={'foo'})) 119 | self.assertTrue(Command().loadable('bar', date(2000, 1, 1), blacklist={'foo'})) 120 | 121 | def test_reload_existing(self): 122 | BoundarySet.objects.create(name='Foo', last_updated=date(2010, 1, 1)) 123 | self.assertTrue(Command().loadable('foo', date(2000, 1, 1), reload_existing=True)) 124 | self.assertFalse(Command().loadable('foo', date(2000, 1, 1), reload_existing=False)) 125 | 126 | def test_out_of_date(self): 127 | BoundarySet.objects.create(name='Foo', last_updated=date(2010, 1, 1)) 128 | self.assertTrue(Command().loadable('foo', date(2020, 1, 1))) 129 | 130 | def test_up_to_date(self): 131 | BoundarySet.objects.create(name='Foo', last_updated=date(2010, 1, 1)) 132 | self.assertFalse(Command().loadable('foo', date(2000, 1, 1))) 133 | 134 | def test_nonexisting(self): 135 | self.assertTrue(Command().loadable('foo', date(2000, 1, 1))) 136 | BoundarySet.objects.create(name='Foo', last_updated=date(2010, 1, 1)) 137 | self.assertFalse(Command().loadable('foo', date(2000, 1, 1))) 138 | 139 | 140 | class LoadBoundaryTestCase(BoundariesTestCase): 141 | definition = Definition({ 142 | 'last_updated': date(2000, 1, 1), 143 | 'name': 'Districts', 144 | 'name_func': lambda feature: 'Test', 145 | }) 146 | 147 | boundary_set = BoundarySet( 148 | last_updated=definition['last_updated'], 149 | name=definition['name'], 150 | singular=definition['singular'], 151 | ) 152 | 153 | feature = Feature(FeatureProxy({}), definition, boundary_set=boundary_set) 154 | 155 | def setUp(self): 156 | self.boundary_set.save() 157 | 158 | def test_no_merge_strategy(self): 159 | boundary = Command().load_boundary(self.feature) 160 | self.assertEqual(boundary.set, self.boundary_set) 161 | self.assertEqual(boundary.set_name, 'District') 162 | self.assertEqual(boundary.external_id, '') 163 | self.assertEqual(boundary.name, 'Test') 164 | self.assertEqual(boundary.slug, 'test') 165 | self.assertEqual(boundary.metadata, {}) 166 | self.assertEqual(boundary.shape.ogr.wkt, 'MULTIPOLYGON (((0 0,0.0001 0.0001,0 5,5 5,0 0)))') 167 | self.assertEqual(boundary.simple_shape.ogr.wkt, 'MULTIPOLYGON (((0 0,0 5,5 5,0 0)))') 168 | self.assertRegex(boundary.centroid.ogr.wkt, r'\APOINT \(1\.6667 3\.3333666666666\d+\)\Z') 169 | self.assertTupleAlmostEqual(boundary.extent, (0.0, 0.0, 5.0, 5.0)) 170 | self.assertEqual(boundary.label_point, None) 171 | self.assertEqual(boundary.start_date, None) 172 | self.assertEqual(boundary.end_date, None) 173 | 174 | def test_invalid_merge_strategy_when_nothing_to_merge(self): 175 | try: 176 | Command().load_boundary(self.feature, 'invalid') 177 | except Exception as e: 178 | self.fail(f'Exception {type(e).__name__} raised: {e} {traceback.format_exc()}') 179 | 180 | def test_invalid_merge_strategy(self): 181 | Command().load_boundary(self.feature, 'invalid') 182 | 183 | self.assertRaisesRegex(ValueError, r"\AThe merge strategy 'invalid' must be 'combine' or 'union'.\Z", Command().load_boundary, self.feature, 'invalid') 184 | 185 | def test_combine_merge_strategy(self): 186 | self.boundary_set.save() 187 | Command().load_boundary(self.feature, 'invalid') 188 | 189 | boundary = Command().load_boundary(self.feature, 'combine') 190 | self.assertEqual(boundary.shape.ogr.wkt, 'MULTIPOLYGON (((0 0,0.0001 0.0001,0 5,5 5,0 0)),((0 0,0.0001 0.0001,0 5,5 5,0 0)))') 191 | self.assertEqual(boundary.simple_shape.ogr.wkt, 'MULTIPOLYGON (((0 0,0 5,5 5,0 0)),((0 0,0 5,5 5,0 0)))') 192 | self.assertRegex(boundary.centroid.ogr.wkt, r'\APOINT \(1\.6667 3\.3333666666666+7\)\Z') 193 | self.assertEqual(boundary.extent, (0.0, 0.0, 5.0, 5.0)) 194 | 195 | def test_union_merge_strategy(self): 196 | self.boundary_set.save() 197 | Command().load_boundary(self.feature, 'invalid') 198 | 199 | boundary = Command().load_boundary(self.feature, 'union') 200 | expected = OGRGeometry('MULTIPOLYGON (((0.0001 0.0001,0 5,5 5,0.0001 0.0001)))') 201 | self.assertEqual(boundary.shape.ogr.difference(expected).wkt, 'POLYGON EMPTY') 202 | self.assertEqual(boundary.simple_shape.ogr.difference(expected).wkt, 'POLYGON EMPTY') 203 | self.assertRegex(boundary.centroid.ogr.wkt, r'\APOINT \(1\.6667 3\.3333666666666+7\)\Z') 204 | self.assertEqual(boundary.extent, (0.0, 0.0001, 5.0, 5.0)) 205 | 206 | 207 | class DataSourcesTestCase(TestCase): 208 | 209 | def test_empty_txt(self): 210 | self.assertRaisesRegex(ValueError, r"\AThe path must be a shapefile, a ZIP file, or a directory: .+/boundaries/tests/fixtures/empty/empty\.txt\.\Z", create_data_sources, fixture('empty/empty.txt')) 211 | 212 | def test_foo_shp(self): 213 | path = fixture('foo.shp') 214 | data_sources, tmpdirs = create_data_sources(path) 215 | self.assertEqual(len(data_sources), 1) 216 | self.assertEqual(tmpdirs, []) 217 | self.assertEqual(data_sources[0].name, path) 218 | self.assertEqual(data_sources[0].layer_count, 1) 219 | 220 | def test_flat_zip(self): 221 | path = fixture('flat.zip') # foo.shp, etc. 222 | data_sources, tmpdirs = create_data_sources(path) 223 | self.assertEqual(len(data_sources), 1) 224 | self.assertEqual(len(tmpdirs), 1) 225 | self.assertEqual(data_sources[0].name, os.path.join(tmpdirs[0], 'foo.shp')) 226 | self.assertEqual(data_sources[0].layer_count, 1) 227 | 228 | def test_bad_zip(self): 229 | self.assertRaisesRegex(BadZipfile, r"\AFile is not a zip file\Z", create_data_sources, fixture('bad.zip')) 230 | 231 | def test_empty(self): 232 | data_sources, tmpdirs = create_data_sources(fixture('empty')) 233 | self.assertEqual(data_sources, []) 234 | self.assertEqual(len(tmpdirs), 1) 235 | 236 | def test_empty_zip(self): 237 | data_sources, tmpdirs = create_data_sources(fixture('empty.zip')) # empty.txt 238 | self.assertEqual(data_sources, []) 239 | self.assertEqual(len(tmpdirs), 1) 240 | 241 | def test_multiple(self): 242 | path = fixture('multiple') 243 | data_sources, tmpdirs = create_data_sources(path) 244 | self.assertEqual(len(tmpdirs), 3) 245 | self.assertEqual(len(data_sources), 5) 246 | 247 | paths = [ 248 | os.path.join(path, 'bar.shp'), 249 | os.path.join(path, 'foo.shp'), 250 | os.path.join(path, 'dir.zip', 'foo.shp'), 251 | os.path.join(tmpdirs[0], 'foo.shp'), 252 | os.path.join(tmpdirs[1], 'foo.shp'), 253 | os.path.join(tmpdirs[2], 'dir.zip', 'foo.shp'), 254 | ] 255 | 256 | zipfiles = [ 257 | os.path.join(path, 'flat.zip'), 258 | os.path.join(path, 'nested.zip'), 259 | ] 260 | 261 | for data_source in data_sources: 262 | self.assertIn(data_source.name, paths) 263 | self.assertEqual(data_source.layer_count, 1) 264 | if hasattr(data_source, 'zipfile'): 265 | self.assertIn(data_source.zipfile, zipfiles) 266 | 267 | def test_multiple_zip(self): 268 | path = fixture('multiple.zip') 269 | data_sources, tmpdirs = create_data_sources(path) 270 | self.assertEqual(len(tmpdirs), 4) 271 | self.assertEqual(len(data_sources), 5) 272 | 273 | paths = [ 274 | os.path.join(tmpdirs[0], 'bar.shp'), 275 | os.path.join(tmpdirs[0], 'foo.shp'), 276 | os.path.join(tmpdirs[0], 'dir.zip', 'foo.shp'), 277 | os.path.join(tmpdirs[1], 'foo.shp'), 278 | os.path.join(tmpdirs[2], 'foo.shp'), 279 | os.path.join(tmpdirs[3], 'dir.zip', 'foo.shp'), 280 | ] 281 | 282 | zipfiles = [ 283 | path, 284 | os.path.join(tmpdirs[0], 'flat.zip'), 285 | os.path.join(tmpdirs[0], 'nested.zip'), 286 | ] 287 | 288 | for data_source in data_sources: 289 | self.assertIn(data_source.name, paths) 290 | self.assertEqual(data_source.layer_count, 1) 291 | if hasattr(data_source, 'zipfile'): 292 | self.assertIn(data_source.zipfile, zipfiles) 293 | 294 | def test_nested(self): 295 | path = fixture('nested') 296 | data_sources, tmpdirs = create_data_sources(path) 297 | self.assertEqual(len(data_sources), 1) 298 | self.assertEqual(tmpdirs, []) 299 | self.assertEqual(data_sources[0].name, os.path.join(path, 'dir.zip', 'foo.shp')) 300 | self.assertEqual(data_sources[0].layer_count, 1) 301 | 302 | def test_nested_zip(self): 303 | path = fixture('nested.zip') 304 | data_sources, tmpdirs = create_data_sources(path) 305 | self.assertEqual(len(data_sources), 1) 306 | self.assertEqual(len(tmpdirs), 1) 307 | self.assertEqual(data_sources[0].name, os.path.join(tmpdirs[0], 'dir.zip', 'foo.shp')) 308 | self.assertEqual(data_sources[0].layer_count, 1) 309 | 310 | def test_converts_3d_to_2d(self): 311 | pass # @todo 312 | -------------------------------------------------------------------------------- /boundaries/tests/test_titlecase.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from boundaries.titlecase import titlecase 4 | 5 | 6 | class TitlecaseTestCase(TestCase): 7 | def test_uc_initials(self): 8 | self.assertEqual(titlecase('X.Y.Z. INC.'), 'X.Y.Z. Inc.') 9 | 10 | def test_apos_second(self): 11 | self.assertEqual(titlecase("duck à l'orange"), "Duck à L'Orange") 12 | 13 | def test_inline_period(self): 14 | self.assertEqual(titlecase('example.com'), 'example.com') 15 | 16 | def test_small_words(self): 17 | self.assertEqual(titlecase('FOR WHOM THE BELL TOLLS'), 'For Whom the Bell Tolls') 18 | 19 | def test_mac_mc(self): 20 | self.assertEqual(titlecase('MACDONALD'), 'MacDonald') 21 | 22 | def test_slash(self): 23 | self.assertEqual(titlecase('foo/bar/baz'), 'Foo/Bar/Baz') 24 | -------------------------------------------------------------------------------- /boundaries/titlecase.py: -------------------------------------------------------------------------------- 1 | """ 2 | Original Perl version by: John Gruber http://daringfireball.net/ 10 May 2008 3 | Python version by Stuart Colville http://muffinresearch.co.uk 4 | License: http://www.opensource.org/licenses/mit-license.php 5 | """ 6 | 7 | import re 8 | 9 | __all__ = ['titlecase'] 10 | __version__ = '0.5.2' 11 | 12 | SMALL = r'a|an|and|as|at|but|by|en|for|if|in|of|on|or|the|to|v\.?|via|vs\.?' 13 | PUNCT = r"""!"#$%&'‘()*+,\-./:;?@[\\\]_`{|}~""" 14 | 15 | SMALL_WORDS = re.compile(r'^(%s)$' % SMALL, re.I) 16 | INLINE_PERIOD = re.compile(r'[a-z][.][a-z]', re.I) 17 | UC_ELSEWHERE = re.compile(r'[%s]*?[a-zA-Z]+[A-Z]+?' % PUNCT) 18 | CAPFIRST = re.compile(r"^[%s]*?([A-Za-z])" % PUNCT) 19 | SMALL_FIRST = re.compile(r'^([{}]*)({})\b'.format(PUNCT, SMALL), re.I) 20 | SMALL_LAST = re.compile(r'\b({})[{}]?$'.format(SMALL, PUNCT), re.I) 21 | SUBPHRASE = re.compile(r'([:.;?!][ ])(%s)' % SMALL) 22 | APOS_SECOND = re.compile(r"^[dol]{1}['‘]{1}[a-z]+$", re.I) 23 | ALL_CAPS = re.compile(r'^[\w\s%s]+$' % PUNCT, flags=re.U) 24 | UC_INITIALS = re.compile(r"^(?:[A-Z]{1}\.{1}|[A-Z]{1}\.{1}[A-Z]{1})+$") 25 | MAC_MC = re.compile(r"^([Mm]a?c)(\w+)") 26 | 27 | 28 | def titlecase(text): 29 | """ 30 | Titlecases input text 31 | 32 | This filter changes all words to Title Caps, and attempts to be clever 33 | about *un*capitalizing SMALL words like a/an/the in the input. 34 | 35 | The list of "SMALL words" which are not capped comes from 36 | the New York Times Manual of Style, plus 'vs' and 'v'. 37 | 38 | """ 39 | 40 | lines = re.split('[\r\n]+', text) 41 | processed = [] 42 | for line in lines: 43 | all_caps = ALL_CAPS.match(line) 44 | words = re.split('[\t ]', line) 45 | tc_line = [] 46 | for word in words: 47 | if all_caps: 48 | if UC_INITIALS.match(word): 49 | tc_line.append(word) 50 | continue 51 | else: 52 | word = word.lower() 53 | 54 | if APOS_SECOND.match(word): 55 | word = word.replace(word[0], word[0].upper()) 56 | word = word.replace(word[2], word[2].upper()) 57 | tc_line.append(word) 58 | continue 59 | if INLINE_PERIOD.search(word) or UC_ELSEWHERE.match(word): 60 | tc_line.append(word) 61 | continue 62 | if SMALL_WORDS.match(word): 63 | tc_line.append(word.lower()) 64 | continue 65 | 66 | match = MAC_MC.match(word) 67 | if match: 68 | tc_line.append(f"{match.group(1).capitalize()}{match.group(2).capitalize()}") 69 | continue 70 | 71 | if "/" in word and "//" not in word: 72 | slashed = [] 73 | for item in word.split('/'): 74 | slashed.append(CAPFIRST.sub(lambda m: m.group(0).upper(), item)) 75 | tc_line.append("/".join(slashed)) 76 | continue 77 | 78 | hyphenated = [] 79 | for item in word.split('-'): 80 | hyphenated.append(CAPFIRST.sub(lambda m: m.group(0).upper(), item)) 81 | tc_line.append("-".join(hyphenated)) 82 | 83 | result = " ".join(tc_line) 84 | 85 | result = SMALL_FIRST.sub(lambda m: f'{m.group(1)}{m.group(2).capitalize()}', result) 86 | 87 | result = SMALL_LAST.sub(lambda m: m.group(0).capitalize(), result) 88 | 89 | result = SUBPHRASE.sub(lambda m: f'{m.group(1)}{m.group(2).capitalize()}', result) 90 | 91 | processed.append(result) 92 | 93 | return "\n".join(processed) 94 | -------------------------------------------------------------------------------- /boundaries/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, re_path 2 | 3 | from boundaries.views import ( 4 | BoundaryDetailView, 5 | BoundaryGeoDetailView, 6 | BoundaryListView, 7 | BoundarySetDetailView, 8 | BoundarySetListView, 9 | ) 10 | 11 | urlpatterns = [ 12 | path( 13 | 'boundary-sets/', 14 | BoundarySetListView.as_view(), 15 | name='boundaries_set_list' 16 | ), 17 | re_path( 18 | r'^boundary-sets/(?P[\w_-]+)/$', 19 | BoundarySetDetailView.as_view(), 20 | name='boundaries_set_detail' 21 | ), 22 | path( 23 | 'boundaries/', 24 | BoundaryListView.as_view(), 25 | name='boundaries_boundary_list' 26 | ), 27 | re_path( 28 | r'^boundaries/(?Pshape|simple_shape|centroid)$', 29 | BoundaryListView.as_view(), 30 | name='boundaries_boundary_list' 31 | ), 32 | re_path( 33 | r'^boundaries/(?P[\w_-]+)/$', 34 | BoundaryListView.as_view(), 35 | name='boundaries_boundary_list' 36 | ), 37 | re_path( 38 | r'^boundaries/(?P[\w_-]+)/(?Pshape|simple_shape|centroid)$', 39 | BoundaryListView.as_view(), 40 | name='boundaries_boundary_list' 41 | ), 42 | re_path( 43 | r'^boundaries/(?P[\w_-]+)/(?P[\w_-]+)/$', 44 | BoundaryDetailView.as_view(), 45 | name='boundaries_boundary_detail' 46 | ), 47 | re_path( 48 | r'^boundaries/(?P[\w_-]+)/(?P[\w_-]+)/(?Pshape|simple_shape|centroid)$', 49 | BoundaryGeoDetailView.as_view(), 50 | name='boundaries_boundary_detail' 51 | ), 52 | ] 53 | -------------------------------------------------------------------------------- /boundaries/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.gis.db import models 2 | from django.http import Http404 3 | from django.utils.translation import gettext as _ 4 | 5 | from boundaries.base_views import BadRequest, ModelDetailView, ModelGeoDetailView, ModelGeoListView, ModelListView 6 | from boundaries.models import Boundary, BoundarySet, app_settings 7 | 8 | 9 | class BoundarySetListView(ModelListView): 10 | 11 | """ e.g. /boundary-set/ """ 12 | 13 | filterable_fields = ['name', 'domain'] 14 | 15 | model = BoundarySet 16 | 17 | 18 | class BoundarySetDetailView(ModelDetailView): 19 | 20 | """ e.g. /boundary-set/federal-electoral-districts/ """ 21 | 22 | model = BoundarySet 23 | 24 | def get_object(self, request, qs, slug): 25 | try: 26 | return qs.get(slug=slug) 27 | except BoundarySet.DoesNotExist: 28 | raise Http404 29 | 30 | 31 | class BoundaryListView(ModelGeoListView): 32 | 33 | """ e.g. /boundary/federal-electoral-districts/ 34 | or /boundary/federal-electoral-districts/centroid """ 35 | 36 | filterable_fields = ['external_id', 'name'] 37 | allowed_geo_fields = ('shape', 'simple_shape', 'centroid') 38 | default_geo_filter_field = 'shape' 39 | model = Boundary 40 | 41 | def filter(self, request, qs): 42 | qs = super().filter(request, qs) 43 | 44 | if 'intersects' in request.GET: 45 | try: 46 | (set_slug, slug) = request.GET['intersects'].split('/') 47 | shape = Boundary.objects.filter(slug=slug, set=set_slug).values_list('shape', flat=True)[0] 48 | except IndexError: 49 | raise Http404 50 | except ValueError: 51 | raise BadRequest(_("Invalid value for intersects filter")) 52 | qs = qs.filter(models.Q(shape__covers=shape) | models.Q(shape__overlaps=shape)) 53 | 54 | if 'touches' in request.GET: 55 | try: 56 | (set_slug, slug) = request.GET['touches'].split('/') 57 | shape = Boundary.objects.filter(slug=slug, set=set_slug).values_list('shape', flat=True)[0] 58 | except IndexError: 59 | raise Http404 60 | except ValueError: 61 | raise BadRequest(_("Invalid value for touches filter")) 62 | qs = qs.filter(shape__touches=shape) 63 | 64 | if 'sets' in request.GET: 65 | set_slugs = request.GET['sets'].split(',') 66 | qs = qs.filter(set__in=set_slugs) 67 | 68 | return qs 69 | 70 | def get_qs(self, request, set_slug=None): 71 | qs = super().get_qs(request) 72 | if set_slug: 73 | if not BoundarySet.objects.filter(slug=set_slug).exists(): 74 | raise Http404 75 | return qs.filter(set=set_slug) 76 | return qs 77 | 78 | def get_related_resources(self, request, qs, meta): 79 | r = super().get_related_resources(request, qs, meta) 80 | if meta['total_count'] == 0 or meta['total_count'] > app_settings.MAX_GEO_LIST_RESULTS: 81 | return r 82 | 83 | geo_url = request.path + r'%s' 84 | if request.META['QUERY_STRING']: 85 | geo_url += '?' + request.META['QUERY_STRING'].replace('%', '%%') 86 | 87 | r.update( 88 | shapes_url=geo_url % 'shape', 89 | simple_shapes_url=geo_url % 'simple_shape', 90 | centroids_url=geo_url % 'centroid' 91 | ) 92 | return r 93 | 94 | 95 | class BoundaryObjectGetterMixin: 96 | 97 | model = Boundary 98 | 99 | def get_object(self, request, qs, set_slug, slug): 100 | try: 101 | return qs.get(slug=slug, set=set_slug) 102 | except Boundary.DoesNotExist: 103 | raise Http404 104 | 105 | 106 | class BoundaryDetailView(ModelDetailView, BoundaryObjectGetterMixin): 107 | 108 | """ e.g. /boundary/federal-electoral-districts/outremont/ """ 109 | 110 | def __init__(self): 111 | super().__init__() 112 | self.base_qs = self.base_qs.defer('shape', 'simple_shape') 113 | 114 | 115 | class BoundaryGeoDetailView(ModelGeoDetailView, BoundaryObjectGetterMixin): 116 | 117 | """ e.g /boundary/federal-electoral-districts/outremont/shape """ 118 | 119 | allowed_geo_fields = ('shape', 'simple_shape', 'centroid') 120 | -------------------------------------------------------------------------------- /definition.example.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | import boundaries 4 | 5 | boundaries.register( 6 | # The string to be used for the boundary set's slug. The slug will be "federal-electoral-districts". 7 | 'Federal electoral districts', 8 | 9 | # (Optional) The path to the shapefile's directory relative to this file. 10 | # If this definition file and the shapefile share the same directory, you 11 | # can omit this parameter, or set it to the empty string. 12 | file='', 13 | # (Optional) The encoding of the shapefile's attributes. The default is 14 | # "ascii", but many shapefiles are encoded as "iso-8859-1". 15 | encoding='iso-8859-1', 16 | # (Optional) Override the Spatial Reference System Identifier (SRID) of 17 | # the shapefile. 18 | srid=4269, 19 | 20 | 21 | # The following Boundary Set fields will be made available via the API. 22 | 23 | # The most recent date on which the data was updated. 24 | last_updated=date(2011, 11, 28), 25 | # The plural name of the boundary set, for display. By default, it will use 26 | # the boundary set's slug. 27 | name='Federal electoral districts', 28 | # A generic singular name for a boundary in the set. If the boundary set's 29 | # name ends in "s", this parameter is optional, as is the case here. 30 | singular='Federal electoral district', 31 | 32 | # (Optional) The geographic area covered by the boundary set, which is 33 | # often a country, a region, a municipality, etc. 34 | domain='Canada', 35 | # (Optional) The entity responsible for publishing the data. 36 | authority='Her Majesty the Queen in Right of Canada', 37 | # (Optional) A URL to the source of the data. 38 | source_url='http://data.gc.ca/data/en/dataset/48f10fb9-78a2-43a9-92ab-354c28d30674', 39 | # (Optional) A URL to the licence under which the data is made available. 40 | licence_url='http://data.gc.ca/eng/open-government-licence-canada', 41 | # (Optional) The date from which the set's boundaries are in effect. 42 | start_date=None, 43 | # (Optional) The date until which the set's boundaries are in effect. 44 | end_date=None, 45 | # (Optional) Free-form text notes, often used to describe changes that were 46 | # made to the original source data, e.g. deleted or merged features. 47 | notes='', 48 | # (Optional) Any additional metadata to include in API responses. 49 | extra={'id': 'ocd-division/country:ca'}, 50 | 51 | 52 | # The following Boundary functions take a feature as an argument and return 53 | # an appropriate value as described below. 54 | # 55 | # In this case, we use helper functions to access and clean attributes from 56 | # the shapefile: 57 | # 58 | # * `attr` retrieves a feature's attribute without making changes. 59 | # * `clean_attr` title-cases a string if it is all-caps, normalizes 60 | # whitespace, and normalizes long dashes. 61 | # * `dashed_attr` does the same as `clean_attr`, but replaces all hyphens 62 | # with long dashes. 63 | # 64 | # If you want to write your own function, set for example `name_func=namer` 65 | # and define a function that looks like: 66 | # 67 | # def namer(f): 68 | # return f.get('FEDENAME') 69 | 70 | # A function that returns a feature's name. 71 | name_func=boundaries.clean_attr('FEDENAME'), 72 | 73 | # (Optional) A function that returns a feature's identifier, which should 74 | # be unique across the features in the shapefile and relatively stable 75 | # across time: for example, a district number or a geographic code. By 76 | # default, features have no identifiers. 77 | id_func=boundaries.attr('FEDUID'), 78 | # (Optional) A function that returns a feature's slug (the last part of its 79 | # URL path). By default, it will use the feature's name. 80 | slug_func=boundaries.attr('FEDUID'), 81 | # (Optional) A function that returns whether a feature should be loaded. By 82 | # default, all features are loaded. 83 | is_valid_func=lambda feature: True, 84 | # (Optional) A function that returns the Point at which to place a label 85 | # for the boundary, in EPSG:4326. 86 | label_point_func=lambda feature: None, 87 | ) 88 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.2"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import django 5 | from django.conf import settings 6 | 7 | if not settings.configured: 8 | ci = os.getenv('CI', False) 9 | 10 | settings.configure( 11 | SECRET_KEY='x', 12 | DATABASES={ 13 | 'default': { 14 | 'ENGINE': 'django.contrib.gis.db.backends.postgis', 15 | 'HOST': 'localhost', 16 | 'NAME': 'postgres' if ci else 'represent_boundaries_test', 17 | 'USER': 'postgres' if ci else '', 18 | 'PASSWORD': 'postgres' if ci else '', 19 | 'PORT': os.getenv('PORT', 5432), 20 | } 21 | }, 22 | INSTALLED_APPS=( 23 | 'django.contrib.admin', 24 | 'django.contrib.auth', 25 | 'django.contrib.contenttypes', 26 | 'django.contrib.gis', 27 | 'django.contrib.sessions', 28 | 'django.contrib.messages', 29 | 'boundaries', 30 | ), 31 | MIDDLEWARE=[ 32 | 'django.contrib.sessions.middleware.SessionMiddleware', 33 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 34 | 'django.contrib.messages.middleware.MessageMiddleware', 35 | ], 36 | TEMPLATES=[ 37 | { 38 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 39 | 'DIRS': [], 40 | 'APP_DIRS': True, 41 | 'OPTIONS': { 42 | 'context_processors': [ 43 | 'django.template.context_processors.debug', 44 | 'django.template.context_processors.request', 45 | 'django.contrib.auth.context_processors.auth', 46 | 'django.contrib.messages.context_processors.messages', 47 | ], 48 | }, 49 | }, 50 | ], 51 | DEFAULT_AUTO_FIELD='django.db.models.AutoField', 52 | ROOT_URLCONF='boundaries.urls', 53 | ) 54 | django.setup() 55 | 56 | if __name__ == '__main__': 57 | from django.test.runner import DiscoverRunner 58 | runner = DiscoverRunner(failfast=False) 59 | failures = runner.run_tests(['boundaries']) 60 | sys.exit(failures) 61 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | To run: env PYTHONPATH=$PWD DJANGO_SETTINGS_MODULE=settings django-admin migrate --noinput 3 | """ 4 | import os 5 | 6 | ci = os.getenv('CI', False) 7 | 8 | SECRET_KEY = 'x' 9 | 10 | DATABASES = { 11 | 'default': { 12 | 'ENGINE': 'django.contrib.gis.db.backends.postgis', 13 | 'HOST': 'localhost', 14 | 'NAME': 'postgres' if ci else 'represent_boundaries', 15 | 'USER': 'postgres' if ci else '', 16 | 'PASSWORD': 'postgres' if ci else '', 17 | 'PORT': os.getenv('PORT', 5432), 18 | } 19 | } 20 | 21 | INSTALLED_APPS = ( 22 | 'django.contrib.admin', 23 | 'django.contrib.auth', 24 | 'django.contrib.contenttypes', 25 | 'django.contrib.gis', 26 | 'django.contrib.sessions', 27 | 'django.contrib.messages', 28 | 'boundaries', 29 | ) 30 | 31 | MIDDLEWARE = [ 32 | 'django.contrib.sessions.middleware.SessionMiddleware', 33 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 34 | 'django.contrib.messages.middleware.MessageMiddleware', 35 | ] 36 | 37 | TEMPLATES = [ 38 | { 39 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 40 | 'DIRS': [], 41 | 'APP_DIRS': True, 42 | 'OPTIONS': { 43 | 'context_processors': [ 44 | 'django.template.context_processors.debug', 45 | 'django.template.context_processors.request', 46 | 'django.contrib.auth.context_processors.auth', 47 | 'django.contrib.messages.context_processors.messages', 48 | ], 49 | }, 50 | }, 51 | ] 52 | 53 | DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 54 | 55 | if 'GDAL_LIBRARY_PATH' in os.environ: 56 | GDAL_LIBRARY_PATH = os.getenv('GDAL_LIBRARY_PATH') 57 | if 'GEOS_LIBRARY_PATH' in os.environ: 58 | GEOS_LIBRARY_PATH = os.getenv('GEOS_LIBRARY_PATH') 59 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = represent-boundaries 3 | version = 0.10.2 4 | author = Open North Inc. 5 | author_email = represent@opennorth.ca 6 | license = MIT 7 | description = A web API to geographic boundaries loaded from shapefiles, packaged as a Django app. 8 | url = https://opennorth.github.io/represent-boundaries-docs/ 9 | long_description = file: README.rst 10 | long_description_content_type = text/x-rst 11 | classifiers = 12 | Programming Language :: Python :: 3 13 | License :: OSI Approved :: MIT License 14 | Framework :: Django 15 | Topic :: Scientific/Engineering :: GIS 16 | 17 | [options] 18 | packages = find: 19 | install_requires = 20 | django-appconf 21 | include_package_data = True 22 | 23 | [options.packages.find] 24 | exclude = boundaries.tests 25 | 26 | [options.extras_require] 27 | test = 28 | coveralls 29 | testfixtures 30 | 31 | [isort] 32 | line_length = 119 33 | profile = black 34 | 35 | [flake8] 36 | max-line-length = 119 37 | exclude = boundaries/migrations 38 | per-file-ignores = 39 | boundaries/tests/*: E501 40 | 41 | [coverage:report] 42 | omit = 43 | */migrations/* 44 | */tests/* 45 | --------------------------------------------------------------------------------