Skip to content

Commit

Permalink
Finished app. Added README and deployment options
Browse files Browse the repository at this point in the history
  • Loading branch information
z3ugma committed Jan 6, 2015
1 parent 9b39125 commit 4e2af41
Show file tree
Hide file tree
Showing 8 changed files with 400 additions and 3 deletions.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
rethink-recipes
===============================
A lightweight, quick and no-frills recipe application. It's best feature is calculating a color palette dynamically based on the title of the recipe.

Tornado webserver wrapping Flask application with a RethinkDB backend

1. Install Vagrant (www.vagrantup.com)
2. Clone this repository. Navigate to its directory
3. Run 'vagrant up'
4. Navigate to http://localhost:5025 in your browser to view the tornado app
5. Navigate to http://localhost:8025to interact with RethinkDB web admin interface

Dependencies
------------
- flask
- rethinkdb
- colorific (for )
- translitcodec (for generating slugs)
- flask-wtf
- tornado

![Screenshot1](https://raw.githubusercontent.com/z3ugma/rethink-recipes/master/screenshot1.png)
![Screenshot2](https://raw.githubusercontent.com/z3ugma/rethink-recipes/master/screenshot2.png)
![Screenshot3](https://raw.githubusercontent.com/z3ugma/rethink-recipes/master/screenshot3.png)


13 changes: 11 additions & 2 deletions Vagrantfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
dirname = File.basename(Dir.getwd)
config.vm.hostname = dirname

config.vm.network "forwarded_port", guest: 5000, host: 5005
config.vm.network "forwarded_port", guest: 8080, host: 8085
config.vm.network "forwarded_port", guest: 5000, host: 5025
config.vm.network "forwarded_port", guest: 8080, host: 8025
config.vm.network "forwarded_port", guest: 9000, host: 9025

$script = <<SCRIPT
echo "Provisioning Flask, RethinkDB, Python imaging libraries"
Expand All @@ -30,9 +31,17 @@ sudo apt-get -y install rethinkdb
sudo pip install PIL --upgrade
sudo pip install -r /vagrant/requirements.txt
cp -v /vagrant/recipes_flask.conf /etc/init/recipes_flask.conf
cp -v /vagrant/recipes_rethinkdb.conf /etc/init/recipes_rethinkdb.conf
sudo initctl emit vagrant-ready
SCRIPT

config.vm.provision "shell", inline: $script

config.vm.provision "shell", inline: "export EDITOR=nano", privileged: false
config.vm.provision "shell", inline: "(crontab -l 2>/dev/null; echo \"0 3 * * * rethinkdb dump -c localhost:28015 -f /vagrant/recipes_db_backup.tar.gz\") | crontab -", privileged: false


end
6 changes: 6 additions & 0 deletions recipes_flask.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
description "Starts Flask as a Tornado IOLoop service"
author "Fred"
start on vagrant-ready
stop on runlevel [06]
exec python /vagrant/recipes_tornado.py
respawn
6 changes: 6 additions & 0 deletions recipes_rethinkdb.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
description "Starts RethinkDB"
author "Fred"
start on vagrant-ready
stop on runlevel [06]
exec rethinkdb --bind all
respawn
8 changes: 8 additions & 0 deletions recipes_tornado.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from rethink_recipes import app

http_server = HTTPServer(WSGIContainer(app))
http_server.listen(5000)
IOLoop.instance().start()
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ flask
rethinkdb
colorific
translitcodec
flask-wtf
flask-wtf
tornado
259 changes: 259 additions & 0 deletions rethink_recipes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
from collections import namedtuple
from math import sqrt
import random
try:
import Image
except ImportError:
from PIL import Image
import json
import colorsys
import colorific
import urllib2
import urllib, cStringIO
import re
import translitcodec



def get_gimages(query):
answer = []
for i in range(0,8,4):
url = 'https://ajax.googleapis.com/ajax/services/search/images?v=1.0&q=%s&start=%d&imgsz=large&imgtype=photo' % (query,i)
r = urllib2.urlopen(url)
for k in json.loads(r.read())['responseData']['results']:
answer.append(k['tbUrl'])
return answer

def chunks(l, n):
""" Yield successive n-sized chunks from l.
"""
for i in xrange(0, len(l), n):
yield l[i:i+n]

## Slug functions ##

_punct_re = re.compile(r'[\t !"#$%&\()*\-/<=>?@\[\\\]^_`{|},.]+')

def slugify(text, delim=u'-'):
"""Generates an ASCII-only slug."""
result = []
for word in _punct_re.split(text.lower()):
word = word.replace('\'', '') #Don't replace apostrophes with a dash, just strip them
word = word.encode('translit/long')
if word:
result.append(word)
return unicode(delim.join(result))

def gettitle(recipe):
return recipe['title']

from flask.ext.wtf import Form
from wtforms.fields import TextField
from wtforms.validators import Required
from wtforms.fields import StringField
from wtforms.fields import BooleanField
from wtforms.widgets import TextArea

class RecipeForm(Form):
title = TextField('title', validators = [Required()])
ingredients = StringField(u'Ingredients', widget=TextArea(),validators = [Required()])
directions = StringField(u'directions', widget=TextArea(), validators = [Required()])

class DeleteForm(Form):
deleterecipe = BooleanField('deleterecipe')



from flask import *
app = Flask(__name__)
app.secret_key = 'correcthorsebatterystaples'

# rethink imports
import rethinkdb as r
from rethinkdb.errors import RqlRuntimeError, RqlDriverError

# rethink config
RDB_HOST = 'localhost'
RDB_PORT = 28015
TODO_DB = 'recipes'

# db setup; only run once
def dbSetup():
connection = r.connect(host=RDB_HOST, port=RDB_PORT)
try:
r.db_create(TODO_DB).run(connection)
r.db(TODO_DB).table_create('recipes').run(connection)
print 'Database setup completed'
except RqlRuntimeError:
print 'Database already exists.'
finally:
connection.close()
dbSetup()

# open connection before each request
@app.before_request
def before_request():
try:
g.rdb_conn = r.connect(host=RDB_HOST, port=RDB_PORT, db=TODO_DB)
except RqlDriverError:
abort(503, "Database connection could be established.")

# close the connection after each request
@app.teardown_request
def teardown_request(exception):
try:
g.rdb_conn.close()
except AttributeError:
pass


@app.route('/')
def index():
allrecipes = list(r.table('recipes').run(g.rdb_conn))
allrecipes.sort(key=gettitle)
return render_template("index.html", allrecipes=allrecipes)

@app.route('/favicon.ico')
def favicon():
abort(404)

@app.route('/add', methods = ['GET', 'POST'])
def add():
form = RecipeForm()
#form.ingredients.data = "1c ingredient\nenter ingredients here "

if request.method == 'POST' and form.validate():

lightness = []
rainbow = []
resp=[]
urls = get_gimages("+".join(form.title.data.split()))
for url in urls:
i = cStringIO.StringIO(urllib.urlopen(url).read())
quiche = colorific.extract_colors(i, max_colors=5)
resp.extend([each.value for each in quiche.colors])
resp = [(m,sqrt(0.299 * m[0]**2 + 0.587 * m[1]**2 + 0.114 * m[2]**2)) for m in resp]
lightness = sorted(resp,key=lambda x: x[1])
lightness = [i[0] for i in lightness]
lightness.sort(key=lambda tup: colorsys.rgb_to_hsv(tup[0],tup[1],tup[2])[2])
for each in chunks(lightness,10):
avg = tuple(map(lambda y: sum(y) / len(y), zip(*each)))
rainbow.append(avg)

slug = slugify(form.title.data)

recipe = { 'title': form.title.data.title(), 'ingredients': [{'amount': " ".join(ingredient.split()[0:2]), 'what': " ".join(ingredient.split()[2:])} for ingredient in form.ingredients.data.split('\r\n')], 'directions': form.directions.data.split('\r\n'), 'urls': urls, 'slug': slug, 'avgcolors': [list(i) for i in rainbow]}



recipe['ingredients'] = [i for i in recipe['ingredients'] if not i['what']=='']
recipe['directions'] = [i for i in recipe['directions'] if not i=='']

r.table('recipes').insert(recipe).run(g.rdb_conn)

return redirect(url_for('recipe', query=slug))


#r.table('recipes').get('de670bed-791d-41d0-97cb-1ceaf9ba54ac').update({'time_updated': r.now(), 'slug': "broccoli-cheese-soup", 'urls': urls, 'avgcolors': [list(i) for i in rainbow]}).run(g.rdb_conn)

return render_template("add.html", form=form)


@app.route('/<query>')
def recipe(query):
recipe = list(r.table('recipes').filter({'slug': query}).run(g.rdb_conn))

if recipe:
recipe = recipe[0]
else:
abort(404)

steps = []
for i in recipe['directions']:
for j in [k['what'].split(",")[0] for k in recipe['ingredients']]:
i = i.replace(j, ("<kbd>" + j + "</kbd>"))
steps.append(Markup(i))

rainbow = [tuple(l) for l in recipe['avgcolors']]

return render_template("recipes.html", urls = recipe['urls'], avg=rainbow, ingredients = recipe['ingredients'], steps = steps, title=recipe['title'], query=query)


@app.route('/<query>/edit', methods = ['GET', 'POST'])
def edit(query):


form = RecipeForm()
recipe = list(r.table('recipes').filter({'slug': query}).run(g.rdb_conn))

if recipe:
recipe = recipe[0]
else:
abort(404)

if request.method == 'GET':

form.ingredients.data = "\r\n".join([(i['amount'] + " " + i['what']) for i in recipe['ingredients']])
form.directions.data = "\r\n".join(i for i in recipe['directions'])
form.title.data = recipe['title']

return render_template("edit.html", form=form, recipe=recipe)

if request.method == 'POST' and form.validate():

lightness = []
rainbow = []
resp=[]
urls = get_gimages("+".join(form.title.data.split()))
for url in urls:
i = cStringIO.StringIO(urllib.urlopen(url).read())
quiche = colorific.extract_colors(i, max_colors=5)
resp.extend([each.value for each in quiche.colors])
resp = [(m,sqrt(0.299 * m[0]**2 + 0.587 * m[1]**2 + 0.114 * m[2]**2)) for m in resp]
lightness = sorted(resp,key=lambda x: x[1])
lightness = [i[0] for i in lightness]
lightness.sort(key=lambda tup: colorsys.rgb_to_hsv(tup[0],tup[1],tup[2])[2])
for each in chunks(lightness,10):
avg = tuple(map(lambda y: sum(y) / len(y), zip(*each)))
rainbow.append(avg)

slug = slugify(form.title.data)

id = recipe['id']

recipe = { 'title': form.title.data.title(), 'ingredients': [{'amount': " ".join(ingredient.split()[0:2]), 'what': " ".join(ingredient.split()[2:])} for ingredient in form.ingredients.data.split('\r\n')], 'directions': form.directions.data.split('\r\n'), 'urls': urls, 'slug': slug, 'avgcolors': [list(i) for i in rainbow]}

recipe['ingredients'] = [i for i in recipe['ingredients'] if not i['what']=='']
recipe['directions'] = [i for i in recipe['directions'] if not i=='']

r.table('recipes').get(id).update(recipe).run(g.rdb_conn)

return redirect(url_for('recipe', query=slug))

@app.route('/<query>/delete', methods = ['GET', 'POST'])
def delete(query):
form = DeleteForm()
recipe = list(r.table('recipes').filter({'slug': query}).run(g.rdb_conn))
if recipe:
recipe = recipe[0]
else:
abort(404)

if request.method == 'GET':

return render_template("delete.html", query=query, recipe=recipe, form=form)

if request.method == 'POST':
if 'deleterecipe' in request.form:

id = recipe['id']

r.table('recipes').get(id).delete().run(g.rdb_conn)
return redirect(url_for('index'))
else:
flash("Are you sure? Check the box")
return render_template("delete.html", query=query, recipe=recipe, form=form)


if __name__ == '__main__':
app.run(debug = True, host='0.0.0.0')
Loading

0 comments on commit 4e2af41

Please sign in to comment.