diff --git a/README.rst b/README.rst index fe31167..6ce2b92 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,9 @@ Installation pip install django-easy-maps Then add 'easy_maps' to INSTALLED_APPS and run ``./manage.py syncdb`` -(or ``./manage.py migrate easy_maps`` if South is in use) +(or ``./manage.py migrate easy_maps`` if South is in use). Since there are +some media files needed to be used, you have to collect the static files +distributed with this application (using ``./manage collectstatic``). Settings ======== @@ -27,6 +29,13 @@ then create a EASY_MAPS_GOOGLE_KEY in your settings.py file:: EASY_MAPS_GOOGLE_KEY = "your-google-maps-api-key" +If you need a place where center the map when no address is inserted yet add the +latitudine and longitude to the EASY_MAPS_CENTER_* variables in your settings.py +like the following:: + + EASY_MAPS_CENTER_LAT = -41.3 + EASY_MAPS_CENTER_LON = 15.2 + Usage ===== diff --git a/easy_maps/admin.py b/easy_maps/admin.py index 57a2b8d..c75c2d9 100644 --- a/easy_maps/admin.py +++ b/easy_maps/admin.py @@ -1,16 +1,59 @@ from django.contrib import admin from django import forms +from django.conf.urls.defaults import patterns, url +from django.http import HttpResponse, HttpResponseBadRequest +from django.views.decorators.csrf import csrf_exempt from .models import Address from .widgets import AddressWithMapWidget +from .geo import geolocalize + +import simplejson + +class AddressAdminForm(forms.ModelForm): + class Meta: + widgets = { + 'address': AddressWithMapWidget({'class': 'vTextField'}) + } class AddressAdmin(admin.ModelAdmin): - list_display = ['address', 'computed_address', 'latitude', 'longitude', 'geocode_error'] - list_filter = ['geocode_error'] search_fields = ['address'] + form = AddressAdminForm + class Media: + js = ( + 'https://maps.google.com/maps/api/js?sensor=false', + 'js/easy_maps.js', + ) + + def get_urls(self): + """Add a view that serves geolocalized data on POST request + """ + urls = super(AddressAdmin, self).get_urls() + my_urls = patterns('', + url(r'^geo/$', self.admin_site.admin_view(self.get_geo), name='address_json'), + ) + return my_urls + urls - class form(forms.ModelForm): - class Meta: - widgets = { - 'address': AddressWithMapWidget({'class': 'vTextField'}) + # FIXME: add CSRF protection look at https://docs.djangoproject.com/en/1.4/ref/contrib/csrf/#ajax + # for example in passing a CSRF token + @csrf_exempt + def get_geo(self, request): + """Return a json that will be used to insert correct value + into the model form. + """ + if request.method != "POST" or not request.POST.has_key('address') or request.POST['address'] == '': + return HttpResponseBadRequest() + + computed_address, latitude, longitude, geocode_error = geolocalize(request.POST["address"]) + return HttpResponse(simplejson.dumps( + { + 'computed_address': computed_address, + 'latitude': latitude, + 'longitude': longitude, + 'geocode_error': geocode_error, } + ), content_type='application/json') + +class AddressInlineAdmin(admin.StackedInline): + extra = 1 + form = AddressAdminForm diff --git a/easy_maps/geo.py b/easy_maps/geo.py new file mode 100644 index 0000000..7c5c7a9 --- /dev/null +++ b/easy_maps/geo.py @@ -0,0 +1,20 @@ +from django.conf import settings +from django.utils.encoding import smart_str + +from geopy import geocoders + +def geolocalize(address): + """From an address return the values needed to fullify an Address model form + """ + try: + if hasattr(settings, "EASY_MAPS_GOOGLE_KEY") and settings.EASY_MAPS_GOOGLE_KEY: + g = geocoders.Google(settings.EASY_MAPS_GOOGLE_KEY) + else: + g = geocoders.Google(resource='maps') + s_address = smart_str(address) + computed_address, (latitude, longitude,) = g.geocode(s_address, exactly_one=False)[0] + geocode_error = False + except (UnboundLocalError, ValueError,geocoders.google.GQueryError): + geocode_error = True + + return computed_address, latitude, longitude, geocode_error diff --git a/easy_maps/models.py b/easy_maps/models.py index 40ba9fd..92e7288 100644 --- a/easy_maps/models.py +++ b/easy_maps/models.py @@ -1,29 +1,22 @@ -from django.conf import settings from django.db import models -from django.utils.encoding import smart_str -from geopy import geocoders + +from .geo import geolocalize +from . import settings + class Address(models.Model): address = models.CharField(max_length=255, db_index=True) computed_address = models.CharField(max_length=255, null=True, blank=True) - latitude = models.FloatField(null=True, blank=True) - longitude = models.FloatField(null=True, blank=True) + latitude = models.FloatField(default=settings.EASY_MAPS_CENTER_LAT, null=True, blank=True) + longitude = models.FloatField(default=settings.EASY_MAPS_CENTER_LON, null=True, blank=True) geocode_error = models.BooleanField(default=False) def fill_geocode_data(self): if not self.address: self.geocode_error = True return - try: - if hasattr(settings, "EASY_MAPS_GOOGLE_KEY") and settings.EASY_MAPS_GOOGLE_KEY: - g = geocoders.Google(settings.EASY_MAPS_GOOGLE_KEY) - else: - g = geocoders.Google(resource='maps') - address = smart_str(self.address) - self.computed_address, (self.latitude, self.longitude,) = g.geocode(address, exactly_one=False)[0] - self.geocode_error = False - except (UnboundLocalError, ValueError,geocoders.google.GQueryError): - self.geocode_error = True + + self.computed_address, self.latitude, self.longitude, self.geocode_error = geolocalize(self.address) def save(self, *args, **kwargs): # fill geocode data if it is unknown @@ -38,3 +31,15 @@ class Meta: verbose_name = "EasyMaps Address" verbose_name_plural = "Address Geocoding Cache" + def json(self): + """Returns a JSON representation of the address data to be used + with the javascript in a template. + """ + import simplejson + dic = { + 'address': self.address, + 'computed_address': self.computed_address, + 'latitude': self.latitude, + 'longitude': self.longitude, + } + return simplejson.dumps(dic) diff --git a/easy_maps/settings.py b/easy_maps/settings.py new file mode 100644 index 0000000..a4b202c --- /dev/null +++ b/easy_maps/settings.py @@ -0,0 +1,4 @@ +from django.conf import settings + +EASY_MAPS_CENTER_LAT = getattr(settings, 'EASY_MAPS_CENTER_LAT', -34.397) +EASY_MAPS_CENTER_LON = getattr(settings, 'EASY_MAPS_CENTER_LON', 150.644) diff --git a/easy_maps/static/js/easy_maps.js b/easy_maps/static/js/easy_maps.js new file mode 100644 index 0000000..8d17cd3 --- /dev/null +++ b/easy_maps/static/js/easy_maps.js @@ -0,0 +1,59 @@ +// will contain the boundary of the map. +var g_lat_long_bound = new google.maps.LatLngBounds(); + +function easy_maps_set_form_value(id_prefix) { + return function (computed_address, lat, lng, error) { + document.getElementById(id_prefix + 'computed_address').value = computed_address; + document.getElementById(id_prefix + 'latitude').value = lat + document.getElementById(id_prefix + 'longitude').value = lng; + document.getElementById(id_prefix + 'geocode_error').value = error; + }; +} + +function easy_maps_bind_button (id_prefix) { + django.jQuery.post( + // FIXME: this is hardcoded + '/admin/easy_maps/address/geo/', { + //'{% url admin:address_json %}', { + 'address': document.getElementById(id_prefix + 'address').value + }, + function(data) { + easy_maps_set_form_value(id_prefix)( + data["computed_address"], + data["latitude"], + data["longitude"], + data["geocode_error"] + ); + var center = new google.maps.LatLng(data["latitude"], data["longitude"]); + marker.setPosition(center); + map.setCenter(center); + } + ); + + return false; +} + +function easy_maps_add_listener(id_prefix, marker) { + // update the coordinate on marker dragging + google.maps.event.addListener(marker, 'dragend', function(evt) { + var ll = marker.getPosition(); + // FIXME: fix id names + document.getElementById(id_prefix + 'latitude').value = ll.lat(); + document.getElementById(id_prefix + 'longitude').value = ll.lng(); + }); +} + +function easy_maps_add_marker(map, marker) { + var latlng = new google.maps.LatLng(marker.latitude, marker.longitude); + var marker = new google.maps.Marker({ + position: latlng, + map: map, + draggable: true, + title: marker.address + }); + + // add marker's coordinate to the boundary + g_lat_long_bound.extend(latlng); + + return marker; +} diff --git a/easy_maps/templates/admin/easy_maps/change_list.html b/easy_maps/templates/admin/easy_maps/change_list.html new file mode 100644 index 0000000..a3f11a1 --- /dev/null +++ b/easy_maps/templates/admin/easy_maps/change_list.html @@ -0,0 +1,6 @@ +{% extends "admin/change_list.html" %} +{% load easy_maps_tags %} +{% block result_list %} + {{block.super}} + {% easy_map cl.query_set 900 700 %} +{% endblock %} diff --git a/easy_maps/templates/easy_maps/map.html b/easy_maps/templates/easy_maps/map.html index 9f92899..7540297 100644 --- a/easy_maps/templates/easy_maps/map.html +++ b/easy_maps/templates/easy_maps/map.html @@ -1,21 +1,15 @@ -{% with map.latitude|stringformat:"f" as lat %} -{% with map.longitude|stringformat:"f" as long %} - -{% block api_js %} - - -{% endblock %} +{% load easy_maps_tags %} +{% with latitude|stringformat:"f" as lat %} +{% with longitude|stringformat:"f" as long %} {% block html %} -
{% block noscript %} {% endblock noscript %}
@@ -24,10 +18,9 @@ {% block map_js %} {% endblock %} {% endwith %} -{% endwith %} \ No newline at end of file +{% endwith %} diff --git a/easy_maps/templatetags/easy_maps_tags.py b/easy_maps/templatetags/easy_maps_tags.py index e91da2b..0fc38e9 100644 --- a/easy_maps/templatetags/easy_maps_tags.py +++ b/easy_maps/templatetags/easy_maps_tags.py @@ -1,14 +1,24 @@ #coding: utf-8 from django import template from django.template.loader import render_to_string -from easy_maps.models import Address +from ..models import Address +from .. import settings + register = template.Library() @register.tag def easy_map(parser, token): """ The syntax: + {% easy_map
[ ] [] [using ] %} + + or + + {% easy_map [ ] [] [using ] %} + + where in the second case you pass a queryset containing the addresses to be + visualized. """ width, height, zoom, template_name = None, None, None, None params = token.split_contents() @@ -42,16 +52,26 @@ def __init__(self, address, width, height, zoom, template_name): def render(self, context): try: - address = self.address.resolve(context) + address = self.address.resolve(context) or '' template_name = self.template_name.resolve(context) - map, _ = Address.objects.get_or_create(address=address or '') + if isinstance(address, basestring): + # if not exists the searched address then created an unsaved instance + try: + address = Address.objects.get(address=address) + except: + address = Address(address=address) + + address = [address,] + context.update({ - 'map': map, + 'markers': address, 'width': self.width, 'height': self.height, + # FIXME: if the list is empty? + 'latitude': address[0].latitude, + 'longitude': address[0].longitude, 'zoom': self.zoom, - 'template_name': template_name }) return render_to_string(template_name, context_instance=context) except template.VariableDoesNotExist: diff --git a/easy_maps/widgets.py b/easy_maps/widgets.py index 26ee447..292b8c1 100644 --- a/easy_maps/widgets.py +++ b/easy_maps/widgets.py @@ -2,8 +2,25 @@ from django.template import Template, Context class AddressWithMapWidget(TextInput): + class Media: + js = ( + 'https://maps.google.com/maps/api/js?sensor=false', + 'js/easy_maps.js', + ) def render(self, name, value, attrs=None): + # retrieve the field's id otherwise it's not possible + # to use correctly the JS + _id = attrs["id"] + + # we assume two conditions on 'id' + assert _id.find('id_') == 0 + + find_id = _id.find('address') + assert find_id > 0 + + _id = _id[:find_id] default_html = super(AddressWithMapWidget, self).render(name, value, attrs) - map_template = Template("{% load easy_maps_tags %}{% easy_map address 700 200 16 %}") - context = Context({'address': value}) + map_template = Template("""{% load easy_maps_tags %}{% easy_map address 700 200 16 %}""") + + context = Context({'id': _id, 'id_safe': _id.replace('-', '_'), 'address': value}) return default_html + map_template.render(context) diff --git a/easy_maps_tests/settings.py b/easy_maps_tests/settings.py index a2340a7..1fe73b0 100644 --- a/easy_maps_tests/settings.py +++ b/easy_maps_tests/settings.py @@ -28,3 +28,5 @@ # 'devserver', 'south', ) + +EASY_MAPS_CENTER = (45.0677201, 7.6825531) diff --git a/easy_maps_tests/test_app/models.py b/easy_maps_tests/test_app/models.py index b7e03fc..135ad27 100644 --- a/easy_maps_tests/test_app/models.py +++ b/easy_maps_tests/test_app/models.py @@ -1 +1,27 @@ #hello, testrunner! +from django.db import models +from django.contrib import admin + +from easy_maps.models import Address +from easy_maps.admin import AddressInlineAdmin + +class Brand(models.Model): + name = models.CharField(max_length=100) + + def count(self): + return self.shop_set.count() + +class Shop(Address): + brand = models.ForeignKey(Brand) + +class ShopInlineAdmin(AddressInlineAdmin): + model = Shop + +class BrandAdmin(admin.ModelAdmin): + list_display = ['name', 'count',] + model = Brand + inlines = [ + ShopInlineAdmin, + ] + +admin.site.register(Brand, BrandAdmin)