Added recipe editing pages.
authorW. Trevor King <wking@drexel.edu>
Thu, 22 Jul 2010 11:58:16 +0000 (07:58 -0400)
committerW. Trevor King <wking@drexel.edu>
Thu, 22 Jul 2010 15:49:03 +0000 (11:49 -0400)
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.

cookbook/cookbook.py
cookbook/server.py
cookbook/template/edit-recipe.html [new file with mode: 0644]
cookbook/template/recipe.html
cookbook/template/recipes.html

index de3a84b278869a2f58876ec0fef12d9b9bc27fa7..b95e576b2d113f2c0b52c1df2adac06c1b565eb5 100644 (file)
@@ -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.
index b5992fcfc15dedf1b5296836d596579f6880c26e..76dded630ff4d3dafd4c90b9fdc9c66fbf63d51a 100644 (file)
 """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 = [
+            '<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
diff --git a/cookbook/template/edit-recipe.html b/cookbook/template/edit-recipe.html
new file mode 100644 (file)
index 0000000..fcd2a01
--- /dev/null
@@ -0,0 +1,13 @@
+{% extends "base.html" %}
+
+{% block page_title %}
+    {% if recipe.url %}
+        <a href="{{ recipe.url }}">{{ recipe.name }}</a>
+    {% else %}
+        {{ recipe.name }}
+    {% endif %}
+{% endblock %}
+
+{% block content %}
+    {{ form }}
+{% endblock %}
index 5864d23a649564592eed3c649264ed0e4a21cf55..cd20d128bf17952144276cefc4669170c04e6e28 100644 (file)
@@ -51,6 +51,7 @@
     </form>
     {% endif %}
 
+    {% if recipe.ingredient_blocks %}
     <div id="recipe-ingredient-blocks">
     {% for ingredient_block in recipe.ingredient_blocks %}
        <h2>{{ ingredient_block.heading() }}</h2>
        </table>
     {% endfor %}
     </div>
+    {% endif %}
 
+    {% if recipe.directions %}
     <div id="recipe-directions">
     <h2>Directions</h2>
     {% for paragraph in recipe.directions.wrapped_paragraphs() %}
         <p>{{ paragraph }}</p>
     {% endfor %}
     </div>
+    {% endif %}
+
+    <p><a href="edit?name={{ recipe.clean_name(ascii=True) }}">
+       edit receipe</a></p>
 {% endblock %}
index c362e660dd9e25b6e7bc090fecba7cfbb550174a..d0b5b36c650c80c348efc7f974e032b385be54b2 100644 (file)
@@ -19,4 +19,6 @@
            {{ recipe.name }}</li>
     {% endfor %}
     </ul>
+
+    <p><a href="edit">add new receipe</a></p>
 {% endblock %}