export entire location query logic into mapdata.utils.locations and cache
This commit is contained in:
parent
247d6b119e
commit
620323b808
4 changed files with 180 additions and 118 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
155
src/c3nav/mapdata/utils/locations.py
Normal file
155
src/c3nav/mapdata/utils/locations.py
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue