From: W. Trevor King Date: Thu, 22 Jul 2010 11:58:16 +0000 (-0400) Subject: Added recipe editing pages. X-Git-Tag: v0.1~10 X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=20d3a594448e3e932fe935799be6db059d91f205;p=cookbook.git Added recipe editing pages. Specifically: * Added "add new recipe" link to recipes.html template. * Added "edit recipe" link to recipe.html template. * Added new template: edit-recipe.html * Added new Server.edit() and sub-functions. * Moved Cookbook's save dir definition to Cookbook.save_dir. * Moved Recipe.path name generation to Cookbook._free_path(). * Cookbook.make_index() now adds paths to Recipes with .path == None. * In-memory updates for everything. * Add/remove ingredient blocks with index mapping. Along the way, I fixed up some random bugs: * recipe.html was failing if there were no ingredient_blocks or directions. * Amount.__unicode__ was failing if some attributes were None. * Ingredient.__unicode__ was failing if .amount was None. * Allow printing of recipes when .directions == None. I also made a few style fixes: * Server.tag_regexp -> ._tag_regexp, since it's internal data. --- diff --git a/cookbook/cookbook.py b/cookbook/cookbook.py index de3a84b..b95e576 100644 --- a/cookbook/cookbook.py +++ b/cookbook/cookbook.py @@ -149,7 +149,7 @@ class Amount (object): 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 = {} @@ -202,8 +202,8 @@ class Ingredient (object): 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 = {} @@ -439,6 +439,7 @@ class Recipe (object): self.source = source self.url = url self.tags = tags + self.path = None def clean_name(self, ascii=False): name = self.name @@ -492,13 +493,17 @@ class Recipe (object): 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): @@ -507,41 +512,47 @@ 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. diff --git a/cookbook/server.py b/cookbook/server.py index b5992fc..76dded6 100644 --- a/cookbook/server.py +++ b/cookbook/server.py @@ -18,17 +18,21 @@ """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.""" @@ -37,7 +41,21 @@ class Server (object): 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') @@ -105,11 +123,305 @@ class Server (object): 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 = [ + '
', + '', + ] + 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([ + '
', + '
', + ]) + 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 [ + ' ', + ' %s' % note, + ' ' + % value, + ' ', + ] + + def _text_form(self, name, user, value): + return [ + ' ', + ' ' % (name, user), + ' ' + % (name, value or ''), + ' ', + ] + + def _textarea_form(self, name, user, value): + return [ + ' ', + ' ' % (name, user), + ' ' + ' ', + ] + + 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 diff --git a/cookbook/template/edit-recipe.html b/cookbook/template/edit-recipe.html new file mode 100644 index 0000000..fcd2a01 --- /dev/null +++ b/cookbook/template/edit-recipe.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block page_title %} + {% if recipe.url %} + {{ recipe.name }} + {% else %} + {{ recipe.name }} + {% endif %} +{% endblock %} + +{% block content %} + {{ form }} +{% endblock %} diff --git a/cookbook/template/recipe.html b/cookbook/template/recipe.html index 5864d23..cd20d12 100644 --- a/cookbook/template/recipe.html +++ b/cookbook/template/recipe.html @@ -51,6 +51,7 @@ {% endif %} + {% if recipe.ingredient_blocks %}
{% for ingredient_block in recipe.ingredient_blocks %}

{{ ingredient_block.heading() }}

@@ -61,11 +62,17 @@ {% endfor %}
+ {% endif %} + {% if recipe.directions %}

Directions

{% for paragraph in recipe.directions.wrapped_paragraphs() %}

{{ paragraph }}

{% endfor %}
+ {% endif %} + +

+ edit receipe

{% endblock %} diff --git a/cookbook/template/recipes.html b/cookbook/template/recipes.html index c362e66..d0b5b36 100644 --- a/cookbook/template/recipes.html +++ b/cookbook/template/recipes.html @@ -19,4 +19,6 @@ {{ recipe.name }} {% endfor %} + +

add new receipe

{% endblock %}