Projet

Général

Profil

0001-maps-add-layers-8454.patch

Voir les différences:

Subject: [PATCH 1/2] maps: add layers (#8454)

 combo/apps/maps/__init__.py                        |  34 ++++++
 combo/apps/maps/forms.py                           |  26 +++++
 combo/apps/maps/manager_views.py                   |  45 ++++++++
 combo/apps/maps/migrations/0001_initial.py         |  25 +++++
 combo/apps/maps/migrations/__init__.py             |   0
 combo/apps/maps/models.py                          |  93 ++++++++++++++++
 combo/apps/maps/templates/maps/manager_base.html   |  11 ++
 combo/apps/maps/templates/maps/manager_home.html   |  26 +++++
 .../templates/maps/map_layer_confirm_delete.html   |  17 +++
 combo/apps/maps/templates/maps/map_layer_form.html |  30 ++++++
 combo/apps/maps/urls.py                            |  36 +++++++
 combo/settings.py                                  |   1 +
 tests/test_maps_manager.py                         | 117 +++++++++++++++++++++
 13 files changed, 461 insertions(+)
 create mode 100644 combo/apps/maps/__init__.py
 create mode 100644 combo/apps/maps/forms.py
 create mode 100644 combo/apps/maps/manager_views.py
 create mode 100644 combo/apps/maps/migrations/0001_initial.py
 create mode 100644 combo/apps/maps/migrations/__init__.py
 create mode 100644 combo/apps/maps/models.py
 create mode 100644 combo/apps/maps/templates/maps/manager_base.html
 create mode 100644 combo/apps/maps/templates/maps/manager_home.html
 create mode 100644 combo/apps/maps/templates/maps/map_layer_confirm_delete.html
 create mode 100644 combo/apps/maps/templates/maps/map_layer_form.html
 create mode 100644 combo/apps/maps/urls.py
 create mode 100644 tests/test_maps_manager.py
combo/apps/maps/__init__.py
1
# combo - content management system
2
# Copyright (C) 2017  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import django.apps
18
from django.core.urlresolvers import reverse
19
from django.utils.translation import ugettext_lazy as _
20

  
21

  
22
class AppConfig(django.apps.AppConfig):
23
    name = 'combo.apps.maps'
24
    verbose_name = _('Maps')
25

  
26
    def get_before_urls(self):
27
        from . import urls
28
        return urls.urlpatterns
29

  
30
    def get_extra_manager_actions(self):
31
        return [{'href': reverse('maps-manager-homepage'),
32
                'text': _('Maps')}]
33

  
34
default_app_config = 'combo.apps.maps.AppConfig'
combo/apps/maps/forms.py
1
# combo - content management system
2
# Copyright (C) 2017  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django import forms
18
from .models import MapLayer
19

  
20
class MapLayerForm(forms.ModelForm):
21
    class Meta:
22
        model = MapLayer
23
        exclude = ('slug', )
24
        widgets = {'marker_colour': forms.TextInput(attrs={'type': 'color'}),
25
                   'icon_colour': forms.TextInput(attrs={'type': 'color'})
26
                   }
combo/apps/maps/manager_views.py
1
# combo - content management system
2
# Copyright (C) 2017  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.core.urlresolvers import reverse_lazy
18
from django.views.generic import (TemplateView, ListView, CreateView,
19
                UpdateView, DeleteView)
20

  
21
from .models import MapLayer
22
from .forms import MapLayerForm
23

  
24

  
25
class MapLayerMixin(object):
26
    model = MapLayer
27
    success_url = reverse_lazy('maps-manager-homepage')
28

  
29

  
30
class ManagerHomeView(MapLayerMixin, ListView):
31
    template_name = 'maps/manager_home.html'
32

  
33

  
34
class LayerAddView(MapLayerMixin, CreateView):
35
    form_class = MapLayerForm
36
    template_name = 'maps/map_layer_form.html'
37

  
38

  
39
class LayerEditView(MapLayerMixin, UpdateView):
40
    form_class = MapLayerForm
41
    template_name = 'maps/map_layer_form.html'
42

  
43

  
44
class LayerDeleteView(MapLayerMixin, DeleteView):
45
    template_name = 'maps/map_layer_confirm_delete.html'
combo/apps/maps/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import migrations, models
5

  
6

  
7
class Migration(migrations.Migration):
8

  
9
    dependencies = [
10
    ]
11

  
12
    operations = [
13
        migrations.CreateModel(
14
            name='MapLayer',
15
            fields=[
16
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
17
                ('label', models.CharField(max_length=128, verbose_name='Label')),
18
                ('slug', models.SlugField(verbose_name='Slug')),
19
                ('geojson_url', models.URLField(max_length=1024, verbose_name='Geojson URL')),
20
                ('marker_colour', models.CharField(default=b'#0000FF', max_length=7, verbose_name='Marker colour')),
21
                ('icon', models.CharField(blank=True, max_length=32, null=True, verbose_name='Marker icon', choices=[(b'fa-home', 'home'), (b'fa-building', 'building'), (b'fa-hospital-o', 'hospital'), (b'fa-ambulance', 'ambulance'), (b'fa-taxi', 'taxi'), (b'fa-subway', 'subway'), (b'fa-wheelchair', 'wheelchair'), (b'fa-bicycle', 'bicycle'), (b'fa-car', 'car'), (b'fa-train', 'train'), (b'fa-bus', 'bus'), (b'fa-motorcycle', 'motorcycle'), (b'fa-truck', 'truck')])),
22
                ('icon_colour', models.CharField(default=b'#FFFFFF', max_length=7, verbose_name='Icon colour')),
23
            ],
24
        ),
25
    ]
combo/apps/maps/models.py
1
# combo - content management system
2
# Copyright (C) 2017  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17

  
18
from django.db import models
19
from django.utils.text import slugify
20
from django.utils.translation import ugettext_lazy as _
21

  
22
from combo.utils import requests
23

  
24

  
25
ICONS = [
26
    ('home', _('Home')),
27
    ('building', _('Building')),
28
    ('hospital', _('Hospital')),
29
    ('ambulance', _('Ambulance')),
30
    ('taxi', _('Taxi')),
31
    ('subway', _('Subway')),
32
    ('wheelchair', _('Wheelchair')),
33
    ('bicycle', _('Bicycle')),
34
    ('car', _('Car')),
35
    ('train', _('Train')),
36
    ('bus', _('Bus')),
37
    ('motorcycle', _('Motorcycle')),
38
    ('truck', _('Truck')),
39
]
40

  
41

  
42
class MapLayer(models.Model):
43
    label = models.CharField(_('Label'), max_length=128)
44
    slug = models.SlugField(_('Slug'))
45
    geojson_url = models.URLField(_('Geojson URL'), max_length=1024)
46
    marker_colour = models.CharField(_('Marker colour'), max_length=7, default='#0000FF')
47
    icon = models.CharField(_('Marker icon'), max_length=32, blank=True, null=True,
48
                            choices=ICONS)
49
    icon_colour = models.CharField(_('Icon colour'), max_length=7, default='#FFFFFF')
50

  
51
    def save(self, *args, **kwargs):
52
        if not self.slug:
53
            base_slug = slugify(self.label)
54
            slug = base_slug
55
            i = 1
56
            while True:
57
                try:
58
                    MapLayer.objects.get(slug=slug)
59
                except self.DoesNotExist:
60
                    break
61
                slug = '%s-%s' % (base_slug, i)
62
                i += 1
63
            self.slug = slug
64
        super(MapLayer, self).save(*args, **kwargs)
65

  
66
    def __unicode__(self):
67
        return self.label
68

  
69
    def get_geojson(self, request):
70
        response = requests.get(self.geojson_url,
71
                            remote_service='auto',
72
                            user=request.user,
73
                            headers={'accept': 'application/json'})
74
        if not response.ok:
75
            return []
76
        data = response.json()
77
        if 'features' in data:
78
            features = data['features']
79
        else:
80
            features = data
81

  
82
        for feature in features:
83
            if 'display_fields' not in feature['properties']:
84
                display_fields = []
85
                for label, value in feature['properties'].iteritems():
86
                    if value is not None:
87
                        display_fields.append((label, value))
88
                feature['properties']['display_fields'] = display_fields
89
            feature['properties']['colour'] = self.marker_colour
90
            feature['properties']['icon_colour'] = self.icon_colour
91
            feature['properties']['label'] = self.label
92
            feature['properties']['icon'] = self.icon
93
        return features
combo/apps/maps/templates/maps/manager_base.html
1
{% extends "combo/manager_base.html" %}
2
{% load i18n %}
3

  
4
{% block appbar %}
5
<h2>{% trans 'Maps' %}</h2>
6
{% endblock %}
7

  
8
{% block breadcrumb %}
9
{{ block.super }}
10
<a href="{% url 'maps-manager-homepage' %}">{% trans 'Maps' %}</a>
11
{% endblock %}
combo/apps/maps/templates/maps/manager_home.html
1
{% extends "maps/manager_base.html" %}
2
{% load i18n %}
3

  
4
{% block appbar %}
5
<h2>{% trans 'Maps' %}</h2>
6
<a rel="popup" href="{% url 'maps-manager-layer-add' %}">{% trans 'New' %}</a>
7
{% endblock %}
8

  
9
{% block content %}
10
{% if object_list %}
11
<div class="objects-list">
12
 {% for layer in object_list %}
13
 <div>
14
 <a href="{% url 'maps-manager-layer-edit' slug=layer.slug %}">{{ layer.label }}</a>
15
 </div>
16
 {% endfor %}
17
</div>
18
{% else %}
19
<div class="big-msg-info">
20
  {% blocktrans %}
21
  This site doesn't have any layer yet. Click on the "New" button in the top
22
  right of the page to add a first one.
23
  {% endblocktrans %}
24
</div>
25
{% endif %}
26
{% endblock %}
combo/apps/maps/templates/maps/map_layer_confirm_delete.html
1
{% extends "combo/manager_base.html" %}
2
{% load i18n %}
3

  
4
{% block appbar %}
5
<h2>{{ view.model.get_verbose_name }}</h2>
6
{% endblock %}
7

  
8
{% block content %}
9
<form method="post">
10
  {% csrf_token %}
11
  {% blocktrans %}Are you sure you want to delete this?{% endblocktrans %}
12
  <div class="buttons">
13
    <button class="delete-button">{% trans 'Delete' %}</button>
14
    <a class="cancel" href="{% url 'maps-manager-homepage' %}">{% trans 'Cancel' %}</a>
15
  </div>
16
</form>
17
{% endblock %}
combo/apps/maps/templates/maps/map_layer_form.html
1
{% extends "maps/manager_home.html" %}
2
{% load i18n %}
3

  
4
{% block appbar %}
5
{% if object.id %}
6
<h2>{% trans "Edit Map Layer" %}</h2>
7
{% else %}
8
<h2>{% trans "New Map Layer" %}</h2>
9
{% endif %}
10
{% endblock %}
11

  
12
{% block breadcrumb %}
13
{{ block.super }}
14
<a href="{% url 'maps-manager-homepage' %}">{% trans 'Maps' %}</a>
15
{% endblock %}
16

  
17
{% block content %}
18

  
19
<form method="post" enctype="multipart/form-data">
20
  {% csrf_token %}
21
  {{ form.as_p }}
22
  <div class="buttons">
23
    <button class="submit-button">{% trans "Save" %}</button>
24
    <a class="cancel" href="{% url 'maps-manager-homepage' %}">{% trans 'Cancel' %}</a>
25
    {% if object.id %}
26
    <a class="delete" rel="popup" href="{% url 'maps-manager-layer-delete' slug=object.slug %}">{% trans 'Delete' %}</a>
27
    {% endif %}
28
  </div>
29
</form>
30
{% endblock %}
combo/apps/maps/urls.py
1
# combo - content management system
2
# Copyright (C) 2017  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.conf.urls import url, include
18

  
19
from combo.urls_utils import decorated_includes, manager_required
20

  
21
from .manager_views import (ManagerHomeView, LayerAddView,
22
                LayerEditView, LayerDeleteView)
23

  
24
maps_manager_urls = [
25
    url('^$', ManagerHomeView.as_view(), name='maps-manager-homepage'),
26
    url('^layers/add/$', LayerAddView.as_view(), name='maps-manager-layer-add'),
27
    url('^layers/(?P<slug>[\w-]+)/edit/$', LayerEditView.as_view(),
28
        name='maps-manager-layer-edit'),
29
    url('^layers/(?P<slug>[\w-]+)/delete/$', LayerDeleteView.as_view(),
30
        name='maps-manager-layer-delete'),
31
]
32

  
33
urlpatterns = [
34
    url(r'^manage/maps/', decorated_includes(manager_required,
35
        include(maps_manager_urls))),
36
]
combo/settings.py
77 77
    'combo.apps.notifications',
78 78
    'combo.apps.search',
79 79
    'combo.apps.usersearch',
80
    'combo.apps.maps',
80 81
    'haystack',
81 82
    'xstatic.pkg.chartnew_js',
82 83
)
tests/test_maps_manager.py
1
# -*- coding: utf-8 -*-
2

  
3
import pytest
4
import mock
5

  
6
from django.contrib.auth.models import User
7

  
8
from combo.apps.maps.models import MapLayer
9

  
10
pytestmark = pytest.mark.django_db
11

  
12
@pytest.fixture
13
def admin_user():
14
    try:
15
        user = User.objects.get(username='admin')
16
    except User.DoesNotExist:
17
        user = User.objects.create_superuser('admin', email=None, password='admin')
18
    return user
19

  
20
def login(app, username='admin', password='admin'):
21
    login_page = app.get('/login/')
22
    login_form = login_page.forms[0]
23
    login_form['username'] = username
24
    login_form['password'] = password
25
    resp = login_form.submit()
26
    assert resp.status_int == 302
27
    return app
28

  
29
def test_access(app, admin_user):
30
    app = login(app)
31
    resp = app.get('/manage/', status=200)
32
    assert '/manage/maps/' in resp.body
33

  
34
def test_add_layer(app, admin_user):
35
    MapLayer.objects.all().delete()
36
    app = login(app)
37
    resp = app.get('/manage/maps/', status=200)
38
    resp = resp.click('New')
39
    resp.forms[0]['label'] = 'Test'
40
    resp.forms[0]['geojson_url'] = 'http://example.net/geojson'
41
    assert resp.form['marker_colour'].value == '#0000FF'
42
    resp.forms[0]['marker_colour'] = '#FFFFFF'
43
    resp.forms[0]['icon'] = 'bicycle'
44
    assert resp.form['icon_colour'].value == '#FFFFFF'
45
    resp.form['icon_colour'] = '#000000'
46
    resp = resp.forms[0].submit()
47
    assert resp.location == 'http://testserver/manage/maps/'
48
    assert MapLayer.objects.count() == 1
49
    layer = MapLayer.objects.get()
50
    assert layer.label == 'Test'
51
    assert layer.slug == 'test'
52

  
53
def test_edit_layer(app, admin_user):
54
    test_add_layer(app, admin_user)
55
    app = login(app)
56
    resp = app.get('/manage/maps/', status=200)
57
    resp = resp.click('Test')
58
    resp.forms[0]['geojson_url'] = 'http://example.net/new_geojson'
59
    resp = resp.forms[0].submit()
60
    assert resp.location == 'http://testserver/manage/maps/'
61
    assert MapLayer.objects.count() == 1
62
    layer = MapLayer.objects.get()
63
    assert layer.geojson_url == 'http://example.net/new_geojson'
64

  
65
def test_delete_layer(app, admin_user):
66
    test_add_layer(app, admin_user)
67
    app = login(app)
68
    resp = app.get('/manage/maps/', status=200)
69
    resp = resp.click('Test')
70
    resp = resp.click('Delete')
71
    assert 'Are you sure you want to delete this?' in resp.body
72
    resp = resp.forms[0].submit()
73
    assert resp.location == 'http://testserver/manage/maps/'
74
    assert MapLayer.objects.count() == 0
75

  
76
@mock.patch('combo.apps.maps.models.requests.get')
77
def test_download_geojson(mock_request, app, admin_user):
78
    test_add_layer(app, admin_user)
79
    layer = MapLayer.objects.get()
80
    mocked_response = mock.Mock()
81
    mocked_response.json.return_value = [{'type': 'Feature',
82
            'geometry': {'type': 'Point',
83
                        'coordinates': [2.3233688436448574, 48.83369263315934]},
84
                        'properties': {'property': 'property value'}}]
85
    mocked_response.ok.return_value = True
86
    mock_request.return_value = mocked_response
87
    geojson = layer.get_geojson(mock_request)
88
    assert len(geojson) > 0
89
    for item in geojson:
90
        assert item['type'] == 'Feature'
91
        assert item['geometry']['type'] == 'Point'
92
        assert item['geometry']['coordinates'] == [2.3233688436448574, 48.83369263315934]
93
        assert item['properties']['display_fields'] == [('property', 'property value')]
94
        assert item['properties']['icon'] == 'bicycle'
95
        assert item['properties']['label'] == 'Test'
96
        assert item['properties']['colour'] == '#FFFFFF'
97
        assert item['properties']['icon_colour'] == '#000000'
98

  
99
    mocked_response.json.return_value = {'type': 'FeatureCollection',
100
            'features': [{'geometry': {'type': 'Point',
101
                        'coordinates': [2.3233688436448574, 48.83369263315934]},
102
                        'properties': {'property': 'a random value',
103
                                       'display_fields': [('foo', 'bar')]}}
104
            ]
105
    }
106
    mocked_response.ok.return_value = True
107
    mock_request.return_value = mocked_response
108
    geojson = layer.get_geojson(mock_request)
109
    assert len(geojson) > 0
110
    for item in geojson:
111
        assert item['geometry']['type'] == 'Point'
112
        assert item['geometry']['coordinates'] == [2.3233688436448574, 48.83369263315934]
113
        assert item['properties']['display_fields'] == [('foo', 'bar')]
114
        assert item['properties']['icon'] == 'bicycle'
115
        assert item['properties']['label'] == 'Test'
116
        assert item['properties']['colour'] == '#FFFFFF'
117
        assert item['properties']['icon_colour'] == '#000000'
0
-