Project

General

Profile

Download (20.2 KB) Statistics
| Branch: | Tag: | Revision:
1
<?php
2
/**
3
 * @file
4
 * Search related functions.
5
 */
6

    
7
/**
8
 * Returns a Drupal path to a search form for a CDM webservice.
9
 *
10
 * For a given CDM webservice end-point, the drupal page path to the
11
 * according search form is returned.
12
 * cdm webservice end points are defined in constant variables like:
13
 * <code>CDM_WS_PORTAL_TAXON_FIND</code> and
14
 * <code>CDM_WS_PORTAL_TAXON_FINDBY_DESCRIPTIONELEMENT_FULLTEXT</code>
15
 *
16
 * @param string $ws_endpoint
17
 *   The cdm webservice endpoint for which to find the search form path.
18
 *
19
 * @return string
20
 *   The Drupal path found.
21
 */
22
function cdm_dataportal_search_form_path_for_ws($ws_endpoint) {
23
  static $form_ws_map = array(
24
    CDM_WS_PORTAL_TAXON_FIND => "cdm_dataportal/search",
25
    CDM_WS_PORTAL_TAXON_SEARCH => "cdm_dataportal/search",
26
    CDM_WS_PORTAL_TAXON_FINDBY_DESCRIPTIONELEMENT_FULLTEXT => "cdm_dataportal/search/taxon_by_description",
27
  );
28
  return $form_ws_map[$ws_endpoint];
29
}
30

    
31
/**
32
 * Prepares a form array for a general purpose search form.
33
 *
34
 * The form is used for general purpose search functionality in the
35
 * dataportal. The form returned is populated with all nessecary fields
36
 * for internal processing and has the textfield element $form['query']
37
 * which holds the query term.
38
 * he sub tree array can be extended to contain additional search parameters.
39
 *
40
 * @param string $action_path
41
 *   The Drupal path to be put into the action url to which the form will
42
 *   be submitted.
43
 * @param string $search_webservice
44
 *   The cdm-remote webservice to be used, valid values are defined by
45
 *   the constants: FIXME.
46
 * @param string $query_field_default_value
47
 *   A default text for the query field
48
 * @param string $query_field_description
49
 *   The description text for the query field
50
 * @param string $process
51
 *   The value for #process, if NULL (default), 'cdm_dataportal_search_process'
52
 *   is used.
53
 *
54
 * @return array
55
 *   The prepared form array.
56
 */
57
function cdm_dataportal_search_form_prepare($action_path, $search_webservice, $query_field_default_value, $query_field_description, $process = NULL) {
58

    
59
  if ($process == NULL) {
60
    $process = 'cdm_dataportal_search_process';
61
  }
62

    
63
  $form['#method'] = 'get';
64
  //
65
  //  $form['#process'] = array(
66
  //  $process => array(),
67
  //  );
68
  //
69
  $form['#action'] = url($action_path, array(
70
    'absolute' => TRUE,
71
  ));
72

    
73
  $form['ws'] = array(
74
    '#type' => 'hidden',
75
    '#value' => $search_webservice,
76
    '#name' => 'ws',
77
  );
78

    
79
  $form['query'] = array(
80
    '#weight' => 0,
81
    '#type' => 'textfield',
82
    '#size' => 68,
83
    // This causes the description to display also when hovering over
84
    // the textfield.
85
    // This is wanted behaviour for the simple seach but could
86
    // be disabled for the advances search.
87
    '#attributes' => array(
88
      'title' => $query_field_description,
89
    ),
90
    '#description' => $query_field_description,
91
    '#value' => $query_field_default_value,
92
    // '#description' => $query_field_description,
93
  );
94

    
95
  $form['search'] = array(
96
    '#weight' => 3,
97
    '#tree' => TRUE,
98
    // '#type' => $advanced_form ? 'fieldset': 'hidden',
99
    '#title' => t('Options'),
100
  );
101

    
102
  // Clean URL get forms breaks if we don't give it a 'q'.
103
  if (!(bool) variable_get('clean_url', '0')) {
104
    $form['search']['q'] = array(
105
      '#type' => 'hidden',
106
      '#value' => $action_path,
107
      '#name' => 'q',
108
    );
109
  }
110

    
111
  $form['submit'] = array(
112
    '#weight' => 5,
113
    '#type' => 'submit',
114
    '#name' => '',
115
    '#value' => t('Search'),
116
  );
117

    
118
  return $form;
119
}
120

    
121
/**
122
 * Creates a search form for searching on taxa.
123
 *
124
 * If advanced $advanced_form id TRUE the form will offer additional choices
125
 *
126
 * @param array $form
127
 *   A drupal form array
128
 * @param array $form_state
129
 *   The drupal form state passed as reference
130
 * @param bool $advanced_form
131
 *   default is FALSE
132
 * @param bool $classification_select
133
 *   set TRUE to offer a classifiaction selector in the form - default is FALSE
134
 *   if only available in the advanced mode
135
 *
136
 * @return array
137
 *   the form array
138
 */
139
function cdm_dataportal_search_taxon_form($form, &$form_state, $advanced_form = FALSE, $classification_select = TRUE) {
140

    
141
  $query_field_default_value = (isset($_SESSION['cdm']['search']['query']) ? $_SESSION['cdm']['search']['query'] : '');
142

    
143
  if ($advanced_form || variable_get(SIMPLE_SEARCH_USE_LUCENE_BACKEND, FALSE)) {
144
    $search_service_endpoint = CDM_WS_PORTAL_TAXON_SEARCH;
145
  }
146
  else {
147
    $search_service_endpoint = CDM_WS_PORTAL_TAXON_FIND;
148
  }
149

    
150
  $form = cdm_dataportal_search_form_prepare(
151
    'cdm_dataportal/search/results/taxon',
152
    $search_service_endpoint,
153
    $query_field_default_value,
154
    t('Enter the name or part of a name you wish to search for.
155
      The asterisk  character * can be used as wildcard.'),
156
      NULL
157
  );
158

    
159
  if (!$advanced_form) {
160
    $form['query']['#size'] = 20;
161
  }
162

    
163
  $form['search']['pageSize'] = array(
164
    '#weight' => -1,
165
    '#type' => 'hidden',
166
    '#value' => variable_get('cdm_dataportal_search_items_on_page', 25),
167
  );
168

    
169
  $form['search']['pageNumber'] = array(
170
    '#weight' => -1,
171
    '#type' => 'hidden',
172
    '#value' => 0,
173
  );
174

    
175
  $search_taxa_mode_settings = get_array_variable_merged(
176
    CDM_SEARCH_TAXA_MODE,
177
    CDM_SEARCH_TAXA_MODE_DEFAULT
178
  );
179
  $preset_do_taxa = $search_taxa_mode_settings['doTaxa'] !== 0;
180
  $preset_do_synonyms = $search_taxa_mode_settings['doSynonyms'] !== 0;
181
  $preset_do_taxa_by_common_names = $search_taxa_mode_settings['doTaxaByCommonNames'] !== 0;
182
  $preset_do_misapplied_names = $search_taxa_mode_settings['doMisappliedNames'] !== 0;
183

    
184
  if ($advanced_form) {
185

    
186
    // --- ADVANCED SEARCH FORM ---
187
    //
188

    
189
    // Get presets from settings.
190
    $preset_classification_uuid = get_current_classification_uuid();
191

    
192
    // Overwrite presets by user choice stored in session.
193
    if (isset($_SESSION['cdm']['search'])) {
194
      $preset_do_taxa = (isset($_SESSION['cdm']['search']['doTaxa']) ? 1 : 0);
195
      $preset_do_synonyms = (isset($_SESSION['cdm']['search']['doSynonyms']) ? 1 : 0);
196
      $preset_do_misapplied_names = (isset($_SESSION['cdm']['search']['doMisappliedNames']) ? 1 : 0);
197
      $preset_do_taxa_by_common_names = (isset($_SESSION['cdm']['search']['doTaxaByCommonNames']) ? 1 : 0);
198
      if (isset($_SESSION['cdm']['search']['tree'])) {
199
        $preset_classification_uuid = $_SESSION['cdm']['search']['tree'];
200
      }
201
    }
202

    
203
    if ($classification_select === TRUE) {
204
      $form['search']['tree'] = array(
205
        '#title' => t('Classification'),
206
        '#weight' => 1,
207
        '#type' => 'select',
208
        '#default_value' => get_current_classification_uuid(),
209
        '#options' => cdm_get_taxontrees_as_options(TRUE),
210
        '#description' => t('A filter to limit the search to a specific classification. Choosing <em>--- ALL ---</em> will disable this filter.'),
211
      );
212
    }
213

    
214
    // General search parameters.
215
    $form['search']['doTaxa'] = array(
216
      '#weight' => 2,
217
      '#type' => 'checkbox',
218
      '#title' => t('Search for') . ' ' . t('accepted taxa'),
219
      '#value' => $preset_do_taxa,
220
    );
221
    $form['search']['doSynonyms'] = array(
222
      '#weight' => 3,
223
      '#type' => 'checkbox',
224
      '#title' => t('Search for') . ' ' . t('synonyms'),
225
      '#value' => $preset_do_synonyms,
226
    );
227
    $form['search']['doMisappliedNames'] = array(
228
      '#weight' => 4,
229
      '#type' => 'checkbox',
230
      '#title' => t('Search for') . ' ' . t('misapplied names'),
231
      '#value' => $preset_do_misapplied_names,
232
    );
233
    $form['search']['doTaxaByCommonNames'] = array(
234
      '#weight' => 5,
235
      '#type' => 'checkbox',
236
      '#title' => t('Search for') . ' ' . t('common names'),
237
      '#value' => $preset_do_taxa_by_common_names,
238
    );
239

    
240
    $area_term_dtos = cdm_ws_fetch_all(
241
      CDM_WS_DESCRIPTION_NAMEDAREAS_IN_USE,
242
      array('includeAllParents' => 'true')
243
    );
244

    
245
    // create map: parent_term_uuid => term
246
    $term_map = array();
247
    foreach ($area_term_dtos as $term_dto) {
248
      $term_map[$term_dto->uuid] = $term_dto;
249
    }
250

    
251
    $term_tree = array();
252
    // mixed_vocabularies will contain the uuid vocabularies which
253
    // also contain terms of foreign vocabuaries due to the term
254
    // hierarchy
255
    $mixed_vocabularies = array();
256

    
257
    // Build hierarchy of the terms regardless of the vocabulary.
258
    foreach ($term_map as $term_dto) {
259
      if (!empty($term_dto->partOfUuid)) {
260
        // Children.
261
        $parent =& $term_map[$term_dto->partOfUuid];
262
        if ($parent) {
263
          if (!isset($parent->children)) {
264
            $parent->children = array();
265
          }
266
          $parent->children[$term_dto->uuid] = $term_dto;
267
          if ($parent->vocabularyUuid != $term_dto->vocabularyUuid) {
268
            $mixed_vocabularies[$parent->vocabularyUuid] = $parent->vocabularyUuid;
269
          }
270
        }
271
      }
272
      else {
273
        // group root nodes by vocabulary
274
        if (!isset($term_tree[$term_dto->vocabularyUuid])) {
275
          $term_tree[$term_dto->vocabularyUuid] = array();
276
        }
277
        $term_tree[$term_dto->vocabularyUuid][$term_dto->uuid] = $term_dto;
278
      }
279
    }
280

    
281
    drupal_add_js(drupal_get_path('module', 'cdm_dataportal') . '/js/search_area_filter.js');
282

    
283
    drupal_add_js('jQuery(document).ready(function() {
284
        jQuery(\'#edit-search-areas\').search_area_filter(\'#edit-search-areas-areas-filter\');
285
      });
286
      ', array('type' => 'inline'));
287

    
288
    $form['search']['areas'] = array(
289
      '#type' => 'fieldset',
290
      '#title' => t('Filter by distribution areas'),
291
      '#description' => t('The search will return taxa having distribution
292
        information for at least one of the chosen areas. The areas are grouped
293
        by the vocabularies to which the highest level areas belong.'),
294
    );
295
    $form['search']['areas']['areas_filter'] = array(
296
      '#type' => 'textfield',
297
      '#description' => t('Enter an area name or abbreviation to filter the areas listed below.'),
298
    );
299
    $vocab_cnt = 0;
300
    $areas_defaults = array();
301
    if (isset($_SESSION['cdm']['search']['area'])) {
302
      $areas_defaults = explode(',', $_SESSION['cdm']['search']['area']);
303
    }
304
    foreach ($term_tree as $vocab_uuid => $term_dto_tree) {
305
      $vocabulary = cdm_ws_get(CDM_WS_TERMVOCABULARY, array($vocab_uuid));
306
      $areas_options = term_tree_as_options($term_dto_tree);
307
      $form['search']['areas']['area'][$vocab_cnt++] = array(
308
        '#prefix' => '<strong>' . $vocabulary->representation_L10n
309
          . (isset($mixed_vocabularies[$vocab_uuid]) ? ' <span title="Contains terms of at least one other area vocabulary.">(' . t('mixed') . ')</span>': '')
310
          . '</strong>',
311
        '#type' => 'checkboxes',
312
        '#default_value' => $areas_defaults,
313
        '#options' => $areas_options,
314
      );
315
    }
316

    
317
  }
318
  else {
319
    // --- SIMPLE SEARCH FORM ---
320
    //
321

    
322
    // Overwrite presets by user choice stored in session.
323
    if (isset($_SESSION['cdm']['search'])) {
324
      $preset_do_misapplied_names = (isset($_SESSION['cdm']['search']['doMisappliedNames']) ? 1 : 0);
325
    }
326

    
327
    $form['search']['doTaxa'] = array(
328
      '#weight' => -2,
329
      '#type' => 'hidden',
330
      '#value' => $preset_do_taxa,
331
    );
332
    $form['search']['doSynonyms'] = array(
333
      '#weight' => -3,
334
      '#type' => 'hidden',
335
      '#value' => $preset_do_synonyms,
336
    );
337
    $form['search']['doMisappliedNames'] = array(
338
      '#weight' => -4,
339
      '#type' => 'checkbox',
340
      '#title' => t('Misapplied names'),
341
      '#value' => $preset_do_misapplied_names,
342
    );
343
    $form['search']['doTaxaByCommonNames'] = array(
344
      '#weight' => -5,
345
      '#type' => 'hidden',
346
      '#value' => $preset_do_taxa_by_common_names,
347
    );
348
  }
349

    
350
  return $form;
351
}
352

    
353
/**
354
 * Wrapper function for cdm_dataportal_search_taxon_form().
355
 *
356
 * This function makes ot possible possible to just pass the
357
 * correct $form_id 'cdm_dataportal_search_taxon_form_advanced' to
358
 * drupal_get_form like:
359
 * drupal_get_form('cdm_dataportal_search_taxon_form_advanced');
360
 *
361
 * @param array $form
362
 *   A drupal form array
363
 * @param array $form_state
364
 *   The drupal form state passed as reference
365
 *
366
 * @return array
367
 *   The form array
368
 */
369
function cdm_dataportal_search_taxon_form_advanced($form, &$form_state) {
370
  return cdm_dataportal_search_taxon_form($form, $form_state, TRUE);
371
}
372

    
373
/**
374
 * Form for searching taxa by the findByDescriptionElementFullText rest service.
375
 */
376
function cdm_dataportal_search_taxon_by_description_form() {
377
  $query_field_default_value = (isset($_SESSION['cdm']['search']['query']) ? $_SESSION['cdm']['search']['query'] : '');
378

    
379
  $form = cdm_dataportal_search_form_prepare(
380
    'cdm_dataportal/search/results/taxon',
381
    CDM_WS_PORTAL_TAXON_FINDBY_DESCRIPTIONELEMENT_FULLTEXT,
382
    $query_field_default_value,
383
    t("Enter the text you wish to search for. The asterisk character * can be
384
        used as wildcard. Terms can be combined with 'AND'. To search for a
385
        full phrase enclose the terms in parentheses. For more syntactical
386
        options please refer to the !link.",
387
      array(
388
        '!link' => l(
389
          t('Apache Lucene - Query Parser Syntax'),
390
          'http://lucene.apache.org/core/old_versioned_docs/versions/2_9_1/queryparsersyntax.html', array(
391
            'attributes' => array(
392
              'absolute' => TRUE,
393
              'html' => TRUE),
394
          )
395
        ),
396
      )
397
    ),
398
    NULL
399
  );
400

    
401
  $form['search']['tree'] = array(
402
    '#weight' => -1,
403
    '#type' => 'hidden',
404
    '#value' => get_current_classification_uuid(),
405
  );
406

    
407
  $form['search']['hl'] = array(
408
    '#weight' => -1,
409
    '#type' => 'hidden',
410
    '#value' => 1,
411
  );
412

    
413
  // Only available to admins:
414
  if (!isset($_SESSION['cdm']['search']['clazz'])) {
415
    $_SESSION['cdm']['search']['clazz'] = '';
416
  }
417
  if (module_exists("user") && user_access('administer')) {
418
    $form['search']['clazz'] = array(
419
      '#type' => 'select',
420
      '#title' => t('Limit to description item type'),
421
      '#default_value' => $_SESSION['cdm']['search']['clazz'],
422
      '#options' => cdm_descriptionElementTypes_as_option(TRUE),
423
    );
424
  }
425

    
426
  $profile_feature_tree = get_profile_feature_tree();
427
  $feature_options = _featureTree_nodes_as_feature_options($profile_feature_tree->root);
428
  if (isset($_SESSION['cdm']['search']['features'])) {
429
    $form['search']['features'] = array(
430
      '#type' => 'checkboxes',
431
      '#title' => t('Limit to selected features'),
432
      '#default_value' => $_SESSION['cdm']['search']['features'],
433
      '#options' => $feature_options,
434
    );
435
  }
436
  else {
437
    $form['search']['features'] = array(
438
      '#type' => 'checkboxes',
439
      '#title' => t('Limit to selected features'),
440
      '#options' => $feature_options,
441
    );
442
  }
443
  return $form;
444
}
445

    
446
/**
447
 * Processes the query parameters of the search form.
448
 *
449
 * Reads the query parameters from $_REQUEST and modifies and adds additional
450
 * query parameters if nessecary.
451
 *
452
 *  - Filters $_REQUEST by a list of valid request parameters
453
 *  - modifies geographic_range parameters
454
 *  - adds taxon tree uuid if it is missing and if it should not be
455
 *    ignored (parameter value = 'IGNORE')
456
 *  - and more
457
 *
458
 *
459
 * @return array
460
 *   the processed request parameters submitted by the search form and
461
 *   also stores them in $_SESSION['cdm']['search']
462
 */
463
function cdm_dataportal_search_form_request() {
464

    
465
  $form_params = array();
466

    
467
  if (isset($_REQUEST['search']) && is_array($_REQUEST['search'])) {
468
    array_deep_copy($_REQUEST['search'], $form_params);
469
  }
470

    
471
  if (isset($_REQUEST['pager']) && is_array($_REQUEST['pager'])) {
472
    $form_params = array_merge($form_params, $_REQUEST['pager']);
473
  }
474

    
475
  $form_params['query'] = trim($_REQUEST['query']);
476

    
477
  // --- handle geographic range
478
  // Split of geographic range.
479
  unset($form_params['areas']);
480
  if (isset($_REQUEST['search']['areas']['area']) && is_array($_REQUEST['search']['areas']['area'])) {
481
    $area_uuids = array();
482
    foreach ($_REQUEST['search']['areas']['area'] as $areas) {
483
      $area_uuids = array_merge($area_uuids, $areas);
484
    }
485
    $form_params['area'] = implode(',', $area_uuids);
486
  }
487

    
488
  // Simple search will not submit a 'tree' query parameter,
489
  // so we add it here from what is stored in the session unless
490
  // SIMPLE_SEARCH_IGNORE_CLASSIFICATION is checked in the settings.
491
  if (!isset($form_params['tree']) && !variable_get(SIMPLE_SEARCH_IGNORE_CLASSIFICATION, 0)) {
492
    $form_params['tree'] = get_current_classification_uuid();
493
  }
494
  // If the 'NONE' classification has been chosen (adanced search)
495
  // delete the tree information to avoid unknown uuid exceptions in the
496
  // cdm service.
497
  if (isset($form_params['tree'])
498
    && ($form_params['tree'] == 'NONE' || !is_uuid($form_params['tree']))
499
  ) {
500
    // $form_params['ignore_classification'] =  TRUE;
501
    unset($form_params['tree']);
502
  }
503
  // else {
504
  //   $form_params['ignore_classification'] =  NULL;
505
  // }
506

    
507
  // Store in session.
508
  $_SESSION['cdm']['search'] = $form_params;
509

    
510
  return $form_params;
511
}
512

    
513
/**
514
 * Provides the classification to which the last search has been limited to..
515
 *
516
 * This function should only be used after the cdm_dataportal_search_execute()
517
 * handler has been run, otherwise it will return the infomation from the last
518
 * search executed. The information is retrieved from
519
 * the $_SESSION variable:  $_SESSION['cdm']['search']['tree']
520
 *
521
 * @return object
522
 *   the CDM classification instance which has been used a filter for the
523
 *   last processed search
524
 *   or NULL, it it was on all classifications
525
 */
526
function cdm_dataportal_searched_in_classification() {
527

    
528
  $classification = &drupal_static(__FUNCTION__);
529

    
530
  if (!isset($classification)) {
531
    if (isset($_SESSION['cdm']['search']['tree'])) {
532
      $classification = cdm_ws_get(CDM_WS_PORTAL_TAXONOMY, ($_SESSION['cdm']['search']['tree']));
533
    }
534
    else {
535
      $classification = FALSE;
536
    }
537
  }
538

    
539
  return $classification !== FALSE ? $classification : NULL;
540
}
541

    
542
/**
543
 * Removes Drupal internal form elements from query.
544
 */
545
function cdm_dataportal_search_process($form, &$form_state) {
546
  unset($form['form_id']);
547
  unset($form['form_token']);
548
  return $form;
549
}
550

    
551
/**
552
 * Sends a search request at the cdm web server.
553
 *
554
 * The parameters to build the query are taken obtained by calling
555
 * cdm_dataportal_search_form_request() which reads the query parameters
556
 * from $_REQUEST and add additional query parameters if nessecary.
557
 *
558
 * @see cdm_dataportal_search_form_request()
559
 */
560
function cdm_dataportal_search_execute() {
561

    
562
  // Store as last search in session.
563
  $_SESSION['cdm']['last_search'] = $_SERVER['REQUEST_URI'];
564

    
565
  // Validate the search webservice parameter:
566
  if (!isset($_REQUEST['ws'])) {
567
    drupal_set_message(
568
      t("Invalid search webservice parameter  'ws' given"), 'warning'
569
    );
570
    return NULL;
571
  }
572
  if (!cdm_dataportal_search_form_path_for_ws($_REQUEST['ws'])) {
573
    // Endpoint is unknown.
574
    drupal_set_message(
575
      t("Invalid search webservice parameter  'ws' given"), 'warning'
576
    );
577
    return NULL;
578
  }
579

    
580
  // Read the query parameters from $_REQUEST and add additional query
581
  // parameters if necessary.
582
  $request_params = cdm_dataportal_search_form_request();
583

    
584
  $taxon_pager = cdm_ws_get($_REQUEST['ws'], NULL, queryString($request_params));
585

    
586
  return $taxon_pager;
587
}
588

    
589
/**
590
 * Transforms the termDTO tree into options array.
591
 *
592
 *   TermDto:
593
 *      - partOfUuid:
594
 *      - representation_L10n:
595
 *      - representation_L10n_abbreviatedLabel:
596
 *      - uuid:
597
 *      - vocabularyUuid:
598
 *      - children: array of TermDto
599
 *
600
 * The options array is suitable for drupal form API elements that
601
 * allow multiple choices.
602
 * @see http://api.drupal.org/api/drupal/developer!topics!forms_api_reference.html/7#options
603
 *
604
 * @param array $term_dto_tree
605
 *   a hierarchic array of CDM TermDto instances, with additional
606
 * 'children' field:
607
 * @param array $options
608
 *   Internally used for recursive calls
609
 * @param string $prefix
610
 *   Internally used for recursive calls
611
 *
612
 * @return array
613
 *   the terms in an array as options for a form element that allows
614
 *   multiple choices.
615
 */
616
function term_tree_as_options($term_dto_tree, &$options = array(), $prefix = '') {
617

    
618
  foreach ($term_dto_tree as $uuid => $dto) {
619
    $label = $prefix . '<span class="child-label">'
620
      .  $dto->representation_L10n
621
      . '</span><span class="child-label-abbreviated"> (' . $dto->representation_L10n_abbreviatedLabel . ')</span>';
622
    $options[$uuid] = $label;
623
    if (isset($dto->children) && is_array($dto->children)) {
624
      uasort($dto->children, 'compare_terms_by_representationL10n');
625
      term_tree_as_options(
626
        $dto->children,
627
        $options, $prefix
628
          . '<span data-cdm-parent="' . $uuid . '" class="parent"></span>');
629
    }
630
  }
631

    
632
  return $options;
633
}
(10-10/15)