├── __init__.py ├── sample ├── __init__.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── templates │ └── sample │ │ └── index.html ├── views.py ├── models.py ├── forms.py ├── tests.py └── admin.py ├── django_google_maps ├── models.py ├── __init__.py ├── tests │ ├── __init__.py │ ├── test_app │ │ ├── __init__.py │ │ └── models.py │ ├── test_typename.py │ ├── test_widget.py │ ├── test_geolocation_field.py │ ├── test_geopt_field.py │ └── test_geopt_has_changed.py ├── templates │ └── django_google_maps │ │ └── widgets │ │ └── map_widget.html ├── static │ └── django_google_maps │ │ ├── css │ │ └── google-maps-admin.css │ │ └── js │ │ └── google-maps-admin.js ├── widgets.py └── fields.py ├── requirements ├── requirements.txt └── test.txt ├── .gitignore ├── MANIFEST.in ├── urls.py ├── manage.py ├── .github └── workflows │ ├── python-publish.yml │ └── django.yml ├── LICENSE ├── setup.py ├── README_kr.rst ├── CHANGELOG.md ├── README.rst └── settings.py /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_google_maps/models.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_google_maps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_google_maps/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_google_maps/tests/test_app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=2.2 2 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | coverage 3 | mock 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | sample_db 3 | build/* 4 | *.egg-info 5 | dist 6 | .coverage 7 | venv/ 8 | *.pyc 9 | *.pyo 10 | .tox/* 11 | -------------------------------------------------------------------------------- /sample/templates/sample/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{form.media}} 4 | 5 | {{form.as_p}} 6 | 7 | -------------------------------------------------------------------------------- /sample/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import FormView 2 | 3 | from sample.forms import SampleForm 4 | 5 | 6 | class SampleFormView(FormView): 7 | form_class = SampleForm 8 | template_name = "sample/index.html" 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | 2 | include LICENSE 3 | include README.md 4 | include CHANGELOG.md 5 | include MANIFEST.in 6 | include requirements/*.txt 7 | 8 | recursive-include django_google_maps/static * 9 | recursive-include django_google_maps/templates * 10 | -------------------------------------------------------------------------------- /django_google_maps/tests/test_app/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django_google_maps.fields import AddressField, GeoLocationField 3 | 4 | 5 | class Person(models.Model): 6 | address = AddressField(max_length=100) 7 | geolocation = GeoLocationField(blank=True) 8 | -------------------------------------------------------------------------------- /django_google_maps/templates/django_google_maps/widgets/map_widget.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from django_google_maps.fields import AddressField, GeoLocationField 4 | 5 | 6 | class SampleModel(models.Model): 7 | address = AddressField(max_length=100) 8 | geolocation = GeoLocationField(blank=True) 9 | 10 | def __str__(self): 11 | return self.address 12 | -------------------------------------------------------------------------------- /sample/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from sample.models import SampleModel 3 | from django_google_maps.widgets import GoogleMapsAddressWidget 4 | 5 | 6 | class SampleForm(forms.ModelForm): 7 | 8 | class Meta(object): 9 | model = SampleModel 10 | fields = ['address', 'geolocation'] 11 | widgets = { 12 | "address": GoogleMapsAddressWidget, 13 | } 14 | -------------------------------------------------------------------------------- /urls.py: -------------------------------------------------------------------------------- 1 | 2 | import django 3 | from django.contrib import admin 4 | admin.autodiscover() 5 | 6 | 7 | if django.get_version() >= '2.0.0': 8 | from django.urls import re_path as url 9 | else: 10 | from django.conf.urls import url 11 | from sample.views import SampleFormView 12 | 13 | urlpatterns = [ 14 | url(r'^admin/', admin.site.urls), 15 | url(r'^$', SampleFormView.as_view()), 16 | ] 17 | -------------------------------------------------------------------------------- /django_google_maps/tests/test_typename.py: -------------------------------------------------------------------------------- 1 | from django import test 2 | from django_google_maps.fields import typename 3 | 4 | 5 | class TypeNameTests(test.TestCase): 6 | def test_simple_type_returns_type_name_as_string(self): 7 | self.assertEqual("str", typename("x")) 8 | 9 | def test_class_object(self): 10 | class X: 11 | pass 12 | 13 | self.assertEqual("type", typename(X)) 14 | -------------------------------------------------------------------------------- /sample/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates writing tests using the unittest module. These will pass 3 | when you run "manage.py test". 4 | 5 | Replace this with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | 11 | class SimpleTest(TestCase): 12 | 13 | def test_basic_addition(self): 14 | """ 15 | Tests that 1 + 1 always equals 2. 16 | """ 17 | self.assertEqual(1 + 1, 2) 18 | -------------------------------------------------------------------------------- /sample/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.forms.widgets import TextInput 3 | 4 | from django_google_maps.widgets import GoogleMapsAddressWidget 5 | from django_google_maps.fields import AddressField, GeoLocationField 6 | 7 | from sample import models 8 | 9 | 10 | class SampleModelAdmin(admin.ModelAdmin): 11 | formfield_overrides = { 12 | AddressField: { 13 | 'widget': GoogleMapsAddressWidget 14 | }, 15 | GeoLocationField: { 16 | 'widget': TextInput(attrs={ 17 | 'readonly': 'readonly' 18 | }) 19 | }, 20 | } 21 | 22 | 23 | admin.site.register(models.SampleModel, SampleModelAdmin) 24 | -------------------------------------------------------------------------------- /sample/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import django_google_maps.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='SampleModel', 15 | fields=[ 16 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 17 | ('address', django_google_maps.fields.AddressField(max_length=100)), 18 | ('geolocation', django_google_maps.fields.GeoLocationField(max_length=100, blank=True)), 19 | ], 20 | options={}, 21 | bases=(models.Model, ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /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", "settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | 12 | 13 | # #!/usr/bin/env python 14 | # from django.core.management import execute_manager 15 | # import imp 16 | # try: 17 | # imp.find_module('settings') # Assumed to be in the same directory. 18 | # except ImportError: 19 | # import sys 20 | # sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) 21 | # sys.exit(1) 22 | 23 | # import settings 24 | 25 | # if __name__ == "__main__": 26 | # execute_manager(settings) 27 | -------------------------------------------------------------------------------- /django_google_maps/static/django_google_maps/css/google-maps-admin.css: -------------------------------------------------------------------------------- 1 | @media (min-width: 848px) { 2 | .main.shifted .map_canvas_wrapper { 3 | margin-left: 0; 4 | } 5 | } 6 | 7 | #id_address { 8 | width: 40em; 9 | } 10 | 11 | .map_widget_wrapper { 12 | display: flex; 13 | flex-direction: column; 14 | width: 100%; 15 | gap: 0.5rem; 16 | } 17 | 18 | .map_canvas_wrapper { 19 | width: 100%; 20 | } 21 | 22 | #map_canvas { 23 | width: 100%; 24 | height: 40em; 25 | } 26 | 27 | #map_message_box { 28 | background-color: #eff6ff; 29 | color: #1e40af; 30 | padding: 12px 20px; 31 | border-radius: 8px; 32 | border: 1px solid #bfdbfe; 33 | display: none; /* Hidden by default */ 34 | font-size: 0.9rem; 35 | opacity: 0; 36 | transition: opacity 0.3s ease-in-out; 37 | float: none; 38 | } 39 | 40 | #map_message_box.show { 41 | display: block; 42 | opacity: 1; 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using trusted publishers 2 | # For more information see: https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | 14 | permissions: 15 | id-token: write # This is important for OIDC 16 | contents: read # Recommended for checkout 17 | 18 | steps: 19 | - uses: actions/checkout@v5 20 | - name: Set up Python 21 | uses: actions/setup-python@v6 22 | with: 23 | python-version: '3.x' 24 | - name: Install pypa/build 25 | run: | 26 | python -m pip install build --user 27 | - name: Build Package 28 | run: python -m build 29 | - name: Publish to Pypi 30 | uses: pypa/gh-action-pypi-publish@release/v1 31 | # This action handles building and publishing automatically 32 | # No need to manually run setup.py or twine 33 | # No secrets needed here for PyPI username/password 34 | # The action will use the OIDC token from the permissions block 35 | -------------------------------------------------------------------------------- /django_google_maps/widgets.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.conf import settings 3 | from django.forms import widgets 4 | 5 | # Check if we're on Django 5.2+ 6 | USE_SCRIPT_OBJECT = django.VERSION >= (5, 2) 7 | 8 | if USE_SCRIPT_OBJECT: 9 | from django.forms.widgets import Script 10 | 11 | 12 | class GoogleMapsAddressWidget(widgets.TextInput): 13 | """a widget that will place a google map right after the #id_address field""" 14 | 15 | template_name = "django_google_maps/widgets/map_widget.html" 16 | 17 | class Media: 18 | css = {"all": ("django_google_maps/css/google-maps-admin.css",)} 19 | 20 | if USE_SCRIPT_OBJECT: 21 | js = ( 22 | Script( 23 | f'https://maps.googleapis.com/maps/api/js?key={settings.GOOGLE_MAPS_API_KEY}&loading=async&callback=initGoogleMap', # noqa: E501 24 | **{ 25 | "async": True, 26 | }), 27 | 'django_google_maps/js/google-maps-admin.js', 28 | ) 29 | else: 30 | js = ( 31 | f"https://maps.googleapis.com/maps/api/js?key={settings.GOOGLE_MAPS_API_KEY}&loading=async&callback=initGoogleMap", # noqa: E501 32 | 'django_google_maps/js/google-maps-admin.js', 33 | ) 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Aaron Madison 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 19 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 22 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /.github/workflows/django.yml: -------------------------------------------------------------------------------- 1 | name: Django CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | max-parallel: 4 15 | fail-fast: false 16 | matrix: 17 | python-version: ['3.10', '3.11', '3.12'] 18 | django-version: ['4.2', '5.2'] 19 | include: 20 | - python-version: '3.9' 21 | django-version: '2.2' 22 | - python-version: '3.9' 23 | django-version: '3.2' 24 | - python-version: '3.10' 25 | django-version: '3.2' 26 | - python-version: '3.9' 27 | django-version: '4.2' 28 | - python-version: '3.13' 29 | django-version: '5.2' 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Set up Python ${{ matrix.python-version }} 33 | uses: actions/setup-python@v5 34 | with: 35 | python-version: ${{ matrix.python-version }} 36 | - name: Install Dependencies 37 | run: | 38 | python -m pip install --upgrade pip 39 | pip install -r requirements/test.txt 40 | pip install -r requirements/requirements.txt 41 | pip install -q Django==${{ matrix.django-version }} 42 | - name: Run Tests 43 | run: | 44 | coverage run --source=django_google_maps manage.py test 45 | coverage report --show-missing 46 | flake8 django_google_maps --max-line-length=120 --max-complexity=4 47 | -------------------------------------------------------------------------------- /django_google_maps/tests/test_widget.py: -------------------------------------------------------------------------------- 1 | from django import test 2 | from django.conf import settings 3 | from django_google_maps.widgets import GoogleMapsAddressWidget 4 | 5 | 6 | class WidgetTests(test.TestCase): 7 | def test_render_returns_xxxxxxx(self): 8 | widget = GoogleMapsAddressWidget() 9 | results = widget.render("name", "value", attrs={"a1": 1, "a2": 2}) 10 | expected = '' 15 | self.assertHTMLEqual(expected, results) 16 | 17 | def test_render_returns_blank_for_value_when_none(self): 18 | widget = GoogleMapsAddressWidget() 19 | results = widget.render("name", None, attrs={"a1": 1, "a2": 2}) 20 | expected = '' 25 | self.assertHTMLEqual(expected, results) 26 | 27 | def test_maps_js_uses_api_key(self): 28 | widget = GoogleMapsAddressWidget() 29 | google_maps_js = ( 30 | f"https://maps.googleapis.com/maps/api/js?key={settings.GOOGLE_MAPS_API_KEY}&loading=async&callback=initGoogleMap" # noqa: E501 31 | ) 32 | self.assertEqual(google_maps_js, widget.Media().js[0]) 33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | from setuptools import setup 4 | 5 | README = os.path.join(os.path.dirname(__file__), 'README.rst') 6 | LONG_DESCRIPTION = open(README, 'r').read() 7 | CLASSIFIERS = [ 8 | "Development Status :: 4 - Beta", 9 | "Environment :: Web Environment", 10 | "Framework :: Django", 11 | "Framework :: Django", 12 | "Framework :: Django :: 2.2", 13 | "Framework :: Django :: 3.2", 14 | "Framework :: Django :: 4.0", 15 | "Framework :: Django :: 5.0", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: BSD License", 18 | "Operating System :: OS Independent", 19 | "Programming Language :: Python", 20 | "Programming Language :: Python :: 3.9", 21 | "Programming Language :: Python :: 3.10", 22 | "Programming Language :: Python :: 3.11", 23 | "Programming Language :: Python :: 3.12", 24 | "Programming Language :: Python :: 3.13", 25 | "Topic :: Software Development", 26 | "Topic :: Software Development :: Libraries :: Application Frameworks", 27 | ] 28 | 29 | setup( 30 | name="django-google-maps", 31 | version='0.14.0', 32 | author="Aaron Madison", 33 | author_email="aaron.l.madison@gmail.com", 34 | description="Plugs google maps V3 api into Django admin.", 35 | long_description=LONG_DESCRIPTION, 36 | long_description_content_type='text/x-rst', 37 | url="https://github.com/madisona/django-google-maps", 38 | packages=("django_google_maps",), 39 | include_package_data=True, 40 | install_requires=open('requirements/requirements.txt').read().splitlines(), 41 | tests_require=open('requirements/test.txt').read().splitlines(), 42 | classifiers=CLASSIFIERS, 43 | zip_safe=False, 44 | ) 45 | -------------------------------------------------------------------------------- /django_google_maps/tests/test_geolocation_field.py: -------------------------------------------------------------------------------- 1 | from django import test 2 | from django_google_maps.tests.test_app import models 3 | from django_google_maps.fields import GeoPt 4 | 5 | 6 | class GeoLocationFieldTests(test.TestCase): 7 | def test_getting_lat_lon_from_model_given_string(self): 8 | sut_create = models.Person.objects.create(geolocation="45,90") 9 | sut = models.Person.objects.get(pk=sut_create.pk) 10 | self.assertEqual(45, sut.geolocation.lat) 11 | self.assertEqual(90, sut.geolocation.lon) 12 | 13 | def test_getting_lat_lon_from_model_given_pt(self): 14 | sut_create = models.Person.objects.create(geolocation=GeoPt("45,90")) 15 | sut = models.Person.objects.get(pk=sut_create.pk) 16 | self.assertEqual(45, sut.geolocation.lat) 17 | self.assertEqual(90, sut.geolocation.lon) 18 | 19 | def test_getting_lat_lon_from_model_in_db_given_string(self): 20 | sut_create = models.Person.objects.create(geolocation="45,90") 21 | sut = models.Person.objects.get(pk=sut_create.pk) 22 | self.assertEqual(45, sut.geolocation.lat) 23 | self.assertEqual(90, sut.geolocation.lon) 24 | 25 | def test_exact_match_query(self): 26 | sut = models.Person.objects.create(geolocation="45,90") 27 | result = models.Person.objects.get(geolocation__exact=GeoPt("45,90")) 28 | self.assertEqual(result, sut) 29 | 30 | def test_in_match_query(self): 31 | sut = models.Person.objects.create(geolocation="45,90") 32 | result = models.Person.objects.get(geolocation__in=[GeoPt("45,90")]) 33 | self.assertEqual(result, sut) 34 | 35 | def test_value_to_string_with_point(self): 36 | sut = models.Person.objects.create(geolocation=GeoPt("45,90")) 37 | field = models.Person._meta.fields[-1] 38 | self.assertEqual("45.0,90.0", field.value_to_string(sut)) 39 | 40 | def test_value_to_string_with_string(self): 41 | sut = models.Person.objects.create(geolocation="45,90") 42 | field = models.Person._meta.fields[-1] 43 | self.assertEqual("45.0,90.0", field.value_to_string(sut)) 44 | 45 | def test_get_prep_value_returns_none_when_none(self): 46 | field = models.Person._meta.fields[-1] 47 | result = field.get_prep_value(None) 48 | self.assertEqual(None, result) 49 | -------------------------------------------------------------------------------- /README_kr.rst: -------------------------------------------------------------------------------- 1 | ``django-google-maps``\ 는 Django 버전 1.11+에서 Django 모델 용 V3 API에 2 | 대한 기본 훅을 제공하는 간단한 애플리케이션입니다. 3 | 4 | ``django-google-maps`` 버전 (0.7.0)부터는 Django가 위젯 템플릿 렌더링 5 | 시스템을 변경했기 때문에 Django 1.11+가 필요합니다. 버전 0.8.0은 Django 6 | 2.0+를 지원하며 Python 2.7 지원을 제거합니다. 7 | 8 | 저는 관리자 패널의 누군가가 자유 형식 주소를 입력하고, 주소가 9 | 변경되고지도 상에 그려 지도록 허용하고 있습니다. 위치가 100 % 정확하지 10 | 않은 경우 사용자는 마커를 올바른 지점으로 드래그 할 수 있으며 지리적 11 | 좌표가 업데이트됩니다. 12 | 13 | 14 | 용법: 15 | ----- 16 | 17 | -``settings.py``\ 에\ ``django_google_maps`` 앱을 포함 시키십시오. - 18 | Google Maps API 키를\ ``settings.py``\ 에 ’GOOGLE_MAPS_API_KEY\` (으)로 19 | 추가하십시오. - 주소 필드와 위치 정보 필드가 모두있는 모델을 만듭니다. 20 | 21 | .. code:: python 22 | 23 | from django.db import models 24 | from django_google_maps import fields as map_fields 25 | 26 | class Rental(models.Model): 27 | address = map_fields.AddressField(max_length=200) 28 | geolocation = map_fields.GeoLocationField(max_length=100) 29 | 30 | -``admin.py``\ 에 다음을 formfield_override로 포함하십시오. 31 | 32 | .. code:: python 33 | 34 | from django.contrib import admin 35 | from django_google_maps import widgets as map_widgets 36 | from django_google_maps import fields as map_fields 37 | 38 | class RentalAdmin(admin.ModelAdmin): 39 | formfield_overrides = { 40 | map_fields.AddressField: {'widget': map_widgets.GoogleMapsAddressWidget}, 41 | } 42 | 43 | -지도 유형 (기본적으로\ ``hybrid ')을 변경하려면,``\ AddressField\` 44 | 위젯에 html 속성을 추가 할 수 있습니다. 허용되는 값 목록은 ‘하이브리드’, 45 | ‘로드맵’, ‘위성’, ’지형’입니다. 46 | 47 | .. code:: python 48 | 49 | from django.contrib import admin 50 | from django_google_maps import widgets as map_widgets 51 | from django_google_maps import fields as map_fields 52 | 53 | class RentalAdmin(admin.ModelAdmin): 54 | formfield_overrides = { 55 | map_fields.AddressField: { 56 | 'widget': map_widgets.GoogleMapsAddressWidget(attrs={'data-map-type': 'roadmap'})}, 57 | } 58 | 59 | 그게 당신이 시작하는 데 필요한 모든 것이어야합니다. 60 | 61 | 나는 또한 관리자가 읽기 전용으로 geolocation 필드를 만들어 사용자 62 | (우연히)가 무의미한 값으로 변경하지 않도록하고 싶습니다. 입력란에 유효성 63 | 검사가 있으므로 잘못된 값을 입력 할 수는 없지만 의도 한 주소와 거의 64 | 일치하지 않는 내용을 입력 할 수 있습니다. 65 | 66 | 사용자에게 다시 주소를 표시 할 때 모델에 저장된 지오 코디네이터를 67 | 사용하여지도를 요청하십시오. 어쩌면 언젠가는 내가 그 모델을 구축 할 68 | 방법을 만들 수 있는지 살펴볼 것입니다. 69 | 70 | .. |Build Status| image:: https://travis-ci.org/madisona/django-google-maps.png 71 | :target: https://travis-ci.org/madisona/django-google-maps -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.14.0] - 2025-09-21 2 | - Big thanks to @RobKuipers - most of the improvements in this release were his contributions 3 | - Fixed bug where `GeoLocationField` was always marked as "changed" 4 | - Allow comparing `GeoPoint` object with string lat,lng 5 | - Removed jQuery dependency, now straight javascript 6 | - Added: Django 5.2 support 7 | - Drop CI checks for Python 3.7 & 3.8 8 | - Improved styling in Django admin for newer versions 9 | - Updated to Google Maps AdvancedMarkerElement 10 | - Load Google Maps Async 11 | 12 | ## [0.13.0] - 2022-03-22 13 | - Added: Django 4.0 support 14 | - Added: Python 3.10 support 15 | 16 | ## [0.12.4] - 2021-04-06 17 | - Added: Django 3.2 support 18 | 19 | ## [0.12.3] - 2021-04-04 20 | - Fixed: Fixed bug catching malformed lat/lon in `GeoPt` field 21 | - Fixed: Removed redundant `STATIC_URL` in `GoogleMapsAddressWidget` 22 | - Changed: Updated css for map to be responsive in the admin 23 | - Changed: Moved CI to Github Actions 24 | 25 | ## [0.12.2] - 2020-08-05 26 | - Added: Django 3.1 support 27 | 28 | ## [0.12.1] - 2020-02-03 29 | - Fixed: Fixed install requirement for Django 3 30 | 31 | ## [0.12.0] - 2020-02-02 32 | - Changed: Updated for `from_db_value` `context` parameter deprecation 33 | - Added: Django 3.0 support 34 | - Removed: Python 2.0 support & django compatibility helpers 35 | 36 | ## [0.11.0] - 2019-04-19 37 | - Added: Django 2.2 support 38 | - Removed: Django 1.10 39 | 40 | ## [0.10.1] - 2018-02-20 41 | - Fixed: Install issue - `setup.py` now properly reads in requirements 42 | 43 | ## [0.10.0] - 2018-02-15 44 | - Added: Allow MapType to be customized 45 | 46 | ## [0.9.0] - 2018-01-19 47 | - Added: Google Places Autocomplete 48 | 49 | ## [0.8.0] - 2018-01-12 50 | - Added: Django 2.0 support 51 | - Removed: python 2.7 support 52 | 53 | ## [0.7.0] - 2017-04-10 54 | - Added: Django 1.11 support (new widget rendering) 55 | - Removed: Django <= 1.10 support 56 | 57 | ## [0.6.0] - 2016-11-28 58 | - Added: Django 1.10 support 59 | - Removed: Django <= 1.7 support 60 | 61 | ## [0.5.0] - 2016-08-12 62 | - Added: Allow using Google Maps API Key 63 | - Changed: jquery version to 3.1.0 64 | 65 | ## [0.4.0] - 2016-07-05 66 | - Added: Python 3 Compatibility 67 | - Fixed: Adjusted widgets imports to maintain backwards compatibility 68 | 69 | ## [0.3.0] - 2015-04-09 70 | - Added: Travis integration 71 | - Added: Django 1.7 & 1.8 support 72 | - Added: additional test coverage 73 | - Added: Map updates affecting geolocation now triggers change event on geolocation field 74 | - Changed: jquery version to 1.11.2 75 | 76 | ## [0.2.3] - 2013-10-08 77 | - Changed: Google Maps JS to always load over https 78 | 79 | ## [0.2.2] - 2012-12-13 80 | - Added: Django 1.4 support 81 | 82 | ## [0.2.1] - 2011-06-28 83 | - Added: `__eq__` method for GeoPt field to allow comparison 84 | 85 | ## [0.2.0] - 2011-06-27 86 | - Initial version 87 | 88 | 89 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ``django-google-maps`` is a simple application that provides the basic 2 | hooks into google maps V3 api for use in Django models from Django 3 | version 1.11+. 4 | 5 | Starting with ``django-google-maps`` version (0.7.0), Django 1.11+ is 6 | required because Django changed their widget template rendering system. 7 | Version 0.8.0 supports Django 2.0+, and as such removes support for 8 | Python 2.7 9 | 10 | I’m using this to allow someone from the admin panels to type a freeform 11 | address, have the address geocoded on change and plotted on the map. If 12 | the location is not 100% correct, the user can drag the marker to the 13 | correct spot and the geo coordinates will update. 14 | 15 | 16 | USAGE: 17 | ------ 18 | 19 | - include the ``django_google_maps`` app in your ``settings.py`` 20 | 21 | - Add your Google Maps API Key in your ``settings.py`` as 22 | ``GOOGLE_MAPS_API_KEY`` 23 | 24 | - create a model that has both an address field and geolocation field 25 | 26 | .. code:: python 27 | 28 | from django.db import models 29 | from django_google_maps import fields as map_fields 30 | 31 | class Rental(models.Model): 32 | address = map_fields.AddressField(max_length=200) 33 | geolocation = map_fields.GeoLocationField(max_length=100) 34 | 35 | - in the ``admin.py`` include the following as a formfield_override 36 | 37 | .. code:: python 38 | 39 | from django.contrib import admin 40 | from django_google_maps import widgets as map_widgets 41 | from django_google_maps import fields as map_fields 42 | 43 | class RentalAdmin(admin.ModelAdmin): 44 | formfield_overrides = { 45 | map_fields.AddressField: {'widget': map_widgets.GoogleMapsAddressWidget}, 46 | } 47 | 48 | - To change the map type (``hybrid`` by default), you can add an html 49 | attribute on the ``AddressField`` widget. The list of allowed values 50 | is: ``hybrid``, ``roadmap``, ``satellite``, ``terrain`` 51 | 52 | .. code:: python 53 | 54 | from django.contrib import admin 55 | from django_google_maps import widgets as map_widgets 56 | from django_google_maps import fields as map_fields 57 | 58 | class RentalAdmin(admin.ModelAdmin): 59 | formfield_overrides = { 60 | map_fields.AddressField: { 61 | 'widget': map_widgets.GoogleMapsAddressWidget(attrs={'data-map-type': 'roadmap'})}, 62 | } 63 | 64 | - To change the autocomplete options, you can add an html attribute on 65 | the ``AddressField`` widget. See 66 | https://developers.google.com/maps/documentation/javascript/places-autocomplete#add_autocomplete 67 | for a list of available options 68 | 69 | .. code:: python 70 | 71 | import json from django.contrib import admin 72 | from django_google_maps import widgets as map_widgets 73 | from django_google_maps import fields as map_fields 74 | 75 | class RentalAdmin(admin.ModelAdmin): formfield_overrides = { 76 | map_fields.AddressField: { 'widget': 77 | map_widgets.GoogleMapsAddressWidget(attrs={ 78 | 'data-autocomplete-options': json.dumps({ 'types': ['geocode', 79 | 'establishment'], 'componentRestrictions': { 80 | 'country': 'us' 81 | } 82 | }) 83 | }) 84 | }, 85 | } 86 | 87 | That should be all you need to get started. 88 | 89 | I also like to make the geolocation field readonly in the admin so a user 90 | (myself) doesn't accidentally change it to a nonsensical value. There is 91 | validation on the field so you can't enter an incorrect value, but you could 92 | enter something that is not even close to the address you intended. 93 | 94 | When you're displaying the address back to the user, just request the map 95 | using the geocoordinates that were saved in your model. Maybe sometime when 96 | I get around to it I'll see if I can create a method that will build that 97 | into the model. 98 | -------------------------------------------------------------------------------- /django_google_maps/tests/test_geopt_field.py: -------------------------------------------------------------------------------- 1 | from django import test 2 | from django.core import exceptions 3 | from django.utils.encoding import force_str 4 | 5 | from django_google_maps import fields 6 | 7 | 8 | class GeoPtFieldTests(test.TestCase): 9 | def test_sets_lat_lon_on_initialization(self): 10 | geo_pt = fields.GeoPt("15.001,32.001") 11 | self.assertEqual(15.001, geo_pt.lat) 12 | self.assertEqual(32.001, geo_pt.lon) 13 | 14 | def test_uses_lat_comma_lon_as_unicode_representation(self): 15 | lat_lon_string = "15.001,32.001" 16 | geo_pt = fields.GeoPt(lat_lon_string) 17 | self.assertEqual(lat_lon_string, force_str(geo_pt)) 18 | 19 | def test_two_GeoPts_with_same_lat_lon_should_be_equal(self): 20 | geo_pt_1 = fields.GeoPt("15.001,32.001") 21 | geo_pt_2 = fields.GeoPt("15.001,32.001") 22 | self.assertEqual(geo_pt_1, geo_pt_2) 23 | 24 | def test_two_GeoPts_with_different_lat_should_not_be_equal(self): 25 | geo_pt_1 = fields.GeoPt("15.001,32.001") 26 | geo_pt_2 = fields.GeoPt("20.001,32.001") 27 | self.assertNotEqual(geo_pt_1, geo_pt_2) 28 | 29 | def test_two_GeoPts_with_different_lon_should_not_be_equal(self): 30 | geo_pt_1 = fields.GeoPt("15.001,32.001") 31 | geo_pt_2 = fields.GeoPt("15.001,62.001") 32 | self.assertNotEqual(geo_pt_1, geo_pt_2) 33 | 34 | def test_is_equal_when_comparison_str_GeoPt_object(self): 35 | geo_pt_1 = fields.GeoPt("15.001,32.001") 36 | geo_pt_2 = "15.001,32.001" 37 | self.assertEqual(geo_pt_1, geo_pt_2) 38 | 39 | def test_is_not_equal_when_comparison_str_GeoPt_object(self): 40 | geo_pt_1 = fields.GeoPt("15.001,32.001") 41 | geo_pt_2 = "25.001,32.001" 42 | self.assertNotEqual(geo_pt_1, geo_pt_2) 43 | 44 | def test_compare_empty_GeoPt_to_None_object(self): 45 | geo_pt_1 = fields.GeoPt(None) 46 | geo_pt_2 = None 47 | self.assertEqual(geo_pt_1, geo_pt_2) 48 | 49 | def test_allows_GeoPt_instantiated_with_empty_string(self): 50 | geo_pt = fields.GeoPt("") 51 | self.assertEqual(None, geo_pt.lat) 52 | self.assertEqual(None, geo_pt.lon) 53 | 54 | def test_uses_empty_string_as_unicode_representation_for_empty_GeoPt(self): 55 | geo_pt = fields.GeoPt("") 56 | self.assertEqual("", force_str(geo_pt)) 57 | 58 | def test_splits_geo_point_on_comma(self): 59 | pt = fields.GeoPt("15.001,32.001") 60 | self.assertEqual("15.001", str(pt.lat)) 61 | self.assertEqual("32.001", str(pt.lon)) 62 | 63 | def test_raises_error_when_attribute_error_on_split(self): 64 | class Fake(object): 65 | pass 66 | 67 | with self.assertRaises(exceptions.ValidationError): 68 | fields.GeoPt(Fake) 69 | 70 | def test_raises_error_when_type_error_on_split(self): 71 | with self.assertRaises(exceptions.ValidationError): 72 | x, y = fields.GeoPt("x,x") 73 | 74 | def test_returns_float_value_when_valid_value(self): 75 | geo_pt = fields.GeoPt("45.005,180") 76 | self.assertEqual(45.005, geo_pt.lat) 77 | 78 | def test_raises_exception_when_value_is_out_of_upper_range(self): 79 | with self.assertRaises(exceptions.ValidationError): 80 | fields.GeoPt("180,180") 81 | 82 | def test_raises_exception_when_value_is_out_of_lower_range(self): 83 | with self.assertRaises(exceptions.ValidationError): 84 | fields.GeoPt("-180,180") 85 | 86 | def test_len_returns_len_of_unicode_value(self): 87 | geo_pt = fields.GeoPt("84,12") 88 | self.assertEqual(9, len(geo_pt)) 89 | 90 | def test_raises_exception_not_enough_values_to_unpack(self): 91 | with self.assertRaises(exceptions.ValidationError): 92 | fields.GeoPt("22") 93 | 94 | def test_raises_exception_too_many_values_to_unpack(self): 95 | with self.assertRaises(exceptions.ValidationError): 96 | fields.GeoPt("22,50,90") 97 | -------------------------------------------------------------------------------- /django_google_maps/tests/test_geopt_has_changed.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.db import models 3 | from django.test import TestCase 4 | 5 | from django_google_maps.fields import GeoPt, GeoLocationField 6 | 7 | 8 | class GeoLocationFieldHasChangedTests(TestCase): 9 | """ 10 | Integration tests for the has_changed functionality with forms. 11 | """ 12 | 13 | def setUp(self): 14 | """Set up a model and form for testing.""" 15 | 16 | class TestModel(models.Model): 17 | location = GeoLocationField(max_length=100, blank=True) 18 | 19 | class Meta: 20 | app_label = "django_google_maps" 21 | 22 | self.TestModel = TestModel 23 | 24 | # Create a form for testing 25 | class TestForm(forms.ModelForm): 26 | class Meta: 27 | model = TestModel 28 | fields = ["location"] 29 | 30 | self.TestForm = TestForm 31 | 32 | def test_has_changed_false_when_same_value(self): 33 | """ 34 | Test that has_changed returns False when the value hasn't actually changed. 35 | This is the main bug that was fixed. 36 | """ 37 | # Create a GeoPt object (simulating initial data from database) 38 | initial_value = GeoPt(40.7128, -74.0060) 39 | 40 | # Create form data (simulating form submission with same value) 41 | form_data = {"location": "40.7128,-74.0060"} 42 | 43 | # Create form with initial data 44 | form = self.TestForm(data=form_data, initial={"location": initial_value}) 45 | 46 | # The field should not be marked as changed 47 | self.assertFalse(form.has_changed()) 48 | self.assertNotIn("location", form.changed_data) 49 | 50 | def test_has_changed_true_when_value_actually_changed(self): 51 | """ 52 | Test that has_changed returns True when the value has actually changed. 53 | """ 54 | # Create a GeoPt object (simulating initial data from database) 55 | initial_value = GeoPt(40.7128, -74.0060) 56 | 57 | # Create form data with different coordinates 58 | form_data = {"location": "41.0000,-73.0000"} 59 | 60 | # Create form with initial data 61 | form = self.TestForm(data=form_data, initial={"location": initial_value}) 62 | 63 | # The field should be marked as changed 64 | self.assertTrue(form.has_changed()) 65 | self.assertIn("location", form.changed_data) 66 | 67 | def test_has_changed_false_with_empty_values(self): 68 | """ 69 | Test that has_changed handles empty values correctly. 70 | """ 71 | # Test empty initial and empty form data 72 | form_data = {"location": ""} 73 | form = self.TestForm(data=form_data, initial={"location": None}) 74 | 75 | self.assertFalse(form.has_changed()) 76 | self.assertNotIn("location", form.changed_data) 77 | 78 | def test_has_changed_true_from_empty_to_value(self): 79 | """ 80 | Test that has_changed returns True when going from empty to a value. 81 | """ 82 | # Empty initial, non-empty form data 83 | form_data = {"location": "40.7128,-74.0060"} 84 | form = self.TestForm(data=form_data, initial={"location": None}) 85 | 86 | self.assertTrue(form.has_changed()) 87 | self.assertIn("location", form.changed_data) 88 | 89 | def test_has_changed_true_from_value_to_empty(self): 90 | """ 91 | Test that has_changed returns True when going from a value to empty. 92 | """ 93 | # Non-empty initial, empty form data 94 | initial_value = GeoPt(40.7128, -74.0060) 95 | form_data = {"location": ""} 96 | form = self.TestForm(data=form_data, initial={"location": initial_value}) 97 | 98 | self.assertTrue(form.has_changed()) 99 | self.assertIn("location", form.changed_data) 100 | 101 | def test_widget_has_changed_method(self): 102 | """ 103 | Test the widget's has_changed method directly. 104 | """ 105 | # Get the field from the form 106 | form = self.TestForm() 107 | field = form.fields["location"] 108 | widget = field.widget 109 | 110 | # Test the widget's has_changed method directly 111 | initial_geopt = GeoPt(40.7128, -74.0060) 112 | same_string = "40.7128,-74.0060" 113 | different_string = "41.0000,-73.0000" 114 | 115 | # If using custom widget, test its has_changed method 116 | if hasattr(widget, "has_changed"): 117 | self.assertFalse(widget.has_changed(initial_geopt, same_string)) 118 | self.assertTrue(widget.has_changed(initial_geopt, different_string)) 119 | -------------------------------------------------------------------------------- /django_google_maps/fields.py: -------------------------------------------------------------------------------- 1 | # The core of this module was adapted from Google AppEngine's 2 | # GeoPt field, so I've included their copyright and license. 3 | # 4 | # Copyright 2007 Google Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | from django.core import exceptions 20 | from django.db import models 21 | from django.utils.encoding import force_str 22 | 23 | __all__ = ("AddressField", "GeoLocationField") 24 | 25 | 26 | def typename(obj): 27 | """Returns the type of obj as a string. More descriptive and specific than 28 | type(obj), and safe for any object, unlike __class__.""" 29 | if hasattr(obj, "__class__"): 30 | return getattr(obj, "__class__").__name__ 31 | else: 32 | return type(obj).__name__ 33 | 34 | 35 | class GeoPt(object): 36 | """A geographical point.""" 37 | 38 | lat = None 39 | lon = None 40 | 41 | def __init__(self, lat, lon=None): 42 | """ 43 | If the model field has 'blank=True' or 'null=True' then 44 | we can't always expect the GeoPt to be instantiated with 45 | a valid value. In this case we'll let GeoPt be instantiated 46 | as an empty item, and the string representation should be 47 | an empty string instead of 'lat,lon'. 48 | """ 49 | if not lat: 50 | return 51 | 52 | if lon is None: 53 | lat, lon = self._split_geo_point(lat) 54 | self.lat = self._validate_geo_range(lat, 90) 55 | self.lon = self._validate_geo_range(lon, 180) 56 | 57 | def __str__(self): 58 | if self.lat is not None and self.lon is not None: 59 | return "%s,%s" % (self.lat, self.lon) 60 | return "" 61 | 62 | def __eq__(self, other): 63 | if other is None: 64 | other = GeoPt(None) 65 | elif isinstance(other, str): 66 | other = GeoPt(other) 67 | if isinstance(other, GeoPt): 68 | return bool(self.lat == other.lat and self.lon == other.lon) 69 | return NotImplemented 70 | 71 | def __len__(self): 72 | return len(force_str(self)) 73 | 74 | def _split_geo_point(self, geo_point): 75 | """splits the geo point into lat and lon""" 76 | try: 77 | lat, lon = geo_point.split(",") 78 | return lat, lon 79 | except (AttributeError, ValueError): 80 | m = 'Expected a "lat,long" formatted string; received %s (a %s).' 81 | raise exceptions.ValidationError(m % (geo_point, typename(geo_point))) 82 | 83 | def _validate_geo_range(self, geo_part, range_val): 84 | try: 85 | geo_part = float(geo_part) 86 | if abs(geo_part) > range_val: 87 | m = "Must be between -%s and %s; received %s" 88 | raise exceptions.ValidationError(m % (range_val, range_val, geo_part)) 89 | except (TypeError, ValueError): 90 | raise exceptions.ValidationError( 91 | "Expected float, received %s (a %s)." % (geo_part, typename(geo_part)) 92 | ) 93 | return geo_part 94 | 95 | 96 | class AddressField(models.CharField): 97 | pass 98 | 99 | 100 | class GeoLocationField(models.CharField): 101 | """ 102 | A geographical point, specified by floating-point latitude and longitude 103 | coordinates. Often used to integrate with mapping sites like Google Maps. 104 | May also be used as ICBM coordinates. 105 | 106 | This is the georss:point element. In XML output, the coordinates are 107 | provided as the lat and lon attributes. See: http://georss.org/ 108 | 109 | Serializes to '