Commit 20767d5c by Arthur Barrett

create notes djangoapp for annotator.js

parent 0b2226b0
/*
** 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);
\ No newline at end of file
/*
** 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);
\ No newline at end of file
...@@ -184,6 +184,11 @@ def _combined_open_ended_grading(tab, user, course, active_page): ...@@ -184,6 +184,11 @@ def _combined_open_ended_grading(tab, user, course, active_page):
return tab return tab
return [] 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 #### Validators
...@@ -226,6 +231,7 @@ VALID_TAB_TYPES = { ...@@ -226,6 +231,7 @@ VALID_TAB_TYPES = {
'peer_grading': TabImpl(null_validator, _peer_grading), 'peer_grading': TabImpl(null_validator, _peer_grading),
'staff_grading': TabImpl(null_validator, _staff_grading), 'staff_grading': TabImpl(null_validator, _staff_grading),
'open_ended': TabImpl(null_validator, _combined_open_ended_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): ...@@ -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(_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: if user.is_authenticated() and not course.hide_progress_tab:
tabs.extend(_progress({'name': 'Progress'}, user, course, active_page)) tabs.extend(_progress({'name': 'Progress'}, user, course, active_page))
......
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'}]
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
\ No newline at end of file
"""
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)
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')
)
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)
...@@ -86,7 +86,9 @@ MITX_FEATURES = { ...@@ -86,7 +86,9 @@ MITX_FEATURES = {
# Give a UI to show a student's submission history in a problem by the # Give a UI to show a student's submission history in a problem by the
# Staff Debug tool. # Staff Debug tool.
'ENABLE_STUDENT_HISTORY_VIEW': True 'ENABLE_STUDENT_HISTORY_VIEW': True,
'ENABLE_STUDENT_NOTES': True
} }
# Used for A/B testing # Used for A/B testing
...@@ -414,6 +416,9 @@ main_vendor_js = [ ...@@ -414,6 +416,9 @@ main_vendor_js = [
'js/vendor/jquery.qtip.min.js', 'js/vendor/jquery.qtip.min.js',
'js/vendor/swfobject/swfobject.js', 'js/vendor/swfobject/swfobject.js',
'js/vendor/jquery.ba-bbq.min.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')) discussion_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/discussion/**/*.coffee'))
...@@ -431,6 +436,7 @@ PIPELINE_CSS = { ...@@ -431,6 +436,7 @@ PIPELINE_CSS = {
'css/vendor/jquery.treeview.css', 'css/vendor/jquery.treeview.css',
'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css', 'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css',
'css/vendor/jquery.qtip.min.css', 'css/vendor/jquery.qtip.min.css',
'css/vendor/annotator.min.css',
'sass/course.scss' 'sass/course.scss'
], ],
'output_filename': 'css/lms-course.css', 'output_filename': 'css/lms-course.css',
...@@ -580,4 +586,7 @@ INSTALLED_APPS = ( ...@@ -580,4 +586,7 @@ INSTALLED_APPS = (
# Discussion forums # Discussion forums
'django_comment_client', 'django_comment_client',
# Student notes
'notes',
) )
...@@ -360,6 +360,10 @@ if settings.COURSEWARE_ENABLED: ...@@ -360,6 +360,10 @@ if settings.COURSEWARE_ENABLED:
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading$',
'open_ended_grading.views.peer_grading', name='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 # discussion forums live within courseware, so courseware must be enabled first
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment