editor edit API
This commit is contained in:
parent
9be630c0c6
commit
badda67549
3 changed files with 230 additions and 39 deletions
|
@ -2,6 +2,7 @@ from itertools import chain
|
|||
|
||||
from django.db.models import Prefetch, Q
|
||||
from django.urls import Resolver404, resolve
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.decorators import detail_route, list_route
|
||||
from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError
|
||||
|
@ -266,13 +267,19 @@ class EditorViewSet(ViewSet):
|
|||
'bounds': Source.max_bounds(),
|
||||
})
|
||||
|
||||
def __getattr__(self, name):
|
||||
# allow POST and DELETE methods for the editor API
|
||||
if name in ('post', 'delete'):
|
||||
if getattr(self.resolved.func, 'allow_'+name, False):
|
||||
if getattr(self, 'get', None).__name__ in ('list', 'retrieve'):
|
||||
return self.retrieve
|
||||
raise AttributeError
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
return self.retrieve(request, *args, **kwargs)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
if not can_access_editor(request):
|
||||
return PermissionDenied
|
||||
|
||||
@cached_property
|
||||
def resolved(self):
|
||||
resolved = None
|
||||
path = self.kwargs.get('path', '')
|
||||
if path:
|
||||
|
@ -285,13 +292,23 @@ class EditorViewSet(ViewSet):
|
|||
try:
|
||||
resolved = resolve('/editor/'+path)
|
||||
except Resolver404:
|
||||
raise NotFound(_('No matching editor view endpoint found.'))
|
||||
pass
|
||||
|
||||
return resolved
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
if not can_access_editor(request):
|
||||
return PermissionDenied
|
||||
|
||||
resolved = self.resolved
|
||||
if not resolved:
|
||||
raise NotFound(_('No matching editor view endpoint found.'))
|
||||
|
||||
if not getattr(resolved.func, 'api_hybrid', False):
|
||||
raise NotFound(_('Matching editor view point does not provide an API.'))
|
||||
|
||||
response = resolved.func(request, *resolved.args, **resolved.kwargs)
|
||||
return Response(str(response))
|
||||
response = resolved.func(request, api=True, *resolved.args, **resolved.kwargs)
|
||||
return response
|
||||
|
||||
|
||||
class ChangeSetViewSet(ReadOnlyModelViewSet):
|
||||
|
|
|
@ -1,25 +1,33 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from collections import OrderedDict
|
||||
from functools import wraps
|
||||
from typing import Optional
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.views import redirect_to_login
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpResponseNotModified, HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.http import HttpResponse, HttpResponseNotModified, HttpResponseRedirect
|
||||
from django.shortcuts import redirect, render
|
||||
from django.utils.cache import patch_vary_headers
|
||||
from django.utils.translation import get_language
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.response import Response as APIResponse
|
||||
|
||||
from c3nav.editor.models import ChangeSet
|
||||
from c3nav.mapdata.models.access import AccessPermission
|
||||
from c3nav.mapdata.utils.user import can_access_editor
|
||||
|
||||
|
||||
def sidebar_view(func=None, select_related=None, api_hybrid=False):
|
||||
def sidebar_view(func=None, select_related=None, api_hybrid=False, allow_post=True, allow_delete=True):
|
||||
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, api_hybrid=api_hybrid,
|
||||
allow_post=True, allow_delete=True)
|
||||
return wrapped
|
||||
|
||||
@wraps(func)
|
||||
def wrapped(request, *args, api=False, **kwargs):
|
||||
if api and api_hybrid:
|
||||
if api and not api_hybrid:
|
||||
raise Exception('API call on a view without api_hybrid!')
|
||||
|
||||
if not can_access_editor(request):
|
||||
|
@ -53,19 +61,162 @@ def sidebar_view(func=None, select_related=None, api_hybrid=False):
|
|||
response['Cache-Control'] = 'no-cache'
|
||||
patch_vary_headers(response, ('X-Requested-With', ))
|
||||
return response
|
||||
|
||||
wrapped.api_hybrid = api_hybrid
|
||||
wrapped.allow_post = allow_post
|
||||
wrapped.allow_delete = allow_delete
|
||||
|
||||
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
|
||||
|
||||
@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):
|
||||
self.level = level
|
||||
self.message = message
|
||||
self.redirect_to = self.redirect_to
|
||||
|
||||
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):
|
||||
type_mapping = {
|
||||
'TextInput': 'text',
|
||||
'NumberInput': 'number',
|
||||
'Textarea': 'text',
|
||||
'CheckboxInput': 'boolean',
|
||||
'Select': 'single_choice',
|
||||
'SelectMultiple': 'multiple_choice',
|
||||
}
|
||||
|
||||
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'] = str(self.error.message)
|
||||
self.status_code = self.error.status_code
|
||||
if request.method == 'POST':
|
||||
if not self.form.is_valid():
|
||||
if not self.form.is_valid() and 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
|
||||
field = {
|
||||
'type': self.type_mapping[type(widget).__name__],
|
||||
"required": field.required
|
||||
}
|
||||
if hasattr(widget, 'choices'):
|
||||
field['choices'] = dict(widget.choices)
|
||||
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)
|
||||
return render(request, self.template, self.ctx)
|
||||
|
||||
|
||||
class NoAPIHybridResponse(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def call_api_hybrid_view_for_api(func, request, *args, **kwargs):
|
||||
response = func(request, *args, **kwargs)
|
||||
return response
|
||||
if isinstance(response, APIHybridResponse):
|
||||
api_response = APIResponse(response.get_api_response(request), status=response.status_code)
|
||||
if response.etag:
|
||||
api_response['ETag'] = response.etag
|
||||
if response.last_modified:
|
||||
api_response['Last-Modified'] = response.last_modified
|
||||
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)
|
||||
return response
|
||||
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 etag_func(request, *args, **kwargs):
|
||||
|
|
|
@ -5,7 +5,6 @@ from contextlib import suppress
|
|||
|
||||
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, ObjectDoesNotExist, PermissionDenied
|
||||
from django.db import IntegrityError, models
|
||||
|
@ -17,7 +16,8 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from django.views.decorators.http import etag
|
||||
|
||||
from c3nav.editor.forms import GraphEdgeSettingsForm, GraphEditorActionForm
|
||||
from c3nav.editor.views.base import etag_func, sidebar_view
|
||||
from c3nav.editor.views.base import (APIHybridError, APIHybridFormTemplateResponse, APIHybridLoginRequiredResponse,
|
||||
APIHybridMessageRedirectResponse, etag_func, sidebar_view)
|
||||
from c3nav.mapdata.models.access import AccessPermission
|
||||
from c3nav.mapdata.utils.user import can_access_editor
|
||||
|
||||
|
@ -121,7 +121,7 @@ def get_changeset_exceeded(request):
|
|||
return request.user_permissions.max_changeset_changes <= request.changeset.changed_objects_count
|
||||
|
||||
|
||||
@sidebar_view
|
||||
@sidebar_view(api_hybrid=True, allow_post=True, allow_delete=True)
|
||||
@etag(etag_func)
|
||||
def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, explicit_edit=False):
|
||||
changeset_exceeded = get_changeset_exceeded(request)
|
||||
|
@ -258,8 +258,10 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
|
|||
nosave = False
|
||||
if changeset_exceeded:
|
||||
if new:
|
||||
messages.error(request, _('You can not create new objects because your changeset is full.'))
|
||||
return redirect(ctx['back_url'])
|
||||
return APIHybridMessageRedirectResponse(
|
||||
level='error', message=_('You can not create new objects because your changeset is full.'),
|
||||
redirect_to=ctx['back_url']
|
||||
)
|
||||
elif obj.pk not in model_changes:
|
||||
messages.warning(request, _('You can not edit this object because your changeset is full.'))
|
||||
nosave = True
|
||||
|
@ -274,17 +276,22 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
|
|||
})
|
||||
|
||||
if new and model.__name__ == 'WifiMeasurement' and not request.user.is_authenticated:
|
||||
messages.info(request, _('You need to log in to create Wifi Measurements.'))
|
||||
return redirect_to_login(request.path_info, 'editor.login')
|
||||
return APIHybridLoginRequiredResponse(next=request.path_info, login_url='editor.login', level='info',
|
||||
message=_('You need to log in to create Wifi Measurements.'))
|
||||
|
||||
error = None
|
||||
if request.method == 'POST':
|
||||
if nosave:
|
||||
messages.error(request, _('You can not edit this object because your changeset is full.'))
|
||||
return redirect(request.path)
|
||||
return APIHybridMessageRedirectResponse(
|
||||
level='error', message=_('You can not edit this object because your changeset is full.'),
|
||||
redirect_to=request.path
|
||||
)
|
||||
|
||||
if not can_edit:
|
||||
messages.error(request, _('You can not edit this object.'))
|
||||
return redirect(request.path)
|
||||
return APIHybridMessageRedirectResponse(
|
||||
level='error', message=_('You can not edit this object.'),
|
||||
redirect_to=request.path
|
||||
)
|
||||
|
||||
if not new and request.POST.get('delete') == '1':
|
||||
# Delete this mapitem!
|
||||
|
@ -292,24 +299,37 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
|
|||
if not request.changeset.get_changed_object(obj).can_delete():
|
||||
raise PermissionError
|
||||
except (ObjectDoesNotExist, PermissionError):
|
||||
messages.error(request, _('You can not delete this object because other objects still depend on it.'))
|
||||
return redirect(request.path)
|
||||
return APIHybridMessageRedirectResponse(
|
||||
level='error',
|
||||
message=_('You can not delete this object because other objects still depend on it.'),
|
||||
redirect_to=request.path
|
||||
)
|
||||
|
||||
if request.POST.get('delete_confirm') == '1':
|
||||
with request.changeset.lock_to_edit(request) as changeset:
|
||||
if changeset.can_edit(request):
|
||||
obj.delete()
|
||||
else:
|
||||
messages.error(request, _('You can not edit changes on this changeset.'))
|
||||
return redirect(request.path)
|
||||
messages.success(request, _('Object was successfully deleted.'))
|
||||
return APIHybridMessageRedirectResponse(
|
||||
level='error',
|
||||
message=_('You can not edit changes on this changeset.'),
|
||||
redirect_to=request.path
|
||||
)
|
||||
redirect_to = None
|
||||
if model == Level:
|
||||
if obj.on_top_of_id is not None:
|
||||
return redirect(reverse('editor.levels.detail', kwargs={'pk': obj.on_top_of_id}))
|
||||
return redirect(reverse('editor.index'))
|
||||
redirect_to = reverse('editor.levels.detail', kwargs={'pk': obj.on_top_of_id})
|
||||
else:
|
||||
redirect_to = reverse('editor.index')
|
||||
elif model == Space:
|
||||
return redirect(reverse('editor.spaces.list', kwargs={'level': obj.level.pk}))
|
||||
return redirect(ctx['back_url'])
|
||||
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
|
||||
)
|
||||
ctx['obj_title'] = obj.title
|
||||
return render(request, 'editor/delete.html', ctx)
|
||||
|
||||
|
@ -333,7 +353,7 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
|
|||
try:
|
||||
obj.save()
|
||||
except IntegrityError:
|
||||
messages.error(request, _('Duplicate entry.'))
|
||||
error = APIHybridError(status_code=400, message=_('Duplicate entry.'))
|
||||
else:
|
||||
if form.redirect_slugs is not None:
|
||||
for slug in form.add_redirect_slugs:
|
||||
|
@ -343,10 +363,13 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
|
|||
obj.redirects.filter(slug=slug).delete()
|
||||
|
||||
form.save_m2m()
|
||||
messages.success(request, _('Object was successfully saved.'))
|
||||
return redirect(ctx['back_url'])
|
||||
return APIHybridMessageRedirectResponse(
|
||||
level='success',
|
||||
message=_('Object was successfully saved.'),
|
||||
redirect_to=ctx['back_url']
|
||||
)
|
||||
else:
|
||||
messages.error(request, _('You can not edit changes on this changeset.'))
|
||||
error = APIHybridError(status_code=403, message=_('You can not edit changes on this changeset.'))
|
||||
|
||||
else:
|
||||
form = model.EditorForm(instance=obj, request=request, space_id=space_id,
|
||||
|
@ -356,7 +379,7 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
|
|||
'form': form,
|
||||
})
|
||||
|
||||
return render(request, 'editor/edit.html', ctx)
|
||||
return APIHybridFormTemplateResponse('editor/edit.html', ctx, form=form, error=error)
|
||||
|
||||
|
||||
def get_visible_spaces(request):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue