Initial commit
This commit is contained in:
commit
79d6455236
164
lasertube.py
Normal file
164
lasertube.py
Normal file
|
@ -0,0 +1,164 @@
|
|||
import flask
|
||||
from flask import g, session, request, abort, render_template
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
import uuid
|
||||
import json
|
||||
import models
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
DATABASE = 'sqlite:///lasertube.db'
|
||||
DEBUG = True
|
||||
SECRET_KEY = "barglargl"
|
||||
USERNAME = 'admin'
|
||||
PASSWORD = 'password'
|
||||
SWF_DIR = 'C:/dev/flash/LaserTube/bin'
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
app.config.from_object(__name__)
|
||||
|
||||
engine = create_engine(app.config['DATABASE'])
|
||||
Session = sessionmaker(bind=engine)
|
||||
|
||||
def connect_db():
|
||||
return Session()
|
||||
|
||||
def init_db():
|
||||
models.Base.metadata.create_all(engine)
|
||||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
g.db = connect_db()
|
||||
|
||||
@app.teardown_request
|
||||
def teardown_request(e):
|
||||
g.db.close()
|
||||
|
||||
def requires_login(dg):
|
||||
def func(*args, **kwargs):
|
||||
if not session.get('logged_in'):
|
||||
abort(401)
|
||||
return dg(*args, **kwargs)
|
||||
func.__name__ = dg.__name__
|
||||
return func
|
||||
|
||||
def verify_edit(dg):
|
||||
def func(id, *args, **kwargs):
|
||||
try:
|
||||
edit = g.db.query(models.EditSession).filter_by(disc_id=id).one()
|
||||
except:
|
||||
return flask.jsonify(err='invalid')
|
||||
if edit.guid != request.json['csrf'] or edit.expires < datetime.utcnow():
|
||||
g.db.delete(edit)
|
||||
g.db.commit()
|
||||
return flask.jsonify(err='expired')
|
||||
g.session = create_session(id)
|
||||
return dg(id, *args, **kwargs)
|
||||
func.__name__ = dg.__name__
|
||||
return func
|
||||
|
||||
def create_session(disc_id):
|
||||
g.db.query(models.EditSession).filter_by(disc_id=disc_id).delete()
|
||||
edit = models.EditSession()
|
||||
edit.disc_id = disc_id
|
||||
edit.guid = uuid.uuid4().hex
|
||||
edit.expires = datetime.utcnow() + timedelta(hours=1)
|
||||
g.db.add(edit)
|
||||
g.db.commit()
|
||||
return edit
|
||||
|
||||
@app.route("/")
|
||||
def list_discs():
|
||||
entries = [{'id': row[0], 'title': row[1]} for row in g.db.query(models.Disc.id, models.Disc.title)]
|
||||
return render_template('show_discs.html', entries=entries, fShowEdit=session.get('logged_in'))
|
||||
|
||||
@app.route("/add", methods=['POST'])
|
||||
@requires_login
|
||||
def add_disc():
|
||||
# if not session.get('logged_in'):
|
||||
# abort(401)
|
||||
disc = models.fromJso(request.form, models.Disc, ('title', 'url', 'ktube'))
|
||||
g.db.add(disc)
|
||||
g.db.commit()
|
||||
flask.flash("New entry was successfully posted")
|
||||
return flask.redirect(flask.url_for('list_discs'))
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
error = None
|
||||
if request.method == 'POST':
|
||||
if request.form['username'] != app.config['USERNAME']:
|
||||
error = 'Invalid username'
|
||||
elif request.form['password'] != app.config['PASSWORD']:
|
||||
error = 'Invalid password'
|
||||
else:
|
||||
session['logged_in'] = True
|
||||
flask.flash('You were logged in')
|
||||
return flask.redirect(flask.url_for('list_discs'))
|
||||
return render_template('login.html', error=error)
|
||||
|
||||
@app.route('/logout', methods=['POST'])
|
||||
def logout():
|
||||
session.pop('logged_in', None)
|
||||
flask.flash('You were logged out')
|
||||
return flask.redirect(flask.url_for('list_discs'))
|
||||
|
||||
# for debugging
|
||||
@app.route('/swf/<path:fn>')
|
||||
def swf(fn):
|
||||
return flask.send_from_directory(app.config['SWF_DIR'], fn)
|
||||
|
||||
@app.route('/disc/<int:id>/')
|
||||
def disc_play(id):
|
||||
return render_disc(id)
|
||||
|
||||
@app.route('/disc/<int:id>/json/')
|
||||
def disc_json(id):
|
||||
disc = g.db.query(models.Disc).filter_by(id=id).one()
|
||||
return json.dumps(models.toJso(disc))
|
||||
|
||||
@app.route('/disc/<int:id>/edit/')
|
||||
@requires_login
|
||||
def disc_edit(id):
|
||||
return render_disc(id, urlPostQte=flask.url_for('edit_qte', id=id, _external=True), csrf=create_session(id).guid)
|
||||
|
||||
@app.route('/disc/<int:id>/qte/', methods=['POST'])
|
||||
@verify_edit
|
||||
def edit_qte(id):
|
||||
if request.json['action'] == 'put':
|
||||
qte = models.fromJso(request.json['qte'], models.Qte)
|
||||
# verify no overlapping qtes
|
||||
# t----f t-f t----f
|
||||
# t---------f
|
||||
# yes this is row-by-agonizing-row but there should only be one row
|
||||
# right now the debug info is more useful than optimizing with a bulk delete
|
||||
to_delete = g.db.query(models.Qte).filter(models.Qte.disc_id == id,
|
||||
models.Qte.ms_trigger <= qte.ms_finish, models.Qte.ms_finish >= qte.ms_trigger)
|
||||
print "adding qte", qte.ms_trigger, qte.ms_finish
|
||||
for qte_to_delete in to_delete:
|
||||
print " deleting", qte_to_delete.ms_trigger, qte_to_delete.ms_finish
|
||||
g.db.delete(qte_to_delete)
|
||||
qte.disc_id = id
|
||||
g.db.add(qte)
|
||||
g.db.commit()
|
||||
return flask.jsonify(err='ok', csrf=g.session.guid)
|
||||
elif request.json['action'] == 'delete':
|
||||
deleted = g.db.query(models.Qte).filter_by(disc_id=id, ms_trigger=request.json['ms_trigger']).delete()
|
||||
if deleted > 0:
|
||||
print "DELETE: ", deleted, "qtes deleted", request.json['ms_trigger']
|
||||
else:
|
||||
print "DELETE: nothing deleted", request.json['ms_trigger']
|
||||
g.db.commit()
|
||||
return flask.jsonify(err='ok', csrf=g.session.guid)
|
||||
else:
|
||||
return flask.jsonify(err='invalid_action', csrf=g.session.guid)
|
||||
|
||||
def render_disc(id, **kwargs):
|
||||
disc = g.db.query(models.Disc).filter_by(id=id).one()
|
||||
flashvars = {'jsonDisc': json.dumps(models.toJso(disc))}
|
||||
for k, v in kwargs.iteritems():
|
||||
flashvars[k] = json.dumps(v)
|
||||
return render_template('disc.html', flashvars=flashvars, disc=disc_edit)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
125
models.py
Normal file
125
models.py
Normal file
|
@ -0,0 +1,125 @@
|
|||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.schema import ForeignKey, PrimaryKeyConstraint
|
||||
from sqlalchemy.types import TypeDecorator, BLOB, DateTime
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import Column, Integer, String
|
||||
import numbers
|
||||
import json
|
||||
|
||||
# jso: An object which can be encoded in JSON
|
||||
|
||||
def attr_spec(k):
|
||||
if not isinstance(k, basestring):
|
||||
return k
|
||||
else:
|
||||
return k, None
|
||||
|
||||
def rgattr_spec(o, spec):
|
||||
if spec == None:
|
||||
return ((attr, None) for attr in o)
|
||||
else:
|
||||
return (attr_spec(k) for k in spec)
|
||||
|
||||
def toJso(o, spec=None):
|
||||
if isinstance(o, (basestring, numbers.Number)):
|
||||
return o
|
||||
if isinstance(o, list):
|
||||
result = []
|
||||
for v in o:
|
||||
result.append(toJso(v, spec))
|
||||
return result
|
||||
|
||||
if isinstance(o, dict):
|
||||
result = {}
|
||||
for attr, specNew in rgattr_spec(o, spec):
|
||||
result[attr] = toJso(o[attr], specNew)
|
||||
return result
|
||||
if spec == None and hasattr(o, '__json_spec__'):
|
||||
spec = o.__json_spec__
|
||||
if spec != None:
|
||||
result = {}
|
||||
for attr, specNew in rgattr_spec(o, spec):
|
||||
result[attr] = toJso(getattr(o, attr), specNew)
|
||||
return result
|
||||
|
||||
raise Exception("Don't know how to convert " + str(type(o)) + " to json: " + repr(o))
|
||||
|
||||
def fromJso(jso, classes, spec=None):
|
||||
if classes == None:
|
||||
return jso
|
||||
|
||||
if isinstance(classes, type):
|
||||
classes = (classes,)
|
||||
|
||||
if spec == None:
|
||||
spec = classes[0].__json_spec__
|
||||
|
||||
if len(classes) == 1:
|
||||
if hasattr(classes[0], '__json_classmap__'):
|
||||
classmap = classes[0].__json_classmap__
|
||||
else:
|
||||
classmap = {}
|
||||
else:
|
||||
classmap = classes[1]
|
||||
|
||||
def set_attr(o, k, v):
|
||||
if isinstance(o, dict):
|
||||
o[k] = v
|
||||
else:
|
||||
setattr(o, k, v)
|
||||
|
||||
if isinstance(jso, dict):
|
||||
o = classes[0]()
|
||||
for attr, specNew in (attr_spec(k) for k in spec):
|
||||
set_attr(o, attr, fromJso(jso[attr], classmap.get(attr), specNew))
|
||||
return o
|
||||
|
||||
if isinstance(jso, list):
|
||||
return [fromJso(v, spec, classes) for v in jso]
|
||||
|
||||
return jso
|
||||
|
||||
class JsonSqlType(TypeDecorator):
|
||||
impl = BLOB
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
if value is not None:
|
||||
value = json.dumps(value)
|
||||
return value
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
if value is not None:
|
||||
value = json.loads(value)
|
||||
return value
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
class Disc(Base):
|
||||
__tablename__ = 'disc'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
title = Column(String(128))
|
||||
url = Column(String(256))
|
||||
ktube = Column(String(10))
|
||||
|
||||
class Qte(Base):
|
||||
__tablename__ = 'qte'
|
||||
|
||||
disc_id = Column(Integer, ForeignKey("disc.id"))
|
||||
ms_trigger = Column(Integer)
|
||||
ms_finish = Column(Integer)
|
||||
shape = Column(JsonSqlType)
|
||||
|
||||
disc = relationship("Disc", backref="qtes")
|
||||
__table_args__ = (PrimaryKeyConstraint('disc_id', 'ms_trigger'),)
|
||||
|
||||
class EditSession(Base):
|
||||
__tablename__ = 'editsession'
|
||||
|
||||
disc_id = Column(Integer, ForeignKey("disc.id"), primary_key=True)
|
||||
guid = Column(String(32))
|
||||
expires = Column(DateTime)
|
||||
|
||||
Disc.__json_spec__ = ('id', 'title', 'url', 'ktube', 'qtes')
|
||||
Disc.__json_classmap__ = {'qtes': Qte}
|
||||
Qte.__json_spec__ = ('ms_trigger', 'ms_finish', 'shape')
|
18
static/style.css
Normal file
18
static/style.css
Normal file
|
@ -0,0 +1,18 @@
|
|||
body { font-family: sans-serif; background: #eee; }
|
||||
a, h1, h2 { color: #377BA8; }
|
||||
h1, h2 { font-family: 'Georgia', serif; margin: 0; }
|
||||
h1 { border-bottom: 2px solid #eee; }
|
||||
h2 { font-size: 1.2em; }
|
||||
|
||||
.page { margin: 2em auto; width: 800px; border: 5px solid #ccc;
|
||||
padding: 0.8em; background: white; }
|
||||
.entries { list-style: none; margin: 0; padding: 0; }
|
||||
.entries li { margin: 0.8em 1.2em; }
|
||||
.entries li h2 { margin-left: -1em; }
|
||||
.add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; }
|
||||
.add-entry dl { font-weight: bold; }
|
||||
.metanav { text-align: right; font-size: 0.8em; padding: 0.3em;
|
||||
margin-bottom: 1em; background: #fafafa; }
|
||||
.flash { background: #CEE5F5; padding: 0.5em;
|
||||
border: 1px solid #AACBE2; }
|
||||
.error { background: #F0D6D6; padding: 0.5em; }
|
44
templates/disc.html
Normal file
44
templates/disc.html
Normal file
|
@ -0,0 +1,44 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block head %}
|
||||
<script src="/swf/js/swfobject.js" type="text/javascript"></script>
|
||||
<script type="text/javascript">
|
||||
// function getCookie(name) {
|
||||
// var cookieValue = null;
|
||||
// if (document.cookie && document.cookie != '') {
|
||||
// var cookies = document.cookie.split(';');
|
||||
// for (var i = 0; i < cookies.length; i++) {
|
||||
// var cookie = cookies[i].replace(/^\s+|\s+$/, ''); // trim
|
||||
// // Does this cookie string begin with the name we want?
|
||||
// if (cookie.substring(0, name.length + 1) == (name + '=')) {
|
||||
// cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return cookieValue;
|
||||
// }
|
||||
var flashvars = {{ flashvars|tojson|safe }};
|
||||
var params = {
|
||||
menu: "false",
|
||||
scale: "noScale",
|
||||
allowFullscreen: "true",
|
||||
allowScriptAccess: "always",
|
||||
bgcolor: "#FFFFFF"
|
||||
};
|
||||
var attributes = {
|
||||
id:"LaserTube"
|
||||
};
|
||||
swfobject.embedSWF("/swf/LaserTube.swf", "altContent", "800", "600", "10.0.0", "expressInstall.swf", flashvars, params, attributes);
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<h1>{{ disc.title }}</h1>
|
||||
<div id="altContent">
|
||||
<h1>Oh, dear</h1>
|
||||
<p>You need Flash to play with LaserTube.</p>
|
||||
<p><a href="http://www.adobe.com/go/getflashplayer"><img
|
||||
src="http://www.adobe.com/images/shared/download_buttons/get_flash_player.gif"
|
||||
alt="Get Adobe Flash player" /></a></p>
|
||||
</div>
|
||||
{% endblock %}
|
26
templates/layout.html
Normal file
26
templates/layout.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>LaserTube</title>
|
||||
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class=page>
|
||||
<h1>LaserTube</h1>
|
||||
<div class=metanav>
|
||||
{% if not session.logged_in %}
|
||||
<a href="{{ url_for('login') }}">log in</a>
|
||||
{% else %}
|
||||
<form method='post' action="{{ url_for('logout') }}">
|
||||
<input type="submit" value="log out" />
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% for message in get_flashed_messages() %}
|
||||
<div class=flash>{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
14
templates/login.html
Normal file
14
templates/login.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
<h2>Login</h2>
|
||||
{% if error %}<p class=error><strong>Error:</strong> {{ error }}{% endif %}
|
||||
<form action="{{ url_for('login') }}" method=post>
|
||||
<dl>
|
||||
<dt>Username:
|
||||
<dd><input type=text name=username>
|
||||
<dt>Password:
|
||||
<dd><input type=password name=password>
|
||||
<dd><input type=submit value=Login>
|
||||
</dl>
|
||||
</form>
|
||||
{% endblock %}
|
24
templates/show_discs.html
Normal file
24
templates/show_discs.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
{% if session.logged_in %}
|
||||
<form action="{{ url_for('add_disc') }}" method=post class=add-entry>
|
||||
<dl>
|
||||
<dt>Title:
|
||||
<dd><input type=text size=30 name=title>
|
||||
<dt>URL:
|
||||
<dd><input type=text size=80 name=url>
|
||||
<dt>Type:
|
||||
<dd><input type=radio name=ktube value='yt'>YouTube</input>
|
||||
<dd><input type=radio name=ktube value='flv'>FLV</input>
|
||||
<dd><input type=submit value=Add>
|
||||
</dl>
|
||||
</form>
|
||||
{% endif %}
|
||||
<ul class=entries>
|
||||
{% for entry in entries %}
|
||||
<li><h2>{{ entry.title }}</h2> (<a href="{{ url_for('disc_play', id=entry.id) }}">play</a>{% if fShowEdit %} | <a href="{{ url_for('disc_edit', id=entry.id) }}">edit</a>{% endif %})
|
||||
{% else %}
|
||||
<li><em>Unbelievable. No entries here so far</em>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
Loading…
Reference in a new issue