From 79d64552365596e66dc4c7094a0d9c74e79a1e26 Mon Sep 17 00:00:00 2001 From: Jeremy Penner Date: Wed, 11 Apr 2012 08:44:05 -0400 Subject: [PATCH] Initial commit --- lasertube.py | 164 ++++++++++++++++++++++++++++++++++++++ models.py | 125 +++++++++++++++++++++++++++++ static/style.css | 18 +++++ templates/disc.html | 44 ++++++++++ templates/layout.html | 26 ++++++ templates/login.html | 14 ++++ templates/show_discs.html | 24 ++++++ 7 files changed, 415 insertions(+) create mode 100644 lasertube.py create mode 100644 models.py create mode 100644 static/style.css create mode 100644 templates/disc.html create mode 100644 templates/layout.html create mode 100644 templates/login.html create mode 100644 templates/show_discs.html diff --git a/lasertube.py b/lasertube.py new file mode 100644 index 0000000..04a62d6 --- /dev/null +++ b/lasertube.py @@ -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/') +def swf(fn): + return flask.send_from_directory(app.config['SWF_DIR'], fn) + +@app.route('/disc//') +def disc_play(id): + return render_disc(id) + +@app.route('/disc//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//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//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() diff --git a/models.py b/models.py new file mode 100644 index 0000000..1782dd7 --- /dev/null +++ b/models.py @@ -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') diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..7a47324 --- /dev/null +++ b/static/style.css @@ -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; } diff --git a/templates/disc.html b/templates/disc.html new file mode 100644 index 0000000..bd4000d --- /dev/null +++ b/templates/disc.html @@ -0,0 +1,44 @@ +{% extends "layout.html" %} +{% block head %} + + + +{% endblock %} +{% block body %} +

{{ disc.title }}

+
+

Oh, dear

+

You need Flash to play with LaserTube.

+

Get Adobe Flash player

+
+{% endblock %} \ No newline at end of file diff --git a/templates/layout.html b/templates/layout.html new file mode 100644 index 0000000..052616f --- /dev/null +++ b/templates/layout.html @@ -0,0 +1,26 @@ + + + + LaserTube + + {% block head %}{% endblock %} + + +
+

LaserTube

+
+ {% if not session.logged_in %} + log in + {% else %} +
+ +
+ {% endif %} +
+ {% for message in get_flashed_messages() %} +
{{ message }}
+ {% endfor %} + {% block body %}{% endblock %} +
+ + \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..6f70bb7 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,14 @@ +{% extends "layout.html" %} +{% block body %} +

Login

+ {% if error %}

Error: {{ error }}{% endif %} +

+
+
Username: +
+
Password: +
+
+
+
+{% endblock %} diff --git a/templates/show_discs.html b/templates/show_discs.html new file mode 100644 index 0000000..9c5f74e --- /dev/null +++ b/templates/show_discs.html @@ -0,0 +1,24 @@ +{% extends "layout.html" %} +{% block body %} + {% if session.logged_in %} +
+
+
Title: +
+
URL: +
+
Type: +
YouTube +
FLV +
+
+
+ {% endif %} +
    + {% for entry in entries %} +
  • {{ entry.title }}

    (play{% if fShowEdit %} | edit{% endif %}) + {% else %} +
  • Unbelievable. No entries here so far + {% endfor %} +
+{% endblock %} \ No newline at end of file