1 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
3 # This file is part of Cookbook.
5 # Cookbook is free software: you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the
7 # Free Software Foundation, either version 3 of the License, or (at your
8 # option) any later version.
10 # Cookbook is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with Cookbook. If not, see <http://www.gnu.org/licenses/>.
18 """Serve cookbooks over HTTP.
21 from __future__ import absolute_import
22 from __future__ import with_statement
29 from xml.sax import saxutils
32 from jinja2 import Environment, FileSystemLoader
34 from .cookbook import Recipe, Directions, IngredientBlock, Ingredient, Amount
37 class Server (object):
38 """Cookbook web interface."""
40 def __init__(self, cookbook, template_root):
41 self.cookbook = cookbook
42 self.cookbook.make_index()
43 self.env = Environment(loader=FileSystemLoader(template_root))
44 # name regular expressions
45 self._action_ingredient_block_regexp = re.compile(
46 'ingredient block ([0-9]*) ([a-z ]*)')
47 self._action_ingredient_regexp = re.compile(
48 'ingredient ([0-9]*) ([0-9]*) ([a-z ]*)')
49 # value regular expressions
50 self._tag_regexp = re.compile('[a-zA-Z./ ].*') # allowed characters
51 self._action_add_ingredient_block_regexp = re.compile(
52 'add ingredient block')
53 self._action_remove_ingredient_block_regexp = re.compile(
54 'remove ingredient block ([0-9]*)')
55 self._action_add_ingredient_regexp = re.compile(
56 'add ingredient ([0-9]*)')
57 self._action_remove_ingredient_regexp = re.compile(
58 'remove ingredient ([0-9]*) ([0-9]*)')
61 #self.cookbook.save('new-recipe')
65 def index(self, tag=None):
68 Recipes can be filtered by tag.
70 if isinstance(tag, types.StringTypes):
72 template = self.env.get_template('recipes.html')
73 return template.render(cookbook=self.cookbook,
74 recipes=list(self.cookbook.tagged(tag)),
78 def recipe(self, name=None):
79 """Single recipe page.
82 recipe = random.choice(self.cookbook)
84 recipe = self.cookbook.index[name]
85 template = self.env.get_template('recipe.html')
86 return template.render(cookbook=self.cookbook, recipe=recipe)
89 def add_tag(self, name, tag):
90 """Add a tag to a single recipe."""
91 recipe = self.cookbook.index[name]
92 if recipe.tags == None:
94 tag = self._clean_tag(tag)
95 if tag != None and tag not in recipe.tags:
96 recipe.tags.append(tag)
97 with open(recipe.path, 'w') as f:
99 raise cherrypy.HTTPRedirect(
100 u'recipe?name=%s' % recipe.clean_name(ascii=True), status=302)
103 def remove_tag(self, name, tag):
104 """Remove a tag from a single recipe."""
105 recipe = self.cookbook.index[name]
106 if recipe.tags == None:
108 tag = self._clean_tag(tag)
109 if tag != None and tag in recipe.tags:
110 recipe.tags.remove(tag)
111 with open(recipe.path, 'w') as f:
113 raise cherrypy.HTTPRedirect(
114 u'recipe?name=%s' % recipe.clean_name(ascii=True), status=302)
116 def _clean_tag(self, tag):
118 if not isinstance(tag, types.StringTypes):
119 if len(tag) == 2 and '' in tag:
120 # User used either dropdown or textbox
124 # User used both dropdown and textbox
126 m = self._tag_regexp.match(tag)
132 def edit(self, name=None, **kwargs):
133 """Remove a tag from a single recipe."""
134 name,recipe,action = self._normalize_edit_params(name, **kwargs)
135 if action == 'remove':
136 recipe = self.cookbook.index[name]
137 os.remove(recipe.path)
138 self.cookbook.remove(recipe)
139 self.cookbook.make_index()
140 raise cherrypy.HTTPRedirect(u'.', status=302)
141 elif action.startswith('edit'):
142 self._update_recipe(name, recipe)
143 if action == 'edit and redirect': # done editing this recipe
144 raise cherrypy.HTTPRedirect(
145 u'recipe?name=%s' % recipe.clean_name(ascii=True),
147 elif recipe.name != name:
148 raise cherrypy.HTTPRedirect(
149 u'edit?name=%s' % recipe.clean_name(ascii=True),
151 template = self.env.get_template('edit-recipe.html')
152 return template.render(cookbook=self.cookbook,
154 form='\n'.join(self._recipe_form(recipe)))
156 def _update_recipe(self, name, recipe):
157 if len(recipe.ingredient_blocks) == 0:
158 recipe.ingredient_blocks = None
159 if name not in self.cookbook.index: # new recipe
161 self.cookbook.append(recipe)
162 self.cookbook.make_index()
163 elif name != recipe.name: # renamed recipe
165 os.remove(recipe.path)
167 self.cookbook.make_index()
168 with open(recipe.path, 'w') as f:
171 def _normalize_edit_params(self, name, **kwargs):
173 if name in self.cookbook.index:
174 recipe = self.cookbook.index[name]
175 name = recipe.name # get the canonical name for this recipe
177 # Use uuid4() to generate a random name if name is None
178 recipe = Recipe(name=(name or str(uuid.uuid4())))
180 # non-ingredient text updates
181 for attr in ['name','directions','yield_','author','source','url']:
182 n = self._recipe_attr_to_name(attr)
185 v = kwargs.get(n).strip()
188 elif attr == 'directions':
190 v = v.replace('\r\n', '\n').replace('\r', '\n')
191 for p in v.split('\n\n'):
196 if v != getattr(recipe, attr):
197 setattr(recipe, attr, v)
199 if recipe.ingredient_blocks == None:
200 recipe.ingredient_blocks = []
201 # protect against index-changing operations
202 ingredient_block_map = dict(
203 [(i,i) for i in range(len(recipe.ingredient_blocks))])
204 ingredient_map = dict(
205 [(i, dict([(j,j) for j in range(len(ib))]))
206 for i,ib in enumerate(recipe.ingredient_blocks)])
207 # ingredient text updates
208 for key,value in kwargs.items():
209 for k in ['ingredient_block', 'ingredient']:
210 regexp = getattr(self, '_action_%s_regexp' % k)
211 m = regexp.match(key)
213 handler = getattr(self, '_action_%s' % k)
214 action = handler(recipe, action, value, *m.groups(),
215 ingredient_block_map=ingredient_block_map,
216 ingredient_map=ingredient_map)
219 action = kwargs.get('action', action)
220 if action == 'submit':
221 action = 'edit and redirect'
222 for a in ['add_ingredient_block', 'remove_ingredient_block',
223 'add_ingredient', 'remove_ingredient']:
224 regexp = getattr(self, '_action_%s_regexp' % a)
225 m = regexp.match(action)
227 handler = getattr(self, '_action_%s' % a)
228 action = handler(recipe, action, *m.groups(),
229 ingredient_block_map=ingredient_block_map,
230 ingredient_map=ingredient_map)
232 return (name, recipe, action)
234 def _action_ingredient_block(self, recipe, action, value,
235 block_index, block_action,
236 ingredient_block_map, **kwargs):
237 block_index = ingredient_block_map[int(block_index)]
238 ib = recipe.ingredient_blocks[block_index]
241 if block_action == 'position' and value not in [None, '']:
243 if value != block_index:
244 ibs = recipe.ingredient_blocks
245 ibs.insert(value, ibs.pop(block_index))
246 for i in range(min(block_index, value),
247 max(block_index, value)+1):
248 ingredient_block_map[i] = i+1
249 ingredient_block_map[block_index] = value
251 elif block_action == 'name':
257 def _action_ingredient(self, recipe, action, value,
258 block_index, index, ingredient_action,
259 ingredient_block_map, ingredient_map, **kwargs):
260 block_index = ingredient_block_map[int(block_index)]
261 index = ingredient_map[block_index][int(index)]
262 ingredient = recipe.ingredient_blocks[block_index][index]
265 if ingredient_action == 'position' and value != None:
267 if value != block_index:
268 ibs = recipe.ingredient_blocks
269 ibs.insert(value, ibs.pop(block_index))
270 for i in range(min(block_index, value),
271 max(block_index, value)+1):
272 ingredient_block_map[i] = i+1
273 ingredient_block_map[block_index] = value
275 elif ingredient_action in ['name', 'note']:
276 if value != getattr(ingredient, ingredient_action):
277 setattr(ingredient, ingredient_action, value)
279 elif ingredient_action in ['value', 'units']:
280 if value != getattr(ingredient.amount, ingredient_action):
281 setattr(ingredient.amount, ingredient_action, value)
285 def _action_add_ingredient_block(self, recipe, action,
286 ingredient_block_map,
287 ingredient_map, **kwargs):
288 recipe.ingredient_blocks.append(IngredientBlock())
289 block_index = len(recipe.ingredient_blocks)-1
290 ingredient_block_map[block_index] = block_index
291 ingredient_map[block_index] = {}
294 def _action_remove_ingredient_block(self, recipe, action, block_index,
295 ingredient_block_map, **kwargs):
296 block_index = ingredient_block_map[int(block_index)]
297 recipe.ingredient_blocks.pop(block_index)
298 for k,v in ingredient_block_map.items():
300 ingredient_block_map[k] = v-1
303 def _action_add_ingredient(self, recipe, action, block_index,
304 ingredient_block_map,
305 ingredient_map, **kwargs):
306 block_index = ingredient_block_map[int(block_index)]
307 recipe.ingredient_blocks[block_index].append(Ingredient(
309 index = len(recipe.ingredient_blocks[block_index])-1
310 ingredient_map[block_index][index] = index
313 def _action_remove_ingredient(self, recipe, action, block_index, index,
314 ingredient_block_map,
315 ingredient_map, **kwargs):
316 block_index = ingredient_block_map[int(block_index)]
317 index = ingredient_map[block_index][int(index)]
318 recipe.ingredient_blocks[block_index].pop(index)
319 for k,v in ingredient_map[block_index].items():
321 ingredient_map[block_index][k] = v-1
324 def _recipe_form(self, recipe):
326 '<form action="" method="post">',
329 for attr in ['name', 'yield_', 'author', 'source', 'url']:
330 lines.extend(self._text_form(
331 self._recipe_attr_to_name(attr),
332 self._recipe_attr_to_user(attr),
333 getattr(recipe, attr)))
334 for i,ib in enumerate(recipe.ingredient_blocks):
335 lines.extend(self._ingredient_block_form(ib, i))
336 lines.extend(self._submit_form('add ingredient block'))
337 lines.extend(self._textarea_form(
338 self._recipe_attr_to_name('directions'),
339 self._recipe_attr_to_user('directions'),
340 '\n\n'.join(recipe.directions or Directions())))
341 lines.extend(self._submit_form('submit'))
348 def _ingredient_block_form(self, ingredient_block, index):
349 lines = self._submit_form(
350 'remove ingredient block %d' % index,
351 note='Ingredient block %d' % index)
352 for name,user,value in [
353 ('ingredient block %d position' % index,
354 'Position (%d)' % index,
356 ('ingredient block %d name' % index,
358 ingredient_block.name),
360 lines.extend(self._text_form(name, user, value))
361 for i,ingredient in enumerate(ingredient_block):
362 lines.extend(self._ingredient_form(ingredient, index, i))
363 lines.extend(self._submit_form('add ingredient %d' % index))
366 def _ingredient_form(self, ingredient, block_index, index):
367 lines = self._submit_form(
368 'remove ingredient %d %d' % (block_index, index),
369 note='Ingredient %d %d' % (block_index, index))
370 for name,user,value in [
371 ('ingredient %d %d position' % (block_index, index),
372 'Position (%d)' % index,
374 ('ingredient %d %d value' % (block_index, index),
376 ingredient.amount.value),
377 ('ingredient %d %d units' % (block_index, index),
379 ingredient.amount.units),
380 ('ingredient %d %d name' % (block_index, index),
383 ('ingredient %d %d note' % (block_index, index),
387 lines.extend(self._text_form(name, user, value))
388 # TODO: Amount: range_, alternatives
391 def _submit_form(self, value, note=''):
394 ' <td>%s</td>' % note,
395 ' <td><input type="submit" name="action" value="%s"/></td>'
400 def _text_form(self, name, user, value):
403 ' <td><label for="%s">%s</label></td>' % (name, user),
404 ' <td><input type="text" name="%s" size="60" value="%s"/></td>'
405 % (name, value or ''),
409 def _textarea_form(self, name, user, value):
412 ' <td><label for="%s">%s</label></td>' % (name, user),
413 ' <td><textarea name="%s" rows="20" cols="60"/>' % name,
419 def _recipe_attr_to_name(self, attr):
426 def _recipe_attr_to_user(self, attr):
429 return attr.capitalize()