Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 47 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,32 @@ A Django app for store places with autocomplete function and a related map to th

Install `dj-places` and add it to your installed apps:

```
```bash
$ pip install dj-places
```

```
```py
INSTALLED_APPS = (
...
'places',
...
)
```

Add djangorestframework to your installed apps (required for the package as it provide a serializer field):

```py
INSTALLED_APPS = (
...
'rest_framework',
...
)
```


Add the following settings and maps api key ([read more here](https://developers.google.com/maps/documentation/javascript/reference/map)):

```python
```bash
PLACES_MAPS_API_KEY='YourAwesomeUltraSecretKey'
PLACES_MAP_WIDGET_HEIGHT=480
PLACES_MAP_OPTIONS='{"center": { "lat": 38.971584, "lng": -95.235072 }, "zoom": 10}'
Expand All @@ -35,7 +46,7 @@ PLACES_MARKER_OPTIONS='{"draggable": true}'

Then use it in a project:

```python
```py
from django.db import models
from places.fields import PlacesField

Expand All @@ -47,16 +58,16 @@ class MyLocationModel(models.Model):

This enables the following API:

```python
>>> from myapp.models import ModelName
>>> poi = ModelName.objects.get(id=1)
>>> poi.position
```bash
>>> from myapp.models import MyLocationModel
>>> poi = MyLocationModel.objects.get(id=1)
>>> poi.location
Place('Metrocentro, Managua, Nicaragua', 52.522906, 13.41156)
>>> poi.position.place
>>> poi.location.place
'Metrocentro, Managua, Nicaragua'
>>> poi.position.latitude
>>> poi.location.latitude
52.522906
>>> poi.position.longitude
>>> poi.location.longitude
13.41156
```

Expand All @@ -72,6 +83,28 @@ For using outside the Django Admin:
```
Remember to add the `{{ form.media }}` in your template.


For usage in Djangorestframework Serializers:

```py
from places.serializers import PlacesSerializerField
from rest_framework import serializers

class MyLocationModelSerializer(serializers.Serializer):
location = PlaceSerializerField()
```

How the location data is displayed when doing a GET request in JSON with a serializer that has the field included, and is also how the data should be provided when doing a PUT/PATCH/POST:

```json
"location": {
"city": "Stockholm",
"country": "Sverige",
"latitude": "59.32932349999999",
"longitude": "18.0685808"
}
```

## Demo
------

Expand All @@ -86,6 +119,9 @@ Tools used in rendering this package:
* [cookiecutter-djangopackage](https://github.com/pydanny/cookiecutter-djangopackage)
* [jquery-geocomplete](https://github.com/ubilabs/geocomplete) (_no longer used in the project._)

Contributors
[minifisk](https://github.com/minifisk) - Adding serializer field

### Similar Projects
------------

Expand Down
6 changes: 4 additions & 2 deletions places/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@


class Places(object):
def __init__(self, place, latitude, longitude):
def __init__(self, place, latitude, longitude, name=None, formatted_address=None):

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.name = name
self.formatted_address = formatted_address
self.latitude = Decimal(latitude)
self.longitude = Decimal(longitude)

def __str__(self):
return "%s, %s, %s" % (self.place, self.latitude, self.longitude)
return "%s, %s, %s, %s, %s" % (self.place, self.latitude, self.longitude, self.name, self.formatted_address)

def __repr__(self):
return "Places(%s)" % str(self)
Expand Down
71 changes: 45 additions & 26 deletions places/fields.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import json
from decimal import Decimal
import decimal

from django.db import models
from django.utils.translation import gettext_lazy as _
from django.db.models import JSONField

try:
from django.utils.encoding import smart_text
except ImportError:
Expand All @@ -14,50 +19,64 @@
from .forms import PlacesField as PlacesFormField


class PlacesField(models.Field):
class PlacesField(JSONField):
description = _('A geoposition field (latitude and longitude)')

def __init__(self, *args, **kwargs):
kwargs['max_length'] = 255
super(PlacesField, self).__init__(*args, **kwargs)

def get_internal_type(self):
return 'CharField'

def to_python(self, value):
if not value or value == 'None' or value == '':
return None

if isinstance(value, Places):
return value

if isinstance(value, list):
return Places(value[0], value[1], value[2])

value_parts = [Decimal(val) for val in value.split(',')[-2:]]
# Check if value is a string representation of a list
if isinstance(value, str):
try:
value = json.loads(value)
except (ValueError, TypeError):
pass # If it's not a JSON string, proceed with the original value

try:
latitude = value_parts[0]
except IndexError:
latitude = '0.0'
if isinstance(value, list):
# Process list to create a Places object
if len(value) >= 8:
return Places(
country=value[5],
city=value[6],
state=value[7],
latitude=value[1],
longitude=value[2],
name=value[3],
formatted_address=value[4]
)

if value is None or isinstance(value, dict):
return value

# Handle string representation of a dict
try:
longitude = value_parts[1]
except IndexError:
longitude = '0.0'
value_dict = json.loads(value)
return Places.from_dict(value_dict)
except (ValueError, TypeError):
# In case the string cannot be converted to a dict
return None

try:
place = ','.join(value.split(',')[:-2])
except:
pass
def get_prep_value(self, value):
if isinstance(value, Places):
return value.to_dict()

return Places(place, latitude, longitude)
# If the value is already a dict or None, just use it as-is
return value

def clean(self, value, model_instance):
return value

def from_db_value(self, value, expression, connection):
if value is None:
return value
return self.to_python(value)

def get_prep_value(self, value):
return str(value)
def from_db_value(self, value, expression, connection):
return self.to_python(value)

def value_to_string(self, obj):
value = self.value_from_object(obj)
Expand Down
30 changes: 29 additions & 1 deletion places/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,18 @@ class PlacesField(forms.MultiValueField):
default_error_messages = {'invalid': _('Enter a valid geoposition.')}

def __init__(self, *args, **kwargs):
kwargs.pop('encoder', None)
kwargs.pop('decoder', None)

fields = (
forms.CharField(label=_('place')),
forms.DecimalField(label=_('Latitude')),
forms.DecimalField(label=_('Longitude')),
forms.CharField(label=_('Name'), required=False),
forms.CharField(label=_('Formatted Address'), required=False),
forms.CharField(label=_('Country'), required=False),
forms.CharField(label=_('City'), required=False),
forms.CharField(label=_('State'), required=False),
)
if 'initial' in kwargs and kwargs['initial'] != '':
kwargs['initial'] = Places(*kwargs['initial'].split(','))
Expand All @@ -28,5 +36,25 @@ def widget_attrs(self, widget):

def compress(self, value_list):
if value_list:
return value_list
place = Places(
latitude=value_list[1],
longitude=value_list[2],
name=value_list[3],
formatted_address=value_list[4],
country=value_list[5],
city=value_list[6],
state=value_list[7],
)
return place
return ""


def clean(self, value):
return value

def prepare_value(self, value):
if isinstance(value, Places):
return value.to_dict()
return value


22 changes: 22 additions & 0 deletions places/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from rest_framework import serializers
from decimal import Decimal, InvalidOperation
from . import Places

class PlacesSerializerField(serializers.Field):
"""
Custom serializer field for handling a Places object.
The expected input format for deserialization is a dictionary with keys 'country', 'city', 'latitude', 'longitude'.
For serialization, it converts the Places object to this dictionary format.
"""

def to_representation(self, obj):
if isinstance(obj, Places):
return obj.to_dict()
elif isinstance(obj, dict):
return obj
else:
return None

def to_internal_value(self, data):
place_from_dict = Places.from_dict(data)
return place_from_dict
4 changes: 2 additions & 2 deletions places/static/places/places.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.places-widget {
/* .places-widget {
overflow: hidden;
}
.places-widget > input:first-child {
Expand All @@ -12,4 +12,4 @@
.places-widget > div {
border: 1px solid #CACACA;
margin-top: 10px;
}
} */
34 changes: 32 additions & 2 deletions places/static/places/places.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
function setupDjangoPlaces(mapConfig, markerConfig, childs) {
var searchBox = new google.maps.places.SearchBox(childs[0]);
var latInput = childs[1];
var lngInput = childs[2];
var searchBox = new google.maps.places.SearchBox(childs[0]);
var gmap = new google.maps.Map(childs[3], mapConfig);
var nameInput = childs[3];
var addressInput = childs[4];
var countryInput = childs[5];
var cityInput = childs[6];
var stateInput = childs[7];
var gmap = new google.maps.Map(childs[8], mapConfig);

var marker = new google.maps.Marker(markerConfig);

if (latInput.value && lngInput.value) {
Expand Down Expand Up @@ -32,6 +38,30 @@ function setupDjangoPlaces(mapConfig, markerConfig, childs) {
};
marker.setPosition(place.geometry.location);
marker.setMap(gmap);
console.log('place', place)


let country
let city
let state

place.address_components.forEach(function(component) {
if (component.types.includes("country")) {
country = component.long_name;
}
if (component.types.includes("locality") || component.types.includes("postal_town")) {
city = component.long_name;
}
if (component.types.includes("administrative_area_level_1")) {
state = component.long_name;
}
});

nameInput.value = place.name;
addressInput.value = place.formatted_address;
countryInput.value = country;
cityInput.value = city;
stateInput.value = state;
latInput.value = place.geometry.location.lat();
lngInput.value = place.geometry.location.lng();
gmap.setCenter(place.geometry.location);
Expand Down
Loading