multi-language feature titles
This commit is contained in:
parent
9f79e26671
commit
3c96a98344
11 changed files with 101 additions and 44 deletions
|
@ -1,4 +1,5 @@
|
|||
from django.conf.urls import include, url
|
||||
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from ..editor import api as editor_api
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from django.http import Http404
|
||||
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ViewSet
|
||||
|
||||
|
|
|
@ -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.utils.translation import ugettext_lazy as _
|
||||
|
||||
from ..mapdata.models import Feature
|
||||
|
||||
|
@ -11,6 +15,30 @@ class FeatureForm(ModelForm):
|
|||
self.fields['level'].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:
|
||||
# generate extra fields in the number specified via extra_fields
|
||||
model = Feature
|
||||
fields = ['name', 'package', 'level', 'geometry']
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
editor = {
|
||||
feature_types: {},
|
||||
|
||||
init: function() {
|
||||
init: function () {
|
||||
// Init Map
|
||||
editor.map = L.map('map', {
|
||||
zoom: 2,
|
||||
|
@ -19,7 +19,7 @@ editor = {
|
|||
editor.get_sources();
|
||||
editor.get_levels();
|
||||
|
||||
$('#mapeditdetail').on('click', '#btn_abort', function() {
|
||||
$('#mapeditdetail').on('click', '#btn_abort', function () {
|
||||
if (editor._adding !== null) {
|
||||
editor._adding.remove();
|
||||
editor._adding = null;
|
||||
|
@ -28,13 +28,13 @@ editor = {
|
|||
$('#mapeditdetail').html('');
|
||||
$('.start-drawing').prop('disabled', false);
|
||||
}
|
||||
}).on('submit', 'form', function(e) {
|
||||
}).on('submit', 'form', function (e) {
|
||||
e.preventDefault();
|
||||
var data = $(this).serialize();
|
||||
var action = $(this).attr('action');
|
||||
$('#mapeditcontrols').removeClass('detail');
|
||||
$('#mapeditdetail').html('');
|
||||
$.post(action, data, function(data) {
|
||||
$.post(action, data, function (data) {
|
||||
var content = $(data);
|
||||
if ($('<div>').append(content).find('form').length > 0) {
|
||||
$('#mapeditdetail').html(content);
|
||||
|
@ -49,11 +49,11 @@ editor = {
|
|||
});
|
||||
},
|
||||
|
||||
get_feature_types: function() {
|
||||
$.getJSON('/api/v1/featuretypes/', function(feature_types) {
|
||||
get_feature_types: function () {
|
||||
$.getJSON('/api/v1/featuretypes/', function (feature_types) {
|
||||
var feature_type;
|
||||
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];
|
||||
editor.feature_types[feature_type.name] = feature_type;
|
||||
editcontrols.append(
|
||||
|
@ -68,16 +68,16 @@ editor = {
|
|||
},
|
||||
|
||||
packages: {},
|
||||
get_packages: function() {
|
||||
$.getJSON('/api/v1/packages/', function(packages) {
|
||||
get_packages: function () {
|
||||
$.getJSON('/api/v1/packages/', function (packages) {
|
||||
var bounds = [[0, 0], [0, 0]];
|
||||
var pkg;
|
||||
for(var i=0;i<packages.length;i++) {
|
||||
for (var i = 0; i < packages.length; i++) {
|
||||
pkg = packages[i];
|
||||
editor.packages[pkg.name] = pkg;
|
||||
if (pkg.bounds === null) continue;
|
||||
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.fitBounds(bounds, {padding: [30, 50]});
|
||||
|
@ -85,14 +85,14 @@ editor = {
|
|||
},
|
||||
|
||||
sources: {},
|
||||
get_sources: function() {
|
||||
$.getJSON('/api/v1/sources/', function(sources) {
|
||||
get_sources: function () {
|
||||
$.getJSON('/api/v1/sources/', function (sources) {
|
||||
var layers = {};
|
||||
var source;
|
||||
for(var i=0;i<sources.length;i++) {
|
||||
for (var i = 0; i < sources.length; i++) {
|
||||
source = sources[i];
|
||||
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;
|
||||
}
|
||||
L.control.layers([], layers).addTo(editor.map);
|
||||
|
@ -102,8 +102,8 @@ editor = {
|
|||
level_layers: {},
|
||||
levels: {},
|
||||
_level: null,
|
||||
get_levels: function() {
|
||||
$.getJSON('/api/v1/levels/?ordering=-altitude', function(levels) {
|
||||
get_levels: function () {
|
||||
$.getJSON('/api/v1/levels/?ordering=-altitude', function (levels) {
|
||||
L.LevelControl = L.Control.extend({
|
||||
options: {
|
||||
position: 'bottomright'
|
||||
|
@ -111,9 +111,9 @@ editor = {
|
|||
onAdd: function () {
|
||||
var container = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-levels'), link;
|
||||
var level;
|
||||
for(var i=0;i<levels.length;i++) {
|
||||
for (var i = 0; i < levels.length; 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.innerHTML = level.name;
|
||||
link.href = '';
|
||||
|
@ -123,7 +123,7 @@ editor = {
|
|||
});
|
||||
editor.map.addControl(new L.LevelControl());
|
||||
|
||||
$('.leaflet-levels').on('click', 'a', function(e) {
|
||||
$('.leaflet-levels').on('click', 'a', function (e) {
|
||||
e.preventDefault();
|
||||
if (editor._drawing !== null || editor._adding !== null) return;
|
||||
editor.level_layers[editor._level].remove();
|
||||
|
@ -134,19 +134,19 @@ editor = {
|
|||
});
|
||||
|
||||
var level;
|
||||
for(var i=0;i<levels.length;i++) {
|
||||
for (var i = 0; i < levels.length; i++) {
|
||||
level = levels[i];
|
||||
editor.levels[level.name] = level;
|
||||
editor.level_layers[level.name] = L.layerGroup();
|
||||
}
|
||||
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);
|
||||
});
|
||||
},
|
||||
|
||||
init_drawing: function() {
|
||||
init_drawing: function () {
|
||||
// Add drawing new features
|
||||
editor._drawing = null;
|
||||
editor._adding = null;
|
||||
|
@ -155,13 +155,13 @@ editor = {
|
|||
options: {
|
||||
position: 'topleft'
|
||||
},
|
||||
onAdd: function() {
|
||||
onAdd: function () {
|
||||
var container = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-drawbar');
|
||||
$('<a href="#" id="drawcancel">').appendTo(container).text('cancel').attr({
|
||||
href: '#',
|
||||
title: 'cancel drawing',
|
||||
name: ''
|
||||
}).on('click', function(e) {
|
||||
}).on('click', function (e) {
|
||||
e.preventDefault();
|
||||
editor.cancel_drawing();
|
||||
});
|
||||
|
@ -170,7 +170,7 @@ editor = {
|
|||
});
|
||||
editor.map.addControl(new L.DrawControl());
|
||||
|
||||
$('#mapeditlist').on('click', '.start-drawing', function() {
|
||||
$('#mapeditlist').on('click', '.start-drawing', function () {
|
||||
console.log($(this).closest('fieldset'));
|
||||
editor.start_drawing($(this).closest('fieldset').attr('name'));
|
||||
});
|
||||
|
@ -180,9 +180,9 @@ editor = {
|
|||
|
||||
e.layer.disableEdit();
|
||||
$('.leaflet-drawbar').hide();
|
||||
var path = '/editor/features/'+editor._drawing+'/add';
|
||||
var path = '/editor/features/' + editor._drawing + '/add';
|
||||
$('#mapeditcontrols').removeClass('list');
|
||||
$('#mapeditdetail').html('<img src="/static/img/loader.gif">').load(path, function() {
|
||||
$('#mapeditdetail').load(path, function () {
|
||||
$('#mapeditcontrols').addClass('detail');
|
||||
$('#id_level').val(editor._level);
|
||||
$('#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;
|
||||
editor._drawing = feature_type;
|
||||
var options = editor.feature_types[feature_type];
|
||||
|
@ -208,7 +208,7 @@ editor = {
|
|||
$('.leaflet-drawbar').show();
|
||||
$('.start-drawing').prop('disabled', true);
|
||||
},
|
||||
cancel_drawing: function() {
|
||||
cancel_drawing: function () {
|
||||
if (editor._drawing === null || editor._adding !== null) return;
|
||||
editor.map.editTools.stopDrawing();
|
||||
editor._drawing = null;
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.http.response import Http404
|
||||
from django.shortcuts import render
|
||||
|
||||
from c3nav.editor.forms import FeatureForm
|
||||
from c3nav.mapdata.models.feature import FEATURE_TYPES
|
||||
from c3nav.settings import DIRECT_EDITING
|
||||
|
||||
|
||||
def add_feature(request, feature_type):
|
||||
|
@ -15,13 +15,20 @@ def add_feature(request, feature_type):
|
|||
if request.method == 'POST':
|
||||
form = FeatureForm(request.POST, feature_type=feature_type)
|
||||
if form.is_valid():
|
||||
if not DIRECT_EDITING:
|
||||
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.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', {})
|
||||
else:
|
||||
form = FeatureForm(feature_type=feature_type)
|
||||
|
|
|
@ -4,9 +4,10 @@ import os
|
|||
from django.conf import settings
|
||||
from django.core.files import File
|
||||
from django.http import Http404, HttpResponse
|
||||
|
||||
from rest_framework.decorators import detail_route
|
||||
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 .models import FEATURE_TYPES, Feature, Level, Package, Source
|
||||
|
@ -70,6 +71,7 @@ class FeatureTypeViewSet(ViewSet):
|
|||
"""
|
||||
Get Feature types
|
||||
"""
|
||||
|
||||
def list(self, request, version=None):
|
||||
serializer = FeatureTypeSerializer(FEATURE_TYPES.values(), many=True, context={'request': request})
|
||||
return Response(serializer.data)
|
||||
|
@ -81,13 +83,10 @@ class FeatureTypeViewSet(ViewSet):
|
|||
return Response(serializer.data)
|
||||
|
||||
|
||||
ParentModelViewSet = ModelViewSet if settings.DIRECT_EDITING else ReadOnlyModelViewSet
|
||||
|
||||
|
||||
class FeatureViewSet(ParentModelViewSet):
|
||||
class FeatureViewSet(ReadOnlyModelViewSet):
|
||||
"""
|
||||
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
|
||||
lookup_value_regex = '[^/]+'
|
||||
|
|
|
@ -40,4 +40,4 @@ class CachedViewSetMixin:
|
|||
|
||||
class AccessCachedViewSetMixin(CachedViewSetMixin):
|
||||
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))
|
||||
|
|
|
@ -2,9 +2,11 @@ from collections import OrderedDict, namedtuple
|
|||
|
||||
from django.db import models
|
||||
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.package import Package
|
||||
|
||||
from ..fields import GeometryField
|
||||
|
||||
|
||||
|
@ -12,11 +14,14 @@ class FeatureType(namedtuple('FeatureType', ('name', 'title', 'title_plural', 'g
|
|||
def __init__(self, *args, **kwargs):
|
||||
FEATURE_TYPES[self.name] = self
|
||||
|
||||
|
||||
FEATURE_TYPES = OrderedDict()
|
||||
FeatureType('building', _('Building'), _('Buildings'), 'polygon', '#333333')
|
||||
FeatureType('room', _('Room'), _('Rooms'), 'polygon', '#CCCCCC')
|
||||
FeatureType('outside', _('Outside Area'), _('Outside Areas'), 'polygon', '#EEEEEE')
|
||||
FeatureType('obstacle', _('Obstacle'), _('Obstacles'), 'polygon', '#999999')
|
||||
|
||||
|
||||
# FeatureType('door', _('Door'), 'polygon', '#FF00FF')
|
||||
# FeatureType('step', _('Step'), 'polyline', '#FF0000')
|
||||
# FeatureType('elevator', _('Elevator'), 'polygon', '#99CC00')
|
||||
|
@ -36,9 +41,21 @@ class Feature(models.Model):
|
|||
verbose_name=_('level'))
|
||||
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):
|
||||
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'))
|
||||
language = models.CharField(max_length=50)
|
||||
title = models.CharField(max_length=50)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.permissions import BasePermission
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ class PackageSerializer(serializers.ModelSerializer):
|
|||
class Meta:
|
||||
model = Package
|
||||
fields = ('name', 'home_repo', 'commit_id', 'depends', 'bounds', 'public')
|
||||
readonly_fields = ('commit_id', )
|
||||
readonly_fields = ('commit_id',)
|
||||
|
||||
|
||||
class SourceSerializer(serializers.ModelSerializer):
|
||||
|
@ -59,4 +59,4 @@ class FeatureSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = Feature
|
||||
fields = ('name', 'package', 'level', 'feature_type', 'geometry')
|
||||
fields = ('name', 'title', 'feature_type', 'level', 'titles', 'package', 'geometry')
|
||||
|
|
|
@ -112,7 +112,6 @@ LANGUAGE_COOKIE_NAME = 'c3nav_language'
|
|||
CSRF_COOKIE_NAME = 'c3nav_csrftoken'
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
|
@ -222,6 +221,10 @@ STATICFILES_FINDERS = (
|
|||
'compressor.finders.CompressorFinder',
|
||||
)
|
||||
|
||||
BOOTSTRAP3 = {
|
||||
'success_css_class': '',
|
||||
}
|
||||
|
||||
STATICFILES_DIRS = (
|
||||
os.path.join(BASE_DIR, 'static'),
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue