From 38fb99aca8506f54e07e760b8966798c2dd5ea4f 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/static/js/jquery.colourpicker.js | 112 +++++++++++++++++++++ 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 | 1 + tests/test_maps_manager.py | 97 ++++++++++++++++++ 19 files changed, 590 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/static/js/jquery.colourpicker.js 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..42a353d --- /dev/null +++ b/combo/apps/maps/forms.py @@ -0,0 +1,33 @@ +# 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 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..732611b --- /dev/null +++ b/combo/apps/maps/manager_views.py @@ -0,0 +1,47 @@ +# 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 . + +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..1faea6e --- /dev/null +++ b/combo/apps/maps/models.py @@ -0,0 +1,48 @@ +# 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 . + + +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/static/js/jquery.colourpicker.js b/combo/apps/maps/static/js/jquery.colourpicker.js new file mode 100644 index 0000000..8012c43 --- /dev/null +++ b/combo/apps/maps/static/js/jquery.colourpicker.js @@ -0,0 +1,112 @@ +/* adapted from http://andreaslagerkvist.com/jquery/colour-picker/ */ + +jQuery.fn.colourPicker = function (conf) { + // Config for plug + var config = jQuery.extend({ + id: 'jquery-colour-picker', // id of colour-picker container + inputBG: true, // Whether to change the input's background to the selected colour's + speed: 500 // Speed of dialogue-animation + }, conf); + + // Inverts a hex-colour + var hexInvert = function (hex) { + var r = parseInt(hex.substr(0, 2), 16); + var g = parseInt(hex.substr(2, 2), 16); + var b = parseInt(hex.substr(4, 2), 16); + + return 0.212671 * r + 0.715160 * g + 0.072169 * b < 128 ? 'ffffff' : '000000' + }; + + // Add the colour-picker dialogue if not added + var colourPicker = jQuery('#' + config.id); + + if (!colourPicker.length) { + colourPicker = jQuery('
').appendTo(document.body).hide(); + + // Remove the colour-picker if you click outside it (on body) + jQuery(document.body).click(function(event) { + if (!(jQuery(event.target).is('#' + config.id) || jQuery(event.target).parents('#' + config.id).length)) { + colourPicker.hide(config.speed); + } + }); + } + + // For every select passed to the plug-in + return this.each(function () { + // Insert icon and input + var select = jQuery(this); + var icon = jQuery('...').insertAfter(select); + var input = jQuery('').insertAfter(select); + var loc = ''; + + // Build a list of colours based on the colours in the select + jQuery('option', select).each(function () { + var option = jQuery(this); + var hex = option.val(); + var title = option.text(); + + loc += '
  • ' + + title + + '
  • '; + }); + + // Remove select + select.remove(); + + // If user wants to, change the input's BG to reflect the newly selected colour + if (config.inputBG) { + input.change(function () { + input.css({background: '#' + input.val(), color: '#' + hexInvert(input.val())}); + }); + + input.change(); + } + + // When you click the icon + icon.click(function () { + // Show the colour-picker next to the icon and fill it with the colours in the select that used to be there + var iconPos = icon.offset(); + + colourPicker.html('
      ' + loc + '
    ').css({ + position: 'absolute', + left: iconPos.left + 'px', + top: iconPos.top + 'px' + }).show(config.speed); + + // When you click a colour in the colour-picker + jQuery('a', colourPicker).click(function () { + // The hex is stored in the link's rel-attribute + var hex = jQuery(this).attr('rel'); + + input.val(hex); + + // If user wants to, change the input's BG to reflect the newly selected colour + if (config.inputBG) { + input.css({background: '#' + hex, color: '#' + hexInvert(hex)}); + } + + // Trigger change-event on input + input.change(); + + // Hide the colour-picker and return false + colourPicker.hide(config.speed); + + return false; + }); + + return false; + }); + }); +}; + +$(function() { + jQuery('select.colour-picker').colourPicker(); +}); 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..8b970b0 --- /dev/null +++ b/combo/apps/maps/urls.py @@ -0,0 +1,39 @@ +# 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 . + +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..5c657aa --- /dev/null +++ b/combo/apps/maps/views.py @@ -0,0 +1,17 @@ +# 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 . + +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..5fc29ec 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..877cf28 100644 --- a/combo/settings.py +++ b/combo/settings.py @@ -75,6 +75,7 @@ INSTALLED_APPS = ( 'combo.apps.notifications', 'combo.apps.search', 'combo.apps.usersearch', + 'combo.apps.maps', 'haystack', 'xstatic.pkg.chartnew_js', ) 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