From 752b7d6d7d4b6ad5579ccbcc7d7af2cb187a2a43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 28 Aug 2016 17:59:52 +0200 Subject: [PATCH] implement new django-based models and loadmappkg command --- .../control/templates/control/editor.html | 14 +- src/c3nav/control/views.py | 19 +-- src/c3nav/mapdata/__init__.py | 5 - src/c3nav/mapdata/apps.py | 25 ---- src/c3nav/mapdata/classes.py | 76 ---------- src/c3nav/mapdata/management/__init__.py | 0 .../mapdata/management/commands/__init__.py | 0 .../management/commands/loadmappkgs.py | 19 +++ src/c3nav/mapdata/migrations/0001_initial.py | 81 ++++++----- src/c3nav/mapdata/models.py | 118 +++++++++++++--- src/c3nav/mapdata/packageio.py | 132 ++++++++++++++++++ src/c3nav/mapdata/urls.py | 3 +- src/c3nav/mapdata/views.py | 20 ++- src/c3nav/settings.py | 2 - 14 files changed, 318 insertions(+), 196 deletions(-) delete mode 100644 src/c3nav/mapdata/apps.py delete mode 100644 src/c3nav/mapdata/classes.py create mode 100644 src/c3nav/mapdata/management/__init__.py create mode 100644 src/c3nav/mapdata/management/commands/__init__.py create mode 100644 src/c3nav/mapdata/management/commands/loadmappkgs.py create mode 100644 src/c3nav/mapdata/packageio.py diff --git a/src/c3nav/control/templates/control/editor.html b/src/c3nav/control/templates/control/editor.html index 59e500e5..ba9aec08 100644 --- a/src/c3nav/control/templates/control/editor.html +++ b/src/c3nav/control/templates/control/editor.html @@ -10,7 +10,7 @@ var map = L.map('mapeditor', { center: [120, 200], zoom: 2, - maxBounds: [[0, 0], [{{ map.height }}, {{ map.width }}]], + maxBounds: {{ bounds }}, maxZoom: 10, minZoom: 1, crs: L.CRS.Simple, @@ -19,10 +19,10 @@ var map = L.map('mapeditor', { }); // Add Source Layers -{% for pkg in map.pkgs.values %} +{% for pkg in packages %} L.control.layers([], { - {% for source in pkg.sources %} - "{{ source.name }}": L.imageOverlay('{% url 'map.source' filename=source.filename %}', {{ source.jsbounds }}),{% endfor %} + {% for source in pkg.sources.all %} + "{{ source.name }}": L.imageOverlay('{% url 'map.source' source=source.name %}', {{ source.jsbounds }}),{% endfor %} }).addTo(map); {% endfor %} @@ -33,10 +33,10 @@ L.LevelControl = L.Control.extend({ }, onAdd: function (map) { var container = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-levels'), link; - {% for level in map.levels reversed %} + {% for level in levels reversed %} link = L.DomUtil.create('a', '{% if current_level == level %}current{% endif %}', container); - link.href = '{% url "control.editor" level=level %}'; - link.innerHTML = '{{ level }}'; + link.href = '{% url "control.editor" level=level.name %}'; + link.innerHTML = '{{ level.name }}'; {% endfor %} return container; } diff --git a/src/c3nav/control/views.py b/src/c3nav/control/views.py index 09f04697..1541ce04 100644 --- a/src/c3nav/control/views.py +++ b/src/c3nav/control/views.py @@ -1,8 +1,9 @@ -from django.contrib.admin.views.decorators import staff_member_required -from django.http import Http404 -from django.shortcuts import redirect, render +import json -from ..mapdata import mapmanager +from django.contrib.admin.views.decorators import staff_member_required +from django.shortcuts import get_object_or_404, redirect, render + +from ..mapdata.models import MapLevel, MapPackage, MapSource @staff_member_required @@ -13,10 +14,12 @@ def dashboard(request): @staff_member_required def editor(request, level=None): if not level: - return redirect('control.editor', level=mapmanager.levels[0]) - if level not in mapmanager.levels: - raise Http404('Level does not exist') + return redirect('control.editor', level=MapLevel.objects.first().name) + + level = get_object_or_404(MapLevel, name=level) return render(request, 'control/editor.html', { - 'map': mapmanager, + 'bounds': json.dumps(MapSource.max_bounds()), + 'packages': MapPackage.objects.all(), + 'levels': MapLevel.objects.all(), 'current_level': level, }) diff --git a/src/c3nav/mapdata/__init__.py b/src/c3nav/mapdata/__init__.py index aae7124d..e69de29b 100644 --- a/src/c3nav/mapdata/__init__.py +++ b/src/c3nav/mapdata/__init__.py @@ -1,5 +0,0 @@ -from .classes import MapManager - -default_app_config = 'c3nav.mapdata.apps.MapdataConfig' - -mapmanager = MapManager() diff --git a/src/c3nav/mapdata/apps.py b/src/c3nav/mapdata/apps.py deleted file mode 100644 index 56096fdd..00000000 --- a/src/c3nav/mapdata/apps.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.apps import AppConfig -from django.conf import settings -from django.core.checks import Warning, register - -from . import mapmanager - - -@register() -def has_map_data_check(app_configs, **kwargs): - if not settings.MAP_DIRS: - return [Warning( - 'There are no map data directories configured.', - hint='Add mapdirs=/path/to/directory to your c3nav.cfg.', - id='mapdata.W001', - )] - return [] - - -class MapdataConfig(AppConfig): - name = 'c3nav.mapdata' - verbose_name = 'map data manager' - - def ready(self): - for map_dir in settings.MAP_DIRS: - mapmanager.add_map_dir(map_dir) diff --git a/src/c3nav/mapdata/classes.py b/src/c3nav/mapdata/classes.py deleted file mode 100644 index 1d27841d..00000000 --- a/src/c3nav/mapdata/classes.py +++ /dev/null @@ -1,76 +0,0 @@ -import json -import os -from collections import OrderedDict - - -class MapInitError(Exception): - pass - - -class MapManager: - def __init__(self): - self.main_pkg = None - self.pkgs = OrderedDict() - self.levels = [] - self.sources_by_filename = OrderedDict() - - def add_map_dir(self, path): - pkg = MapDataPackage(path) - if pkg.name in self.pkgs: - raise MapInitError('Duplicate map package: '+pkg.name) - - if pkg.extends is None: - if self.main_pkg is not None: - raise MapInitError('There can not be more than one root map package: tried to add '+pkg.name+', ' - 'but '+self.main_pkg.name+' was there first.') - self.main_pkg = pkg - self.levels = pkg.levels - self.width = pkg.width - self.height = pkg.height - else: - if pkg.extends not in self.pkgs: - raise MapInitError('map package'+pkg.name+' extends '+pkg.exends+', which was not imported ' - 'beforehand.') - - for source in pkg.sources: - self.sources_by_filename[source.filename] = source - - self.pkgs[pkg.name] = pkg - - -class MapDataPackage: - def __init__(self, path): - self.path = path - - main_file = os.path.join(path, 'map.json') - try: - data = json.load(open(main_file)) - except FileNotFoundError: - raise MapInitError(main_file+' not found') - except json.decoder.JSONDecodeError as e: - raise MapInitError('Could not decode '+main_file+': '+str(e)) - - self.name = data.get('name') - if self.name is None: - raise MapInitError('Map package '+path+' has no name in map.json.') - - self.extends = data.get('extends') - - self.width = data.get('width') - self.height = data.get('height') - - self.sources = tuple(MapSource(self, source) for source in data.get('sources', [])) - - self.levels = data.get('levels') - - -class MapSource: - def __init__(self, pkg, data): - self.name = data['name'] - self.filename = self.name+'.'+data['src'].split('.')[-1] - self.src = os.path.join(pkg.path, data['src']) - self.bounds = data['bounds'] - - @property - def jsbounds(self): - return json.dumps(self.bounds) diff --git a/src/c3nav/mapdata/management/__init__.py b/src/c3nav/mapdata/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/c3nav/mapdata/management/commands/__init__.py b/src/c3nav/mapdata/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/c3nav/mapdata/management/commands/loadmappkgs.py b/src/c3nav/mapdata/management/commands/loadmappkgs.py new file mode 100644 index 00000000..58336eaa --- /dev/null +++ b/src/c3nav/mapdata/management/commands/loadmappkgs.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand, CommandError +from django.db import transaction +from ...packageio import MapPackagesIO + + +class Command(BaseCommand): + help = 'Load the given map packages into the database' + + def add_arguments(self, parser): + parser.add_argument('mappkgdir', nargs='+', type=str, help='map package directories') + parser.add_argument('-y', action='store_const', const=True, default=False, + help='don\'t ask for confirmation') + + def handle(self, *args, **options): + with transaction.atomic(): + MapPackagesIO(options['mappkgdir']).update_to_db() + print() + if input('Confirm (y/N): ') != 'y': + raise CommandError('Aborted.') diff --git a/src/c3nav/mapdata/migrations/0001_initial.py b/src/c3nav/mapdata/migrations/0001_initial.py index 3cdd9fed..e7ec9275 100644 --- a/src/c3nav/mapdata/migrations/0001_initial.py +++ b/src/c3nav/mapdata/migrations/0001_initial.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.9.9 on 2016-08-25 11:37 +# Generated by Django 1.9.9 on 2016-08-28 15:52 from __future__ import unicode_literals +import c3nav.mapdata.models from django.db import migrations, models import django.db.models.deletion import parler.models @@ -16,72 +17,78 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Bounds', + name='MapFeature', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('bottom', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='bottom coordinate')), - ('left', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='left coordinate')), - ('top', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='top coordinate')), - ('right', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='right coordinate')), + ('name', models.CharField(help_text='e.g. noc', max_length=50, unique=True, verbose_name='feature identifier')), + ('type', models.CharField(choices=[('building', 'Building'), ('room', 'Room'), ('obstacle', 'Obstacle')], max_length=50)), + ('geometry', models.TextField()), ], + options={ + 'abstract': False, + }, + bases=(parler.models.TranslatableModelMixin, models.Model), + ), + migrations.CreateModel( + name='MapFeatureTranslation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('language_code', models.CharField(db_index=True, max_length=15, verbose_name='Language')), + ('title', models.CharField(max_length=50, verbose_name='package title')), + ('master', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='mapdata.MapFeature')), + ], + options={ + 'managed': True, + 'db_tablespace': '', + 'verbose_name': 'map feature Translation', + 'db_table': 'mapdata_mapfeature_translation', + 'default_permissions': (), + }, ), migrations.CreateModel( name='MapLevel', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(help_text='Usually just an integer (e.g. -1, 0, 1, 2)', max_length=50, unique=True, verbose_name='level name')), + ('altitude', models.DecimalField(decimal_places=2, max_digits=6, null=True, verbose_name='level altitude')), ], - options={ - 'abstract': False, - }, - bases=(parler.models.TranslatableModelMixin, models.Model), ), migrations.CreateModel( name='MapPackage', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(help_text='e.g. de.c3nav.33c3.base', max_length=50, unique=True, verbose_name='package identifier')), - ('map', models.CharField(help_text='e.g. de.c3nav.33c3', max_length=50, verbose_name='map identifier')), - ('bounds', models.OneToOneField(null=True, on_delete=django.db.models.deletion.PROTECT, to='mapdata.Bounds', verbose_name='bounds')), + ('bottom', models.DecimalField(decimal_places=2, max_digits=6, null=True, verbose_name='bottom coordinate')), + ('left', models.DecimalField(decimal_places=2, max_digits=6, null=True, verbose_name='left coordinate')), + ('top', models.DecimalField(decimal_places=2, max_digits=6, null=True, verbose_name='top coordinate')), + ('right', models.DecimalField(decimal_places=2, max_digits=6, null=True, verbose_name='right coordinate')), ], - options={ - 'abstract': False, - }, - bases=(parler.models.TranslatableModelMixin, models.Model), - ), - migrations.CreateModel( - name='MapPackageTranslation', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('language_code', models.CharField(db_index=True, max_length=15, verbose_name='Language')), - ('title', models.CharField(max_length=50, verbose_name='package title')), - ('master', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='mapdata.MapPackage')), - ], - options={ - 'db_tablespace': '', - 'verbose_name': 'map package Translation', - 'managed': True, - 'db_table': 'mapdata_mappackage_translation', - 'default_permissions': (), - }, ), migrations.CreateModel( name='MapSource', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.SlugField(unique=True, verbose_name='source name')), - ('image', models.FileField(upload_to='mapsources/', verbose_name='source image')), - ('bounds', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to='mapdata.Bounds', verbose_name='bounds')), - ('package', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sources', to='mapdata.MapPackage', verbose_name='map package')), + ('image', models.FileField(max_length=70, storage=c3nav.mapdata.models.MapSourceImageStorage(), upload_to=c3nav.mapdata.models.map_source_filename, verbose_name='source image')), + ('bottom', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='bottom coordinate')), + ('left', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='left coordinate')), + ('top', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='top coordinate')), + ('right', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='right coordinate')), + ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sources', to='mapdata.MapPackage', verbose_name='map package')), ], ), migrations.AddField( model_name='maplevel', name='package', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='levels', to='mapdata.MapPackage', verbose_name='map package'), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='levels', to='mapdata.MapPackage', verbose_name='map package'), + ), + migrations.AddField( + model_name='mapfeature', + name='package', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='features', to='mapdata.MapPackage', verbose_name='map package'), ), migrations.AlterUniqueTogether( - name='mappackagetranslation', + name='mapfeaturetranslation', unique_together=set([('language_code', 'master')]), ), ] diff --git a/src/c3nav/mapdata/models.py b/src/c3nav/mapdata/models.py index 2cb2356f..e4dd459d 100644 --- a/src/c3nav/mapdata/models.py +++ b/src/c3nav/mapdata/models.py @@ -1,50 +1,122 @@ -from django.db import models +import json +import os + +from django.conf import settings +from django.db import models, transaction from django.utils.translation import ugettext_lazy as _ +from django.dispatch import receiver +from django.core.files.storage import FileSystemStorage from parler.models import TranslatedFields, TranslatableModel -class Bounds(models.Model): - bottom = models.DecimalField(_('bottom coordinate'), max_digits=6, decimal_places=2) - left = models.DecimalField(_('left coordinate'), max_digits=6, decimal_places=2) - top = models.DecimalField(_('top coordinate'), max_digits=6, decimal_places=2) - right = models.DecimalField(_('right coordinate'), max_digits=6, decimal_places=2) - - @property - def __iter__(self): - return iter(((self.bottom, self.left), (self.top, self.right))) - - -class MapPackage(TranslatableModel): +class MapPackage(models.Model): """ A c3nav map package """ name = models.CharField(_('package identifier'), unique=True, max_length=50, help_text=_('e.g. de.c3nav.33c3.base')) - map = models.CharField(_('map identifier'), max_length=50, help_text=_('e.g. de.c3nav.33c3')) - bounds = models.OneToOneField('Bounds', null=True, on_delete=models.PROTECT, verbose_name=_('bounds')) - translations = TranslatedFields( - title=models.CharField(_('package title'), max_length=50), - ) + bottom = models.DecimalField(_('bottom coordinate'), null=True, max_digits=6, decimal_places=2) + left = models.DecimalField(_('left coordinate'), null=True, max_digits=6, decimal_places=2) + top = models.DecimalField(_('top coordinate'), null=True, max_digits=6, decimal_places=2) + right = models.DecimalField(_('right coordinate'), null=True, max_digits=6, decimal_places=2) -class MapLevel(TranslatableModel): +class MapLevel(models.Model): """ A map level (-1, 0, 1, 2…) """ name = models.CharField(_('level name'), max_length=50, unique=True, help_text=_('Usually just an integer (e.g. -1, 0, 1, 2)')) - package = models.ForeignKey('MapPackage', on_delete=models.PROTECT, related_name='levels', + altitude = models.DecimalField(_('level altitude'), null=True, max_digits=6, decimal_places=2) + package = models.ForeignKey('MapPackage', on_delete=models.CASCADE, related_name='levels', verbose_name=_('map package')) + class Meta: + ordering = ['altitude'] + + +class MapSourceImageStorage(FileSystemStorage): + def get_available_name(self, name, *args, max_length=None, **kwargs): + if self.exists(name): + os.remove(os.path.join(settings.MEDIA_ROOT, name)) + return super().get_available_name(name, *args, max_length, **kwargs) + + +def map_source_filename(instance, filename): + return os.path.join('mapsources', '%s.%s' % (instance.name, filename.split('.')[-1])) + class MapSource(models.Model): """ A map source, images of levels that can be useful as backgrounds for the map editor """ name = models.SlugField(_('source name'), max_length=50, unique=True) - package = models.ForeignKey('MapPackage', on_delete=models.PROTECT, related_name='sources', + package = models.ForeignKey('MapPackage', on_delete=models.CASCADE, related_name='sources', verbose_name=_('map package')) - image = models.FileField(_('source image'), upload_to='mapsources/') - bounds = models.OneToOneField('Bounds', on_delete=models.PROTECT, verbose_name=_('bounds')) + + image = models.FileField(_('source image'), max_length=70, + upload_to=map_source_filename, storage=MapSourceImageStorage()) + + bottom = models.DecimalField(_('bottom coordinate'), max_digits=6, decimal_places=2) + left = models.DecimalField(_('left coordinate'), max_digits=6, decimal_places=2) + top = models.DecimalField(_('top coordinate'), max_digits=6, decimal_places=2) + right = models.DecimalField(_('right coordinate'), max_digits=6, decimal_places=2) + + @classmethod + def max_bounds(cls): + result = cls.objects.all().aggregate(models.Min('bottom'), models.Min('left'), + models.Max('top'), models.Max('right')) + return ((float(result['bottom__min']), float(result['left__min'])), + (float(result['top__max']), float(result['right__max']))) + + @property + def bounds(self): + return ((self.bottom, self.left), (self.top, self.right)) + + @property + def jsbounds(self): + return json.dumps(((float(self.bottom), float(self.left)), (float(self.top), float(self.right)))) + + +@receiver(models.signals.post_delete, sender=MapSource) +def delete_image_on_mapsource_delete(sender, instance, **kwargs): + transaction.on_commit(lambda: instance.image.delete(save=False)) + + +@receiver(models.signals.pre_save, sender=MapSource) +def delete_image_on_mapsource_change(sender, instance, **kwargs): + if not instance.pk: + return False + + try: + old_file = MapSource.objects.get(pk=instance.pk).image + except MapSource.DoesNotExist: + return False + + new_file = instance.image + + if map_source_filename(instance, new_file.name) != old_file.name: + transaction.on_commit(lambda: old_file.delete(save=False)) + + +class MapFeature(TranslatableModel): + """ + A map feature + """ + TYPES = ( + ('building', _('Building')), + ('room', _('Room')), + ('obstacle', _('Obstacle')), + ) + + name = models.CharField(_('feature identifier'), unique=True, max_length=50, help_text=_('e.g. noc')) + package = models.ForeignKey('MapPackage', on_delete=models.CASCADE, related_name='features', + verbose_name=_('map package')) + type = models.CharField(max_length=50, choices=TYPES) + geometry = models.TextField() + + translations = TranslatedFields( + title=models.CharField(_('package title'), max_length=50), + ) diff --git a/src/c3nav/mapdata/packageio.py b/src/c3nav/mapdata/packageio.py new file mode 100644 index 00000000..33263357 --- /dev/null +++ b/src/c3nav/mapdata/packageio.py @@ -0,0 +1,132 @@ +import json +import os + +from collections import OrderedDict +from django.core.files import File + +from django.core.management.base import CommandError + + +class PackageIOError(CommandError): + pass + + +class MapPackagesIO(): + def __init__(self, directories): + print('Opening Map Packages…') + self.packages = OrderedDict() + self.levels = OrderedDict() + self.sources = OrderedDict() + + for directory in directories: + print('- '+directory) + + try: + package = json.load(open(os.path.join(directory, 'pkg.json'))) + except FileNotFoundError: + raise PackageIOError('no pkg.json found in %s' % directory) + + if package['name'] in self.packages: + raise PackageIOError('Duplicate package name: %s' % package['name']) + + if 'bounds' in package: + self._validate_bounds(package['bounds']) + + package['directory'] = directory + self.packages[package['name']] = package + + for level in package.get('levels', []): + level = level.copy() + if level['name'] in self.levels: + raise PackageIOError('Duplicate level name: %s in packages %s and %s' % + (level, self.levels[level]['name'], package['name'])) + + if not isinstance(level['altitude'], (int, float)): + raise PackageIOError('levels: %s: altitude has to be int or float.' % level['name']) + + level['package'] = package['name'] + self.levels[level['name']] = level + + for source in package.get('sources', []): + source = source.copy() + if source['name'] in self.sources: + raise PackageIOError('Duplicate source name: %s in packages %s and %s' % + (source['name'], self.sources[source['name']]['name'], package['name'])) + + self._validate_bounds(source['bounds'], 'sources: %s: ' % source['name']) + + source['filename'] = os.path.join(directory, source['src']) + if not os.path.isfile(source['filename']): + raise PackageIOError('Source file not found: '+source['filename']) + + source['package'] = package['name'] + self.sources[source['name']] = source + + def _validate_bounds(self, bounds, prefix=''): + if len(bounds) != 2 or len(bounds[0]) != 2 or len(bounds[1]) != 2: + raise PackageIOError(prefix+'Invalid bounds format.') + if not all(isinstance(i, (float, int)) for i in sum(bounds, [])): + raise PackageIOError(prefix+'All bounds coordinates have to be int or float.') + if bounds[0][0] >= bounds[1][0] or bounds[0][1] >= bounds[1][1]: + raise PackageIOError(prefix+'bounds: lower coordinate has to be first.') + + def update_to_db(self): + from .models import MapPackage, MapLevel, MapSource + print('Updating Map database…') + + # Add new Packages + packages = {} + print('- Updating packages…') + for name, package in self.packages.items(): + bounds = package.get('bounds') + defaults = { + 'bottom': bounds[0][0], + 'left': bounds[0][1], + 'top': bounds[1][0], + 'right': bounds[1][1], + } if bounds else {} + + package, created = MapPackage.objects.update_or_create(name=name, defaults=defaults) + packages[name] = package + if created: + print('- Created package: '+name) + + # Add new levels + print('- Updating levels…') + for name, level in self.levels.items(): + package, created = MapLevel.objects.update_or_create(name=name, defaults={ + 'package': packages[level['package']], + 'altitude': level['altitude'], + 'name': level['name'], + }) + if created: + print('- Created level: '+name) + + # Add new map sources + print('- Updating sources…') + for name, source in self.sources.items(): + source, created = MapSource.objects.update_or_create(name=name, defaults={ + 'package': packages[source['package']], + 'image': File(open(source['filename'], 'rb')), + 'bottom': source['bounds'][0][0], + 'left': source['bounds'][0][1], + 'top': source['bounds'][1][0], + 'right': source['bounds'][1][1], + }) + if created: + print('- Created source: '+name) + + # Remove old sources + for source in MapSource.objects.exclude(name__in=self.sources.keys()): + print('- Deleted source: '+source.name) + source.delete() + + # Remove old levels + for level in MapLevel.objects.exclude(name__in=self.levels.keys()): + print('- Deleted level: '+level.name) + level.delete() + + # Remove old packages + for package in MapPackage.objects.exclude(name__in=self.packages.keys()): + print('- Deleted package: '+package.name) + package.delete() diff --git a/src/c3nav/mapdata/urls.py b/src/c3nav/mapdata/urls.py index 67eb18af..73095b33 100644 --- a/src/c3nav/mapdata/urls.py +++ b/src/c3nav/mapdata/urls.py @@ -3,5 +3,6 @@ from django.conf.urls import url from . import views urlpatterns = [ - url(r'^sources/(?P[^/]+)$', views.source, name='map.source'), + url(r'^sources/(?P[^/]+)$', views.source, name='map.source'), + url(r'^data/add$', views.source, name='map.edit.source'), ] diff --git a/src/c3nav/mapdata/views.py b/src/c3nav/mapdata/views.py index e1a45865..8d4d4142 100644 --- a/src/c3nav/mapdata/views.py +++ b/src/c3nav/mapdata/views.py @@ -1,19 +1,15 @@ import mimetypes from django.contrib.admin.views.decorators import staff_member_required -from django.http import Http404, HttpResponse -from django.shortcuts import render - -from ..mapdata import mapmanager +from django.http import HttpResponse +from django.shortcuts import get_object_or_404, render +from .models import MapSource @staff_member_required -def source(request, filename): - source = mapmanager.sources_by_filename.get(filename) - if source is None: - raise Http404('Source does not exist') - - response = HttpResponse(content_type=mimetypes.guess_type(source.src)[0]) - with open(source.src, 'rb') as f: - response.write(f.read()) +def source(request, source): + source = get_object_or_404(MapSource, name=source) + response = HttpResponse(content_type=mimetypes.guess_type(source.image.name)[0]) + for chunk in source.image.chunks(): + response.write(chunk) return response diff --git a/src/c3nav/settings.py b/src/c3nav/settings.py index 260a97e8..1fa576c7 100644 --- a/src/c3nav/settings.py +++ b/src/c3nav/settings.py @@ -18,8 +18,6 @@ DATA_DIR = config.get('c3nav', 'datadir', fallback=os.environ.get('DATA_DIR', 'd LOG_DIR = os.path.join(DATA_DIR, 'logs') MEDIA_ROOT = os.path.join(DATA_DIR, 'media') -MAP_DIRS = tuple(n for n in config.get('c3nav', 'mapdirs', fallback='').split(',') if n) - if not os.path.exists(DATA_DIR): os.mkdir(DATA_DIR) if not os.path.exists(LOG_DIR):