+++ /dev/null
-# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
-#
-# This file is part of Cookbook.
-#
-# Cookbook is free software: you can redistribute it and/or modify it
-# under the terms of the GNU General Public License as published by the
-# Free Software Foundation, either version 3 of the License, or (at your
-# option) any later version.
-#
-# Cookbook is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Cookbook. If not, see <http://www.gnu.org/licenses/>.
-
-"""Serve cookbooks over HTTP.
-"""
-
-from __future__ import absolute_import
-from __future__ import with_statement
-
-import os
-import random
-import re
-import types
-from urllib import urlencode
-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."""
-
- def __init__(self, cookbook, template_root):
- self.cookbook = cookbook
- self.cookbook.make_index()
- self.env = Environment(loader=FileSystemLoader(template_root))
- # 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')
- pass
-
- @cherrypy.expose
- def index(self, tag=None):
- """Recipe index page.
-
- Recipes can be filtered by tag.
- """
- if isinstance(tag, types.StringTypes):
- tag = [tag]
- template = self.env.get_template('recipes.html')
- return template.render(cookbook=self.cookbook,
- recipes=list(self.cookbook.tagged(tag)),
- selected_tags=tag)
-
- @cherrypy.expose
- def recipe(self, name=None):
- """Single recipe page.
- """
- if name == None:
- recipe = random.choice(self.cookbook)
- else:
- recipe = self.cookbook.index[name]
- tag_links = [
- '<a href="./?%s">%s</a>' % (urlencode({'tag':t}), t)
- for t in (recipe.tags or [])]
- template = self.env.get_template('recipe.html')
- return template.render(cookbook=self.cookbook, recipe=recipe,
- tag_links=tag_links)
-
- @cherrypy.expose
- def add_tag(self, name, tag):
- """Add a tag to a single recipe."""
- recipe = self.cookbook.index[name]
- if recipe.tags == None:
- recipe.tags = []
- tag = self._clean_tag(tag)
- if tag != None and tag not in recipe.tags:
- recipe.tags.append(tag)
- with open(recipe.path, 'w') as f:
- recipe.save(f)
- raise cherrypy.HTTPRedirect(
- u'recipe?name=%s' % recipe.clean_name(ascii=True), status=302)
-
- @cherrypy.expose
- def remove_tag(self, name, tag):
- """Remove a tag from a single recipe."""
- recipe = self.cookbook.index[name]
- if recipe.tags == None:
- return
- tag = self._clean_tag(tag)
- if tag != None and tag in recipe.tags:
- recipe.tags.remove(tag)
- with open(recipe.path, 'w') as f:
- recipe.save(f)
- raise cherrypy.HTTPRedirect(
- u'recipe?name=%s' % recipe.clean_name(ascii=True), status=302)
-
- def _clean_tag(self, tag):
- """Sanitize tag."""
- if not isinstance(tag, types.StringTypes):
- if len(tag) == 2 and '' in tag:
- # User used either dropdown or textbox
- tag.remove('')
- tag = tag[0]
- else:
- # User used both dropdown and textbox
- return None
- 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 == 'remove':
- recipe = self.cookbook.index[name]
- os.remove(recipe.path)
- self.cookbook.remove(recipe)
- self.cookbook.make_index()
- raise cherrypy.HTTPRedirect(u'.', status=302)
- elif 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()
- self.cookbook.sort(key=lambda r: r.path)
- 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)
- kws = {'ingredient_block_map': ingredient_block_map,
- 'ingredient_map': ingredient_map}
- action = handler(recipe, action, value, *m.groups(), **kws)
- 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)
- kws = {'ingredient_block_map': ingredient_block_map,
- 'ingredient_map': ingredient_map}
- action = handler(recipe, action, *m.groups(), **kws)
- 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
- doctest.testmod()