multi-language feature titles

This commit is contained in:
Laura Klünder 2016-09-23 17:02:17 +02:00
parent 9f79e26671
commit 3c96a98344
11 changed files with 101 additions and 44 deletions

View file

@ -1,4 +1,5 @@
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,4 +1,5 @@
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

@ -1,5 +1,9 @@
from django.forms import ModelForm from collections import OrderedDict
from django.conf import settings
from django.forms import CharField, ModelForm, ValidationError
from django.forms.widgets import HiddenInput from django.forms.widgets import HiddenInput
from django.utils.translation import ugettext_lazy as _
from ..mapdata.models import Feature from ..mapdata.models import Feature
@ -11,6 +15,30 @@ class FeatureForm(ModelForm):
self.fields['level'].widget = HiddenInput() self.fields['level'].widget = HiddenInput()
self.fields['geometry'].widget = HiddenInput() self.fields['geometry'].widget = HiddenInput()
titles = OrderedDict((lang_code, '') for lang_code, language in settings.LANGUAGES)
if self.instance is not None and self.instance.pk:
titles.update(self.instance.titles)
language_titles = dict(settings.LANGUAGES)
for language in titles.keys():
new_title = self.data.get('title_' + language)
if new_title is not None:
titles[language] = new_title
self.fields['title_' + language] = CharField(label=language_titles.get(language, language), required=False,
initial=titles[language].strip(), max_length=50)
self.titles = titles
def clean(self):
super().clean()
if not any(self.titles.values()):
raise ValidationError(
_('You have to select a title in at least one language.')
)
def get_languages(self):
pass
class Meta: class Meta:
# generate extra fields in the number specified via extra_fields
model = Feature model = Feature
fields = ['name', 'package', 'level', 'geometry'] fields = ['name', 'package', 'level', 'geometry']

View file

@ -1,7 +1,7 @@
editor = { editor = {
feature_types: {}, feature_types: {},
init: function() { init: function () {
// Init Map // Init Map
editor.map = L.map('map', { editor.map = L.map('map', {
zoom: 2, zoom: 2,
@ -19,7 +19,7 @@ editor = {
editor.get_sources(); editor.get_sources();
editor.get_levels(); editor.get_levels();
$('#mapeditdetail').on('click', '#btn_abort', function() { $('#mapeditdetail').on('click', '#btn_abort', function () {
if (editor._adding !== null) { if (editor._adding !== null) {
editor._adding.remove(); editor._adding.remove();
editor._adding = null; editor._adding = null;
@ -28,13 +28,13 @@ editor = {
$('#mapeditdetail').html(''); $('#mapeditdetail').html('');
$('.start-drawing').prop('disabled', false); $('.start-drawing').prop('disabled', false);
} }
}).on('submit', 'form', function(e) { }).on('submit', 'form', function (e) {
e.preventDefault(); e.preventDefault();
var data = $(this).serialize(); var data = $(this).serialize();
var action = $(this).attr('action'); var action = $(this).attr('action');
$('#mapeditcontrols').removeClass('detail'); $('#mapeditcontrols').removeClass('detail');
$('#mapeditdetail').html(''); $('#mapeditdetail').html('');
$.post(action, data, function(data) { $.post(action, data, function (data) {
var content = $(data); var content = $(data);
if ($('<div>').append(content).find('form').length > 0) { if ($('<div>').append(content).find('form').length > 0) {
$('#mapeditdetail').html(content); $('#mapeditdetail').html(content);
@ -49,11 +49,11 @@ editor = {
}); });
}, },
get_feature_types: function() { get_feature_types: function () {
$.getJSON('/api/v1/featuretypes/', function(feature_types) { $.getJSON('/api/v1/featuretypes/', function (feature_types) {
var feature_type; var feature_type;
var editcontrols = $('#mapeditlist'); var editcontrols = $('#mapeditlist');
for(var i=0;i<feature_types.length;i++) { for (var i = 0; i < feature_types.length; i++) {
feature_type = feature_types[i]; feature_type = feature_types[i];
editor.feature_types[feature_type.name] = feature_type; editor.feature_types[feature_type.name] = feature_type;
editcontrols.append( editcontrols.append(
@ -68,16 +68,16 @@ editor = {
}, },
packages: {}, packages: {},
get_packages: function() { get_packages: function () {
$.getJSON('/api/v1/packages/', function(packages) { $.getJSON('/api/v1/packages/', function (packages) {
var bounds = [[0, 0], [0, 0]]; var bounds = [[0, 0], [0, 0]];
var pkg; var pkg;
for(var i=0;i<packages.length;i++) { for (var i = 0; i < packages.length; i++) {
pkg = packages[i]; pkg = packages[i];
editor.packages[pkg.name] = pkg; editor.packages[pkg.name] = pkg;
if (pkg.bounds === null) continue; if (pkg.bounds === null) continue;
bounds = [[Math.min(bounds[0][0], pkg.bounds[0][0]), Math.min(bounds[0][1], pkg.bounds[0][1])], bounds = [[Math.min(bounds[0][0], pkg.bounds[0][0]), Math.min(bounds[0][1], pkg.bounds[0][1])],
[Math.max(bounds[1][0], pkg.bounds[1][0]), Math.max(bounds[1][1], pkg.bounds[1][1])]]; [Math.max(bounds[1][0], pkg.bounds[1][0]), Math.max(bounds[1][1], pkg.bounds[1][1])]];
} }
editor.map.setMaxBounds(bounds); editor.map.setMaxBounds(bounds);
editor.map.fitBounds(bounds, {padding: [30, 50]}); editor.map.fitBounds(bounds, {padding: [30, 50]});
@ -85,14 +85,14 @@ editor = {
}, },
sources: {}, sources: {},
get_sources: function() { get_sources: function () {
$.getJSON('/api/v1/sources/', function(sources) { $.getJSON('/api/v1/sources/', function (sources) {
var layers = {}; var layers = {};
var source; var source;
for(var i=0;i<sources.length;i++) { for (var i = 0; i < sources.length; i++) {
source = sources[i]; source = sources[i];
editor.sources[source.name] = source; editor.sources[source.name] = source;
source.layer = L.imageOverlay('/api/v1/sources/'+source.name+'/image/', source.bounds); source.layer = L.imageOverlay('/api/v1/sources/' + source.name + '/image/', source.bounds);
layers[source.name] = source.layer; layers[source.name] = source.layer;
} }
L.control.layers([], layers).addTo(editor.map); L.control.layers([], layers).addTo(editor.map);
@ -102,8 +102,8 @@ editor = {
level_layers: {}, level_layers: {},
levels: {}, levels: {},
_level: null, _level: null,
get_levels: function() { get_levels: function () {
$.getJSON('/api/v1/levels/?ordering=-altitude', function(levels) { $.getJSON('/api/v1/levels/?ordering=-altitude', function (levels) {
L.LevelControl = L.Control.extend({ L.LevelControl = L.Control.extend({
options: { options: {
position: 'bottomright' position: 'bottomright'
@ -111,9 +111,9 @@ editor = {
onAdd: function () { onAdd: function () {
var container = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-levels'), link; var container = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-levels'), link;
var level; var level;
for(var i=0;i<levels.length;i++) { for (var i = 0; i < levels.length; i++) {
level = levels[i]; level = levels[i];
link = L.DomUtil.create('a', (i == levels.length-1) ? 'current' : '', container); link = L.DomUtil.create('a', (i == levels.length - 1) ? 'current' : '', container);
link.name = level.name; link.name = level.name;
link.innerHTML = level.name; link.innerHTML = level.name;
link.href = ''; link.href = '';
@ -123,7 +123,7 @@ editor = {
}); });
editor.map.addControl(new L.LevelControl()); editor.map.addControl(new L.LevelControl());
$('.leaflet-levels').on('click', 'a', function(e) { $('.leaflet-levels').on('click', 'a', function (e) {
e.preventDefault(); e.preventDefault();
if (editor._drawing !== null || editor._adding !== null) return; if (editor._drawing !== null || editor._adding !== null) return;
editor.level_layers[editor._level].remove(); editor.level_layers[editor._level].remove();
@ -134,19 +134,19 @@ editor = {
}); });
var level; var level;
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_layers[level.name] = L.layerGroup(); editor.level_layers[level.name] = L.layerGroup();
} }
editor.init_drawing(); editor.init_drawing();
editor._level = levels[levels.length-1].name; editor._level = levels[levels.length - 1].name;
editor.level_layers[editor._level].addTo(editor.map); editor.level_layers[editor._level].addTo(editor.map);
}); });
}, },
init_drawing: function() { init_drawing: function () {
// Add drawing new features // Add drawing new features
editor._drawing = null; editor._drawing = null;
editor._adding = null; editor._adding = null;
@ -155,13 +155,13 @@ editor = {
options: { options: {
position: 'topleft' position: 'topleft'
}, },
onAdd: function() { onAdd: function () {
var container = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-drawbar'); var container = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-drawbar');
$('<a href="#" id="drawcancel">').appendTo(container).text('cancel').attr({ $('<a href="#" id="drawcancel">').appendTo(container).text('cancel').attr({
href: '#', href: '#',
title: 'cancel drawing', title: 'cancel drawing',
name: '' name: ''
}).on('click', function(e) { }).on('click', function (e) {
e.preventDefault(); e.preventDefault();
editor.cancel_drawing(); editor.cancel_drawing();
}); });
@ -170,7 +170,7 @@ editor = {
}); });
editor.map.addControl(new L.DrawControl()); editor.map.addControl(new L.DrawControl());
$('#mapeditlist').on('click', '.start-drawing', function() { $('#mapeditlist').on('click', '.start-drawing', function () {
console.log($(this).closest('fieldset')); console.log($(this).closest('fieldset'));
editor.start_drawing($(this).closest('fieldset').attr('name')); editor.start_drawing($(this).closest('fieldset').attr('name'));
}); });
@ -180,9 +180,9 @@ editor = {
e.layer.disableEdit(); e.layer.disableEdit();
$('.leaflet-drawbar').hide(); $('.leaflet-drawbar').hide();
var path = '/editor/features/'+editor._drawing+'/add'; var path = '/editor/features/' + editor._drawing + '/add';
$('#mapeditcontrols').removeClass('list'); $('#mapeditcontrols').removeClass('list');
$('#mapeditdetail').html('<img src="/static/img/loader.gif">').load(path, function() { $('#mapeditdetail').load(path, function () {
$('#mapeditcontrols').addClass('detail'); $('#mapeditcontrols').addClass('detail');
$('#id_level').val(editor._level); $('#id_level').val(editor._level);
$('#id_geometry').val(JSON.stringify(editor._adding.toGeoJSON().geometry)); $('#id_geometry').val(JSON.stringify(editor._adding.toGeoJSON().geometry));
@ -196,7 +196,7 @@ editor = {
}); });
}, },
start_drawing: function(feature_type) { start_drawing: function (feature_type) {
if (editor._drawing !== null || editor._adding !== null) return; if (editor._drawing !== null || editor._adding !== null) return;
editor._drawing = feature_type; editor._drawing = feature_type;
var options = editor.feature_types[feature_type]; var options = editor.feature_types[feature_type];
@ -208,7 +208,7 @@ editor = {
$('.leaflet-drawbar').show(); $('.leaflet-drawbar').show();
$('.start-drawing').prop('disabled', true); $('.start-drawing').prop('disabled', true);
}, },
cancel_drawing: function() { cancel_drawing: function () {
if (editor._drawing === null || editor._adding !== null) return; if (editor._drawing === null || editor._adding !== null) return;
editor.map.editTools.stopDrawing(); editor.map.editTools.stopDrawing();
editor._drawing = null; editor._drawing = null;

View file

@ -1,10 +1,10 @@
from django.conf import settings
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 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
from c3nav.settings import DIRECT_EDITING
def add_feature(request, feature_type): def add_feature(request, feature_type):
@ -15,13 +15,20 @@ def add_feature(request, feature_type):
if request.method == 'POST': if request.method == 'POST':
form = FeatureForm(request.POST, feature_type=feature_type) form = FeatureForm(request.POST, feature_type=feature_type)
if form.is_valid(): if form.is_valid():
if not DIRECT_EDITING: if not settings.DIRECT_EDITING:
return render(request, 'editor/feature_success.html', {}) return render(request, 'editor/feature_success.html', {})
with transaction.atomic(): with transaction.atomic():
feature = form.instance feature = form.instance
feature.feature_type = feature_type.name feature.feature_type = feature_type.name
feature.save() feature.save()
for language, title in form.titles.items():
if title:
feature.featuretitles.update_or_create(language=language, defaults={'title': title})
else:
feature.featuretitles.filter(language=language).delete()
return render(request, 'editor/feature_success.html', {}) return render(request, 'editor/feature_success.html', {})
else: else:
form = FeatureForm(feature_type=feature_type) form = FeatureForm(feature_type=feature_type)

View file

@ -4,9 +4,10 @@ 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 ModelViewSet, ReadOnlyModelViewSet, ViewSet from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
from .cache import AccessCachedViewSetMixin, CachedViewSetMixin from .cache import AccessCachedViewSetMixin, CachedViewSetMixin
from .models import FEATURE_TYPES, Feature, Level, Package, Source from .models import FEATURE_TYPES, Feature, Level, Package, Source
@ -70,6 +71,7 @@ class FeatureTypeViewSet(ViewSet):
""" """
Get Feature types Get Feature types
""" """
def list(self, request, version=None): def list(self, request, version=None):
serializer = FeatureTypeSerializer(FEATURE_TYPES.values(), many=True, context={'request': request}) serializer = FeatureTypeSerializer(FEATURE_TYPES.values(), many=True, context={'request': request})
return Response(serializer.data) return Response(serializer.data)
@ -81,13 +83,10 @@ class FeatureTypeViewSet(ViewSet):
return Response(serializer.data) return Response(serializer.data)
ParentModelViewSet = ModelViewSet if settings.DIRECT_EDITING else ReadOnlyModelViewSet class FeatureViewSet(ReadOnlyModelViewSet):
class FeatureViewSet(ParentModelViewSet):
""" """
Get all Map Features including ones that are only part of the current session Get all Map Features including ones that are only part of the current session
""" """
queryset = Feature.objects.all() queryset = Feature.objects.all().prefetch_related('featuretitles')
serializer_class = FeatureSerializer serializer_class = FeatureSerializer
lookup_value_regex = '[^/]+' lookup_value_regex = '[^/]+'

View file

@ -40,4 +40,4 @@ class CachedViewSetMixin:
class AccessCachedViewSetMixin(CachedViewSetMixin): class AccessCachedViewSetMixin(CachedViewSetMixin):
def get_cache_params(self, request): def get_cache_params(self, request):
return super().get_cache_params(request)+'___'+'___'.join(get_unlocked_packages(request)) return super().get_cache_params(request) + '___' + '___'.join(get_unlocked_packages(request))

View file

@ -2,9 +2,11 @@ from collections import OrderedDict, namedtuple
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import get_language
from c3nav.mapdata.models.level import Level from c3nav.mapdata.models.level import Level
from c3nav.mapdata.models.package import Package from c3nav.mapdata.models.package import Package
from ..fields import GeometryField from ..fields import GeometryField
@ -12,11 +14,14 @@ class FeatureType(namedtuple('FeatureType', ('name', 'title', 'title_plural', 'g
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
FEATURE_TYPES[self.name] = self FEATURE_TYPES[self.name] = self
FEATURE_TYPES = OrderedDict() FEATURE_TYPES = OrderedDict()
FeatureType('building', _('Building'), _('Buildings'), 'polygon', '#333333') FeatureType('building', _('Building'), _('Buildings'), 'polygon', '#333333')
FeatureType('room', _('Room'), _('Rooms'), 'polygon', '#CCCCCC') FeatureType('room', _('Room'), _('Rooms'), 'polygon', '#CCCCCC')
FeatureType('outside', _('Outside Area'), _('Outside Areas'), 'polygon', '#EEEEEE') FeatureType('outside', _('Outside Area'), _('Outside Areas'), 'polygon', '#EEEEEE')
FeatureType('obstacle', _('Obstacle'), _('Obstacles'), 'polygon', '#999999') FeatureType('obstacle', _('Obstacle'), _('Obstacles'), 'polygon', '#999999')
# FeatureType('door', _('Door'), 'polygon', '#FF00FF') # FeatureType('door', _('Door'), 'polygon', '#FF00FF')
# FeatureType('step', _('Step'), 'polyline', '#FF0000') # FeatureType('step', _('Step'), 'polyline', '#FF0000')
# FeatureType('elevator', _('Elevator'), 'polygon', '#99CC00') # FeatureType('elevator', _('Elevator'), 'polygon', '#99CC00')
@ -36,9 +41,21 @@ class Feature(models.Model):
verbose_name=_('level')) verbose_name=_('level'))
geometry = GeometryField() geometry = GeometryField()
@property
def titles(self):
return {title.language: title.title for title in self.featuretitles.all()}
@property
def title(self):
titles = self.titles
lang = get_language()
if lang in titles:
return titles[lang]
return next(iter(titles.values())) if titles else self.name
class FeatureTitle(models.Model): class FeatureTitle(models.Model):
feature = models.ForeignKey('Feature', on_delete=models.CASCADE, related_name='titles', feature = models.ForeignKey('Feature', on_delete=models.CASCADE, related_name='featuretitles',
verbose_name=_('map package')) verbose_name=_('map package'))
language = models.CharField(max_length=50) language = models.CharField(max_length=50)
title = models.CharField(max_length=50) title = models.CharField(max_length=50)

View file

@ -1,5 +1,6 @@
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

View file

@ -37,7 +37,7 @@ class PackageSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Package model = Package
fields = ('name', 'home_repo', 'commit_id', 'depends', 'bounds', 'public') fields = ('name', 'home_repo', 'commit_id', 'depends', 'bounds', 'public')
readonly_fields = ('commit_id', ) readonly_fields = ('commit_id',)
class SourceSerializer(serializers.ModelSerializer): class SourceSerializer(serializers.ModelSerializer):
@ -59,4 +59,4 @@ class FeatureSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Feature model = Feature
fields = ('name', 'package', 'level', 'feature_type', 'geometry') fields = ('name', 'title', 'feature_type', 'level', 'titles', 'package', 'geometry')

View file

@ -112,7 +112,6 @@ LANGUAGE_COOKIE_NAME = 'c3nav_language'
CSRF_COOKIE_NAME = 'c3nav_csrftoken' CSRF_COOKIE_NAME = 'c3nav_csrftoken'
SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_HTTPONLY = True
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
@ -222,6 +221,10 @@ STATICFILES_FINDERS = (
'compressor.finders.CompressorFinder', 'compressor.finders.CompressorFinder',
) )
BOOTSTRAP3 = {
'success_css_class': '',
}
STATICFILES_DIRS = ( STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'), os.path.join(BASE_DIR, 'static'),
) )