Source code for annotator_store.views

from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.db.models import Q
from django.http import JsonResponse, HttpResponse, HttpResponseBadRequest
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from django.views.generic import View
from eulcommon.djangoextras.auth import login_required_with_ajax, \
    permission_required_with_ajax
from eulcommon.djangoextras.http.responses import HttpResponseSeeOtherRedirect
import six

from .models import get_annotation_model, ANNOTATION_OBJECT_PERMISSIONS
from .utils import absolutize_url, permission_required


# get and use configured annotation model
Annotation = get_annotation_model()


[docs]class AnnotationIndex(View): 'Annotator store API index view, with information and links for API urls.' def get(self, request): # Include absolute API links as per annotator 2.0 documentation # http://docs.annotatorjs.org/en/latest/modules/storage.html#storage-api base_url = absolutize_url(reverse('annotation-api:index')) return JsonResponse({ "name": "Annotator Store API", "version": "2.0.0", "links": { "annotation": { "create": { "desc": "Create a new annotation", "method": "POST", "url": "%sannotations" % base_url }, "delete": { "desc": "Delete an annotation", "method": "DELETE", "url": "%sannotations/:id" % base_url }, "read": { "desc": "Get an existing annotation", "method": "GET", "url": "%sannotations/:id" % base_url }, "update": { "desc": "Update an existing annotation", "method": "PUT", "url": "%sannotations/:id" % base_url } }, "search": { "desc": "Basic search API", "method": "GET", "url": "%ssearch" % base_url } } })
non_ajax_error_msg = 'Currently Annotations can only be updated or created via AJAX.'
[docs]class Annotations(View): """API annotations view. On GET, lists annotations. On AJAX POST with json data in request body, creates a new annotation. Users must be logged in to create new annotations, and can only view their own annotations. """
[docs] def get(self, request): 'List viewable annotations as JSON.' # NOTE: this method doesn't *technically* require that the user # be logged in, but under current permission model, no # annotations will be visible to anonymous users. notes = Annotation.objects.visible_to(request.user) # TODO: sort order? # TODO: pagination? look at reference implementation return JsonResponse([n.info() for n in notes], safe=False)
@method_decorator(permission_required('annotator_store.add_annotation'))
[docs] def post(self, request): 'Create a new annotation via AJAX.' # for now, only support creation via ajax if request.is_ajax(): note = Annotation.create_from_request(request) note.save() # create log entry for creation of the annotation LogEntry.objects.log_action( user_id=request.user.id, content_type_id=ContentType.objects.get_for_model(note).pk, object_id=note.pk, object_repr=str(note), change_message='Created via annotator API', action_flag=ADDITION) # annotator store documentation says to return 303 # not sure why this isn't a 201 Created... return HttpResponseSeeOtherRedirect(note.get_absolute_url()) else: return HttpResponseBadRequest(non_ajax_error_msg)
[docs]class AnnotationView(View): '''Views for displaying, updating, and removing a single :class:`~readux.annotations.models.Annotation`. All views require that the user be logged in and own the annotation being viewed, updated, or deleted.''' # all single-annotation views currently require user to be logged in @method_decorator(login_required_with_ajax()) def dispatch(self, *args, **kwargs): return super(AnnotationView, self).dispatch(*args, **kwargs) def get_object(self): note = get_object_or_404(Annotation, id=self.kwargs.get('id', None)) # check permissions for view access # NOTE: assumes user must have view access in order to change/delete if not note.user_can_view(self.request.user): raise PermissionDenied() return note
[docs] def get(self, request, id): '''Display the JSON information for the requested annotation.''' # NOTE: if id is not a valid uuid this results in a ValueError # instead of a 404; should be handled by uuid regex in url config return JsonResponse(self.get_object().info())
[docs] def put(self, request, id): '''Update the annotation via JSON data posted by AJAX.''' if request.is_ajax(): note = self.get_object() if not note.user_can_update(self.request.user): raise PermissionDenied() # NOTE: if user has update permission but not admin permission, # any changes to annotation permissions will be ignored note.update_from_request(request) # create log entry for modification of the annotation LogEntry.objects.log_action( user_id=request.user.id, content_type_id=ContentType.objects.get_for_model(note).pk, object_id=note.pk, object_repr=str(note), change_message='Updated via annotator API', action_flag=CHANGE) return HttpResponseSeeOtherRedirect(note.get_absolute_url()) else: return HttpResponseBadRequest(non_ajax_error_msg)
[docs] def delete(self, request, id): '''Remove the annotation. On success, returns a 204 No Content response as per the annotator store API documentation.''' note = self.get_object() if not note.user_can_delete(self.request.user): raise PermissionDenied() # log that the annotation is being deleted LogEntry.objects.log_action( user_id=request.user.id, content_type_id=ContentType.objects.get_for_model(note).pk, object_id=note.pk, object_repr=str(note), change_message='Deleted via annotator API', action_flag=DELETION) # delete the note note.delete() response = HttpResponse('') # return 204 no content, according to annotator store api docs response.status_code = 204 return response
[docs]class AnnotationSearch(View): '''Search annotations and display as JSON. Results are restricted to annotations the users has permission to view (currently only annotations owned by the user for everyone other than superusers). The following search fields are currently supported: - uri (exact match) - text (case-insensitive partial match) - quote (case-insensitive partial match) - user (exact match on username) - keyword: case-insensitive partial match on text, quote, or with extra data (e.g., to match tags) Search results can be limited by specifying ``limit`` or ``offset`` parameters. ''' def get(self, request): # TODO: look at reference implementation to see what # other search fields should be supported # Only provide access to notes a user can view # (For non-superusers, this is only notes they own) notes = Annotation.objects.visible_to(request.user) search_keys = request.GET.keys() for field in search_keys: search_val = request.GET[field] if field == 'text': notes = notes.filter(text__icontains=search_val) elif field == 'quote': notes = notes.filter(quote__icontains=search_val) elif field == 'user': notes = notes.filter(user__username=search_val) elif field in Annotation.common_fields: notes = notes.filter(**{field: search_val}) # special case: "keyword" search on multiple fields elif field == 'keyword': notes = notes.filter( Q(text__icontains=search_val) | Q(quote__icontains=search_val) | Q(extra_data__icontains=search_val) ) # NOTE: contains search on extra data jsonfield is # probably not a great idea... # for now, ignore date fields and extra data # NOTE: date searching would be nice, but probably requires # parsing dates and generating date ranges # tag searching may be important eventually too # minimal pagination: limit/offset limit = request.GET.get('limit', None) offset = request.GET.get('offset', None) # slice queryset by offset first, so limit will be relative to that try: if offset is not None: notes = notes[int(offset):] if limit is not None: notes = notes[:int(limit)] except ValueError: # if non-numeric values are passed, just ignore them pass return JsonResponse({ 'total': notes.count(), 'rows': [n.info() for n in notes] })