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
28 from urllib import urlencode
30 from xml.sax import saxutils
33 from jinja2 import Environment, FileSystemLoader
35 from .cookbook import Recipe, Directions, IngredientBlock, Ingredient, Amount
38 class Server (object):
39 """Cookbook web interface."""
41 def __init__(self, cookbook, template_root):
42 self.cookbook = cookbook
43 self.cookbook.make_index()
44 self.env = Environment(loader=FileSystemLoader(template_root))
45 # name regular expressions
46 self._action_ingredient_block_regexp = re.compile(
47 'ingredient block ([0-9]*) ([a-z ]*)')
48 self._action_ingredient_regexp = re.compile(
49 'ingredient ([0-9]*) ([0-9]*) ([a-z ]*)')
50 # value regular expressions
51 self._tag_regexp = re.compile('[a-zA-Z./ ].*') # allowed characters
52 self._action_add_ingredient_block_regexp = re.compile(
53 'add ingredient block')
54 self._action_remove_ingredient_block_regexp = re.compile(
55 'remove ingredient block ([0-9]*)')
56 self._action_add_ingredient_regexp = re.compile(
57 'add ingredient ([0-9]*)')
58 self._action_remove_ingredient_regexp = re.compile(
59 'remove ingredient ([0-9]*) ([0-9]*)')
62 #self.cookbook.save('new-recipe')
66 def index(self, tag=None):
69 Recipes can be filtered by tag.
71 if isinstance(tag, types.StringTypes):
73 template = self.env.get_template('recipes.html')
74 return template.render(cookbook=self.cookbook,
75 recipes=list(self.cookbook.tagged(tag)),
79 def recipe(self, name=None):
80 """Single recipe page.
83 recipe = random.choice(self.cookbook)
85 recipe = self.cookbook.index[name]
87 '<a href="./?%s">%s</a>' % (urlencode({'tag':t}), t)
88 for t in (recipe.tags or [])]
89 template = self.env.get_template('recipe.html')
90 return template.render(cookbook=self.cookbook, recipe=recipe,
94 def add_tag(self, name, tag):
95 """Add a tag to a single recipe."""
96 recipe = self.cookbook.index[name]
97 if recipe.tags == None:
99 tag = self._clean_tag(tag)
100 if tag != None and tag not in recipe.tags:
101 recipe.tags.append(tag)
102 with open(recipe.path, 'w') as f:
104 raise cherrypy.HTTPRedirect(
105 u'recipe?name=%s' % recipe.clean_name(ascii=True), status=302)
108 def remove_tag(self, name, tag):
109 """Remove a tag from a single recipe."""
110 recipe = self.cookbook.index[name]
111 if recipe.tags == None:
113 tag = self._clean_tag(tag)
114 if tag != None and tag in recipe.tags:
115 recipe.tags.remove(tag)
116 with open(recipe.path, 'w') as f:
118 raise cherrypy.HTTPRedirect(
119 u'recipe?name=%s' % recipe.clean_name(ascii=True), status=302)
121 def _clean_tag(self, tag):
123 if not isinstance(tag, types.StringTypes):
124 if len(tag) == 2 and '' in tag:
125 # User used either dropdown or textbox
129 # User used both dropdown and textbox
131 m = self._tag_regexp.match(tag)
137 def edit(self, name=None, **kwargs):
138 """Remove a tag from a single recipe."""
139 name,recipe,action = self._normalize_edit_params(name, **kwargs)
140 if action == 'remove':
141 recipe = self.cookbook.index[name]
142 os.remove(recipe.path)
143 self.cookbook.remove(recipe)
144 self.cookbook.make_index()
145 raise cherrypy.HTTPRedirect(u'.', status=302)
146 elif action.startswith('edit'):
147 self._update_recipe(name, recipe)
148 if action == 'edit and redirect': # done editing this recipe
149 raise cherrypy.HTTPRedirect(
150 u'recipe?name=%s' % recipe.clean_name(ascii=True),
152 elif recipe.name != name:
153 raise cherrypy.HTTPRedirect(
154 u'edit?name=%s' % recipe.clean_name(ascii=True),
156 template = self.env.get_template('edit-recipe.html')
157 return template.render(cookbook=self.cookbook,
159 form='\n'.join(self._recipe_form(recipe)))
161 def _update_recipe(self, name, recipe):
162 if len(recipe.ingredient_blocks) == 0:
163 recipe.ingredient_blocks = None
164 if name not in self.cookbook.index: # new recipe
166 self.cookbook.append(recipe)
167 self.cookbook.make_index()
168 self.cookbook.sort(key=lambda r: r.path)
169 elif name != recipe.name: # renamed recipe
171 os.remove(recipe.path)
173 self.cookbook.make_index()
174 with open(recipe.path, 'w') as f:
177 def _normalize_edit_params(self, name, **kwargs):
179 if name in self.cookbook.index:
180 recipe = self.cookbook.index[name]
181 name = recipe.name # get the canonical name for this recipe
183 # Use uuid4() to generate a random name if name is None
184 recipe = Recipe(name=(name or str(uuid.uuid4())))
186 # non-ingredient text updates
187 for attr in ['name','directions','yield_','author','source','url']:
188 n = self._recipe_attr_to_name(attr)
191 v = kwargs.get(n).strip()
194 elif attr == 'directions':
196 v = v.replace('\r\n', '\n').replace('\r', '\n')
197 for p in v.split('\n\n'):
202 if v != getattr(recipe, attr):
203 setattr(recipe, attr, v)
205 if recipe.ingredient_blocks == None:
206 recipe.ingredient_blocks = []
207 # protect against index-changing operations
208 ingredient_block_map = dict(
209 [(i,i) for i in range(len(recipe.ingredient_blocks))])
210 ingredient_map = dict(
211 [(i, dict([(j,j) for j in range(len(ib))]))
212 for i,ib in enumerate(recipe.ingredient_blocks)])
213 # ingredient text updates
214 for key,value in kwargs.items():
215 for k in ['ingredient_block', 'ingredient']:
216 regexp = getattr(self, '_action_%s_regexp' % k)
217 m = regexp.match(key)
219 handler = getattr(self, '_action_%s' % k)
220 kws = {'ingredient_block_map': ingredient_block_map,
221 'ingredient_map': ingredient_map}
222 action = handler(recipe, action, value, *m.groups(), **kws)
225 action = kwargs.get('action', action)
226 if action == 'submit':
227 action = 'edit and redirect'
228 for a in ['add_ingredient_block', 'remove_ingredient_block',
229 'add_ingredient', 'remove_ingredient']:
230 regexp = getattr(self, '_action_%s_regexp' % a)
231 m = regexp.match(action)
233 handler = getattr(self, '_action_%s' % a)
234 kws = {'ingredient_block_map': ingredient_block_map,
235 'ingredient_map': ingredient_map}
236 action = handler(recipe, action, *m.groups(), **kws)
238 return (name, recipe, action)
240 def _action_ingredient_block(self, recipe, action, value,
241 block_index, block_action,
242 ingredient_block_map, **kwargs):
243 block_index = ingredient_block_map[int(block_index)]
244 ib = recipe.ingredient_blocks[block_index]
247 if block_action == 'position' and value not in [None, '']:
249 if value != block_index:
250 ibs = recipe.ingredient_blocks
251 ibs.insert(value, ibs.pop(block_index))
252 for i in range(min(block_index, value),
253 max(block_index, value)+1):
254 ingredient_block_map[i] = i+1
255 ingredient_block_map[block_index] = value
257 elif block_action == 'name':
263 def _action_ingredient(self, recipe, action, value,
264 block_index, index, ingredient_action,
265 ingredient_block_map, ingredient_map, **kwargs):
266 block_index = ingredient_block_map[int(block_index)]
267 index = ingredient_map[block_index][int(index)]
268 ingredient = recipe.ingredient_blocks[block_index][index]
271 if ingredient_action == 'position' and value != None:
273 if value != block_index:
274 ibs = recipe.ingredient_blocks
275 ibs.insert(value, ibs.pop(block_index))
276 for i in range(min(block_index, value),
277 max(block_index, value)+1):
278 ingredient_block_map[i] = i+1
279 ingredient_block_map[block_index] = value
281 elif ingredient_action in ['name', 'note']:
282 if value != getattr(ingredient, ingredient_action):
283 setattr(ingredient, ingredient_action, value)
285 elif ingredient_action in ['value', 'units']:
286 if value != getattr(ingredient.amount, ingredient_action):
287 setattr(ingredient.amount, ingredient_action, value)
291 def _action_add_ingredient_block(self, recipe, action,
292 ingredient_block_map,
293 ingredient_map, **kwargs):
294 recipe.ingredient_blocks.append(IngredientBlock())
295 block_index = len(recipe.ingredient_blocks)-1
296 ingredient_block_map[block_index] = block_index
297 ingredient_map[block_index] = {}
300 def _action_remove_ingredient_block(self, recipe, action, block_index,
301 ingredient_block_map, **kwargs):
302 block_index = ingredient_block_map[int(block_index)]
303 recipe.ingredient_blocks.pop(block_index)
304 for k,v in ingredient_block_map.items():
306 ingredient_block_map[k] = v-1
309 def _action_add_ingredient(self, recipe, action, block_index,
310 ingredient_block_map,
311 ingredient_map, **kwargs):
312 block_index = ingredient_block_map[int(block_index)]
313 recipe.ingredient_blocks[block_index].append(Ingredient(
315 index = len(recipe.ingredient_blocks[block_index])-1
316 ingredient_map[block_index][index] = index
319 def _action_remove_ingredient(self, recipe, action, block_index, index,
320 ingredient_block_map,
321 ingredient_map, **kwargs):
322 block_index = ingredient_block_map[int(block_index)]
323 index = ingredient_map[block_index][int(index)]
324 recipe.ingredient_blocks[block_index].pop(index)
325 for k,v in ingredient_map[block_index].items():
327 ingredient_map[block_index][k] = v-1
330 def _recipe_form(self, recipe):
332 '<form action="" method="post">',
335 for attr in ['name', 'yield_', 'author', 'source', 'url']:
336 lines.extend(self._text_form(
337 self._recipe_attr_to_name(attr),
338 self._recipe_attr_to_user(attr),
339 getattr(recipe, attr)))
340 for i,ib in enumerate(recipe.ingredient_blocks):
341 lines.extend(self._ingredient_block_form(ib, i))
342 lines.extend(self._submit_form('add ingredient block'))
343 lines.extend(self._textarea_form(
344 self._recipe_attr_to_name('directions'),
345 self._recipe_attr_to_user('directions'),
346 '\n\n'.join(recipe.directions or Directions())))
347 lines.extend(self._submit_form('submit'))
354 def _ingredient_block_form(self, ingredient_block, index):
355 lines = self._submit_form(
356 'remove ingredient block %d' % index,
357 note='Ingredient block %d' % index)
358 for name,user,value in [
359 ('ingredient block %d position' % index,
360 'Position (%d)' % index,
362 ('ingredient block %d name' % index,
364 ingredient_block.name),
366 lines.extend(self._text_form(name, user, value))
367 for i,ingredient in enumerate(ingredient_block):
368 lines.extend(self._ingredient_form(ingredient, index, i))
369 lines.extend(self._submit_form('add ingredient %d' % index))
372 def _ingredient_form(self, ingredient, block_index, index):
373 lines = self._submit_form(
374 'remove ingredient %d %d' % (block_index, index),
375 note='Ingredient %d %d' % (block_index, index))
376 for name,user,value in [
377 ('ingredient %d %d position' % (block_index, index),
378 'Position (%d)' % index,
380 ('ingredient %d %d value' % (block_index, index),
382 ingredient.amount.value),
383 ('ingredient %d %d units' % (block_index, index),
385 ingredient.amount.units),
386 ('ingredient %d %d name' % (block_index, index),
389 ('ingredient %d %d note' % (block_index, index),
393 lines.extend(self._text_form(name, user, value))
394 # TODO: Amount: range_, alternatives
397 def _submit_form(self, value, note=''):
400 ' <td>%s</td>' % note,
401 ' <td><input type="submit" name="action" value="%s"/></td>'
406 def _text_form(self, name, user, value):
409 ' <td><label for="%s">%s</label></td>' % (name, user),
410 ' <td><input type="text" name="%s" size="60" value="%s"/></td>'
411 % (name, value or ''),
415 def _textarea_form(self, name, user, value):
418 ' <td><label for="%s">%s</label></td>' % (name, user),
419 ' <td><textarea name="%s" rows="20" cols="60"/>' % name,
425 def _recipe_attr_to_name(self, attr):
432 def _recipe_attr_to_user(self, attr):
435 return attr.capitalize()