get rid of editor api hybrid responses

This commit is contained in:
Laura Klünder 2025-03-10 14:21:45 +01:00
parent 79fb026363
commit 09c092e129
4 changed files with 65 additions and 385 deletions

View file

@ -97,62 +97,6 @@ def level_geometries(request, level_id: EditorID, update_cache_key: UpdateCacheK
) )
# todo: need a way to pass the changeset if it's not a session API key
def resolve_editor_path_api(request, path):
resolved = None
if path:
try:
resolved = resolve('/editor/'+path+'/')
except Resolver404:
pass
if not resolved:
try:
resolved = resolve('/editor/'+path)
except Resolver404:
pass
request.sub_resolver_match = resolved
return resolved
@editor_api_router.get('/as_api/{path:path}', summary="raw editor access",
response={200: dict, **API404.dict(), **auth_permission_responses},
openapi_extra={"security": [{"APIKeyAuth": ["editor_access"]}]})
@api_etag() # todo: correct?
def get_view_as_api(request, path: str):
"""
get editor views rendered as JSON instead of HTML.
`path` is the path after /editor/.
this is a mess. good luck. if you actually want to use this, poke us so we might add better documentation.
"""
resolved = resolve_editor_path_api(request, path)
if not resolved:
raise API404(_('No matching editor view endpoint found.'))
if not getattr(resolved.func, 'api_hybrid', False):
raise API404(_('Matching editor view point does not provide an API.'))
response = resolved.func(request, api=True, *resolved.args, **resolved.kwargs)
return response
@editor_api_router.post('/as_api/{path:path}', summary="raw editor access",
response={200: dict, **API404.dict(), **auth_permission_responses},
openapi_extra={"security": [{"APIKeyAuth": ["editor_access", "write"]}]})
@api_etag() # todo: correct?
def post_view_as_api(request, path: str):
"""
get editor views rendered as JSON instead of HTML.
`path` is the path after /editor/.
this is a mess. good luck. if you actually want to use this, poke us so we might add better documentation.
"""
raise NotImplementedError
@editor_api_router.get('/beacons-lookup/', summary="get beacon coordinates", @editor_api_router.get('/beacons-lookup/', summary="get beacon coordinates",
description="get xyz coordinates for all known positioning beacons", description="get xyz coordinates for all known positioning beacons",
response={200: EditorBeaconsLookup, **auth_permission_responses}, response={200: EditorBeaconsLookup, **auth_permission_responses},

View file

@ -9,7 +9,6 @@ from django.contrib.auth.views import redirect_to_login
from django.contrib.messages import DEFAULT_TAGS as DEFAULT_MESSAGE_TAGS from django.contrib.messages import DEFAULT_TAGS as DEFAULT_MESSAGE_TAGS
from django.contrib.messages import get_messages from django.contrib.messages import get_messages
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db.models import QuerySet
from django.http import HttpResponse, HttpResponseNotModified, HttpResponseRedirect from django.http import HttpResponse, HttpResponseNotModified, HttpResponseRedirect
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.utils.cache import patch_vary_headers from django.utils.cache import patch_vary_headers
@ -100,34 +99,24 @@ def accesses_mapdata(func):
return wrapped return wrapped
def sidebar_view(func=None, select_related=None, api_hybrid=False): def sidebar_view(func=None, select_related=None):
if func is None: if func is None:
def wrapped(inner_func): def wrapped(inner_func):
return sidebar_view(inner_func, select_related=select_related, api_hybrid=api_hybrid) return sidebar_view(inner_func, select_related=select_related)
return wrapped return wrapped
@wraps(func) @wraps(func)
def wrapped(request, *args, api=False, **kwargs): def wrapped(request, *args, **kwargs):
if api and not api_hybrid:
raise Exception('API call on a view without api_hybrid!')
if not can_access_editor(request): if not can_access_editor(request):
raise PermissionDenied raise PermissionDenied
if getattr(request, "changeset", None) is None: if getattr(request, "changeset", None) is None:
request.changeset = ChangeSet.get_for_request(request, select_related) request.changeset = ChangeSet.get_for_request(request, select_related)
if api:
request.is_delete = request.method == 'DELETE'
return call_api_hybrid_view_for_api(func, request, *args, **kwargs)
ajax = request.headers.get('x-requested-with') == 'XMLHttpRequest' or 'ajax' in request.GET ajax = request.headers.get('x-requested-with') == 'XMLHttpRequest' or 'ajax' in request.GET
if not ajax: if not ajax:
request.META.pop('HTTP_IF_NONE_MATCH', None) request.META.pop('HTTP_IF_NONE_MATCH', None)
if api_hybrid:
response = call_api_hybrid_view_for_html(func, request, *args, **kwargs)
else:
response = func(request, *args, **kwargs) response = func(request, *args, **kwargs)
if ajax: if ajax:
@ -147,227 +136,9 @@ def sidebar_view(func=None, select_related=None, api_hybrid=False):
patch_vary_headers(response, ('X-Requested-With', )) patch_vary_headers(response, ('X-Requested-With', ))
return response return response
wrapped.api_hybrid = api_hybrid
return wrapped return wrapped
class APIHybridResponse(ABC):
status_code = None
etag = None
last_modified = None
def has_header(self, header):
header = header.lower()
if header == 'etag':
return self.etag is not None
elif header == 'last-modified':
return self.last_modified is not None
else:
raise KeyError
def __setitem__(self, header, value):
header = header.lower()
if header == 'etag':
self.etag = value
elif header == 'last-modified':
self.last_modified = value
else:
raise KeyError
def setdefault(self, header, value):
if not self.has_header(header):
self[header] = value
def add_headers(self, response):
if self.etag is not None:
response['ETag'] = self.etag
if self.last_modified is not None:
response['Last-Modified'] = self.last_modified
return response
@abstractmethod
def get_api_response(self, request):
pass
@abstractmethod
def get_html_response(self, request):
pass
class APIHybridMessageRedirectResponse(APIHybridResponse):
def __init__(self, level, message, redirect_to, status_code=None):
self.level = level
self.message = message
self.redirect_to = redirect_to
if self.level == 'error' and status_code is None:
raise Exception('Error with HTTP 200 makes no sense!')
self.status_code = status_code
def get_api_response(self, request):
return {self.level: self.message}
def get_html_response(self, request):
getattr(messages, self.level)(request, self.message)
return redirect(self.redirect_to)
class APIHybridLoginRequiredResponse(APIHybridResponse):
def __init__(self, next, login_url=None, level='error', message=_('Log in required.')):
self.login_url = login_url
self.next = next
self.level = level
self.message = message
def get_api_response(self, request):
return {self.level: self.message}
def get_html_response(self, request):
getattr(messages, self.level)(request, self.message)
return redirect_to_login(self.next, self.login_url)
class APIHybridError:
def __init__(self, status_code: int, message):
self.status_code = status_code
self.message = message
class APIHybridFormTemplateResponse(APIHybridResponse):
name_to_type_mapping = {
'geometry': 'geojson'
}
type_mapping = {
'TextInput': 'text',
'NumberInput': 'number',
'Textarea': 'text',
'CheckboxInput': 'boolean',
'Select': 'single_choice',
'SelectMultiple': 'multiple_choice',
'HiddenInput': 'hidden',
}
type_required_mapping = {
# name, inverted, only_required
'TextInput': ('allowed_empty', True, False),
'NumberInput': ('null_allowed', True, False),
'Textarea': ('allowed_empty', True, False),
'CheckboxInput': ('true_required', False, True),
'Select': ('choice_required', False, False),
'SelectMultiple': ('choice_required', False, False),
'HiddenInput': ('null_allowed', True, False),
}
def __init__(self, template: str, ctx: dict, form, error: Optional[APIHybridError]):
self.template = template
self.ctx = ctx
self.form = form
self.error = error
def get_api_response(self, request):
result = {}
if self.error:
result['error'] = self.error.message
self.status_code = self.error.status_code
if request.method == 'POST':
if not self.form.is_valid():
if self.status_code is None:
self.status_code = 400
result['form_errors'] = self.form.errors
else:
form = OrderedDict()
for name, field in self.form.fields.items():
widget = field.widget
required = field.required
field = {
'type': self.name_to_type_mapping.get(name, None) or self.type_mapping[type(widget).__name__],
}
required_name, required_invert, required_only_true = self.type_required_mapping[type(widget).__name__]
if not required_only_true or required:
field[required_name] = not required if required_invert else required
if hasattr(widget, 'choices'):
field['choices'] = dict(widget.choices)
if hasattr(widget, 'disabled'):
field['disabled'] = True
field.update(widget.attrs)
field.update({
'value': self.form[name].value(),
})
form[name] = field
result['form'] = form
return result
def get_html_response(self, request):
if self.error:
messages.error(request, self.error.message)
response = render(request, self.template, self.ctx)
return self.add_headers(response) if request.method == 'GET' else response
class APIHybridTemplateContextResponse(APIHybridResponse):
def __init__(self, template: str, ctx: dict, fields=None):
self.template = template
self.ctx = ctx
self.fields = fields
def _maybe_serialize_value(self, value):
if isinstance(value, SerializableMixin):
value = value.serialize(geometry=False, detailed=False)
elif isinstance(value, QuerySet) and issubclass(value.model, SerializableMixin):
value = [item.serialize(geometry=False, detailed=False) for item in value]
return value
def get_api_response(self, request):
result = self.ctx
if self.fields:
result = {name: self._maybe_serialize_value(value)
for name, value in result.items() if name in self.fields}
return result
def get_html_response(self, request):
response = render(request, self.template, self.ctx)
return self.add_headers(response) if request.method == 'GET' else response
class NoAPIHybridResponse(Exception):
pass
def call_api_hybrid_view_for_api(func, request, *args, **kwargs):
response = func(request, *args, **kwargs)
if isinstance(response, APIHybridResponse):
result = OrderedDict(response.get_api_response(request))
messages = []
for message in get_messages(request):
messages.append({
'level': DEFAULT_MESSAGE_TAGS[message.level],
'message': message.message
})
if messages:
result['messages'] = messages
result.move_to_end('messages', last=False)
# todo: fix this
# api_response = APIResponse(result, status=response.status_code)
# if request.method == 'GET':
# response.add_headers(api_response)
# return api_response
elif isinstance(response, HttpResponse) and response.status_code in (304, 412):
# 304 Not Modified, 412 Precondition Failed
return response
raise NoAPIHybridResponse
def call_api_hybrid_view_for_html(func, request, *args, **kwargs):
response = func(request, *args, **kwargs)
if isinstance(response, APIHybridResponse):
return response.get_html_response(request)
elif isinstance(response, HttpResponse) and response.status_code in (304, 412):
# 304 Not Modified, 412 Precondition Failed
return response
raise NoAPIHybridResponse
def editor_etag_func(request, *args, **kwargs): def editor_etag_func(request, *args, **kwargs):
try: try:
changeset = request.changeset changeset = request.changeset

View file

@ -5,6 +5,7 @@ from contextlib import suppress
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.views import redirect_to_login
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import FieldDoesNotExist, PermissionDenied from django.core.exceptions import FieldDoesNotExist, PermissionDenied
from django.db import IntegrityError, models from django.db import IntegrityError, models
@ -18,9 +19,7 @@ from shapely import LineString
from c3nav.editor.forms import GraphEdgeSettingsForm, GraphEditorActionForm, get_editor_form, DoorGraphForm from c3nav.editor.forms import GraphEdgeSettingsForm, GraphEditorActionForm, get_editor_form, DoorGraphForm
from c3nav.editor.utils import DefaultEditUtils, LevelChildEditUtils, SpaceChildEditUtils from c3nav.editor.utils import DefaultEditUtils, LevelChildEditUtils, SpaceChildEditUtils
from c3nav.editor.views.base import (APIHybridError, APIHybridFormTemplateResponse, APIHybridLoginRequiredResponse, from c3nav.editor.views.base import editor_etag_func, sidebar_view, accesses_mapdata
APIHybridMessageRedirectResponse, APIHybridTemplateContextResponse,
editor_etag_func, sidebar_view, accesses_mapdata)
from c3nav.mapdata.models import Level, Space, LocationGroupCategory, GraphNode, GraphEdge, Door from c3nav.mapdata.models import Level, Space, LocationGroupCategory, GraphNode, GraphEdge, Door
from c3nav.mapdata.models.access import AccessPermission, AccessRestriction, AccessRestrictionGroup from c3nav.mapdata.models.access import AccessPermission, AccessRestriction, AccessRestrictionGroup
from c3nav.mapdata.utils.geometry import unwrap_geom from c3nav.mapdata.utils.geometry import unwrap_geom
@ -47,9 +46,9 @@ def child_model(request, model: typing.Union[str, models.Model], kwargs=None, pa
@etag(editor_etag_func) @etag(editor_etag_func)
@accesses_mapdata @accesses_mapdata
@sidebar_view(api_hybrid=True) @sidebar_view
def main_index(request): def main_index(request):
return APIHybridTemplateContextResponse('editor/index.html', { return render(request, 'editor/index.html', {
'levels': Level.objects.filter(Level.q_for_request(request), on_top_of__isnull=True), 'levels': Level.objects.filter(Level.q_for_request(request), on_top_of__isnull=True),
'can_create_level': (request.user_permissions.can_access_base_mapdata and 'can_create_level': (request.user_permissions.can_access_base_mapdata and
request.changeset.can_edit(request)), request.changeset.can_edit(request)),
@ -68,12 +67,12 @@ def main_index(request):
child_model(request, 'Theme'), child_model(request, 'Theme'),
child_model(request, 'DataOverlay'), child_model(request, 'DataOverlay'),
], ],
}, fields=('can_create_level', 'child_models')) })
@etag(editor_etag_func) @etag(editor_etag_func)
@accesses_mapdata @accesses_mapdata
@sidebar_view(api_hybrid=True) @sidebar_view
def level_detail(request, pk): def level_detail(request, pk):
qs = Level.objects.filter(Level.q_for_request(request)) qs = Level.objects.filter(Level.q_for_request(request))
level = get_object_or_404(qs.select_related('on_top_of').prefetch_related('levels_on_top'), pk=pk) level = get_object_or_404(qs.select_related('on_top_of').prefetch_related('levels_on_top'), pk=pk)
@ -83,7 +82,7 @@ def level_detail(request, pk):
else: else:
submodels = ('Space', ) submodels = ('Space', )
return APIHybridTemplateContextResponse('editor/level.html', { return render(request, 'editor/level.html', {
'levels': Level.objects.filter(Level.q_for_request(request), on_top_of__isnull=True), 'levels': Level.objects.filter(Level.q_for_request(request), on_top_of__isnull=True),
'level': level, 'level': level,
'level_url': 'editor.levels.detail', 'level_url': 'editor.levels.detail',
@ -97,12 +96,12 @@ def level_detail(request, pk):
'levels_on_top': level.levels_on_top.filter(Level.q_for_request(request)).all(), 'levels_on_top': level.levels_on_top.filter(Level.q_for_request(request)).all(),
'geometry_url': ('/api/v2/editor/geometries/level/'+str(level.primary_level_pk) 'geometry_url': ('/api/v2/editor/geometries/level/'+str(level.primary_level_pk)
if request.user_permissions.can_access_base_mapdata else None), if request.user_permissions.can_access_base_mapdata else None),
}, fields=('level', 'can_edit_graph', 'can_create_level', 'child_models', 'levels_on_top')) })
@etag(editor_etag_func) @etag(editor_etag_func)
@accesses_mapdata @accesses_mapdata
@sidebar_view(api_hybrid=True) @sidebar_view
def space_detail(request, level, pk): def space_detail(request, level, pk):
# todo: HOW TO GET DATA # todo: HOW TO GET DATA
qs = Space.objects.filter(Space.q_for_request(request)) qs = Space.objects.filter(Space.q_for_request(request))
@ -117,7 +116,7 @@ def space_detail(request, level, pk):
else: else:
submodels = ('POI', 'Area', 'AltitudeMarker', 'LeaveDescription', 'CrossDescription') submodels = ('POI', 'Area', 'AltitudeMarker', 'LeaveDescription', 'CrossDescription')
return APIHybridTemplateContextResponse('editor/space.html', { return render(request, 'editor/space.html', {
'levels': Level.objects.filter(Level.q_for_request(request), on_top_of__isnull=True), 'levels': Level.objects.filter(Level.q_for_request(request), on_top_of__isnull=True),
'level': space.level, 'level': space.level,
'level_url': 'editor.spaces.list', 'level_url': 'editor.spaces.list',
@ -127,7 +126,7 @@ def space_detail(request, level, pk):
'child_models': [child_model(request, model_name, kwargs={'space': pk}, parent=space) 'child_models': [child_model(request, model_name, kwargs={'space': pk}, parent=space)
for model_name in submodels], for model_name in submodels],
'geometry_url': edit_utils.geometry_url, 'geometry_url': edit_utils.geometry_url,
}, fields=('level', 'space', 'can_edit_graph', 'child_models')) })
def get_changeset_exceeded(request): def get_changeset_exceeded(request):
@ -136,7 +135,7 @@ def get_changeset_exceeded(request):
@etag(editor_etag_func) @etag(editor_etag_func)
@accesses_mapdata @accesses_mapdata
@sidebar_view(api_hybrid=True) @sidebar_view
def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, explicit_edit=False): def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, explicit_edit=False):
if isinstance(model, str): if isinstance(model, str):
model = apps.get_model(app_label="mapdata", model_name=model) model = apps.get_model(app_label="mapdata", model_name=model)
@ -266,10 +265,8 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
nosave = False nosave = False
if changeset_exceeded: if changeset_exceeded:
if new: if new:
return APIHybridMessageRedirectResponse( messages.error(request, _('You can not create new objects because your changeset is full.'))
level='error', message=_('You can not create new objects because your changeset is full.'), return redirect(ctx['back_url'])
redirect_to=ctx['back_url'], status_code=409,
)
elif obj.pk not in request.changeset.changes.objects.get(obj._meta.model_name, {}): elif obj.pk not in request.changeset.changes.objects.get(obj._meta.model_name, {}):
messages.warning(request, _('You can not edit this object because your changeset is full.')) messages.warning(request, _('You can not edit this object because your changeset is full.'))
nosave = True nosave = True
@ -284,8 +281,8 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
}) })
if new and model.__name__ == 'BeaconMeasurement' and not request.user.is_authenticated: if new and model.__name__ == 'BeaconMeasurement' and not request.user.is_authenticated:
return APIHybridLoginRequiredResponse(next=request.path_info, login_url='editor.login', level='info', messages.info(request, _('You need to log in to create Beacon Measurements.'))
message=_('You need to log in to create Beacon Measurements.')) return redirect_to_login(request.path_info, 'editor.login')
graph_form = None graph_form = None
if model == Door and not new: if model == Door and not new:
@ -316,16 +313,12 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
delete = getattr(request, 'is_delete', None) delete = getattr(request, 'is_delete', None)
if request.method == 'POST' or (not new and delete): if request.method == 'POST' or (not new and delete):
if nosave: if nosave:
return APIHybridMessageRedirectResponse( messages.error(request, _('You can not edit this object because your changeset is full.'))
level='error', message=_('You can not edit this object because your changeset is full.'), return redirect(request.path)
redirect_to=request.path, status_code=409,
)
if not can_edit_changeset: if not can_edit_changeset:
return APIHybridMessageRedirectResponse( messages.error(request, _('You can not edit changes on this changeset.'))
level='error', message=_('You can not edit changes on this changeset.'), return redirect(request.path)
redirect_to=request.path, status_code=403,
)
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!
@ -333,11 +326,8 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
if request.changeset.can_edit(request): # todo: move this somewhere else if request.changeset.can_edit(request): # todo: move this somewhere else
obj.delete() obj.delete()
else: else:
return APIHybridMessageRedirectResponse( messages.error(request, _('You can not edit changes on this changeset.'))
level='error', return redirect(request.path)
message=_('You can not edit changes on this changeset.'),
redirect_to=request.path, status_code=403,
)
if model == Level: if model == Level:
if obj.on_top_of_id is not None: if obj.on_top_of_id is not None:
@ -348,13 +338,10 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
redirect_to = reverse('editor.spaces.list', kwargs={'level': obj.level.pk}) redirect_to = reverse('editor.spaces.list', kwargs={'level': obj.level.pk})
else: else:
redirect_to = ctx['back_url'] redirect_to = ctx['back_url']
return APIHybridMessageRedirectResponse( messages.success(request, _('Object was successfully deleted.'))
level='success', return redirect(redirect_to)
message=_('Object was successfully deleted.'),
redirect_to=redirect_to
)
ctx['obj_title'] = obj.title ctx['obj_title'] = obj.title
return APIHybridTemplateContextResponse('editor/delete.html', ctx, fields=()) return render(request, 'editor/delete.html', ctx)
json_body = getattr(request, 'json_body', None) json_body = getattr(request, 'json_body', None)
data = json_body if json_body is not None else request.POST data = json_body if json_body is not None else request.POST
@ -383,7 +370,7 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
try: try:
obj.save() obj.save()
except IntegrityError: except IntegrityError:
error = APIHybridError(status_code=400, message=_('Duplicate entry.')) messages.error(request, _('Duplicate entry.'))
else: else:
if form.redirect_slugs is not None: if form.redirect_slugs is not None:
for slug in form.add_redirect_slugs: for slug in form.add_redirect_slugs:
@ -394,13 +381,10 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
if graph_form is not None: if graph_form is not None:
graph_form.save() graph_form.save()
form.save_m2m() form.save_m2m()
return APIHybridMessageRedirectResponse( messages.success(request, _('Object was successfully saved.'))
level='success', return redirect(ctx['back_url'])
message=_('Object was successfully saved.'),
redirect_to=ctx['back_url']
)
else: else:
error = APIHybridError(status_code=403, message=_('You can not edit changes on this changeset.')) messages.error(request, _('You can not edit changes on this changeset.'))
else: else:
form = get_editor_form(model)(instance=obj, request=request, space_id=space_id, form = get_editor_form(model)(instance=obj, request=request, space_id=space_id,
@ -421,7 +405,7 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
"access_restriction_select": True, "access_restriction_select": True,
}) })
return APIHybridFormTemplateResponse('editor/edit.html', ctx, form=form, error=error) return render(request, 'editor/edit.html', ctx)
def get_visible_spaces(request): def get_visible_spaces(request):
@ -448,7 +432,7 @@ def get_visible_spaces_kwargs(model, request):
@etag(editor_etag_func) @etag(editor_etag_func)
@accesses_mapdata @accesses_mapdata
@sidebar_view(api_hybrid=True) @sidebar_view
def list_objects(request, model=None, level=None, space=None, explicit_edit=False): def list_objects(request, model=None, level=None, space=None, explicit_edit=False):
if isinstance(model, str): if isinstance(model, str):
model = apps.get_model(app_label="mapdata", model_name=model) model = apps.get_model(app_label="mapdata", model_name=model)
@ -582,8 +566,7 @@ def list_objects(request, model=None, level=None, space=None, explicit_edit=Fals
"level_geometry_urls": True, "level_geometry_urls": True,
}) })
return APIHybridTemplateContextResponse('editor/list.html', ctx, return render(request, 'editor/list.html', ctx)
fields=('can_create', 'create_url', 'objects'))
def connect_nodes(request, active_node, clicked_node, edge_settings_form): def connect_nodes(request, active_node, clicked_node, edge_settings_form):

View file

@ -1,23 +1,21 @@
from c3nav.editor.forms import get_editor_form from django.contrib import messages
from c3nav.editor.views.base import (APIHybridError, APIHybridFormTemplateResponse, from django.core.exceptions import PermissionDenied
APIHybridMessageRedirectResponse, APIHybridTemplateContextResponse, from django.db import IntegrityError
editor_etag_func, sidebar_view, accesses_mapdata) from django.shortcuts import get_object_or_404, redirect, render
from django.shortcuts import get_object_or_404
from django.views.decorators.http import etag
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.views.decorators.http import etag
from django.contrib import messages
from django.db import IntegrityError
from c3nav.editor.utils import DefaultEditUtils, LevelChildEditUtils from c3nav.editor.forms import get_editor_form
from c3nav.editor.utils import LevelChildEditUtils
from c3nav.editor.views.base import editor_etag_func, sidebar_view, accesses_mapdata
from c3nav.editor.views.edit import get_changeset_exceeded from c3nav.editor.views.edit import get_changeset_exceeded
from c3nav.mapdata.models import DataOverlay, Level, DataOverlayFeature from c3nav.mapdata.models import DataOverlay, Level, DataOverlayFeature
@etag(editor_etag_func) @etag(editor_etag_func)
@accesses_mapdata @accesses_mapdata
@sidebar_view(api_hybrid=True) @sidebar_view
def overlays_list(request, level): def overlays_list(request, level):
queryset = DataOverlay.objects.all().order_by('id') queryset = DataOverlay.objects.all().order_by('id')
if hasattr(DataOverlay, 'q_for_request'): if hasattr(DataOverlay, 'q_for_request'):
@ -34,11 +32,11 @@ def overlays_list(request, level):
'overlays': queryset, 'overlays': queryset,
} }
return APIHybridTemplateContextResponse('editor/overlays.html', ctx, fields=('overlays',)) return render(request, 'editor/overlays.html', ctx)
@etag(editor_etag_func) @etag(editor_etag_func)
@accesses_mapdata @accesses_mapdata
@sidebar_view(api_hybrid=True) @sidebar_view
def overlay_features(request, level, pk): def overlay_features(request, level, pk):
ctx = { ctx = {
'path': request.path, 'path': request.path,
@ -79,12 +77,11 @@ def overlay_features(request, level, pk):
}, },
}) })
return APIHybridTemplateContextResponse('editor/overlay_features.html', ctx, return render(request, 'editor/overlay_features.html', ctx)
fields=('can_create', 'create_url', 'objects'))
@etag(editor_etag_func) @etag(editor_etag_func)
@accesses_mapdata @accesses_mapdata
@sidebar_view(api_hybrid=True) @sidebar_view
def overlay_feature_edit(request, level=None, overlay=None, pk=None): def overlay_feature_edit(request, level=None, overlay=None, pk=None):
changeset_exceeded = get_changeset_exceeded(request) changeset_exceeded = get_changeset_exceeded(request)
@ -142,10 +139,8 @@ def overlay_feature_edit(request, level=None, overlay=None, pk=None):
nosave = False nosave = False
if changeset_exceeded: if changeset_exceeded:
if new: if new:
return APIHybridMessageRedirectResponse( messages.error(request, _('You can not create new objects because your changeset is full.'))
level='error', message=_('You can not create new objects because your changeset is full.'), return redirect(ctx['back_url'])
redirect_to=ctx['back_url'], status_code=409,
)
elif obj.pk not in request.changeset.changes.objects.get(obj._meta.model_name, {}): elif obj.pk not in request.changeset.changes.objects.get(obj._meta.model_name, {}):
messages.warning(request, _('You can not edit this object because your changeset is full.')) messages.warning(request, _('You can not edit this object because your changeset is full.'))
nosave = True nosave = True
@ -164,16 +159,12 @@ def overlay_feature_edit(request, level=None, overlay=None, pk=None):
if request.method == 'POST' or (not new and delete): if request.method == 'POST' or (not new and delete):
if nosave: if nosave:
return APIHybridMessageRedirectResponse( messages.error(request, _('You can not edit this object because your changeset is full.'))
level='error', message=_('You can not edit this object because your changeset is full.'), return redirect(request.path)
redirect_to=request.path, status_code=409,
)
if not can_edit_changeset: if not can_edit_changeset:
return APIHybridMessageRedirectResponse( messages.error(request, _('You can not edit changes on this changeset.'))
level='error', message=_('You can not edit changes on this changeset.'), return redirect(request.path)
redirect_to=request.path, status_code=403,
)
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!
@ -182,20 +173,14 @@ def overlay_feature_edit(request, level=None, overlay=None, pk=None):
if changeset.can_edit(request): if changeset.can_edit(request):
obj.delete() obj.delete()
else: else:
return APIHybridMessageRedirectResponse( messages.error(request, _('You can not edit changes on this changeset.'))
level='error', return redirect(request.path)
message=_('You can not edit changes on this changeset.'),
redirect_to=request.path, status_code=403,
)
redirect_to = ctx['back_url'] redirect_to = ctx['back_url']
return APIHybridMessageRedirectResponse( messages.success(request, _('Object was successfully deleted.'))
level='success', return redirect(redirect_to)
message=_('Object was successfully deleted.'),
redirect_to=redirect_to
)
ctx['obj_title'] = obj.title ctx['obj_title'] = obj.title
return APIHybridTemplateContextResponse('editor/delete.html', ctx, fields=()) return render(request, 'editor/delete.html', ctx)
json_body = getattr(request, 'json_body', None) json_body = getattr(request, 'json_body', None)
data = json_body if json_body is not None else request.POST data = json_body if json_body is not None else request.POST
@ -214,7 +199,7 @@ def overlay_feature_edit(request, level=None, overlay=None, pk=None):
try: try:
obj.save() obj.save()
except IntegrityError as e: except IntegrityError as e:
error = APIHybridError(status_code=400, message=_('Duplicate entry.')) messages.error(request, _('Duplicate entry.'))
else: else:
if form.redirect_slugs is not None: if form.redirect_slugs is not None:
for slug in form.add_redirect_slugs: for slug in form.add_redirect_slugs:
@ -224,13 +209,10 @@ def overlay_feature_edit(request, level=None, overlay=None, pk=None):
obj.redirects.filter(slug=slug).delete() obj.redirects.filter(slug=slug).delete()
form.save_m2m() form.save_m2m()
return APIHybridMessageRedirectResponse( messages.success(request, _('Object was successfully saved.'))
level='success', return redirect(ctx['back_url'])
message=_('Object was successfully saved.'),
redirect_to=ctx['back_url']
)
else: else:
error = APIHybridError(status_code=403, message=_('You can not edit changes on this changeset.')) messages.error(request, _('You can not edit changes on this changeset.'))
else: else:
form = get_editor_form(DataOverlayFeature)(instance=obj, request=request, space_id=space_id, form = get_editor_form(DataOverlayFeature)(instance=obj, request=request, space_id=space_id,
@ -243,5 +225,5 @@ def overlay_feature_edit(request, level=None, overlay=None, pk=None):
} }
}) })
return APIHybridFormTemplateResponse('editor/edit.html', ctx, form=form, error=error) return render(request, 'editor/edit.html', ctx)