Projet

Général

Profil

0001-maps-add-layers-8454.patch

Voir les différences:

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

 combo/apps/maps/__init__.py                        |  34 ++++++
 combo/apps/maps/forms.py                           |  26 +++++
 combo/apps/maps/manager_views.py                   |  43 ++++++++
 combo/apps/maps/migrations/0001_initial.py         |  24 +++++
 combo/apps/maps/migrations/__init__.py             |   0
 combo/apps/maps/models.py                          |  53 ++++++++++
 combo/apps/maps/templates/maps/manager_base.html   |  11 ++
 combo/apps/maps/templates/maps/manager_home.html   |  26 +++++
 .../templates/maps/maplayer_confirm_delete.html    |  17 +++
 combo/apps/maps/templates/maps/maplayer_form.html  |  30 ++++++
 combo/apps/maps/urls.py                            |  38 +++++++
 combo/manager/static/css/combo.manager.css         |  32 ++++++
 combo/manager/static/js/combo.manager.js           |   6 ++
 combo/settings.py                                  |   2 +
 debian/control                                     |   1 +
 requirements.txt                                   |   1 +
 setup.py                                           |   1 +
 tests/test_maps_manager.py                         | 115 +++++++++++++++++++++
 18 files changed, 460 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/maplayer_confirm_delete.html
 create mode 100644 combo/apps/maps/templates/maps/maplayer_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

  
37

  
38
class LayerEditView(MapLayerMixin, UpdateView):
39
    form_class = MapLayerForm
40

  
41

  
42
class LayerDeleteView(MapLayerMixin, DeleteView):
43
    pass
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(help_text='FontAwesome style name', max_length=32, null=True, verbose_name='Marker icon', blank=True)),
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
from combo.utils import requests
22

  
23

  
24
class MapLayer(models.Model):
25
    label = models.CharField(_('Label'), max_length=128)
26
    geojson_url = models.URLField(_('Geojson URL'))
27
    marker_colour = models.CharField(_('Marker color'), max_length=7, default='#0000FF')
28
    icon = models.CharField(_('Marker icon'), max_length=32, blank=True, null=True,
29
                            help_text=_('FontAwesome style name'))
30
    icon_colour = models.CharField(_('Icon colour'), max_length=7, default='#FFFFFF')
31

  
32
    def __unicode__(self):
33
        return self.label
34

  
35
    def get_geojson(self):
36
        response = requests.get(self.geojson_url,
37
                            remote_service='auto',
38
                            headers={'accept': 'application/json'})
39
        if not response.ok:
40
            return []
41
        data = response.json()
42
        if 'features' in data:
43
            features = data['features']
44
        else:
45
            features = data
46

  
47
        for feature in features:
48
                feature['display_fields'] = feature['properties'].copy()
49
                feature['properties']['colour'] = self.marker_colour
50
                feature['properties']['icon_colour'] = self.icon_colour
51
                feature['properties']['label'] = self.label
52
                feature['properties']['icon'] = self.icon
53
        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/maplayer_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/maplayer_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+)/edit/$', LayerEditView.as_view(),
30
        name='maps-manager-layer-edit'),
31
    url('^layers/(?P<pk>\w+)/delete/$', LayerDeleteView.as_view(),
32
        name='maps-manager-layer-delete'),
33
]
34

  
35
urlpatterns = [
36
    url(r'^manage/maps/', decorated_includes(manager_required,
37
        include(maps_manager_urls))),
38
]
combo/manager/static/css/combo.manager.css
305 305
img.page-picture {
306 306
	max-width: 95%;
307 307
}
308

  
309
/* colour picker styles */
310

  
311
#jquery-colour-picker {
312
        background: #fafafa;
313
        width: 250px;
314
        padding: 10px 5px;
315
        border-radius: 5px;
316
        box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.3);
317
        z-index: 2000;
318
}
319

  
320
#jquery-colour-picker ul {
321
        margin: 0;
322
        padding: 0;
323
        list-style-type: none;
324
}
325

  
326
#jquery-colour-picker li {
327
        float: left;
328
        margin: 0 5px 5px 0;
329
}
330

  
331
#jquery-colour-picker li a {
332
        display: block;
333
        width: 13px;
334
        height: 13px;
335
        text-decoration: none;
336
        text-indent: -10000px;
337
        outline: 0;
338
        border: 1px solid #aaa;
339
}
combo/manager/static/js/combo.manager.js
223 223
    window.location = $(this).parent('div').find('option:selected').data('add-url');
224 224
    return false;
225 225
  });
226

  
227
  $(document).on('gadjo:dialog-loaded', function(e, dialog) {
228
      if (jQuery.fn.colourPicker !== undefined) {
229
          jQuery('select.colour-picker').colourPicker({title: ''});
230
      }
231
  });
226 232
});
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
-