generate quests, but they can't be solved yet

This commit is contained in:
Laura Klünder 2024-12-24 18:50:53 +01:00
parent 38671a7947
commit 2805061c47
5 changed files with 135 additions and 2 deletions

View file

@ -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):

View file

@ -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')

View file

@ -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)

View file

@ -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')

107
src/c3nav/mapdata/quests.py Normal file
View file

@ -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
)))