├── .coveragerc ├── .editorconfig ├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── docs └── README.md ├── google-maps-api.png ├── runtests.py ├── screenshot.png ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── test_panels.py ├── test_widgets.py └── testapp │ ├── __init__.py │ ├── core │ ├── __init__.py │ ├── fixtures │ │ └── test_data.json │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ └── templates │ │ └── core │ │ └── map_page.html │ ├── manage.py │ ├── testapp │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py │ └── var │ └── .keep ├── tox.ini └── wagtailgmaps ├── __init__.py ├── panels.py ├── static └── wagtailgmaps │ ├── css │ └── admin.css │ └── js │ └── map-field-panel.js ├── templates └── wagtailgmaps │ └── forms │ └── widgets │ └── map_input.html └── widgets.py /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | [run] 3 | branch = True 4 | 5 | source = ./wagtailgmaps 6 | 7 | [report] 8 | # Regexes for lines to exclude from consideration 9 | exclude_lines = 10 | # Have to re-enable the standard pragma 11 | pragma: no cover 12 | 13 | # Don't complain about missing debug-only code: 14 | def __repr__ 15 | if self\.debug 16 | 17 | # Don't complain if tests don't hit defensive assertion code: 18 | raise AssertionError 19 | raise NotImplementedError 20 | return NotImplemented 21 | 22 | # Don't complain if non-runnable code isn't run: 23 | if 0: 24 | if __name__ == .__main__.: 25 | 26 | ignore_errors = True 27 | 28 | [html] 29 | directory = coverage_html_report 30 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Defines the coding style for different editors and IDEs. 2 | # http://editorconfig.org 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | # Rules for source code. 8 | [*] 9 | charset = utf-8 10 | end_of_line = lf 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 4 15 | 16 | # Makefiles are special. 17 | [Makefile] 18 | indent_style = tab 19 | indent_size = 8 20 | 21 | [*.yml] 22 | indent_size = 2 23 | 24 | # Rules for markdown documents. 25 | [*.md] 26 | trim_trailing_whitespace = false 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'docs/**' 7 | pull_request: 8 | paths-ignore: 9 | - 'docs/**' 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | test-postgres: 20 | runs-on: ubuntu-latest 21 | strategy: 22 | matrix: 23 | include: 24 | - python: '3.9' 25 | django: 'Django>=4.2,<4.3' 26 | wagtail: 'wagtail>=7.0,<7.1' 27 | postgres: 'postgres:15' 28 | - python: '3.10' 29 | django: 'Django>=5.2,<5.3' 30 | wagtail: 'wagtail>=7.0,<7.1' 31 | postgres: 'postgres:15' 32 | - python: '3.11' 33 | django: 'Django>=5.2,<5.3' 34 | wagtail: 'wagtail>=7.0,<7.1' 35 | postgres: 'postgres:15' 36 | - python: '3.12' 37 | django: 'Django>=5.2,<5.3' 38 | wagtail: 'wagtail>=7.0,<7.1' 39 | postgres: 'postgres:15' 40 | - python: '3.13' 41 | django: 'Django>=5.2,<5.3' 42 | wagtail: 'wagtail>=7.0,<7.1' 43 | postgres: 'postgres:15' 44 | 45 | services: 46 | postgres: 47 | image: ${{ matrix.postgres || 'postgres:15' }} 48 | env: 49 | POSTGRES_PASSWORD: postgres 50 | ports: 51 | - 5432:5432 52 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 53 | 54 | steps: 55 | - uses: actions/checkout@v4 56 | with: 57 | persist-credentials: false 58 | - name: Set up Python ${{ matrix.python }} 59 | uses: actions/setup-python@v5 60 | with: 61 | python-version: ${{ matrix.python }} 62 | # cache: 'pip' # we can have this back if we add pyproject.toml 63 | - name: Install dependencies 64 | run: | 65 | python -m pip install --upgrade pip 66 | pip install -e '.[testing]' --config-settings editable_mode=strict 67 | pip install "${{ matrix.django }}" 68 | pip install "${{ matrix.wagtail }}" 69 | ${{ matrix.install_extras }} 70 | - name: Test 71 | run: | 72 | coverage run --parallel-mode --source wagtail runtests.py ${{ matrix.parallel }} 73 | env: 74 | DATABASE_ENGINE: django.db.backends.postgresql 75 | DATABASE_HOST: localhost 76 | DATABASE_USER: postgres 77 | DATABASE_PASSWORD: postgres 78 | - name: Upload coverage data 79 | uses: actions/upload-artifact@v4 80 | with: 81 | name: coverage-data-${{ github.job }}-${{ strategy.job-index }} 82 | path: .coverage.* 83 | include-hidden-files: true 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | .DS_Store 4 | *.pyc 5 | *.pyo 6 | *.egg-info 7 | *.log 8 | *.pot 9 | .venv 10 | *.sqlite3 11 | tests/testapp/var/media/ 12 | .coverage 13 | coverage 14 | coverage_html_report/ 15 | local.py 16 | .tox 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [Unreleased] 9 | 10 | ## [2.0.0] - 2025-04-27 11 | 12 | ### Removed 13 | 14 | - Compatibility with Wagtail 6.3 and before (use wagtailgmap==1.0.1 instead) 15 | 16 | ### Changed 17 | 18 | - (BREAKING) Rename the `latlng` kwarg to `latlngMode` to better reflect it's meaning 19 | - (BREAKING) Rename `edit_handlers` to `panels` in line with Wagtail Core 20 | - Migrated admin JS to Stimulus 21 | - Replace "WidgetWithScript" with a plain input that has `media` 22 | - Migrate CI testing to Github Actions 23 | - Update tox file 24 | - Format code with black 25 | - Updated test app for modern Django (mostly `url` to `path`) 26 | - Documentation updated with guide for setting up API keys (thanks @FlipperPA) 27 | 28 | ## [1.0.1] - 2018-06-25 29 | 30 | Thanks to @scisteffan for their contribution. 31 | 32 | ## Fixed 33 | 34 | - Map would not display correctly in the admin (#36, #37) 35 | 36 | ## [1.0] - 2018-03-09 37 | 38 | ### Removed 39 | 40 | - Compatibility with Wagtail 1.13 and before (use wagtailgmap==0.4 instead) 41 | 42 | ### Changed 43 | 44 | - Simplified the implementation of the MapFieldPanel 45 | - Derive the map ID from the field ID (instead of using a randomly generated ID) 46 | - Simplifies the flow of options between the widget, the template and the JS. 47 | 48 | ### Fixed 49 | 50 | - Compatibility with Wagtail 2.0 51 | - Admin map missing API key for display with noscript 52 | 53 | ## [0.4] - 2018-01-15 54 | 55 | Thanks to @balinabbb for their contribution. 56 | 57 | ### Added 58 | 59 | - Add the possibility to set the (admin) map language with `WAGTAIL_ADDRESS_MAP_LANGUAGE` 60 | 61 | ## [0.3.1] - 2017-11-20 62 | 63 | ### Fixed 64 | 65 | - Installation on Python 2.7.6 would fail 66 | 67 | ## [0.3] - 2017-09-21 68 | 69 | Thanks to @danreeves, @craigloftus, @urlsangel and @SalahAdDin for their contributions. 70 | 71 | ### Added 72 | 73 | - Dedicated `MapFieldPanel` edit handler 74 | 75 | ### Changed 76 | 77 | - License is now MIT 78 | - Do not require `django-overextends` anymore 79 | 80 | ### Fixed 81 | 82 | - Compatibility with Django >= 1.9 83 | 84 | ## [0.2.5] - 2016-04-04 85 | 86 | ### Fixed 87 | 88 | - Compatibility with Wagtail 1.4 89 | 90 | ## [0.2.3] - 2015-09-02 91 | 92 | ### Added 93 | 94 | - Multiple classes allowed on the panel (e.g. classname="gmap col3") 95 | - Added logic to allow outputting a `latlng` value rather than the street address. Adding the `gmap--latlng` modifier class to the panel enables the feature. 96 | 97 | ### Fixed 98 | 99 | - Compatibility with Wagtail 1.0 100 | 101 | ## [0.2.2] - 2015-07-07 102 | 103 | ... 104 | 105 | ## [0.2.1] - 2015-05-25 106 | 107 | ### Fixed 108 | 109 | - Compatibility with Wagtail 1.0b2 110 | 111 | ## [0.2] - 2015-05-12 112 | 113 | ... 114 | 115 | ## [0.1] - 2015-02-27 116 | 117 | Initial Release 118 | 119 | [Unreleased]: https://github.com/springload/wagtailgmaps/compare/2.0.0...HEAD 120 | [2.0.0]: https://github.com/springload/wagtailgmaps/compare/1.0.1...2.0.0 121 | [1.0.1]: https://github.com/springload/wagtailgmaps/compare/v1.0...v1.0.1 122 | [1.0]: https://github.com/springload/wagtailgmaps/compare/v0.4...v1.0 123 | [0.4]: https://github.com/springload/wagtailgmaps/compare/v0.3.1...v0.4 124 | [0.3.1]: https://github.com/springload/wagtailgmaps/compare/v0.3...v0.3.1 125 | [0.3]: https://github.com/springload/wagtailgmaps/compare/v0.2.5...v0.3 126 | [0.2.5]: https://github.com/springload/wagtailgmaps/compare/v0.2.3...v0.2.5 127 | [0.2.3]: https://github.com/springload/wagtailgmaps/compare/v0.2.2...v0.2.3 128 | [0.2.2]: https://github.com/springload/wagtailgmaps/compare/v0.2.1...v0.2.2 129 | [0.2.1]: https://github.com/springload/wagtailgmaps/compare/v0.2...v0.2.1 130 | [0.2]: https://github.com/springload/wagtailgmaps/compare/v0.1...v0.2 131 | [0.1]: https://github.com/springload/wagtailgmaps/compare/9b4372371576da8f96a52cfc225d1c5c1b3c76d1...v0.1 132 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Springload 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE *.rst *.txt *.md 2 | recursive-include wagtailgmaps * 3 | global-exclude __pycache__ 4 | global-exclude *.py[co] 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help init start lint test test-coverage test-ci clean-pyc publish 2 | .DEFAULT_GOAL := help 3 | 4 | help: ## See what commands are available. 5 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36mmake %-15s\033[0m # %s\n", $$1, $$2}' 6 | 7 | init: clean-pyc ## Install dependencies and initialise for development. 8 | pip install -e .[testing,docs] 9 | python ./tests/testapp/manage.py migrate --noinput 10 | python ./tests/testapp/manage.py loaddata test_data 11 | 12 | start: ## Starts the development server. 13 | python ./tests/testapp/manage.py runserver 14 | 15 | lint: ## Lint the project. 16 | flake8 wagtailgmaps tests setup.py 17 | isort --check-only --diff --recursive wagtailgmaps tests setup.py 18 | 19 | test: ## Test the project. 20 | python ./runtests.py 21 | 22 | test-coverage: ## Run the tests while generating test coverage data. 23 | coverage run ./runtests.py && coverage report && coverage html 24 | 25 | test-ci: ## Continuous integration test suite. 26 | tox 27 | 28 | update-test-fixture: ## Update test fixture from the db. 29 | python ./tests/testapp/manage.py dumpdata --indent=4 -e contenttypes -e auth.permission -e auth.group -e sessions -e wagtailcore.site -e wagtailcore.pagerevision -e wagtailcore.grouppagepermission -e wagtailimages.rendition -e wagtailcore.collection -e wagtailcore.groupcollectionpermission > tests/testapp/core/fixtures/test_data.json 30 | 31 | clean-pyc: ## Remove Python file artifacts. 32 | find . -name '*.pyc' -exec rm -f {} + 33 | find . -name '*.pyo' -exec rm -f {} + 34 | find . -name '*~' -exec rm -f {} + 35 | 36 | publish: ## Publishes a new version to pypi. 37 | rm dist/* && python setup.py sdist && twine upload dist/* && echo 'Success! Go to https://pypi.python.org/pypi/wagtailgmaps and check that all is well.' 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wagtailgmaps [![PyPI](https://img.shields.io/pypi/v/wagtailgmaps.svg)](https://pypi.python.org/pypi/wagtailgmaps) 2 | 3 | > Simple Google Maps address formatter and LatLng provider for [Wagtail](https://wagtail.io/) fields. 4 | 5 | *Check out [Awesome Wagtail](https://github.com/springload/awesome-wagtail) for more awesome packages and resources from the Wagtail community.* 6 | 7 | ![Wagtailgmaps screenshot](./screenshot.png) 8 | 9 | ## Quickstart 10 | 11 | ### Setting Up Your Google API Key 12 | 13 | 1. Follow the instructions to [get a key](https://developers.google.com/maps/documentation/javascript/get-api-key) 14 | 2. Enable the following services under `API Restrictions`: 15 | * [Geocoding API](https://developers.google.com/maps/documentation/javascript/geocoding) 16 | * [Maps Static API](https://developers.google.com/maps/documentation/static-maps/) 17 | * [Maps JavaScript API](https://developers.google.com/maps/documentation/javascript/) 18 | * [Maps Embed API](https://developers.google.com/maps/documentation/javascript/) 19 | 20 | ![Google_API_Screenshot](./google-maps-api.png) 21 | 22 | ### Installing and Configuration the Python Package 23 | 24 | 1. Install with `pip install wagtailgmaps` 25 | 2. Add `wagtailgmaps` to your `settings.py` `INSTALLED_APPS` section. 26 | 3. Add some configuration in your `settings.py` file: 27 | 28 | ```python 29 | # Mandatory 30 | WAGTAIL_ADDRESS_MAP_CENTER = 'Wellington, New Zealand' # It must be a properly formatted address 31 | WAGTAIL_ADDRESS_MAP_KEY = 'xxx' 32 | 33 | # Optional 34 | WAGTAIL_ADDRESS_MAP_ZOOM = 8 # See https://developers.google.com/maps/documentation/javascript/tutorial#MapOptions for more information. 35 | WAGTAIL_ADDRESS_MAP_LANGUAGE = 'ru' # See https://developers.google.com/maps/faq#languagesupport for supported languages. 36 | ``` 37 | 38 | 4. Use it: 39 | 40 | ```python 41 | # myapp/models.py 42 | from django.db import models 43 | from wagtail.wagtailcore.models import Page 44 | from wagtailgmaps.edit_handlers import MapFieldPanel 45 | 46 | class MapPage(Page): 47 | # Wagtailgmaps expects a `CharField` (or any other field that renders as a text input) 48 | formatted_address = models.CharField(max_length=255) 49 | latlng_address = models.CharField(max_length=255) 50 | 51 | # Use the `MapFieldPanel` just like you would use a `FieldPanel` 52 | content_panels = Page.content_panels + [ 53 | MapFieldPanel('formatted_address'), 54 | MapFieldPanel('latlng_address', latlng=True), 55 | ] 56 | ``` 57 | 58 | ```html 59 | # myapp/templates/myapp/map_page.html 60 | Open map (Formatted Address) 61 | Open map (Lat/Long Address) 62 | ``` 63 | 64 | ## Additional information 65 | 66 | ### `MapFieldPanel` options 67 | 68 | - `heading` - A custom heading in the admin, defaults to "Location" 69 | - `classname` - Add extra css classes to the field 70 | - `latlng` - Field returns a LatLng instead of an address 71 | - `centre` - A custom override for this field 72 | - `zoom` - A custom override for this field 73 | 74 | ### How the address option works under the hook 75 | 76 | If using the address option, the field gets updated according to the [Google Geocoding Service](https://developers.google.com/maps/documentation/geocoding/) each time: 77 | 78 | * The map marker gets dragged and dropped into a location (`dragend` JS event). 79 | * Click happens somewhere in the map (`click` JS event). 80 | * Return key is pressed after editing the field (`enterKey` JS event for return key only). 81 | 82 | ### Troubleshooting 83 | 84 | When editing the model from the admin interface the affected field shows up with a map, like the screenshot above. If it doesn't, check your browser console to make sure that there is no error related to your API key. 85 | 86 | ## Development 87 | 88 | ### Releases 89 | 90 | - Make a new branch for the release of the new version. 91 | - Update the [CHANGELOG](https://github.com/springload/wagtailgmaps/CHANGELOG.md). 92 | - Update the version number in `wagtailgmaps/__init__.py`, following semver. 93 | - Make a PR and squash merge it. 94 | - Back on master with the PR merged, use `make publish` (confirm, and enter your password). 95 | - Finally, go to GitHub and create a release and a tag for the new version. 96 | - Done! 97 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ## Browser support 4 | 5 | We align our browser support targets with that of Wagtail. Have a look at the [official documentation](http://docs.wagtail.io/en/latest/contributing/developing.html). 6 | 7 | ## Python/Django/Wagtail support 8 | 9 | Python versions as defined in `setup.py` classifiers. 10 | 11 | Wagtail versions `>=7.0` and as [supported](http://docs.wagtail.io/en/latest/releases/upgrading.html) by Wagtail (LTS, current and current-1). 12 | 13 | Django/Wagtail combinations as [supported](http://docs.wagtail.io/en/latest/releases/upgrading.html#compatible-django-python-versions) by Wagtail (for the Wagtail versions as defined above). 14 | 15 | Note: The last version of this plugin to support Wagtail prior to 2.0 is `v0.4`. 16 | Note: The last version of this plugin to support Wagtail prior to 7.0 is `v1.0.1`. 17 | 18 | ### Which version combinations to include in Travis test matrix? 19 | 20 | In order to keep for CI build time from growing out of control, not all Python/Django/Wagtail combinations will be tested. 21 | 22 | Test as follow: 23 | 24 | - All supported Django/Wagtail combinations with the latest supported Python version. 25 | - The latest supported Django/Wagtail combination for the remaining Python versions. 26 | -------------------------------------------------------------------------------- /google-maps-api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/springload/wagtailgmaps/636acbc78169835f00215b481d6b11c2fd63bbf2/google-maps-api.png -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | 6 | 7 | def run(): 8 | from django.core.management import execute_from_command_line 9 | os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.testapp.testapp.settings' 10 | 11 | sys.path.append(os.path.join('tests', 'testapp')) 12 | execute_from_command_line([sys.argv[0], 'test'] + sys.argv[1:]) 13 | 14 | 15 | if __name__ == '__main__': 16 | run() 17 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/springload/wagtailgmaps/636acbc78169835f00215b481d6b11c2fd63bbf2/screenshot.png -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E303,F403,F401 3 | max-line-length = 120 4 | exclude = 5 | migrations, 6 | 7 | [isort] 8 | line_length=100 9 | multi_line_output=3 10 | skip= 11 | migrations, 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_packages, setup 4 | 5 | from wagtailgmaps import __version__ 6 | 7 | with open(os.path.join(os.path.dirname(__file__), "README.md")) as readme: 8 | README = readme.read() 9 | 10 | # allow setup.py to be run from any path 11 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 12 | 13 | # Package dependencies 14 | install_requires = [] 15 | 16 | # Testing dependencies 17 | testing_extras = [ 18 | # Required for running the tests 19 | "tox~=4.26.0", 20 | # For coverage and PEP8 linting 21 | "coverage>=4.1.0,<4.2", 22 | "flake8>=3.2.0,<3.3", 23 | "flake8-colors>=0.1.6,<1", 24 | "isort==4.2.5", 25 | # For test site 26 | "Django>=4.2", 27 | "wagtail>=7.0.0", 28 | ] 29 | 30 | # Documentation dependencies 31 | documentation_extras = [] 32 | 33 | setup( 34 | name="wagtailgmaps", 35 | version=__version__, 36 | packages=find_packages(), 37 | include_package_data=True, 38 | license="MIT", 39 | description="Google Maps widget for address fields in Wagtail", 40 | long_description=README, 41 | long_description_content_type="text/markdown", 42 | url="https://github.com/springload/wagtailgmaps/", 43 | author="Springload", 44 | author_email="hello@springload.co.nz", 45 | project_urls={ 46 | "Changelog": "https://github.com/springload/wagtailgmaps/blob/master/CHANGELOG.md", 47 | }, 48 | classifiers=[ 49 | "Environment :: Web Environment", 50 | "Framework :: Django", 51 | "Intended Audience :: Developers", 52 | "License :: OSI Approved :: MIT License", 53 | "Operating System :: OS Independent", 54 | "Programming Language :: Python", 55 | "Programming Language :: Python :: 3", 56 | "Programming Language :: Python :: 3.9", 57 | "Programming Language :: Python :: 3.10", 58 | "Programming Language :: Python :: 3.11", 59 | "Programming Language :: Python :: 3.12", 60 | "Programming Language :: Python :: 3.13", 61 | "Topic :: Internet :: WWW/HTTP", 62 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 63 | ], 64 | install_requires=install_requires, 65 | extras_require={ 66 | "testing": testing_extras, 67 | "docs": documentation_extras, 68 | }, 69 | ) 70 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/springload/wagtailgmaps/636acbc78169835f00215b481d6b11c2fd63bbf2/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_panels.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from django.conf import settings 4 | from django.test import SimpleTestCase 5 | 6 | from wagtailgmaps.panels import MapFieldPanel 7 | from wagtailgmaps.widgets import MapInput 8 | 9 | 10 | class EditHandlersTestCase(SimpleTestCase): 11 | def test_init_with_defaults(self): 12 | panel = MapFieldPanel("field-name") 13 | 14 | self.assertEqual(panel.default_centre, settings.WAGTAIL_ADDRESS_MAP_CENTER) 15 | self.assertEqual(panel.zoom, 8) 16 | self.assertEqual(panel.latlng, False) 17 | 18 | def test_init_with_values(self): 19 | panel = MapFieldPanel( 20 | "field-name", 21 | centre="somewhere", 22 | zoom=0, 23 | latlng=True, 24 | ) 25 | 26 | self.assertEqual(panel.default_centre, "somewhere") 27 | self.assertEqual(panel.zoom, 0) 28 | self.assertEqual(panel.latlng, True) 29 | 30 | def test_clone(self): 31 | panel = MapFieldPanel( 32 | "field-name", 33 | centre="somewhere", 34 | zoom=0, 35 | latlng=True, 36 | ) 37 | clone = panel.clone() 38 | 39 | self.assertEqual(panel.field_name, clone.field_name) 40 | self.assertEqual(panel.default_centre, clone.default_centre) 41 | self.assertEqual(panel.zoom, clone.zoom) 42 | self.assertEqual(panel.latlng, clone.latlng) 43 | 44 | @unittest.skip("TODO: Bind the panel to the model for the test to succeed.") 45 | def test_classes(self): 46 | panel = MapFieldPanel( 47 | "field-name", 48 | centre="somewhere", 49 | zoom=0, 50 | latlng=True, 51 | ) 52 | classes = panel.classes() 53 | 54 | self.assertIn("wagtailgmap", classes) 55 | -------------------------------------------------------------------------------- /tests/test_widgets.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.test import SimpleTestCase, override_settings 3 | 4 | from wagtailgmaps.widgets import MapInput 5 | 6 | 7 | class MapInputTestCasse(SimpleTestCase): 8 | def _get_init_data(self, **kwargs): 9 | data = { 10 | "default_centre": "Springload, Te Aro, Wellington, New Zealand", 11 | "zoom": 8, 12 | "latlngMode": False, 13 | } 14 | 15 | if kwargs: 16 | data.update(kwargs) 17 | 18 | return data 19 | 20 | def _get_widget(self): 21 | data = self._get_init_data() 22 | return MapInput(**data) 23 | 24 | def test_factory_is_valid(self): 25 | data = self._get_init_data() 26 | widget = MapInput(**data) 27 | 28 | self.assertEqual(widget.default_centre, data["default_centre"]) 29 | self.assertEqual(widget.zoom, data["zoom"]) 30 | self.assertEqual(widget.latlngMode, data["latlngMode"]) 31 | 32 | @override_settings() 33 | def test_init_raises_for_missing_api_key(self): 34 | del settings.WAGTAIL_ADDRESS_MAP_KEY 35 | 36 | data = self._get_init_data() 37 | with self.assertRaises(Exception) as cm: 38 | MapInput(**data) 39 | 40 | self.assertEqual( 41 | str(cm.exception), "Google Maps API key is missing from settings" 42 | ) 43 | 44 | def test_get_map_centre_for_none(self): 45 | data = self._get_init_data() 46 | widget = MapInput(**data) 47 | 48 | map_centre = widget.get_map_centre(None) 49 | self.assertEqual(map_centre, data["default_centre"]) 50 | 51 | def test_get_map_centre_for_empty_value(self): 52 | data = self._get_init_data() 53 | widget = MapInput(**data) 54 | 55 | map_centre = widget.get_map_centre("") 56 | self.assertEqual(map_centre, data["default_centre"]) 57 | 58 | def test_get_map_centre_with_value(self): 59 | data = self._get_init_data() 60 | widget = MapInput(**data) 61 | 62 | given_address = "Torchbox, Charlbury, Chipping Norton, UK" 63 | map_centre = widget.get_map_centre(given_address) 64 | self.assertEqual(map_centre, given_address) 65 | 66 | def test_get_map_id(self): 67 | data = self._get_init_data() 68 | widget = MapInput(**data) 69 | 70 | field_id = "the-field" 71 | expected_map_id = "{}-map-canvas".format(field_id) 72 | map_id = widget.get_map_id(field_id) 73 | self.assertEqual(map_id, expected_map_id) 74 | 75 | def test_get_map_id_raises_with_no_field_id(self): 76 | data = self._get_init_data() 77 | widget = MapInput(**data) 78 | 79 | with self.assertRaises(AssertionError): 80 | widget.get_map_id(None) 81 | 82 | with self.assertRaises(AssertionError): 83 | widget.get_map_id("") 84 | 85 | def test_get_context(self): 86 | data = self._get_init_data() 87 | widget = MapInput(**data) 88 | field_id = "the-id" 89 | 90 | expected_context = { 91 | "address": data["default_centre"], 92 | "zoom": data["zoom"], 93 | "map_id": widget.get_map_id(field_id), 94 | "gmaps_api_key": settings.WAGTAIL_ADDRESS_MAP_KEY, 95 | } 96 | context = widget.get_context("the-name", None, {"id": field_id}) 97 | 98 | self.assertTrue(expected_context.items() <= context.items()) 99 | 100 | def test_media_css(self): 101 | data = self._get_init_data() 102 | widget = MapInput(**data) 103 | 104 | css = widget.media._css 105 | self.assertEqual(len(css), 1) 106 | self.assertIn("screen", css) 107 | self.assertEqual(len(css["screen"]), 1) 108 | self.assertEqual(css["screen"][0], "wagtailgmaps/css/admin.css") 109 | 110 | def test_media_js(self): 111 | data = self._get_init_data() 112 | widget = MapInput(**data) 113 | 114 | js = widget.media._js 115 | self.assertEqual(len(js), 2) 116 | self.assertEqual( 117 | js[0], 118 | "https://maps.googleapis.com/maps/api/js?key={}".format( 119 | settings.WAGTAIL_ADDRESS_MAP_KEY 120 | ), 121 | ) 122 | self.assertEqual(js[1], "wagtailgmaps/js/map-field-panel.js") 123 | 124 | @override_settings(WAGTAIL_ADDRESS_MAP_LANGUAGE="ru") 125 | def test_media_js_map_with_lang(self): 126 | data = self._get_init_data() 127 | widget = MapInput(**data) 128 | 129 | js = widget.media._js 130 | self.assertIn("&language=ru", js[0]) 131 | -------------------------------------------------------------------------------- /tests/testapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/springload/wagtailgmaps/636acbc78169835f00215b481d6b11c2fd63bbf2/tests/testapp/__init__.py -------------------------------------------------------------------------------- /tests/testapp/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/springload/wagtailgmaps/636acbc78169835f00215b481d6b11c2fd63bbf2/tests/testapp/core/__init__.py -------------------------------------------------------------------------------- /tests/testapp/core/fixtures/test_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "auth.user", 4 | "pk": 1, 5 | "fields": { 6 | "password": "pbkdf2_sha256$36000$WHGVXpwPNyC3$nSSN9jA0BOTa/C2P22XWE+BG/oNuzKRZoxM+Is95JWU=", 7 | "last_login": "2018-02-22T03:14:58.695Z", 8 | "is_superuser": true, 9 | "username": "admin", 10 | "first_name": "", 11 | "last_name": "", 12 | "email": "", 13 | "is_staff": true, 14 | "is_active": true, 15 | "date_joined": "2018-02-22T03:14:52.999Z", 16 | "groups": [], 17 | "user_permissions": [] 18 | } 19 | }, 20 | { 21 | "model": "wagtailcore.page", 22 | "pk": 1, 23 | "fields": { 24 | "path": "0001", 25 | "depth": 1, 26 | "numchild": 1, 27 | "title": "Root", 28 | "draft_title": "Root", 29 | "slug": "root", 30 | "content_type": 1, 31 | "live": true, 32 | "has_unpublished_changes": false, 33 | "url_path": "/", 34 | "owner": null, 35 | "seo_title": "", 36 | "show_in_menus": false, 37 | "search_description": "", 38 | "go_live_at": null, 39 | "expire_at": null, 40 | "expired": false, 41 | "locked": false, 42 | "first_published_at": null, 43 | "last_published_at": null, 44 | "latest_revision_created_at": null, 45 | "live_revision": null 46 | } 47 | }, 48 | { 49 | "model": "wagtailcore.page", 50 | "pk": 2, 51 | "fields": { 52 | "path": "00010001", 53 | "depth": 2, 54 | "numchild": 1, 55 | "title": "Welcome to your new Wagtail site!", 56 | "draft_title": "Welcome to your new Wagtail site!", 57 | "slug": "home", 58 | "content_type": 1, 59 | "live": true, 60 | "has_unpublished_changes": false, 61 | "url_path": "/home/", 62 | "owner": null, 63 | "seo_title": "", 64 | "show_in_menus": false, 65 | "search_description": "", 66 | "go_live_at": null, 67 | "expire_at": null, 68 | "expired": false, 69 | "locked": false, 70 | "first_published_at": null, 71 | "last_published_at": null, 72 | "latest_revision_created_at": null, 73 | "live_revision": null 74 | } 75 | }, 76 | { 77 | "model": "wagtailcore.page", 78 | "pk": 3, 79 | "fields": { 80 | "path": "000100010001", 81 | "depth": 3, 82 | "numchild": 0, 83 | "title": "Map", 84 | "draft_title": "Map", 85 | "slug": "map", 86 | "content_type": 5, 87 | "live": true, 88 | "has_unpublished_changes": false, 89 | "url_path": "/home/map/", 90 | "owner": 1, 91 | "seo_title": "", 92 | "show_in_menus": false, 93 | "search_description": "", 94 | "go_live_at": null, 95 | "expire_at": null, 96 | "expired": false, 97 | "locked": false, 98 | "first_published_at": "2018-02-22T03:15:27.187Z", 99 | "last_published_at": "2018-02-22T03:15:27.187Z", 100 | "latest_revision_created_at": "2018-02-22T03:15:27.167Z", 101 | "live_revision": null 102 | } 103 | }, 104 | { 105 | "model": "core.mappage", 106 | "pk": 3, 107 | "fields": { 108 | "formatted_address": "Springload, Te Aro, Wellington, New Zealand", 109 | "latlng_address": "-41.29244389999999, 174.77836619999994" 110 | } 111 | } 112 | ] 113 | -------------------------------------------------------------------------------- /tests/testapp/core/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-02-22 03:19 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('wagtailcore', '0040_page_draft_title'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='MapPage', 18 | fields=[ 19 | ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), 20 | ('formatted_address', models.CharField(max_length=255)), 21 | ('latlng_address', models.CharField(max_length=255)), 22 | ], 23 | options={ 24 | 'abstract': False, 25 | }, 26 | bases=('wagtailcore.page',), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /tests/testapp/core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/springload/wagtailgmaps/636acbc78169835f00215b481d6b11c2fd63bbf2/tests/testapp/core/migrations/__init__.py -------------------------------------------------------------------------------- /tests/testapp/core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from wagtail.admin.panels import MultiFieldPanel 3 | from wagtail.models import Page 4 | 5 | from wagtailgmaps.panels import MapFieldPanel 6 | 7 | 8 | class MapPage(Page): 9 | formatted_address = models.CharField(max_length=255) 10 | latlng_address = models.CharField(max_length=255) 11 | 12 | content_panels = Page.content_panels + [ 13 | MapFieldPanel("formatted_address"), 14 | MultiFieldPanel( 15 | [ 16 | MapFieldPanel("latlng_address", latlng=True), 17 | ], 18 | "LatLng Address (nested)", 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /tests/testapp/core/templates/core/map_page.html: -------------------------------------------------------------------------------- 1 | {% load wagtailcore_tags %} 2 | 3 | 4 | {{ page.title }} 5 | 6 | 7 |

{{ page.title }}

8 | Open map (Formatted Address) 9 | Open map (Lat/Long Address) 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/testapp/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /tests/testapp/testapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/springload/wagtailgmaps/636acbc78169835f00215b481d6b11c2fd63bbf2/tests/testapp/testapp/__init__.py -------------------------------------------------------------------------------- /tests/testapp/testapp/settings.py: -------------------------------------------------------------------------------- 1 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 2 | import os 3 | 4 | PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 5 | BASE_DIR = os.path.dirname(PROJECT_DIR) 6 | 7 | 8 | # Quick-start development settings - unsuitable for production 9 | # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ 10 | 11 | 12 | # Application definition 13 | 14 | INSTALLED_APPS = [ 15 | "core", 16 | "wagtailgmaps", 17 | "wagtail.sites", 18 | "wagtail.users", 19 | "wagtail.admin", 20 | "wagtail", 21 | "wagtail.documents", 22 | "wagtail.images", 23 | "modelcluster", 24 | "taggit", 25 | "django.contrib.admin", 26 | "django.contrib.auth", 27 | "django.contrib.contenttypes", 28 | "django.contrib.sessions", 29 | "django.contrib.messages", 30 | "django.contrib.staticfiles", 31 | ] 32 | 33 | MIDDLEWARE = [ 34 | "django.contrib.sessions.middleware.SessionMiddleware", 35 | "django.middleware.common.CommonMiddleware", 36 | "django.middleware.csrf.CsrfViewMiddleware", 37 | "django.contrib.auth.middleware.AuthenticationMiddleware", 38 | "django.contrib.messages.middleware.MessageMiddleware", 39 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 40 | "django.middleware.security.SecurityMiddleware", 41 | "wagtail.middleware.SiteMiddleware", 42 | ] 43 | 44 | ROOT_URLCONF = "testapp.urls" 45 | 46 | TEMPLATES = [ 47 | { 48 | "BACKEND": "django.template.backends.django.DjangoTemplates", 49 | "DIRS": [ 50 | os.path.join(PROJECT_DIR, "templates"), 51 | ], 52 | "APP_DIRS": True, 53 | "OPTIONS": { 54 | "context_processors": [ 55 | "django.template.context_processors.debug", 56 | "django.template.context_processors.request", 57 | "django.contrib.auth.context_processors.auth", 58 | "django.contrib.messages.context_processors.messages", 59 | ], 60 | }, 61 | }, 62 | ] 63 | 64 | WSGI_APPLICATION = "testapp.wsgi.application" 65 | 66 | # Database 67 | # https://docs.djangoproject.com/en/1.10/ref/settings/#databases 68 | 69 | DATABASES = { 70 | "default": { 71 | "ENGINE": "django.db.backends.sqlite3", 72 | "NAME": os.path.join(PROJECT_DIR, "var", "db.sqlite3"), 73 | } 74 | } 75 | 76 | # Internationalization 77 | # https://docs.djangoproject.com/en/1.10/topics/i18n/ 78 | 79 | LANGUAGE_CODE = "en-us" 80 | 81 | TIME_ZONE = "UTC" 82 | 83 | USE_I18N = True 84 | 85 | USE_L10N = True 86 | 87 | USE_TZ = True 88 | 89 | # Static files (CSS, JavaScript, Images) 90 | # https://docs.djangoproject.com/en/1.10/howto/static-files/ 91 | 92 | STATICFILES_FINDERS = [ 93 | "django.contrib.staticfiles.finders.FileSystemFinder", 94 | "django.contrib.staticfiles.finders.AppDirectoriesFinder", 95 | ] 96 | 97 | STATICFILES_DIRS = [ 98 | os.path.join(PROJECT_DIR, "static"), 99 | ] 100 | 101 | STATIC_ROOT = os.path.join(BASE_DIR, "static") 102 | STATIC_URL = "/static/" 103 | 104 | MEDIA_ROOT = os.path.join(PROJECT_DIR, "var", "media") 105 | MEDIA_URL = "/media/" 106 | 107 | 108 | # Wagtail settings 109 | 110 | WAGTAIL_SITE_NAME = "testapp" 111 | 112 | # Base URL to use when referring to full URLs within the Wagtail admin backend - 113 | # e.g. in notification emails. Don't include '/admin' or a trailing slash 114 | BASE_URL = "http://example.com" 115 | 116 | # SECURITY WARNING: don't run with debug turned on in production! 117 | DEBUG = True 118 | 119 | # SECURITY WARNING: keep the secret key used in production secret! 120 | SECRET_KEY = "4*5e^@2%(h#$*b4=ze_kcdw46-$0z#rrf3661c5(&+x^oj=4)+" 121 | 122 | EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" 123 | 124 | # Wagtailgmaps 125 | WAGTAIL_ADDRESS_MAP_KEY = "DummyKey" 126 | WAGTAIL_ADDRESS_MAP_CENTER = "Wellington, New Zealand" 127 | WAGTAIL_ADDRESS_MAP_ZOOM = 8 128 | 129 | try: 130 | from .local import * 131 | except ImportError: 132 | pass 133 | -------------------------------------------------------------------------------- /tests/testapp/testapp/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from wagtail.admin import urls as wagtailadmin_urls 3 | from wagtail import urls as wagtail_urls 4 | from wagtail.documents import urls as wagtaildocs_urls 5 | from wagtail.images import urls as wagtailimages_urls 6 | 7 | from django.urls import include, path 8 | 9 | 10 | urlpatterns = [ 11 | path("admin/", include(wagtailadmin_urls)), 12 | path("documents/", include(wagtaildocs_urls)), 13 | path("images/", include(wagtailimages_urls)), 14 | path("", include(wagtail_urls)), 15 | ] 16 | 17 | 18 | if settings.DEBUG: 19 | from django.conf.urls.static import static 20 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 21 | 22 | # Serve static and media files from development server 23 | urlpatterns += staticfiles_urlpatterns() 24 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 25 | -------------------------------------------------------------------------------- /tests/testapp/testapp/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for testapp project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /tests/testapp/var/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/springload/wagtailgmaps/636acbc78169835f00215b481d6b11c2fd63bbf2/tests/testapp/var/.keep -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | skipsdist = True 8 | usedevelop = True 9 | envlist = 10 | py{39,310,311,312,313}-dj{42,51,52}-wt{7} 11 | 12 | [testenv] 13 | install_command = pip install -e ".[testing]" -U {opts} {packages} 14 | whitelist_externals = 15 | make 16 | pip 17 | 18 | basepython = 19 | py39: python3.9 20 | py310: python3.10 21 | py311: python3.11 22 | py312: python3.12 23 | py313: python3.13 24 | 25 | deps = 26 | dj42: Django>=4.2,<4.3 27 | dj51: Django>=5.1,<5.2 28 | dj52: Django>=5.2,<5,3 29 | wt7: wagtail>=7.0,<7.1 30 | 31 | commands = 32 | make lint 33 | make test-coverage 34 | 35 | [gh] 36 | python = 37 | 3.13 = 3.13 38 | 3.12 = 3.12 39 | 3.11 = 3.11 40 | -------------------------------------------------------------------------------- /wagtailgmaps/__init__.py: -------------------------------------------------------------------------------- 1 | __title__ = "wagtailgmaps" 2 | __version__ = "2.0.0" 3 | __author__ = "Springload" 4 | __license__ = "MIT" 5 | __copyright__ = "Copyright 2025 Springload" 6 | -------------------------------------------------------------------------------- /wagtailgmaps/panels.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from wagtail.admin.panels import FieldPanel 3 | 4 | from .widgets import MapInput 5 | 6 | 7 | class MapFieldPanel(FieldPanel): 8 | def __init__(self, field_name, *args, **kwargs): 9 | self.default_centre = kwargs.pop( 10 | "centre", getattr(settings, "WAGTAIL_ADDRESS_MAP_CENTER", None) 11 | ) 12 | self.zoom = kwargs.pop("zoom", getattr(settings, "WAGTAIL_ADDRESS_MAP_ZOOM", 8)) 13 | self.latlng = kwargs.pop("latlng", False) 14 | 15 | kwargs["widget"] = kwargs.get( 16 | "widget", 17 | MapInput( 18 | default_centre=self.default_centre, 19 | zoom=self.zoom, 20 | latlngMode=self.latlng, 21 | ), 22 | ) 23 | 24 | super().__init__(field_name, *args, **kwargs) 25 | 26 | def clone(self): 27 | instance = super().clone() 28 | 29 | instance.default_centre = self.default_centre 30 | instance.zoom = self.zoom 31 | instance.latlng = self.latlng 32 | 33 | return instance 34 | 35 | def classes(self): 36 | classes = super().classes() 37 | classes.append("wagtailgmap") 38 | return classes 39 | -------------------------------------------------------------------------------- /wagtailgmaps/static/wagtailgmaps/css/admin.css: -------------------------------------------------------------------------------- 1 | /* Avoid default max-width: 100%; to display map controls correctly */ 2 | li.wagtailgmap { 3 | overflow: hidden; 4 | -webkit-perspective: 1000; 5 | -moz-perspective: 1000; 6 | perspective: 1000; 7 | clear: both; 8 | } 9 | 10 | li.wagtailgmap img { 11 | max-width: none; 12 | } 13 | 14 | .map-googlemap { 15 | margin-top: 20px; 16 | } 17 | -------------------------------------------------------------------------------- /wagtailgmaps/static/wagtailgmaps/js/map-field-panel.js: -------------------------------------------------------------------------------- 1 | class MapInputController extends window.StimulusModule.Controller { 2 | static targets = ["map", "textbox"]; 3 | connect() { 4 | // One geocoder var to rule them all 5 | this.geocoder = new google.maps.Geocoder(); 6 | 7 | // Trigger the event so the maps can start doing their things 8 | var event; // The custom event that will be created 9 | if (document.createEvent) { 10 | event = document.createEvent("HTMLEvents"); 11 | event.initEvent("wagtailmaps_ready", true, true); 12 | } else { 13 | event = document.createEventObject(); 14 | event.eventType = "wagtailmaps_ready"; 15 | } 16 | 17 | event.eventName = "wagtailmaps_ready"; 18 | 19 | if (document.createEvent) { 20 | document.dispatchEvent(event); 21 | } else { 22 | document.fireEvent("on" + event.eventType, event); 23 | } 24 | 25 | this.initialize_map({ 26 | address: this.textboxTarget.value, 27 | zoom: Number(this.element.dataset.zoom), 28 | latlngMode: Boolean(this.element.dataset.latlngMode), 29 | }); 30 | } 31 | 32 | // Method to initialize a map and all of its related components (usually address input and marker) 33 | initialize_map(params) { 34 | const controller = this; 35 | // Get latlong from address to initialize map 36 | this.geocoder.geocode( 37 | { address: params.address }, 38 | function (results, status) { 39 | if (status == google.maps.GeocoderStatus.OK) { 40 | controller.setAddress( 41 | results[0].geometry.location, 42 | params.zoom, 43 | params.latlngMode 44 | ); 45 | } else { 46 | alert( 47 | "Geocode was not successful for the following reason: " + 48 | status 49 | ); 50 | } 51 | } 52 | ); 53 | } 54 | 55 | // Get formatted address from LatLong position 56 | geocodePosition(pos, input, latlngMode) { 57 | this.geocoder.geocode( 58 | { 59 | latLng: pos, 60 | }, 61 | function (responses) { 62 | if (responses && responses.length > 0) { 63 | if (latlngMode) { 64 | input.value = 65 | String(responses[0].geometry.location.lat()) + 66 | ", " + 67 | String(responses[0].geometry.location.lng()); 68 | } else { 69 | input.value = responses[0].formatted_address; 70 | } 71 | } else { 72 | alert("Cannot determine address at this location."); 73 | } 74 | } 75 | ); 76 | } 77 | 78 | // Get LatLong position and formatted address from inaccurate address string 79 | geocodeAddress(address, input, latlngMode, marker, map) { 80 | this.geocoder.geocode({ address: address }, function (results, status) { 81 | if (status == google.maps.GeocoderStatus.OK) { 82 | marker.setPosition(results[0].geometry.location); 83 | if (latlngMode) { 84 | input.value = 85 | String(results[0].geometry.location.lat()) + 86 | ", " + 87 | String(results[0].geometry.location.lng()); 88 | } else { 89 | input.value = results[0].formatted_address; 90 | } 91 | map.setCenter(results[0].geometry.location); 92 | } else { 93 | alert( 94 | "Geocode was not successful for the following reason: " + 95 | status 96 | ); 97 | } 98 | }); 99 | } 100 | 101 | setAddress(latlng, zoom, latlngMode) { 102 | // Create map options and map 103 | var mapOptions = { 104 | zoom: zoom, 105 | center: latlng, 106 | mapTypeId: google.maps.MapTypeId.ROADMAP, 107 | }; 108 | 109 | this.map = new google.maps.Map(this.mapTarget, mapOptions); 110 | this.marker = new google.maps.Marker({ 111 | position: latlng, 112 | map: this.map, 113 | draggable: true, 114 | }); 115 | 116 | const controller = this; 117 | // Set events listeners to update marker/input values/positions 118 | google.maps.event.addListener(this.marker, "dragend", function (event) { 119 | controller.geocodePosition( 120 | controller.marker.getPosition(), 121 | controller.textboxTarget, 122 | latlngMode 123 | ); 124 | }); 125 | google.maps.event.addListener(this.map, "click", function (event) { 126 | controller.marker.setPosition(event.latLng); 127 | controller.geocodePosition( 128 | controller.marker.getPosition(), 129 | controller.textboxTarget, 130 | latlngMode 131 | ); 132 | }); 133 | 134 | // Event listeners to update map when press enter or tab 135 | $(this.textboxTarget).bind("enterKey", function (event) { 136 | controller.geocodeAddress( 137 | this.value, 138 | controller, 139 | latlngMode, 140 | controller.marker, 141 | controller.map 142 | ); 143 | }); 144 | 145 | $(this.textboxTarget).keypress(function (event) { 146 | if (event.keyCode == 13) { 147 | event.preventDefault(); 148 | $(this).trigger("enterKey"); 149 | } 150 | }); 151 | } 152 | } 153 | 154 | window.wagtail.app.register("map-input", MapInputController); 155 | -------------------------------------------------------------------------------- /wagtailgmaps/templates/wagtailgmaps/forms/widgets/map_input.html: -------------------------------------------------------------------------------- 1 |
9 | {% include "django/forms/widgets/text.html" %} 10 |
11 |
15 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /wagtailgmaps/widgets.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.forms import Media, TextInput 3 | 4 | 5 | class MapInput(TextInput): 6 | template_name = "wagtailgmaps/forms/widgets/map_input.html" 7 | 8 | def __init__(self, default_centre, zoom, latlngMode, attrs=None): 9 | self.default_centre = default_centre 10 | self.zoom = zoom 11 | self.latlngMode = latlngMode 12 | attrs = (attrs or {}) | {"data-map-input-target": "textbox"} 13 | 14 | try: 15 | self.apikey = settings.WAGTAIL_ADDRESS_MAP_KEY 16 | except AttributeError: 17 | raise Exception("Google Maps API key is missing from settings") 18 | 19 | super().__init__(attrs) 20 | 21 | def get_map_centre(self, value): 22 | return value or self.default_centre 23 | 24 | def get_map_id(self, field_id): 25 | assert field_id 26 | return "{}-map-canvas".format(field_id) 27 | 28 | def get_context(self, name, value, attrs): 29 | context = super().get_context(name, value, attrs) 30 | 31 | context.update( 32 | { 33 | "address": self.get_map_centre(value), 34 | "gmaps_api_key": self.apikey, 35 | "latlngMode": self.latlngMode, 36 | "map_id": self.get_map_id(attrs["id"]), 37 | "zoom": self.zoom, 38 | } 39 | ) 40 | 41 | return context 42 | 43 | @property 44 | def media(self): 45 | maps_api_js = "https://maps.googleapis.com/maps/api/js?key={}".format( 46 | settings.WAGTAIL_ADDRESS_MAP_KEY 47 | ) 48 | language = getattr(settings, "WAGTAIL_ADDRESS_MAP_LANGUAGE", None) 49 | if language: 50 | maps_api_js += "&language={}".format(language) 51 | 52 | return Media( 53 | css={"screen": ("wagtailgmaps/css/admin.css",)}, 54 | js=(maps_api_js, "wagtailgmaps/js/map-field-panel.js"), 55 | ) 56 | --------------------------------------------------------------------------------