api.py 7.63 KB
Newer Older
1 2 3 4
import collections
import json
import logging

5
from django.contrib.auth.decorators import login_required
6
from django.core.exceptions import ValidationError
7
from django.http import Http404, HttpResponse
8
from opaque_keys.edx.keys import CourseKey
9

10
from courseware.courses import get_course_with_access
11
from notes.models import Note
12
from notes.utils import notes_enabled_for_course
13 14 15

log = logging.getLogger(__name__)

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

Arthur Barrett committed
19
    # Maps resources to HTTP methods and actions
20 21 22 23 24 25 26 27 28
    '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,
29 30
}

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

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

Arthur Barrett committed
37

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

Arthur Barrett committed
45

46
@login_required
47
def api_request(request, course_id, **kwargs):
Arthur Barrett committed
48
    '''
49 50 51
    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.
52
    '''
53
    assert isinstance(course_id, basestring)
54
    course_key = CourseKey.from_string(course_id)
55

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

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

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

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

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

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

84
    api_response = module[func](request, course_key, **kwargs)
85
    http_response = api_format(api_response)
86

87
    return http_response
88

Arthur Barrett committed
89

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

98
    # not doing a strict boolean check on data becuase it could be an empty list
99 100 101 102 103 104 105 106 107
    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
108

Arthur Barrett committed
109

110
def _get_course(request, course_key):
111 112 113
    '''
    Helper function to load and return a user's course.
    '''
114
    return get_course_with_access(request.user, 'load', course_key)
115

116
#----------------------------------------------------------------------#
117
# API actions exposed via the resource map.
118

Arthur Barrett committed
119

120
def index(request, course_key):
Arthur Barrett committed
121 122
    '''
    Returns a list of annotation objects.
123
    '''
124
    MAX_LIMIT = API_SETTINGS.get('MAX_NOTE_LIMIT')
125

126
    notes = Note.objects.order_by('id').filter(course_id=course_key,
Arthur Barrett committed
127
                                               user=request.user)[:MAX_LIMIT]
128

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

Arthur Barrett committed
131

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

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

144
    note.save()
145 146 147
    response = HttpResponse('', status=303)
    response['Location'] = note.get_absolute_url()

148
    return ApiResponse(http_response=response, data=None)
149

Arthur Barrett committed
150

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

160
    if note.user.id != request.user.id:
161
        return ApiResponse(http_response=HttpResponse('', status=403), data=None)
162

163
    return ApiResponse(http_response=HttpResponse(), data=note.as_dict())
164

Arthur Barrett committed
165

166
def update(request, course_key, note_id):  # pylint: disable=unused-argument
Arthur Barrett committed
167 168
    '''
    Updates an annotation object and returns a 303 with the read location.
169
    '''
170
    try:
171
        note = Note.objects.get(id=note_id)
172
    except Note.DoesNotExist:
173
        return ApiResponse(http_response=HttpResponse('', status=404), data=None)
174

175
    if note.user.id != request.user.id:
176
        return ApiResponse(http_response=HttpResponse('', status=403), data=None)
177

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

184
    note.save()
185

186 187 188
    response = HttpResponse('', status=303)
    response['Location'] = note.get_absolute_url()

189
    return ApiResponse(http_response=response, data=None)
190

Arthur Barrett committed
191

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

201
    if note.user.id != request.user.id:
202
        return ApiResponse(http_response=HttpResponse('', status=403), data=None)
203

204 205
    note.delete()

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

Arthur Barrett committed
208

209
def search(request, course_key):
Arthur Barrett committed
210
    '''
211 212
    Returns a subset of  annotation objects based on a search query.
    '''
213
    MAX_LIMIT = API_SETTINGS.get('MAX_NOTE_LIMIT')
Arthur Barrett committed
214

215
    # search parameters
216 217 218
    offset = request.GET.get('offset', '')
    limit = request.GET.get('limit', '')
    uri = request.GET.get('uri', '')
219

220
    # validate search parameters
221 222 223 224 225 226
    if offset.isdigit():
        offset = int(offset)
    else:
        offset = 0

    if limit.isdigit():
227 228 229 230 231 232
        limit = int(limit)
        if limit == 0 or limit > MAX_LIMIT:
            limit = MAX_LIMIT
    else:
        limit = MAX_LIMIT

233
    # set filters
234
    filters = {'course_id': course_key, 'user': request.user}
235
    if uri != '':
236 237
        filters['uri'] = uri

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

247
    return ApiResponse(http_response=HttpResponse(), data=result)
248

Arthur Barrett committed
249

250
def root(request, course_key):  # pylint: disable=unused-argument
Arthur Barrett committed
251 252
    '''
    Returns version information about the API.
253
    '''
254
    return ApiResponse(http_response=HttpResponse(), data=API_SETTINGS.get('META'))