From d90ba7ee93c188c81befb51d65e3ba448d302b2e Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Fri, 12 May 2017 11:53:07 +0200 Subject: [PATCH 1/2] maps: add layers (#8454) --- combo/apps/maps/__init__.py | 34 ++++++++ combo/apps/maps/forms.py | 33 ++++++++ combo/apps/maps/manager_views.py | 47 +++++++++++ combo/apps/maps/migrations/0001_initial.py | 24 ++++++ combo/apps/maps/migrations/__init__.py | 0 combo/apps/maps/models.py | 48 +++++++++++ combo/apps/maps/templates/maps/manager_base.html | 11 +++ combo/apps/maps/templates/maps/manager_home.html | 8 ++ .../templates/maps/maplayer_confirm_delete.html | 17 ++++ combo/apps/maps/templates/maps/maplayer_form.html | 30 +++++++ combo/apps/maps/templates/maps/maplayer_list.html | 33 ++++++++ combo/apps/maps/urls.py | 39 +++++++++ combo/apps/maps/views.py | 17 ++++ combo/manager/static/css/combo.manager.css | 32 +++++++ combo/manager/static/js/combo.manager.js | 6 ++ combo/manager/templates/combo/manager_base.html | 1 + combo/settings.py | 3 + debian/control | 2 + requirements.txt | 2 + setup.py | 2 + tests/test_maps_manager.py | 97 ++++++++++++++++++++++ 21 files changed, 486 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/templates/maps/maplayer_list.html create mode 100644 combo/apps/maps/urls.py create mode 100644 combo/apps/maps/views.py create mode 100644 tests/test_maps_manager.py diff --git a/combo/apps/maps/__init__.py b/combo/apps/maps/__init__.py new file mode 100644 index 0000000..7c988ec --- /dev/null +++ b/combo/apps/maps/__init__.py @@ -0,0 +1,34 @@ +# combo - content management system +# Copyright (C) 2015 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import django.apps +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + + +class AppConfig(django.apps.AppConfig): + name = 'combo.apps.maps' + verbose_name = _('Maps') + + def get_before_urls(self): + from . import urls + return urls.urlpatterns + + def get_extra_manager_actions(self): + return [{'href': reverse('maps-manager-homepage'), + 'text': _('Maps')}] + +default_app_config = 'combo.apps.maps.AppConfig' diff --git a/combo/apps/maps/forms.py b/combo/apps/maps/forms.py new file mode 100644 index 0000000..39c38f2 --- /dev/null +++ b/combo/apps/maps/forms.py @@ -0,0 +1,33 @@ +# combo - content management system +# Copyright (C) 2017 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import itertools + +from django import forms + +from .models import MapLayer + +colours = [('%s%s%s' % x, '%s%s%s' % x) for x in itertools.product(('00', '66', '99', 'FF'), repeat=3)] + +class MapLayerForm(forms.ModelForm): + class Meta: + model = MapLayer + fields = '__all__' + widgets = {'marker_colour': forms.Select(attrs={'class': 'colour-picker'}, + choices=colours), + 'icon_colour': forms.Select(attrs={'class': 'colour-picker'}, + choices=colours), + } diff --git a/combo/apps/maps/manager_views.py b/combo/apps/maps/manager_views.py new file mode 100644 index 0000000..5807ced --- /dev/null +++ b/combo/apps/maps/manager_views.py @@ -0,0 +1,47 @@ +# combo - content management system +# Copyright (C) 2017 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.core.urlresolvers import reverse_lazy +from django.views.generic import (TemplateView, ListView, CreateView, + UpdateView, DeleteView) + +from .models import MapLayer +from .forms import MapLayerForm + + +class ManagerHomeView(TemplateView): + template_name = 'maps/manager_home.html' + + +class LayersManagerView(ListView): + model = MapLayer + + +class LayerAddView(CreateView): + model = MapLayer + form_class = MapLayerForm + success_url = reverse_lazy('maps-manager-layers-list') + + +class LayerEditView(UpdateView): + model = MapLayer + form_class = MapLayerForm + success_url = reverse_lazy('maps-manager-layers-list') + + +class LayerDeleteView(DeleteView): + model = MapLayer + success_url = reverse_lazy('maps-manager-layers-list') diff --git a/combo/apps/maps/migrations/0001_initial.py b/combo/apps/maps/migrations/0001_initial.py new file mode 100644 index 0000000..bcf7005 --- /dev/null +++ b/combo/apps/maps/migrations/0001_initial.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='MapLayer', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('label', models.CharField(max_length=128, verbose_name='Label')), + ('geojson_url', models.URLField(verbose_name='Geojson URL')), + ('marker_colour', models.CharField(default=b'0000FF', max_length=6, verbose_name='Marker color')), + ('icon', models.CharField(help_text='FontAwesome style name', max_length=32, null=True, verbose_name='Marker icon', blank=True)), + ('icon_colour', models.CharField(default=b'FFFFFF', max_length=6, verbose_name='Icon colour')), + ], + ), + ] diff --git a/combo/apps/maps/migrations/__init__.py b/combo/apps/maps/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/combo/apps/maps/models.py b/combo/apps/maps/models.py new file mode 100644 index 0000000..3aa3b8a --- /dev/null +++ b/combo/apps/maps/models.py @@ -0,0 +1,48 @@ +# combo - content management system +# Copyright (C) 2017 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from combo.utils import requests + + +class MapLayer(models.Model): + label = models.CharField(_('Label'), max_length=128) + geojson_url = models.URLField(_('Geojson URL')) + marker_colour = models.CharField(_('Marker color'), max_length=6, default='0000FF') + icon = models.CharField(_('Marker icon'), max_length=32, blank=True, null=True, + help_text=_('FontAwesome style name')) + icon_colour = models.CharField(_('Icon colour'), max_length=6, default='FFFFFF') + + def __unicode__(self): + return self.label + + def get_geojson(self): + response = requests.get(self.geojson_url, + remote_service='auto', + headers={'accept': 'application/json'}) + if not response.ok: + return [] + features = response.json() + for feature in features: + feature['display_fields'] = feature['properties'].copy() + feature['properties']['colour'] = '#%s' % self.marker_colour + feature['properties']['icon_colour'] = '#%s' % self.icon_colour + feature['properties']['label'] = self.label + feature['properties']['icon'] = self.icon + return features diff --git a/combo/apps/maps/templates/maps/manager_base.html b/combo/apps/maps/templates/maps/manager_base.html new file mode 100644 index 0000000..fa979f0 --- /dev/null +++ b/combo/apps/maps/templates/maps/manager_base.html @@ -0,0 +1,11 @@ +{% extends "combo/manager_base.html" %} +{% load i18n %} + +{% block appbar %} +

{% trans 'Maps' %}

+{% endblock %} + +{% block breadcrumb %} +{{ block.super }} +{% trans 'Maps' %} +{% endblock %} diff --git a/combo/apps/maps/templates/maps/manager_home.html b/combo/apps/maps/templates/maps/manager_home.html new file mode 100644 index 0000000..f9a6a99 --- /dev/null +++ b/combo/apps/maps/templates/maps/manager_home.html @@ -0,0 +1,8 @@ +{% extends "maps/manager_base.html" %} +{% load i18n %} + +{% block content %} + +{% endblock %} diff --git a/combo/apps/maps/templates/maps/maplayer_confirm_delete.html b/combo/apps/maps/templates/maps/maplayer_confirm_delete.html new file mode 100644 index 0000000..94ecc80 --- /dev/null +++ b/combo/apps/maps/templates/maps/maplayer_confirm_delete.html @@ -0,0 +1,17 @@ +{% extends "combo/manager_base.html" %} +{% load i18n %} + +{% block appbar %} +

{{ view.model.get_verbose_name }}

+{% endblock %} + +{% block content %} +
+ {% csrf_token %} + {% blocktrans %}Are you sure you want to delete this?{% endblocktrans %} +
+ + {% trans 'Cancel' %} +
+
+{% endblock %} diff --git a/combo/apps/maps/templates/maps/maplayer_form.html b/combo/apps/maps/templates/maps/maplayer_form.html new file mode 100644 index 0000000..e0e9d47 --- /dev/null +++ b/combo/apps/maps/templates/maps/maplayer_form.html @@ -0,0 +1,30 @@ +{% extends "maps/maplayer_list.html" %} +{% load i18n %} + +{% block appbar %} +{% if object.id %} +

{% trans "Edit Layer" %}

+{% else %} +

{% trans "New Layer" %}

+{% endif %} +{% endblock %} + +{% block breadcrumb %} +{{ block.super }} +{% trans 'Layers' %} +{% endblock %} + +{% block content %} + +
+ {% csrf_token %} + {{ form.as_p }} +
+ + {% trans 'Cancel' %} + {% if object.id %} + {% trans 'Delete' %} + {% endif %} +
+
+{% endblock %} diff --git a/combo/apps/maps/templates/maps/maplayer_list.html b/combo/apps/maps/templates/maps/maplayer_list.html new file mode 100644 index 0000000..bea3ef9 --- /dev/null +++ b/combo/apps/maps/templates/maps/maplayer_list.html @@ -0,0 +1,33 @@ +{% extends "maps/manager_base.html" %} +{% load i18n %} + +{% block breadcrumb %} +{{ block.super }} +{% trans 'Layers' %} +{% endblock %} + +{% block appbar %} +

{% trans 'Layers' %}

+{% trans 'New' %} +{% endblock %} + +{% block content %} + +{% if object_list %} +
+ {% for layer in object_list %} + + {% endfor %} +
+{% else %} +
+ {% blocktrans %} + This site doesn't have any layer yet. Click on the "New" button in the top + right of the page to add a first one. + {% endblocktrans %} +
+{% endif %} +{% endblock %} diff --git a/combo/apps/maps/urls.py b/combo/apps/maps/urls.py new file mode 100644 index 0000000..d73de54 --- /dev/null +++ b/combo/apps/maps/urls.py @@ -0,0 +1,39 @@ +# combo - content management system +# Copyright (C) 2017 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.conf.urls import url, include + +from combo.urls_utils import decorated_includes, manager_required + +from .manager_views import (ManagerHomeView, LayersManagerView, LayerAddView, + LayerEditView, LayerDeleteView) + +maps_manager_urls = [ + url('^$', ManagerHomeView.as_view(), name='maps-manager-homepage'), + url('^layers/$', LayersManagerView.as_view(), name='maps-manager-layers-list'), + url('^layers/add/$', LayerAddView.as_view(), name='maps-manager-layer-add'), + url('^layers/(?P\w+)/edit$', LayerEditView.as_view(), + name='maps-manager-layer-edit'), + url('^layers/(?P\w+)/edit/$', LayerEditView.as_view(), + name='maps-manager-layer-edit'), + url('^layers/(?P\w+)/delete/$', LayerDeleteView.as_view(), + name='maps-manager-layer-delete'), +] + +urlpatterns = [ + url(r'^manage/maps/', decorated_includes(manager_required, + include(maps_manager_urls))), +] diff --git a/combo/apps/maps/views.py b/combo/apps/maps/views.py new file mode 100644 index 0000000..2f8175e --- /dev/null +++ b/combo/apps/maps/views.py @@ -0,0 +1,17 @@ +# combo - content management system +# Copyright (C) 2017 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.shortcuts import render diff --git a/combo/manager/static/css/combo.manager.css b/combo/manager/static/css/combo.manager.css index 9c1810f..36e1e1f 100644 --- a/combo/manager/static/css/combo.manager.css +++ b/combo/manager/static/css/combo.manager.css @@ -301,3 +301,35 @@ span.extra-info { font-size: 80%; opacity: 0.5; } + +/* colour picker styles */ + +#jquery-colour-picker { + background: #fafafa; + width: 250px; + padding: 10px 5px; + border-radius: 5px; + box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.3); + z-index: 2000; +} + +#jquery-colour-picker ul { + margin: 0; + padding: 0; + list-style-type: none; +} + +#jquery-colour-picker li { + float: left; + margin: 0 5px 5px 0; +} + +#jquery-colour-picker li a { + display: block; + width: 13px; + height: 13px; + text-decoration: none; + text-indent: -10000px; + outline: 0; + border: 1px solid #aaa; +} diff --git a/combo/manager/static/js/combo.manager.js b/combo/manager/static/js/combo.manager.js index 01a6592..9f5acb3 100644 --- a/combo/manager/static/js/combo.manager.js +++ b/combo/manager/static/js/combo.manager.js @@ -222,4 +222,10 @@ $(function() { window.location = $(this).parent('div').find('option:selected').data('add-url'); return false; }); + + $(document).on('gadjo:dialog-loaded', function(e, dialog) { + if (jQuery.fn.colourPicker !== undefined) { + jQuery('select.colour-picker').colourPicker({title: ''}); + } + }); }); diff --git a/combo/manager/templates/combo/manager_base.html b/combo/manager/templates/combo/manager_base.html index 943352c..4d5870e 100644 --- a/combo/manager/templates/combo/manager_base.html +++ b/combo/manager/templates/combo/manager_base.html @@ -30,4 +30,5 @@ + {% endblock %} diff --git a/combo/settings.py b/combo/settings.py index 0f0247d..ebeaa1e 100644 --- a/combo/settings.py +++ b/combo/settings.py @@ -75,8 +75,11 @@ INSTALLED_APPS = ( 'combo.apps.notifications', 'combo.apps.search', 'combo.apps.usersearch', + 'combo.apps.maps', 'haystack', 'xstatic.pkg.chartnew_js', + 'xstatic.pkg.font_awesome', + 'xstatic.pkg.jquery_colourpicker', ) INSTALLED_APPS = plugins.register_plugins_apps(INSTALLED_APPS) diff --git a/debian/control b/debian/control index 1ef3ef8..a82e2bc 100644 --- a/debian/control +++ b/debian/control @@ -16,6 +16,8 @@ Depends: ${misc:Depends}, ${python:Depends}, python-feedparser, python-django-cmsplugin-blurp, python-xstatic-chartnew-js, + python-xstatic-font-awesome, + python-xstatic-jquery-colourpicker, python-eopayment (>= 1.9), python-django-haystack (>= 2.4.0) Recommends: python-django-mellon, python-whoosh diff --git a/requirements.txt b/requirements.txt index 88c5fac..e31be0a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,8 @@ feedparser django-jsonfield requests XStatic-ChartNew.js +XStatic-Font-Awesome +git+http://git.entrouvert.org/debian/xstatic-jquery-colourpicker.git eopayment>=1.13 python-dateutil djangorestframework>=3.3, <3.4 diff --git a/setup.py b/setup.py index 6a7351e..ea8b815 100644 --- a/setup.py +++ b/setup.py @@ -111,6 +111,8 @@ setup( 'django-jsonfield', 'requests', 'XStatic-ChartNew.js', + 'XStatic-Font-Awesome', + 'XStatic-Jquery-Colourpicker', 'eopayment>=1.13', 'python-dateutil', 'djangorestframework>=3.3, <3.4', diff --git a/tests/test_maps_manager.py b/tests/test_maps_manager.py new file mode 100644 index 0000000..1371396 --- /dev/null +++ b/tests/test_maps_manager.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- + +import pytest +import mock + +from django.contrib.auth.models import User + +from combo.apps.maps.models import MapLayer + +pytestmark = pytest.mark.django_db + +@pytest.fixture +def admin_user(): + try: + user = User.objects.get(username='admin') + except User.DoesNotExist: + user = User.objects.create_superuser('admin', email=None, password='admin') + return user + +def login(app, username='admin', password='admin'): + login_page = app.get('/login/') + login_form = login_page.forms[0] + login_form['username'] = username + login_form['password'] = password + resp = login_form.submit() + assert resp.status_int == 302 + return app + +def test_access(app, admin_user): + app = login(app) + resp = app.get('/manage/', status=200) + assert '/manage/maps/' in resp.body + +def test_add_layer(app, admin_user): + MapLayer.objects.all().delete() + app = login(app) + resp = app.get('/manage/maps/layers/', status=200) + resp = resp.click('New') + assert 'js/jquery.colourpicker.js' in resp.content + resp.forms[0]['label'] = 'Test' + resp.forms[0]['geojson_url'] = 'http://example.net/geojson' + assert resp.form['marker_colour'].value == '0000FF' + resp.forms[0]['marker_colour'] = 'FFFFFF' + resp.forms[0]['icon'] = 'fa-bicycle' + assert resp.form['icon_colour'].value == 'FFFFFF' + resp.form['icon_colour'] = '000000' + resp = resp.forms[0].submit() + assert resp.location == 'http://testserver/manage/maps/layers/' + assert MapLayer.objects.count() == 1 + layer = MapLayer.objects.get() + assert layer.label == 'Test' + +def test_edit_layer(app, admin_user): + test_add_layer(app, admin_user) + app = login(app) + resp = app.get('/manage/maps/layers/', status=200) + resp = resp.click('Test') + resp.forms[0]['geojson_url'] = 'http://example.net/new_geojson' + resp = resp.forms[0].submit() + assert resp.location == 'http://testserver/manage/maps/layers/' + assert MapLayer.objects.count() == 1 + layer = MapLayer.objects.get() + assert layer.geojson_url == 'http://example.net/new_geojson' + +def test_delete_layer(app, admin_user): + test_add_layer(app, admin_user) + app = login(app) + resp = app.get('/manage/maps/layers/', status=200) + resp = resp.click('Test') + resp = resp.click('Delete') + assert 'Are you sure you want to delete this?' in resp.body + resp = resp.forms[0].submit() + assert resp.location == 'http://testserver/manage/maps/layers/' + assert MapLayer.objects.count() == 0 + +@mock.patch('combo.apps.maps.models.requests.get') +def test_download_geojson(mock_request, app, admin_user): + test_add_layer(app, admin_user) + layer = MapLayer.objects.get() + mocked_response = mock.Mock() + mocked_response.json.return_value = [{'type': 'Feature', + 'geometry': {'type': 'Point', + 'coordinates': [2.3233688436448574, 48.83369263315934]}, + 'properties': {'property': 'property value'}}] + mocked_response.ok.return_value = True + mock_request.return_value = mocked_response + geojson = layer.get_geojson() + assert len(geojson) > 0 + for item in geojson: + assert item['type'] == 'Feature' + assert item['geometry']['type'] == 'Point' + assert item['geometry']['coordinates'] == [2.3233688436448574, 48.83369263315934] + assert item['display_fields'] == {'property': 'property value'} + assert item['properties']['icon'] == 'fa-bicycle' + assert item['properties']['label'] == 'Test' + assert item['properties']['colour'] == '#FFFFFF' + assert item['properties']['icon_colour'] == '#000000' -- 2.11.0