Development

/branches/1.0/lib/generator/sfAdminGenerator.class.php

You must first sign up to be able to contribute.

root/branches/1.0/lib/generator/sfAdminGenerator.class.php

Revision 9861, 22.0 kB (checked in by fabien, 6 years ago)

fixed getColumnFilterTag() component type (closes #2861)

  • Property svn:mime-type set to text/x-php
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1 <?php
2
3 /*
4  * This file is part of the symfony package.
5  * (c) 2004-2006 Fabien Potencier <fabien.potencier@symfony-project.com>
6  *
7  * For the full copyright and license information, please view the LICENSE
8  * file that was distributed with this source code.
9  */
10
11 /**
12  * Admin generator.
13  *
14  * This class generates an admin module.
15  *
16  * This class calls two ORM specific methods:
17  *   getAllColumns()
18  * and
19  *   getAdminColumnForField($field, $flag = null)
20  *
21  * @package    symfony
22  * @subpackage generator
23  * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
24  * @version    SVN: $Id$
25  */
26 abstract class sfAdminGenerator extends sfCrudGenerator
27 {
28   protected
29     $fields = array();
30
31   /**
32    * Returns HTML code for a help icon.
33    *
34    * @param string The column name
35    * @param string The field type (list, edit)
36    *
37    * @return string HTML code
38    */
39   public function getHelpAsIcon($column, $type = '')
40   {
41     $help = $this->getParameterValue($type.'.fields.'.$column->getName().'.help');
42     if ($help)
43     {
44       return "[?php echo image_tag(sfConfig::get('sf_admin_web_dir').'/images/help.png', array('align' => 'absmiddle', 'alt' => __('".$this->escapeString($help)."'), 'title' => __('".$this->escapeString($help)."'))) ?]";
45     }
46
47     return '';
48   }
49
50   /**
51    * Returns HTML code for a help text.
52    *
53    * @param string The column name
54    * @param string The field type (list, edit)
55    *
56    * @return string HTML code
57    */
58   public function getHelp($column, $type = '')
59   {
60     $help = $this->getParameterValue($type.'.fields.'.$column->getName().'.help');
61     if ($help)
62     {
63       return "<div class=\"sf_admin_edit_help\">[?php echo __('".$this->escapeString($help)."') ?]</div>";
64     }
65
66     return '';
67   }
68
69   /**
70    * Returns HTML code for an action button.
71    *
72    * @param string  The action name
73    * @param array   The parameters
74    * @param boolean Whether to add a primary key link or not
75    *
76    * @return string HTML code
77    */
78   public function getButtonToAction($actionName, $params, $pk_link = false)
79   {
80     $params   = (array) $params;
81     $options  = isset($params['params']) ? sfToolkit::stringToArray($params['params']) : array();
82     $method   = 'button_to';
83     $li_class = '';
84     $only_for = isset($params['only_for']) ? $params['only_for'] : null;
85
86     // default values
87     if ($actionName[0] == '_')
88     {
89       $actionName     = substr($actionName, 1);
90       $default_name   = strtr($actionName, '_', ' ');
91       $default_icon   = sfConfig::get('sf_admin_web_dir').'/images/'.$actionName.'_icon.png';
92       $default_action = $actionName;
93       $default_class  = 'sf_admin_action_'.$actionName;
94
95       if ($actionName == 'save' || $actionName == 'save_and_add' || $actionName == 'save_and_list')
96       {
97         $method = 'submit_tag';
98         $options['name'] = $actionName;
99       }
100
101       if ($actionName == 'delete')
102       {
103         $options['post'] = true;
104         if (!isset($options['confirm']))
105         {
106           $options['confirm'] = 'Are you sure?';
107         }
108
109         $li_class = 'float-left';
110
111         $only_for = 'edit';
112       }
113     }
114     else
115     {
116       $default_name   = strtr($actionName, '_', ' ');
117       $default_icon   = sfConfig::get('sf_admin_web_dir').'/images/default_icon.png';
118       $default_action = 'List'.sfInflector::camelize($actionName);
119       $default_class  = '';
120     }
121
122     $name   = isset($params['name']) ? $params['name'] : $default_name;
123     $icon   = isset($params['icon']) ? sfToolkit::replaceConstants($params['icon']) : $default_icon;
124     $action = isset($params['action']) ? $params['action'] : $default_action;
125     $url_params = $pk_link ? '?'.$this->getPrimaryKeyUrlParams() : '\'';
126
127     if (!isset($options['class']))
128     {
129       if ($default_class)
130       {
131         $options['class'] = $default_class;
132       }
133       else
134       {
135         $options['style'] = 'background: #ffc url('.$icon.') no-repeat 3px 2px';
136       }
137     }
138
139     $li_class = $li_class ? ' class="'.$li_class.'"' : '';
140
141     $html = '<li'.$li_class.'>';
142
143     if ($only_for == 'edit')
144     {
145       $html .= '[?php if ('.$this->getPrimaryKeyIsSet().'): ?]'."\n";
146     }
147     else if ($only_for == 'create')
148     {
149       $html .= '[?php if (!'.$this->getPrimaryKeyIsSet().'): ?]'."\n";
150     }
151     else if ($only_for !== null)
152     {
153       throw new sfConfigurationException(sprintf('The "only_for" parameter can only takes "create" or "edit" as argument ("%s")', $only_for));
154     }
155
156     if ($method == 'submit_tag')
157     {
158       $html .= '[?php echo submit_tag(__(\''.$name.'\'), '.var_export($options, true).') ?]';
159     }
160     else
161     {
162       $phpOptions = var_export($options, true);
163
164       // little hack
165       $phpOptions = preg_replace("/'confirm' => '(.+?)(?<!\\\)'/", '\'confirm\' => __(\'$1\')', $phpOptions);
166
167       $html .= '[?php echo button_to(__(\''.$name.'\'), \''.$this->getModuleName().'/'.$action.$url_params.', '.$phpOptions.') ?]';
168     }
169
170     if ($only_for !== null)
171     {
172       $html .= '[?php endif; ?]'."\n";
173     }
174
175     $html .= '</li>'."\n";
176
177     return $html;
178   }
179
180   /**
181    * Returns HTML code for an action link.
182    *
183    * @param string  The action name
184    * @param array   The parameters
185    * @param boolean Whether to add a primary key link or not
186    *
187    * @return string HTML code
188    */
189   public function getLinkToAction($actionName, $params, $pk_link = false)
190   {
191     $options = isset($params['params']) ? sfToolkit::stringToArray($params['params']) : array();
192
193     // default values
194     if ($actionName[0] == '_')
195     {
196       $actionName = substr($actionName, 1);
197       $name       = $actionName;
198       $icon       = sfConfig::get('sf_admin_web_dir').'/images/'.$actionName.'_icon.png';
199       $action     = $actionName;
200
201       if ($actionName == 'delete')
202       {
203         $options['post'] = true;
204         if (!isset($options['confirm']))
205         {
206           $options['confirm'] = 'Are you sure?';
207         }
208       }
209     }
210     else
211     {
212       $name   = isset($params['name']) ? $params['name'] : $actionName;
213       $icon   = isset($params['icon']) ? sfToolkit::replaceConstants($params['icon']) : sfConfig::get('sf_admin_web_dir').'/images/default_icon.png';
214       $action = isset($params['action']) ? $params['action'] : 'List'.sfInflector::camelize($actionName);
215     }
216
217     $url_params = $pk_link ? '?'.$this->getPrimaryKeyUrlParams() : '\'';
218
219     $phpOptions = var_export($options, true);
220
221     // little hack
222     $phpOptions = preg_replace("/'confirm' => '(.+?)(?<!\\\)'/", '\'confirm\' => __(\'$1\')', $phpOptions);
223
224     return '<li>[?php echo link_to(image_tag(\''.$icon.'\', array(\'alt\' => __(\''.$name.'\'), \'title\' => __(\''.$name.'\'))), \''.$this->getModuleName().'/'.$action.$url_params.($options ? ', '.$phpOptions : '').') ?]</li>'."\n";
225   }
226
227   /**
228    * Returns HTML code for a column in edit mode.
229    *
230    * @param string  The column name
231    * @param array   The parameters
232    *
233    * @return string HTML code
234    */
235   public function getColumnEditTag($column, $params = array())
236   {
237     // user defined parameters
238     $user_params = $this->getParameterValue('edit.fields.'.$column->getName().'.params');
239     $user_params = is_array($user_params) ? $user_params : sfToolkit::stringToArray($user_params);
240     $params      = $user_params ? array_merge($params, $user_params) : $params;
241
242     if ($column->isComponent())
243     {
244       return "get_component('".$this->getModuleName()."', '".$column->getName()."', array('type' => 'edit', '{$this->getSingularName()}' => \${$this->getSingularName()}))";
245     }
246     else if ($column->isPartial())
247     {
248       return "get_partial('".$column->getName()."', array('type' => 'edit', '{$this->getSingularName()}' => \${$this->getSingularName()}))";
249     }
250
251     // default control name
252     $params = array_merge(array('control_name' => $this->getSingularName().'['.$column->getName().']'), $params);
253
254     // default parameter values
255     $type = $column->getCreoleType();
256     if ($type == CreoleTypes::DATE)
257     {
258       $params = array_merge(array('rich' => true, 'calendar_button_img' => sfConfig::get('sf_admin_web_dir').'/images/date.png'), $params);
259     }
260     else if ($type == CreoleTypes::TIMESTAMP)
261     {
262       $params = array_merge(array('rich' => true, 'withtime' => true, 'calendar_button_img' => sfConfig::get('sf_admin_web_dir').'/images/date.png'), $params);
263     }
264
265     // user sets a specific tag to use
266     if ($inputType = $this->getParameterValue('edit.fields.'.$column->getName().'.type'))
267     {
268       if ($inputType == 'plain')
269       {
270         return $this->getColumnListTag($column, $params);
271       }
272       else
273       {
274         return $this->getPHPObjectHelper($inputType, $column, $params);
275       }
276     }
277
278     // guess the best tag to use with column type
279     return parent::getCrudColumnEditTag($column, $params);
280   }
281
282   /**
283    * Returns all column categories.
284    *
285    * @param string  The parameter name
286    *
287    * @return array The column categories
288    */
289   public function getColumnCategories($paramName)
290   {
291     if (is_array($this->getParameterValue($paramName)))
292     {
293       $fields = $this->getParameterValue($paramName);
294
295       // do we have categories?
296       if (!isset($fields[0]))
297       {
298         return array_keys($fields);
299       }
300
301     }
302
303     return array('NONE');
304   }
305
306   /**
307    * Wraps content with a credential condition.
308    *
309    * @param string  The content
310    * @param array   The parameters
311    *
312    * @return string HTML code
313    */
314   public function addCredentialCondition($content, $params = array())
315   {
316     if (isset($params['credentials']))
317     {
318       $credentials = str_replace("\n", ' ', var_export($params['credentials'], true));
319
320       return <<<EOF
321 [?php if (\$sf_user->hasCredential($credentials)): ?]
322 $content
323 [?php endif; ?]
324 EOF;
325     }
326     else
327     {
328       return $content;
329     }
330   }
331
332   /**
333    * Gets sfAdminColumn objects for a given category.
334    *
335    * @param string The parameter name
336    *
337    * @return array sfAdminColumn array
338    */
339   public function getColumns($paramName, $category = 'NONE')
340   {
341     $phpNames = array();
342
343     // user has set a personnalized list of fields?
344     $fields = $this->getParameterValue($paramName);
345     if (is_array($fields))
346     {
347       // categories?
348       if (isset($fields[0]))
349       {
350         // simulate a default one
351         $fields = array('NONE' => $fields);
352       }
353
354       if (!$fields)
355       {
356         return array();
357       }
358
359       foreach ($fields[$category] as $field)
360       {
361         list($field, $flags) = $this->splitFlag($field);
362
363         $phpNames[] = $this->getAdminColumnForField($field, $flags);
364       }
365     }
366     else
367     {
368       // no, just return the full list of columns in table
369       return $this->getAllColumns();
370     }
371
372     return $phpNames;
373   }
374
375   /**
376    * Gets modifier flags from a column name.
377    *
378    * @param string The column name
379    *
380    * @return array An array of detected flags
381    */
382   public function splitFlag($text)
383   {
384     $flags = array();
385     while (in_array($text[0], array('=', '-', '+', '_', '~')))
386     {
387       $flags[] = $text[0];
388       $text = substr($text, 1);
389     }
390
391     return array($text, $flags);
392   }
393
394   /**
395    * Gets a parameter value.
396    *
397    * @param string The key name
398    * @param mixed  The default value
399    *
400    * @return mixed The parameter value
401    */
402   public function getParameterValue($key, $default = null)
403   {
404     if (preg_match('/^([^\.]+)\.fields\.(.+)$/', $key, $matches))
405     {
406       return $this->getFieldParameterValue($matches[2], $matches[1], $default);
407     }
408     else
409     {
410       return $this->getValueFromKey($key, $default);
411     }
412   }
413
414   /**
415    * Gets a field parameter value.
416    *
417    * @param string The key name
418    * @param string The type (list, edit)
419    * @param mixed  The default value
420    *
421    * @return mixed The parameter value
422    */
423   protected function getFieldParameterValue($key, $type = '', $default = null)
424   {
425     $retval = $this->getValueFromKey($type.'.fields.'.$key, $default);
426     if ($retval !== null)
427     {
428       return $retval;
429     }
430
431     $retval = $this->getValueFromKey('fields.'.$key, $default);
432     if ($retval !== null)
433     {
434       return $retval;
435     }
436
437     if (preg_match('/\.name$/', $key))
438     {
439       // default field.name
440       return sfInflector::humanize(($pos = strpos($key, '.')) ? substr($key, 0, $pos) : $key);
441     }
442     else
443     {
444       return null;
445     }
446   }
447
448   /**
449    * Gets the value for a given key.
450    *
451    * @param string The key name
452    * @param mixed  The default value
453    *
454    * @return mixed The key value
455    */
456   protected function getValueFromKey($key, $default = null)
457   {
458     $ref   =& $this->params;
459     $parts explode('.', $key);
460     $count count($parts);
461     for ($i = 0; $i < $count; $i++)
462     {
463       $partKey = $parts[$i];
464       if (!isset($ref[$partKey]))
465       {
466         return $default;
467       }
468
469       if ($count == $i + 1)
470       {
471         return $ref[$partKey];
472       }
473       else
474       {
475         $ref =& $ref[$partKey];
476       }
477     }
478
479     return $default;
480   }
481
482   /**
483    * Wraps a content for I18N.
484    *
485    * @param string The key name
486    * @param string The defaul value
487    *
488    * @return string HTML code
489    */
490   public function getI18NString($key, $default = null, $withEcho = true)
491   {
492     $value = $this->escapeString($this->getParameterValue($key, $default));
493
494     // find %%xx%% strings
495     preg_match_all('/%%([^%]+)%%/', $value, $matches, PREG_PATTERN_ORDER);
496     $this->params['tmp']['display'] = array();
497     foreach ($matches[1] as $name)
498     {
499       $this->params['tmp']['display'][] = $name;
500     }
501
502     $vars = array();
503     foreach ($this->getColumns('tmp.display') as $column)
504     {
505       if ($column->isLink())
506       {
507         $vars[] = '\'%%'.$column->getName().'%%\' => link_to('.$this->getColumnListTag($column).', \''.$this->getModuleName().'/edit?'.$this->getPrimaryKeyUrlParams().')';
508       }
509       elseif ($column->isPartial())
510       {
511         $vars[] = '\'%%_'.$column->getName().'%%\' => '.$this->getColumnListTag($column);
512       }
513       else if ($column->isComponent())
514       {
515         $vars[] = '\'%%~'.$column->getName().'%%\' => '.$this->getColumnListTag($column);
516       }
517       else
518       {
519         $vars[] = '\'%%'.$column->getName().'%%\' => '.$this->getColumnListTag($column);
520       }
521     }
522
523     // strip all = signs
524     $value = preg_replace('/%%=([^%]+)%%/', '%%$1%%', $value);
525
526     $i18n = '__(\''.$value.'\', '."\n".'array('.implode(",\n", $vars).'))';
527
528     return $withEcho ? '[?php echo '.$i18n.' ?]' : $i18n;
529   }
530
531   /**
532    * Replaces constants in a string.
533    *
534    * @param string
535    *
536    * @return string
537    */
538   public function replaceConstants($value)
539   {
540     // find %%xx%% strings
541     preg_match_all('/%%([^%]+)%%/', $value, $matches, PREG_PATTERN_ORDER);
542     $this->params['tmp']['display'] = array();
543     foreach ($matches[1] as $name)
544     {
545       $this->params['tmp']['display'][] = $name;
546     }
547
548     foreach ($this->getColumns('tmp.display') as $column)
549     {
550       $value = str_replace('%%'.$column->getName().'%%', '{'.$this->getColumnGetter($column, true, 'this->').'}', $value);
551     }
552
553     return $value;
554   }
555
556   /**
557    * Returns HTML code for a column in list mode.
558    *
559    * @param string  The column name
560    * @param array   The parameters
561    *
562    * @return string HTML code
563    */
564   public function getColumnListTag($column, $params = array())
565   {
566     $user_params = $this->getParameterValue('list.fields.'.$column->getName().'.params');
567     $user_params = is_array($user_params) ? $user_params : sfToolkit::stringToArray($user_params);
568     $params      = $user_params ? array_merge($params, $user_params) : $params;
569
570     $type = $column->getCreoleType();
571     
572     $columnGetter = $this->getColumnGetter($column, true);
573
574     if ($column->isComponent())
575     {
576       return "get_component('".$this->getModuleName()."', '".$column->getName()."', array('type' => 'list', '{$this->getSingularName()}' => \${$this->getSingularName()}))";
577     }
578     else if ($column->isPartial())
579     {
580       return "get_partial('".$column->getName()."', array('type' => 'list', '{$this->getSingularName()}' => \${$this->getSingularName()}))";
581     }
582     else if ($type == CreoleTypes::DATE || $type == CreoleTypes::TIMESTAMP)
583     {
584       $format = isset($params['date_format']) ? $params['date_format'] : ($type == CreoleTypes::DATE ? 'D' : 'f');
585       return "($columnGetter !== null && $columnGetter !== '') ? format_date($columnGetter, \"$format\") : ''";
586     }
587     elseif ($type == CreoleTypes::BOOLEAN)
588     {
589       return "$columnGetter ? image_tag(sfConfig::get('sf_admin_web_dir').'/images/tick.png') : '&nbsp;'";
590     }
591     else
592     {
593       return "$columnGetter";
594     }
595   }
596
597   /**
598    * Returns HTML code for a column in filter mode.
599    *
600    * @param string  The column name
601    * @param array   The parameters
602    *
603    * @return string HTML code
604    */
605   public function getColumnFilterTag($column, $params = array())
606   {
607     $user_params = $this->getParameterValue('list.fields.'.$column->getName().'.params');
608     $user_params = is_array($user_params) ? $user_params : sfToolkit::stringToArray($user_params);
609     $params      = $user_params ? array_merge($params, $user_params) : $params;
610
611     if ($column->isComponent())
612     {
613       return "get_component('".$this->getModuleName()."', '".$column->getName()."', array('type' => 'filter'))";
614     }
615     else if ($column->isPartial())
616     {
617       return "get_partial('".$column->getName()."', array('type' => 'filter', 'filters' => \$filters))";
618     }
619
620     $type = $column->getCreoleType();
621
622     $default_value = "isset(\$filters['".$column->getName()."']) ? \$filters['".$column->getName()."'] : null";
623     $unquotedName = 'filters['.$column->getName().']';
624     $name = "'$unquotedName'";
625
626     if ($column->isForeignKey())
627     {
628       $params = $this->getObjectTagParams($params, array('include_blank' => true, 'related_class'=>$this->getRelatedClassName($column), 'text_method'=>'__toString', 'control_name'=>$unquotedName));
629       return "object_select_tag($default_value, null, $params)";
630
631     }
632     else if ($type == CreoleTypes::DATE)
633     {
634       // rich=false not yet implemented
635       $params = $this->getObjectTagParams($params, array('rich' => true, 'calendar_button_img' => sfConfig::get('sf_admin_web_dir').'/images/date.png'));
636       return "input_date_range_tag($name, $default_value, $params)";
637     }
638     else if ($type == CreoleTypes::TIMESTAMP)
639     {
640       // rich=false not yet implemented
641       $params = $this->getObjectTagParams($params, array('rich' => true, 'withtime' => true, 'calendar_button_img' => sfConfig::get('sf_admin_web_dir').'/images/date.png'));
642       return "input_date_range_tag($name, $default_value, $params)";
643     }
644     else if ($type == CreoleTypes::BOOLEAN)
645     {
646       $defaultIncludeCustom = '__("yes or no")';
647
648       $option_params = $this->getObjectTagParams($params, array('include_custom' => $defaultIncludeCustom));
649       $params = $this->getObjectTagParams($params);
650
651       // little hack
652       $option_params = preg_replace("/'".preg_quote($defaultIncludeCustom)."'/", $defaultIncludeCustom, $option_params);
653
654       $options = "options_for_select(array(1 => __('yes'), 0 => __('no')), $default_value, $option_params)";
655
656       return "select_tag($name, $options, $params)";
657     }
658     else if ($type == CreoleTypes::CHAR || $type == CreoleTypes::VARCHAR || $type == CreoleTypes::TEXT || $type == CreoleTypes::LONGVARCHAR)
659     {
660       $size = ($column->getSize() < 15 ? $column->getSize() : 15);
661       $params = $this->getObjectTagParams($params, array('size' => $size));
662       return "input_tag($name, $default_value, $params)";
663     }
664     else if ($type == CreoleTypes::INTEGER || $type == CreoleTypes::TINYINT || $type == CreoleTypes::SMALLINT || $type == CreoleTypes::BIGINT)
665     {
666       $params = $this->getObjectTagParams($params, array('size' => 7));
667       return "input_tag($name, $default_value, $params)";
668     }
669     else if ($type == CreoleTypes::FLOAT || $type == CreoleTypes::DOUBLE || $type == CreoleTypes::DECIMAL || $type == CreoleTypes::NUMERIC || $type == CreoleTypes::REAL)
670     {
671       $params = $this->getObjectTagParams($params, array('size' => 7));
672       return "input_tag($name, $default_value, $params)";
673     }
674     else
675     {
676       $params = $this->getObjectTagParams($params, array('disabled' => true));
677       return "input_tag($name, $default_value, $params)";
678     }
679   }
680
681   /**
682    * Escapes a string.
683    *
684    * @param string
685    *
686    * @param string
687    */
688   protected function escapeString($string)
689   {
690     return preg_replace('/\'/', '\\\'', $string);
691   }
692 }
693
694 /**
695  * Admin generator column.
696  *
697  * @package    symfony
698  * @subpackage generator
699  * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
700  * @version    SVN: $Id$
701  */
702 class sfAdminColumn
703 {
704   protected
705     $phpName    = '',
706     $column     = null,
707     $flags      = array();
708
709   /**
710    * Constructor.
711    *
712    * @param string The column php name
713    * @param string The column name
714    * @param array  The column flags
715    */
716   public function __construct($phpName, $column = null, $flags = array())
717   {
718     $this->phpName = $phpName;
719     $this->column  = $column;
720     $this->flags   = (array) $flags;
721   }
722
723   /**
724    * Returns true if the column maps a database column.
725    *
726    * @return boolean true if the column maps a database column, false otherwise
727    */
728   public function isReal()
729   {
730     return $this->column ? true : false;
731   }
732
733   /**
734    * Gets the name of the column.
735    *
736    * @return string The column name
737    */
738   public function getName()
739   {
740     return sfInflector::underscore($this->phpName);
741   }
742
743   /**
744    * Returns true if the column is a partial.
745    *
746    * @return boolean true if the column is a partial, false otherwise
747    */
748   public function isPartial()
749   {
750     return in_array('_', $this->flags) ? true : false;
751   }
752
753   /**
754    * Returns true if the column is a component.
755    *
756    * @return boolean true if the column is a component, false otherwise
757    */
758   public function isComponent()
759   {
760     return in_array('~', $this->flags) ? true : false;
761   }
762
763   /**
764    * Returns true if the column has a link.
765    *
766    * @return boolean true if the column has a link, false otherwise
767    */
768   public function isLink()
769   {
770     return (in_array('=', $this->flags) || $this->isPrimaryKey()) ? true : false;
771   }
772
773   /**
774    * Gets the php name of the column.
775    *
776    * @return string The php name
777    */
778   public function getPhpName()
779   {
780     return $this->phpName;
781   }
782
783   // FIXME: those methods are only used in the propel admin generator
784   public function __call($name, $arguments)
785   {
786     return $this->column ? $this->column->$name() : null;
787   }
788 }
789
Note: See TracBrowser for help on using the browser.