/[drupal]/contributions/modules/recipe/recipe.module
ViewVC logotype

Contents of /contributions/modules/recipe/recipe.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.85 - (show annotations) (download) (as text)
Fri Jun 26 12:39:17 2009 UTC (5 months ago) by marble
Branch: MAIN
CVS Tags: HEAD
Changes since 1.84: +15 -1 lines
File MIME type: text/x-php
feature request #489930 by jbemmel, fixed by jbemmel. Hooked nodeapi to apply links to ingredient nodes in existing recipes if the nodes are created later.
1 <?php
2 // $Id: recipe.module,v 1.84 2009/06/26 12:18:11 marble Exp $
3
4 /**
5 * @file
6 * recipe.module - share recipes
7 * for drupal 5.x
8 *
9 * Updated to drupal 6.x by shadow
10 *
11 * @todo
12 */
13
14 /**
15 * Implementation of hook_perm().
16 */
17 function recipe_perm() {
18 return array(t('create recipes'), t('edit own recipes'));
19 }
20
21 /**
22 * Implementation of hook_load().
23 */
24 function recipe_load($node) {
25 $recipe = db_fetch_object(db_query('SELECT * FROM {recipe} WHERE nid = %d', $node->nid));
26 $recipe->ingredients = recipe_load_ingredients($node);
27 return $recipe;
28 }
29
30 /**
31 * Implementation of hook_link().
32 */
33 function recipe_link($type, $node = NULL, $teaser = FALSE) {
34 $links = array();
35
36 if ($type == 'node' && $node->type == 'recipe') {
37 if (!$teaser) {
38 if (variable_get('recipe_export_recipeml_enable', 1) == 1) {
39 $links['recipe_recipeml'] = array(
40 'title' => t('Export to RecipeML'),
41 'href' => "recipe/export/recipeml/$node->nid",
42 'attributes' => array('title' => t('Export this recipe to RecipeML.')),
43 );
44 }
45 if (variable_get('recipe_export_html_enable', 1) == 1) {
46 $links['recipe_html'] = array(
47 'title' => t('Printer-friendly version'),
48 'href' => "recipe/export/html/$node->nid",
49 'attributes' => array('title' => t('Show a printer-friendly version of this recipe.')),
50 );
51 }
52 }
53 }
54
55 return $links;
56 }
57
58 /**
59 * Implementation of hook_node_info(). This function replaces hook_node_name()
60 * and hook_node_types() from 4.6.
61 */
62 function recipe_node_info() {
63 return array(
64 'recipe' => array(
65 'name' => t('Recipe'),
66 'module' => 'recipe',
67 'description' => t("Share your favorite recipes with your fellow cooks."),
68 )
69 );
70 }
71
72 /**
73 * Implementation of hook_help().
74 */
75 function recipe_help($path, $arg) {
76 switch ($path) {
77 case 'node/add/recipe':
78 return variable_get("recipe_help", "");
79 }
80 }
81
82 /**
83 * Implementation of hook_insert().
84 *
85 * As a new node is being inserted into the database, we need to do our own
86 * database inserts.
87 */
88 function recipe_insert($node) {
89 db_query("INSERT INTO {recipe} (nid, source, yield, notes, instructions, preptime) VALUES (%d, '%s', '%s', '%s', '%s', '%d')", $node->nid, $node->source, $node->yield, $node->notes, $node->instructions, $node->preptime);
90 recipe_save_ingredients($node);
91 }
92
93 /**
94 * Implementation of hook_update().
95 *
96 * As an existing node is being updated in the database, we need to do our own
97 * database updates.
98 */
99 function recipe_update($node) {
100 db_query("UPDATE {recipe} SET source = '%s', yield = '%s', notes = '%s', instructions = '%s', preptime = '%d' WHERE nid = %d", $node->source, $node->yield, $node->notes, $node->instructions, $node->preptime, $node->nid);
101 recipe_save_ingredients($node);
102 }
103
104 /**
105 * Implementation of hook_delete().
106 *
107 * When a node is deleted, we need to clean up related tables.
108 */
109 function recipe_delete($node) {
110 db_query("DELETE FROM {recipe} WHERE nid = %d", $node->nid);
111 db_query("DELETE FROM {recipe_node_ingredient} WHERE nid = %d", $node->nid);
112 }
113
114 /**
115 * Implementation of hook_form().
116 */
117 function recipe_form(&$node,$form_state) {
118 // drupal 4.7 requires the title field to be defined by the custom node's module
119 $form['title'] = array('#type' => 'textfield', '#title' => t('Title'),
120 '#size' => 60, '#maxlength' => 128, '#required' => TRUE,
121 '#default_value' => $node->title);
122
123 // Now we define the form elements specific to our node type.
124 $form['body'] = array(
125 '#type' => 'textarea',
126 '#title' => t('Description'),
127 '#default_value' => $node->body,
128 '#cols' => 60,
129 '#rows' => 2,
130 '#description' => t('A short description or "teaser" for the recipe.'),
131 '#required' => TRUE,
132 );
133 $form['yield'] = array(
134 '#type' => 'textfield',
135 '#title' => t('Yield'),
136 '#default_value' => $node->yield,
137 '#size' => 10,
138 '#maxlength' => 10,
139 '#description' => t('The number of servings the recipe will make.'),
140 '#attributes' => NULL,
141 '#required' => TRUE,
142 );
143 $form['preptime'] = array(
144 '#type' => 'select',
145 '#title' => t("Preparation time"),
146 '#default_value' => $node->preptime,
147 '#options' => array(5 => t('5 minutes'), 10 => t('10 minutes'), 15 => t('15 minutes'), 20 => t('20 minutes'), 30 => t('30 minutes'), 45 => t('45 minutes'), 60 => t('1 hour'), 90 => t('1 1/2 hours'), 120 => t('2 hours'), 150 => t('2 1/2 hours'), 180 => t('3 hours'), 210 => t('3 1/2 hours'), 240 => t('4 hours'), 300 => t('5 hours'), 360 => t('6 hours')),
148 '#description' => t("How long does this recipe take to prepare (i.e. elapsed time)"),
149 );
150 $form["source"] = array(
151 '#type' => 'textfield',
152 '#title' => t("Source"),
153 '#default_value' => $node->source,
154 '#size' => 60,
155 '#maxlength' => 127,
156 '#description' => t("Optional. Does anyone else deserve credit for this recipe?"),
157 );
158
159 // Table of existing ingredients
160 $form['ingredients']['#tree'] = TRUE;
161 $system = variable_get('recipe_ingredient_system', 'complex');
162 if ($system == 'complex') {
163 $form['ingredients']['headings'] = array(
164 '#value' => '<div><table ><thead><tr><th>'. t('Quantity') .'</th><th>'.
165 t('Units') .'</th><th>'. t('Ingredient Name') .
166 '</th></tr></thead>'."\n".'<tbody>',
167 );
168 }
169 else {
170 $form['ingredients']['headings'] = array(
171 '#value' => '<div><table ><thead><tr><th>'. t('Ingredients') .'</th></tr></thead>'."\n".'<tbody>',
172 );
173 }
174 $rows = array();
175 $callback = 'recipe/ingredient/autocomplete';
176 $num_ingredients = 0;
177 if ($node->ingredients) {
178 foreach ($node->ingredients as $id => $ingredient) {
179 $num_ingredients = $id+1;
180 if ($id == 0) {
181 $j = '0';
182 } else {
183 $j = $id;
184 }
185
186 // For preview, node->ingredients is an array, for actual display, it's an object
187 $name = '';
188 $unit_id = '';
189 $abbreviation = '';
190 $quantity = 0;
191 if (is_array($ingredient)) {
192 $name = $ingredient['name'];
193 $unit_id = $ingredient['unit_id'];
194 $abbreviation = $ingredient['abbreviation'];
195 $quantity = $ingredient['quantity'];
196 } else {
197 $name = $ingredient->name;
198 $unit_id = $ingredient->unit_id;
199 $abbreviation = $ingredient->abbreviation;
200 $quantity = $ingredient->quantity;
201 }
202
203 if ($name && isset($quantity)) {
204 // When can the following statement be true?
205 if (!$ingredient) {
206 drupal_set_message(t('Recipe Module: An error has occured. Please report this error to the system administrator.'), 'error');
207 if (is_array($ingredient)) {
208 $ingredient['quantity'] = '';
209 $ingredient['unit_id'] = 21;
210 $ingredient['name'] = '';
211 } else {
212 $ingredient->quantity = '';
213 $ingredient->unit_id = 21;
214 $ingredient->name = '';
215 }
216 }
217 if ($system == 'complex') {
218 $form['ingredients'][$j]['open_tags'] = array(
219 '#value' => '<tr><th>',
220 );
221 $form['ingredients'][$j]['quantity'] = array(
222 '#type' => 'textfield',
223 '#title' => '',
224 '#default_value' => preg_replace('/\&frasl;/', '/', recipe_ingredient_quantity_from_decimal($quantity)),
225 '#size' => 8,
226 '#maxlength' => 8,
227 );
228 $form['ingredients'][$j]['mid1_tags'] = array(
229 '#value' => '</th><th>',
230 );
231 $form['ingredients'][$j]['unit_id'] = array(
232 '#type' => 'select',
233 '#title' => '',
234 '#default_value' => $unit_id,
235 '#options' => recipe_unit_options(),
236 );
237 $form['ingredients'][$j]['mid2_tags'] = array(
238 '#value' => '</th><th>',
239 );
240 $form['ingredients'][$j]['name'] = array(
241 '#type' => 'textfield',
242 '#title' => '',
243 '#default_value' => $name,
244 '#size' => 64,
245 '#maxlength' => 128,
246 '#autocomplete_path' => $callback,
247 );
248 $form['ingredients'][$j]['close_tags'] = array(
249 '#value' => '</th></tr>',
250 );
251 }
252 else {
253 if ($name) {
254 if ($quantity == 0) {
255 $quantity = '';
256 }
257 else {
258 $quantity .= ' ';
259 }
260 if ($abbreviation != '') {
261 $abbreviation .= ' ';
262 }
263 $name = $quantity . $abbreviation . $name;
264 }
265 $form['ingredients'][$j]['open_tags'] = array(
266 '#value' => '<tr><th>',
267 '#tree' => TRUE,
268 );
269 $form['ingredients'][$j]['name'] = array(
270 '#type' => 'textfield',
271 '#title' => '',
272 '#default_value' => $name,
273 '#size' => 64,
274 '#maxlength' => 128,
275 '#autocomplete_path' => $callback,
276 );
277 $form['ingredients'][$j]['close_tags'] = array(
278 '#value' => '</th></tr>',
279 );
280 } // else
281 } // if ($ingredient->name && isset($ingredient->quantity))
282 } // foreach ($node->ingredients as $id => $ingredient)
283 } // if ($node->ingredients)
284 // Add ten more spots for ingredients than are already used
285 for ($i = $num_ingredients; $i < $num_ingredients+10; $i++) {
286 if ($i == 0) {
287 $j = '0';
288 }
289 else {
290 $j = $i;
291 }
292 if ($system == 'complex') {
293 $form['ingredients'][$j]['open_tags'] = array(
294 '#value' => '<tr><th>',
295 );
296 $form['ingredients'][$j]['quantity'] = array(
297 '#type' => 'textfield',
298 '#title' => '',
299 '#size' => 8,
300 '#maxlength' => 8,
301 );
302 $form['ingredients'][$j]['mid1_tags'] = array(
303 '#value' => '</th><th>',
304 );
305 $form['ingredients'][$j]['unit_id'] = array(
306 '#type' => 'select',
307 '#title' => '',
308 '#options' => recipe_unit_options(),
309 '#default_value' => 2,
310 );
311 $form['ingredients'][$j]['mid2_tags'] = array(
312 '#value' => '</th><th>',
313 );
314 $form['ingredients'][$j]['name'] = array(
315 '#type' => 'textfield',
316 '#title' => '',
317 '#size' => 64,
318 '#maxlength' => 128,
319 '#autocomplete_path' => $callback,
320 );
321 $form['ingredients'][$j]['close_tags'] = array(
322 '#value' => '</th></tr>',
323 );
324 }
325 else {
326 $form['ingredients'][$j]['open_tags'] = array(
327 '#value' => '<tr><th>',
328 );
329 $form['ingredients'][$j]['name'] = array(
330 '#type' => 'textfield',
331 '#title' => '',
332 '#size' => 64,
333 '#maxlength' => 128,
334 '#autocomplete_path' => $callback,
335 );
336 $form['ingredients'][$j]['close_tags'] = array(
337 '#value' => '</th></tr>',
338 );
339 }
340 }
341 $form['ingredients']['end'] = array(
342 '#value' => '</tbody>'."\n".'</table></div>'."\n",
343 );
344
345 $form['instructions'] = array(
346 '#type' => 'textarea',
347 '#title' => t('Instructions'),
348 '#default_value' => $node->instructions,
349 '#cols' => 60,
350 '#rows' => 10,
351 '#description' => t('Step by step instructions on how to prepare and cook the recipe.'),
352 );
353 $form['notes'] = array(
354 '#type' => 'textarea',
355 '#title' => t("Additional Notes"),
356 '#default_value' => $node->notes,
357 '#cols' => 60,
358 '#rows' => 5,
359 '#description' => t("Optional. Describe a great dining experience relating to this recipe, or note which wine or other dishes complement this recipe"),
360 );
361 $form['filter'] = filter_form($node->format);
362
363 return $form;
364 }
365
366 /**
367 * Settings form for menu callback
368 */
369 function recipe_admin_settings() {
370 $form['recipe_index_depth'] = array(
371 '#type' => 'select',
372 '#title' => t('Index Depth'),
373 '#default_value' => variable_get('recipe_index_depth', 0),
374 '#options' => array(0 => t('All Terms'), 1 => "1", 2 => "2", 3 => "3", 4 => "4", 5 => "5", 6 => "6"),
375 '#description' => t("Defines how many levels of terms should be displayed on any given recipe index page. For example, if you select 1 then only one level of the Recipe index tree will be displayed at a time."),
376 );
377 $form['recipe_recent_box_enable'] = array(
378 '#type' => 'radios',
379 '#title' => t('Recent Recipes Box'),
380 '#default_value' => variable_get('recipe_recent_box_enable', 1),
381 '#options' => array(t('Disabled'), t('Enabled')),
382 '#description' => t('Enables or Disables the recent recipes box on the recipes index page.'),
383 '#required' => false,
384 );
385 $form['recipe_recent_box_title'] = array(
386 '#type' => 'textfield',
387 '#title' => t('Box Title'),
388 '#default_value' => variable_get('recipe_recent_box_title', t('Latest Recipes')),
389 '#size' => 35,
390 '#maxlength' => 255,
391 '#description' => t('Title of the Recent Recipes Box on the Recipes index page.'),
392 );
393 $form['recipe_recent_display'] = array(
394 '#type' => 'select',
395 '#title' => t('Recipes to Display'),
396 '#default_value' => variable_get('recipe_recent_display', '5'),
397 '#options' => array(5 => "5", 10 => "10", 15 => "15"),
398 '#description' => t("Sets the number of recent recipes that will be displayed in the Recent Recipes box. (0 = not displayed)."),
399 );
400 $form['recipe_help'] = array(
401 '#type' => 'textarea',
402 '#title' => t('Explanation or submission guidelines'),
403 '#default_value' => variable_get('recipe_help', ''),
404 '#cols' => 55,
405 '#rows' => 4,
406 '#description' => t('This text will be displayed at the top of the recipe submission form. Useful for helping or instructing your users.'),
407 );
408 $options = array('simple' => t('Simple'), 'complex' => t('Complex'));
409 $form['recipe_ingredient_system'] = array(
410 '#type' => 'radios',
411 '#title' => t('Ingredient entering system'),
412 '#default_value' => variable_get('recipe_ingredient_system', 'complex'),
413 '#options' => $options,
414 '#description' => t('The simple ingredient system allows all ingredients to be entered on one line. The complex system forces the user to seperate the quanity and units from the ingredient'),
415 );
416 $form['recipe_fraction_display'] = array(
417 '#type' => 'textfield',
418 '#title' => t('Fractions Display String'),
419 '#default_value' => variable_get('recipe_fraction_display', t('{%d }%d&frasl;%d')),
420 '#size' => 35,
421 '#maxlength' => 255,
422 '#description' => t('How fractions should be displayed. Leave blank to display as decimals. Each incidence of %d will be replaced by the whole number, the numerator, and the denominator in that order. Anything between curly braces will not be displayed when the whole number is equal to 0. Recommended settings are "{%d }%d&amp;frasl;%d" or "{%d }&lt;sup&gt;%d&lt;/sup&gt;/&lt;sub&gt;%d&lt;/sub&gt;"'),
423 );
424 $form['recipe_export_html_enable'] = array(
425 '#type' => 'radios',
426 '#title' => t('Export HTML'),
427 '#default_value' => variable_get('recipe_export_html_enable', 1),
428 '#options' => array(t('Disabled'), t('Enabled')),
429 '#description' => t('Enables or Disables the Export as HTML link.'),
430 '#required' => false,
431 );
432 $form['recipe_export_recipeml_enable'] = array(
433 '#type' => 'radios',
434 '#title' => t('Export RecipeML'),
435 '#default_value' => variable_get('recipe_export_recipeml_enable', 1),
436 '#options' => array(t('Disabled'), t('Enabled')),
437 '#description' => t('Enables or Disables the Export as RecipeML link.'),
438 '#required' => false,
439 );
440 return system_settings_form($form);
441 }
442
443 /**
444 * Implementation of hook_menu().
445 *
446 * Note: when editing this function you must visit 'admin/menu' to reset the cache
447 */
448 function recipe_menu() {
449 $items = array();
450 $items['recipe'] = array(
451 'title' => t('Recipes'),
452 'page callback' => 'recipe_page',
453 'access arguments' => array('access content'),
454 'type' => MENU_SUGGESTED_ITEM
455 );
456 $items['recipe/ingredient/autocomplete'] = array(
457 'title' => t('Ingredient autocomplete'),
458 'page callback' => 'recipe_autocomplete_page',
459 'type' => MENU_CALLBACK,
460 'access arguments' => array('access content')
461 );
462 $items['recipe/export'] = array(
463 'page callback' => 'recipe_export',
464 'type' => MENU_CALLBACK,
465 'access arguments' => array('access content')
466 );
467 $items['admin/settings/recipe'] = array(
468 'title' => t('Recipe module'),
469 'description' => t('Settings that control how the recipe module functions.'),
470 'page callback' => 'drupal_get_form',
471 'page arguments' => array('recipe_admin_settings'),
472 'access arguments' => array('administer site configuration'),
473 'type' => MENU_NORMAL_ITEM,
474 );
475 return $items;
476 }
477
478 /**
479 * Implementation of hook_access().
480 */
481 function recipe_access($op, $node, $account) {
482 global $user;
483
484 if ($op == 'create') {
485 // Only users with permission to do so may create this node type.
486 return user_access('create recipes');
487 }
488
489 // Users who create a node may edit or delete it later, assuming they have the
490 // necessary permissions.
491 if ($op == 'update' || $op == 'delete') {
492 if (user_access('edit own recipes') && ($user->uid == $node->uid)) {
493 return TRUE;
494 }
495 }
496 }
497
498 /**
499 * Implementation of hook_block().
500 */
501 function recipe_block($op = 'list', $delta = 0, $edit = array()) {
502 // The $op parameter determines what piece of information is being requested.
503 switch ($op) {
504 case 'list':
505 // If $op is "list", we just need to return a list of block descriptions.
506 // This is used to provide a list of possible blocks to the administrator,
507 // end users will not see these descriptions.
508 $blocks[0]['info'] = t('Newest recipes');
509 return $blocks;
510 case 'view':
511 // If $op is "view", then we need to generate the block for display
512 // purposes. The $delta parameter tells us which block is being requested.
513 switch ($delta) {
514 case 0:
515 // The subject is displayed at the top of the block. Note that it
516 // should be passed through t() for translation.
517 $block['subject'] = t('Newest Recipes');
518 // The content of the block is typically generated by calling a custom
519 // function.
520 $result = db_query_range(db_rewrite_sql("SELECT n.nid, n.title, n.uid, u.name FROM {node} n INNER JOIN {node_revisions} r ON n.vid = r.vid INNER JOIN {users} u ON n.uid = u.uid WHERE n.type='recipe' AND n.status =1 ORDER BY n.created DESC"), 0, 5);
521 $block["content"] = node_title_list($result);
522 break;
523 }
524 return $block;
525 }
526 }
527
528 /**
529 * Implementation of hook_view().
530 */
531 function recipe_view(&$node, $teaser = FALSE, $page = FALSE) {
532 if ($page) {
533 drupal_set_breadcrumb(array(l(t('Home'), ''), l(t('Recipes'), 'recipe')));
534 drupal_add_css(drupal_get_path('module', 'recipe') .'/recipe.css');
535 }
536 $node = recipe_node_prepare($node, $teaser);
537
538 $node->content['body'] = array(
539 '#value' => $teaser ? $node->teaser : theme('recipe_node', $node, $page),
540 '#weight' => 1,
541 );
542
543 return $node;
544 }
545
546 /**
547 * Returns a cached array of recipe unit types
548 */
549 function recipe_unit_options() {
550 static $options;
551 static $unit_rs;
552 if ( !isset( $unit_rs ) ) {
553 $unit_rs = db_query('SELECT id,type,name,abbreviation FROM {recipe_unit} ORDER BY type ASC, metric');
554 $options = array();
555 while ($r = db_fetch_object($unit_rs)) {
556 if (isset($r->type)) {
557 if (!isset($options[$r->type])) {
558 $options[$r->type] = array();
559 }
560 $options[$r->type][$r->id] = $r->name .' ('. $r->abbreviation .')';
561 }
562 else {
563 $options[$r->id] = $r->name .' ('. $r->abbreviation .')';
564 }
565 }
566 }
567 return $options;
568 }
569
570 /**
571 * Converts a recipe ingredient name to and ID
572 */
573 function recipe_ingredient_id_from_name($name) {
574 static $cache;
575
576 if (!$cache[$name]) {
577 $ingredient_id = db_result(db_query("SELECT id FROM {recipe_ingredient} WHERE LOWER(name)='%s'", trim(strtolower($name))));
578
579 if (!$ingredient_id) {
580 global $active_db;
581 $node_link = db_result(db_query("SELECT nid FROM {node} n WHERE title = '%s'", $name));
582
583 db_query("INSERT INTO {recipe_ingredient} (name, link) VALUES ('%s', '%s')", $name, $node_link);
584 $ingredient_id = db_result(db_query("SELECT id FROM {recipe_ingredient} WHERE LOWER(name)='%s'", trim(strtolower($name))));
585 }
586 $cache[$name] = $ingredient_id;
587 }
588
589 return $cache[$name];
590 }
591
592 /**
593 * Converts an ingredient's quantity from decimal to fraction
594 */
595 function recipe_ingredient_quantity_from_decimal($ingredient_quantity) {
596 if (strpos($ingredient_quantity, '.') && variable_get('recipe_fraction_display', t('{%d} %d&frasl;%d'))) {
597 $decimal = $ingredient_quantity;
598
599 if ($decimal == 0) {
600 $whole = 0;
601 $numerator = 0;
602 $denominator = 1;
603 $top_heavy = 0;
604 }
605 else {
606 $sign = 1;
607 if ($decimal < 0)
608 $sign = -1;
609 }
610
611 if (floor(abs($decimal)) == 0) {
612 $whole = 0;
613 $conversion = abs($decimal);
614 }
615 else {
616 $whole = floor(abs($decimal));
617 $conversion = abs($decimal);
618 }
619
620 $power = 1;
621 $flag = 0;
622 while ($flag == 0) {
623 $argument = $conversion * $power;
624 if ($argument == floor($argument)) {
625 $flag = 1;
626 }
627 else {
628 $power = $power * 10;
629 }
630 }
631
632 // workaround for thirds, sixths, ninths, twelfths
633 $overrides = array(
634 '3333' => array(1, 3), '6666' => array(2, 3), '9999' => array(3, 3), // thirds
635 '1666' => array(1, 6), '8333' => array(5, 6), // sixths
636 '1111' => array(1, 9), '2222' => array(2, 9), '4444' => array(4, 9), '5555' => array(5, 9), '7777' => array(7, 9), '8888' => array(8, 9), // ninths
637 '0833' => array(1, 12), '4166' => array(5, 12), '5833' => array(7, 12), '9166' => array(11, 12), // twelfths
638 );
639 $conversionstr = substr((string) ($conversion - floor($conversion)), 2, 4);
640 if (array_key_exists($conversionstr, $overrides)) {
641 if ($overrides[$conversionstr][0] == $overrides[$conversionstr][1]) {
642 return ($whole + 1) * $sign;
643 }
644 $denominator = $overrides[$conversionstr][1];
645 $numerator = (floor($conversion) * $denominator) + $overrides[$conversionstr][0];
646 }
647 else {
648 $numerator = $conversion * $power;
649 $denominator = $power;
650 }
651
652 $hcf = recipe_euclid($numerator, $denominator);
653
654 $numerator = $numerator/$hcf;
655 $denominator = $denominator/$hcf;
656 $whole = $sign * $whole;
657 $top_heavy = $sign * $numerator;
658
659 $numerator = abs($top_heavy) - (abs($whole) * $denominator);
660
661 if (($whole == 0) && ($sign == -1)) {
662 $numerator = $numerator * $sign;
663 }
664
665 $ingredient_quantity = sprintf(variable_get('recipe_fraction_display', t('{%d} %d&frasl;%d')), $whole, $numerator, $denominator);
666
667 if ( ($whole == 0) && (strpos($ingredient_quantity, '{') >= 0) ) {
668 /* remove anything in curly braces */
669 $ingredient_quantity = preg_replace('/{.*}/', '', $ingredient_quantity);
670 }
671 else {
672 /* remove just the curly braces, but keep everything between them */
673 $ingredient_quantity = preg_replace('/{|}/', '', $ingredient_quantity);
674 }
675 }
676
677 return filter_xss_admin($ingredient_quantity);
678 }
679
680 /**
681 * Converts an ingredient's quantity from fractions to decimal
682 */
683 function recipe_ingredient_quantity_from_fraction($ingredient_quantity) {
684 if ($pos_slash = strpos($ingredient_quantity, '/')) {
685 $pos_space = strpos($ingredient_quantity, ' ');
686 // can't trust $pos_space to be a zero value if there is no space
687 // so set it explicitly
688 if ($pos_space === false)
689 $pos_space = 0;
690
691 $whole = substr($ingredient_quantity, 0, $pos_space);
692 $numerator = substr($ingredient_quantity, $pos_space, $pos_slash);
693 $denominator = substr($ingredient_quantity, $pos_slash+1);
694 $ingredient_quantity = $whole+($numerator/$denominator);
695 }
696
697 return $ingredient_quantity;
698 }
699
700 /**
701 * Saves the changed ingredients of a recipe node to the database
702 * (by comparing the old and new ingredients first)
703 */
704 function recipe_save_ingredients($node) {
705 if (!$node->ingredients) {
706 $node->ingredients = array();
707 }
708 $changes = recipe_ingredients_diff($node->ingredients, recipe_load_ingredients($node));
709
710 if (count($changes->remove) > 0) {
711 $ids = implode(',', $changes->remove);
712 db_query("DELETE FROM {recipe_node_ingredient} WHERE id IN (%s)", $ids);
713 }
714
715 foreach ($changes->add as $ingredient) {
716 $ingredient->id = recipe_ingredient_id_from_name($ingredient->name);
717 $ingredient->quantity = recipe_ingredient_quantity_from_fraction($ingredient->quantity);
718 db_query("INSERT INTO {recipe_node_ingredient} (nid,ingredient_id,quantity,unit_id) VALUES (%d,%d,%f,%d)", $node->nid, $ingredient->id, $ingredient->quantity, $ingredient->unit_id);
719 }
720
721 foreach ($changes->update as $ingredient) {
722 $ingredient->id = recipe_ingredient_id_from_name($ingredient->name);
723 $ingredient->quantity = recipe_ingredient_quantity_from_fraction($ingredient->quantity);
724 db_query("UPDATE {recipe_node_ingredient} SET quantity='%f', unit_id='%d' WHERE nid='%d' AND ingredient_id='%d'", $ingredient->quantity, $ingredient->unit_id, $node->nid, $ingredient->id);
725 }
726 }
727
728 /**
729 * Compares two arrays of ingredients and returns the differences
730 */
731 function recipe_ingredients_diff($a1, $a2) {
732 $return->add = array();
733 $return->remove = array();
734 $return->update = array();
735
736 foreach ($a1 as $pl) {
737 $pl = (object)$pl;
738 $pl->name = trim($pl->name);
739 if ($pl->name) {
740 if (!_in_array($pl, $return->add)) {
741 // Duplicate entries for the same ingredient are ignored.
742 if (!_in_array($pl, $a2))
743 $return->add[] = $pl;
744 else
745 if (!_in_array($pl, $return->update))
746 $return->update[] = $pl;
747 }
748 }
749 }
750 foreach ($a2 as $k => $pl) {
751 if (!_in_array($pl, $a1)) {
752 $return->remove[] = $pl->id;
753 }
754 }
755 return $return;
756 }
757
758 /**
759 * Custom in_array() function because PHP 4 in_aray() doesnt seem to
760 * handle the first arguement being an object
761 */
762 function _in_array($a, $b) {
763 $a->name = trim(strtolower($a->name));
764 foreach ($b as $row) {
765 $compareto="";
766 if (is_array($row)) {
767 $compareto = trim(strtolower($row["name"]));
768 }
769 else {
770 $compareto = trim(strtolower($row->name));
771 }
772 if ($a->name === $compareto)
773 return true;
774 }
775 return false;
776 }
777
778 /**
779 * Loads the ingredients for a recipe
780 */
781 function recipe_load_ingredients($node) {
782 $rs = db_query('
783 SELECT
784 ri.id,
785 i.name,
786 i.link,
787 ri.quantity,
788 ri.unit_id,
789 u.abbreviation,
790 ri.ingredient_id
791 FROM
792 {recipe_node_ingredient} ri,
793 {recipe_ingredient} i,
794 {recipe_unit} u
795 WHERE
796 ri.ingredient_id = i.id
797 AND ri.unit_id = u.id
798 AND ri.nid=%d
799 ORDER BY
800 ri.id', $node->nid);
801 $ingredients = array();
802 while ($ingredient = db_fetch_object($rs)) {
803 $ingredients[] = $ingredient;
804 }
805 return $ingredients;
806 }
807
808 /**
809 * Converts a recipe unit ID to it's abbreviation
810 */
811 function recipe_unit_abbreviation($unit_id) {
812 static $abbreviations;
813
814 if (!$abbreviations) {
815 $rs = db_query('SELECT id,abbreviation FROM {recipe_unit}');
816 while ($unit = db_fetch_object($rs)) {
817 $abbreviations[$unit->id] = $unit->abbreviation;
818 }
819 }
820
821 return $abbreviations[$unit_id];
822 }
823
824 /**
825 * Converts a recipe unit ID to it's name */
826 function recipe_unit_name($unit_id) {
827 static $unit_names;
828
829 if (!$unit_names) {
830 $rs = db_query('SELECT id,name FROM {recipe_unit}');
831 while ($unit = db_fetch_object($rs)) {
832 $unit_names[$unit->id] = $unit->name;
833 }
834 }
835
836 return $unit_names[$unit_id];
837 }
838
839 /**
840 * Menu callback; Generates various representation of a recipe page with
841 * all descendants and prints the requested representation to output.
842 *
843 * The function delegates the generation of output to helper functions.
844 * The function name is derived by prepending 'recipe_export_' to the
845 * given output type. So, e.g., a type of 'html' results in a call to
846 * the function recipe_export_html().
847 *
848 * @param type
849 * - a string encoding the type of output requested.
850 * The following types are currently supported in recipe module
851 * html: HTML (printer friendly output)
852 * recipeml: XML (RecipeML formatted output)
853 * Other types can be supported with contributed modules.
854 * @param nid
855 * - an integer representing the node id (nid) of the node to export
856 *
857 */
858 function recipe_export($type = 'html', $nid = 0) {
859 $type = drupal_strtolower($type);
860 $export_function = 'recipe_export_'. $type;
861
862 if (function_exists($export_function)) {
863 echo call_user_func($export_function, $nid);
864 }
865 else {
866 drupal_set_message(t('Unknown export format.'));
867 drupal_not_found();
868 }
869 }
870
871 /**
872 * This function is called by recipe_export() to generate HTML for export.
873 *
874 * @param nid
875 * - an integer representing the node id (nid) of the node to export
876 * @return
877 * - string containing HTML representing the recipe
878 */
879 function recipe_export_html($nid) {
880 if ($nid == 0) {
881 drupal_goto('recipe');
882 }
883 $node = node_load(array('nid' => $nid, 'type' => 'recipe'));
884
885 $node = recipe_node_prepare($node, FALSE);
886 $output = theme('recipe_node', $node, FALSE);
887
888 $html = theme('recipe_export_html', check_plain($node->title), $output);
889 return $html;
890 }
891
892 /**
893 * This function is called by recipe_export() to generate RecipeML for export.
894 *
895 * @param nid
896 * - an integer representing the node id (nid) of the node to export
897 * @return
898 * - string containing the recipe in RecipeML
899 */
900 function recipe_export_recipeml($nid) {
901 if ($nid == 0) {
902 drupal_goto('recipe');
903 }
904
905 $node = node_load(array('nid' => $nid, 'type' => 'recipe'));
906
907 drupal_set_header('Content-type: text/xml');
908
909 $output = '<?xml version="1.0" encoding="UTF-8"?>'."\n".
910 '<!DOCTYPE recipeml PUBLIC "-//FormatData//DTD RecipeML 0.5//EN" "http://www.formatdata.com/recipeml/recipeml.dtd">'."\n".
911 '<recipeml version="0.5">'."\n".
912 ' <recipe>'."\n".
913 ' <head>'."\n".
914 ' <title>'. $node->title .'</title>'."\n".
915 ' </head>'."\n".
916 ' <yield><qty>'. $node->yield .'</qty></yield>'."\n".
917 ' <ingredients>';
918
919 foreach ($node->ingredients as $ingredient) {
920 $output .= "\n".'<ing><amt><qty>'. $ingredient->quantity .'</qty><unit>'. $ingredient->abbreviation .'</unit></amt><item>'. $ingredient->name .'</item></ing>';
921 }
922
923 $output .= "\n".
924 ' </ingredients>'."\n".
925 ' <directions>'. $node->instructions .'</directions>'."\n".
926 ' </recipe>'."\n".
927 '</recipeml>';
928
929 return $output;
930 }
931
932 /**
933 * Callback function for ingredient autocomplete
934 */
935 function recipe_autocomplete_page($string = "", $limit = 10) {
936 $matches = array();
937 $rs = db_query("SELECT name FROM {recipe_ingredient} WHERE LOWER(name) LIKE '%s%%' ORDER BY name LIMIT %d", strtolower($string), $limit);
938 while ($r = db_fetch_object($rs)) {
939 $matches[$r->name] = check_plain($r->name);
940 }
941 print drupal_to_js($matches);
942 exit();
943 }
944
945 /**
946 * Implementation of hook_validate().
947 *
948 * Errors should be signaled with form_set_error().
949 */
950 function recipe_validate(&$node) {
951 if (!$node->ingredients) return;
952 $ingredients = array();
953 foreach ($node->ingredients as $key => $ingredient) {
954 $ingredient = (object)$ingredient;
955 if (!isset($ingredient->quantity)) {
956 $ingredient = recipe_parse_ingredient_string($ingredient->name);
957 }
958 if ($ingredient->name && _in_array($ingredient, $ingredients)) {
959 form_set_error("recipe", t('Duplicate ingredients are not allowed.'));
960 }
961 else {
962 $ingredients[] = $ingredient;
963 }
964 $node->ingredients[$key] = $ingredient;
965 }
966 }
967
968 /**
969 * Converts an ingredients name string to an ingredient object
970 */
971 function recipe_parse_ingredient_string($ingredient_string) {
972 if (preg_match('#([0-9.]+(?:\s?\d*/\d*)?\s?)?(?:([a-zA-Z.]*)\s)?(.*)#', trim($ingredient_string), $matches)) {
973 $ingredient->name = $matches[3];
974 $ingredient->quantity = trim($matches[1]);
975 if ($ingredient->quantity == 0) {
976 $ingredient->quantity = 0;
977 }
978 $t_unit = $matches[2];
979 $unit = recipe_unit_from_name($t_unit);
980
981 if ($unit) {
982 $ingredient->unit_id = $unit->id;
983 $ingredient->abbreviation = $unit->abbreviation;
984 }
985 else {
986 $ingredient->unit_id = 29;
987 $ingredient->abbreviation = '';
988 $ingredient->name = $t_unit .' '. $ingredient->name;
989 }
990
991 $ingredient->name = trim($ingredient->name);
992
993 return $ingredient;
994 }
995 else {
996 return false;
997 }
998 }
999
1000 /**
1001 * Returns information about a unit based on a unit abbreviation or name
1002 */
1003 function recipe_unit_from_name($name) {
1004 if (strlen($name) > 1)
1005 $string = strtolower($name);
1006 else
1007 $string = $name;
1008 $ending = substr($string, -1, 1);
1009 if ($ending == 's' && $string != 'ds' || $ending == '.') {
1010 $string = substr($string, 0, strlen($string) -1);
1011 }
1012 $ending = substr($string, -1, 1);
1013 if ($ending == 's' && $string != 'ds'|| $ending == '.') {
1014 $string = substr($string, 0, strlen($string) -1);
1015 }
1016
1017 static $units_array;
1018
1019 if (!$units_array) {
1020 $rs = db_query('SELECT id,name,abbreviation FROM {recipe_unit}');
1021 while ($unit = db_fetch_object($rs)) {
1022 $units_array[strtolower($unit->name)] = $unit;
1023 $units_array[$unit->abbreviation] = $unit;
1024 }
1025 }
1026
1027 return $units_array[$string];
1028 }
1029
1030 /**
1031 * Menu Callback - created output for the main recipe page.
1032 *
1033 * @return $body
1034 */
1035 function recipe_page() {
1036 $body = "";
1037
1038 if (arg(1) == "feed") {
1039 module_invoke('node', 'feed', module_invoke('taxonomy', 'select_nodes', recipe_get_recipe_terms(), 'or', 0, FALSE));
1040 }
1041 else {
1042 if (arg(1) != NULL) {
1043 $breadcrumb = drupal_get_breadcrumb();
1044 $term = recipe_build_breadcrumbs($breadcrumb);
1045 drupal_set_breadcrumb($breadcrumb);
1046
1047 if ($term != NULL) {
1048 $content = recipe_index($term->tid);
1049 if ($content != '') {
1050 $body = theme('box', $term->name .'- '. t('Sub Categories'), $content);
1051 }
1052
1053 $terms = array_merge(array($term->tid), array_map('_recipe_get_tid_from_term', module_invoke('taxonomy', 'get_children', $term->tid)));
1054 $body .= module_invoke('taxonomy', 'render_nodes', module_invoke('taxonomy', 'select_nodes', $terms));
1055 }
1056 }
1057 else {
1058 $body = '';
1059
1060 if (variable_get('recipe_recent_box_enable', 1)) {
1061 $body = theme('box', variable_get('recipe_recent_box_title', t('Latest Recipes')), module_invoke('node', 'title_list', recipe_get_latest(variable_get('recipe_recent_display', '5')), '') . theme('recipe_more_info', theme('feed_icon', url("recipe/feed"), t('Syndicate'))));
1062 }
1063 $content = recipe_index();
1064 if ($content != '') {
1065 $body .= theme('box', t('Recipe Categories'), $content);
1066 }
1067 }
1068 }
1069 return $body;
1070 }
1071
1072 /**
1073 * Builds a breadcrumb list.
1074 *
1075 * @param breadcrumb a reference to the breadcrumb array. New items will be appending to this array.
1076 *
1077 * @return returns a term object if the last item in the url is a term, otherwise returns NULL.
1078 */
1079 function recipe_build_breadcrumbs(&$breadcrumb) {
1080 if (arg(1) != NULL) {
1081 $i = 1;
1082 $url = 'recipe';
1083 $breadcrumb[] = l(ucwords(t('Recipes')), $url);
1084 while (arg($i) != NULL) {
1085 $last_term = urldecode(arg($i));
1086 $url = $url .'/'. urlencode($last_term);
1087 $breadcrumb[] = l(ucwords($last_term), $url);
1088 $i++;
1089 }
1090
1091 $term = current(module_invoke('taxonomy', 'get_term_by_name', $last_term));
1092 return $term;
1093 }
1094 return NULL;
1095 }
1096
1097 /**
1098 * Recursively traverses the term tree to construct the index.
1099 *
1100 * @return string the output for this tree.
1101 */
1102 function recipe_build_index(&$tree, $parent_url) {
1103 $output = '';
1104
1105 if ($tree == array()) {
1106 return '';
1107 }
1108
1109 do {
1110 $cur =