export entire location query logic into mapdata.utils.locations and cache

This commit is contained in:
Laura Klünder 2017-10-31 15:22:12 +01:00
parent 247d6b119e
commit 620323b808
4 changed files with 180 additions and 118 deletions

View file

@ -1,5 +1,4 @@
import mimetypes
import operator
from functools import wraps
from django.core.cache import cache
@ -18,12 +17,13 @@ from rest_framework.viewsets import GenericViewSet, ReadOnlyModelViewSet, ViewSe
from c3nav.mapdata.models import AccessRestriction, Building, Door, Hole, LocationGroup, MapUpdate, Source, Space
from c3nav.mapdata.models.access import AccessPermission
from c3nav.mapdata.models.geometry.base import GeometryMixin
from c3nav.mapdata.models.geometry.level import LevelGeometryMixin
from c3nav.mapdata.models.geometry.space import POI, Area, Column, LineObstacle, Obstacle, SpaceGeometryMixin, Stair
from c3nav.mapdata.models.level import Level
from c3nav.mapdata.models.locations import (Location, LocationGroupCategory, LocationRedirect, LocationSlug,
SpecificLocation)
from c3nav.mapdata.utils.locations import (get_location_by_slug_for_request, searchable_locations_for_request,
visible_locations_for_request)
from c3nav.mapdata.utils.models import get_submodels
@ -223,9 +223,6 @@ class LocationViewSet(RetrieveModelMixin, GenericViewSet):
queryset = LocationSlug.objects.all()
lookup_field = 'slug'
def get_queryset(self, can=None):
return LocationSlug.location_qs_for_request(self.request, can=can)
@simple_api_cache()
def list(self, request, *args, **kwargs):
searchable = 'searchable' in request.GET
@ -238,72 +235,34 @@ class LocationViewSet(RetrieveModelMixin, GenericViewSet):
)
result = cache.get(cache_key, None)
if result is None:
queryset_cache_key = 'mapdata:api:location:queryset:%d:%s' % (
searchable,
AccessPermission.cache_key_for_request(self.request)
)
queryset = cache.get(queryset_cache_key, None)
if queryset is None:
queryset = self.get_queryset(can=(('search', ) if searchable else ('search', 'describe')))
queryset = tuple(obj.get_child() for obj in queryset)
if searchable:
queryset = sorted(queryset, key=operator.attrgetter('order'), reverse=True)
else:
queryset = tuple(queryset)
# add locations to groups
locationgroups = {obj.pk: obj for obj in queryset if isinstance(obj, LocationGroup)}
for group in locationgroups.values():
group.locations = []
for obj in queryset:
if not isinstance(obj, SpecificLocation):
continue
for group in obj.groups.all():
group = locationgroups.get(group.pk, None)
if group is not None:
group.locations.append(obj)
# add levels to spaces
levels = {obj.pk: obj for obj in queryset if isinstance(obj, Level)}
for obj in queryset:
if isinstance(obj, LevelGeometryMixin):
obj.level_cache = levels.get(obj.level_id, None)
# add spaces to areas and POIs
spaces = {obj.pk: obj for obj in queryset if isinstance(obj, Space)}
for obj in queryset:
if isinstance(obj, SpaceGeometryMixin):
obj.space_cache = spaces.get(obj.space_id, None)
# precache cached properties
for obj in queryset:
# noinspection PyStatementEffect
obj.subtitle, obj.order
if isinstance(obj, GeometryMixin):
# noinspection PyStatementEffect
obj.centroid
cache.set(queryset_cache_key, queryset, 300)
if searchable:
locations = searchable_locations_for_request(self.request)
else:
locations = visible_locations_for_request(self.request).values()
result = tuple(obj.serialize(include_type=True, detailed=detailed, geometry=geometry, simple_geometry=True)
for obj in queryset)
for obj in locations)
cache.set(cache_key, result, 300)
return Response(result)
@simple_api_cache()
def retrieve(self, request, slug=None, *args, **kwargs):
result = Location.get_by_slug(slug, request)
if result is None:
show_redirects = 'show_redirects' in request.GET
detailed = 'detailed' in request.GET
geometry = 'geometry' in request.GET
location = get_location_by_slug_for_request(slug, request)
if location is None:
raise NotFound
result = result.get_child()
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.serialize(include_type=True, detailed='detailed' in request.GET))
if isinstance(location, LocationRedirect):
if not show_redirects:
return redirect('../' + location.target.slug) # todo: why does redirect/reverse not work here?
return Response(location.serialize(include_type=True, detailed=detailed,
geometry=geometry, simple_geometry=True))
@list_route(methods=['get'])
@simple_api_cache(permissions=False)

View file

@ -65,9 +65,9 @@ class GeometryMixin(SerializableMixin):
def centroid(self):
return self.geometry.centroid
def serialize(self, geometry=True, **kwargs):
result = super().serialize(geometry=geometry, **kwargs)
if geometry:
def serialize(self, **kwargs):
result = super().serialize(**kwargs)
if 'geometry' in result:
result.move_to_end('geometry')
return result

View file

@ -1,11 +1,8 @@
import operator
from collections import OrderedDict
from contextlib import suppress
from functools import reduce
from django.apps import apps
from django.db import models
from django.db.models import Prefetch, Q
from django.db.models import Prefetch
from django.utils.functional import cached_property
from django.utils.text import format_lazy
from django.utils.translation import ugettext_lazy as _
@ -69,29 +66,6 @@ class LocationSlug(SerializableMixin, models.Model):
verbose_name_plural = _('Location with Slug')
default_related_name = 'locationslugs'
@classmethod
def location_qs_for_request(cls, request, can=None):
queryset = cls.objects.all().order_by('id')
conditions = []
for model in get_submodels(Location):
related_name = model._meta.default_related_name
condition = Q(**{related_name + '__isnull': False})
if can:
condition &= reduce(operator.or_, (Q(**{related_name+'__can_'+s: True}) for s in can))
# noinspection PyUnresolvedReferences
condition &= model.q_for_request(request, prefix=related_name + '__')
conditions.append(condition)
queryset = queryset.filter(reduce(operator.or_, conditions))
# prefetch locationgroups
base_qs = LocationGroup.qs_for_request(request).select_related('category')
for model in get_submodels(SpecificLocation):
queryset = queryset.prefetch_related(Prefetch(model._meta.default_related_name + '__groups',
queryset=base_qs))
return queryset
class Location(LocationSlug, AccessRestrictionMixin, TitledMixin, models.Model):
can_search = models.BooleanField(default=True, verbose_name=_('can be searched'))
@ -127,32 +101,6 @@ class Location(LocationSlug, AccessRestrictionMixin, TitledMixin, models.Model):
return code+':'+str(self.id)
return self.slug
@classmethod
def get_by_slug(cls, slug, request=None, can=None):
if request is None:
queryset = LocationSlug.objects.all()
else:
queryset = LocationSlug.location_qs_for_request(request, can)
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 self.titles and self.slug:

View file

@ -0,0 +1,155 @@
import operator
from functools import reduce
from typing import List, Mapping, Optional
from django.apps import apps
from django.core.cache import cache
from django.db.models import Prefetch, Q
from c3nav.mapdata.models import Level, Location, LocationGroup
from c3nav.mapdata.models.access import AccessPermission
from c3nav.mapdata.models.geometry.base import GeometryMixin
from c3nav.mapdata.models.geometry.level import LevelGeometryMixin, Space
from c3nav.mapdata.models.geometry.space import SpaceGeometryMixin
from c3nav.mapdata.models.locations import LocationRedirect, LocationSlug, SpecificLocation
from c3nav.mapdata.utils.models import get_submodels
def locations_for_request(request) -> Mapping[int, LocationSlug]:
cache_key = 'mapdata:locations:%s' % AccessPermission.cache_key_for_request(request)
locations = cache.get(cache_key, None)
if locations is not None:
return locations
locations = LocationSlug.objects.all().order_by('id')
conditions = []
for model in get_submodels(Location):
related_name = model._meta.default_related_name
condition = Q(**{related_name + '__isnull': False})
# noinspection PyUnresolvedReferences
condition &= model.q_for_request(request, prefix=related_name + '__')
conditions.append(condition)
locations = locations.filter(reduce(operator.or_, conditions))
locations.select_related('redirect', 'locationgroups__category')
# prefetch locationgroups
base_qs = LocationGroup.qs_for_request(request).select_related('category')
for model in get_submodels(SpecificLocation):
locations = locations.prefetch_related(Prefetch(model._meta.default_related_name + '__groups',
queryset=base_qs))
locations = {obj.pk: obj.get_child() for obj in locations}
# add locations to groups
locationgroups = {pk: obj for pk, obj in locations.items() if isinstance(obj, LocationGroup)}
for group in locationgroups.values():
group.locations = []
for obj in locations.values():
if not isinstance(obj, SpecificLocation):
continue
for group in obj.groups.all():
group = locationgroups.get(group.pk, None)
if group is not None:
group.locations.append(obj)
# add levels to spaces
levels = {pk: obj for pk, obj in locations.items() if isinstance(obj, Level)}
for obj in locations.values():
if isinstance(obj, LevelGeometryMixin):
obj.level_cache = levels.get(obj.level_id, None)
# add spaces to areas and POIs
spaces = {pk: obj for pk, obj in locations.items() if isinstance(obj, Space)}
for obj in locations.values():
if isinstance(obj, SpaceGeometryMixin):
obj.space_cache = spaces.get(obj.space_id, None)
# add targets to LocationRedirects
levels = {pk: obj for pk, obj in locations.items() if isinstance(obj, Level)}
for obj in locations.values():
if isinstance(obj, LocationRedirect):
obj.target_cache = locations.get(obj.target_id, None)
# precache cached properties
for obj in locations.values():
# noinspection PyStatementEffect
obj.subtitle, obj.order
if isinstance(obj, GeometryMixin):
# noinspection PyStatementEffect
obj.centroid
cache.set(cache_key, locations, 300)
return locations
def visible_locations_for_request(request) -> Mapping[int, Location]:
cache_key = 'mapdata:locations:real:%s' % AccessPermission.cache_key_for_request(request)
locations = cache.get(cache_key, None)
if locations is not None:
return locations
locations = {pk: location for pk, location in locations_for_request(request).items()
if not isinstance(location, LocationRedirect) and (location.can_search or location.can_describe)}
cache.set(cache_key, locations, 300)
return locations
def searchable_locations_for_request(request) -> List[Location]:
cache_key = 'mapdata:locations:searchable:%s' % AccessPermission.cache_key_for_request(request)
locations = cache.get(cache_key, None)
if locations is not None:
return locations
locations = (location for location in locations_for_request(request).values() if isinstance(location, Location))
locations = tuple(location for location in locations if location.can_search)
locations = sorted(locations, key=operator.attrgetter('order'), reverse=True)
cache.set(cache_key, locations, 300)
return locations
def locations_by_slug_for_request(request) -> Mapping[str, LocationSlug]:
cache_key = 'mapdata:locations:by_slug:%s' % AccessPermission.cache_key_for_request(request)
locations = cache.get(cache_key, None)
if locations is not None:
return locations
locations = {location.slug: location for location in locations_for_request(request).values() if location.slug}
cache.set(cache_key, locations, 300)
return locations
def get_location_by_slug_for_request(slug: str, request) -> Optional[LocationSlug]:
cache_key = 'mapdata:location:by_slug:%s' % AccessPermission.cache_key_for_request(request)
locations = cache.get(cache_key, None)
if locations is not None:
return locations
if ':' in slug:
code, pk = slug.split(':', 1)
model_name = LocationSlug.LOCATION_TYPE_BY_CODE.get(code)
if model_name is None or not pk.isdigit():
return None
model = apps.get_model('mapdata', model_name)
location = locations_for_request(request).get(int(pk), None)
if location is None or not isinstance(location, model):
return None
if location.slug is not None:
location = LocationRedirect(slug=slug, target=location)
else:
location = locations_by_slug_for_request(request).get(slug, None)
cache.set(cache_key, location, 300)
return locations