Skip to content

Commit aefceee

Browse files
Merge pull request #8 from Simon-the-Shark/geodjango
Geodjango
2 parents bf52534 + 806ec8c commit aefceee

File tree

10 files changed

+220
-66
lines changed

10 files changed

+220
-66
lines changed

.travis.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
dist: xenial
22
language: python
3-
3+
sudo: true
4+
addons:
5+
postgresql: '10'
6+
apt:
7+
packages:
8+
- postgresql-10-postgis-2.4
9+
- postgresql-10-postgis-2.4-scripts
10+
- postgresql-client-10
411
matrix:
512
fast_finish: true
613
include:
@@ -17,12 +24,18 @@ matrix:
1724
- { python: "3.7", env: DJANGO_VERSION=2.0 }
1825
- { python: "3.7", env: DJANGO_VERSION=2.1 }
1926
- { python: "3.7", env: DJANGO_VERSION=2.2 }
27+
before_install:
28+
- sudo -u postgres psql -c "CREATE USER testuser WITH PASSWORD 'password'"
29+
- sudo -u postgres psql -c "ALTER ROLE testuser SUPERUSER"
2030

2131
install:
2232
- pip install coverage
2333
- pip install coveralls
2434
- pip install -q Django==$DJANGO_VERSION
35+
- pip install psycopg2
2536

37+
before_script:
38+
- psql -U postgres -c "create extension postgis"
2639
script:
2740
- coverage run --source=mapbox_location_field manage.py test
2841
after_success:

README.md

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
* [Instalation](#instalation)
1515
* [Configuration](#configuration)
1616
* [Usage](#usage)
17+
* [PLAIN (non-spatial) db](#plain-database)
18+
* [SPATIAL db](#spatial-database)
1719
* [Customization](#customization)
1820
* [map_attrs](#map_attrs)
1921
* [bootstrap](#bootstrap)
@@ -35,7 +37,8 @@ PS. Django 1.11 does not support Python 3.7 anymore.
3537

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

6063
# Usage
61-
* Just create some model with LocationField.
62-
```python
63-
from django.db import models
64-
from mapbox_location_field.models import LocationField
64+
* ### PLAIN DATABASE
65+
* Just create some model with LocationField.
66+
```python
67+
from django.db import models
68+
from mapbox_location_field.models import LocationField
6569

66-
class SomeLocationModel(models.Model):
67-
location = LocationField()
70+
class SomeLocationModel(models.Model):
71+
location = LocationField()
6872

69-
```
70-
* Create ModelForm
71-
```python
72-
from django import forms
73-
from .models import Location
73+
```
74+
* ### SPATIAL DATABASE
75+
* Just create some model with SpatialLocationField.
76+
```python
77+
from django.db import models
78+
from mapbox_location_field.models import SpatialLocationField
7479

75-
class LocationForm(forms.ModelForm):
76-
class Meta:
77-
model = Location
78-
fields = "__all__"
79-
```
80-
Of course you can also use CreateView, UpdateView or build Form yourself with mapbox_location_field.forms.LocationField
80+
class SomeLocationModel(models.Model):
81+
location = SpatialLocationField()
8182

83+
```
8284

85+
* Create ModelForm
86+
```python
87+
from django import forms
88+
from .models import Location
89+
90+
class LocationForm(forms.ModelForm):
91+
class Meta:
92+
model = Location
93+
fields = "__all__"
94+
```
95+
Of course you can also use CreateView, UpdateView or build Form yourself with `mapbox_location_field.forms.LocationField` or `mapbox_location_field.forms.SpatialLocationField`
8396
* Then just use it in html view. It can't be simpler!
8497
Paste this in your html head:
85-
```django
86-
{% load mapbox_location_field_tags %}
87-
{% location_field_includes %}
88-
{% include_jquery %}
89-
```
98+
```django
99+
{% load mapbox_location_field_tags %}
100+
{% location_field_includes %}
101+
{% include_jquery %}
102+
```
90103
* And this in your body:
91-
```django
92-
<form method="post">
93-
{% csrf_token %}
94-
{{form}}
95-
<input type="submit" value="submit">
96-
</form>
97-
{{ form.media }}
98-
```
104+
```django
105+
<form method="post">
106+
{% csrf_token %}
107+
{{form}}
108+
<input type="submit" value="submit">
109+
</form>
110+
{{ form.media }}
111+
```
99112
* Your form is ready! Start your website and see how it looks. If you want to change something look to the [customization](#customization) section.
100113

101114
# Customization

mapbox_location_field/admin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
from django.contrib import admin
22
from django.forms import Media
33

4+
from .models import LocationField, AddressAutoHiddenField, SpatialLocationField
45
from .widgets import MapAdminInput, AddressAutoHiddenInput
5-
from .models import LocationField, AddressAutoHiddenField
66

77

88
class MapAdmin(admin.ModelAdmin):
99
"""custom ModelAdmin for LocationField and AddressAutoHiddenField"""
1010
change_form_template = "mapbox_location_field/admin_change.html"
1111
formfield_overrides = {
1212
LocationField: {'widget': MapAdminInput},
13+
SpatialLocationField: {'widget': MapAdminInput},
1314
AddressAutoHiddenField: {"widget": AddressAutoHiddenInput, }
1415

1516
}

mapbox_location_field/forms.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,34 @@
11
from django import forms
2+
from django.contrib.gis.forms import PointField
3+
from django.contrib.gis.geos import Point
4+
from django.core.exceptions import ValidationError
5+
from django.utils.translation import ugettext_lazy as _
26

37
from .widgets import MapInput, AddressAutoHiddenInput
48

59

10+
def parse_location(location_string):
11+
"""parse and convert coordinates from string to tuple"""
12+
13+
args = location_string.split(",")
14+
if len(args) != 2:
15+
raise ValidationError(_("Invalid input for a Location instance"))
16+
17+
lat = args[0]
18+
lng = args[1]
19+
20+
try:
21+
lat = float(lat)
22+
except ValueError:
23+
raise ValidationError(_("Invalid input for a Location instance. Latitude must be convertible to float "))
24+
try:
25+
lng = float(lng)
26+
except ValueError:
27+
raise ValidationError(_("Invalid input for a Location instance. Longitude must be convertible to float "))
28+
29+
return lat, lng
30+
31+
632
class LocationField(forms.CharField):
733
"""custom form field for picking location"""
834

@@ -21,3 +47,30 @@ class AddressAutoHiddenField(forms.CharField):
2147
def __init__(self, **kwargs):
2248
super().__init__(**kwargs)
2349
self.label = ""
50+
51+
52+
class SpatialLocationField(PointField):
53+
"""custom form field for picking location for spatial databases"""
54+
55+
def __init__(self, *args, **kwargs):
56+
map_attrs = kwargs.pop("map_attrs", None)
57+
self.widget = MapInput(map_attrs=map_attrs, )
58+
59+
super().__init__(*args, **kwargs)
60+
self.error_messages = {"required": "Please pick a location, it's required", }
61+
62+
def clean(self, value):
63+
try:
64+
return Point(parse_location(value), srid=4326)
65+
except (ValueError, ValidationError):
66+
return None
67+
68+
def to_python(self, value):
69+
"""Transform the value to a Geometry object."""
70+
if value in self.empty_values:
71+
return None
72+
73+
if isinstance(value, Point):
74+
return value
75+
76+
return Point(parse_location(value), srid=4326)

mapbox_location_field/models.py

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,10 @@
1-
from django.core.exceptions import ValidationError
1+
from django.contrib.gis.db.models import PointField
22
from django.db import models
33
from django.utils.translation import ugettext_lazy as _
44

5-
from .forms import AddressAutoHiddenField as AddressAutoHiddenFormField
5+
from .forms import AddressAutoHiddenField as AddressAutoHiddenFormField, parse_location
66
from .forms import LocationField as LocationFormField
7-
8-
9-
def parse_location(location_string):
10-
"""parse and convert coordinates from string to tuple"""
11-
args = location_string.split(",")
12-
if len(args) != 2:
13-
raise ValidationError(_("Invalid input for a Location instance"))
14-
15-
lat = args[0]
16-
lng = args[1]
17-
18-
try:
19-
lat = float(lat)
20-
except ValueError:
21-
raise ValidationError(_("Invalid input for a Location instance. Latitude must be convertible to float "))
22-
try:
23-
lng = float(lng)
24-
except ValueError:
25-
raise ValidationError(_("Invalid input for a Location instance. Longitude must be convertible to float "))
26-
27-
return lat, lng
7+
from .forms import SpatialLocationField as SpatialLocationFormField
288

299

3010
class LocationField(models.CharField):
@@ -82,3 +62,24 @@ def formfield(self, **kwargs):
8262
defaults = {'form_class': AddressAutoHiddenFormField}
8363
defaults.update(kwargs)
8464
return models.Field.formfield(self, **defaults)
65+
66+
67+
class SpatialLocationField(PointField):
68+
"""custom model field for storing location in spatial databases"""
69+
70+
description = _("Location field for spatial databases, stores Points.")
71+
72+
def __init__(self, *args, **kwargs):
73+
self.map_attrs = kwargs.pop("map_attrs", {})
74+
super().__init__(*args, **kwargs)
75+
76+
def deconstruct(self):
77+
name, path, args, kwargs = super().deconstruct()
78+
kwargs["map_attrs"] = self.map_attrs
79+
return name, path, args, kwargs
80+
81+
def formfield(self, **kwargs):
82+
defaults = {'form_class': SpatialLocationFormField}
83+
defaults.update(kwargs)
84+
defaults.update({"map_attrs": self.map_attrs})
85+
return super().formfield(**defaults)

mapbox_location_field/tests/test_forms.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
from django.contrib.gis.geos import Point
12
from django.test import TestCase
23

3-
from mapbox_location_field.forms import LocationField, AddressAutoHiddenField
4+
from mapbox_location_field.forms import LocationField, AddressAutoHiddenField, SpatialLocationField
45
from mapbox_location_field.widgets import MapInput, AddressAutoHiddenInput
56

67

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

2122

23+
class SpatialLocationFieldTests(TestCase):
24+
25+
def test_widget(self):
26+
field = SpatialLocationField()
27+
self.assertEqual(field.widget.__class__, MapInput().__class__)
28+
29+
def test_error_messages(self):
30+
field = SpatialLocationField()
31+
self.assertEqual(field.error_messages["required"], "Please pick a location, it's required")
32+
33+
def test_passing_map_attrs(self):
34+
field = SpatialLocationField(map_attrs={"some": "value", "and some": "cool value"})
35+
self.assertEqual(field.widget.map_attrs, {"some": "value", "and some": "cool value"})
36+
37+
def test_clean(self):
38+
field = SpatialLocationField()
39+
point = field.clean("12,13")
40+
41+
self.assertIsInstance(point, Point)
42+
self.assertEqual(point.x, 12)
43+
self.assertEqual(point.y, 13)
44+
45+
point = field.clean("12")
46+
self.assertIsNone(point)
47+
48+
def test_to_python(self):
49+
field = SpatialLocationField()
50+
51+
for empty in field.empty_values:
52+
self.assertIsNone(field.to_python(empty))
53+
54+
point = Point(12, 13)
55+
self.assertIs(point, field.to_python(point))
56+
57+
point = field.to_python("12,13")
58+
self.assertIsInstance(point, Point)
59+
self.assertEqual(point.x, 12)
60+
self.assertEqual(point.y, 13)
61+
62+
2263
class AddressAutoHiddenFieldTests(TestCase):
2364
def test_widget(self):
2465
field = AddressAutoHiddenField()

mapbox_location_field/tests/test_models.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
from django.test import TestCase
21
from django.core.exceptions import ValidationError
2+
from django.test import TestCase
33

4-
from mapbox_location_field.models import parse_location, LocationField, AddressAutoHiddenField
5-
from mapbox_location_field.forms import LocationField as FormLocationField
64
from mapbox_location_field.forms import AddressAutoHiddenField as FormAddressAutoHiddenField
5+
from mapbox_location_field.forms import LocationField as FormLocationField
6+
from mapbox_location_field.forms import SpatialLocationField as FormSpatialLocationField
7+
from mapbox_location_field.models import parse_location, LocationField, AddressAutoHiddenField, SpatialLocationField
78

89

910
class LocationFieldTests(TestCase):
@@ -47,10 +48,24 @@ def test_get_prep_value(self):
4748

4849
def test_form_field(self):
4950
instance = LocationField()
50-
self.assertEqual(instance.formfield().__class__, FormLocationField().__class__)
51+
self.assertTrue(isinstance(instance.formfield(), FormLocationField))
52+
53+
54+
class SpatialLocationFieldTests(TestCase):
55+
56+
def test_SpatialLocationField(self):
57+
instance = SpatialLocationField()
58+
self.assertIsInstance(instance, SpatialLocationField)
59+
name, path, args, kwargs = instance.deconstruct()
60+
new_instance = SpatialLocationField(*args, **kwargs)
61+
self.assertEqual(instance.map_attrs, new_instance.map_attrs)
62+
63+
def test_form_field(self):
64+
instance = SpatialLocationField()
65+
self.assertTrue(isinstance(instance.formfield(), FormSpatialLocationField))
5166

5267

5368
class AddressAutoHiddenFieldTests(TestCase):
5469
def test_form_field(self):
5570
instance = AddressAutoHiddenField()
56-
self.assertEqual(instance.formfield().__class__, FormAddressAutoHiddenField().__class__)
71+
self.assertTrue(isinstance(instance.formfield(), FormAddressAutoHiddenField))

mapbox_location_field/tests/test_widgets.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,11 @@ def test_parse_tuple_string(self):
164164
self.assertEqual(parse_tuple_string("(123456,155413.452)"), (123456, 155413.452))
165165
self.assertEqual(parse_tuple_string("(123.456,155413.452)"), (123.456, 155413.452))
166166

167+
self.assertEqual(parse_tuple_string("SRID=4376POINT (123456 155413)"), (123456, 155413))
168+
self.assertEqual(parse_tuple_string("SRID=4376POINT (123456.864534 155413452)"), (123456.864534, 155413452))
169+
self.assertEqual(parse_tuple_string("SRID=4376POINT (123456 155413.452)"), (123456, 155413.452))
170+
self.assertEqual(parse_tuple_string("SRID=4376POINT (123.456 155413.452)"), (123.456, 155413.452))
171+
167172
def test_setting_center_point(self):
168173
widget = MapInput()
169174
widget.get_context("name", (1234.3, 2352145.6), {})

0 commit comments

Comments
 (0)