0002-maps-map-cell-8454.patch
combo/apps/maps/migrations/0002_mapcell.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 |
('auth', '0006_require_contenttypes_0002'), |
|
11 |
('data', '0026_jsoncell_force_async'), |
|
12 |
('maps', '0001_initial'), |
|
13 |
] |
|
14 | ||
15 |
operations = [ |
|
16 |
migrations.CreateModel( |
|
17 |
name='MapCell', |
|
18 |
fields=[ |
|
19 |
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), |
|
20 |
('placeholder', models.CharField(max_length=20)), |
|
21 |
('order', models.PositiveIntegerField()), |
|
22 |
('slug', models.SlugField(verbose_name='Slug', blank=True)), |
|
23 |
('extra_css_class', models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True)), |
|
24 |
('public', models.BooleanField(default=True, verbose_name='Public')), |
|
25 |
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')), |
|
26 |
('last_update_timestamp', models.DateTimeField(auto_now=True)), |
|
27 |
('title', models.CharField(max_length=150, verbose_name='Title', blank=True)), |
|
28 |
('default_position', models.CharField(default=b'48.83369263315934;2.3233688436448574', max_length=128, null=True, verbose_name='Default position', blank=True)), |
|
29 |
('initial_zoom', models.CharField(default=b'13', max_length=2, verbose_name='Initial zoom level', choices=[(b'0', 'Whole world'), (b'9', 'Wide area'), (b'11', 'Area'), (b'13', 'Town'), (b'16', 'Small road'), (b'19', 'Ant')])), |
|
30 |
('min_zoom', models.CharField(default=b'0', max_length=2, verbose_name='Minimal zoom level', choices=[(b'0', 'Whole world'), (b'9', 'Wide area'), (b'11', 'Area'), (b'13', 'Town'), (b'16', 'Small road'), (b'19', 'Ant')])), |
|
31 |
('max_zoom', models.CharField(default=19, max_length=2, verbose_name='Maximal zoom level', choices=[(b'0', 'Whole world'), (b'9', 'Wide area'), (b'11', 'Area'), (b'13', 'Town'), (b'16', 'Small road'), (b'19', 'Ant')])), |
|
32 |
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)), |
|
33 |
('layers', models.ManyToManyField(to='maps.MapLayer', verbose_name='Layers', blank=True)), |
|
34 |
('page', models.ForeignKey(to='data.Page')), |
|
35 |
], |
|
36 |
options={ |
|
37 |
'verbose_name': 'Map Cell', |
|
38 |
}, |
|
39 |
), |
|
40 |
] |
combo/apps/maps/models.py | ||
---|---|---|
17 | 17 | |
18 | 18 |
from django.db import models |
19 | 19 |
from django.utils.translation import ugettext_lazy as _ |
20 |
from django.core.urlresolvers import reverse_lazy |
|
21 |
from django import forms |
|
22 |
from django import template |
|
23 | ||
24 |
from combo.data.models import CellBase |
|
25 |
from combo.data.library import register_cell_class |
|
26 | ||
27 |
zoom_levels = [ ('0', _('Whole world')), |
|
28 |
('9', _('Wide area')), |
|
29 |
('11', _('Area')), |
|
30 |
('13', _('Town')), |
|
31 |
('16', _('Small road')), |
|
32 |
('19', _('Ant')),] |
|
20 | 33 | |
21 | 34 |
from combo.utils import requests |
22 | 35 | |
23 | 36 | |
37 |
class MapWidget(forms.TextInput): |
|
38 |
template_name = 'maps/map_widget.html' |
|
39 | ||
40 |
def render(self, name, value, attrs): |
|
41 |
final_attrs = self.build_attrs(attrs, name=name, value=value, |
|
42 |
type='hidden') |
|
43 |
cell_form_template = template.loader.get_template(self.template_name) |
|
44 |
return cell_form_template.render(final_attrs) |
|
45 | ||
46 | ||
24 | 47 |
class MapLayer(models.Model): |
25 | 48 |
label = models.CharField(_('Label'), max_length=128) |
26 | 49 |
geojson_url = models.URLField(_('Geojson URL')) |
... | ... | |
46 | 69 |
feature['properties']['label'] = self.label |
47 | 70 |
feature['properties']['icon'] = self.icon |
48 | 71 |
return features |
72 | ||
73 | ||
74 |
@register_cell_class |
|
75 |
class MapCell(CellBase): |
|
76 |
title = models.CharField(_('Title'), max_length=150, blank=True) |
|
77 |
default_position = models.CharField(_('Default position'), null=True, blank=True, |
|
78 |
default='48.83369263315934;2.3233688436448574', |
|
79 |
max_length=128) |
|
80 |
initial_zoom = models.CharField(_('Initial zoom level'), max_length=2, |
|
81 |
choices=zoom_levels, default='13') |
|
82 |
min_zoom = models.CharField(_('Minimal zoom level'), max_length=2, |
|
83 |
choices=zoom_levels, default='0') |
|
84 |
max_zoom = models.CharField(_('Maximal zoom level'), max_length=2, |
|
85 |
choices=zoom_levels, default=19) |
|
86 |
layers = models.ManyToManyField(MapLayer, verbose_name=_('Layers'), blank=True) |
|
87 | ||
88 |
template_name = 'maps/mapcell.html' |
|
89 | ||
90 |
class Meta: |
|
91 |
verbose_name = _('Map Cell') |
|
92 | ||
93 |
class Media: |
|
94 |
js = ('xstatic/leaflet.js', 'js/map.js') |
|
95 |
css = {'all': ('xstatic/leaflet.css', 'xstatic/css/font-awesome.min.css', |
|
96 |
'css/map.css')} |
|
97 | ||
98 |
def get_default_form_class(self): |
|
99 |
fields = ('title', 'default_position', 'initial_zoom', 'min_zoom', |
|
100 |
'max_zoom', 'layers') |
|
101 |
lat, lng = self.default_position.split(';') |
|
102 |
widgets = {'layers': forms.widgets.CheckboxSelectMultiple, |
|
103 |
'default_position': MapWidget(attrs={'init_lat': lat, |
|
104 |
'init_lng': lng})} |
|
105 |
return forms.models.modelform_factory(self.__class__, fields=fields, |
|
106 |
widgets=widgets) |
|
107 | ||
108 | ||
109 |
@classmethod |
|
110 |
def is_enabled(cls): |
|
111 |
return MapLayer.objects.count() > 0 |
|
112 | ||
113 |
def get_cell_extra_context(self, context): |
|
114 |
ctx = super(MapCell, self).get_cell_extra_context(context) |
|
115 |
ctx['title'] = self.title |
|
116 |
ctx['initial_lat'], ctx['initial_lng'] = self.default_position.split(';'); |
|
117 |
ctx['initial_zoom'] = self.initial_zoom |
|
118 |
ctx['min_zoom'] = self.min_zoom |
|
119 |
ctx['max_zoom'] = self.max_zoom |
|
120 |
ctx['geojson_url'] = reverse_lazy('cell-geojson', kwargs={'cell_id': self.pk}) |
|
121 |
return ctx |
combo/apps/maps/static/css/map.css | ||
---|---|---|
1 |
div.combo-cell-map { |
|
2 |
height: 60vh; |
|
3 |
} |
|
4 | ||
5 |
/* leaflet styles */ |
|
6 | ||
7 |
div.leaflet-div-icon span { |
|
8 |
width: 2.3rem; |
|
9 |
height: 2.3rem; |
|
10 |
display: block; |
|
11 |
left: -1rem; |
|
12 |
top: -1rem; |
|
13 |
position: relative; |
|
14 |
border-radius: 11rem 6rem 0.8rem; |
|
15 |
transform: scale(1, 1.3) rotate(45deg); |
|
16 |
border: 1px solid #aaa; |
|
17 |
} |
|
18 | ||
19 |
div.leaflet-popup-content span { |
|
20 |
display: block; |
|
21 |
} |
|
22 | ||
23 |
div.leaflet-popup-content span.field-label { |
|
24 |
font-weight: bold; |
|
25 |
text-transform: capitalize; |
|
26 |
} |
|
27 | ||
28 |
div.leaflet-div-icon span i:before { |
|
29 |
display: inline-block; |
|
30 |
margin: 9px; |
|
31 |
transform: scale(1.1) rotate(-45deg); |
|
32 |
} |
combo/apps/maps/static/js/map.js | ||
---|---|---|
1 |
$(window).on('load combo:cellform-reload', function() { |
|
2 |
$('div.combo-cell-map').each(function() { |
|
3 |
var $map_widget = $(this); |
|
4 |
var map_options = Object(); |
|
5 |
var initial_zoom = parseInt($map_widget.data('initial-zoom')); |
|
6 |
if (! isNaN(initial_zoom)) { |
|
7 |
map_options.zoom = initial_zoom; |
|
8 |
} else { |
|
9 |
map_options.zoom = 13; |
|
10 |
} |
|
11 |
var max_zoom = parseInt($map_widget.data('max_zoom')); |
|
12 |
if (!isNaN(max_zoom)) map_options.maxZoom = max_zoom; |
|
13 |
var min_zoom = parseInt($map_widget.data('min-zoom')); |
|
14 |
if (!isNaN(min_zoom)) map_options.minZoom = min_zoom; |
|
15 |
var latlng = [$map_widget.data('init-lat'), $map_widget.data('init-lng')]; |
|
16 |
var geojson_url = $map_widget.data('geojson-url'); |
|
17 |
var map = L.map(this, map_options); |
|
18 |
var store_position_selector = $map_widget.data('store-position'); |
|
19 |
map.setView(latlng, map_options.zoom); |
|
20 | ||
21 |
L.tileLayer( |
|
22 |
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', |
|
23 |
{ |
|
24 |
attribution: 'Map data © <a href="https://openstreetmap.org">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>' |
|
25 |
}).addTo(map); |
|
26 |
if (store_position_selector) { |
|
27 |
map.marker = L.marker(latlng); |
|
28 |
map.marker.addTo(map); |
|
29 |
var hidden = $('input#' + store_position_selector); |
|
30 |
map.on('click', function(e) { |
|
31 |
map.marker.setLatLng(e.latlng); |
|
32 |
hidden.val(e.latlng.lat + ';' + e.latlng.lng); |
|
33 |
}); |
|
34 |
} |
|
35 |
if (geojson_url) { |
|
36 |
$.getJSON(geojson_url, function(data) { |
|
37 |
var geo_json = L.geoJson(data, { |
|
38 |
onEachFeature: function(feature, layer) { |
|
39 |
if (feature.display_fields) { |
|
40 |
var popup = ''; |
|
41 |
$.each(feature.display_fields, function(key, value) { |
|
42 |
popup += '<p class="popup-field"><span class="field-label">' + key + '</span>'; |
|
43 |
popup += '<span class="field-value">' + value + '</span></p>'; |
|
44 |
}); |
|
45 |
} else { |
|
46 |
var popup = '<p class="popup-field">' + feature.properties.label + '</p>'; |
|
47 |
} |
|
48 |
layer.bindPopup(popup); |
|
49 |
}, |
|
50 |
pointToLayer: function (feature, latlng) { |
|
51 |
var markerStyles = "background-color: "+feature.properties.colour+";"; |
|
52 |
marker = L.divIcon({iconAnchor: [0, 30], |
|
53 |
popupAnchor: [5, -45], |
|
54 |
html: '<span style="' + markerStyles + '"><i class="fa '+feature.properties.icon+'" style="color:'+feature.properties.icon_colour+'"></i></span>' |
|
55 |
}); |
|
56 |
return L.marker(latlng, {icon: marker}); |
|
57 |
} |
|
58 |
}); |
|
59 |
map.fitBounds(geo_json.getBounds()); |
|
60 |
geo_json.addTo(map); |
|
61 |
}); |
|
62 |
} |
|
63 |
}); |
|
64 |
}); |
combo/apps/maps/templates/maps/map_widget.html | ||
---|---|---|
1 |
<div class="combo-cell-map" data-init-lat="{{ init_lat }}" data-init-lng="{{ init_lng }}" data-store-position="combo-map-{{ id }}"> |
|
2 |
</div> |
|
3 | ||
4 |
<input type="{{ type }}" name="{{ name }}" value="{{ value }}" id="combo-map-{{ id }}" /> |
combo/apps/maps/templates/maps/mapcell.html | ||
---|---|---|
1 |
{% load i18n %} |
|
2 |
<h2>{{ title }}</h2> |
|
3 |
<div class="combo-cell-map" data-initial-zoom="{{ initial_zoom }}" data-min-zoom="{{ min_zoom }}" data-max-zoom="{{ max_zoom }}" data-init-lat="{{ initial_lat }}" data-init-lng="{{ initial_lng }}" data-geojson-url="{{ geojson_url }}"> |
|
4 |
</div> |
combo/apps/maps/urls.py | ||
---|---|---|
21 | 21 |
from .manager_views import (ManagerHomeView, LayersManagerView, LayerAddView, |
22 | 22 |
LayerEditView, LayerDeleteView) |
23 | 23 | |
24 |
from .views import GeojsonView |
|
25 | ||
24 | 26 |
maps_manager_urls = [ |
25 | 27 |
url('^$', ManagerHomeView.as_view(), name='maps-manager-homepage'), |
26 | 28 |
url('^layers/$', LayersManagerView.as_view(), name='maps-manager-layers-list'), |
... | ... | |
36 | 38 |
urlpatterns = [ |
37 | 39 |
url(r'^manage/maps/', decorated_includes(manager_required, |
38 | 40 |
include(maps_manager_urls))), |
41 |
url(r'^maps/geojson/(?P<cell_id>\w+)/$', GeojsonView.as_view(), |
|
42 |
name='cell-geojson'), |
|
39 | 43 |
] |
combo/apps/maps/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 |
import json |
|
18 | ||
19 |
from django.views.generic.base import View |
|
20 |
from django.http import HttpResponse, Http404 |
|
21 |
from django.core.exceptions import PermissionDenied |
|
22 | ||
23 |
from combo.utils import requests |
|
24 | ||
25 |
from .models import MapCell |
|
26 | ||
27 | ||
28 |
class GeojsonView(View): |
|
29 | ||
30 |
def get(self, request, *args, **kwargs): |
|
31 |
try: |
|
32 |
cell = MapCell.objects.get(pk=kwargs['cell_id']) |
|
33 |
except MapCell.DoesNotExist: |
|
34 |
raise Http404() |
|
35 |
if not cell.page.public: |
|
36 |
raise PermissionDenied() |
|
37 |
if not cell.is_visible(request.user): |
|
38 |
raise PermissionDenied() |
|
39 |
geojson = {'type': 'FeatureCollection', 'features': []} |
|
40 |
for layer in cell.layers.all(): |
|
41 |
geojson['features'] += layer.get_geojson() |
|
42 | ||
43 |
content_type = 'application/json' |
|
44 |
return HttpResponse(json.dumps(geojson), content_type=content_type) |
combo/manager/static/js/combo.manager.js | ||
---|---|---|
175 | 175 |
/* update form with new content, unless it has a ckeditor, as |
176 | 176 |
* this causes an unpleasant flickering */ |
177 | 177 |
$button.parents('form').find('div.cell-form').html(data); |
178 |
var e = $.Event('combo:cellform-reload'); |
|
179 |
$(document).trigger(e); |
|
178 | 180 |
} |
179 | 181 |
if (data.indexOf('class="errorlist"') == -1) { |
180 | 182 |
$.getJSON($form.data('label-url'), |
combo/manager/templates/combo/manager_base.html | ||
---|---|---|
3 | 3 | |
4 | 4 |
{% block css %} |
5 | 5 |
<link rel="stylesheet" type="text/css" media="all" href="{{ STATIC_URL }}css/combo.manager.css"/> |
6 |
<link rel="stylesheet" type="text/css" media="all" href="{% static "xstatic/leaflet.css" %}"></script> |
|
7 |
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/map.css" %}"></script> |
|
6 | 8 |
{% endblock %} |
7 | 9 |
{% block page-title %}{% firstof site_title "Combo" %}{% endblock %} |
8 | 10 |
{% block site-title %}{% firstof site_title "Combo" %}{% endblock %} |
... | ... | |
31 | 33 |
<script type="text/javascript" src="{% static "ckeditor/ckeditor-init.js" %}"></script> |
32 | 34 |
<script src="{% static "js/combo.manager.js" %}"></script> |
33 | 35 |
<script src="{% static "xstatic/jquery.colourpicker.js" %}"></script> |
36 |
<script src="{% static "xstatic/leaflet.js" %}"></script> |
|
37 |
<script src="{% static "js/map.js" %}"></script> |
|
34 | 38 |
{% endblock %} |
combo/settings.py | ||
---|---|---|
80 | 80 |
'xstatic.pkg.chartnew_js', |
81 | 81 |
'xstatic.pkg.font_awesome', |
82 | 82 |
'xstatic.pkg.jquery_colourpicker', |
83 |
'xstatic.pkg.leaflet', |
|
83 | 84 |
) |
84 | 85 | |
85 | 86 |
INSTALLED_APPS = plugins.register_plugins_apps(INSTALLED_APPS) |
debian/control | ||
---|---|---|
18 | 18 |
python-xstatic-chartnew-js, |
19 | 19 |
python-xstatic-font-awesome, |
20 | 20 |
python-xstatic-jquery-colourpicker, |
21 |
python-xstatic-leaflet, |
|
21 | 22 |
python-eopayment (>= 1.9), |
22 | 23 |
python-django-haystack (>= 2.4.0) |
23 | 24 |
Recommends: python-django-mellon, python-whoosh |
requirements.txt | ||
---|---|---|
8 | 8 |
XStatic-ChartNew.js |
9 | 9 |
XStatic-Font-Awesome |
10 | 10 |
git+http://git.entrouvert.org/debian/xstatic-jquery-colourpicker.git |
11 |
XStatic-Leaflet |
|
11 | 12 |
eopayment>=1.13 |
12 | 13 |
python-dateutil |
13 | 14 |
djangorestframework>=3.3, <3.4 |
setup.py | ||
---|---|---|
113 | 113 |
'XStatic-ChartNew.js', |
114 | 114 |
'XStatic-Font-Awesome', |
115 | 115 |
'XStatic-Jquery-Colourpicker', |
116 |
'XStatic-Leaflet', |
|
116 | 117 |
'eopayment>=1.13', |
117 | 118 |
'python-dateutil', |
118 | 119 |
'djangorestframework>=3.3, <3.4', |
tests/test_maps_cells.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
import pytest |
|
3 | ||
4 |
from django.contrib.auth.models import User |
|
5 |
from django.test.client import RequestFactory |
|
6 |
from django.template import Context |
|
7 |
from django.test import Client |
|
8 |
from django.core.urlresolvers import reverse |
|
9 |
from django.contrib.auth.models import Group |
|
10 | ||
11 |
from combo.data.models import Page |
|
12 |
from combo.apps.maps.models import MapLayer, MapCell |
|
13 | ||
14 |
pytestmark = pytest.mark.django_db |
|
15 | ||
16 |
client = Client() |
|
17 | ||
18 |
@pytest.fixture |
|
19 |
def user(): |
|
20 |
try: |
|
21 |
user = User.objects.get(username='admin') |
|
22 |
except User.DoesNotExist: |
|
23 |
user = User.objects.create_user('admin', email=None, password='admin') |
|
24 |
return user |
|
25 | ||
26 |
@pytest.fixture |
|
27 |
def layer(): |
|
28 |
try: |
|
29 |
layer = MapLayer.objects.get() |
|
30 |
except MapLayer.DoesNotExist: |
|
31 |
layer = MapLayer() |
|
32 |
layer.geojson_url = 'http://example.net/geojson' |
|
33 |
layer.marker_colour = 'FF0000' |
|
34 |
layer.icon = 'fa-bicycle' |
|
35 |
layer.icon_colour = '0000FF' |
|
36 |
layer.save() |
|
37 |
return layer |
|
38 | ||
39 |
def login(username='admin', password='admin'): |
|
40 |
resp = client.post('/login/', {'username': username, 'password': password}) |
|
41 |
assert resp.status_code == 302 |
|
42 | ||
43 |
def test_cell_disabled(): |
|
44 |
MapLayer.objects.all().delete() |
|
45 |
assert MapCell.is_enabled() is False |
|
46 | ||
47 |
def test_cell_enabled(layer): |
|
48 |
assert MapCell.is_enabled() is True |
|
49 | ||
50 |
def test_cell_rendering(layer): |
|
51 |
page = Page(title='xxx', slug='test_map_cell', template_name='standard') |
|
52 |
page.save() |
|
53 |
cell = MapCell(page=page, placeholder='content', order=0, |
|
54 |
title = 'Map with points') |
|
55 |
cell.save() |
|
56 |
cell.layers.add(layer) |
|
57 |
context = Context({'request': RequestFactory().get('/')}) |
|
58 |
rendered = cell.render(context) |
|
59 |
assert 'data-initial-zoom="13"' in rendered |
|
60 |
assert 'data-min-zoom="0"' in rendered |
|
61 |
assert 'data-max-zoom="19"' in rendered |
|
62 |
assert 'data-init-lat="48.83369263315934"' in rendered |
|
63 |
assert 'data-init-lng="2.3233688436448574"' in rendered |
|
64 |
assert 'data-geojson-url="/maps/geojson/1/"' in rendered |
|
65 |
resp = client.get('/test_map_cell/') |
|
66 |
print dir(resp) |
|
67 |
assert 'xstatic/leaflet.js' in resp.content |
|
68 |
assert 'js/map.js' in resp.content |
|
69 |
assert 'xstatic/leaflet.css' in resp.content |
|
70 |
assert 'xstatic/css/font-awesome.min.css' in resp.content |
|
71 |
assert 'css/map.css' in resp.content |
|
72 | ||
73 | ||
74 |
def test_get_geojson_on_non_public_page(layer): |
|
75 |
page = Page(title='xxx', slug='new', template_name='standard', |
|
76 |
public=False) |
|
77 |
page.save() |
|
78 |
cell = MapCell(page=page, placeholder='content', order=0, |
|
79 |
title = 'Map with points') |
|
80 |
cell.save() |
|
81 |
cell.layers.add(layer) |
|
82 |
resp = client.get(reverse('cell-geojson', kwargs={'cell_id': cell.id})) |
|
83 |
assert resp.status_code == 403 |
|
84 | ||
85 |
def test_get_geojson_on_non_publik_cell(layer): |
|
86 |
page = Page(title='xxx', slug='new', template_name='standard') |
|
87 |
page.save() |
|
88 |
cell = MapCell(page=page, placeholder='content', order=0, public=False, |
|
89 |
title = 'Map with points') |
|
90 |
cell.save() |
|
91 |
cell.layers.add(layer) |
|
92 |
resp = client.get(reverse('cell-geojson', kwargs={'cell_id': cell.id})) |
|
93 |
assert resp.status_code == 403 |
|
94 | ||
95 |
def test_geojson_on_restricted_cell(layer, user): |
|
96 |
page = Page(title='xxx', slug='new', template_name='standard') |
|
97 |
page.save() |
|
98 |
group = Group.objects.create(name='map tester') |
|
99 |
cell = MapCell(page=page, placeholder='content', order=0, public=False) |
|
100 |
cell.title = 'Map with points' |
|
101 |
cell.save() |
|
102 |
cell.layers.add(layer) |
|
103 |
cell.groups.add(group) |
|
104 |
login() |
|
105 |
resp = client.get(reverse('cell-geojson', kwargs={'cell_id': cell.id})) |
|
106 |
assert resp.status_code == 403 |
|
0 |
- |