diff --git a/src/c3nav/api/middleware.py b/src/c3nav/api/middleware.py new file mode 100644 index 00000000..1356541b --- /dev/null +++ b/src/c3nav/api/middleware.py @@ -0,0 +1,18 @@ +import json + + +class JsonRequestBodyMiddleware: + """ + Enables posting JSON requests. + """ + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + is_json = request.META.get('CONTENT_TYPE').lower() == 'application/json' + if is_json: + try: + request.json_body = json.loads(request.body) + except json.JSONDecodeError: + pass + return self.get_response(request) diff --git a/src/c3nav/api/utils.py b/src/c3nav/api/utils.py index 2757b3e5..bcdc9f76 100644 --- a/src/c3nav/api/utils.py +++ b/src/c3nav/api/utils.py @@ -1,5 +1,3 @@ -import json - from rest_framework.exceptions import ParseError @@ -7,10 +5,8 @@ def get_api_post_data(request): is_json = request.META.get('CONTENT_TYPE').lower() == 'application/json' if is_json: try: - data = json.loads(request.body) - except json.JSONDecodeError: + data = request.json_body + except AttributeError: raise ParseError('Invalid JSON.') - else: - request.json_body = data return data return request.POST diff --git a/src/c3nav/editor/api.py b/src/c3nav/editor/api.py index f8b1c0ee..35149986 100644 --- a/src/c3nav/editor/api.py +++ b/src/c3nav/editor/api.py @@ -6,13 +6,14 @@ from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from rest_framework.authentication import SessionAuthentication from rest_framework.decorators import action -from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError +from rest_framework.exceptions import NotFound, ParseError, PermissionDenied, ValidationError from rest_framework.generics import get_object_or_404 from rest_framework.response import Response from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet from shapely.ops import cascaded_union from c3nav.api.utils import get_api_post_data +from c3nav.editor.forms import ChangeSetForm, RejectForm from c3nav.editor.models import ChangeSet from c3nav.editor.views.base import etag_func from c3nav.mapdata.api import api_etag @@ -328,21 +329,62 @@ class EditorViewSet(ViewSet): class ChangeSetViewSet(ReadOnlyModelViewSet): """ - List change sets + List and manipulate changesets. All lists are ordered by last update descending. Use ?offset= to specify an offset. + Don't forget to set X-Csrftoken for POST requests! + + / lists all changesets this user can see. + /user/ lists changesets by this user + /reviewing/ lists changesets this user is currently reviewing. + /pending_review/ lists changesets this user can review. + /current/ returns the current changeset. /direct_editing/ POST to activate direct editing (if available). /deactive/ POST to deactivate current changeset or deactivate direct editing - /{id}/changes/ returns the changes of a given changeset. + + /{id}/changes/ list all changes of a given changeset. + /{id}/activate/ POST to activate given changeset. + /{id}/edit/ POST to edit given changeset (provide title and description in POST data). + /{id}/delete/ POST to delete given changeset. + /{id}/propose/ POST to propose given changeset. + /{id}/unpropose/ POST to unpropose given changeset. + /{id}/review/ POST to review given changeset. + /{id}/reject/ POST to reject given changeset (provide reject=1 in POST data for final rejection). + /{id}/unreject/ POST to unreject given changeset. + /{id}/apply/ POST to accept and apply given changeset. """ queryset = ChangeSet.objects.all() def get_queryset(self): return ChangeSet.qs_for_request(self.request).select_related('last_update', 'last_state_update', 'last_change') - def list(self, request, *args, **kwargs): + def _list(self, request, qs): if not can_access_editor(request): raise PermissionDenied - return Response([obj.serialize() for obj in self.get_queryset().order_by('id')]) + offset = 0 + if 'offset' in request.GET: + if not request.GET['offset'].isdigit(): + raise ParseError('Offset has to be a positive integer.') + offset = int(request.GET['offset']) + return Response([obj.serialize() for obj in qs.order_by('-last_update')[offset:offset+20]]) + + def list(self, request, *args, **kwargs): + return self._list(request, self.get_queryset()) + + @action(detail=False, methods=['get']) + def user(self, request, *args, **kwargs): + return self._list(request, self.get_queryset().filter(author=request.user)) + + @action(detail=False, methods=['get']) + def reviewing(self, request, *args, **kwargs): + return self._list(request, self.get_queryset().filter( + assigned_to=request.user, state='review' + )) + + @action(detail=False, methods=['get']) + def pending_review(self, request, *args, **kwargs): + return self._list(request, self.get_queryset().filter( + state__in=('proposed', 'reproposed'), + )) def retrieve(self, request, *args, **kwargs): if not can_access_editor(request): @@ -401,3 +443,111 @@ class ChangeSetViewSet(ReadOnlyModelViewSet): changeset = self.get_object() changeset.fill_changes_cache() return Response([obj.serialize() for obj in changeset.iter_changed_objects()]) + + @action(detail=True, methods=['post']) + def activate(self, request, *args, **kwargs): + changeset = self.get_object() + with changeset.lock_to_edit(request) as changeset: + if not changeset.can_activate(request): + raise PermissionDenied(_('You can not activate this change set.')) + + changeset.activate(request) + return Response({'success': True}) + + @action(detail=True, methods=['post']) + def edit(self, request, *args, **kwargs): + changeset = self.get_object() + with changeset.lock_to_edit(request) as changeset: + if not changeset.can_edit(request): + raise PermissionDenied(_('You cannot edit this change set.')) + + form = ChangeSetForm(instance=changeset, data=get_api_post_data(request)) + if not form.is_valid(): + raise ParseError(form.errors) + + changeset = form.instance + update = changeset.updates.create(user=request.user, + title=changeset.title, description=changeset.description) + changeset.last_update = update + changeset.save() + return Response({'success': True}) + + @action(detail=True, methods=['post']) + def propose(self, request, *args, **kwargs): + if not request.user.is_authenticated: + raise PermissionDenied(_('You need to log in to propose changes.')) + + changeset = self.get_object() + with changeset.lock_to_edit(request) as changeset: + if not changeset.title or not changeset.description: + raise PermissionDenied(_('You need to add a title an a description to propose this change set.')) + + if not changeset.can_propose(request): + raise PermissionDenied(_('You cannot propose this change set.')) + + changeset.propose(request.user) + return Response({'success': True}) + + @action(detail=True, methods=['post']) + def unpropose(self, request, *args, **kwargs): + changeset = self.get_object() + with changeset.lock_to_edit(request) as changeset: + if not changeset.can_unpropose(request): + raise PermissionDenied(_('You cannot unpropose this change set.')) + + changeset.unpropose(request.user) + return Response({'success': True}) + + @action(detail=True, methods=['post']) + def review(self, request, *args, **kwargs): + changeset = self.get_object() + with changeset.lock_to_edit(request) as changeset: + if not changeset.can_start_review(request): + raise PermissionDenied(_('You cannot review these changes.')) + + changeset.start_review(request.user) + return Response({'success': True}) + + @action(detail=True, methods=['post']) + def reject(self, request, *args, **kwargs): + changeset = self.get_object() + with changeset.lock_to_edit(request) as changeset: + if not not changeset.can_end_review(request): + raise PermissionDenied(_('You cannot reject these changes.')) + + form = RejectForm(get_api_post_data(request)) + if not form.is_valid(): + raise ParseError(form.errors) + + changeset.reject(request.user, form.cleaned_data['comment'], form.cleaned_data['final']) + return Response({'success': True}) + + @action(detail=True, methods=['post']) + def unreject(self, request, *args, **kwargs): + changeset = self.get_object() + with changeset.lock_to_edit(request) as changeset: + if not changeset.can_unreject(request): + raise PermissionDenied(_('You cannot unreject these changes.')) + + changeset.unreject(request.user) + return Response({'success': True}) + + @action(detail=True, methods=['post']) + def apply(self, request, *args, **kwargs): + changeset = self.get_object() + with changeset.lock_to_edit(request) as changeset: + if not changeset.can_end_review(request): + raise PermissionDenied(_('You cannot accept and apply these changes.')) + + changeset.apply(request.user) + return Response({'success': True}) + + @action(detail=True, methods=['post']) + def delete(self, request, *args, **kwargs): + changeset = self.get_object() + with changeset.lock_to_edit(request) as changeset: + if not changeset.can_delete(request): + raise PermissionDenied(_('You cannot delete this change set.')) + + changeset.delete() + return Response({'success': True}) diff --git a/src/c3nav/editor/models/changeset.py b/src/c3nav/editor/models/changeset.py index 94435d39..11b9eb5e 100644 --- a/src/c3nav/editor/models/changeset.py +++ b/src/c3nav/editor/models/changeset.py @@ -488,6 +488,9 @@ class ChangeSet(models.Model): return self.assigned_to == request.user return False + def can_activate(self, request): + return not self.closed and self.can_edit(request) + def can_delete(self, request): return self.can_edit(request) and self.state == 'unproposed' diff --git a/src/c3nav/editor/views/changes.py b/src/c3nav/editor/views/changes.py index 4a3b377e..0ee662da 100644 --- a/src/c3nav/editor/views/changes.py +++ b/src/c3nav/editor/views/changes.py @@ -59,7 +59,7 @@ def changeset_detail(request, pk): elif request.POST.get('activate') == '1': with changeset.lock_to_edit(request) as changeset: - if not changeset.closed and changeset.can_edit: + if changeset.can_activate(request): changeset.activate(request) messages.success(request, _('You activated this change set.')) else: diff --git a/src/c3nav/editor/views/users.py b/src/c3nav/editor/views/users.py index 88a5d766..10ed928d 100644 --- a/src/c3nav/editor/views/users.py +++ b/src/c3nav/editor/views/users.py @@ -34,7 +34,7 @@ def user_detail(request, pk): ctx = { 'user': user, 'can_direct_edit': ChangeSet.can_direct_edit(request), - 'recent_changesets': ChangeSet.objects.filter(author=user).order_by('-last_update')[:15], + 'recent_changesets': ChangeSet.qs_for_request(request).filter(author=user).order_by('-last_update')[:15], } if request.user_permissions.review_changesets: diff --git a/src/c3nav/settings.py b/src/c3nav/settings.py index d150f96e..b6b2b52b 100644 --- a/src/c3nav/settings.py +++ b/src/c3nav/settings.py @@ -227,6 +227,7 @@ MIDDLEWARE = [ 'c3nav.mapdata.middleware.UserDataMiddleware', 'c3nav.site.middleware.MobileclientMiddleware', 'c3nav.control.middleware.UserPermissionsMiddleware', + 'c3nav.api.middleware.JsonRequestBodyMiddleware', ] with suppress(ImportError):