create notes djangoapp for annotator.js

This commit is contained in:
Arthur Barrett
2013-03-13 17:34:58 -04:00
parent 0b2226b051
commit 20767d5cc7
13 changed files with 260 additions and 1 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,10 @@
/*
** Annotator v1.2.6
** https://github.com/okfn/annotator/
**
** Copyright 2012 Aron Carroll, Rufus Pollock, and Nick Stenning.
** Dual licensed under the MIT and GPLv3 licenses.
** https://github.com/okfn/annotator/blob/master/LICENSE
**
** Built at: 2013-01-21 09:43:51Z
*/((function(){var a=function(a,b){return function(){return a.apply(b,arguments)}},b=Object.prototype.hasOwnProperty,c=function(a,c){function e(){this.constructor=a}for(var d in c)b.call(c,d)&&(a[d]=c[d]);return e.prototype=c.prototype,a.prototype=new e,a.__super__=c.prototype,a},d=Array.prototype.indexOf||function(a){for(var b=0,c=this.length;b<c;b++)if(b in this&&this[b]===a)return b;return-1};Annotator.Plugin.Store=function(b){function e(b,c){this._onError=a(this._onError,this),this._onLoadAnnotationsFromSearch=a(this._onLoadAnnotationsFromSearch,this),this._onLoadAnnotations=a(this._onLoadAnnotations,this),this._getAnnotations=a(this._getAnnotations,this),e.__super__.constructor.apply(this,arguments),this.annotations=[]}return c(e,b),e.prototype.events={annotationCreated:"annotationCreated",annotationDeleted:"annotationDeleted",annotationUpdated:"annotationUpdated"},e.prototype.options={prefix:"/store",autoFetch:!0,annotationData:{},loadFromSearch:!1,urls:{create:"/annotations",read:"/annotations/:id",update:"/annotations/:id",destroy:"/annotations/:id",search:"/search"}},e.prototype.pluginInit=function(){if(!Annotator.supported())return;return this.annotator.plugins.Auth?this.annotator.plugins.Auth.withToken(this._getAnnotations):this._getAnnotations()},e.prototype._getAnnotations=function(){return this.options.loadFromSearch?this.loadAnnotationsFromSearch(this.options.loadFromSearch):this.loadAnnotations()},e.prototype.annotationCreated=function(a){var b=this;return d.call(this.annotations,a)<0?(this.registerAnnotation(a),this._apiRequest("create",a,function(c){return c.id==null&&console.warn(Annotator._t("Warning: No ID returned from server for annotation "),a),b.updateAnnotation(a,c)})):this.updateAnnotation(a,{})},e.prototype.annotationUpdated=function(a){var b=this;if(d.call(this.annotations,a)>=0)return this._apiRequest("update",a,function(c){return b.updateAnnotation(a,c)})},e.prototype.annotationDeleted=function(a){var b=this;if(d.call(this.annotations,a)>=0)return this._apiRequest("destroy",a,function(){return b.unregisterAnnotation(a)})},e.prototype.registerAnnotation=function(a){return this.annotations.push(a)},e.prototype.unregisterAnnotation=function(a){return this.annotations.splice(this.annotations.indexOf(a),1)},e.prototype.updateAnnotation=function(a,b){return d.call(this.annotations,a)<0?console.error(Annotator._t("Trying to update unregistered annotation!")):$.extend(a,b),$(a.highlights).data("annotation",a)},e.prototype.loadAnnotations=function(){return this._apiRequest("read",null,this._onLoadAnnotations)},e.prototype._onLoadAnnotations=function(a){return a==null&&(a=[]),this.annotations=a,this.annotator.loadAnnotations(a.slice())},e.prototype.loadAnnotationsFromSearch=function(a){return this._apiRequest("search",a,this._onLoadAnnotationsFromSearch)},e.prototype._onLoadAnnotationsFromSearch=function(a){return a==null&&(a={}),this._onLoadAnnotations(a.rows||[])},e.prototype.dumpAnnotations=function(){var a,b,c,d,e;d=this.annotations,e=[];for(b=0,c=d.length;b<c;b++)a=d[b],e.push(JSON.parse(this._dataFor(a)));return e},e.prototype._apiRequest=function(a,b,c){var d,e,f,g;return d=b&&b.id,g=this._urlFor(a,d),e=this._apiRequestOptions(a,b,c),f=$.ajax(g,e),f._id=d,f._action=a,f},e.prototype._apiRequestOptions=function(a,b,c){var d;return d={type:this._methodFor(a),headers:this.element.data("annotator:headers"),dataType:"json",success:c||function(){},error:this._onError},a==="search"?d=$.extend(d,{data:b}):d=$.extend(d,{data:b&&this._dataFor(b),contentType:"application/json; charset=utf-8"}),d},e.prototype._urlFor=function(a,b){var c,d;return c=b!=null?"/"+b:"",d=this.options.prefix!=null?this.options.prefix:"",d+=this.options.urls[a],d=d.replace(/\/:id/,c),d},e.prototype._methodFor=function(a){var b;return b={create:"POST",read:"GET",update:"PUT",destroy:"DELETE",search:"GET"},b[a]},e.prototype._dataFor=function(a){var b,c;return c=a.highlights,delete a.highlights,$.extend(a,this.options.annotationData),b=JSON.stringify(a),c&&(a.highlights=c),b},e.prototype._onError=function(a){var b,c;b=a._action,c=Annotator._t("Sorry we could not ")+b+Annotator._t(" this annotation"),a._action==="search"?c=Annotator._t("Sorry we could not search the store for annotations"):a._action==="read"&&!a._id&&(c=Annotator._t("Sorry we could not ")+b+Annotator._t(" the annotations from the store"));switch(a.status){case 401:c=Annotator._t("Sorry you are not allowed to ")+b+Annotator._t(" this annotation");break;case 404:c=Annotator._t("Sorry we could not connect to the annotations store");break;case 500:c=Annotator._t("Sorry something went wrong with the annotation store")}return Annotator.showNotification(c,Annotator.Notification.ERROR),console.error(Annotator._t("API request failed:")+(" '"+a.status+"'"))},e}(Annotator.Plugin)})).call(this);

View File

@@ -0,0 +1,10 @@
/*
** Annotator v1.2.6
** https://github.com/okfn/annotator/
**
** Copyright 2012 Aron Carroll, Rufus Pollock, and Nick Stenning.
** Dual licensed under the MIT and GPLv3 licenses.
** https://github.com/okfn/annotator/blob/master/LICENSE
**
** Built at: 2013-01-21 09:43:52Z
*/((function(){var a=function(a,b){return function(){return a.apply(b,arguments)}},b=Object.prototype.hasOwnProperty,c=function(a,c){function e(){this.constructor=a}for(var d in c)b.call(c,d)&&(a[d]=c[d]);return e.prototype=c.prototype,a.prototype=new e,a.__super__=c.prototype,a};Annotator.Plugin.Tags=function(b){function d(){this.setAnnotationTags=a(this.setAnnotationTags,this),this.updateField=a(this.updateField,this),d.__super__.constructor.apply(this,arguments)}return c(d,b),d.prototype.options={parseTags:function(a){var b;return a=$.trim(a),b=[],a&&(b=a.split(/\s+/)),b},stringifyTags:function(a){return a.join(" ")}},d.prototype.field=null,d.prototype.input=null,d.prototype.pluginInit=function(){if(!Annotator.supported())return;return this.field=this.annotator.editor.addField({label:Annotator._t("Add some tags here")+"…",load:this.updateField,submit:this.setAnnotationTags}),this.annotator.viewer.addField({load:this.updateViewer}),this.annotator.plugins.Filter&&this.annotator.plugins.Filter.addFilter({label:Annotator._t("Tag"),property:"tags",isFiltered:Annotator.Plugin.Tags.filterCallback}),this.input=$(this.field).find(":input")},d.prototype.parseTags=function(a){return this.options.parseTags(a)},d.prototype.stringifyTags=function(a){return this.options.stringifyTags(a)},d.prototype.updateField=function(a,b){var c;return c="",b.tags&&(c=this.stringifyTags(b.tags)),this.input.val(c)},d.prototype.setAnnotationTags=function(a,b){return b.tags=this.parseTags(this.input.val())},d.prototype.updateViewer=function(a,b){return a=$(a),b.tags&&$.isArray(b.tags)&&b.tags.length?a.addClass("annotator-tags").html(function(){var a;return a=$.map(b.tags,function(a){return'<span class="annotator-tag">'+Annotator.$.escape(a)+"</span>"}).join(" ")}):a.remove()},d}(Annotator.Plugin),Annotator.Plugin.Tags.filterCallback=function(a,b){var c,d,e,f,g,h,i,j;b==null&&(b=[]),e=0,d=[];if(a){d=a.split(/\s+/g);for(g=0,i=d.length;g<i;g++){c=d[g];if(b.length)for(h=0,j=b.length;h<j;h++)f=b[h],f.indexOf(c)!==-1&&(e+=1)}}return e===d.length}})).call(this);

View File

@@ -184,6 +184,11 @@ def _combined_open_ended_grading(tab, user, course, active_page):
return tab
return []
def _student_notes(tab, user, course, active_page):
if settings.MITX_FEATURES.get('ENABLE_STUDENT_NOTES'):
link = reverse('notes', args=[course.id])
return [CourseTab('My Notes', link, active_page == 'notes')]
return []
#### Validators
@@ -226,6 +231,7 @@ VALID_TAB_TYPES = {
'peer_grading': TabImpl(null_validator, _peer_grading),
'staff_grading': TabImpl(null_validator, _staff_grading),
'open_ended': TabImpl(null_validator, _combined_open_ended_grading),
'notes': TabImpl(null_validator, _student_notes)
}
@@ -319,6 +325,8 @@ def get_default_tabs(user, course, active_page):
tabs.extend(_wiki({'name': 'Wiki', 'type': 'wiki'}, user, course, active_page))
tabs.extend(_student_notes({'name': 'My Notes', 'type': 'notes'}, user, course, active_page))
if user.is_authenticated() and not course.hide_progress_tab:
tabs.extend(_progress({'name': 'Progress'}, user, course, active_page))

View File

134
lms/djangoapps/notes/api.py Normal file
View File

@@ -0,0 +1,134 @@
from django.http import HttpResponse, Http404
from notes.models import Note
import json
import logging
log = logging.getLogger(__name__)
#----------------------------------------------------------------------#
# API requests are routed through api_request() using the resource map.
def api_resource_map():
''' Maps API resources to (method, action) pairs. '''
(GET, PUT, POST, DELETE) = ('GET', 'PUT', 'POST', 'DELETE') # for convenience
return {
'root': {GET: version},
'notes': {GET: index, POST: create},
'note': {GET: read, PUT: update, DELETE: delete},
'search': {GET: search}
}
def api_request(request, course_id, **kwargs):
''' Routes API requests to the appropriate action method and formats the results
(defaults to JSON).
Raises a 404 if the resource type doesn't exist, or if there is no action
method associated with the HTTP method.
'''
resource_map = api_resource_map()
resource_name = kwargs.pop('resource')
resource = resource_map.get(resource_name)
if resource is None:
log.debug('Resource "{0}" does not exist'.format(resource_name))
raise Http404
if request.method not in resource.keys():
log.debug('Resource "{0}" does not support method "{1}"'.format(resource_name, request.method))
raise Http404
log.debug("API request: {0} {1}".format(request.method, resource_name))
action = resource.get(request.method)
result = action(request, course_id, **kwargs)
response = result[0]
data = None
if len(result) == 2:
data = result[1]
formatted = api_format(request, response, data)
response['Content-type'] = formatted[0]
response.content = formatted[1]
log.debug("API response:")
log.debug(response)
return response
def api_format(request, response, data):
''' Returns a two-element list containing the content type and content.
This method does not modify the request or response.
'''
content_type = 'application/json'
if data is None:
content = ''
else:
content = json.dumps(data)
return [content_type, content]
#----------------------------------------------------------------------#
# Exposed API actions via the resource map.
def index(request, course_id):
notes = Note.objects.all()
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()
response = HttpResponse('', status=303)
response['Location'] = note.get_absolute_url()
return [response, None]
def read(request, course_id, note_id):
try:
note = Note.objects.get(id=note_id)
except Note.DoesNotExist:
return [HttpResponse('', status=404), None]
except Note.MultipleObjectsReturned:
return [HttpResponse('', status=404), None]
if not note.user.id == request.user.id:
return [HttpResponse('', status=403)]
return [HttpResponse(), note.as_dict()]
def update(request, course_id, note_id):
try:
note = Note.objects.get(note_id)
except Note.DoesNotExist:
return [HttpResponse('', status=404), None]
except Note.MultipleObjectsReturned:
return [HttpResponse('', status=404), None]
if not note.user.id == request.user.id:
return [HttpResponse('', status=403)]
note.body = request.body
note.save(update_fields=['body', 'updated'])
return [HttpResponse('', status=303), None]
def delete(request, course_id, note_id):
try:
note = Note.objects.get(note_id)
except Note.DoesNotExist:
return [HttpResponse('', status=404), None]
except Note.MultipleObjectsReturned:
return [HttpResponse('', status=404), None]
if not note.user.id == request.user.id:
return [HttpResponse('', status=403)]
return [HttpResponse('', status=204), None]
def search(request, course_id):
return [HttpResponse(), []]
def version(request, course_id):
return [HttpResponse(), {'name': 'Notes API', 'version': '1.0'}]

View File

@@ -0,0 +1,24 @@
from django.db import models
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
import json
class Note(models.Model):
user = models.ForeignKey(User, db_index=True)
course_id = models.CharField(max_length=255, db_index=True)
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 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
return d

View File

@@ -0,0 +1,16 @@
"""
This file demonstrates writing tests using the unittest module. These will pass
when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
from django.test import TestCase
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.assertEqual(1 + 1, 2)

View File

@@ -0,0 +1,9 @@
from django.conf.urls import patterns, url
id_regex = r"(?P<note_id>[0-9A-Fa-f]+)"
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')
)

View File

@@ -0,0 +1,24 @@
from django.http import HttpResponse
from notes.models import Note
import datetime
import logging
log = logging.getLogger(__name__)
#----------------------------------------------------------------------#
# HTML views.
#
# Example for enabling annotator.js (snippet):
#
# $('body').annotator()
# .annotator('addPlugin', 'Tags')
# .annotator('addPlugin', 'Store', { 'prefix': '/courses/HarvardX/CB22x/2013_Spring/notes/api' });
#
# See annotator.js docs:
#
# https://github.com/okfn/annotator/wiki
def notes(request, course_id):
now = datetime.datetime.now()
html = "<html><body>It is now %s. Course_id: %s</body></html>" % (now, course_id)
return HttpResponse(html)

View File

@@ -86,7 +86,9 @@ MITX_FEATURES = {
# Give a UI to show a student's submission history in a problem by the
# Staff Debug tool.
'ENABLE_STUDENT_HISTORY_VIEW': True
'ENABLE_STUDENT_HISTORY_VIEW': True,
'ENABLE_STUDENT_NOTES': True
}
# Used for A/B testing
@@ -414,6 +416,9 @@ main_vendor_js = [
'js/vendor/jquery.qtip.min.js',
'js/vendor/swfobject/swfobject.js',
'js/vendor/jquery.ba-bbq.min.js',
'js/vendor/annotator.min.js',
'js/vendor/annotator.store.min.js',
'js/vendor/annotator.tags.min.js'
]
discussion_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/discussion/**/*.coffee'))
@@ -431,6 +436,7 @@ PIPELINE_CSS = {
'css/vendor/jquery.treeview.css',
'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css',
'css/vendor/jquery.qtip.min.css',
'css/vendor/annotator.min.css',
'sass/course.scss'
],
'output_filename': 'css/lms-course.css',
@@ -580,4 +586,7 @@ INSTALLED_APPS = (
# Discussion forums
'django_comment_client',
# Student notes
'notes',
)

File diff suppressed because one or more lines are too long

View File

@@ -360,6 +360,10 @@ if settings.COURSEWARE_ENABLED:
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading$',
'open_ended_grading.views.peer_grading', name='peer_grading'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/notes$', 'notes.views.notes', name='notes'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/notes/', include('notes.urls')),
)
# discussion forums live within courseware, so courseware must be enabled first