diff --git a/lms/djangoapps/notes/api.py b/lms/djangoapps/notes/api.py index c20891748f..b0ee45c890 100644 --- a/lms/djangoapps/notes/api.py +++ b/lms/djangoapps/notes/api.py @@ -1,4 +1,5 @@ from django.http import HttpResponse, Http404 +from django.core.exceptions import ValidationError from notes.models import Note import json import logging @@ -73,13 +74,19 @@ def api_format(request, response, data): # Exposed API actions via the resource map. def index(request, course_id): - notes = Note.objects.all() + notes = Note.objects.filter(course_id=course_id, user=request.user) return [HttpResponse(), [note.as_dict() for note in notes]] def create(request, course_id): - note = Note(course_id=course_id, body=request.body, user=request.user) - note.save() + note = Note(course_id=course_id, user=request.user) + try: + note.clean(request.body) + except ValidationError as e: + log.debug(e) + return [HttpResponse('', status=500), None] + + note.save() response = HttpResponse('', status=303) response['Location'] = note.get_absolute_url() @@ -105,8 +112,13 @@ def update(request, course_id, note_id): if not note.user.id == request.user.id: return [HttpResponse('', status=403)] - note.body = request.body - note.save(update_fields=['body', 'updated']) + try: + note.clean(request.body) + except ValidationError as e: + log.debug(e) + return [HttpResponse('', status=500), None] + + note.save(update_fields=['text', 'tags', 'updated']) return [HttpResponse('', status=303), None] @@ -124,7 +136,20 @@ def delete(request, course_id, note_id): return [HttpResponse('', status=204), None] def search(request, course_id): - return [HttpResponse(), []] + limit = request.GET.get('limit') + uri = request.GET.get('uri') + + filters = {'course_id':course_id, 'user':request.user} + if uri is not None: + filters['uri'] = uri + + notes = Note.objects.filter(**filters) + #if limit is not None and limit > 0: + #notes = notes[:limit] + + result = {'rows': [note.as_dict() for note in notes]} + + return [HttpResponse(), result] def version(request, course_id): return [HttpResponse(), {'name': 'Notes API', 'version': '1.0'}] diff --git a/lms/djangoapps/notes/models.py b/lms/djangoapps/notes/models.py index 0779cb5fcd..3534cd9a44 100644 --- a/lms/djangoapps/notes/models.py +++ b/lms/djangoapps/notes/models.py @@ -1,25 +1,69 @@ from django.db import models from django.contrib.auth.models import User from django.core.urlresolvers import reverse +from django.core.exceptions import ValidationError import json +import logging + +log = logging.getLogger(__name__) class Note(models.Model): user = models.ForeignKey(User, db_index=True) course_id = models.CharField(max_length=255, db_index=True) + uri = models.CharField(max_length=1024, db_index=True) + text = models.TextField(default="") + quote = models.TextField(default="") + range_start = models.CharField(max_length=2048) + range_start_offset = models.IntegerField() + range_end = models.CharField(max_length=2048) + range_end_offset = models.IntegerField() + tags = models.TextField(default="") # comma-separated string created = models.DateTimeField(auto_now_add=True, null=True, db_index=True) updated = models.DateTimeField(auto_now=True, db_index=True) - body = models.TextField() + + def clean(self, json_body): + if json_body is None: + raise ValidationError('Note must have a body.') + + body = json.loads(json_body) + if not type(body) is dict: + raise ValidationError('Note body must be a dictionary.') + + self.uri = body.get('uri') + self.text = body.get('text') + self.quote = body.get('quote') + + ranges = body.get('ranges') + if ranges is None or len(ranges) != 1: + raise ValidationError('Note must contain exactly one range.') + + self.range_start = ranges[0]['start'] + self.range_start_offset = ranges[0]['startOffset'] + self.range_end = ranges[0]['end'] + self.range_end_offset = ranges[0]['endOffset'] + + self.tags = "" + tags = body.get('tags', []) + if len(tags) > 0: + self.tags = ",".join(tags) def get_absolute_url(self): kwargs = {'course_id': self.course_id, 'note_id': str(self.id)} return reverse('notes_api_note', kwargs=kwargs) def as_dict(self): - d = {} - json_body = json.loads(self.body) - if type(json_body) is dict: - d.update(json_body) - d['id'] = self.id - d['user_id'] = self.user.id - return d \ No newline at end of file + return { + 'id': self.id, + 'user_id': self.user.id, + 'uri': self.uri, + 'text': self.text, + 'quote': self.quote, + 'ranges': [{ + 'start': self.range_start, + 'startOffset': self.range_start_offset, + 'end': self.range_end, + 'endOffset': self.range_end_offset + }], + 'tags': self.tags.split(",") + } \ No newline at end of file diff --git a/lms/djangoapps/notes/urls.py b/lms/djangoapps/notes/urls.py index 92aeaa1c2a..7811a5f044 100644 --- a/lms/djangoapps/notes/urls.py +++ b/lms/djangoapps/notes/urls.py @@ -5,5 +5,5 @@ urlpatterns = patterns('notes.api', url(r'^api$', 'api_request', {'resource':'root'}, name='notes_api_root'), url(r'^api/annotations$', 'api_request', {'resource':'notes'}, name='notes_api_notes'), url(r'^api/annotations/' + id_regex + r'$', 'api_request', {'resource':'note'}, name='notes_api_note'), - url(r'^api/annotations/search$', 'api_request', {'resource':'search'}, name='notes_api_search') + url(r'^api/search', 'api_request', {'resource':'search'}, name='notes_api_search') ) diff --git a/lms/static/coffee/src/notes.coffee b/lms/static/coffee/src/notes.coffee index 7ab9fbb13a..5153939427 100644 --- a/lms/static/coffee/src/notes.coffee +++ b/lms/static/coffee/src/notes.coffee @@ -1,47 +1,74 @@ class StudentNotes _debug: true - targets: [] # elements with annotator() instances + targets: [] # holds elements with annotator() instances + # Adds a listener for "notes" events that may bubble up from descendants. constructor: ($, el) -> console.log 'student notes init', arguments, this if @_debug - if $(el).data('notes-ready') isnt 'yes' - $(el).delegate '*', 'notes:init': @onInitNotes - $(el).data('notes-ready', 'yes') + if not $(el).data('notes-instance') + events = 'notes:init': @onInitNotes + $(el).delegate('*', events) + $(el).data('notes-instance', @) - onInitNotes: (event, annotationData=null) => + # Initializes annotations on a container element in response to an init event. + onInitNotes: (event, uri=null) => event.stopPropagation() + storeConfig = @getStoreConfig uri found = @targets.some (target) -> target is event.target if found annotator = $(event.target).data('annotator') - store = annotator.plugins['Store'] - store.options.annotationData = annotationData if annotationData - store.loadAnnotations() + if annotator + store = annotator.plugins['Store'] + $.extend(store.options, storeConfig) + if uri + store.loadAnnotationsFromSearch(storeConfig['loadFromSearch']) + else + console.log 'URI is required to load annotations' + else + console.log 'No annotator() instance found for target: ', event.target else $(event.target).annotator() .annotator('addPlugin', 'Tags') - .annotator('addPlugin', 'Store', @getStoreConfig(annotationData)) + .annotator('addPlugin', 'Store', storeConfig) @targets.push(event.target) - getStoreConfig: (annotationData) -> - storeConfig = - prefix: @getPrefix() - annotationData: - uri: @getURIPath() # defaults to current URI path + # Returns a JSON config object that can be passed to the annotator Store plugin + getStoreConfig: (uri) -> + prefix = @getPrefix() + if uri is null + console.log 'getURIPath()', uri, @getURIPath() + uri = @getURIPath() - $.extend storeConfig.annotationData, annotationData if annotationData + storeConfig = + prefix: prefix + loadFromSearch: + uri: uri + limit: 0 + annotationData: + uri: uri storeConfig + # Returns the API endpoint for the annotation store getPrefix: () -> re = /^(\/courses\/[^/]+\/[^/]+\/[^/]+)/ match = re.exec(@getURIPath()) prefix = (if match then match[1] else '') return "#{prefix}/notes/api" + # Returns the URI path of the current page for filtering annotations getURIPath: () -> window.location.href.toString().split(window.location.host)[1] -$(document).ready ($) -> new StudentNotes($, this) \ No newline at end of file + +# Enable notes by default on the document root. +# To initialize annotations on a container element in the document: +# +# $('#myElement').trigger('notes:init'); +# +# Comment this line to disable notes. + +$(document).ready ($) -> new StudentNotes $, @ \ No newline at end of file diff --git a/lms/templates/static_htmlbook.html b/lms/templates/static_htmlbook.html index bea211ae47..2d65e6aae7 100644 --- a/lms/templates/static_htmlbook.html +++ b/lms/templates/static_htmlbook.html @@ -33,8 +33,7 @@ var onComplete = function(url) { return function() { - var annotationData = { 'uri': url } - $('#viewerContainer').trigger('notes:init', [annotationData]); + $('#viewerContainer').trigger('notes:init', [url]); } };