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         |  24 +++++
 combo/apps/maps/migrations/__init__.py             |   0
 combo/apps/maps/models.py                          |  69 +++++++++++++
 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                                  |   2 +
 debian/control                                     |   1 +
 requirements.txt                                   |   1 +
 setup.py                                           |   1 +
 tests/test_maps_manager.py                         | 115 +++++++++++++++++++++
 16 files changed, 438 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) 2015  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 Layers')}]
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
        fields = '__all__'
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
                ('geojson_url', models.URLField(verbose_name='Geojson URL')),
19
                ('marker_colour', models.CharField(default=b'#0000FF', max_length=7, verbose_name='Marker color')),
20
                ('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')])),
21
                ('icon_colour', models.CharField(default=b'#FFFFFF', max_length=7, verbose_name='Icon colour')),
22
            ],
23
        ),
24
    ]
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.translation import ugettext_lazy as _
20

  
21
icons = [
22
    ('fa-home', _('home')),
23
    ('fa-building', _('building')),
24
    ('fa-hospital-o', _('hospital')),
25
    ('fa-ambulance', _('ambulance')),
26
    ('fa-taxi', _('taxi')),
27
    ('fa-subway', _('subway')),
28
    ('fa-wheelchair', _('wheelchair')),
29
    ('fa-bicycle', _('bicycle')),
30
    ('fa-car', _('car')),
31
    ('fa-train', _('train')),
32
    ('fa-bus', _('bus')),
33
    ('fa-motorcycle', _('motorcycle')),
34
    ('fa-truck', _('truck')),
35
]
36

  
37
from combo.utils import requests
38

  
39

  
40
class MapLayer(models.Model):
41
    label = models.CharField(_('Label'), max_length=128)
42
    geojson_url = models.URLField(_('Geojson URL'))
43
    marker_colour = models.CharField(_('Marker color'), max_length=7, default='#0000FF')
44
    icon = models.CharField(_('Marker icon'), max_length=32, blank=True, null=True,
45
                            choices=icons)
46
    icon_colour = models.CharField(_('Icon colour'), max_length=7, default='#FFFFFF')
47

  
48
    def __unicode__(self):
49
        return self.label
50

  
51
    def get_geojson(self):
52
        response = requests.get(self.geojson_url,
53
                            remote_service='auto',
54
                            headers={'accept': 'application/json'})
55
        if not response.ok:
56
            return []
57
        data = response.json()
58
        if 'features' in data:
59
            features = data['features']
60
        else:
61
            features = data
62

  
63
        for feature in features:
64
            feature['display_fields'] = feature['properties'].copy()
65
            feature['properties']['colour'] = self.marker_colour
66
            feature['properties']['icon_colour'] = self.icon_colour
67
            feature['properties']['label'] = self.label
68
            feature['properties']['icon'] = self.icon
69
        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 Layers' %}</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 Layers' %}</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' pk=layer.id %}">{{ 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 Layer" %}</h2>
7
{% else %}
8
<h2>{% trans "New Layer" %}</h2>
9
{% endif %}
10
{% endblock %}
11

  
12
{% block breadcrumb %}
13
{{ block.super }}
14
<a href="{% url 'maps-manager-homepage' %}">{% trans 'Layers' %}</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' pk=object.id %}">{% 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<pk>\w+)/edit/$', LayerEditView.as_view(),
28
        name='maps-manager-layer-edit'),
29
    url('^layers/(?P<pk>\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',
83
    'xstatic.pkg.font_awesome',
82 84
)
83 85

  
84 86
INSTALLED_APPS = plugins.register_plugins_apps(INSTALLED_APPS)
debian/control
16 16
    python-feedparser,
17 17
    python-django-cmsplugin-blurp,
18 18
    python-xstatic-chartnew-js,
19
    python-xstatic-font-awesome,
19 20
    python-eopayment (>= 1.9),
20 21
    python-django-haystack (>= 2.4.0),
21 22
    python-sorl-thumbnail,
requirements.txt
6 6
django-jsonfield
7 7
requests
8 8
XStatic-ChartNew.js
9
XStatic-Font-Awesome
9 10
eopayment>=1.13
10 11
python-dateutil
11 12
djangorestframework>=3.3, <3.4
setup.py
111 111
        'django-jsonfield',
112 112
        'requests',
113 113
        'XStatic-ChartNew.js',
114
        'XStatic-Font-Awesome',
114 115
        'eopayment>=1.13',
115 116
        'python-dateutil',
116 117
        'djangorestframework>=3.3, <3.4',
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'] = 'fa-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

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

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

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

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