if len(self.alternatives) > 0:
ret.append('(%s)' % ', '.join(
[unicode(a) for a in self.alternatives]))
- return ' '.join(ret)
+ return ' '.join([x for x in ret if x != None])
def to_yaml(self):
d = {}
def __unicode__(self):
if self.note == None:
- return '%s %s' % (unicode(self.amount), self.name)
- return '%s %s, %s' % (unicode(self.amount), self.name, self.note)
+ return '%s %s' % (unicode(self.amount or ''), self.name)
+ return '%s %s, %s' % (unicode(self.amount or ''), self.name, self.note)
def to_yaml(self):
d = {}
self.source = source
self.url = url
self.tags = tags
+ self.path = None
def clean_name(self, ascii=False):
name = self.name
self.directions = Directions()
self.directions.from_yaml(dict.get('directions', None))
- def save(self, stream):
+ def save(self, stream, path=None):
yaml.dump(self.to_yaml(), stream,
default_flow_style=False, allow_unicode=True, width=78)
+ if path != None:
+ self.path = path
- def load(self, stream):
+ def load(self, stream, path=None):
dict = yaml.load(stream)
self.from_yaml(dict)
+ if path != None:
+ self.path = path
class Cookbook (list):
"""
def __init__(self, name="Mom's cookbook", *args, **kwargs):
self.name = name
+ self.save_dir = u'recipe'
super(Cookbook, self).__init__(*args, **kwargs)
- def save(self, dir='recipe'):
- if not os.path.isdir(dir):
- os.mkdir(dir)
+ def save(self):
+ if not os.path.isdir(self.save_dir):
+ os.mkdir(self.save_dir)
paths = []
for recipe in self:
- base_path = recipe.clean_name()
- path = base_path
- i = 2
- while path in paths:
- path = '%s_%d' % (base_path, i)
- i += 1
- for x in ['large']:
- if x in path:
- print paths[-4:]
- paths.append(path)
- with open(os.path.join(dir, path), 'w') as f:
- recipe.save(f)
-
- def load(self, dir='recipe'):
- for path in os.listdir(dir):
+ p = self._free_path(recipe, paths)
+ paths.append(p)
+ with open(p, 'w') as f:
+ recipe.save(f, p)
+
+ def load(self):
+ for path in os.listdir(self.save_dir):
r = Recipe()
- p = os.path.join(dir, path)
+ p = os.path.join(self.save_dir, path)
with open(p, 'r') as f:
- r.load(f)
- r.path = p
+ r.load(f, p)
self.append(r)
def make_index(self):
self.index = {}
+ paths = [recipe.path for recipe in self]
for recipe in self:
self.index[recipe.name] = recipe
self.index[recipe.clean_name()] = recipe
self.index[recipe.clean_name(ascii=True)] = recipe
+ if recipe.path == None:
+ p = self._free_path(recipe, paths)
+ paths.append(p)
+ recipe.path = p
+
+ def _free_path(self, recipe, paths):
+ base_path = os.path.join(self.save_dir, recipe.clean_name())
+ path = base_path
+ i = 2
+ while path in paths:
+ path = '%s_%d' % (base_path, i)
+ i += 1
+ return path
def tags(self):
"""List all tags used in this cookbook.
"""Serve cookbooks over HTTP.
"""
+from __future__ import absolute_import
from __future__ import with_statement
import os
import random
import re
import types
+import uuid
from xml.sax import saxutils
import cherrypy
from jinja2 import Environment, FileSystemLoader
+from .cookbook import Recipe, Directions, IngredientBlock, Ingredient, Amount
+
class Server (object):
"""Cookbook web interface."""
self.cookbook = cookbook
self.cookbook.make_index()
self.env = Environment(loader=FileSystemLoader(template_root))
- self.tag_regexp = re.compile('[a-zA-Z./ ].*') # allowed characters
+ # name regular expressions
+ self._action_ingredient_block_regexp = re.compile(
+ 'ingredient block ([0-9]*) ([a-z ]*)')
+ self._action_ingredient_regexp = re.compile(
+ 'ingredient ([0-9]*) ([0-9]*) ([a-z ]*)')
+ # value regular expressions
+ self._tag_regexp = re.compile('[a-zA-Z./ ].*') # allowed characters
+ self._action_add_ingredient_block_regexp = re.compile(
+ 'add ingredient block')
+ self._action_remove_ingredient_block_regexp = re.compile(
+ 'remove ingredient block ([0-9]*)')
+ self._action_add_ingredient_regexp = re.compile(
+ 'add ingredient ([0-9]*)')
+ self._action_remove_ingredient_regexp = re.compile(
+ 'remove ingredient ([0-9]*) ([0-9]*)')
def cleanup(self):
#self.cookbook.save('new-recipe')
else:
# User used both dropdown and textbox
return None
- m = self.tag_regexp.match(tag)
+ m = self._tag_regexp.match(tag)
if m != None:
return m.group()
return None
+ @cherrypy.expose
+ def edit(self, name=None, **kwargs):
+ """Remove a tag from a single recipe."""
+ name,recipe,action = self._normalize_edit_params(name, **kwargs)
+ if action.startswith('edit'):
+ self._update_recipe(name, recipe)
+ if action == 'edit and redirect': # done editing this recipe
+ raise cherrypy.HTTPRedirect(
+ u'recipe?name=%s' % recipe.clean_name(ascii=True),
+ status=302)
+ elif recipe.name != name:
+ raise cherrypy.HTTPRedirect(
+ u'edit?name=%s' % recipe.clean_name(ascii=True),
+ status=302)
+ template = self.env.get_template('edit-recipe.html')
+ return template.render(cookbook=self.cookbook,
+ recipe=recipe,
+ form='\n'.join(self._recipe_form(recipe)))
+
+ def _update_recipe(self, name, recipe):
+ if len(recipe.ingredient_blocks) == 0:
+ recipe.ingredient_blocks = None
+ if name not in self.cookbook.index: # new recipe
+ print 'new'
+ self.cookbook.append(recipe)
+ self.cookbook.make_index()
+ elif name != recipe.name: # renamed recipe
+ print 'rename'
+ os.remove(recipe.path)
+ recipe.path = None
+ self.cookbook.make_index()
+ with open(recipe.path, 'w') as f:
+ recipe.save(f)
+
+ def _normalize_edit_params(self, name, **kwargs):
+ action = 'view form'
+ if name in self.cookbook.index:
+ recipe = self.cookbook.index[name]
+ name = recipe.name # get the canonical name for this recipe
+ else:
+ # Use uuid4() to generate a random name if name is None
+ recipe = Recipe(name=(name or str(uuid.uuid4())))
+ recipe.path = None
+ # non-ingredient text updates
+ for attr in ['name','directions','yield_','author','source','url']:
+ n = self._recipe_attr_to_name(attr)
+ if n not in kwargs:
+ continue
+ v = kwargs.get(n).strip()
+ if v == '':
+ v = None
+ elif attr == 'directions':
+ d = Directions()
+ v = v.replace('\r\n', '\n').replace('\r', '\n')
+ for p in v.split('\n\n'):
+ p = p.strip()
+ if len(p) > 0:
+ d.append(p)
+ v = d
+ if v != getattr(recipe, attr):
+ setattr(recipe, attr, v)
+ action = 'edit'
+ if recipe.ingredient_blocks == None:
+ recipe.ingredient_blocks = []
+ # protect against index-changing operations
+ ingredient_block_map = dict(
+ [(i,i) for i in range(len(recipe.ingredient_blocks))])
+ ingredient_map = dict(
+ [(i, dict([(j,j) for j in range(len(ib))]))
+ for i,ib in enumerate(recipe.ingredient_blocks)])
+ # ingredient text updates
+ for key,value in kwargs.items():
+ for k in ['ingredient_block', 'ingredient']:
+ regexp = getattr(self, '_action_%s_regexp' % k)
+ m = regexp.match(key)
+ if m != None:
+ handler = getattr(self, '_action_%s' % k)
+ action = handler(recipe, action, value, *m.groups(),
+ ingredient_block_map=ingredient_block_map,
+ ingredient_map=ingredient_map)
+ break
+ # button updates
+ action = kwargs.get('action', action)
+ if action == 'submit':
+ action = 'edit and redirect'
+ for a in ['add_ingredient_block', 'remove_ingredient_block',
+ 'add_ingredient', 'remove_ingredient']:
+ regexp = getattr(self, '_action_%s_regexp' % a)
+ m = regexp.match(action)
+ if m != None:
+ handler = getattr(self, '_action_%s' % a)
+ action = handler(recipe, action, *m.groups(),
+ ingredient_block_map=ingredient_block_map,
+ ingredient_map=ingredient_map)
+ break
+ return (name, recipe, action)
+
+ def _action_ingredient_block(self, recipe, action, value,
+ block_index, block_action,
+ ingredient_block_map, **kwargs):
+ block_index = ingredient_block_map[int(block_index)]
+ ib = recipe.ingredient_blocks[block_index]
+ if value == '':
+ value = None
+ if block_action == 'position' and value not in [None, '']:
+ value = int(value)
+ if value != block_index:
+ ibs = recipe.ingredient_blocks
+ ibs.insert(value, ibs.pop(block_index))
+ for i in range(min(block_index, value),
+ max(block_index, value)+1):
+ ingredient_block_map[i] = i+1
+ ingredient_block_map[block_index] = value
+ return 'edit'
+ elif block_action == 'name':
+ if value != ib.name:
+ ib.name = value
+ return 'edit'
+ return action
+
+ def _action_ingredient(self, recipe, action, value,
+ block_index, index, ingredient_action,
+ ingredient_block_map, ingredient_map, **kwargs):
+ block_index = ingredient_block_map[int(block_index)]
+ index = ingredient_map[block_index][int(index)]
+ ingredient = recipe.ingredient_blocks[block_index][index]
+ if value == '':
+ value = None
+ if ingredient_action == 'position' and value != None:
+ value = int(value)
+ if value != block_index:
+ ibs = recipe.ingredient_blocks
+ ibs.insert(value, ibs.pop(block_index))
+ for i in range(min(block_index, value),
+ max(block_index, value)+1):
+ ingredient_block_map[i] = i+1
+ ingredient_block_map[block_index] = value
+ return 'edit'
+ elif ingredient_action in ['name', 'note']:
+ if value != getattr(ingredient, ingredient_action):
+ setattr(ingredient, ingredient_action, value)
+ return 'edit'
+ elif ingredient_action in ['value', 'units']:
+ if value != getattr(ingredient.amount, ingredient_action):
+ setattr(ingredient.amount, ingredient_action, value)
+ return 'edit'
+ return action
+
+ def _action_add_ingredient_block(self, recipe, action,
+ ingredient_block_map,
+ ingredient_map, **kwargs):
+ recipe.ingredient_blocks.append(IngredientBlock())
+ block_index = len(recipe.ingredient_blocks)-1
+ ingredient_block_map[block_index] = block_index
+ ingredient_map[block_index] = {}
+ return 'edit'
+
+ def _action_remove_ingredient_block(self, recipe, action, block_index,
+ ingredient_block_map, **kwargs):
+ block_index = ingredient_block_map[int(block_index)]
+ recipe.ingredient_blocks.pop(block_index)
+ for k,v in ingredient_block_map.items():
+ if v >= block_index:
+ ingredient_block_map[k] = v-1
+ return 'edit'
+
+ def _action_add_ingredient(self, recipe, action, block_index,
+ ingredient_block_map,
+ ingredient_map, **kwargs):
+ block_index = ingredient_block_map[int(block_index)]
+ recipe.ingredient_blocks[block_index].append(Ingredient(
+ amount=Amount()))
+ index = len(recipe.ingredient_blocks[block_index])-1
+ ingredient_map[block_index][index] = index
+ return 'edit'
+
+ def _action_remove_ingredient(self, recipe, action, block_index, index,
+ ingredient_block_map,
+ ingredient_map, **kwargs):
+ block_index = ingredient_block_map[int(block_index)]
+ index = ingredient_map[block_index][int(index)]
+ recipe.ingredient_blocks[block_index].pop(index)
+ for k,v in ingredient_map[block_index].items():
+ if v >= index:
+ ingredient_map[block_index][k] = v-1
+ return 'edit'
+
+ def _recipe_form(self, recipe):
+ lines = [
+ '<form action="" method="post">',
+ '<table>',
+ ]
+ for attr in ['name', 'yield_', 'author', 'source', 'url']:
+ lines.extend(self._text_form(
+ self._recipe_attr_to_name(attr),
+ self._recipe_attr_to_user(attr),
+ getattr(recipe, attr)))
+ for i,ib in enumerate(recipe.ingredient_blocks):
+ lines.extend(self._ingredient_block_form(ib, i))
+ lines.extend(self._submit_form('add ingredient block'))
+ lines.extend(self._textarea_form(
+ self._recipe_attr_to_name('directions'),
+ self._recipe_attr_to_user('directions'),
+ '\n\n'.join(recipe.directions or Directions())))
+ lines.extend(self._submit_form('submit'))
+ lines.extend([
+ '</table>',
+ '</form>',
+ ])
+ return lines
+
+ def _ingredient_block_form(self, ingredient_block, index):
+ lines = self._submit_form(
+ 'remove ingredient block %d' % index,
+ note='Ingredient block %d' % index)
+ for name,user,value in [
+ ('ingredient block %d position' % index,
+ 'Position (%d)' % index,
+ None),
+ ('ingredient block %d name' % index,
+ 'Name',
+ ingredient_block.name),
+ ]:
+ lines.extend(self._text_form(name, user, value))
+ for i,ingredient in enumerate(ingredient_block):
+ lines.extend(self._ingredient_form(ingredient, index, i))
+ lines.extend(self._submit_form('add ingredient %d' % index))
+ return lines
+
+ def _ingredient_form(self, ingredient, block_index, index):
+ lines = self._submit_form(
+ 'remove ingredient %d %d' % (block_index, index),
+ note='Ingredient %d %d' % (block_index, index))
+ for name,user,value in [
+ ('ingredient %d %d position' % (block_index, index),
+ 'Position (%d)' % index,
+ ''),
+ ('ingredient %d %d value' % (block_index, index),
+ 'Value',
+ ingredient.amount.value),
+ ('ingredient %d %d units' % (block_index, index),
+ 'Units',
+ ingredient.amount.units),
+ ('ingredient %d %d name' % (block_index, index),
+ 'Name',
+ ingredient.name),
+ ('ingredient %d %d note' % (block_index, index),
+ 'Note',
+ ingredient.note),
+ ]:
+ lines.extend(self._text_form(name, user, value))
+ # TODO: Amount: range_, alternatives
+ return lines
+
+ def _submit_form(self, value, note=''):
+ return [
+ ' <tr>',
+ ' <td>%s</td>' % note,
+ ' <td><input type="submit" name="action" value="%s"/></td>'
+ % value,
+ ' </tr>',
+ ]
+
+ def _text_form(self, name, user, value):
+ return [
+ ' <tr>',
+ ' <td><label for="%s">%s</label></td>' % (name, user),
+ ' <td><input type="text" name="%s" size="60" value="%s"/></td>'
+ % (name, value or ''),
+ ' </tr>',
+ ]
+
+ def _textarea_form(self, name, user, value):
+ return [
+ ' <tr>',
+ ' <td><label for="%s">%s</label></td>' % (name, user),
+ ' <td><textarea name="%s" rows="20" cols="60"/>' % name,
+ value,
+ ' </textarea></td>'
+ ' </tr>',
+ ]
+
+ def _recipe_attr_to_name(self, attr):
+ if attr == 'yield_':
+ return 'yield'
+ elif attr == 'name':
+ return 'new_name'
+ return attr
+
+ def _recipe_attr_to_user(self, attr):
+ if attr == 'yield_':
+ attr = 'yield'
+ return attr.capitalize()
+
def test():
import doctest