Skip to content

Commit

Permalink
Merge branch 'audio'
Browse files Browse the repository at this point in the history
Closes #40
  • Loading branch information
emillon committed Nov 7, 2014
2 parents 4d86cb3 + 4716f18 commit 1ebbb53
Show file tree
Hide file tree
Showing 15 changed files with 557 additions and 38 deletions.
78 changes: 78 additions & 0 deletions app/audio_annotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from flask import Blueprint
from flask import jsonify
from flask import request
from flask.ext.login import current_user

from models import AudioAnnotation
from models import db

audioann = Blueprint('audioann', __name__)

@audioann.route('/audioannotation/new', methods=['POST'])
def audioann_new():
"""
Create a new audio annotation.
:<json int doc: Document ID.
:<json start: Start of annotation, in seconds
:<json length: Length of annotation, in seconds
:<json string text: The text content of the annotation.
:>json int id: The new ID.
"""
user = current_user.id
doc = request.form['doc']
start = request.form['start']
length = request.form['length']
text = request.form['text']
aa = AudioAnnotation(user, doc, start, length, text)
db.session.add(aa)
db.session.commit()
return jsonify(id=aa.id)


@audioann.route('/view/<id>/audioannotations')
def audio_annotations_for_doc(id):
"""
Get the audio annotations associated to a Document.
:param id: Integer ID
:>json array data: Results
"""
data = []
for ann in AudioAnnotation.query.filter_by(doc_id=id):
data.append(ann.to_json())
return jsonify(data=data)


@audioann.route('/audioannotation/<id>', methods=['DELETE'])
def annotation_delete(id):
"""
Delete an audio annotation.
:param id: Integer ID
:>json string status: The string 'ok'
"""
ann = AudioAnnotation.query.get(id)
if not ann.editable_by(current_user):
return lm.unauthorized()
db.session.delete(ann)
db.session.commit()
return jsonify(status='ok')


@audioann.route('/audioannotation/<id>', methods=['PUT'])
def audio_annotation_edit(id):
"""
Edit an AudioAnnotation.
For JSON parameters, see :py:func:`audioann_new`.
:>json string status: The string 'ok'
"""
ann = AudioAnnotation.query.get(id)
if not ann.editable_by(current_user):
return lm.unauthorized()
ann.load_json(request.form)
db.session.commit()
return jsonify(status='ok')
4 changes: 4 additions & 0 deletions app/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ def create_app(config_file=None):
'coffee/page.coffee',
'coffee/upload.coffee',
'coffee/listview.coffee',
'coffee/audioplayer.coffee',
'coffee/rest.coffee',
filters='coffeescript',
output='gen/app.js'
)
Expand Down Expand Up @@ -92,5 +94,7 @@ def is_accessible(self):
app.register_blueprint(bp)
from auth import auth
app.register_blueprint(auth)
from audio_annotation import audioann
app.register_blueprint(audioann)

return app
55 changes: 55 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ def detect_filetype(filename):
return 'pdf'
elif filename.endswith('.png'):
return 'image'
elif filename.endswith('.mp3'):
return 'audio'
else:
assert False, "Unknown file extension in file {}".format(filename)

@staticmethod
def generate(pdfdata):
Expand Down Expand Up @@ -250,3 +254,54 @@ def history(doc):
(project, _) = Revision.project_for(doc)
revs = Revision.query.filter_by(project=project)
return revs


class AudioAnnotation(db.Model):
"""
Annotation for an audio document.
This is somehow simpler than for pdf documents since they are 1D only.
start and length are in seconds.
"""
id = db.Column(db.Integer, primary_key=True, nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey(User.id))
doc_id = db.Column(db.Integer, db.ForeignKey('document.id'), nullable=False)
start = db.Column(db.Integer, nullable=False)
length = db.Column(db.Integer, nullable=False)
text = db.Column(db.String, nullable=False)
state = db.Column(db.SmallInteger, nullable=False, default=0)

def __init__(self, user_id, doc_id, start, length, text):
self.user_id = user_id
self.doc_id = doc_id
self.start = start
self.length = length
self.text = text
self.state = Annotation.STATE_OPEN

def to_json(self):
return {'id': self.id,
'user': self.user_id,
'doc': self.doc_id,
'start': self.start,
'length': self.length,
'text': self.text,
'state': Annotation.state_encode(self.state)
}

def load_json(self, data):
if 'start' in data:
self.start = data['start']
if 'length' in data:
self.length = data['length']
if 'text' in data:
self.text = data['text']
if 'state' in data:
self.state = Annotation.state_decode(data['state'])

def editable_by(self, user):
return user.is_authenticated() and user.id == self.user_id

def is_closed(self):
return self.state == Annotation.STATE_CLOSED
2 changes: 1 addition & 1 deletion app/uploads.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ def documents_dir(app):


documents = UploadSet('documents',
extensions=['pdf', 'png'],
extensions=['pdf', 'png', 'mp3'],
default_dest=documents_dir,
)
12 changes: 10 additions & 2 deletions app/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from .auth import lm
from .models import Annotation
from .models import AudioAnnotation
from .models import Comment
from .models import db
from .models import Document
Expand Down Expand Up @@ -125,8 +126,15 @@ def view_list(id):
"""
id = kore_id(id)
doc = Document.query.get_or_404(id)
annotations = Annotation.query.filter_by(doc=id)
return render_template('list.html',
if doc.filetype == 'pdf' or doc.filetype == 'image':
annotations = Annotation.query.filter_by(doc=id)
template = 'list.html'
elif doc.filetype == 'audio':
annotations = AudioAnnotation.query.filter_by(doc_id=id)
template = 'list_audio.html'
else:
assert False, 'Unknown filetype: {}'.format(doc.filetype)
return render_template(template,
doc=doc,
annotations=annotations,
)
Expand Down
37 changes: 37 additions & 0 deletions migrations/versions/51a83463dbb6_add_audioannotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Add AudioAnnotation
Revision ID: 51a83463dbb6
Revises: 22608a4a8a06
Create Date: 2014-11-07 13:51:43.097465
"""

# revision identifiers, used by Alembic.
revision = '51a83463dbb6'
down_revision = '22608a4a8a06'

from alembic import op
import sqlalchemy as sa


def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('audio_annotation',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('doc_id', sa.Integer(), nullable=False),
sa.Column('start', sa.Integer(), nullable=False),
sa.Column('length', sa.Integer(), nullable=False),
sa.Column('text', sa.String(), nullable=False),
sa.Column('state', sa.SmallInteger(), nullable=False),
sa.ForeignKeyConstraint(['doc_id'], ['document.id'], ),
sa.ForeignKeyConstraint(['user_id'], [u'user.id'], ),
sa.PrimaryKeyConstraint('id')
)
### end Alembic commands ###


def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_table('audio_annotation')
### end Alembic commands ###
42 changes: 12 additions & 30 deletions static/coffee/annotation.coffee
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
class Annotation
constructor: (@$tld, @docid, @page, @text, @annid, @geom, @state) ->
constructor: (@$tld, @docid, @page, @text, @id, @geom, @state) ->
@$div = jQuery('<div>').addClass 'annotation'
@$div = jQuery('<div>').addClass ('annotation-' + @state)
setGeom @$div, @geom
$closeBtn = jQuery('<a>').text '[X]'
@$div.append $closeBtn

@rest = new RestClient '/annotation/'

$closeBtn.click =>
delDone = =>
@rest.delete this, =>
@$div.remove()
if @annid
ANN_URL = '/annotation/' + @annid
$.ajax
url: ANN_URL
type: 'DELETE'
success: -> delDone()
else
delDone()

$annText = jQuery('<div>').text(@text)
@$div.append $annText
Expand Down Expand Up @@ -46,23 +40,11 @@ class Annotation
@geom.height = ui.size.height

submitChanges: ->
if @annid
type = 'PUT'
url = '/annotation/' + @annid
else
type = 'POST'
url = '/annotation/new'
$.ajax
type: type
url: url
data:
posx: @geom.posx | 0
posy: @geom.posy | 0
width: @geom.width | 0
height: @geom.height | 0
doc: @docid
page: @page
value: @text
success: (d) =>
if type == 'POST'
@annid = d.id
@rest.post_or_put this,
posx: @geom.posx | 0
posy: @geom.posy | 0
width: @geom.width | 0
height: @geom.height | 0
doc: @docid
page: @page
value: @text
Loading

0 comments on commit 1ebbb53

Please sign in to comment.