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 mimetypes
import operator
from functools import wraps from functools import wraps
from django.core.cache import cache 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 import AccessRestriction, Building, Door, Hole, LocationGroup, MapUpdate, Source, Space
from c3nav.mapdata.models.access import AccessPermission 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.level import LevelGeometryMixin
from c3nav.mapdata.models.geometry.space import POI, Area, Column, LineObstacle, Obstacle, SpaceGeometryMixin, Stair 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.level import Level
from c3nav.mapdata.models.locations import (Location, LocationGroupCategory, LocationRedirect, LocationSlug, from c3nav.mapdata.models.locations import (Location, LocationGroupCategory, LocationRedirect, LocationSlug,
SpecificLocation) 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 from c3nav.mapdata.utils.models import get_submodels
@ -223,9 +223,6 @@ class LocationViewSet(RetrieveModelMixin, GenericViewSet):
queryset = LocationSlug.objects.all() queryset = LocationSlug.objects.all()
lookup_field = 'slug' lookup_field = 'slug'
def get_queryset(self, can=None):
return LocationSlug.location_qs_for_request(self.request, can=can)
@simple_api_cache() @simple_api_cache()
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
searchable = 'searchable' in request.GET searchable = 'searchable' in request.GET
@ -238,72 +235,34 @@ class LocationViewSet(RetrieveModelMixin, GenericViewSet):
) )
result = cache.get(cache_key, None) result = cache.get(cache_key, None)
if result is None: if result is None:
queryset_cache_key = 'mapdata:api:location:queryset:%d:%s' % ( if searchable:
searchable, locations = searchable_locations_for_request(self.request)
AccessPermission.cache_key_for_request(self.request) else:
) locations = visible_locations_for_request(self.request).values()
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)
result = tuple(obj.serialize(include_type=True, detailed=detailed, geometry=geometry, simple_geometry=True) 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) cache.set(cache_key, result, 300)
return Response(result) return Response(result)
@simple_api_cache() @simple_api_cache()
def retrieve(self, request, slug=None, *args, **kwargs): def retrieve(self, request, slug=None, *args, **kwargs):
result = Location.get_by_slug(slug, request) show_redirects = 'show_redirects' in request.GET
if result is None: 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 raise NotFound
result = result.get_child()
if isinstance(result, LocationRedirect): if isinstance(location, LocationRedirect):
if 'show_redirects' in request.GET: if not show_redirects:
return Response(result.serialize(include_type=True)) return redirect('../' + location.target.slug) # todo: why does redirect/reverse not work here?
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)) return Response(location.serialize(include_type=True, detailed=detailed,
geometry=geometry, simple_geometry=True))
@list_route(methods=['get']) @list_route(methods=['get'])
@simple_api_cache(permissions=False) @simple_api_cache(permissions=False)

View file

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

View file

@ -1,11 +1,8 @@
import operator
from collections import OrderedDict from collections import OrderedDict
from contextlib import suppress from contextlib import suppress
from functools import reduce
from django.apps import apps
from django.db import models 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.functional import cached_property
from django.utils.text import format_lazy from django.utils.text import format_lazy
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -69,29 +66,6 @@ class LocationSlug(SerializableMixin, models.Model):
verbose_name_plural = _('Location with Slug') verbose_name_plural = _('Location with Slug')
default_related_name = 'locationslugs' 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): class Location(LocationSlug, AccessRestrictionMixin, TitledMixin, models.Model):
can_search = models.BooleanField(default=True, verbose_name=_('can be searched')) 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 code+':'+str(self.id)
return self.slug 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 @property
def title(self): def title(self):
if not self.titles and self.slug: 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