quests can now be done!
This commit is contained in:
parent
df777ecc05
commit
d811170716
14 changed files with 219 additions and 63 deletions
|
@ -21,7 +21,7 @@ from c3nav.mapdata.forms import I18nModelFormMixin
|
||||||
from c3nav.mapdata.models import MapUpdate, Space
|
from c3nav.mapdata.models import MapUpdate, Space
|
||||||
from c3nav.mapdata.models.access import (AccessPermission, AccessPermissionToken, AccessPermissionTokenItem,
|
from c3nav.mapdata.models.access import (AccessPermission, AccessPermissionToken, AccessPermissionTokenItem,
|
||||||
AccessRestriction, AccessRestrictionGroup)
|
AccessRestriction, AccessRestrictionGroup)
|
||||||
from c3nav.mapdata.quests import quest_types
|
from c3nav.mapdata.quests.base import quest_types
|
||||||
from c3nav.site.models import Announcement
|
from c3nav.site.models import Announcement
|
||||||
|
|
||||||
|
|
||||||
|
|
20
src/c3nav/editor/templates/editor/quest_form.html
Normal file
20
src/c3nav/editor/templates/editor/quest_form.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{% extends 'site/base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="account">
|
||||||
|
<h3>{{ title }}</h3>
|
||||||
|
|
||||||
|
{% if back_url %}
|
||||||
|
<p>
|
||||||
|
<a href="{{ back_url }}">« {% trans 'back' %}</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action="{{ request.path_info }}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<button type="submit">{% trans 'Submit answer' %}</button>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
10
src/c3nav/editor/templates/editor/thanks.html
Normal file
10
src/c3nav/editor/templates/editor/thanks.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{% extends 'site/base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="account">
|
||||||
|
<h3>{% trans 'Thank you!' %}</h3>
|
||||||
|
|
||||||
|
<p>{% trans 'Have a cookie <3 🍪' %}</p>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
|
@ -1,10 +1,12 @@
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
from c3nav.editor.views.account import change_password_view, login_view, logout_view, register_view
|
from c3nav.editor.views.account import change_password_view, login_view, logout_view, register_view
|
||||||
from c3nav.editor.views.changes import changeset_detail, changeset_edit, changeset_redirect
|
from c3nav.editor.views.changes import changeset_detail, changeset_edit, changeset_redirect
|
||||||
from c3nav.editor.views.edit import edit, graph_edit, level_detail, list_objects, main_index, sourceimage, space_detail
|
from c3nav.editor.views.edit import edit, graph_edit, level_detail, list_objects, main_index, sourceimage, space_detail
|
||||||
from c3nav.editor.views.overlays import overlays_list, overlay_features, overlay_feature_edit
|
from c3nav.editor.views.overlays import overlays_list, overlay_features, overlay_feature_edit
|
||||||
|
from c3nav.editor.views.quest import QuestFormView
|
||||||
from c3nav.editor.views.users import user_detail, user_redirect
|
from c3nav.editor.views.users import user_detail, user_redirect
|
||||||
|
|
||||||
|
|
||||||
|
@ -55,6 +57,8 @@ urlpatterns = [
|
||||||
path('logout', logout_view, name='editor.logout'),
|
path('logout', logout_view, name='editor.logout'),
|
||||||
path('register', register_view, name='editor.register'),
|
path('register', register_view, name='editor.register'),
|
||||||
path('change_password', change_password_view, name='editor.change_password'),
|
path('change_password', change_password_view, name='editor.change_password'),
|
||||||
|
path('quests/<str:quest_type>/<str:identifier>/', QuestFormView.as_view(), name='editor.quest'),
|
||||||
|
path('thanks/', TemplateView.as_view(template_name="editor/thanks.html"), name='editor.thanks'),
|
||||||
path('', main_index, name='editor.index'),
|
path('', main_index, name='editor.index'),
|
||||||
]
|
]
|
||||||
urlpatterns.extend(add_editor_urls('Level', with_list=False, explicit_edit=True))
|
urlpatterns.extend(add_editor_urls('Level', with_list=False, explicit_edit=True))
|
||||||
|
|
|
@ -26,14 +26,35 @@ from c3nav.mapdata.utils.user import can_access_editor
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def maybe_lock_changeset_to_edit(request):
|
def maybe_lock_changeset_to_edit(changeset):
|
||||||
""" Lock the changeset of the given request, if it can be locked (= has ever been saved to the database)"""
|
""" Lock the changeset of the given request, if it can be locked (= has ever been saved to the database)"""
|
||||||
if request.changeset.pk:
|
if changeset.pk:
|
||||||
with request.changeset.lock_to_edit(request=request) as changeset:
|
with changeset.lock_to_edit() as locked_changeset:
|
||||||
request.changeset = changeset
|
yield locked_changeset
|
||||||
yield
|
|
||||||
else:
|
else:
|
||||||
yield
|
yield changeset
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def within_changeset(changeset, user):
|
||||||
|
with maybe_lock_changeset_to_edit(changeset=changeset) as locked_changeset:
|
||||||
|
# Turn the changes from the changeset into a list of operations
|
||||||
|
operations = locked_changeset.as_operations
|
||||||
|
|
||||||
|
# Enable the overlay manager, temporarily applying the changeset changes
|
||||||
|
# commit is set to false, meaning all changes will be reset once we leave the manager
|
||||||
|
with DatabaseOverlayManager.enable(operations=operations, commit=False) as manager:
|
||||||
|
yield locked_changeset
|
||||||
|
if manager.operations:
|
||||||
|
# Add new operations to changeset
|
||||||
|
locked_changeset.changes.add_operations(manager.operations)
|
||||||
|
locked_changeset.save()
|
||||||
|
|
||||||
|
# Add new changeset update
|
||||||
|
update = locked_changeset.updates.create(user=user, objects_changed=True)
|
||||||
|
locked_changeset.last_update = update
|
||||||
|
locked_changeset.last_change = update
|
||||||
|
locked_changeset.save()
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
|
@ -71,24 +92,9 @@ def accesses_mapdata(func):
|
||||||
raise ValueError # todo: good error message, but this shouldn't happen
|
raise ValueError # todo: good error message, but this shouldn't happen
|
||||||
else:
|
else:
|
||||||
# For non-direct editing, we will interact with the changeset
|
# For non-direct editing, we will interact with the changeset
|
||||||
with maybe_lock_changeset_to_edit(request=request):
|
with within_changeset(changeset=request.changeset, user=request.user) as locked_changeset:
|
||||||
# Turn the changes from the changeset into a list of operations
|
request.changeset = locked_changeset
|
||||||
operations = request.changeset.as_operations
|
return func(request, *args, **kwargs)
|
||||||
|
|
||||||
# Enable the overlay manager, temporarily applying the changeset changes
|
|
||||||
# commit is set to false, meaning all changes will be reset once we leave the manager
|
|
||||||
with DatabaseOverlayManager.enable(operations=operations, commit=False) as manager:
|
|
||||||
result = func(request, *args, **kwargs)
|
|
||||||
if manager.operations:
|
|
||||||
# Add new operations to changeset
|
|
||||||
request.changeset.changes.add_operations(manager.operations)
|
|
||||||
request.changeset.save()
|
|
||||||
|
|
||||||
# Add new changeset update
|
|
||||||
update = request.changeset.updates.create(user=request.user, objects_changed=True)
|
|
||||||
request.changeset.last_update = update
|
|
||||||
request.changeset.last_change = update
|
|
||||||
request.changeset.save()
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
|
@ -62,7 +62,7 @@ def changeset_detail(request, pk):
|
||||||
return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk}))
|
return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk}))
|
||||||
|
|
||||||
elif request.POST.get('activate') == '1':
|
elif request.POST.get('activate') == '1':
|
||||||
with changeset.lock_to_edit(request) as changeset:
|
with changeset.lock_to_edit() as changeset:
|
||||||
if changeset.can_activate(request):
|
if changeset.can_activate(request):
|
||||||
changeset.activate(request)
|
changeset.activate(request)
|
||||||
messages.success(request, _('You activated this change set.'))
|
messages.success(request, _('You activated this change set.'))
|
||||||
|
@ -76,7 +76,7 @@ def changeset_detail(request, pk):
|
||||||
messages.info(request, _('You need to log in to propose changes.'))
|
messages.info(request, _('You need to log in to propose changes.'))
|
||||||
return redirect(reverse('editor.login') + '?r=' + request.path)
|
return redirect(reverse('editor.login') + '?r=' + request.path)
|
||||||
|
|
||||||
with changeset.lock_to_edit(request) as changeset:
|
with changeset.lock_to_edit() as changeset:
|
||||||
if not changeset.title:
|
if not changeset.title:
|
||||||
form = ChangeSetForm(instance=changeset, data=request.POST)
|
form = ChangeSetForm(instance=changeset, data=request.POST)
|
||||||
if not form.is_valid():
|
if not form.is_valid():
|
||||||
|
@ -108,7 +108,7 @@ def changeset_detail(request, pk):
|
||||||
messages.info(request, _('You need to log in to apply changes.'))
|
messages.info(request, _('You need to log in to apply changes.'))
|
||||||
return redirect(reverse('editor.login') + '?r=' + request.path)
|
return redirect(reverse('editor.login') + '?r=' + request.path)
|
||||||
|
|
||||||
with changeset.lock_to_edit(request) as changeset:
|
with changeset.lock_to_edit() as changeset:
|
||||||
if not changeset.title:
|
if not changeset.title:
|
||||||
form = ChangeSetForm(instance=changeset, data=request.POST)
|
form = ChangeSetForm(instance=changeset, data=request.POST)
|
||||||
if not form.is_valid():
|
if not form.is_valid():
|
||||||
|
@ -136,7 +136,7 @@ def changeset_detail(request, pk):
|
||||||
return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk}))
|
return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk}))
|
||||||
|
|
||||||
elif request.POST.get('unpropose') == '1':
|
elif request.POST.get('unpropose') == '1':
|
||||||
with changeset.lock_to_edit(request) as changeset:
|
with changeset.lock_to_edit() as changeset:
|
||||||
if changeset.can_unpropose(request):
|
if changeset.can_unpropose(request):
|
||||||
changeset.unpropose(request.user)
|
changeset.unpropose(request.user)
|
||||||
messages.success(request, _('You unproposed your changes.'))
|
messages.success(request, _('You unproposed your changes.'))
|
||||||
|
@ -146,7 +146,7 @@ def changeset_detail(request, pk):
|
||||||
return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk}))
|
return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk}))
|
||||||
|
|
||||||
elif request.POST.get('review') == '1':
|
elif request.POST.get('review') == '1':
|
||||||
with changeset.lock_to_edit(request) as changeset:
|
with changeset.lock_to_edit() as changeset:
|
||||||
if changeset.can_start_review(request):
|
if changeset.can_start_review(request):
|
||||||
changeset.start_review(request.user)
|
changeset.start_review(request.user)
|
||||||
messages.success(request, _('You are now reviewing these changes.'))
|
messages.success(request, _('You are now reviewing these changes.'))
|
||||||
|
@ -156,7 +156,7 @@ def changeset_detail(request, pk):
|
||||||
return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk}))
|
return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk}))
|
||||||
|
|
||||||
elif request.POST.get('reject') == '1':
|
elif request.POST.get('reject') == '1':
|
||||||
with changeset.lock_to_edit(request) as changeset:
|
with changeset.lock_to_edit() as changeset:
|
||||||
if not changeset.can_end_review(request):
|
if not changeset.can_end_review(request):
|
||||||
messages.error(request, _('You cannot reject these changes.'))
|
messages.error(request, _('You cannot reject these changes.'))
|
||||||
return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk}))
|
return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk}))
|
||||||
|
@ -176,7 +176,7 @@ def changeset_detail(request, pk):
|
||||||
})
|
})
|
||||||
|
|
||||||
elif request.POST.get('unreject') == '1':
|
elif request.POST.get('unreject') == '1':
|
||||||
with changeset.lock_to_edit(request) as changeset:
|
with changeset.lock_to_edit() as changeset:
|
||||||
if not changeset.can_unreject(request):
|
if not changeset.can_unreject(request):
|
||||||
messages.error(request, _('You cannot unreject these changes.'))
|
messages.error(request, _('You cannot unreject these changes.'))
|
||||||
return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk}))
|
return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk}))
|
||||||
|
@ -187,7 +187,7 @@ def changeset_detail(request, pk):
|
||||||
return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk}))
|
return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk}))
|
||||||
|
|
||||||
elif request.POST.get('apply') == '1':
|
elif request.POST.get('apply') == '1':
|
||||||
with changeset.lock_to_edit(request) as changeset:
|
with changeset.lock_to_edit() as changeset:
|
||||||
if not changeset.can_end_review(request):
|
if not changeset.can_end_review(request):
|
||||||
messages.error(request, _('You cannot accept and apply these changes.'))
|
messages.error(request, _('You cannot accept and apply these changes.'))
|
||||||
return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk}))
|
return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk}))
|
||||||
|
@ -204,7 +204,7 @@ def changeset_detail(request, pk):
|
||||||
return render(request, 'editor/changeset_apply.html', {})
|
return render(request, 'editor/changeset_apply.html', {})
|
||||||
|
|
||||||
elif request.POST.get('delete') == '1':
|
elif request.POST.get('delete') == '1':
|
||||||
with changeset.lock_to_edit(request) as changeset:
|
with changeset.lock_to_edit() as changeset:
|
||||||
if not changeset.can_delete(request):
|
if not changeset.can_delete(request):
|
||||||
messages.error(request, _('You cannot delete this change set.'))
|
messages.error(request, _('You cannot delete this change set.'))
|
||||||
|
|
||||||
|
@ -502,7 +502,7 @@ def changeset_edit(request, pk):
|
||||||
if str(pk) != str(request.changeset.pk):
|
if str(pk) != str(request.changeset.pk):
|
||||||
changeset = get_object_or_404(ChangeSet.qs_for_request(request), pk=pk)
|
changeset = get_object_or_404(ChangeSet.qs_for_request(request), pk=pk)
|
||||||
|
|
||||||
with changeset.lock_to_edit(request) as changeset:
|
with changeset.lock_to_edit() as changeset:
|
||||||
if not changeset.can_edit(request):
|
if not changeset.can_edit(request):
|
||||||
messages.error(request, _('You cannot edit this change set.'))
|
messages.error(request, _('You cannot edit this change set.'))
|
||||||
return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk}))
|
return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk}))
|
||||||
|
|
|
@ -179,7 +179,7 @@ def overlay_feature_edit(request, level=None, overlay=None, pk=None):
|
||||||
if not new and ((request.POST.get('delete') == '1' and delete is not False) or delete):
|
if not new and ((request.POST.get('delete') == '1' and delete is not False) or delete):
|
||||||
# Delete this mapitem!
|
# Delete this mapitem!
|
||||||
if request.POST.get('delete_confirm') == '1' or delete:
|
if request.POST.get('delete_confirm') == '1' or delete:
|
||||||
with request.changeset.lock_to_edit(request) as changeset:
|
with request.changeset.lock_to_edit() as changeset:
|
||||||
if changeset.can_edit(request):
|
if changeset.can_edit(request):
|
||||||
obj.delete()
|
obj.delete()
|
||||||
else:
|
else:
|
||||||
|
@ -210,7 +210,7 @@ def overlay_feature_edit(request, level=None, overlay=None, pk=None):
|
||||||
obj.level = level
|
obj.level = level
|
||||||
obj.overlay = overlay
|
obj.overlay = overlay
|
||||||
|
|
||||||
with request.changeset.lock_to_edit(request) as changeset:
|
with request.changeset.lock_to_edit() as changeset:
|
||||||
if changeset.can_edit(request):
|
if changeset.can_edit(request):
|
||||||
try:
|
try:
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
41
src/c3nav/editor/views/quest.py
Normal file
41
src/c3nav/editor/views/quest.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
from functools import cached_property
|
||||||
|
|
||||||
|
from django.http import Http404
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.views.generic.edit import FormView
|
||||||
|
|
||||||
|
from c3nav.mapdata.quests.base import get_quest_for_request
|
||||||
|
|
||||||
|
|
||||||
|
class QuestFormView(FormView):
|
||||||
|
template_name = "editor/quest_form.html"
|
||||||
|
success_url = reverse_lazy("editor.thanks")
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def quest(self):
|
||||||
|
quest = get_quest_for_request(request=self.request,
|
||||||
|
quest_type=self.kwargs["quest_type"],
|
||||||
|
identifier=self.kwargs["identifier"])
|
||||||
|
if quest is None:
|
||||||
|
raise Http404
|
||||||
|
return quest
|
||||||
|
|
||||||
|
def get_form_class(self):
|
||||||
|
return self.quest.get_form_class()
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
return {
|
||||||
|
"request": self.request,
|
||||||
|
**super().get_form_kwargs(),
|
||||||
|
**self.quest.get_form_kwargs(request=self.request),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
return {
|
||||||
|
**super().get_context_data(**kwargs),
|
||||||
|
"title": self.quest.quest_type_label,
|
||||||
|
}
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.save()
|
||||||
|
return super().form_valid(form)
|
|
@ -21,7 +21,7 @@ from c3nav.mapdata.grid import grid
|
||||||
from c3nav.mapdata.models import Source, Theme, Area, Space
|
from c3nav.mapdata.models import Source, Theme, Area, Space
|
||||||
from c3nav.mapdata.models.geometry.space import ObstacleGroup, Obstacle
|
from c3nav.mapdata.models.geometry.space import ObstacleGroup, Obstacle
|
||||||
from c3nav.mapdata.models.locations import DynamicLocation, LocationRedirect, Position, LocationGroup
|
from c3nav.mapdata.models.locations import DynamicLocation, LocationRedirect, Position, LocationGroup
|
||||||
from c3nav.mapdata.quests import QuestSchema, get_all_quests_for_request
|
from c3nav.mapdata.quests.base import QuestSchema, get_all_quests_for_request
|
||||||
from c3nav.mapdata.render.theme import ColorManager
|
from c3nav.mapdata.render.theme import ColorManager
|
||||||
from c3nav.mapdata.schemas.filters import BySearchableFilter, RemoveGeometryFilter
|
from c3nav.mapdata.schemas.filters import BySearchableFilter, RemoveGeometryFilter
|
||||||
from c3nav.mapdata.schemas.model_base import AnyLocationID, AnyPositionID, CustomLocationID
|
from c3nav.mapdata.schemas.model_base import AnyLocationID, AnyPositionID, CustomLocationID
|
||||||
|
|
0
src/c3nav/mapdata/quests/__init__.py
Normal file
0
src/c3nav/mapdata/quests/__init__.py
Normal file
|
@ -1,17 +1,16 @@
|
||||||
from abc import abstractmethod
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from typing import Self, Optional, Any, Type
|
from typing import Any, Self, Optional, Type
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.forms import ModelForm
|
||||||
from pydantic import BaseModel
|
from pydantic import TypeAdapter, BaseModel
|
||||||
from pydantic.type_adapter import TypeAdapter
|
|
||||||
from shapely.geometry import Point, mapping
|
|
||||||
|
|
||||||
from c3nav.api.schema import BaseSchema, PointSchema
|
from c3nav.api.schema import BaseSchema, PointSchema
|
||||||
|
from c3nav.editor.models import ChangeSet
|
||||||
|
from c3nav.editor.views.base import within_changeset
|
||||||
|
|
||||||
from c3nav.mapdata.models.access import AccessPermission
|
from c3nav.mapdata.models.access import AccessPermission
|
||||||
from c3nav.mapdata.models.geometry.space import RangingBeacon
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -19,7 +18,6 @@ class Quest:
|
||||||
obj: Any
|
obj: Any
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
|
||||||
def point(self) -> dict:
|
def point(self) -> dict:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@ -43,8 +41,11 @@ class Quest:
|
||||||
def get_for_request(cls, request, identifier: Any) -> Optional[Self]:
|
def get_for_request(cls, request, identifier: Any) -> Optional[Self]:
|
||||||
if not identifier.isdigit():
|
if not identifier.isdigit():
|
||||||
return None
|
return None
|
||||||
|
if not (request.user.is_superuser or cls.quest_type in request.user_permissions.quests):
|
||||||
|
return None
|
||||||
|
|
||||||
results = list(chain(
|
results = list(chain(
|
||||||
+(cls._obj_to_quests(obj) for obj in cls._qs_for_request(request).filter(pk=int(identifier)))
|
*(cls._obj_to_quests(obj) for obj in cls._qs_for_request(request).filter(pk=int(identifier)))
|
||||||
))
|
))
|
||||||
if len(results) > 1:
|
if len(results) > 1:
|
||||||
raise ValueError('wrong number of results')
|
raise ValueError('wrong number of results')
|
||||||
|
@ -52,6 +53,8 @@ class Quest:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all_for_request(cls, request) -> list[Self]:
|
def get_all_for_request(cls, request) -> list[Self]:
|
||||||
|
if not (request.user.is_superuser or cls.quest_type in request.user_permissions.quests):
|
||||||
|
return None
|
||||||
return list(chain(
|
return list(chain(
|
||||||
*(cls._obj_to_quests(obj) for obj in cls._qs_for_request(request))
|
*(cls._obj_to_quests(obj) for obj in cls._qs_for_request(request))
|
||||||
))
|
))
|
||||||
|
@ -67,6 +70,32 @@ class Quest:
|
||||||
cache.set(cache_key, result, 900)
|
cache.set(cache_key, result, 900)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def get_form_class(self):
|
||||||
|
return self.form_class
|
||||||
|
|
||||||
|
def get_form_kwargs(self, request):
|
||||||
|
return {"instance": self.obj}
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeSetModelForm(ModelForm):
|
||||||
|
def __init__(self, request, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.request = request
|
||||||
|
|
||||||
|
@property
|
||||||
|
def changeset_title(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def save(self, **kwargs):
|
||||||
|
changeset = ChangeSet()
|
||||||
|
changeset.author = self.request.user
|
||||||
|
with within_changeset(changeset=changeset, user=self.request.user) as locked_changeset:
|
||||||
|
super().save(**kwargs)
|
||||||
|
with changeset.lock_to_edit() as locked_changeset:
|
||||||
|
locked_changeset.title = self.changeset_title
|
||||||
|
locked_changeset.description = 'quest'
|
||||||
|
locked_changeset.apply(self.request.user)
|
||||||
|
|
||||||
|
|
||||||
quest_types: dict[str, Type[BaseModel]] = {}
|
quest_types: dict[str, Type[BaseModel]] = {}
|
||||||
|
|
||||||
|
@ -76,20 +105,11 @@ def register_quest(cls):
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
|
||||||
@register_quest
|
def get_quest_for_request(request, quest_type: str, identifier: str) -> Optional[Quest]:
|
||||||
@dataclass
|
quest_cls = quest_types.get(quest_type, None)
|
||||||
class RangingBeaconAltitudeQuest(Quest):
|
if quest_cls is None:
|
||||||
quest_type = "ranging_beacon_altitude"
|
return None
|
||||||
quest_type_label = _('Ranging Beacon Altitude')
|
return quest_cls.get_for_request(request, identifier)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class QuestSchema(BaseSchema):
|
class QuestSchema(BaseSchema):
|
52
src/c3nav/mapdata/quests/simple.py
Normal file
52
src/c3nav/mapdata/quests/simple.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from shapely import Point
|
||||||
|
from shapely.geometry import mapping
|
||||||
|
|
||||||
|
from c3nav.mapdata.models.geometry.space import RangingBeacon
|
||||||
|
from c3nav.mapdata.quests.base import register_quest, Quest, ChangeSetModelForm
|
||||||
|
|
||||||
|
|
||||||
|
@register_quest
|
||||||
|
@dataclass
|
||||||
|
class RangingBeaconAltitudeQuest(Quest):
|
||||||
|
quest_type = "ranging_beacon_altitude"
|
||||||
|
quest_type_label = _('Ranging Beacon Altitude')
|
||||||
|
form_class = RangingBeaconAltitudeQuestForm
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
class RangingBeaconAltitudeQuestForm(ChangeSetModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields["altitude"].label = (
|
||||||
|
_('How many meters above ground is the access point “%s” mounted?') % self.instance.title
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean_altitude(self):
|
||||||
|
data = self.cleaned_data["altitude"]
|
||||||
|
if not data:
|
||||||
|
raise ValidationError(_("The AP should not be 0m above ground."))
|
||||||
|
return data
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RangingBeacon
|
||||||
|
fields = ("altitude", )
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.instance.altitude_quest = False
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def changeset_title(self):
|
||||||
|
return f'Altitude Quest: {self.instance.title}'
|
|
@ -6,7 +6,6 @@ from django.utils.translation import ngettext_lazy
|
||||||
from c3nav.mapdata.models import DataOverlay
|
from c3nav.mapdata.models import DataOverlay
|
||||||
from c3nav.mapdata.models.access import AccessPermission, AccessRestriction
|
from c3nav.mapdata.models.access import AccessPermission, AccessRestriction
|
||||||
from c3nav.mapdata.models.locations import Position
|
from c3nav.mapdata.models.locations import Position
|
||||||
from c3nav.mapdata.quests import quest_types
|
|
||||||
from c3nav.mapdata.schemas.models import DataOverlaySchema
|
from c3nav.mapdata.schemas.models import DataOverlaySchema
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,6 +34,7 @@ def get_user_data(request):
|
||||||
result['title'] = request.user.username
|
result['title'] = request.user.username
|
||||||
|
|
||||||
# todo: cache this
|
# todo: cache this
|
||||||
|
from c3nav.mapdata.quests.base import quest_types
|
||||||
result.update({
|
result.update({
|
||||||
'overlays': [
|
'overlays': [
|
||||||
DataOverlaySchema.model_validate(overlay).model_dump()
|
DataOverlaySchema.model_validate(overlay).model_dump()
|
||||||
|
|
|
@ -2494,7 +2494,10 @@ QuestsControl = L.Control.extend({
|
||||||
c3nav_api.get('map/quests/')
|
c3nav_api.get('map/quests/')
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
for (const quest of data) {
|
for (const quest of data) {
|
||||||
const layer = L.geoJson(quest.point, {}).addTo(c3nav._questsLayers[quest.level_id]);
|
L.geoJson(quest.point, {}).addTo(c3nav._questsLayers[quest.level_id]).on('click', function() {
|
||||||
|
c3nav.open_modal();
|
||||||
|
$.get(`/editor/quests/${quest.quest_type}/${quest.identifier}`, c3nav._modal_loaded).fail(c3nav._modal_error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch();
|
.catch();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue