Skip to content

Commit

Permalink
Merge pull request simon-the-shark#8 from Simon-the-Shark/geodjango
Browse files Browse the repository at this point in the history
Geodjango
  • Loading branch information
simon-the-shark committed Nov 3, 2019
2 parents bf52534 + 806ec8c commit aefceee
Show file tree
Hide file tree
Showing 10 changed files with 220 additions and 66 deletions.
15 changes: 14 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
dist: xenial
language: python

sudo: true
addons:
postgresql: '10'
apt:
packages:
- postgresql-10-postgis-2.4
- postgresql-10-postgis-2.4-scripts
- postgresql-client-10
matrix:
fast_finish: true
include:
Expand All @@ -17,12 +24,18 @@ matrix:
- { python: "3.7", env: DJANGO_VERSION=2.0 }
- { python: "3.7", env: DJANGO_VERSION=2.1 }
- { python: "3.7", env: DJANGO_VERSION=2.2 }
before_install:
- sudo -u postgres psql -c "CREATE USER testuser WITH PASSWORD 'password'"
- sudo -u postgres psql -c "ALTER ROLE testuser SUPERUSER"

install:
- pip install coverage
- pip install coveralls
- pip install -q Django==$DJANGO_VERSION
- pip install psycopg2

before_script:
- psql -U postgres -c "create extension postgis"
script:
- coverage run --source=mapbox_location_field manage.py test
after_success:
Expand Down
75 changes: 44 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* [Instalation](#instalation)
* [Configuration](#configuration)
* [Usage](#usage)
* [PLAIN (non-spatial) db](#plain-database)
* [SPATIAL db](#spatial-database)
* [Customization](#customization)
* [map_attrs](#map_attrs)
* [bootstrap](#bootstrap)
Expand All @@ -35,7 +37,8 @@ PS. Django 1.11 does not support Python 3.7 anymore.

#### Browser support
django-mapbox-location-field support all browsers, which are suported by mapbox gl js. Read more [here](https://docs.mapbox.com/help/troubleshooting/mapbox-browser-support/#mapbox-gl-js)

#### Databases support
It should work with every **spatial** and **plain** (non-spatial) database, that works with django and geodjango.
# Live demo
Curious how it works and looks like ? See live demo on https://django-mapbox-location-field.herokuapp.com
Demo app uses [django-bootstrap4](https://github.com/zostera/django-bootstrap4) for a little better looking form fields.
Expand All @@ -58,44 +61,54 @@ MAPBOX_KEY = "pk.eyJ1IjoibWlnaHR5c2hhcmt5IiwiYSI6ImNqd2duaW4wMzBhcWI0M3F1MTRvbHB
**PS. This above is only example access token. You have to paste here yours.**

# Usage
* Just create some model with LocationField.
```python
from django.db import models
from mapbox_location_field.models import LocationField
* ### PLAIN DATABASE
* Just create some model with LocationField.
```python
from django.db import models
from mapbox_location_field.models import LocationField

class SomeLocationModel(models.Model):
location = LocationField()
class SomeLocationModel(models.Model):
location = LocationField()

```
* Create ModelForm
```python
from django import forms
from .models import Location
```
* ### SPATIAL DATABASE
* Just create some model with SpatialLocationField.
```python
from django.db import models
from mapbox_location_field.models import SpatialLocationField

class LocationForm(forms.ModelForm):
class Meta:
model = Location
fields = "__all__"
```
Of course you can also use CreateView, UpdateView or build Form yourself with mapbox_location_field.forms.LocationField
class SomeLocationModel(models.Model):
location = SpatialLocationField()

```

* Create ModelForm
```python
from django import forms
from .models import Location

class LocationForm(forms.ModelForm):
class Meta:
model = Location
fields = "__all__"
```
Of course you can also use CreateView, UpdateView or build Form yourself with `mapbox_location_field.forms.LocationField` or `mapbox_location_field.forms.SpatialLocationField`
* Then just use it in html view. It can't be simpler!
Paste this in your html head:
```django
{% load mapbox_location_field_tags %}
{% location_field_includes %}
{% include_jquery %}
```
```django
{% load mapbox_location_field_tags %}
{% location_field_includes %}
{% include_jquery %}
```
* And this in your body:
```django
<form method="post">
{% csrf_token %}
{{form}}
<input type="submit" value="submit">
</form>
{{ form.media }}
```
```django
<form method="post">
{% csrf_token %}
{{form}}
<input type="submit" value="submit">
</form>
{{ form.media }}
```
* Your form is ready! Start your website and see how it looks. If you want to change something look to the [customization](#customization) section.

# Customization
Expand Down
3 changes: 2 additions & 1 deletion mapbox_location_field/admin.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from django.contrib import admin
from django.forms import Media

from .models import LocationField, AddressAutoHiddenField, SpatialLocationField
from .widgets import MapAdminInput, AddressAutoHiddenInput
from .models import LocationField, AddressAutoHiddenField


class MapAdmin(admin.ModelAdmin):
"""custom ModelAdmin for LocationField and AddressAutoHiddenField"""
change_form_template = "mapbox_location_field/admin_change.html"
formfield_overrides = {
LocationField: {'widget': MapAdminInput},
SpatialLocationField: {'widget': MapAdminInput},
AddressAutoHiddenField: {"widget": AddressAutoHiddenInput, }

}
Expand Down
53 changes: 53 additions & 0 deletions mapbox_location_field/forms.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
from django import forms
from django.contrib.gis.forms import PointField
from django.contrib.gis.geos import Point
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _

from .widgets import MapInput, AddressAutoHiddenInput


def parse_location(location_string):
"""parse and convert coordinates from string to tuple"""

args = location_string.split(",")
if len(args) != 2:
raise ValidationError(_("Invalid input for a Location instance"))

lat = args[0]
lng = args[1]

try:
lat = float(lat)
except ValueError:
raise ValidationError(_("Invalid input for a Location instance. Latitude must be convertible to float "))
try:
lng = float(lng)
except ValueError:
raise ValidationError(_("Invalid input for a Location instance. Longitude must be convertible to float "))

return lat, lng


class LocationField(forms.CharField):
"""custom form field for picking location"""

Expand All @@ -21,3 +47,30 @@ class AddressAutoHiddenField(forms.CharField):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.label = ""


class SpatialLocationField(PointField):
"""custom form field for picking location for spatial databases"""

def __init__(self, *args, **kwargs):
map_attrs = kwargs.pop("map_attrs", None)
self.widget = MapInput(map_attrs=map_attrs, )

super().__init__(*args, **kwargs)
self.error_messages = {"required": "Please pick a location, it's required", }

def clean(self, value):
try:
return Point(parse_location(value), srid=4326)
except (ValueError, ValidationError):
return None

def to_python(self, value):
"""Transform the value to a Geometry object."""
if value in self.empty_values:
return None

if isinstance(value, Point):
return value

return Point(parse_location(value), srid=4326)
47 changes: 24 additions & 23 deletions mapbox_location_field/models.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,10 @@
from django.core.exceptions import ValidationError
from django.contrib.gis.db.models import PointField
from django.db import models
from django.utils.translation import ugettext_lazy as _

from .forms import AddressAutoHiddenField as AddressAutoHiddenFormField
from .forms import AddressAutoHiddenField as AddressAutoHiddenFormField, parse_location
from .forms import LocationField as LocationFormField


def parse_location(location_string):
"""parse and convert coordinates from string to tuple"""
args = location_string.split(",")
if len(args) != 2:
raise ValidationError(_("Invalid input for a Location instance"))

lat = args[0]
lng = args[1]

try:
lat = float(lat)
except ValueError:
raise ValidationError(_("Invalid input for a Location instance. Latitude must be convertible to float "))
try:
lng = float(lng)
except ValueError:
raise ValidationError(_("Invalid input for a Location instance. Longitude must be convertible to float "))

return lat, lng
from .forms import SpatialLocationField as SpatialLocationFormField


class LocationField(models.CharField):
Expand Down Expand Up @@ -82,3 +62,24 @@ def formfield(self, **kwargs):
defaults = {'form_class': AddressAutoHiddenFormField}
defaults.update(kwargs)
return models.Field.formfield(self, **defaults)


class SpatialLocationField(PointField):
"""custom model field for storing location in spatial databases"""

description = _("Location field for spatial databases, stores Points.")

def __init__(self, *args, **kwargs):
self.map_attrs = kwargs.pop("map_attrs", {})
super().__init__(*args, **kwargs)

def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
kwargs["map_attrs"] = self.map_attrs
return name, path, args, kwargs

def formfield(self, **kwargs):
defaults = {'form_class': SpatialLocationFormField}
defaults.update(kwargs)
defaults.update({"map_attrs": self.map_attrs})
return super().formfield(**defaults)
43 changes: 42 additions & 1 deletion mapbox_location_field/tests/test_forms.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.contrib.gis.geos import Point
from django.test import TestCase

from mapbox_location_field.forms import LocationField, AddressAutoHiddenField
from mapbox_location_field.forms import LocationField, AddressAutoHiddenField, SpatialLocationField
from mapbox_location_field.widgets import MapInput, AddressAutoHiddenInput


Expand All @@ -19,6 +20,46 @@ def test_passing_map_attrs(self):
self.assertEqual(field.widget.map_attrs, {"some": "value", "and some": "cool value"})


class SpatialLocationFieldTests(TestCase):

def test_widget(self):
field = SpatialLocationField()
self.assertEqual(field.widget.__class__, MapInput().__class__)

def test_error_messages(self):
field = SpatialLocationField()
self.assertEqual(field.error_messages["required"], "Please pick a location, it's required")

def test_passing_map_attrs(self):
field = SpatialLocationField(map_attrs={"some": "value", "and some": "cool value"})
self.assertEqual(field.widget.map_attrs, {"some": "value", "and some": "cool value"})

def test_clean(self):
field = SpatialLocationField()
point = field.clean("12,13")

self.assertIsInstance(point, Point)
self.assertEqual(point.x, 12)
self.assertEqual(point.y, 13)

point = field.clean("12")
self.assertIsNone(point)

def test_to_python(self):
field = SpatialLocationField()

for empty in field.empty_values:
self.assertIsNone(field.to_python(empty))

point = Point(12, 13)
self.assertIs(point, field.to_python(point))

point = field.to_python("12,13")
self.assertIsInstance(point, Point)
self.assertEqual(point.x, 12)
self.assertEqual(point.y, 13)


class AddressAutoHiddenFieldTests(TestCase):
def test_widget(self):
field = AddressAutoHiddenField()
Expand Down
25 changes: 20 additions & 5 deletions mapbox_location_field/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from django.test import TestCase
from django.core.exceptions import ValidationError
from django.test import TestCase

from mapbox_location_field.models import parse_location, LocationField, AddressAutoHiddenField
from mapbox_location_field.forms import LocationField as FormLocationField
from mapbox_location_field.forms import AddressAutoHiddenField as FormAddressAutoHiddenField
from mapbox_location_field.forms import LocationField as FormLocationField
from mapbox_location_field.forms import SpatialLocationField as FormSpatialLocationField
from mapbox_location_field.models import parse_location, LocationField, AddressAutoHiddenField, SpatialLocationField


class LocationFieldTests(TestCase):
Expand Down Expand Up @@ -47,10 +48,24 @@ def test_get_prep_value(self):

def test_form_field(self):
instance = LocationField()
self.assertEqual(instance.formfield().__class__, FormLocationField().__class__)
self.assertTrue(isinstance(instance.formfield(), FormLocationField))


class SpatialLocationFieldTests(TestCase):

def test_SpatialLocationField(self):
instance = SpatialLocationField()
self.assertIsInstance(instance, SpatialLocationField)
name, path, args, kwargs = instance.deconstruct()
new_instance = SpatialLocationField(*args, **kwargs)
self.assertEqual(instance.map_attrs, new_instance.map_attrs)

def test_form_field(self):
instance = SpatialLocationField()
self.assertTrue(isinstance(instance.formfield(), FormSpatialLocationField))


class AddressAutoHiddenFieldTests(TestCase):
def test_form_field(self):
instance = AddressAutoHiddenField()
self.assertEqual(instance.formfield().__class__, FormAddressAutoHiddenField().__class__)
self.assertTrue(isinstance(instance.formfield(), FormAddressAutoHiddenField))
5 changes: 5 additions & 0 deletions mapbox_location_field/tests/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ def test_parse_tuple_string(self):
self.assertEqual(parse_tuple_string("(123456,155413.452)"), (123456, 155413.452))
self.assertEqual(parse_tuple_string("(123.456,155413.452)"), (123.456, 155413.452))

self.assertEqual(parse_tuple_string("SRID=4376POINT (123456 155413)"), (123456, 155413))
self.assertEqual(parse_tuple_string("SRID=4376POINT (123456.864534 155413452)"), (123456.864534, 155413452))
self.assertEqual(parse_tuple_string("SRID=4376POINT (123456 155413.452)"), (123456, 155413.452))
self.assertEqual(parse_tuple_string("SRID=4376POINT (123.456 155413.452)"), (123.456, 155413.452))

def test_setting_center_point(self):
widget = MapInput()
widget.get_context("name", (1234.3, 2352145.6), {})
Expand Down
Loading

0 comments on commit aefceee

Please sign in to comment.