diff options
author | Max Kanat-Alexander <mkanat@bugzilla.org> | 2011-08-09 23:19:43 +0200 |
---|---|---|
committer | Max Kanat-Alexander <mkanat@bugzilla.org> | 2011-08-09 23:19:43 +0200 |
commit | 80c6d150b42ae5d9ba7464c5e20023cc90388259 (patch) | |
tree | 77df9794d444fbc861f53aa0240128a53f9d6467 | |
parent | 93175c689f0349d879b3dfca5bd0236c19b73855 (diff) | |
download | bugzilla-80c6d150b42ae5d9ba7464c5e20023cc90388259.tar.gz bugzilla-80c6d150b42ae5d9ba7464c5e20023cc90388259.tar.xz |
Bug 636416: Use the standard value-controller javascript to control the
drop-down fields on the Advanced Search page.
r=glob, a=mkanat
-rw-r--r-- | js/field.js | 190 | ||||
-rw-r--r-- | js/util.js | 28 | ||||
-rwxr-xr-x | query.cgi | 66 | ||||
-rw-r--r-- | template/en/default/search/field.html.tmpl | 79 | ||||
-rw-r--r-- | template/en/default/search/form.html.tmpl | 104 | ||||
-rw-r--r-- | template/en/default/search/search-advanced.html.tmpl | 12 | ||||
-rw-r--r-- | template/en/default/search/search-create-series.html.tmpl | 2 | ||||
-rw-r--r-- | template/en/default/search/search-report-graph.html.tmpl | 2 | ||||
-rw-r--r-- | template/en/default/search/search-report-table.html.tmpl | 2 |
9 files changed, 310 insertions, 175 deletions
diff --git a/js/field.js b/js/field.js index 1a3bc3efd..ea1769bd1 100644 --- a/js/field.js +++ b/js/field.js @@ -517,44 +517,143 @@ function handleVisControllerValueChange(e, args) { } } -function showValueWhen(controlled_field_id, controlled_value_ids, - controller_field_id, controller_value_id) +/** + * This is a data structure representing the tree of controlled values. + * Let's call the "controller value" the "source" and the "controlled + * value" the "target". A target can have only one source, but a source + * can have an infinite number of targets. + * + * The data structure is a series of hash tables that go something + * like this: + * + * source_field -> target_field -> source_value_id -> target_value_ids + * + * We always know source_field when our event handler is called, since + * that's the field the event is being triggered on. We can then enumerate + * through every target field, check the status of each source field value, + * and act appropriately on each target value. + */ +var bz_value_controllers = {}; +// This keeps track of whether or not we've added an onchange handler +// for the source field yet. +var bz_value_controller_has_handler = {}; +function showValueWhen(target_field_id, target_value_ids, + source_field_id, source_value_id, empty_shows_all) { - var controller_field = document.getElementById(controller_field_id); - // Note that we don't get an object for the controlled field here, - // because it might not yet exist in the DOM. We just pass along its id. - YAHOO.util.Event.addListener(controller_field, 'change', - handleValControllerChange, [controlled_field_id, controlled_value_ids, - controller_field, controller_value_id]); + if (!bz_value_controllers[source_field_id]) { + bz_value_controllers[source_field_id] = {}; + } + if (!bz_value_controllers[source_field_id][target_field_id]) { + bz_value_controllers[source_field_id][target_field_id] = {}; + } + var source_values = bz_value_controllers[source_field_id][target_field_id]; + source_values[source_value_id] = target_value_ids; + + if (!bz_value_controller_has_handler[source_field_id]) { + var source_field = document.getElementById(source_field_id); + YAHOO.util.Event.addListener(source_field, 'change', + handleValControllerChange, [source_field, empty_shows_all]); + bz_value_controller_has_handler[source_field_id] = true; + } } function handleValControllerChange(e, args) { - var controlled_field = document.getElementById(args[0]); - var controlled_value_ids = args[1]; - var controller_field = args[2]; - var controller_value_id = args[3]; - - var controller_item = document.getElementById( - _value_id(controller_field.id, controller_value_id)); - - for (var i = 0; i < controlled_value_ids.length; i++) { - var item = getPossiblyHiddenOption(controlled_field, - controlled_value_ids[i]); - if (item.disabled && controller_item && controller_item.selected) { - item = showOptionInIE(item, controlled_field); - YAHOO.util.Dom.removeClass(item, 'bz_hidden_option'); - item.disabled = false; + var source = args[0]; + var empty_shows_all = args[1]; + + for (var target_field_id in bz_value_controllers[source.id]) { + var target = document.getElementById(target_field_id); + if (!target) continue; + _update_displayed_values(source, target, empty_shows_all); + } +} + +/* See the docs for bz_option_duplicate count lower down for an explanation + * of this data structure. + */ +var bz_option_hide_count = {}; + +function _update_displayed_values(source, target, empty_shows_all) { + var show_all = (empty_shows_all && source.selectedIndex == -1); + + bz_option_hide_count[target.id] = {}; + + var source_values = bz_value_controllers[source.id][target.id]; + for (source_value_id in source_values) { + var source_option = getPossiblyHiddenOption(source, source_value_id); + var target_values = source_values[source_value_id]; + for (var i = 0; i < target_values.length; i++) { + var target_value_id = target_values[i]; + _handle_source_target(source_option, target, target_value_id, + show_all); } - else if (!item.disabled) { - YAHOO.util.Dom.addClass(item, 'bz_hidden_option'); - if (item.selected) { - item.selected = false; - bz_fireEvent(controlled_field, 'change'); - } - item.disabled = true; - hideOptionInIE(item, controlled_field); + } + + // We may have updated which elements are selected or not selected + // in the target field, and it may have handlers associated with + // that, so we need to fire the change event on the target. + bz_fireEvent(target, 'change'); +} + +function _handle_source_target(source_option, target, target_value_id, + show_all) +{ + var target_option = getPossiblyHiddenOption(target, target_value_id); + + // We always call either _show_option or _hide_option on every single + // target value. Although this is not theoretically the most efficient + // thing we can do, it handles all possible edge cases, and there are + // a lot of those, particularly when this code is being used on the + // search form. + if (source_option.selected || (show_all && !source_option.disabled)) { + _show_option(target_option, target); + } + else { + _hide_option(target_option, target); + } +} + +/* When an option has duplicates (see the docs for bz_option_duplicates + * lower down in this file), we only want to hide it if *all* the duplicates + * would be hidden. So we keep a counter of how many duplicates each option + * has. Then, when we run through a "change" call for a source field, + * we count how many times each value gets hidden, and only actually + * hide it if the counter hits a number higher than the duplicate count. + */ +var bz_option_duplicate_count = {}; + +function _show_option(option, field) { + if (!option.disabled) return; + option = showOptionInIE(option, field); + YAHOO.util.Dom.removeClass(option, 'bz_hidden_option'); + option.disabled = false; +} + +function _hide_option(option, field) { + if (option.disabled) return; + + var value_id = option.bz_value_id; + + if (field.id in bz_option_duplicate_count + && value_id in bz_option_duplicate_count[field.id]) + { + if (!bz_option_hide_count[field.id][value_id]) { + bz_option_hide_count[field.id][value_id] = 0; } + bz_option_hide_count[field.id][value_id]++; + var current = bz_option_hide_count[field.id][value_id]; + var dups = bz_option_duplicate_count[field.id][value_id]; + // We check <= because the value in bz_option_duplicate_count is + // 1 less than the total number of duplicates (since the shown + // option is also a "duplicate" but not counted in + // bz_option_duplicate_count). + if (current <= dups) return; } + + YAHOO.util.Dom.addClass(option, 'bz_hidden_option'); + option.selected = false; + option.disabled = true; + hideOptionInIE(option, field); } // A convenience function to generate the "id" tag of an <option> @@ -571,7 +670,7 @@ function _value_id(field_name, id) { * on <option> tags. However, you *can* insert a Comment Node as a * child of a <select> tag. So we just insert a Comment where the <option> * used to be. */ -var ie_hidden_options = new Array(); +var ie_hidden_options = {}; function hideOptionInIE(anOption, aSelect) { if (browserCanHideOptions(aSelect)) return; @@ -591,7 +690,7 @@ function hideOptionInIE(anOption, aSelect) { // Store the comment node for quick access for getPossiblyHiddenOption if (!ie_hidden_options[aSelect.id]) { - ie_hidden_options[aSelect.id] = new Array(); + ie_hidden_options[aSelect.id] = {}; } ie_hidden_options[aSelect.id][anOption.id] = commentNode; } @@ -620,6 +719,7 @@ function showOptionInIE(aNode, aSelect) { function initHidingOptionsForIE(select_name) { var aSelect = document.getElementById(select_name); if (browserCanHideOptions(aSelect)) return; + if (!aSelect) return; for (var i = 0; ;i++) { var item = aSelect.options[i]; @@ -631,7 +731,27 @@ function initHidingOptionsForIE(select_name) { } } +/* Certain fields, like the Component field, have duplicate values in + * them (the same name, but different ids). We don't display these + * duplicate values in the UI, but the option hiding/showing code still + * uses the ids of these unshown duplicates. So, whenever we get the + * id of an unshown duplicate in getPossiblyHiddenOption, we have to + * return the actually-used <option> instead. + * + * The structure of the data looks like: + * + * field_name -> unshown_value_id -> shown_value_id_it_is_a_duplicate_of + */ +var bz_option_duplicates = {}; + function getPossiblyHiddenOption(aSelect, optionId) { + + if (bz_option_duplicates[aSelect.id] + && bz_option_duplicates[aSelect.id][optionId]) + { + optionId = bz_option_duplicates[aSelect.id][optionId]; + } + // Works always for <option> tags, and works for commentNodes // in IE (but not in Webkit). var id = _value_id(aSelect.id, optionId); @@ -643,6 +763,10 @@ function getPossiblyHiddenOption(aSelect, optionId) { val = ie_hidden_options[aSelect.id][id]; } + // We add this property for our own convenience, it's used in + // other places. + val.bz_value_id = optionId; + return val; } diff --git a/js/util.js b/js/util.js index 6dcabbbc9..56649ac66 100644 --- a/js/util.js +++ b/js/util.js @@ -220,6 +220,34 @@ function bz_valueSelected(aSelect, aValue) { } /** + * Returns all Option elements that are selected in a <select>, + * as an array. Returns an empty array if nothing is selected. + * + * @param aSelect The select you want the selected values of. + */ +function bz_selectedOptions(aSelect) { + // HTML 5 + if (aSelect.selectedOptions) { + return aSelect.selectedOptions; + } + + var start_at = aSelect.selectedIndex; + if (start_at == -1) return []; + var first_selected = aSelect.options[start_at]; + if (!aSelect.multiple) return first_selected; + // selectedIndex is specified as being the "first selected item", + // so we can start from there. + var selected = [first_selected]; + var options_length = aSelect.options.length; + // We start after first_selected + for (var i = start_at + 1; i < options_length; i++) { + var this_option = aSelect.options[i]; + if (this_option.selected) selected.push(this_option); + } + return selected; +} + +/** * Tells you where (what index) in a <select> a particular option is. * Returns -1 if the value is not in the <select> * @@ -40,6 +40,46 @@ use Bugzilla::Keyword; use Bugzilla::Field; use Bugzilla::Install::Util qw(vers_cmp); +############### +# Subroutines # +############### + +sub get_product_values { + my ($products, $field, $vars) = @_; + my @all_values = map { @{ $_->$field } } @$products; + + my (@unique, %duplicates, %duplicate_count, %seen); + foreach my $value (@all_values) { + my $lc_name = lc($value->name); + if ($seen{$lc_name}) { + $duplicate_count{$seen{$lc_name}->id}++; + $duplicates{$value->id} = $seen{$lc_name}; + next; + } + push(@unique, $value); + $seen{$lc_name} = $value; + } + + if ($field eq 'version') { + @unique = sort { vers_cmp(lc($a->name), lc($b->name)) } @unique; + } + else { + @unique = sort { lc($a->name) cmp lc($b->name) } @unique; + } + + $field =~ s/s$//; + $field = 'target_milestone' if $field eq 'milestone'; + $vars->{duplicates}->{$field} = \%duplicates; + $vars->{duplicate_count}->{$field} = \%duplicate_count; + # "component" is a reserved word in Template Toolkit. + $field = 'component_' if $field eq 'component'; + $vars->{$field} = \@unique; +} + +############### +# Main Script # +############### + my $cgi = Bugzilla->cgi; my $dbh = Bugzilla->dbh; my $template = Bugzilla->template; @@ -133,38 +173,18 @@ if (!PrefillForm($buffer)) { my @selectable_products = sort {lc($a->name) cmp lc($b->name)} @{$user->get_selectable_products}; Bugzilla::Product::preload(\@selectable_products); +$vars->{'product'} = \@selectable_products; # Create the component, version and milestone lists. -my %components; -my %versions; -my %milestones; - -foreach my $product (@selectable_products) { - $components{$_->name} = 1 foreach (@{$product->components}); - $versions{$_->name} = 1 foreach (@{$product->versions}); - $milestones{$_->name} = 1 foreach (@{$product->milestones}); +foreach my $field (qw(components versions milestones)) { + get_product_values(\@selectable_products, $field, $vars); } -my @components = sort(keys %components); -my @versions = sort { vers_cmp (lc($a), lc($b)) } keys %versions; -my @milestones = sort(keys %milestones); - -$vars->{'product'} = \@selectable_products; - # Create data structures representing each classification if (Bugzilla->params->{'useclassification'}) { $vars->{'classification'} = $user->get_selectable_classifications; } -# We use 'component_' because 'component' is a Template Toolkit reserved word. -$vars->{'component_'} = \@components; - -$vars->{'version'} = \@versions; - -if (Bugzilla->params->{'usetargetmilestone'}) { - $vars->{'target_milestone'} = \@milestones; -} - my @chfields; push @chfields, "[Bug creation]"; diff --git a/template/en/default/search/field.html.tmpl b/template/en/default/search/field.html.tmpl index defc94cc3..c237ac16c 100644 --- a/template/en/default/search/field.html.tmpl +++ b/template/en/default/search/field.html.tmpl @@ -121,22 +121,71 @@ [% legal_values = ${"component_"} %] [% END %] [% FOREACH current_value = legal_values %] - [% IF current_value.id %] - [%# current_value is a hash instead of a value which - only applies for Resolution really, everywhere else current_value - is just the value %] - [% v = current_value.name OR '---' -%] - <option value="[% v FILTER html %]" - [% ' selected="selected"' IF value.contains( v ) %]> - [% display_value(field.name, current_value.name) FILTER html %] - </option> - [% ELSE %] - <option value="[% current_value OR '---' FILTER html %]" - [% ' selected="selected"' IF value.contains( current_value ) %]> - [% display_value(field.name, current_value) FILTER html %] - </option> - [% END %] + [% SET v = current_value.name OR '---' -%] + [% SET display = display_value(field.name, current_value.name) %] + <option [% IF v != display %]value="[% v FILTER html %]"[% END ~%] + id="v[% current_value.id FILTER html %]_[% field.name FILTER html %]" + [% ' selected="selected"' IF value.contains( v ) %]> + [%~ display FILTER html ~%] + </option> [% END %] </select> </div> + + [% IF value_controllers.${field.name}.defined %] + <script type="text/javascript"><!-- + [%+ FILTER collapse %] + [% FOREACH accessor = value_controllers.${field.name}.keys %] + [% PROCESS controller_js %] + [% END %] + [%~ END ~%] + // --></script> + [% END %] + [% IF duplicates.${field.name}.keys.size %] + [% SET field_dups = duplicates.${field.name} %] + [% SET dup_counts = duplicate_count.${field.name} %] + <script type="text/javascript"> + [%+ FILTER collapse %] + bz_option_duplicates['[% field.name FILTER js %]'] = { + [% FOREACH dup = field_dups.keys %] + [% dup FILTER js %]:[% field_dups.$dup.id FILTER js %] + [%~ ',' UNLESS loop.last %] + [% END ~%] + }; + bz_option_duplicate_count['[% field.name FILTER js %]'] = { + [% FOREACH dup_target = dup_counts.keys %] + [% dup_target FILTER js %]:[% dup_counts.$dup_target %] + [%~ ',' UNLESS loop.last %] + [% END %] + }; + [% END %] + </script> + [% END %] + + [% END %] +[%# END OF SWITCH %] + +[% BLOCK controller_js %] + [%# If there are selected values already, we need to fire the + # "change" event once the page has loaded, so we can set all + # the values in all the other <select>s properly. + #%] + YAHOO.util.Event.onDOMReady(function() { + var field = document.getElementById('[% field.name FILTER js %]'); + if (field.selectedIndex != -1) bz_fireEvent(field, 'change'); + }); + + [% SET sub_field = value_controllers.${field.name}.$accessor %] + [% FOREACH legal_value = legal_values %] + [% SET controlled_ids = [] %] + [% FOREACH sub_value = legal_value.$accessor %] + [% controlled_ids.push(sub_value.id) %] + [% END %] + [% NEXT IF !controlled_ids.size %] + showValueWhen('[% sub_field.name FILTER js %]', + [[% controlled_ids.join(',') FILTER js %]], + '[% field.name FILTER js %]', + [% legal_value.id FILTER js %], + true); [% END %] +[% END %] diff --git a/template/en/default/search/form.html.tmpl b/template/en/default/search/form.html.tmpl index 41e116518..fb9454ff6 100644 --- a/template/en/default/search/form.html.tmpl +++ b/template/en/default/search/form.html.tmpl @@ -25,85 +25,6 @@ <script type="text/javascript"> -var first_load = true; [%# is this the first time we load the page? %] -var last_sel = new Array(); [%# caches last selection %] - -[% IF Param('useclassification') %] -var useclassification = true; -var prods = new Array(); -[% ELSE %] -var useclassification = false; -[% END %] -var cpts = new Array(); -var vers = new Array(); -[% IF Param('usetargetmilestone') %] -var tms = new Array(); -[% END %] - -[%# Create an array of products, indexed by the classification #%] - -[% nclass = 0 %] -[% FOREACH c = classification %] - prods[[% nclass FILTER js %]] = [ - [% sep = '' %] - [%- FOREACH item = user.get_selectable_products(c.id) -%] - [%- IF item.components.size -%] - [%- sep FILTER js %]'[% item.name FILTER js %]' - [%- sep = ',' -%] - [%- END -%] - [%- END -%] ]; - [% nclass = nclass+1 %] -[% END %] - -[%# Create three arrays of components, versions and target milestones, indexed - # numerically according to the product they refer to. #%] - -[% n = 0 %] -[% FOREACH p = product %] - [% NEXT IF NOT p.components.size %] - [% IF Param('useclassification') %] - prods['[% p.name FILTER js %]'] = [% n %] - [% END %] - cpts[[% n %]] = [ - [%- FOREACH item = p.components %]'[% item.name FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ]; - vers[[% n %]] = [ - [%- FOREACH item = p.versions -%]'[% item.name FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ]; - [% IF Param('usetargetmilestone') %] - tms[[% n %]] = [ - [%- FOREACH item = p.milestones %]'[% item.name FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ]; - [% END %] - [% n = n+1 %] -[% END %] - -/* - * doOnSelectProduct determines which selection should get updated - * - * - selectmode = 0 - init - * selectmode = 1 - classification selected - * selectmode = 2 - product selected - * - * globals: - * queryform - string holding the name of the selection form - */ -function doOnSelectProduct(selectmode) { - var f = document.forms[queryform]; - var milestone = (typeof(f.target_milestone) == "undefined" ? - null : f.target_milestone); - if (selectmode == 0) { - // If there is no classification selected, give us a chance to fill - // the select fields with values from the possibly selected product. - if (useclassification && f.classification.selectedIndex > -1) { - selectClassification(f.classification, f.product, f.component, f.version, milestone); - } else { - selectProduct(f.product, f.component, f.version, milestone, null); - } - } else if (selectmode == 1) { - selectClassification(f.classification, f.product, f.component, f.version, milestone); - } else { - selectProduct(f.product, f.component, f.version, milestone, null); - } -} - // Hide the Advanced Fields by default, unless the user has a cookie // that specifies otherwise. // ▸ and ▾ are both utf8 escaped characters for right @@ -143,7 +64,7 @@ TUI_hide_default('information_query'); accesskey = "s" %] <script type="text/javascript"> <!-- - document.forms[queryform].short_desc.focus(); + document.getElementById('short_desc').focus(); // --> </script> @@ -154,23 +75,26 @@ TUI_hide_default('information_query'); </div> [%# *** Classification Product Component *** %] - + +[% value_controllers = { + 'classification' => { 'products' => bug_fields.product }, + 'product' => { 'components' => bug_fields.component, + 'versions' => bug_fields.version, + 'milestones' => bug_fields.target_milestone }, +} %] + [% Hook.process('before_selects_top') %] [% IF Param('useclassification') %] - [% fake_classfication = { name => bug_fields.classification.name, - type => constants.FIELD_TYPE_SINGLE_SELECT } %] - [% INCLUDE "search/field.html.tmpl" - field => fake_classfication - accesskey => "c" - onchange => "doOnSelectProduct(1);" - value => default.classification - %] + [% INCLUDE "search/field.html.tmpl" + field => bug_fields.classification + accesskey => "c" + value => default.classification + %] [% END %] [% INCLUDE "search/field.html.tmpl" field => bug_fields.product accesskey => "p" - onchange => "doOnSelectProduct(2);" value => default.product %] [% INCLUDE "search/field.html.tmpl" diff --git a/template/en/default/search/search-advanced.html.tmpl b/template/en/default/search/search-advanced.html.tmpl index ef7fa769a..4f6f37bf2 100644 --- a/template/en/default/search/search-advanced.html.tmpl +++ b/template/en/default/search/search-advanced.html.tmpl @@ -30,21 +30,12 @@ [% cgi = Bugzilla.cgi %] -[% js_data = BLOCK %] -var queryform = "queryform" -[% END %] - [% PROCESS global/header.html.tmpl title = "Search for $terms.bugs" - onload = "doOnSelectProduct(0);" - javascript = js_data yui = [ 'autocomplete', 'calendar' ] - javascript_urls = [ "js/productform.js", "js/util.js", "js/TUI.js", "js/field.js"] + javascript_urls = [ "js/util.js", "js/TUI.js", "js/field.js"] style_urls = [ "skins/standard/search_form.css" ] doc_section = "query.html" - style = "dl.bug_changes dt { - margin-top: 15px; - }" %] [% WRAPPER search/tabs.html.tmpl %] @@ -63,7 +54,6 @@ var queryform = "queryform" </form> - [% END %] [% PROCESS global/footer.html.tmpl %] diff --git a/template/en/default/search/search-create-series.html.tmpl b/template/en/default/search/search-create-series.html.tmpl index 3ca68ac49..6863eba10 100644 --- a/template/en/default/search/search-create-series.html.tmpl +++ b/template/en/default/search/search-create-series.html.tmpl @@ -35,7 +35,7 @@ onload = "doOnSelectProduct(0);" yui = [ 'autocomplete', 'calendar' ] javascript = js_data - javascript_urls = [ "js/util.js", "js/productform.js", "js/TUI.js", "js/field.js" ] + javascript_urls = [ "js/util.js", "js/TUI.js", "js/field.js" ] style_urls = [ "skins/standard/search_form.css" ] doc_section = "reporting.html#charts-new-series" %] diff --git a/template/en/default/search/search-report-graph.html.tmpl b/template/en/default/search/search-report-graph.html.tmpl index 3c894cf73..362c910fa 100644 --- a/template/en/default/search/search-report-graph.html.tmpl +++ b/template/en/default/search/search-report-graph.html.tmpl @@ -34,7 +34,7 @@ var queryform = "reportform" onload = "doOnSelectProduct(0); chartTypeChanged()" yui = [ 'autocomplete', 'calendar' ] javascript = js_data - javascript_urls = [ "js/util.js", "js/productform.js", "js/TUI.js", "js/field.js" ] + javascript_urls = [ "js/util.js", "js/TUI.js", "js/field.js" ] style_urls = [ "skins/standard/search_form.css" ] doc_section = "reporting.html#reports" %] diff --git a/template/en/default/search/search-report-table.html.tmpl b/template/en/default/search/search-report-table.html.tmpl index 7e087e7fe..c59743965 100644 --- a/template/en/default/search/search-report-table.html.tmpl +++ b/template/en/default/search/search-report-table.html.tmpl @@ -34,7 +34,7 @@ var queryform = "reportform" onload = "doOnSelectProduct(0)" yui = [ 'autocomplete', 'calendar' ] javascript = js_data - javascript_urls = [ "js/util.js", "js/productform.js", "js/TUI.js", "js/field.js" ] + javascript_urls = [ "js/util.js", "js/TUI.js", "js/field.js" ] style_urls = [ "skins/standard/search_form.css" ] doc_section = "reporting.html#reports" %] |