From 9d67cdcb75142093dde6a6754d43ed9016f676d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Thu, 11 May 2017 22:40:48 +0200 Subject: [PATCH] add LocationRedirect and fallback slug for locations --- src/c3nav/mapdata/api.py | 15 ++++- .../migrations/0002_locationredirect.py | 27 ++++++++ src/c3nav/mapdata/models/base.py | 11 +++- src/c3nav/mapdata/models/locations.py | 66 +++++++++++++++++-- 4 files changed, 109 insertions(+), 10 deletions(-) create mode 100644 src/c3nav/mapdata/migrations/0002_locationredirect.py diff --git a/src/c3nav/mapdata/api.py b/src/c3nav/mapdata/api.py index 11779e83..0074b93e 100644 --- a/src/c3nav/mapdata/api.py +++ b/src/c3nav/mapdata/api.py @@ -2,6 +2,7 @@ import mimetypes from itertools import chain from django.http import HttpResponse +from django.shortcuts import redirect from django.utils.translation import ugettext_lazy as _ from rest_framework.decorators import detail_route, list_route from rest_framework.exceptions import NotFound, ValidationError @@ -13,7 +14,7 @@ from c3nav.access.apply import filter_queryset_by_access from c3nav.mapdata.models import Building, Door, Hole, LocationGroup, Source, Space from c3nav.mapdata.models.geometry.section import SECTION_MODELS from c3nav.mapdata.models.geometry.space import SPACE_MODELS, Area, LineObstacle, Obstacle, Point, Stair -from c3nav.mapdata.models.locations import LOCATION_MODELS, LocationSlug +from c3nav.mapdata.models.locations import LOCATION_MODELS, Location, LocationRedirect, LocationSlug from c3nav.mapdata.models.section import Section from c3nav.mapdata.serializers.main import SourceSerializer from c3nav.mapdata.utils.cache import CachedReadOnlyViewSetMixin @@ -137,11 +138,19 @@ class LocationGroupViewSet(MapdataViewSet): class LocationViewSet(RetrieveModelMixin, GenericViewSet): + """ Add ?show_redirect=1 to suppress redirects and show them as JSON. """ queryset = LocationSlug.objects.all() lookup_field = 'slug' - def retrieve(self, request, *args, **kwargs): - return Response(self.get_object().get_child().serialize(include_type=True)) + def retrieve(self, request, slug=None, *args, **kwargs): + result = Location.get_by_slug(slug, self.get_queryset()) + if result is None: + raise NotFound + if isinstance(result, LocationRedirect): + if 'show_redirects' in request.GET: + return Response(result.serialize(include_type=True)) + return redirect('../'+result.target.slug) # todo: why does redirect/reverse not work here? + return Response(result.get_child().serialize(include_type=True)) @list_route(methods=['get']) def types(self, request): diff --git a/src/c3nav/mapdata/migrations/0002_locationredirect.py b/src/c3nav/mapdata/migrations/0002_locationredirect.py new file mode 100644 index 00000000..781669d3 --- /dev/null +++ b/src/c3nav/mapdata/migrations/0002_locationredirect.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-05-11 19:59 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapdata', '0001_squashed_refactor_2017'), + ] + + operations = [ + migrations.CreateModel( + name='LocationRedirect', + fields=[ + ('locationslug_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='redirect', serialize=False, to='mapdata.LocationSlug')), + ('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='redirects', to='mapdata.LocationSlug', verbose_name='target')), + ], + options={ + 'default_related_name': 'redirect', + }, + bases=('mapdata.locationslug',), + ), + ] diff --git a/src/c3nav/mapdata/models/base.py b/src/c3nav/mapdata/models/base.py index 35bad383..e857f1e4 100644 --- a/src/c3nav/mapdata/models/base.py +++ b/src/c3nav/mapdata/models/base.py @@ -5,9 +5,7 @@ from django.db import models EDITOR_FORM_MODELS = OrderedDict() -class EditorFormMixin(models.Model): - EditorForm = None - +class SerializableMixin(models.Model): class Meta: abstract = True @@ -29,3 +27,10 @@ class EditorFormMixin(models.Model): result['type'] = self.__class__.__name__.lower() result['id'] = self.id return result + + +class EditorFormMixin(SerializableMixin, models.Model): + EditorForm = None + + class Meta: + abstract = True diff --git a/src/c3nav/mapdata/models/locations.py b/src/c3nav/mapdata/models/locations.py index 953110b5..6997318f 100644 --- a/src/c3nav/mapdata/models/locations.py +++ b/src/c3nav/mapdata/models/locations.py @@ -1,4 +1,5 @@ import numpy as np +from django.apps import apps from django.core.cache import cache from django.db import models from django.utils.functional import cached_property @@ -7,17 +8,25 @@ from django.utils.translation import get_language, ungettext_lazy from c3nav.mapdata.fields import JSONField from c3nav.mapdata.lastupdate import get_last_mapdata_update -from c3nav.mapdata.models.base import EditorFormMixin +from c3nav.mapdata.models.base import EditorFormMixin, SerializableMixin LOCATION_MODELS = [] -class LocationSlug(models.Model): +class LocationSlug(SerializableMixin, models.Model): + LOCATION_TYPE_CODES = { + 'Section': 'se', + 'Space': 'sp', + 'Area': 'a', + 'Point': 'p', + 'LocationGroup': 'g' + } + LOCATION_TYPE_BY_CODE = {code: model_name for model_name, code in LOCATION_TYPE_CODES.items()} slug = models.SlugField(_('name'), unique=True, null=True, max_length=50) def get_child(self): # todo: cache this - for model in LOCATION_MODELS: + for model in LOCATION_MODELS+[LocationRedirect]: try: return getattr(self, model._meta.default_related_name) except AttributeError: @@ -47,7 +56,7 @@ class Location(LocationSlug, EditorFormMixin, models.Model): def _serialize(self, **kwargs): result = super()._serialize(**kwargs) - result['slug'] = self.slug + result['slug'] = self.get_slug() result['titles'] = self.titles result['can_search'] = self.can_search result['can_describe'] = self.can_search @@ -55,6 +64,37 @@ class Location(LocationSlug, EditorFormMixin, models.Model): result['public'] = self.public return result + def get_slug(self): + if self.slug is None: + code = self.LOCATION_TYPE_CODES.get(self.__class__.__name__) + if code is not None: + return code+':'+str(self.id) + return self.slug + + @classmethod + def get_by_slug(cls, slug, queryset=None): + if queryset is None: + queryset = LocationSlug.objects.all() + + if ':' in slug: + code, pk = slug.split(':', 1) + model_name = cls.LOCATION_TYPE_BY_CODE.get(code) + if model_name is None or not pk.isdigit(): + return None + + model = apps.get_model('mapdata', model_name) + try: + location = model.objects.get(pk=pk) + except model.DoesNotExist: + return None + + if location.slug is not None: + return LocationRedirect(slug=slug, target=location) + + return location + + return queryset.filter(slug=slug).first() + @property def title(self): if not hasattr(self, 'titles'): @@ -135,6 +175,24 @@ class LocationGroup(Location, EditorFormMixin, models.Model): return result +class LocationRedirect(LocationSlug): + target = models.ForeignKey(LocationSlug, verbose_name=_('target'), related_name='redirects') + + def _serialize(self, with_type=True, **kwargs): + result = super()._serialize(with_type=with_type, **kwargs) + if type(self.target) == LocationSlug: + result['target'] = self.target.get_child().slug + else: + result['target'] = self.target.slug + if with_type: + result['type'] = 'redirect' + result.pop('id') + return result + + class Meta: + default_related_name = 'redirect' + + class PointLocation: def __init__(self, section: 'Section', x: int, y: int, request): self.section = section