create notes djangoapp for annotator.js
This commit is contained in:
10
common/static/js/vendor/annotator.min.js
vendored
Normal file
10
common/static/js/vendor/annotator.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
10
common/static/js/vendor/annotator.store.min.js
vendored
Normal file
10
common/static/js/vendor/annotator.store.min.js
vendored
Normal 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);
|
||||
10
common/static/js/vendor/annotator.tags.min.js
vendored
Normal file
10
common/static/js/vendor/annotator.tags.min.js
vendored
Normal 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);
|
||||
@@ -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))
|
||||
|
||||
|
||||
0
lms/djangoapps/notes/__init__.py
Normal file
0
lms/djangoapps/notes/__init__.py
Normal file
134
lms/djangoapps/notes/api.py
Normal file
134
lms/djangoapps/notes/api.py
Normal 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'}]
|
||||
24
lms/djangoapps/notes/models.py
Normal file
24
lms/djangoapps/notes/models.py
Normal 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
|
||||
16
lms/djangoapps/notes/tests.py
Normal file
16
lms/djangoapps/notes/tests.py
Normal 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)
|
||||
9
lms/djangoapps/notes/urls.py
Normal file
9
lms/djangoapps/notes/urls.py
Normal 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')
|
||||
)
|
||||
24
lms/djangoapps/notes/views.py
Normal file
24
lms/djangoapps/notes/views.py
Normal 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)
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
1
lms/static/css/vendor/annotator.min.css
vendored
Normal file
1
lms/static/css/vendor/annotator.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user