From 2805061c47f7408a3cfa618bdd424963084fc7ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Tue, 24 Dec 2024 18:50:53 +0100 Subject: [PATCH] generate quests, but they can't be solved yet --- src/c3nav/control/forms.py | 14 ++- src/c3nav/control/models.py | 2 + src/c3nav/mapdata/api/map.py | 12 +++ src/c3nav/mapdata/models/geometry/space.py | 2 + src/c3nav/mapdata/quests.py | 107 +++++++++++++++++++++ 5 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 src/c3nav/mapdata/quests.py diff --git a/src/c3nav/control/forms.py b/src/c3nav/control/forms.py index 87f86ea4..90428e7f 100644 --- a/src/c3nav/control/forms.py +++ b/src/c3nav/control/forms.py @@ -10,7 +10,7 @@ from typing import Sequence from django.contrib.auth.models import User from django.db.models import Prefetch -from django.forms import ChoiceField, Form, IntegerField, ModelForm, Select +from django.forms import ChoiceField, Form, IntegerField, ModelForm, Select, MultipleChoiceField from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.utils.translation import ngettext_lazy @@ -21,6 +21,7 @@ from c3nav.mapdata.forms import I18nModelFormMixin from c3nav.mapdata.models import MapUpdate, Space from c3nav.mapdata.models.access import (AccessPermission, AccessPermissionToken, AccessPermissionTokenItem, AccessRestriction, AccessRestrictionGroup) +from c3nav.mapdata.quests import quest_types from c3nav.site.models import Announcement @@ -28,10 +29,19 @@ class UserPermissionsForm(ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['review_group_reports'].label_from_instance = lambda obj: obj.title + self.fields['allowed_quests'] = MultipleChoiceField( + label=_('Available quests'), + choices=[(key, quest.quest_type_label) for key, quest in quest_types.items()], + initial=self.instance.quests, + ) + + def save(self, *args, **kwargs): + self.instance.quests = self.cleaned_data['allowed_quests'] + super().save() class Meta: model = UserPermissions - exclude = ('user', 'max_changeset_changes', 'api_secret') + exclude = ('user', 'max_changeset_changes', 'api_secret', 'quests') class AccessPermissionForm(Form): diff --git a/src/c3nav/control/models.py b/src/c3nav/control/models.py index e879418b..0d90020e 100644 --- a/src/c3nav/control/models.py +++ b/src/c3nav/control/models.py @@ -7,6 +7,7 @@ from django.core.cache import cache from django.db import models, transaction from django.utils.functional import cached_property, lazy from django.utils.translation import gettext_lazy as _ +from django_pydantic_field.fields import SchemaField from c3nav.mapdata.models import Space from c3nav.mapdata.models.access import AccessPermission @@ -41,6 +42,7 @@ class UserPermissions(models.Model): mesh_control = models.BooleanField(default=False, verbose_name=_('can access mesh control')) nonpublic_themes = models.BooleanField(default=False, verbose_name=_('show non-public themes in theme selector')) + quests: list[str] = SchemaField(schema=list[str], default=list) class Meta: verbose_name = _('User Permissions') diff --git a/src/c3nav/mapdata/api/map.py b/src/c3nav/mapdata/api/map.py index 459b8f25..7d1a3407 100644 --- a/src/c3nav/mapdata/api/map.py +++ b/src/c3nav/mapdata/api/map.py @@ -21,6 +21,7 @@ from c3nav.mapdata.grid import grid from c3nav.mapdata.models import Source, Theme, Area, Space from c3nav.mapdata.models.geometry.space import ObstacleGroup, Obstacle from c3nav.mapdata.models.locations import DynamicLocation, LocationRedirect, Position, LocationGroup +from c3nav.mapdata.quests import QuestSchema, get_all_quests_for_request from c3nav.mapdata.render.theme import ColorManager from c3nav.mapdata.schemas.filters import BySearchableFilter, RemoveGeometryFilter from c3nav.mapdata.schemas.model_base import AnyLocationID, AnyPositionID, CustomLocationID @@ -391,3 +392,14 @@ def legend_for_theme(request, theme_id: int): for group in obstaclegroups) if item.fill or item.border], ) + + +""" +Quests +""" + + +@map_api_router.get('/quests/', summary="get open quests", + response={200: list[QuestSchema], **auth_responses}) +def list_quests(request): + return get_all_quests_for_request(request) \ No newline at end of file diff --git a/src/c3nav/mapdata/models/geometry/space.py b/src/c3nav/mapdata/models/geometry/space.py index 865a7628..2ecc338b 100644 --- a/src/c3nav/mapdata/models/geometry/space.py +++ b/src/c3nav/mapdata/models/geometry/space.py @@ -493,6 +493,8 @@ class RangingBeacon(SpaceGeometryMixin, models.Model): validators=[MinValueValidator(Decimal('0'))]) comment = models.TextField(null=True, blank=True, verbose_name=_('comment')) + altitude_quest = models.BooleanField(_('altitude quest'), default=True) + class Meta: verbose_name = _('Ranging beacon') verbose_name_plural = _('Ranging beacons') diff --git a/src/c3nav/mapdata/quests.py b/src/c3nav/mapdata/quests.py new file mode 100644 index 00000000..c92b09a7 --- /dev/null +++ b/src/c3nav/mapdata/quests.py @@ -0,0 +1,107 @@ +from abc import abstractmethod +from dataclasses import dataclass +from itertools import chain +from typing import Self, Optional, Any, Type + +from django.core.cache import cache +from django.utils.translation import gettext_lazy as _ +from pydantic import BaseModel +from pydantic.type_adapter import TypeAdapter +from shapely.geometry import Point, mapping + +from c3nav.api.schema import BaseSchema, PointSchema +from c3nav.mapdata.models.access import AccessPermission +from c3nav.mapdata.models.geometry.space import RangingBeacon + + +@dataclass +class Quest: + obj: Any + + @property + @abstractmethod + def point(self) -> dict: + raise NotImplementedError + + @property + def level_id(self) -> int: + return self.obj.level_id + + @property + def identifier(self) -> str: + return str(self.obj.pk) + + @classmethod + def _qs_for_request(cls, request): + raise NotImplementedError + + @classmethod + def _obj_to_quests(cls, obj) -> list[Self]: + return [cls(obj=obj)] + + @classmethod + def get_for_request(cls, request, identifier: Any) -> Optional[Self]: + if not identifier.isdigit(): + return None + results = list(chain( + +(cls._obj_to_quests(obj) for obj in cls._qs_for_request(request).filter(pk=int(identifier))) + )) + if len(results) > 1: + raise ValueError('wrong number of results') + return results[0] if results else None + + @classmethod + def get_all_for_request(cls, request) -> list[Self]: + return list(chain( + *(cls._obj_to_quests(obj) for obj in cls._qs_for_request(request)) + )) + + @classmethod + def cached_get_all_for_request(cls, request) -> list["QuestSchema"]: + cache_key = f'quests:{cls.identifier}:{AccessPermission.cache_key_for_request(request)}' + result = cache.get(cache_key, None) + if result is not None: + return result + adapter = TypeAdapter(list[QuestSchema]) + result = adapter.dump_python(adapter.validate_python(cls.get_all_for_request(request))) + cache.set(cache_key, result, 900) + return result + + +quest_types: dict[str, Type[BaseModel]] = {} + + +def register_quest(cls): + quest_types[cls.quest_type] = cls + return cls + + +@register_quest +@dataclass +class RangingBeaconAltitudeQuest(Quest): + quest_type = "ranging_beacon_altitude" + quest_type_label = _('Ranging Beacon Altitude') + obj: RangingBeacon + + @property + def point(self) -> Point: + return mapping(self.obj.geometry) + + @classmethod + def _qs_for_request(cls, request): + return RangingBeacon.qs_for_request(request).select_related('space').filter(altitude_quest=True)[:10] + + +class QuestSchema(BaseSchema): + quest_type: str + identifier: str + level_id: int + point: PointSchema + + +def get_all_quests_for_request(request) -> list[QuestSchema]: + return list(chain(*( + quest.cached_get_all_for_request(request) + for key, quest in quest_types.items() + if request.user.is_superuser or key in request.user_permissions.quests + )))