Editor: edit & delete features

This commit is contained in:
Laura Klünder 2016-09-26 13:32:05 +02:00
parent 6ce96148d7
commit 576b33fd1a
13 changed files with 239 additions and 31 deletions

View file

@ -1,5 +1,4 @@
from django.conf.urls import include, url from django.conf.urls import include, url
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from ..editor import api as editor_api from ..editor import api as editor_api

View file

@ -1,5 +1,4 @@
from django.http import Http404 from django.http import Http404
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ViewSet from rest_framework.viewsets import ViewSet

View file

@ -17,6 +17,7 @@ class FeatureForm(ModelForm):
titles = OrderedDict((lang_code, '') for lang_code, language in settings.LANGUAGES) titles = OrderedDict((lang_code, '') for lang_code, language in settings.LANGUAGES)
if self.instance is not None and self.instance.pk: if self.instance is not None and self.instance.pk:
self.fields['name'].widget.attrs['readonly'] = True
titles.update(self.instance.titles) titles.update(self.instance.titles)
language_titles = dict(settings.LANGUAGES) language_titles = dict(settings.LANGUAGES)
@ -35,6 +36,14 @@ class FeatureForm(ModelForm):
_('You have to select a title in at least one language.') _('You have to select a title in at least one language.')
) )
def clean_name(self):
if self.instance is not None and self.instance.pk and self.cleaned_data['name'] != self.instance.name:
raise ValidationError(
_('You cannot edit feature identifiers of existing objects.')
)
return self.cleaned_data['name']
def get_languages(self): def get_languages(self):
pass pass

View file

@ -78,9 +78,46 @@
padding-left:5.5px; padding-left:5.5px;
padding-right:5.5px; padding-right:5.5px;
} }
legend {
margin: 0;
}
.start-drawing { .start-drawing {
display:none; display:none;
} }
.leaflet-editable-drawing .leaflet-overlay-pane .leaflet-interactive { .leaflet-editable-drawing .leaflet-overlay-pane .leaflet-interactive {
cursor:crosshair; cursor:crosshair;
} }
.feature_level_list {
list-style-type:none;
display:none;
margin-bottom:15px;
padding:0;
}
.feature_level_list li {
padding:5px 0 0;
border-style:solid;
border-width:0 0 1px;
border-color:#EEEEEE;
}
.feature_level_list li:hover, .feature_level_list li.hover {
background-color:#FFFFEE;
}
.feature_level_list p {
margin:0 0 5px;
cursor:default;
}
.feature_level_list p:first-child {
font-weight:bold;
font-size:16px;
}
.feature_level_list p:first-child em {
font-weight:normal;
font-size:14px;
padding-left:5px;
}
#map .leaflet-overlay-pane .c3nav-highlight {
pointer-events:none;
}
#btn_editing_cancel {
margin-right:8px;
}

View file

@ -1,6 +1,7 @@
editor = { editor = {
feature_types: {}, feature_types: {},
feature_types_order: [], feature_types_order: [],
_highlight_layer: null,
init: function () { init: function () {
// Init Map // Init Map
@ -14,6 +15,7 @@ editor = {
}); });
L.control.scale({imperial: false}).addTo(editor.map); L.control.scale({imperial: false}).addTo(editor.map);
editor._highlight_layer = L.layerGroup().addTo(editor.map);
editor.get_feature_types(); editor.get_feature_types();
editor.get_packages(); editor.get_packages();
@ -29,7 +31,7 @@ editor = {
editor.feature_types[feature_type.name] = feature_type; editor.feature_types[feature_type.name] = feature_type;
editor.feature_types_order.push(feature_type.name); editor.feature_types_order.push(feature_type.name);
editcontrols.append( editcontrols.append(
$('<fieldset>').attr('name', feature_type.name).append( $('<fieldset class="feature_list">').attr('name', feature_type.name).append(
$('<legend>').text(feature_type.title_plural).append( $('<legend>').text(feature_type.title_plural).append(
$('<button class="btn btn-default btn-xs pull-right start-drawing"><i class="glyphicon glyphicon-plus"></i></button>') $('<button class="btn btn-default btn-xs pull-right start-drawing"><i class="glyphicon glyphicon-plus"></i></button>')
) )
@ -101,17 +103,21 @@ editor = {
editor.set_current_level($(this).attr('name')); editor.set_current_level($(this).attr('name'));
}); });
var level; var level, feature_type;
for (var i = 0; i < levels.length; i++) { for (var i = 0; i < levels.length; i++) {
level = levels[i]; level = levels[i];
editor.levels[level.name] = level; editor.levels[level.name] = level;
editor.level_feature_layers[level.name] = {}; editor.level_feature_layers[level.name] = {};
for (var j = 0; j < editor.feature_types_order.length; j++) { for (var j = 0; j < editor.feature_types_order.length; j++) {
editor.level_feature_layers[level.name][editor.feature_types_order[j]] = L.layerGroup(); feature_type = editor.feature_types_order[j];
editor.level_feature_layers[level.name][feature_type] = L.layerGroup();
$('.feature_list[name='+feature_type+']').append(
$('<ul class="feature_level_list">').attr('data-level', level.name)
);
} }
} }
editor.set_current_level(levels[levels.length - 1].name); editor.set_current_level(levels[levels.length - 1].name);
editor.init_drawing(); editor.init_features();
}); });
}, },
set_current_level: function(level_name) { set_current_level: function(level_name) {
@ -125,11 +131,13 @@ editor = {
editor._level = level_name; editor._level = level_name;
$('.leaflet-levels .current').removeClass('current'); $('.leaflet-levels .current').removeClass('current');
$('.leaflet-levels a[name='+level_name+']').addClass('current'); $('.leaflet-levels a[name='+level_name+']').addClass('current');
$('.feature_level_list').hide();
$('.feature_level_list[data-level='+level_name+']').show();
}, },
_creating: null, _creating: null,
_editing: null, _editing: null,
init_drawing: function () { init_features: function () {
// Add drawing new features // Add drawing new features
L.DrawControl = L.Control.extend({ L.DrawControl = L.Control.extend({
options: { options: {
@ -150,29 +158,38 @@ editor = {
}); });
editor.map.addControl(new L.DrawControl()); editor.map.addControl(new L.DrawControl());
$('#mapeditlist').on('click', '.start-drawing', function () { $('#mapeditlist').on('click', '.start-drawing', editor._click_start_drawing)
editor.start_creating($(this).closest('fieldset').attr('name')); .on('mouseenter', '.feature_level_list li', editor._hover_feature_detail)
}); .on('mouseleave', '.feature_level_list li', editor._unhover_feature_detail)
.on('click', '.feature_level_list li', editor._click_feature_detail);
editor.map.on('editable:drawing:commit', editor.done_creating); editor.map.on('editable:drawing:commit', editor.done_creating);
editor.map.on('editable:editing', editor.update_editing); editor.map.on('editable:editing', editor.update_editing);
editor.map.on('editable:drawing:cancel', editor._canceled_creating); editor.map.on('editable:drawing:cancel', editor._canceled_creating);
$('#mapeditdetail').on('click', '#btn_editing_cancel', editor.cancel_editing) $('#mapeditdetail').on('click', '#btn_editing_cancel', editor.cancel_editing)
.on('submit', 'form', editor.submit_editing); .on('click', 'button[type=submit]', editor.submit_editing_btn_click)
.on('submit', 'form', editor.submit_editing)
editor.get_features(); editor.get_features();
}, },
features: {},
get_features: function () { get_features: function () {
$.getJSON('/api/v1/features/', function(features) { $.getJSON('/api/v1/features/', function(features) {
for (level in editor.level_layers) { var feature_type;
editor.level_layers[level].clearLayers(); for (var level in editor.levels) {
for (var j = 0; j < editor.feature_types_order.length; j++) {
feature_type = editor.feature_types_order[j];
editor.level_feature_layers[level][feature_type].clearLayers();
}
} }
var feature; $('.feature_level_list li').remove();
var feature, layergroup;
for (var i=0; i < features.length; i++) { for (var i=0; i < features.length; i++) {
feature = features[i]; feature = features[i];
L.geoJSON({ layergroup = L.geoJSON({
type: 'Feature', type: 'Feature',
geometry: feature.geometry, geometry: feature.geometry,
properties: { properties: {
@ -181,20 +198,86 @@ editor = {
} }
}, { }, {
style: editor._get_feature_style style: editor._get_feature_style
}).addTo(editor.level_feature_layers[feature.level][feature.feature_type]); }).on('mouseover', editor._hover_feature_layer)
.on('mouseout', editor._unhover_feature_layer)
.on('click', editor._click_feature_layer)
.addTo(editor.level_feature_layers[feature.level][feature.feature_type]);
feature.layer = layergroup.getLayers()[0];
editor.features[feature.name] = feature;
$('.feature_list[name='+feature.feature_type+'] > [data-level='+feature.level+']').append(
$('<li>').attr('name', feature.name).append(
$('<p>').text(feature.title).append(' ').append(
$('<em>').text(feature.name)
)
)
);
} }
$('.start-drawing').show(); $('.start-drawing').show();
$('#mapeditcontrols').addClass('list'); $('#mapeditcontrols').addClass('list');
editor.set_current_level(editor._level); editor.set_current_level(editor._level);
}); });
}, },
_get_feature_style: function (feature) { _get_feature_style: function (feature) {
return editor.feature_types[feature.properties.feature_type]; return editor.feature_types[feature.properties.feature_type];
}, },
_click_start_drawing: function (e) {
editor.start_creating($(this).closest('fieldset').attr('name'));
},
_hover_feature_detail: function (e) {
editor._highlight_layer.clearLayers();
L.geoJSON(editor.features[$(this).attr('name')].geometry, {
style: function() {
return {
color: '#FFFFEE',
opacity: 0.5,
fillOpacity: 0.5,
className: 'c3nav-highlight'
};
}
}).addTo(editor._highlight_layer);
},
_unhover_feature_detail: function () {
editor._highlight_layer.clearLayers();
},
_click_feature_detail: function() {
editor.start_editing($(this).attr('name'));
},
_hover_feature_layer: function (e) {
editor._unhover_feature_layer();
if (editor._editing === null && editor._creating === null) {
editor._highlight_layer.clearLayers();
L.geoJSON(e.layer.toGeoJSON(), {
style: function() {
return {
color: '#FFFFEE',
opacity: 0.5,
fillOpacity: 0.5,
className: 'c3nav-highlight'
};
}
}).addTo(editor._highlight_layer);
}
$('.feature_list li[name='+e.layer.feature.properties.name+']').addClass('hover');
},
_unhover_feature_layer: function (e) {
editor._highlight_layer.clearLayers();
$('.feature_list .hover').removeClass('hover');
},
_click_feature_layer: function(e) {
editor.start_editing(e.layer.feature.properties.name);
if ((e.originalEvent.ctrlKey || e.originalEvent.metaKey) && this.editEnabled()) {
if (e.layer.feature.properties.geomtype == 'polygon') {
this.editor.newHole(e.latlng);
}
}
},
start_creating: function (feature_type) { start_creating: function (feature_type) {
if (editor._creating !== null || editor._editing !== null) return; if (editor._creating !== null || editor._editing !== null) return;
editor._highlight_layer.clearLayers();
editor._creating = feature_type; editor._creating = feature_type;
var options = editor.feature_types[feature_type]; var options = editor.feature_types[feature_type];
if (options.geomtype == 'polygon') { if (options.geomtype == 'polygon') {
@ -230,8 +313,13 @@ editor = {
} }
}, },
start_editing: function () { start_editing: function (name) {
// todo if (editor._creating !== null || editor._editing !== null) return;
editor._highlight_layer.clearLayers();
editor._editing = editor.features[name].layer;
var path = '/editor/features/edit/' + name + '/';
$('#mapeditcontrols').removeClass('list');
$('#mapeditdetail').load(path, editor.edit_form_loaded);
}, },
edit_form_loaded: function() { edit_form_loaded: function() {
$('#mapeditcontrols').addClass('detail'); $('#mapeditcontrols').addClass('detail');
@ -263,9 +351,18 @@ editor = {
editor.get_features(); editor.get_features();
} }
}, },
submit_editing: function(e) { submit_editing_btn_click: function(e) {
e.preventDefault();
$(this).closest('form').trigger('submit', $(this));
},
submit_editing: function(e, btn) {
e.preventDefault(); e.preventDefault();
var data = $(this).serialize(); var data = $(this).serialize();
console.log($(btn));
console.log($(btn).is('[name]'));
if (btn !== undefined && $(btn).is('[name]')) {
data += '&'+$('<input>').attr('name', $(btn).attr('name')).val($(btn).val()).serialize();
}
var action = $(this).attr('action'); var action = $(this).attr('action');
$('#mapeditcontrols').removeClass('detail'); $('#mapeditcontrols').removeClass('detail');
$('#mapeditdetail').html(''); $('#mapeditdetail').html('');

View file

@ -5,11 +5,16 @@
{% csrf_token %} {% csrf_token %}
{% bootstrap_form form %} {% bootstrap_form form %}
{% buttons %} {% buttons %}
<button type="button" id="btn_editing_cancel" class="btn btn-danger"> {% if not new %}
Cancel <button type="submit" name="delete" value="1" class="btn btn-danger">
</button> Delete
</button>
{% endif %}
<button type="submit" class="btn btn-primary pull-right"> <button type="submit" class="btn btn-primary pull-right">
Submit Submit
</button> </button>
<button type="button" id="btn_editing_cancel" class="btn {% if new %}btn-danger{% else %}btn-default pull-right{% endif %}">
Cancel
</button>
{% endbuttons %} {% endbuttons %}
</form> </form>

View file

@ -0,0 +1,17 @@
{% load bootstrap3 %}
<h3>Delete {{ feature_type.title }}</h3>
<form action="{{ path }}" method="post">
{% csrf_token %}
<p>Please confirm deleting: {{ name }}</p>
<input type="hidden" name="delete" value="1">
<input type="hidden" name="name" value="{{ name }}">
{% buttons %}
<button type="button" id="btn_editing_cancel" class="btn btn-default">
Cancel
</button>
<button type="submit" name="delete_confirm" value="1" class="btn btn-danger pull-right">
Delete
</button>
{% endbuttons %}
</form>

View file

@ -1,9 +1,10 @@
from django.conf.urls import url from django.conf.urls import url
from django.views.generic import TemplateView from django.views.generic import TemplateView
from c3nav.editor.views import add_feature from c3nav.editor.views import add_feature, edit_feature
urlpatterns = [ urlpatterns = [
url(r'^$', TemplateView.as_view(template_name='editor/map.html'), name='editor.index'), url(r'^$', TemplateView.as_view(template_name='editor/map.html'), name='editor.index'),
url(r'^features/(?P<feature_type>[^/]+)/add/$', add_feature, name='editor.feature.add') url(r'^features/(?P<feature_type>[^/]+)/add/$', add_feature, name='editor.feature.add'),
url(r'^features/edit/(?P<name>[^/]+)/$', edit_feature, name='editor.feature.edit')
] ]

View file

@ -1,10 +1,12 @@
from django.conf import settings from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.db import transaction from django.db import transaction
from django.http.response import Http404 from django.http.response import Http404
from django.shortcuts import render from django.shortcuts import get_object_or_404, render
from c3nav.editor.forms import FeatureForm from c3nav.editor.forms import FeatureForm
from c3nav.mapdata.models.feature import FEATURE_TYPES from c3nav.mapdata.models.feature import FEATURE_TYPES, Feature
from c3nav.mapdata.permissions import can_access_package
def add_feature(request, feature_type): def add_feature(request, feature_type):
@ -37,3 +39,47 @@ def add_feature(request, feature_type):
'path': request.path, 'path': request.path,
'new': True 'new': True
}) })
def edit_feature(request, name):
feature = get_object_or_404(Feature, name=name)
if not can_access_package(feature.package):
raise PermissionDenied
feature_type = FEATURE_TYPES.get(feature.feature_type)
if request.method == 'POST':
if request.POST.get('delete') == '1':
if request.POST.get('delete_confirm') == '1':
feature.delete()
return render(request, 'editor/feature_success.html', {})
return render(request, 'editor/feature_delete.html', {
'name': feature.name,
'feature_type': feature_type,
'path': request.path
})
form = FeatureForm(instance=feature, data=request.POST, feature_type=feature_type)
if form.is_valid():
if not settings.DIRECT_EDITING:
return render(request, 'editor/feature_success.html', {})
with transaction.atomic():
feature = form.instance
feature.feature_type = feature_type.name
feature.titles = {}
for language, title in form.titles.items():
if title:
feature.titles[language] = title
feature.save()
return render(request, 'editor/feature_success.html', {})
else:
form = FeatureForm(instance=feature, feature_type=feature_type)
return render(request, 'editor/feature.html', {
'form': form,
'feature_type': feature_type,
'path': request.path,
'new': False
})

View file

@ -4,7 +4,6 @@ import os
from django.conf import settings from django.conf import settings
from django.core.files import File from django.core.files import File
from django.http import Http404, HttpResponse from django.http import Http404, HttpResponse
from rest_framework.decorators import detail_route from rest_framework.decorators import detail_route
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet

View file

@ -7,6 +7,7 @@ from django.utils.translation import get_language
from shapely.geometry import mapping, shape from shapely.geometry import mapping, shape
from c3nav.mapdata.utils import sort_geojson from c3nav.mapdata.utils import sort_geojson
from ..fields import GeometryField, JSONField from ..fields import GeometryField, JSONField
@ -35,7 +36,7 @@ class Feature(models.Model):
""" """
TYPES = tuple((name, t.title) for name, t in FEATURE_TYPES.items()) TYPES = tuple((name, t.title) for name, t in FEATURE_TYPES.items())
name = models.SlugField(_('feature identifier'), primary_key=True, max_length=50, help_text=_('e.g. noc')) name = models.SlugField(_('feature identifier'), primary_key=True, max_length=50)
package = models.ForeignKey('mapdata.Package', on_delete=models.CASCADE, related_name='features', package = models.ForeignKey('mapdata.Package', on_delete=models.CASCADE, related_name='features',
verbose_name=_('map package')) verbose_name=_('map package'))
feature_type = models.CharField(max_length=50, choices=TYPES) feature_type = models.CharField(max_length=50, choices=TYPES)

View file

@ -1,4 +1,3 @@
from ..models import Feature, Level, Package, Source from ..models import Feature, Level, Package, Source
ordered_models = (Package, Level, Source, Feature) ordered_models = (Package, Level, Source, Feature)

View file

@ -1,6 +1,5 @@
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import BasePermission from rest_framework.permissions import BasePermission