Skip to content

Commit

Permalink
Add basic moderation, CouchDB storage, and API.
Browse files Browse the repository at this point in the history
  • Loading branch information
chromakode committed Dec 3, 2013
1 parent deb6eef commit 4edccef
Show file tree
Hide file tree
Showing 9 changed files with 3,097 additions and 39 deletions.
1 change: 1 addition & 0 deletions danceparty/default_settings.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
DEBUG = False
MAX_CONTENT_LENGTH = 2 * 1024 * 1024 # MB
DB_NAME = "danceparty"
171 changes: 160 additions & 11 deletions danceparty/main.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
import binascii
import cStringIO
import hashlib
import os
import random
import time
import uuid
from functools import wraps

import bcrypt
import couchdb
from PIL import Image
from flask import request, render_template, send_from_directory
from flask import (
g,
json,
abort,
redirect,
request,
Response,
render_template,
session,
send_from_directory,
url_for,
)
from werkzeug.security import safe_str_cmp

from danceparty import app

Expand All @@ -19,25 +36,157 @@ def check_gif(data):
return False


def connect_db():
g.couch = couchdb.client.Server()
db_name = app.config['DB_NAME']
if not db_name in g.couch:
g.couch.create(db_name)
g.db = g.couch[db_name]

views = {
'_id': '_design/' + db_name,
'language': 'javascript',
'views': {
'approved': {
'map': "function(doc) { if (doc.status == 'approved') { emit(doc.ts, doc) } }"
},
'review-queue': {
'map': "function(doc) { if (doc.status == 'new') { emit(doc.ts, doc) } }"
},
'review-all': {
'map': "function(doc) { emit(doc.ts, doc) }"
},
},
}
doc = g.db.get(views['_id'], {})
if doc['views'] != views['views']:
doc.update(views)
g.db.save(doc)


def dance_json(dance):
data = {}
data['id'] = dance['_id']
data['ts'] = dance['ts']
data['url'] = '/dance/' + dance['_id'] + '.gif'
if g.is_reviewer:
data['status'] = dance['status']
return data


def dances_json(view, limit=100, shuffle=False):
rows = g.db.view(view, limit=limit, include_docs=True)
if shuffle:
rows = random.sample(rows, min(limit, len(rows)))
return [dance_json(row.doc) for row in rows]


def require_reviewer(f):
@wraps(f)
def with_auth(*args, **kwargs):
if request.scheme != 'https':
return redirect(url_for(
request.endpoint,
_scheme='https',
_external='true'
))

if not g.is_reviewer:
return Response(
'Could not verify your access level for that URL.\n'
'You have to login with proper credentials', 401,
{'WWW-Authenticate': 'Basic realm="mc"'})
else:
return f(*args, **kwargs)

return with_auth


def csrf_token(salt=None):
if not session.get('csrft'):
session['csrft'] = binascii.b2a_hex(os.urandom(16))
return session['csrft']


@app.before_request
def before_request():
g.couch = connect_db()

if request.method not in ['GET', 'HEAD', 'OPTIONS']:
if (not request.headers.get('X-CSRFT') or
not session.get('csrft') or
not safe_str_cmp(session['csrft'], request.headers['X-CSRFT'])):
abort(400)

g.is_reviewer = False
auth = request.authorization
if (auth and request.scheme == 'https' and
safe_str_cmp(auth.username, app.config['REVIEWER_USERNAME'])):
crypted = bcrypt.hashpw(auth.password, app.config['REVIEWER_PASSWORD'])
if safe_str_cmp(crypted, app.config['REVIEWER_PASSWORD']):
g.is_reviewer = True


@app.route('/')
def dances_plz():
fs = os.listdir(app.config['UPLOAD_FOLDER'])
fs.sort()
dances = random.sample(fs, min(100, len(fs)))
return render_template('dance.html', dances=dances)
return render_template('dance.html',
dances_json=dances_json('danceparty/approved'),
config={'mode': 'party', 'csrft': csrf_token()},
)


@app.route('/review')
@app.route('/review/all')
@require_reviewer
def review_dances_plz():
if request.path.endswith('all'):
query = 'danceparty/review-all'
else:
query = 'danceparty/review-queue'

@app.route('/dance/<num>')
def uploaded_file(num):
return send_from_directory(app.config['UPLOAD_FOLDER'], num)
return render_template('dance.html',
dances_json=dances_json(query),
config={'mode': 'review', 'csrft': csrf_token()},
)


@app.route('/dance/<dance_id>', methods=['GET'])
def get_dance(dance_id):
dance = g.db[dance_id]
return json.jsonify(dance_json(dance))


@app.route('/dance/<dance_id>', methods=['PUT'])
@require_reviewer
def update_dance(dance_id):
dance = g.db[dance_id]
data = request.get_json()
if data['status'] in ['new', 'approved', 'rejected']:
dance['status'] = data['status']
g.db.save(dance)
return json.jsonify(dance_json(dance))


@app.route('/dance', methods=['POST'])
def upload_dance():
gif = request.files['moves']
gif_data = gif.read()
if gif and check_gif(gif_data):
name = hashlib.sha1(str(uuid.uuid4())).hexdigest() + '.gif'
with open(os.path.join(app.config['UPLOAD_FOLDER'], name), 'w') as out:
dance_id = hashlib.sha1(gif_data).hexdigest()
dance = {
'_id': dance_id,
'ts': time.time(),
'ip': request.remote_addr,
'ua': request.user_agent.string,
'status': 'new',
}
g.db.save(dance)
with open(os.path.join(app.config['UPLOAD_FOLDER'], dance_id + '.gif'), 'w') as out:
out.write(gif_data)
return 'sweet moves!'
return get_dance(dance_id)


@app.route('/dance/<dance_id>.gif')
def uploaded_file(dance_id):
if app.debug:
return send_from_directory(app.config['UPLOAD_FOLDER'], dance_id + '.gif')
41 changes: 39 additions & 2 deletions danceparty/static/dance.less
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,48 @@ button {
}
}

//.dancer { width: 160; height: 120; }
#dances {
.dancer {
.dance {
position: relative;
width: 320px;
height: 240px;
display: block;
float: left;
.transition(background, .5s);

.actions {
position: absolute;
bottom: 4px;
right: 4px;

button {
margin-left: 4px;

&.approve {
background: green;
}

&.reject {
background: red;
}
}
}

&.rejected {
background: red;

img {
opacity: .75;
}
}

&.approved {
background: green;

img {
opacity: .75;
}
}
}
}

Expand Down
98 changes: 83 additions & 15 deletions danceparty/static/danceparty.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,6 @@ booth = {

showPreview: function(preview) {
this.$preview.find('.gif').prop('src', preview)
},

addDance: function(dance) {
$('#dances').prepend(
$('#dances :first-child')
.clone()
.prop('src', dance)
)
}
}
// todo: confirm before uploading
Expand Down Expand Up @@ -91,7 +83,6 @@ recorder = {
onCameraNotSupported: function() {},

record: function() {
console.log('recording')
booth.setState('recording', 0)

setTimeout($.proxy(function() {
Expand Down Expand Up @@ -137,7 +128,7 @@ recorder = {
var formData = new FormData()
formData.append('moves', this.blob)

$.ajax({
Backbone.ajax({
url: '/dance',
type: 'POST',
data: formData,
Expand All @@ -152,8 +143,8 @@ recorder = {
this.gif = this.blob = null
},

onUploaded: function() {
booth.addDance(URL.createObjectURL(this.blob))
onUploaded: function(data) {
dances.add(data)
this._reset()
camera.stop()
booth.hide()
Expand All @@ -166,8 +157,85 @@ recorder = {
}
}

DanceCollection = Backbone.Collection.extend({
model: Backbone.Model.extend({}),
url: '/dance'
})

DanceItem = Backbone.View.extend({
className: 'dance',
template: _.template('<img src="<%- img_url %>">'),
render: function() {
this.$el.html(this.template({
img_url: this.model.get('url')
}))
return this
}
})

DanceReviewItem = DanceItem.extend({
template: _.template('<img src="<%- img_url %>"><div class="actions"><button class="approve">splendid!</button><button class="reject">unacceptable</button></div>'),
events: {
'click .approve': 'approve',
'click .reject': 'reject'
},

initialize: function() {
this.listenTo(this.model, 'change', this.render)
},

render: function() {
DanceItem.prototype.render.apply(this)
var danceStatus = this.model.get('status')
this.$el.toggleClass('rejected', danceStatus == 'rejected')
this.$el.toggleClass('approved', danceStatus == 'approved')
return this
},

approve: function() {
this.model.save({'status': 'approved'})
},

reject: function() {
this.model.save({'status': 'rejected'})
}
})

DanceGrid = Backbone.View.extend({
initialize: function() {
this.listenTo(this.collection, 'add', this.addDance)
},

render: function() {
this.collection.each(this.addDance, this)
},

addDance: function(dance) {
var viewType = config.mode == 'review' ? DanceReviewItem : DanceItem
var view = new viewType({model: dance})
this.$el.append(view.render().$el)
}
})

Backbone.ajax = function(request) {
if (!request.headers) {
request.headers = {}
}
request.headers['X-CSRFT'] = config.csrft
return $.ajax(request)
}

dances = new DanceCollection

$(function() {
booth.init()
booth.show()
booth.setState('no-camera')
if (config.mode == 'party') {
booth.init()
booth.show()
booth.setState('no-camera')
}

grid = new DanceGrid({
el: $('#dances'),
collection: dances
}).render()
})
Loading

0 comments on commit 4edccef

Please sign in to comment.