Skip to content

Commit

Permalink
Merge branch 'review-link'
Browse files Browse the repository at this point in the history
Closes #47
  • Loading branch information
emillon committed Dec 18, 2014
2 parents 85c6a00 + 4737c2d commit f1565d9
Show file tree
Hide file tree
Showing 17 changed files with 286 additions and 16 deletions.
21 changes: 21 additions & 0 deletions app/auth.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import bcrypt
from flask import Blueprint
from flask import current_app
from flask import flash
from flask import redirect
from flask import render_template
Expand All @@ -9,6 +10,7 @@
from flask.ext.login import LoginManager
from flask.ext.login import logout_user
from flask.ext.wtf import Form
from itsdangerous import URLSafeSerializer
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm.exc import NoResultFound
from wtforms import PasswordField
Expand All @@ -18,6 +20,7 @@

from models import db
from models import User
from models import ROLE_GUEST

lm = LoginManager()

Expand Down Expand Up @@ -103,3 +106,21 @@ def logout():
"""
logout_user()
return redirect(url_for('bp.home'))


def shared_link_serializer():
salt = 'shared-link'
serializer = URLSafeSerializer(current_app.secret_key, salt=salt)
return serializer


def pseudo_user(name, docid):
user = User.query.filter_by(role=ROLE_GUEST, full_name=name).first()
if user is None:
user = User(None, None)
user.full_name = name
user.role = ROLE_GUEST
user.only_doc_id = docid
db.session.add(user)
db.session.commit()
return user
3 changes: 2 additions & 1 deletion app/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def create_app(config_file=None):
'coffee/selection.coffee',
'coffee/annotation.coffee',
'coffee/page.coffee',
'coffee/upload.coffee',
'coffee/form.coffee',
'coffee/listview.coffee',
'coffee/audioplayer.coffee',
'coffee/rest.coffee',
Expand All @@ -72,6 +72,7 @@ def load_user(userid):
"""
return models.User.query.get(int(userid))


@app.before_request
def set_g_user():
g.user = current_user
Expand Down
40 changes: 37 additions & 3 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

ROLE_USER = 0
ROLE_ADMIN = 1
ROLE_GUEST = 2


class User(db.Model):
Expand All @@ -30,10 +31,17 @@ class User(db.Model):
name = db.Column(db.String, unique=True, nullable=False)
password = db.Column(db.String, nullable=False)
role = db.Column(db.SmallInteger, default=ROLE_USER, nullable=False)
full_name = db.Column(db.String, unique=True, nullable=True)
only_doc_id = db.Column(db.Integer, nullable=True)

def __init__(self, login, password, workfactor=12):
def __init__(self, login, password, workfactor=None):
if login is None:
login = 'guest'
self.name = login
self.set_password(password, workfactor=workfactor)
if password is None:
self.disable_password()
else:
self.set_password(password, workfactor=workfactor)

def is_active(self):
"""
Expand Down Expand Up @@ -67,10 +75,36 @@ def generate(fake):
user = User(username, password, workfactor=4)
return user

def set_password(self, clear, workfactor=12):
def set_password(self, clear, workfactor):
if workfactor is None:
workfactor = current_app.config['BCRYPT_WORKFACTOR']
salt = bcrypt.gensalt(workfactor)
self.password = bcrypt.hashpw(clear.encode('utf-8'), salt)

def disable_password(self):
self.password = '!'

def pretty_name(self):
if self.full_name is not None:
pretty = self.full_name
else:
pretty = self.name
if self.role == ROLE_GUEST:
pretty += ' (guest)'
return pretty

def can_act_on(self, docid):
docid = int(docid)
if self.role == ROLE_GUEST:
return docid == self.only_doc_id
return True

def can_annotate(self, docid):
return self.can_act_on(docid)

def can_comment_on(self, docid):
return self.can_act_on(docid)


class Document(db.Model):
"""
Expand Down
60 changes: 60 additions & 0 deletions app/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from io import BytesIO

import koremutake
from flask import url_for
from flask.ext.testing import TestCase
from werkzeug import FileStorage

Expand Down Expand Up @@ -34,7 +35,13 @@ def _upload(self, filename, title=None):
r = self.client.post('/upload', data=post_data)
return r

def _new_upload_id(self, filename):
r = self._upload('toto.pdf', title='')
docid = self._extract_docid(r)
return koremutake.decode(docid)

def test_upload(self):
r = self._login('a', 'a', signup=True)
r = self._upload('toto.pdf')
self.assertStatus(r, 302)
m = re.search('/view/(\w+)', r.location)
Expand Down Expand Up @@ -268,3 +275,56 @@ def test_signup_twice(self):
self._signup('a', 'b')
r = self._signup('a', 'c')
self.assertIn('already taken', r.data)

def test_share_link(self):
docid = self._new_upload_id('toto.pdf')
data = {'name': 'Bob'}
r = self.client.post(url_for('bp.share_doc', id=docid), data=data)
self.assert200(r)
d = json.loads(r.data)
self.assertIn('data', d)
h = d['data']

h2 = h + 'x'
r = self.client.get(url_for('bp.view_shared_doc', key=h2))
self.assertRedirects(r, url_for('bp.home'))

r = self.client.get(url_for('bp.view_shared_doc', key=h))
self.assertRedirects(r, url_for('bp.view_doc', id=docid))
r = self.client.get(r.location)
self.assertIn('Signed in as Bob (guest)', r.data)

r = self.client.get(url_for('bp.view_shared_doc', key=h))
self.assertRedirects(r, url_for('bp.view_doc', id=docid))
r = self.client.get(r.location)
self.assertIn('Signed in as Bob (guest)', r.data)

other_docid = self._new_upload_id('blabla.pdf')

self.assertTrue(self._can_annotate(docid))
self.assertFalse(self._can_annotate(other_docid))

self.assertTrue(self._can_comment_on(docid))
self.assertFalse(self._can_comment_on(other_docid))

def _can_annotate(self, docid):
data = {'doc': docid,
'page': 2,
'posx': 3,
'posy': 4,
'width': 5,
'height': 6,
'value': 'Oh oh',
}
r = self.client.post('/annotation/new', data=data)
return r.status_code == 200

def _can_comment_on(self, docid):
comm = 'bla bla bla'
r = self.client.post('/comment/new',
data={'docid': docid,
'comment': comm
},
follow_redirects=True,
)
return r.status_code == 200
44 changes: 44 additions & 0 deletions app/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,20 @@
from flask import url_for
from flask.ext.login import current_user
from flask.ext.login import login_required
from flask.ext.login import login_user
from flask.ext.uploads import UploadNotAllowed
from flask.ext.wtf import Form
from flask_wtf.file import FileField
from itsdangerous import BadSignature
from werkzeug.exceptions import BadRequest
from werkzeug.exceptions import Unauthorized
from wtforms import HiddenField
from wtforms import TextAreaField
from wtforms import TextField

from .auth import lm
from .auth import pseudo_user
from .auth import shared_link_serializer
from .models import Annotation
from .models import AudioAnnotation
from .models import Comment
Expand Down Expand Up @@ -133,13 +138,15 @@ def view_doc(id):
doc = Document.query.get_or_404(id)
form_comm = CommentForm(docid=id)
form_up = UploadForm()
form_share = ShareForm()
comments = Comment.query.filter_by(doc=id)
annotations = Annotation.query.filter_by(doc=id)
readOnly = not current_user.is_authenticated()
return render_template('view.html',
doc=doc,
form_comm=form_comm,
form_up=form_up,
form_share=form_share,
comments=comments,
annotations=annotations,
readOnly=readOnly,
Expand Down Expand Up @@ -173,10 +180,13 @@ def post_comment():
Create a new comment.
:status 302: Redirects to the "view document" page.
:status 401: Not allowed to comment.
"""
form = CommentForm()
assert(form.validate_on_submit())
docid = kore_id(form.docid.data)
if not (current_user.is_authenticated() and current_user.can_comment_on(docid)):
return Unauthorized()
comm = Comment(docid, form.comment.data)
db.session.add(comm)
db.session.commit()
Expand Down Expand Up @@ -226,6 +236,8 @@ def annotation_new():
state = Annotation.STATE_OPEN
if 'state' in request.form:
state = Annotation.state_decode(request.form['state'])
if not current_user.can_annotate(doc):
return Unauthorized()
user = current_user.id
ann = Annotation(doc, page, posx, posy, width, height, text, user,
state=state)
Expand Down Expand Up @@ -335,3 +347,35 @@ def delete_doc(id):
doc.delete()
return redirect(url_for('.home'))
return redirect(url_for('.view_doc', id=id))


class ShareForm(Form):
name = TextField('Name', description='The person you are giving this link to')


@bp.route('/view/<id>/share', methods=['POST'])
def share_doc(id):
form = ShareForm()
if form.validate_on_submit():
data = {'doc': id,
'name': form.name.data,
}
h = shared_link_serializer().dumps(data)
return jsonify(data=h)
return BadRequest()


@bp.route('/view/shared/<key>')
def view_shared_doc(key):
try:
data = shared_link_serializer().loads(key)
except BadSignature:
flash('This link is invalid.')
return redirect(url_for('.home'))
docid = kore_id(data['doc'])
doc = Document.query.get(docid)
name = data['name']
user = pseudo_user(name, docid)
login_user(user)
flash("Hello, {}!".format(name))
return redirect(url_for('.view_doc', id=doc.id))
1 change: 1 addition & 0 deletions conf/common.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
PROPAGATE_EXCEPTIONS = True
X_PDFJS_VERSION = '1.0.473'
BCRYPT_WORKFACTOR = 12
1 change: 1 addition & 0 deletions conf/testing.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
SQLALCHEMY_DATABASE_URI = 'sqlite://'
CSRF_ENABLED = False
WTF_CSRF_ENABLED = False
BCRYPT_WORKFACTOR = 4
26 changes: 26 additions & 0 deletions migrations/versions/35e1dcbfdf30_add_user_only_doc_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Add User.only_doc_id
Revision ID: 35e1dcbfdf30
Revises: 4e474383082f
Create Date: 2014-12-18 15:15:19.349578
"""

# revision identifiers, used by Alembic.
revision = '35e1dcbfdf30'
down_revision = '4e474383082f'

from alembic import op
import sqlalchemy as sa


def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('user', sa.Column('only_doc_id', sa.Integer(), nullable=True))
### end Alembic commands ###


def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_column('user', 'only_doc_id')
### end Alembic commands ###
26 changes: 26 additions & 0 deletions migrations/versions/4e474383082f_add_user_full_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Add User.full_name
Revision ID: 4e474383082f
Revises: 3706a29a80c6
Create Date: 2014-12-18 14:14:25.264445
"""

# revision identifiers, used by Alembic.
revision = '4e474383082f'
down_revision = '3706a29a80c6'

from alembic import op
import sqlalchemy as sa


def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('user', sa.Column('full_name', sa.String(), nullable=True))
### end Alembic commands ###


def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_column('user', 'full_name')
### end Alembic commands ###
16 changes: 16 additions & 0 deletions scripts/lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash
src_py="*.py scripts/*.py conf/*.py app/*.py"
src_coffee="static/coffee/*"

isort -c -sl $src_py
pep8 $src_py

coffee_npm_path=node_modules/coffeelint/bin
PATH="$coffee_npm_path:$PATH"

if coffeelint -v > /dev/null 2>&1 ; then
coffeelint $src_coffee
else
echo "Cannot find coffeelint, skipping it."
fi

6 changes: 6 additions & 0 deletions static/coffee/form.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
form_init = (dialog_selector, link_selector) ->
$(dialog_selector).dialog
autoOpen: false
modal: true
$(link_selector).click ->
$(dialog_selector).dialog "open"
6 changes: 0 additions & 6 deletions static/coffee/upload.coffee

This file was deleted.

Loading

0 comments on commit f1565d9

Please sign in to comment.