diff --git a/LICENSE b/LICENSE index 116e98c..4b0eb18 100644 --- a/LICENSE +++ b/LICENSE @@ -1,12 +1,10 @@ -Copyright (c) 2016, Oscar Cortez -All rights reserved. -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +MIT License -* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +Copyright (c) 2017, Oscar Cortez -* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +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: -* Neither the name of dj-places nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +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. diff --git a/MANIFEST.in b/MANIFEST.in index 57f02f3..6232472 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,4 +2,5 @@ include AUTHORS.md include CHANGELOG.md include LICENSE include README.md -recursive-include djplaces *.html *js *.css *py +recursive-include places/templates *.html +recursive-include places/static *.js *.css diff --git a/Makefile b/Makefile index 9f32ae3..e452622 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ help: @echo "test-all - run tests on every Python version with tox" @echo "coverage - check code coverage quickly with the default Python" @echo "docs - generate Sphinx HTML documentation, including API docs" + @echo "demo - run the demo project" @echo "release - package and upload a release" @echo "sdist - package" @@ -46,6 +47,9 @@ docs: $(MAKE) -C docs html open docs/_build/html/index.html +run: + python example/manage.py runserver + release: clean python setup.py sdist upload python setup.py bdist_wheel upload diff --git a/README.md b/README.md index b25d5b1..3e5975d 100644 --- a/README.md +++ b/README.md @@ -20,32 +20,29 @@ Install dj-places and add it to your installed apps: INSTALLED_APPS = ( ... - 'djplaces', + 'places', ... ) Add your maps api key in your settings ( [read more here](https://developers.google.com/maps/documentation/javascript/3.exp/reference) ): - MAPS_API_KEY='YourAwesomeUltraSecretKey' + PLACES_MAPS_API_KEY='YourAwesomeUltraSecretKey' Then use it in a project: - from djplaces.fields import LocationField - place = models.CharField(max_length=250) - location = LocationField(base_field='place') + from places.fields import PlacesField + location = PlacesField() Demo ------ -![](http://g.recordit.co/hZabhhYLHS.gif) +![](http://g.recordit.co/LheQH0HDMR.gif) TODO-LIST -------- * [ ] Write some test ASAP! * [ ] Support Inline Admin -* [ ] Set custom zoom map value -* [ ] Custom property for lat and lng values Running Tests -------------- @@ -59,8 +56,6 @@ Does the code actually work? Credits --------- -Special thanks to [Helmy Giacoman](https://github.com/eos87) for motivating me to make this package. - Tools used in rendering this package: * [Cookiecutter](https://github.com/audreyr/cookiecutter) @@ -71,4 +66,4 @@ Similar Projects ------------ * [Django Location Field](https://github.com/caioariede/django-location-field) -* [Django Geoposition](https://github.com/philippbosch/django-geoposition) +* [Django GeoPosition](https://github.com/philippbosch/django-geoposition) diff --git a/djplaces/__init__.py b/djplaces/__init__.py deleted file mode 100644 index 858de17..0000000 --- a/djplaces/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = '1.0.5' diff --git a/djplaces/fields.py b/djplaces/fields.py deleted file mode 100644 index 11cec6a..0000000 --- a/djplaces/fields.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- - -from django.db.models import CharField -from django.core.exceptions import ImproperlyConfigured -from django.utils.translation import ugettext_lazy as _ - -from .widgets import LocationWidget - - -class LocationField(CharField): - description = _("A geoposition field (latitude and longitude)") - - def __init__(self, verbose_name=None, name=None, - base_field=None, *args, **kwargs): - self.verbose_name = verbose_name - self.name = name - self.base_field = base_field - kwargs['max_length'] = 63 - super(LocationField, self).__init__( - verbose_name, name, *args, **kwargs) - - def deconstruct(self): - name, path, args, kwargs = super(LocationField, self).deconstruct() - del kwargs["max_length"] - - if self.base_field == '': - raise ImproperlyConfigured() - return name, path, args, kwargs - - def formfield(self, **kwargs): - kwargs['widget'] = LocationWidget - return super(LocationField, self).formfield(**kwargs) diff --git a/djplaces/static/css/djplaces.css b/djplaces/static/css/djplaces.css deleted file mode 100644 index 80ee0e5..0000000 --- a/djplaces/static/css/djplaces.css +++ /dev/null @@ -1,7 +0,0 @@ -.djplaces { - height: 500px; - width: 500px; - border: 1px solid #CACACA; - margin-top: 10px; -} - diff --git a/djplaces/static/js/djplaces.js b/djplaces/static/js/djplaces.js deleted file mode 100644 index c324eda..0000000 --- a/djplaces/static/js/djplaces.js +++ /dev/null @@ -1,32 +0,0 @@ -var dj = jQuery.noConflict(); - -dj(function() { - - var options = { - map: "#map_location", - mapOptions: { zoom: 10 }, - markerOptions: { draggable: true }, - types: ["geocode", "establishment"], - }, - geocomplete = dj("#id_place"); - - if ( dj('#id_location').val() ) { - options.location = dj('#id_location').val() - } - - geocomplete - .geocomplete(options) - .bind("geocode:result", function(event, result) { - dj('#id_location').val(result.geometry.location.lat() + ',' + result.geometry.location.lng()); - }) - .bind("geocode:error", function(event, status){ - console.log("ERROR: " + status); - }) - .bind("geocode:multiple", function(event, results){ - console.log("Multiple: " + results.length + " results found"); - }) - .bind("geocode:dragged", function(event, latLng){ - dj('#id_location').val(latLng.lat() + ',' + latLng.lng()); - }); - -}); diff --git a/djplaces/templates/djplaces/map_widget.html b/djplaces/templates/djplaces/map_widget.html deleted file mode 100644 index 5ef867b..0000000 --- a/djplaces/templates/djplaces/map_widget.html +++ /dev/null @@ -1,5 +0,0 @@ - -{{ field_input }} - -
- diff --git a/djplaces/widgets.py b/djplaces/widgets.py deleted file mode 100644 index 4b5dc6c..0000000 --- a/djplaces/widgets.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- - -from django import forms -from django.forms import widgets -from django.conf import settings -from django.template.loader import render_to_string -from django.utils.safestring import mark_safe - - -class LocationWidget(widgets.TextInput): - - def render(self, name, value, attrs=None): - text_input = super(LocationWidget, self).render(name, value, attrs) - - return render_to_string('djplaces/map_widget.html', { - 'field_name': name, - 'field_input': mark_safe(text_input) - }) - - def _media(self): - return forms.Media( - css={'all': ('css/djplaces.css',)}, - js=( - '//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.0/jquery.min.js', # NOQA - '//maps.googleapis.com/maps/api/js?key='+ settings.MAPS_API_KEY +'&libraries=places', # NOQA - '//cdnjs.cloudflare.com/ajax/libs/geocomplete/1.7.0/jquery.geocomplete.js', # NOQA - 'js/djplaces.js', - ) - ) - media = property(_media) diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..1431ae5 --- /dev/null +++ b/example/README.md @@ -0,0 +1,24 @@ +##Example Project for django-places + +This example is provided as a convenience feature to allow potential users to try the app straight from the app repo without having to create a django project. + +It can also be used to develop the app in place. + +To run this example, follow these instructions: + +1. Navigate to the `example` directory +2. Install the requirements for the package: + + pip install -r requirements.txt + +3. Make and apply migrations + + python manage.py makemigrations + + python manage.py migrate + +4. Run the server + + python manage.py runserver + +5. Access from the browser at `http://127.0.0.1:8000` diff --git a/example/example/__init__.py b/example/example/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example/example/settings.py b/example/example/settings.py new file mode 100644 index 0000000..05337c7 --- /dev/null +++ b/example/example/settings.py @@ -0,0 +1,116 @@ +""" +Django settings for example project. + +Generated by Cookiecutter Django Package + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.9/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '%b&p!s^@301dc=@y^*x92ff*hzelqm0)m0vy29-dwexs=e1w+t' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + + 'places', + 'geoposition', + 'points', +] + +MIDDLEWARE_CLASSES = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'example.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'templates'), ], + 'OPTIONS': { + 'debug': True, + 'loaders': [ + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + ], + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.template.context_processors.i18n', + 'django.template.context_processors.media', + 'django.template.context_processors.static', + 'django.template.context_processors.tz', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'example.wsgi.application' + +# Database +# https://docs.djangoproject.com/en/1.9/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + +# Password validation +# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [] + +# Internationalization +# https://docs.djangoproject.com/en/1.9/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.9/howto/static-files/ + +STATIC_URL = '/static/' + +PLACES_MAPS_API_KEY = 'AIzaSyBS2jGczsTKS9oMjcr_OS4BV3nRcp8EgHw' diff --git a/example/example/urls.py b/example/example/urls.py new file mode 100644 index 0000000..6446991 --- /dev/null +++ b/example/example/urls.py @@ -0,0 +1,22 @@ +"""example URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.9/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf.urls import url, include +from django.contrib import admin + + +urlpatterns = [ + url(r'^admin/', admin.site.urls), +] diff --git a/example/example/wsgi.py b/example/example/wsgi.py new file mode 100644 index 0000000..fd6d782 --- /dev/null +++ b/example/example/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for example project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") + +application = get_wsgi_application() diff --git a/example/manage.py b/example/manage.py new file mode 100644 index 0000000..2605e37 --- /dev/null +++ b/example/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/example/points/__init__.py b/example/points/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example/points/admin.py b/example/points/admin.py new file mode 100644 index 0000000..3d4d4de --- /dev/null +++ b/example/points/admin.py @@ -0,0 +1,22 @@ +from django.conf import settings +from django.contrib import admin + +from .models import Place + +class PlaceAdmin(admin.ModelAdmin): + list_display = ('position_map', 'location') + + def position_map(self, instance): + if instance.location is not None: + return '' % { + 'latitude': instance.location.latitude, + 'longitude': instance.location.longitude, + 'key': getattr(settings, 'PLACES_MAPS_API_KEY'), + 'zoom': 15, + 'width': 100, + 'height': 100, + 'scale': 2 + } + position_map.allow_tags = True + +admin.site.register(Place, PlaceAdmin) diff --git a/example/points/migrations/0001_initial.py b/example/points/migrations/0001_initial.py new file mode 100644 index 0000000..e35283b --- /dev/null +++ b/example/points/migrations/0001_initial.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-09 16:07 +from __future__ import unicode_literals + +from django.db import migrations, models +import places.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Place', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('location', places.fields.PlacesField(blank=True, max_length=63)), + ], + ), + ] diff --git a/example/points/migrations/__init__.py b/example/points/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example/points/models.py b/example/points/models.py new file mode 100644 index 0000000..4ab241e --- /dev/null +++ b/example/points/models.py @@ -0,0 +1,13 @@ +from django.db import models + +from places.fields import PlacesField + + +class Place(models.Model): + location = PlacesField(blank=True) + + def __unicode__(self): + return self.location.place + + def __str__(self): + return self.__unicode__() diff --git a/example/requirements.txt b/example/requirements.txt new file mode 100644 index 0000000..bc331ce --- /dev/null +++ b/example/requirements.txt @@ -0,0 +1,5 @@ +# Your app requirements. +-r ../requirements_test.txt + +# Your app in editable mode. +-e ../ diff --git a/example/templates/djplaces/base.html b/example/templates/djplaces/base.html new file mode 100644 index 0000000..0d53bee --- /dev/null +++ b/example/templates/djplaces/base.html @@ -0,0 +1,88 @@ +{% load staticfiles i18n %} + + + + + {% block title %}Django Places{% endblock title %} + + + + + + + + {% block css %} + + + + + + {% endblock %} + + + + +
+ +
+ +
+ + {% if messages %} + {% for message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + + {% block content %} +
+

Use this document as a way to quick start any new project.

+

The current template is loaded from + django-places/example/templates/base.html.

+

Whenever you overwrite the contents of django-places/djplaces/urls.py with your + own content, you should see it here.

+
+ {% endblock content %} + +
+ +{% block modal %}{% endblock modal %} + + + +{% block javascript %} + + + + + + + + + + + +{% endblock javascript %} + + + diff --git a/places/__init__.py b/places/__init__.py new file mode 100644 index 0000000..0fa9c52 --- /dev/null +++ b/places/__init__.py @@ -0,0 +1,34 @@ +from __future__ import unicode_literals +from decimal import Decimal + +default_app_config = 'places.apps.PlacesConfig' +__version__ = '1.1.0' + + +class Places(object): + + def __init__(self, place, latitude, longitude): + + if isinstance(latitude, float) or isinstance(latitude, int): + latitude = str(latitude) + if isinstance(longitude, float) or isinstance(longitude, int): + longitude = str(longitude) + + self.place = place + self.latitude = Decimal(latitude) + self.longitude = Decimal(longitude) + + def __str__(self): + return "%s, %s, %s" % (self.place, self.latitude, self.longitude) + + def __repr__(self): + return "Places(%s)" % str(self) + + def __len__(self): + return len(str(self)) + + def __eq__(self, other): + return isinstance(other, Places) and self.latitude == other.latitude and self.longitude == other.longitude + + def __ne__(self, other): + return not isinstance(other, Places) or self.latitude != other.latitude or self.longitude != other.longitude diff --git a/places/apps.py b/places/apps.py new file mode 100644 index 0000000..803b6d7 --- /dev/null +++ b/places/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + +class PlacesConfig(AppConfig): + name = 'places' + verbose_name = "Places" diff --git a/places/conf.py b/places/conf.py new file mode 100644 index 0000000..24af601 --- /dev/null +++ b/places/conf.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +from django.conf import settings as django_settings +from django.core.exceptions import ImproperlyConfigured + + +class AppSettings(object): + prefix = 'PLACES' + required_settings = ['MAPS_API_KEY'] + defaults = { + 'MAPS_API_KEY': None, + 'MAP_WIDGET_HEIGHT': 480, + 'MAP_OPTIONS': {}, + 'MARKER_OPTIONS': {} + } + + def __init__(self, django_settings): + self.django_settings = django_settings + + for setting in self.required_settings: + prefixed_name = '%s_%s' % (self.prefix, setting) + if not hasattr(self.django_settings, prefixed_name): + raise ImproperlyConfigured("The '%s' setting is required." % prefixed_name) + + def __getattr__(self, name): + prefixed_name = '%s_%s' % (self.prefix, name) + if hasattr(django_settings, prefixed_name): + return getattr(django_settings, prefixed_name) + if name in self.defaults: + return self.defaults[name] + import pdb; pdb.set_trace() + raise AttributeError("'AppSettings' object does not have a '%s' attribute" % name) + + +settings = AppSettings(django_settings) diff --git a/places/fields.py b/places/fields.py new file mode 100644 index 0000000..6a66f3b --- /dev/null +++ b/places/fields.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import re +from decimal import Decimal + +from django.db import models +from django.utils.six import with_metaclass +from django.utils.encoding import smart_text +from django.utils.translation import ugettext_lazy as _ + +from . import Places +from .forms import PlacesField as PlacesFormField + + +class PlacesField(models.Field): + description = _("A geoposition field (latitude and longitude)") + + def __init__(self, *args, **kwargs): + kwargs['max_length'] = 63 + super(PlacesField, self).__init__(*args, **kwargs) + + def get_internal_type(self): + return 'CharField' + + def to_python(self, value): + if not value or value == 'None': + return None + if isinstance(value, Places): + return value + if isinstance(value, list): + return Places(value[0], value[1], value[2]) + + matched = re.finditer(r'[-+]?\d*\.\d+|\d+', value) + value_parts = [Decimal(x.group()) for x in matched] + try: + latitude = value_parts[0] + except IndexError: + latitude = '0.0' + try: + longitude = value_parts[1] + except IndexError: + longitude = '0.0' + try: + place = re.sub(r'[-+]?\d*\.\d+|\d+', '', value)[:-4] + except: + pass + + return Places(place, latitude, longitude) + + def from_db_value(self, value, expression, connection, context): + return self.to_python(value) + + def get_prep_value(self, value): + return str(value) + + def value_to_string(self, obj): + value = self._get_val_from_obj(obj) + return smart_text(value) + + def formfield(self, **kwargs): + defaults = { + 'form_class': PlacesFormField + } + defaults.update(kwargs) + return super(PlacesField, self).formfield(**defaults) diff --git a/places/forms.py b/places/forms.py new file mode 100644 index 0000000..5ded832 --- /dev/null +++ b/places/forms.py @@ -0,0 +1,34 @@ +from __future__ import unicode_literals + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from .widgets import PlacesWidget +from . import Places + + +class PlacesField(forms.MultiValueField): + default_error_messages = { + 'invalid': _('Enter a valid geoposition.') + } + + def __init__(self, *args, **kwargs): + self.widget = PlacesWidget() + fields = ( + forms.CharField(label=_('place')), + forms.DecimalField(label=_('latitude')), + forms.DecimalField(label=_('longitude')), + ) + if 'initial' in kwargs: + kwargs['initial'] = Location(*kwargs['initial'].split(',')) + super(PlacesField, self).__init__(fields, **kwargs) + + def widget_attrs(self, widget): + classes = widget.attrs.get('class', '').split() + classes.append('places') + return {'class': ' '.join(classes)} + + def compress(self, value_list): + if value_list: + return value_list + return "" diff --git a/places/models.py b/places/models.py new file mode 100644 index 0000000..e69de29 diff --git a/places/static/places/places.css b/places/static/places/places.css new file mode 100644 index 0000000..71c9117 --- /dev/null +++ b/places/static/places/places.css @@ -0,0 +1,8 @@ +.places-widget { + overflow: hidden; +} + + .places-widget > div { + border: 1px solid #CACACA; + margin-top: 10px; + } diff --git a/places/static/places/places.js b/places/static/places/places.js new file mode 100644 index 0000000..9154dbb --- /dev/null +++ b/places/static/places/places.js @@ -0,0 +1,32 @@ +var dj = jQuery.noConflict(); + +dj(function() { + var mapElement = dj("#map_location"); + var mapInput = dj("#map_place input"); + var options = { + map: mapElement, + mapOptions: dj(".places-widget").data("mapOptions") ? dj(".places-widget").data("mapOptions") : { zoom: 10 }, + markerOptions: dj(".places-widget").data("markerOptions") ? dj(".places-widget").data("markerOptions") : { draggable: true }, + types: ["geocode", "establishment"], + location: mapInput.val().length > 0 ? [dj("#map_latitude input").val(), dj("#map_longitude input").val()] : false, + }, + geocomplete = mapInput; + + geocomplete + .geocomplete(options) + .bind("geocode:result", function(event, result) { + dj("#map_latitude input").val(result.geometry.location.lat()); + dj("#map_longitude input").val(result.geometry.location.lng()); + }) + .bind("geocode:error", function(event, status){ + console.log("ERROR: " + status); + }) + .bind("geocode:multiple", function(event, results){ + console.log("Multiple: " + results.length + " results found"); + }) + .bind("geocode:dragged", function(event, latLng){ + dj("#map_latitude input").val(latLng.lat()); + dj("#map_longitude input").val(latLng.lng()); + }); + +}); diff --git a/places/templates/places/widgets/places.html b/places/templates/places/widgets/places.html new file mode 100644 index 0000000..897dd68 --- /dev/null +++ b/places/templates/places/widgets/places.html @@ -0,0 +1,17 @@ +
+ + + + + + + + + + + + + +
{{ place.label|capfirst }}:{{ place.html }}
{{ latitude.label|capfirst }}:{{ latitude.html }}{{ longitude.label|capfirst }}:{{ longitude.html }}
+
+
diff --git a/places/widgets.py b/places/widgets.py new file mode 100644 index 0000000..06cbdb6 --- /dev/null +++ b/places/widgets.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- + +from django import forms +from django.utils import six +from django.template.loader import render_to_string +from django.utils.translation import ugettext_lazy as _ + +from .conf import settings + + +class PlacesWidget(forms.MultiWidget): + def __init__(self, attrs=None): + widgets = ( + forms.TextInput(attrs={'data-geo': 'formatted_address',}), + forms.TextInput(attrs={'data-geo': 'lat',}), + forms.TextInput(attrs={'data-geo': 'lng',}), + ) + super(PlacesWidget, self).__init__(widgets, attrs) + + def decompress(self, value): + if isinstance(value, six.text_type): + return value.rsplit(',') + if value: + return [value.place, value.latitude, value.longitude] + return [None, None] + + def format_output(self, rendered_widgets): + return render_to_string('places/widgets/places.html', { + 'place': { + 'html': rendered_widgets[0], + 'label': _("place"), + }, + 'latitude': { + 'html': rendered_widgets[1], + 'label': _("latitude"), + }, + 'longitude': { + 'html': rendered_widgets[2], + 'label': _("longitude"), + }, + 'config': { + 'map_widget_height': settings.MAP_WIDGET_HEIGHT or 500, + 'map_options': settings.MAP_OPTIONS or '', + 'marker_options': settings.MARKER_OPTIONS or '', + } + }) + + class Media: + js = ( + '//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.0/jquery.min.js', # NOQA + '//maps.googleapis.com/maps/api/js?key='+ settings.MAPS_API_KEY +'&libraries=places', # NOQA + '//cdnjs.cloudflare.com/ajax/libs/geocomplete/1.7.0/jquery.geocomplete.js', # NOQA + 'places/places.js', + ) + css = { + 'all': ('places/places.css',) + } diff --git a/requirements.txt b/requirements.txt index 6eeb004..6463638 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -django>=1.9.1 +django>=1.8 # Additional requirements go here diff --git a/requirements-test.txt b/requirements_test.txt similarity index 86% rename from requirements-test.txt rename to requirements_test.txt index 22cf712..9894fe2 100644 --- a/requirements-test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -django>=1.9.1 +django>=1.8 coverage mock>=1.0.1 flake8>=2.1.0 diff --git a/setup.cfg b/setup.cfg index 93758bc..772f2ec 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.0.0 +current_version = 1.0.5 commit = True tag = True @@ -9,3 +9,13 @@ tag = True [wheel] universal = 1 + +[flake8] +ignore = D203 +exclude = + .git, + .tox + docs/source/conf.py, + build, + dist +max-line-length = 119 diff --git a/setup.py b/setup.py index 15b9993..6043a9f 100755 --- a/setup.py +++ b/setup.py @@ -5,9 +5,9 @@ import sys try: - from setuptools import setup + from setuptools import setup, find_packages except ImportError: - from distutils.core import setup + from distutils.core import setup, find_packages def get_version(*file_paths): @@ -19,7 +19,7 @@ def get_version(*file_paths): return version_match.group(1) raise RuntimeError('Unable to find version string.') -version = get_version('djplaces', '__init__.py') +version = get_version('places', '__init__.py') if sys.argv[-1] == 'publish': try: @@ -44,32 +44,29 @@ def get_version(*file_paths): name='dj-places', version=version, description="""A django app for store places""", - long_description=readme + '\n\n' + history, author='Oscar Cortez', author_email='om.cortez.2010@gmail.com', url='https://github.com/oscarmcm/django-places', - packages=[ - 'djplaces', - ], + packages=find_packages(), + package_data={ + 'places': [ + 'locale/*/LC_MESSAGES/*', + 'templates/places/widgets/*.html', + 'static/places/*', + ], + }, include_package_data=True, install_requires=[ ], - license="BSD", + license="MIT", zip_safe=False, keywords='django geocomplete google maps places', classifiers=[ - 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', 'Framework :: Django', - 'Framework :: Django :: 1.8', - 'Framework :: Django :: 1.9', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Natural Language :: English', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python', ], ) diff --git a/tox.ini b/tox.ini index 1dee82e..3763e7a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,17 @@ [tox] -envlist = py27, py33, py34, py35 +envlist = + {py27,py32,py33,py34,py35}-django-18 [testenv] setenv = PYTHONPATH = {toxinidir}:{toxinidir}/djplaces -commands = python runtests.py +commands = coverage run --source djplaces runtests.py deps = - -r{toxinidir}/requirements-test.txt + django-18: Django>=1.8,<1.9 + -r{toxinidir}/requirements_test.txt +basepython = + py35: python3.5 + py34: python3.4 + py33: python3.3 + py32: python3.2 + py27: python2.7