api.py 7.41 KB
Newer Older
1
from django.contrib.auth.decorators import login_required
2
from django.http import HttpResponse, Http404
3
from django.core.exceptions import ValidationError
4

5
from notes.models import Note
6 7
from notes.utils import notes_enabled_for_course
from courseware.courses import get_course_with_access
8

9 10
import json
import logging
11
import collections
12 13 14

log = logging.getLogger(__name__)

15
API_SETTINGS = {
Arthur Barrett committed
16
    'META': {'name': 'Notes API', 'version': 1},
17

Arthur Barrett committed
18
    # Maps resources to HTTP methods and actions
19 20 21 22 23 24 25 26 27
    'RESOURCE_MAP': {
        'root': {'GET': 'root'},
        'notes': {'GET': 'index', 'POST': 'create'},
        'note': {'GET': 'read', 'PUT': 'update', 'DELETE': 'delete'},
        'search': {'GET': 'search'},
    },

    # Cap the number of notes that can be returned in one request
    'MAX_NOTE_LIMIT': 1000,
28 29
}

30 31 32
# Wrapper class for HTTP response and data. All API actions are expected to return this.
ApiResponse = collections.namedtuple('ApiResponse', ['http_response', 'data'])

33 34 35
#----------------------------------------------------------------------#
# API requests are routed through api_request() using the resource map.

Arthur Barrett committed
36

37 38 39 40 41 42 43
def api_enabled(request, course_id):
    '''
    Returns True if the api is enabled for the course, otherwise False.
    '''
    course = _get_course(request, course_id)
    return notes_enabled_for_course(course)

Arthur Barrett committed
44

45
@login_required
46
def api_request(request, course_id, **kwargs):
Arthur Barrett committed
47
    '''
48 49 50
    Routes API requests to the appropriate action method and returns JSON.
    Raises a 404 if the requested resource does not exist or notes are
        disabled for the course.
51
    '''
52

Arthur Barrett committed
53
    # Verify that the api should be accessible to this course
54
    if not api_enabled(request, course_id):
55
        log.debug('Notes are disabled for course: {0}'.format(course_id))
56 57
        raise Http404

Arthur Barrett committed
58
    # Locate the requested resource
Arthur Barrett committed
59
    resource_map = API_SETTINGS.get('RESOURCE_MAP', {})
60
    resource_name = kwargs.pop('resource')
61
    resource_method = request.method
62 63 64 65 66 67
    resource = resource_map.get(resource_name)

    if resource is None:
        log.debug('Resource "{0}" does not exist'.format(resource_name))
        raise Http404

68
    if resource_method not in resource.keys():
Arthur Barrett committed
69
        log.debug('Resource "{0}" does not support method "{1}"'.format(resource_name, resource_method))
70 71
        raise Http404

Arthur Barrett committed
72
    # Execute the action associated with the resource
73 74 75
    func = resource.get(resource_method)
    module = globals()
    if func not in module:
76
        log.debug('Function "{0}" does not exist for request {1} {2}'.format(func, resource_method, resource_name))
77
        raise Http404
78

79
    log.debug('API request: {0} {1}'.format(resource_method, resource_name))
80

81 82
    api_response = module[func](request, course_id, **kwargs)
    http_response = api_format(api_response)
83

84
    return http_response
85

Arthur Barrett committed
86

87
def api_format(api_response):
Arthur Barrett committed
88
    '''
89
    Takes an ApiResponse and returns an HttpResponse.
Arthur Barrett committed
90
    '''
91
    http_response = api_response.http_response
92
    content_type = 'application/json'
93 94
    content = ''

95
    # not doing a strict boolean check on data becuase it could be an empty list
96 97 98 99 100 101 102 103 104
    if api_response.data is not None and api_response.data != '':
        content = json.dumps(api_response.data)

    http_response['Content-type'] = content_type
    http_response.content = content

    log.debug('API response type: {0} content: {1}'.format(content_type, content))

    return http_response
105

Arthur Barrett committed
106

107 108 109 110 111 112
def _get_course(request, course_id):
    '''
    Helper function to load and return a user's course.
    '''
    return get_course_with_access(request.user, course_id, 'load')

113
#----------------------------------------------------------------------#
114
# API actions exposed via the resource map.
115

Arthur Barrett committed
116

117
def index(request, course_id):
Arthur Barrett committed
118 119
    '''
    Returns a list of annotation objects.
120
    '''
121
    MAX_LIMIT = API_SETTINGS.get('MAX_NOTE_LIMIT')
122

123
    notes = Note.objects.order_by('id').filter(course_id=course_id,
Arthur Barrett committed
124
                                               user=request.user)[:MAX_LIMIT]
125

126
    return ApiResponse(http_response=HttpResponse(), data=[note.as_dict() for note in notes])
127

Arthur Barrett committed
128

129
def create(request, course_id):
Arthur Barrett committed
130 131
    '''
    Receives an annotation object to create and returns a 303 with the read location.
132
    '''
133 134 135 136 137 138
    note = Note(course_id=course_id, user=request.user)

    try:
        note.clean(request.body)
    except ValidationError as e:
        log.debug(e)
139
        return ApiResponse(http_response=HttpResponse('', status=400), data=None)
140

141
    note.save()
142 143 144
    response = HttpResponse('', status=303)
    response['Location'] = note.get_absolute_url()

145
    return ApiResponse(http_response=response, data=None)
146

Arthur Barrett committed
147

148
def read(request, course_id, note_id):
Arthur Barrett committed
149 150
    '''
    Returns a single annotation object.
151
    '''
152 153
    try:
        note = Note.objects.get(id=note_id)
154
    except Note.DoesNotExist:
155
        return ApiResponse(http_response=HttpResponse('', status=404), data=None)
156

157
    if note.user.id != request.user.id:
158
        return ApiResponse(http_response=HttpResponse('', status=403), data=None)
159

160
    return ApiResponse(http_response=HttpResponse(), data=note.as_dict())
161

Arthur Barrett committed
162

163
def update(request, course_id, note_id):
Arthur Barrett committed
164 165
    '''
    Updates an annotation object and returns a 303 with the read location.
166
    '''
167
    try:
168
        note = Note.objects.get(id=note_id)
169
    except Note.DoesNotExist:
170
        return ApiResponse(http_response=HttpResponse('', status=404), data=None)
171

172
    if note.user.id != request.user.id:
173
        return ApiResponse(http_response=HttpResponse('', status=403), data=None)
174

175 176 177 178
    try:
        note.clean(request.body)
    except ValidationError as e:
        log.debug(e)
179
        return ApiResponse(http_response=HttpResponse('', status=400), data=None)
180

181
    note.save()
182

183 184 185
    response = HttpResponse('', status=303)
    response['Location'] = note.get_absolute_url()

186
    return ApiResponse(http_response=response, data=None)
187

Arthur Barrett committed
188

189
def delete(request, course_id, note_id):
Arthur Barrett committed
190
    '''
191 192
    Deletes the annotation object and returns a 204 with no content.
    '''
193
    try:
194
        note = Note.objects.get(id=note_id)
195
    except Note.DoesNotExist:
196
        return ApiResponse(http_response=HttpResponse('', status=404), data=None)
197

198
    if note.user.id != request.user.id:
199
        return ApiResponse(http_response=HttpResponse('', status=403), data=None)
200

201 202
    note.delete()

203
    return ApiResponse(http_response=HttpResponse('', status=204), data=None)
204

Arthur Barrett committed
205

206
def search(request, course_id):
Arthur Barrett committed
207
    '''
208 209
    Returns a subset of  annotation objects based on a search query.
    '''
210
    MAX_LIMIT = API_SETTINGS.get('MAX_NOTE_LIMIT')
Arthur Barrett committed
211

212
    # search parameters
213 214 215
    offset = request.GET.get('offset', '')
    limit = request.GET.get('limit', '')
    uri = request.GET.get('uri', '')
216

217
    # validate search parameters
218 219 220 221 222 223
    if offset.isdigit():
        offset = int(offset)
    else:
        offset = 0

    if limit.isdigit():
224 225 226 227 228 229
        limit = int(limit)
        if limit == 0 or limit > MAX_LIMIT:
            limit = MAX_LIMIT
    else:
        limit = MAX_LIMIT

230
    # set filters
231
    filters = {'course_id': course_id, 'user': request.user}
232
    if uri != '':
233 234
        filters['uri'] = uri

235 236 237
    # retrieve notes
    notes = Note.objects.order_by('id').filter(**filters)
    total = notes.count()
Arthur Barrett committed
238
    rows = notes[offset:offset + limit]
239
    result = {
240
        'total': total,
241 242
        'rows': [note.as_dict() for note in rows]
    }
243

244
    return ApiResponse(http_response=HttpResponse(), data=result)
245

Arthur Barrett committed
246

247
def root(request, course_id):
Arthur Barrett committed
248 249
    '''
    Returns version information about the API.
250
    '''
251
    return ApiResponse(http_response=HttpResponse(), data=API_SETTINGS.get('META'))