diff --git a/src/c3nav/api/urls.py b/src/c3nav/api/urls.py index 481dd772..0935808f 100644 --- a/src/c3nav/api/urls.py +++ b/src/c3nav/api/urls.py @@ -11,11 +11,11 @@ from rest_framework.routers import SimpleRouter from c3nav.api.api import SessionViewSet from c3nav.editor.api import ChangeSetViewSet, EditorViewSet from c3nav.mapdata.api import (AccessRestrictionGroupViewSet, AccessRestrictionViewSet, AreaViewSet, BuildingViewSet, - ColumnViewSet, CrossDescriptionViewSet, DoorViewSet, HoleViewSet, - LeaveDescriptionViewSet, LevelViewSet, LineObstacleViewSet, LocationBySlugViewSet, - LocationGroupCategoryViewSet, LocationGroupViewSet, LocationViewSet, MapViewSet, - ObstacleViewSet, POIViewSet, RampViewSet, SourceViewSet, SpaceViewSet, StairViewSet, - UpdatesViewSet) + ColumnViewSet, CrossDescriptionViewSet, DoorViewSet, DynamicLocationPositionViewSet, + HoleViewSet, LeaveDescriptionViewSet, LevelViewSet, LineObstacleViewSet, + LocationBySlugViewSet, LocationGroupCategoryViewSet, LocationGroupViewSet, + LocationViewSet, MapViewSet, ObstacleViewSet, POIViewSet, RampViewSet, SourceViewSet, + SpaceViewSet, StairViewSet, UpdatesViewSet) from c3nav.mapdata.utils.user import can_access_editor from c3nav.routing.api import RoutingViewSet @@ -41,6 +41,7 @@ router.register(r'accessrestrictiongroups', AccessRestrictionGroupViewSet) router.register(r'locations', LocationViewSet) router.register(r'locations/by_slug', LocationBySlugViewSet, base_name='location-by-slug') +router.register(r'locations/dynamic', DynamicLocationPositionViewSet, base_name='dynamic-location') router.register(r'locationgroupcategories', LocationGroupCategoryViewSet) router.register(r'locationgroups', LocationGroupViewSet) diff --git a/src/c3nav/mapdata/api.py b/src/c3nav/mapdata/api.py index d1ea952e..c8e5536d 100644 --- a/src/c3nav/mapdata/api.py +++ b/src/c3nav/mapdata/api.py @@ -6,7 +6,7 @@ from urllib.parse import urlparse from django.core.cache import cache from django.db.models import Prefetch -from django.http import HttpResponse +from django.http import Http404, HttpResponse from django.shortcuts import redirect from django.utils.cache import get_conditional_response from django.utils.http import http_date, quote_etag, urlsafe_base64_encode @@ -14,6 +14,7 @@ from django.utils.translation import get_language from django.utils.translation import ugettext_lazy as _ from rest_framework.decorators import action from rest_framework.exceptions import NotFound, ValidationError +from rest_framework.generics import get_object_or_404 from rest_framework.mixins import RetrieveModelMixin from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet, ReadOnlyModelViewSet, ViewSet @@ -25,8 +26,8 @@ from c3nav.mapdata.models.geometry.level import LevelGeometryMixin from c3nav.mapdata.models.geometry.space import (POI, Area, Column, CrossDescription, LeaveDescription, LineObstacle, Obstacle, Ramp, SpaceGeometryMixin, Stair) from c3nav.mapdata.models.level import Level -from c3nav.mapdata.models.locations import (Location, LocationGroupCategory, LocationRedirect, LocationSlug, - SpecificLocation) +from c3nav.mapdata.models.locations import (DynamicLocation, Location, LocationGroupCategory, LocationRedirect, + LocationSlug, Position, SpecificLocation) from c3nav.mapdata.utils.cache.local import LocalCacheProxy from c3nav.mapdata.utils.cache.stats import increment_cache_key from c3nav.mapdata.utils.locations import (get_location_by_id_for_request, get_location_by_slug_for_request, @@ -463,6 +464,25 @@ class LocationBySlugViewSet(LocationViewSetBase): return get_location_by_slug_for_request(self.kwargs['slug'], self.request) +class DynamicLocationPositionViewSet(RetrieveModelMixin, GenericViewSet): + queryset = LocationSlug.objects.all() + lookup_field = 'slug' + lookup_value_regex = r'[^/]+' + + def get_object(self): + slug = self.kwargs['slug'] + if slug.startswith('p:'): + return get_object_or_404(Position, secret=slug[2:]) + if slug.isdigit(): + return get_object_or_404(DynamicLocation, pk=slug) + raise Http404 + + @api_stats('dynamic_location_retrieve') + def retrieve(self, request, key=None, *args, **kwargs): + obj = self.get_object() + return Response(obj.serialize_position()) + + class SourceViewSet(MapdataViewSet): queryset = Source.objects.all() order_by = ('name',) diff --git a/src/c3nav/mapdata/models/access.py b/src/c3nav/mapdata/models/access.py index fec361c2..3e128f7c 100644 --- a/src/c3nav/mapdata/models/access.py +++ b/src/c3nav/mapdata/models/access.py @@ -199,7 +199,7 @@ class AccessPermission(models.Model): @classmethod def get_for_request(cls, request): - if not request.user.is_authenticated: + if not request or not request.user.is_authenticated: return set() if request.user_permissions.grant_all_access: diff --git a/src/c3nav/mapdata/models/locations.py b/src/c3nav/mapdata/models/locations.py index 438640e0..3165903b 100644 --- a/src/c3nav/mapdata/models/locations.py +++ b/src/c3nav/mapdata/models/locations.py @@ -111,7 +111,7 @@ class Location(LocationSlug, AccessRestrictionMixin, TitledMixin, models.Model): result = super().serialize(detailed=detailed, **kwargs) if not detailed: fields = ('id', 'type', 'slug', 'title', 'subtitle', 'icon', 'point', 'bounds', 'grid_square', - 'locations', 'on_top_of', 'label_settings', 'label_override', 'add_search') + 'locations', 'on_top_of', 'label_settings', 'label_override', 'add_search', 'dynamic') result = {name: result[name] for name in fields if name in result} return result @@ -236,11 +236,15 @@ class SpecificLocation(Location, models.Model): return result - @property - def subtitle(self): + @cached_property + def describing_groups(self): groups = tuple(self.groups.all() if 'groups' in getattr(self, '_prefetched_objects_cache', ()) else ()) groups = tuple(group for group in groups if group.can_describe) - subtitle = groups[0].title if groups else self.__class__._meta.verbose_name + return groups + + @property + def subtitle(self): + subtitle = self.describing_groups[0].title if self.describing_groups else self.__class__._meta.verbose_name if self.grid_square: return '%s, %s' % (subtitle, self.grid_square) return subtitle @@ -462,7 +466,15 @@ class LabelSettings(SerializableMixin, models.Model): ordering = ('min_zoom', '-font_size') -class DynamicLocation(SpecificLocation, models.Model): +class CustomLocationProxyMixin: + def get_custom_location(self): + raise NotImplementedError + + def serialize_position(self): + raise NotImplementedError + + +class DynamicLocation(CustomLocationProxyMixin, SpecificLocation, models.Model): position_secret = models.CharField(_('position secret'), max_length=32, null=True, blank=True) class Meta: @@ -470,21 +482,55 @@ class DynamicLocation(SpecificLocation, models.Model): verbose_name_plural = _('Dynamic locations') default_related_name = 'dynamic_locations' - """ def _serialize(self, **kwargs): + """custom_location = self.get_custom_location() + print(custom_location) + result = {} if custom_location is None else custom_location.serialize(**kwargs) + super_result = super()._serialize(**kwargs) + super_result['subtitle'] = '%s %s, %s' % (_('(moving)'), result['title'], result['subtitle']) + result.update(super_result)""" result = super()._serialize(**kwargs) + result['dynamic'] = True return result - @property - def grid_square(self): - return grid.get_squares_for_bounds(self.geometry.bounds) or '' + def serialize_position(self): + custom_location = self.get_custom_location() + if custom_location is None: + return { + 'available': False, + 'title': self.title, + 'subtitle': '%s %s, %s' % (_('currently unavailable'), _('(moving)'), self.subtitle) + } + result = custom_location.serialize() + result.update({ + 'available': True, + 'id': self.pk, + 'slug': self.slug, + 'coordinates': custom_location.pk, + 'icon': self.get_icon(), + 'title': self.title, + 'subtitle': '%s %s%s, %s' % ( + _('(moving)'), + ('%s, ' % self.subtitle) if self.describing_groups else '', + result['title'], + result['subtitle'] + ), + }) + return result + + def get_custom_location(self): + if not self.position_secret: + return None + try: + return Position.objects.get(secret=self.position_secret).get_custom_location() + except Position.DoesNotExist: + return None def details_display(self, editor_url=True, **kwargs): result = super().details_display(**kwargs) if editor_url: - result['editor_url'] = reverse('editor.areas.edit', kwargs={'space': self.space_id, 'pk': self.pk}) + result['editor_url'] = reverse('editor.dynamic_locations.edit', kwargs={'pk': self.pk}) return result - """ def get_position_secret(): @@ -495,7 +541,7 @@ def get_position_api_secret(): return get_random_string(64, string.ascii_letters+string.digits) -class Position(models.Model): +class Position(CustomLocationProxyMixin, models.Model): owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) name = models.CharField(_('name'), max_length=32) secret = models.CharField(_('secret'), unique=True, max_length=32, default=get_position_secret) @@ -519,6 +565,9 @@ class Position(models.Model): self.cordinates = None self.last_coordinates_update = end_time + def get_custom_location(self): + return self.coordinates + @classmethod def user_has_positions(cls, user): if not user.is_authenticated: @@ -530,6 +579,33 @@ class Position(models.Model): cache.set(cache_key, result, 600) return result + def serialize_position(self): + custom_location = self.get_custom_location() + if custom_location is None: + return { + 'id': self.secret, + 'slug': self.secret, + 'available': False, + 'icon': 'my_location', + 'title': self.name, + 'subtitle': _('currently unavailable'), + } + result = custom_location.serialize() + result.update({ + 'available': True, + 'id': 'p:%s' % self.secret, + 'slug': 'p:%s' % self.secret, + 'coordinates': custom_location.pk, + 'icon': 'my_location', + 'title': self.name, + 'subtitle': '%s, %s, %s' % ( + _('Position'), + result['title'], + result['subtitle'] + ), + }) + return result + def save(self, *args, **kwargs): with transaction.atomic(): super().save(*args, **kwargs)