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",
description="get xyz coordinates for all known positioning beacons",
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 get_messages
from django.core.exceptions import PermissionDenied
from django.db.models import QuerySet
from django.http import HttpResponse, HttpResponseNotModified, HttpResponseRedirect
from django.shortcuts import redirect, render
from django.utils.cache import patch_vary_headers
@ -100,34 +99,24 @@ def accesses_mapdata(func):
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:
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
@wraps(func)
def wrapped(request, *args, api=False, **kwargs):
if api and not api_hybrid:
raise Exception('API call on a view without api_hybrid!')
def wrapped(request, *args, **kwargs):
if not can_access_editor(request):
raise PermissionDenied
if getattr(request, "changeset", None) is None:
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
if not ajax:
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)
if ajax:
@ -147,227 +136,9 @@ def sidebar_view(func=None, select_related=None, api_hybrid=False):
patch_vary_headers(response, ('X-Requested-With', ))
return response
wrapped.api_hybrid = api_hybrid
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):
try:
changeset = request.changeset

View file

@ -5,6 +5,7 @@ from contextlib import suppress
from django.apps import apps
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.views import redirect_to_login
from django.core.cache import cache
from django.core.exceptions import FieldDoesNotExist, PermissionDenied
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.utils import DefaultEditUtils, LevelChildEditUtils, SpaceChildEditUtils
from c3nav.editor.views.base import (APIHybridError, APIHybridFormTemplateResponse, APIHybridLoginRequiredResponse,
APIHybridMessageRedirectResponse, APIHybridTemplateContextResponse,
editor_etag_func, sidebar_view, accesses_mapdata)
from c3nav.editor.views.base import editor_etag_func, sidebar_view, accesses_mapdata
from c3nav.mapdata.models import Level, Space, LocationGroupCategory, GraphNode, GraphEdge, Door
from c3nav.mapdata.models.access import AccessPermission, AccessRestriction, AccessRestrictionGroup
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)
@accesses_mapdata
@sidebar_view(api_hybrid=True)
@sidebar_view
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),
'can_create_level': (request.user_permissions.can_access_base_mapdata and
request.changeset.can_edit(request)),
@ -68,12 +67,12 @@ def main_index(request):
child_model(request, 'Theme'),
child_model(request, 'DataOverlay'),
],
}, fields=('can_create_level', 'child_models'))
})
@etag(editor_etag_func)
@accesses_mapdata
@sidebar_view(api_hybrid=True)
@sidebar_view
def level_detail(request, pk):
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)
@ -83,7 +82,7 @@ def level_detail(request, pk):
else:
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),
'level': level,
'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(),
'geometry_url': ('/api/v2/editor/geometries/level/'+str(level.primary_level_pk)
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)
@accesses_mapdata
@sidebar_view(api_hybrid=True)
@sidebar_view
def space_detail(request, level, pk):
# todo: HOW TO GET DATA
qs = Space.objects.filter(Space.q_for_request(request))
@ -117,7 +116,7 @@ def space_detail(request, level, pk):
else:
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),
'level': space.level,
'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)
for model_name in submodels],
'geometry_url': edit_utils.geometry_url,
}, fields=('level', 'space', 'can_edit_graph', 'child_models'))
})
def get_changeset_exceeded(request):
@ -136,7 +135,7 @@ def get_changeset_exceeded(request):
@etag(editor_etag_func)
@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):
if isinstance(model, str):
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
if changeset_exceeded:
if new:
return APIHybridMessageRedirectResponse(
level='error', message=_('You can not create new objects because your changeset is full.'),
redirect_to=ctx['back_url'], status_code=409,
)
messages.error(request, _('You can not create new objects because your changeset is full.'))
return redirect(ctx['back_url'])
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.'))
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:
return APIHybridLoginRequiredResponse(next=request.path_info, login_url='editor.login', level='info',
message=_('You need to log in to create Beacon Measurements.'))
messages.info(request, _('You need to log in to create Beacon Measurements.'))
return redirect_to_login(request.path_info, 'editor.login')
graph_form = None
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)
if request.method == 'POST' or (not new and delete):
if nosave:
return APIHybridMessageRedirectResponse(
level='error', message=_('You can not edit this object because your changeset is full.'),
redirect_to=request.path, status_code=409,
)
messages.error(request, _('You can not edit this object because your changeset is full.'))
return redirect(request.path)
if not can_edit_changeset:
return APIHybridMessageRedirectResponse(
level='error', message=_('You can not edit changes on this changeset.'),
redirect_to=request.path, status_code=403,
)
messages.error(request, _('You can not edit changes on this changeset.'))
return redirect(request.path)
if not new and ((request.POST.get('delete') == '1' and delete is not False) or delete):
# 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
obj.delete()
else:
return APIHybridMessageRedirectResponse(
level='error',
message=_('You can not edit changes on this changeset.'),
redirect_to=request.path, status_code=403,
)
messages.error(request, _('You can not edit changes on this changeset.'))
return redirect(request.path)
if model == Level:
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})
else:
redirect_to = ctx['back_url']
return APIHybridMessageRedirectResponse(
level='success',
message=_('Object was successfully deleted.'),
redirect_to=redirect_to
)
messages.success(request, _('Object was successfully deleted.'))
return redirect(redirect_to)
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)
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:
obj.save()
except IntegrityError:
error = APIHybridError(status_code=400, message=_('Duplicate entry.'))
messages.error(request, _('Duplicate entry.'))
else:
if form.redirect_slugs is not None:
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:
graph_form.save()
form.save_m2m()
return APIHybridMessageRedirectResponse(
level='success',
message=_('Object was successfully saved.'),
redirect_to=ctx['back_url']
)
messages.success(request, _('Object was successfully saved.'))
return redirect(ctx['back_url'])
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:
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,
})
return APIHybridFormTemplateResponse('editor/edit.html', ctx, form=form, error=error)
return render(request, 'editor/edit.html', ctx)
def get_visible_spaces(request):
@ -448,7 +432,7 @@ def get_visible_spaces_kwargs(model, request):
@etag(editor_etag_func)
@accesses_mapdata
@sidebar_view(api_hybrid=True)
@sidebar_view
def list_objects(request, model=None, level=None, space=None, explicit_edit=False):
if isinstance(model, str):
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,
})
return APIHybridTemplateContextResponse('editor/list.html', ctx,
fields=('can_create', 'create_url', 'objects'))
return render(request, 'editor/list.html', ctx)
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 c3nav.editor.views.base import (APIHybridError, APIHybridFormTemplateResponse,
APIHybridMessageRedirectResponse, APIHybridTemplateContextResponse,
editor_etag_func, sidebar_view, accesses_mapdata)
from django.shortcuts import get_object_or_404
from django.views.decorators.http import etag
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.db import IntegrityError
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.contrib import messages
from django.db import IntegrityError
from django.views.decorators.http import etag
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.mapdata.models import DataOverlay, Level, DataOverlayFeature
@etag(editor_etag_func)
@accesses_mapdata
@sidebar_view(api_hybrid=True)
@sidebar_view
def overlays_list(request, level):
queryset = DataOverlay.objects.all().order_by('id')
if hasattr(DataOverlay, 'q_for_request'):
@ -34,11 +32,11 @@ def overlays_list(request, level):
'overlays': queryset,
}
return APIHybridTemplateContextResponse('editor/overlays.html', ctx, fields=('overlays',))
return render(request, 'editor/overlays.html', ctx)
@etag(editor_etag_func)
@accesses_mapdata
@sidebar_view(api_hybrid=True)
@sidebar_view
def overlay_features(request, level, pk):
ctx = {
'path': request.path,
@ -79,12 +77,11 @@ def overlay_features(request, level, pk):
},
})
return APIHybridTemplateContextResponse('editor/overlay_features.html', ctx,
fields=('can_create', 'create_url', 'objects'))
return render(request, 'editor/overlay_features.html', ctx)
@etag(editor_etag_func)
@accesses_mapdata
@sidebar_view(api_hybrid=True)
@sidebar_view
def overlay_feature_edit(request, level=None, overlay=None, pk=None):
changeset_exceeded = get_changeset_exceeded(request)
@ -142,10 +139,8 @@ def overlay_feature_edit(request, level=None, overlay=None, pk=None):
nosave = False
if changeset_exceeded:
if new:
return APIHybridMessageRedirectResponse(
level='error', message=_('You can not create new objects because your changeset is full.'),
redirect_to=ctx['back_url'], status_code=409,
)
messages.error(request, _('You can not create new objects because your changeset is full.'))
return redirect(ctx['back_url'])
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.'))
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 nosave:
return APIHybridMessageRedirectResponse(
level='error', message=_('You can not edit this object because your changeset is full.'),
redirect_to=request.path, status_code=409,
)
messages.error(request, _('You can not edit this object because your changeset is full.'))
return redirect(request.path)
if not can_edit_changeset:
return APIHybridMessageRedirectResponse(
level='error', message=_('You can not edit changes on this changeset.'),
redirect_to=request.path, status_code=403,
)
messages.error(request, _('You can not edit changes on this changeset.'))
return redirect(request.path)
if not new and ((request.POST.get('delete') == '1' and delete is not False) or delete):
# Delete this mapitem!
@ -182,20 +173,14 @@ def overlay_feature_edit(request, level=None, overlay=None, pk=None):
if changeset.can_edit(request):
obj.delete()
else:
return APIHybridMessageRedirectResponse(
level='error',
message=_('You can not edit changes on this changeset.'),
redirect_to=request.path, status_code=403,
)
messages.error(request, _('You can not edit changes on this changeset.'))
return redirect(request.path)
redirect_to = ctx['back_url']
return APIHybridMessageRedirectResponse(
level='success',
message=_('Object was successfully deleted.'),
redirect_to=redirect_to
)
messages.success(request, _('Object was successfully deleted.'))
return redirect(redirect_to)
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)
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:
obj.save()
except IntegrityError as e:
error = APIHybridError(status_code=400, message=_('Duplicate entry.'))
messages.error(request, _('Duplicate entry.'))
else:
if form.redirect_slugs is not None:
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()
form.save_m2m()
return APIHybridMessageRedirectResponse(
level='success',
message=_('Object was successfully saved.'),
redirect_to=ctx['back_url']
)
messages.success(request, _('Object was successfully saved.'))
return redirect(ctx['back_url'])
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:
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)