diff --git a/CHANGELOG.md b/CHANGELOG.md
index 43413525..c3bdade4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+## [0.13.1] - 2024-03-02
+ - Added: Inline form support
+
## [0.13.0] - 2022-03-22
- Added: Django 4.0 support
- Added: Python 3.10 support
diff --git a/README.rst b/README.rst
index 24d7e8f0..7ca415df 100644
--- a/README.rst
+++ b/README.rst
@@ -88,6 +88,55 @@ USAGE:
},
}
+USING INLINE FORMS:
+===================
+- To use as a model admin inline form, everything is basically the same as above except for the widget
+ used. It's best to use ``django.contrib.admin.StackedInline`` as opposed to ``django.contrib.admin.TabularInline``
+
+ .. code:: python
+
+ from django.db import models
+ from django_google_maps import fields as map_fields
+
+ class Shipment(models.Model):
+ tracking_id = models.CharField(max_length=255)
+ carrier = models.CharField(max_length=255)
+
+ class Location(models.Model):
+ shipment = models.ForeignKey(Shipment)
+ address = map_fields.AddressField(max_length=200)
+ geolocation = map_fields.GeoLocationField(max_length=50)
+
+- in the ``forms.py`` file, define the form and set widget for "address" field:
+
+ .. code:: python
+
+ from django import forms
+ from django_google_maps import widgets as map_widgets
+
+ class LocationForm(forms.ModelForm):
+ class Meta:
+ model = models.Location
+ widgets = {
+ "address": map_widgets.GoogleMapsAddressInlineWidget(),
+ }
+
+- in the ``admin.py`` file, define a model form and a stacked inline like below:
+
+ .. code:: python
+
+ from django.contrib import admin
+
+ from . import models, forms
+
+ class LocationInline(admin.StackedInline):
+ model = models.Location
+ form = forms.LocationForm
+
+ @admin.register(models.Shipment)
+ class ShipmentAdmin(admin.ModelAdmin):
+ inlines = [LocationInline]
+
That should be all you need to get started.
I also like to make the geolocation field readonly in the admin so a user
@@ -101,4 +150,4 @@ I get around to it I'll see if I can create a method that will build that
into the model.
.. |Build Status| image:: https://travis-ci.org/madisona/django-google-maps.png
- :target: https://travis-ci.org/madisona/django-google-maps
\ No newline at end of file
+ :target: https://travis-ci.org/madisona/django-google-maps
diff --git a/django_google_maps/models.py b/django_google_maps/models.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/django_google_maps/static/django_google_maps/css/google-maps-admin.css b/django_google_maps/static/django_google_maps/css/google-maps-admin.css
index 5ba4d8c7..4055ee8d 100644
--- a/django_google_maps/static/django_google_maps/css/google-maps-admin.css
+++ b/django_google_maps/static/django_google_maps/css/google-maps-admin.css
@@ -1,11 +1,31 @@
@media (min-width: 848px) {
- .map_canvas_wrapper {margin-left: 170px;}
- .main.shifted .map_canvas_wrapper {margin-left: 0;}
+ .map_canvas_wrapper {
+ margin-left: 170px;
+ }
+
+ .main.shifted .map_canvas_wrapper {
+ margin-left: 0;
+ }
}
+
@media (min-width: 1118px) {
- .main.shifted .map_canvas_wrapper {margin-left: 170px;}
+ .main.shifted .map_canvas_wrapper {
+ margin-left: 170px;
+ }
+}
+
+#id_address,
+input[id^="id_locations-"][id$="-address"] {
+ width: 40em;
+ max-width: 100%;
}
-#id_address {width: 40em;}
-.map_canvas_wrapper {width: 100%;}
-#map_canvas {width: 100%; height: 40em;}
+.map_canvas_wrapper {
+ width: 100%;
+}
+
+#map_canvas,
+[id^="locations-"][id$="-address_map_canvas"] {
+ width: 100%;
+ height: 30em;
+}
diff --git a/django_google_maps/static/django_google_maps/js/google-maps-admin-inline.js b/django_google_maps/static/django_google_maps/js/google-maps-admin-inline.js
new file mode 100644
index 00000000..927bc2da
--- /dev/null
+++ b/django_google_maps/static/django_google_maps/js/google-maps-admin-inline.js
@@ -0,0 +1,160 @@
+/*
+This script expects:
+
+
+
+*/
+
+class LocationFormGoogleMap {
+ constructor(index) {
+ this.index = index;
+ this.autocomplete = null;
+ this.geocoder = new google.maps.Geocoder();
+ this.map = null;
+ this.marker = null;
+ this.geolocationId = `id_locations-${index}-geolocation`;
+ this.addressId = `id_locations-${index}-address`;
+ this.mapCanvasId = `locations-${index}-address_map_canvas`;
+ }
+
+ initialize() {
+ let lat = 0;
+ let lng = 0;
+ let zoom = 2;
+
+ // Update zoom level and initial cordinates if form is bound
+ const existingLocation = this.getExistingLocation();
+ if (existingLocation) {
+ lat = existingLocation[0];
+ lng = existingLocation[1];
+ zoom = 12;
+ }
+
+ const latlng = new google.maps.LatLng(lat, lng);
+ const mapOptions = {
+ zoom: zoom,
+ center: latlng,
+ mapTypeId: this.getMapType(),
+ };
+ this.map = new google.maps.Map(document.getElementById(this.mapCanvasId), mapOptions);
+
+ if (existingLocation) this.setMarker(latlng);
+
+ this.autocomplete = new google.maps.places.Autocomplete(
+ document.getElementById(this.addressId),
+ this.getAutoCompleteOptions()
+ );
+
+ this.autocomplete.addListener("place_changed", () => this.codeAddress());
+
+ $(`#${this.addressId}`).keydown(function (e) {
+ if (e.keyCode == 13) {
+ e.preventDefault();
+ return false;
+ }
+ });
+ }
+
+ getMapType() {
+ const geolocationInput = document.getElementById(this.addressId);
+ const allowedType = ["roadmap", "satellite", "hybrid", "terrain"];
+ const mapType = geolocationInput.getAttribute("data-map-type");
+
+ if (mapType && -1 !== allowedType.indexOf(mapType)) return mapType;
+ return google.maps.MapTypeId.HYBRID;
+ }
+
+ getAutoCompleteOptions() {
+ const geolocationInput = document.getElementById(this.addressId);
+ const autocompleteOptions = geolocationInput.getAttribute(
+ "data-autocomplete-options"
+ );
+
+ if (!autocompleteOptions) return { types: ["geocode"] };
+ return JSON.parse(autocompleteOptions);
+ }
+
+ getExistingLocation() {
+ const geolocationInput = document.getElementById(this.geolocationId);
+ if (geolocationInput && geolocationInput.value) {
+ return geolocationInput.value.split(",");
+ }
+ }
+
+ codeAddress() {
+ const place = this.autocomplete.getPlace();
+
+ if (place.geometry !== undefined) {
+ this.updateWithCoordinates(place.geometry.location);
+ } else {
+ this.geocoder.geocode({ address: place.name }, (results, status) => {
+ if (status == google.maps.GeocoderStatus.OK) {
+ const latlng = results[0].geometry.location;
+ this.updateWithCoordinates(latlng);
+ } else {
+ alert("Geocode was not successful for the following reason: " + status);
+ }
+ });
+ }
+ }
+
+ updateWithCoordinates(latlng) {
+ this.map.setCenter(latlng);
+ this.map.setZoom(18);
+ this.setMarker(latlng);
+ this.updateGeolocation(latlng);
+ }
+
+ setMarker(latlng) {
+ this.marker
+ ? this.updateMarker(latlng)
+ : this.addMarker({ latlng: latlng, draggable: true });
+ }
+
+ addMarker(Options) {
+ this.marker = new google.maps.Marker({
+ map: this.map,
+ position: Options.latlng,
+ });
+
+ const draggable = Options.draggable || false;
+ if (draggable) {
+ this.addMarkerDrag(this.marker);
+ }
+ }
+
+ addMarkerDrag(marker) {
+ marker.setDraggable(true);
+ google.maps.event.addListener(marker, "dragend", (new_location) => {
+ this.updateGeolocation(new_location.latLng);
+ });
+ }
+
+ updateMarker(latlng) {
+ this.marker.setPosition(latlng);
+ }
+
+ updateGeolocation(latlng) {
+ document.getElementById(this.geolocationId).value = latlng.lat() + "," + latlng.lng();
+ $(`#${this.geolocationId}`).trigger("change");
+ }
+}
+
+$(document).ready(function () {
+ const addressInputSelector = "input[id^='id_locations-'][id$='-address']:visible";
+
+ // Initialize existing location forms
+ $(addressInputSelector).each(function (index) {
+ new LocationFormGoogleMap(index).initialize();
+ });
+
+ // Listen and check for insertion of new location forms
+ $(document).bind("formset:added", function (e) {
+ const newAddressInput = $(e.target).find(addressInputSelector);
+
+ if (newAddressInput) {
+ const index = parseInt(newAddressInput.attr("id").match(/(?<=-)\d+(?=-)/)[0]);
+ new LocationFormGoogleMap(index).initialize();
+ }
+ });
+});
diff --git a/django_google_maps/templates/django_google_maps/widgets/map_widget_inline.html b/django_google_maps/templates/django_google_maps/widgets/map_widget_inline.html
new file mode 100644
index 00000000..f43a1d0a
--- /dev/null
+++ b/django_google_maps/templates/django_google_maps/widgets/map_widget_inline.html
@@ -0,0 +1,4 @@
+{% include "django/forms/widgets/text.html" %}
+
diff --git a/django_google_maps/tests/test_widget.py b/django_google_maps/tests/test_widget.py
deleted file mode 100644
index 8e8c9c3f..00000000
--- a/django_google_maps/tests/test_widget.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from django import test
-from django.conf import settings
-from django_google_maps.widgets import GoogleMapsAddressWidget
-
-
-class WidgetTests(test.TestCase):
- def test_render_returns_xxxxxxx(self):
- widget = GoogleMapsAddressWidget()
- results = widget.render('name', 'value', attrs={'a1': 1, 'a2': 2})
- expected = ''
- expected += ''
- self.assertHTMLEqual(expected, results)
-
- def test_render_returns_blank_for_value_when_none(self):
- widget = GoogleMapsAddressWidget()
- results = widget.render('name', None, attrs={'a1': 1, 'a2': 2})
- expected = ''
- expected += ''
- self.assertHTMLEqual(expected, results)
-
- def test_maps_js_uses_api_key(self):
- widget = GoogleMapsAddressWidget()
- google_maps_js = "https://maps.google.com/maps/api/js?key={}&libraries=places".format(
- settings.GOOGLE_MAPS_API_KEY)
- self.assertEqual(google_maps_js, widget.Media().js[1])
diff --git a/django_google_maps/tests/test_widgets.py b/django_google_maps/tests/test_widgets.py
new file mode 100644
index 00000000..4be4c260
--- /dev/null
+++ b/django_google_maps/tests/test_widgets.py
@@ -0,0 +1,75 @@
+from django import test
+from django.conf import settings
+
+from django_google_maps import widgets
+
+
+class WidgetTests(test.TestCase):
+ def test_render_returns_with_value(self):
+ widget = widgets.GoogleMapsAddressWidget()
+ results = widget.render("name", "value", attrs={"a1": 1, "a2": 2})
+ expected = ''
+ expected += ''
+ self.assertHTMLEqual(expected, results)
+
+ def test_render_returns_blank_for_value_when_none(self):
+ widget = widgets.GoogleMapsAddressWidget()
+ results = widget.render("name", None, attrs={"a1": 1, "a2": 2})
+ expected = ''
+ expected += ''
+ self.assertHTMLEqual(expected, results)
+
+ def test_widgets_media_js(self):
+ widget = widgets.GoogleMapsAddressWidget()
+ google_maps_js = (
+ "https://maps.google.com/maps/api/js?key={}&libraries=places".format(
+ settings.GOOGLE_MAPS_API_KEY
+ )
+ )
+ admin_js = "django_google_maps/js/google-maps-admin.js"
+ self.assertEqual(google_maps_js, widget.Media().js[1])
+ self.assertEqual(admin_js, widget.Media().js[2])
+
+ def test_template_used(self):
+ widget = widgets.GoogleMapsAddressWidget()
+ self.assertEqual(
+ widget.template_name,
+ "django_google_maps/widgets/map_widget.html",
+ )
+
+
+class InlineWidgetTests(test.TestCase):
+ def test_render_returns_with_value(self):
+ widget = widgets.GoogleMapsAddressInlineWidget()
+ results = widget.render("locations-0-address", "New York", attrs={"attr1": 1})
+ expected = ''
+ expected += ''
+ self.assertHTMLEqual(expected, results)
+
+ def test_render_returns_blank_for_value_when_none(self):
+ widget = widgets.GoogleMapsAddressInlineWidget()
+ results = widget.render("locations-1-address", None, attrs={"a1": 1, "a2": 2})
+ expected = ''
+ expected += ''
+ self.assertHTMLEqual(expected, results)
+
+ def test_widgets_media_js(self):
+ widget = widgets.GoogleMapsAddressInlineWidget()
+ google_maps_js = (
+ "https://maps.google.com/maps/api/js?key={}&libraries=places".format(
+ settings.GOOGLE_MAPS_API_KEY
+ )
+ )
+ admin_js = "django_google_maps/js/google-maps-admin-inline.js"
+ self.assertEqual(google_maps_js, widget.Media().js[1])
+ self.assertEqual(admin_js, widget.Media().js[2])
+
+ def test_template_used(self):
+ widget = widgets.GoogleMapsAddressInlineWidget()
+ self.assertEqual(
+ widget.template_name,
+ "django_google_maps/widgets/map_widget_inline.html",
+ )
diff --git a/django_google_maps/widgets.py b/django_google_maps/widgets.py
index 3afac3c9..50e88e74 100644
--- a/django_google_maps/widgets.py
+++ b/django_google_maps/widgets.py
@@ -16,3 +16,22 @@ class Media:
settings.GOOGLE_MAPS_API_KEY),
'django_google_maps/js/google-maps-admin.js',
)
+
+
+class GoogleMapsAddressInlineWidget(widgets.TextInput):
+ """
+ a widget that will place a google map right after the #id_locations-{index}-address
+ field and give it unique identifier.
+ """
+ template_name = 'django_google_maps/widgets/map_widget_inline.html'
+
+ class Media:
+ css = {
+ 'all': ('django_google_maps/css/google-maps-admin.css', )
+ }
+ js = (
+ 'https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js',
+ 'https://maps.google.com/maps/api/js?key={}&libraries=places'.format(
+ settings.GOOGLE_MAPS_API_KEY),
+ 'django_google_maps/js/google-maps-admin-inline.js',
+ )
diff --git a/sample/admin.py b/sample/admin.py
index d6278220..f246775e 100644
--- a/sample/admin.py
+++ b/sample/admin.py
@@ -1,23 +1,28 @@
from django.contrib import admin
from django.forms.widgets import TextInput
-from django_google_maps.widgets import GoogleMapsAddressWidget
from django_google_maps.fields import AddressField, GeoLocationField
-
-from sample import models
+from django_google_maps.widgets import GoogleMapsAddressWidget
+from sample import forms, models
-class SampleModelAdmin(admin.ModelAdmin):
+class LocationAdmin(admin.ModelAdmin):
formfield_overrides = {
- AddressField: {
- 'widget': GoogleMapsAddressWidget
- },
- GeoLocationField: {
- 'widget': TextInput(attrs={
- 'readonly': 'readonly'
- })
- },
+ AddressField: {"widget": GoogleMapsAddressWidget},
+ GeoLocationField: {"widget": TextInput(attrs={"readonly": "readonly"})},
}
-admin.site.register(models.SampleModel, SampleModelAdmin)
+class HotelLocationInlineAdmin(admin.StackedInline):
+ model = models.HotelLocation
+ form = forms.HotelLocationForm
+ extra = 1
+
+
+class HotelAdmin(admin.ModelAdmin):
+ list_display = ["name"]
+ inlines = [HotelLocationInlineAdmin]
+
+
+admin.site.register(models.Location, LocationAdmin)
+admin.site.register(models.Hotel, HotelAdmin)
diff --git a/sample/forms.py b/sample/forms.py
index 29a9f3b4..db53d61c 100644
--- a/sample/forms.py
+++ b/sample/forms.py
@@ -1,13 +1,21 @@
from django import forms
-from sample.models import SampleModel
-from django_google_maps.widgets import GoogleMapsAddressWidget
+from django_google_maps import widgets
+from sample import models
-class SampleForm(forms.ModelForm):
+class LocationForm(forms.ModelForm):
class Meta(object):
- model = SampleModel
- fields = ['address', 'geolocation']
+ model = models.Location
+ fields = ["address", "geolocation"]
+ widgets = {"address": widgets.GoogleMapsAddressWidget}
+
+
+class HotelLocationForm(forms.ModelForm):
+ class Meta(object):
+ model = models.HotelLocation
+ fields = ["address", "geolocation", "hotel"]
widgets = {
- "address": GoogleMapsAddressWidget,
+ "address": widgets.GoogleMapsAddressInlineWidget,
+ "geolocation": forms.widgets.TextInput(attrs={"readonly": "readonly"}),
}
diff --git a/sample/models.py b/sample/models.py
index 8113d52a..c70b44b6 100644
--- a/sample/models.py
+++ b/sample/models.py
@@ -3,9 +3,22 @@
from django_google_maps.fields import AddressField, GeoLocationField
-class SampleModel(models.Model):
+class Location(models.Model):
address = AddressField(max_length=100)
geolocation = GeoLocationField(blank=True)
def __str__(self):
return self.address
+
+
+class Hotel(models.Model):
+ name = models.CharField(max_length=100)
+
+ def __str__(self):
+ return self.name
+
+
+class HotelLocation(models.Model):
+ address = AddressField(max_length=100)
+ geolocation = GeoLocationField(blank=True)
+ hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, related_name="locations")
diff --git a/sample/views.py b/sample/views.py
index 2bbc68a0..5f4d75a8 100644
--- a/sample/views.py
+++ b/sample/views.py
@@ -1,8 +1,8 @@
from django.views.generic import FormView
-from sample.forms import SampleForm
+from sample.forms import LocationForm
-class SampleFormView(FormView):
- form_class = SampleForm
+class LocationFormView(FormView):
+ form_class = LocationForm
template_name = "sample/index.html"
diff --git a/urls.py b/urls.py
index d8f45d19..2ef7f678 100644
--- a/urls.py
+++ b/urls.py
@@ -1,16 +1,17 @@
-
import django
from django.contrib import admin
+
admin.autodiscover()
-if django.get_version() >= '2.0.0':
+if django.get_version() >= "2.0.0":
from django.urls import re_path as url
else:
from django.conf.urls import url
-from sample.views import SampleFormView
+
+from sample.views import LocationFormView
urlpatterns = [
- url(r'^admin/', admin.site.urls),
- url(r'^$', SampleFormView.as_view()),
+ url(r"^admin/", admin.site.urls),
+ url(r"^$", LocationFormView.as_view()),
]