diff options
Diffstat (limited to 'extensions/BMO')
-rw-r--r-- | extensions/BMO/Extension.pm | 6 | ||||
-rw-r--r-- | extensions/BMO/lib/Reports.pm | 360 | ||||
-rw-r--r-- | extensions/BMO/template/en/default/hook/global/user-error-errors.html.tmpl | 16 | ||||
-rw-r--r-- | extensions/BMO/template/en/default/hook/reports/menu-end.html.tmpl | 5 | ||||
-rw-r--r-- | extensions/BMO/template/en/default/pages/release_tracking_report.html.tmpl | 98 | ||||
-rw-r--r-- | extensions/BMO/web/js/release_tracking_report.js | 206 | ||||
-rw-r--r-- | extensions/BMO/web/styles/reports.css | 4 |
7 files changed, 28 insertions, 667 deletions
diff --git a/extensions/BMO/Extension.pm b/extensions/BMO/Extension.pm index 265d09a29..3cf0e8317 100644 --- a/extensions/BMO/Extension.pm +++ b/extensions/BMO/Extension.pm @@ -57,8 +57,7 @@ use Bugzilla::Extension::BMO::Data qw($cf_visible_in_products use Bugzilla::Extension::BMO::Reports qw(user_activity_report triage_reports group_admins - email_queue_report - release_tracking_report); + email_queue_report); our $VERSION = '0.1'; @@ -174,9 +173,6 @@ sub page_before_template { elsif ($page eq 'email_queue.html') { email_queue_report($vars); } - elsif ($page eq 'release_tracking_report.html') { - release_tracking_report($vars); - } } sub _get_field_values_sort_key { diff --git a/extensions/BMO/lib/Reports.pm b/extensions/BMO/lib/Reports.pm index 79d04afb2..e9e2670b9 100644 --- a/extensions/BMO/lib/Reports.pm +++ b/extensions/BMO/lib/Reports.pm @@ -20,26 +20,20 @@ package Bugzilla::Extension::BMO::Reports; use strict; -use Bugzilla::Extension::BMO::Data qw($cf_disabled_flags); - -use Bugzilla::Constants; -use Bugzilla::Error; -use Bugzilla::Field; use Bugzilla::User; -use Bugzilla::Util qw(trim detaint_natural trick_taint correct_urlbase); +use Bugzilla::Util qw(trim detaint_natural); +use Bugzilla::Error; +use Bugzilla::Constants; use Date::Parse; use DateTime; -use JSON qw(-convert_blessed_universally); -use List::MoreUtils qw(uniq); use base qw(Exporter); our @EXPORT_OK = qw(user_activity_report triage_reports group_admins - email_queue_report - release_tracking_report); + email_queue_report); sub user_activity_report { my ($vars) = @_; @@ -65,10 +59,20 @@ sub user_activity_report { } Bugzilla::User::match_field({ 'who' => {'type' => 'multi'} }); - my $from_dt = _string_to_datetime($from); + ThrowUserError('user_activity_missing_from_date') unless $from; + my $from_time = _parse_date($from) + or ThrowUserError('user_activity_invalid_date', { date => $from }); + my $from_dt = DateTime->from_epoch(epoch => $from_time) + ->set_time_zone('local') + ->truncate(to => 'day'); $from = $from_dt->ymd(); - my $to_dt = _string_to_datetime($to); + ThrowUserError('user_activity_missing_to_date') unless $to; + my $to_time = _parse_date($to) + or ThrowUserError('user_activity_invalid_date', { date => $to }); + my $to_dt = DateTime->from_epoch(epoch => $to_time) + ->set_time_zone('local') + ->truncate(to => 'day'); $to = $to_dt->ymd(); # add one day to include all activity that happened on the 'to' date $to_dt->add(days => 1); @@ -294,20 +298,6 @@ sub user_activity_report { $vars->{'to'} = $to; } -sub _string_to_datetime { - my $input = shift; - my $time = _parse_date($input) - or ThrowUserError('report_invalid_date', { date => $input }); - return _time_to_datetime($time); -} - -sub _time_to_datetime { - my $time = shift; - return DateTime->from_epoch(epoch => $time) - ->set_time_zone('local') - ->truncate(to => 'day'); -} - sub _parse_date { my ($str) = @_; if ($str =~ /^(-|\+)?(\d+)([hHdDwWmMyY])$/) { @@ -535,7 +525,7 @@ sub triage_reports { } sub group_admins { - my ($vars) = @_; + my ($vars, $filter) = @_; my $dbh = Bugzilla->dbh; my $user = Bugzilla->user; @@ -597,320 +587,4 @@ sub email_queue_report { $vars->{'now'} = (time); } -sub release_tracking_report { - my ($vars) = @_; - my $dbh = Bugzilla->dbh; - my $input = Bugzilla->input_params; - my $user = Bugzilla->user; - - my @flag_names = qw( - approval-mozilla-release - approval-mozilla-beta - approval-mozilla-aurora - approval-comm-release - approval-comm-beta - approval-comm-aurora - ); - - my @flags_json; - my @fields_json; - my @products_json; - - # - # tracking flags - # - - my $all_products = $user->get_selectable_products; - my @usable_products; - - # build list of flags and their matching products - - foreach my $flag_name (@flag_names) { - # grab all matching flag_types - my @flag_types = @{Bugzilla::FlagType::match({ name => $flag_name, is_active => 1 })}; - - # we need a list of products, based on inclusions/exclusions - my @products; - my %flag_types; - foreach my $flag_type (@flag_types) { - $flag_types{$flag_type->name} = $flag_type->id; - my $has_all = 0; - my @exclusion_ids; - my @inclusion_ids; - foreach my $flag_type (@flag_types) { - if (scalar keys %{$flag_type->inclusions}) { - my $inclusions = $flag_type->inclusions; - foreach my $key (keys %$inclusions) { - push @inclusion_ids, ($inclusions->{$key} =~ /^(\d+)/); - } - } elsif (scalar keys %{$flag_type->exclusions}) { - my $exclusions = $flag_type->exclusions; - foreach my $key (keys %$exclusions) { - push @exclusion_ids, ($exclusions->{$key} =~ /^(\d+)/); - } - } else { - $has_all = 1; - last; - } - } - - if ($has_all) { - push @products, @$all_products; - } elsif (scalar @exclusion_ids) { - push @products, @$all_products; - foreach my $exclude_id (uniq @exclusion_ids) { - @products = grep { $_->id != $exclude_id } @products; - } - } else { - foreach my $include_id (uniq @inclusion_ids) { - push @products, grep { $_->id == $include_id } @$all_products; - } - } - } - @products = uniq @products; - push @usable_products, @products; - my @product_ids = map { $_->id } sort { lc($a->name) cmp lc($b->name) } @products; - - push @flags_json, { - name => $flag_name, - id => $flag_types{$flag_name} || 0, - products => \@product_ids, - fields => [], - }; - } - @usable_products = uniq @usable_products; - - # build a list of tracking flags for each product - # also build the list of all fields - - my @unlink_products; - foreach my $product (@usable_products) { - my @fields = - grep { _is_active_status_field($_->name) } - Bugzilla->active_custom_fields({ product => $product }); - my @field_ids = map { $_->id } @fields; - if (!scalar @fields) { - push @unlink_products, $product; - next; - } - - # product - push @products_json, { - name => $product->name, - id => $product->id, - fields => \@field_ids, - }; - - # add fields to flags - foreach my $rh (@flags_json) { - if (grep { $_ eq $product->id } @{$rh->{products}}) { - push @{$rh->{fields}}, @field_ids; - } - } - - # add fields to fields_json - foreach my $field (@fields) { - my $existing = 0; - foreach my $rh (@fields_json) { - if ($rh->{id} == $field->id) { - $existing = 1; - last; - } - } - if (!$existing) { - push @fields_json, { - name => $field->name, - id => $field->id, - }; - } - } - } - foreach my $rh (@flags_json) { - my @fields = uniq @{$rh->{fields}}; - $rh->{fields} = \@fields; - } - - # remove products which aren't linked with status fields - - foreach my $rh (@flags_json) { - my @product_ids; - foreach my $id (@{$rh->{products}}) { - unless (grep { $_->id == $id } @unlink_products) { - push @product_ids, $id; - } - $rh->{products} = \@product_ids; - } - } - - # - # rapid release dates - # - - my @ranges; - my $start_date = _string_to_datetime('2011-08-16'); - my $end_date = $start_date->clone->add(weeks => 6)->add(days => -1); - my $now_date = _time_to_datetime((time)); - - while ($start_date <= $now_date) { - unshift @ranges, { - value => sprintf("%s-%s", $start_date->ymd(''), $end_date->ymd('')), - label => sprintf("%s and %s", $start_date->ymd('-'), $end_date->ymd('-')), - }; - - $start_date = $end_date->clone;; - $start_date->add(days => 1); - $end_date->add(weeks => 6); - } - - # - # run report - # - - if ($input->{q}) { - my $q = _parse_query($input->{q}); - - my @where; - my @params; - my $query = " - SELECT a.bug_id - FROM bugs_activity a - INNER JOIN flags f ON f.bug_id = a.bug_id - INNER JOIN bugs b ON b.bug_id = a.bug_id - WHERE "; - - push @where, "(a.fieldid = ?)"; - push @params, $q->{field_id}; - - push @where, "(a.bug_when >= ?)"; - push @params, $q->{start_date} . ' 00:00:00'; - push @where, "(a.bug_when < ?)"; - push @params, $q->{end_date} . ' 00:00:00'; - - push @where, "(a.added LIKE ?)"; - push @params, '%' . $q->{flag_name} . '?%'; - - push @where, "(f.type_id IN (SELECT id FROM flagtypes WHERE name = ?))"; - push @params, $q->{flag_name}; - - push @where, "(f.status = ?)"; - push @params, $q->{flag_status}; - - if ($q->{product_id}) { - push @where, "(b.product_id = ?)"; - push @params, $q->{product_id}; - } - - if (scalar @{$q->{fields}}) { - my @fields; - foreach my $field (@{$q->{fields}}) { - my $op = $field->{value} eq '+' ? '=' : '<>'; - push @fields, "(b.".$field->{name}." $op 'fixed') "; - } - my $join = uc $q->{join}; - push @where, '(' . join(" $join ", @fields) . ')'; - } - - $query .= join("\nAND ", @where); - - my $bugs = $dbh->selectcol_arrayref($query, undef, @params); - push @$bugs, 0 unless @$bugs; - - my $urlbase = correct_urlbase(); - my $cgi = Bugzilla->cgi; - print $cgi->redirect( - -url => "${urlbase}buglist.cgi?bug_id=" . join(',', @$bugs) - ); - exit; - } - - # - # set template vars - # - - my $json = JSON->new(); - if (0) { - # debugging - $json->shrink(0); - $json->canonical(1); - $vars->{flags_json} = $json->pretty->encode(\@flags_json); - $vars->{products_json} = $json->pretty->encode(\@products_json); - $vars->{fields_json} = $json->pretty->encode(\@fields_json); - } else { - $json->shrink(1); - $vars->{flags_json} = $json->encode(\@flags_json); - $vars->{products_json} = $json->encode(\@products_json); - $vars->{fields_json} = $json->encode(\@fields_json); - } - - $vars->{flag_names} = \@flag_names; - $vars->{ranges} = \@ranges; - $vars->{default_query} = $input->{q}; - foreach my $field (qw(product flags range)) { - $vars->{$field} = $input->{$field}; - } -} - -sub _parse_query { - my $q = shift; - my @query = split(/:/, $q); - my $query; - - # field_id for flag changes - $query->{field_id} = get_field_id('flagtypes.name'); - - # flag_name - my $flag_name = shift @query; - @{Bugzilla::FlagType::match({ name => $flag_name, is_active => 1 })} - or ThrowUserError('report_invalid_parameter', { name => 'flag_name' }); - trick_taint($flag_name); - $query->{flag_name} = $flag_name; - - # flag_status - my $flag_status = shift @query; - $flag_status =~ /^([\?\-\+])$/ - or ThrowUserError('report_invalid_parameter', { name => 'flag_status' }); - $query->{flag_status} = $1; - - # date_range -> from_ymd to_ymd - my $date_range = shift @query; - $date_range =~ /^(\d\d\d\d)(\d\d)(\d\d)-(\d\d\d\d)(\d\d)(\d\d)$/ - or ThrowUserError('report_invalid_parameter', { name => 'date_range' }); - $query->{start_date} = "$1-$2-$3"; - $query->{end_date} = "$4-$5-$6"; - - # product_id - my $product_id = shift @query; - $product_id =~ /^(\d+)$/ - or ThrowUserError('report_invalid_parameter', { name => 'product_id' }); - $query->{product_id} = $1; - - # join - my $join = shift @query; - $join =~ /^(and|or)$/ - or ThrowUserError('report_invalid_parameter', { name => 'join' }); - $query->{join} = $1; - - # fields - my @fields; - foreach my $field (@query) { - $field =~ /^(\d+)([\-\+])$/ - or ThrowUserError('report_invalid_parameter', { name => 'fields' }); - my ($id, $value) = ($1, $2); - my $field_obj = Bugzilla::Field->new($id) - or ThrowUserError('report_invalid_parameter', { name => 'field_id' }); - push @fields, { id => $id, value => $value, name => $field_obj->name }; - } - $query->{fields} = \@fields; - - return $query; -} - -sub _is_active_status_field { - my ($field_name) = @_; - if ($field_name =~ /^cf_status/) { - return !grep { $field_name eq $_ } @$cf_disabled_flags - } - return 0; -} - 1; diff --git a/extensions/BMO/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/BMO/template/en/default/hook/global/user-error-errors.html.tmpl index eff0e35cc..85881aca7 100644 --- a/extensions/BMO/template/en/default/hook/global/user-error-errors.html.tmpl +++ b/extensions/BMO/template/en/default/hook/global/user-error-errors.html.tmpl @@ -22,15 +22,19 @@ [% title = "Missing Username" %] You must provide at least one email address to report on. -[% ELSIF error == "report_invalid_date" %] +[% ELSIF error == "user_activity_missing_from_date" %] + [% title = "Missing Date" %] + You must provided the period start date. + +[% ELSIF error == "user_activity_missing_to_date" %] + [% title = "Missing Date" %] + You must provided the period end date. + +[% ELSIF error == "user_activity_invalid_date" %] [% title = "Invalid Date" %] The date '[% date FILTER html %]' is invalid. -[% ELSIF error == "report_invalid_parameter" %] - [% title = "Invalid Parameter" %] - The value for parameter [% name FILTER html %] is invalid. - [% ELSIF error == "invalid_object" %] - Invalid [% object FILTER html %]: "[% value FILTER html %]" + Invalid [% object FILTER html %]: "[% value FILTER html %]" [% END %] diff --git a/extensions/BMO/template/en/default/hook/reports/menu-end.html.tmpl b/extensions/BMO/template/en/default/hook/reports/menu-end.html.tmpl index 7c2b2d753..846f12767 100644 --- a/extensions/BMO/template/en/default/hook/reports/menu-end.html.tmpl +++ b/extensions/BMO/template/en/default/hook/reports/menu-end.html.tmpl @@ -31,11 +31,6 @@ %]page.cgi?id=triage_reports.html">Triage Report</a></strong> - Report on UNCONFIRMED [% terms.bugs %] to assist triage. </li> - <li> - <strong><a href="[% urlbase FILTER none - %]page.cgi?id=release_tracking_report.html">Release Tracking Report</a></strong> - - For triaging release-train flag information. - </li> [% IF user.in_group('editusers') %] <li> <strong><a href="[% urlbase FILTER none diff --git a/extensions/BMO/template/en/default/pages/release_tracking_report.html.tmpl b/extensions/BMO/template/en/default/pages/release_tracking_report.html.tmpl deleted file mode 100644 index 5e64c6b5e..000000000 --- a/extensions/BMO/template/en/default/pages/release_tracking_report.html.tmpl +++ /dev/null @@ -1,98 +0,0 @@ -[%# This Source Code Form is subject to the terms of the Mozilla Public - # License, v. 2.0. If a copy of the MPL was not distributed with this - # file, You can obtain one at http://mozilla.org/MPL/2.0/. - # - # This Source Code Form is "Incompatible With Secondary Licenses", as - # defined by the Mozilla Public License, v. 2.0. - #%] - -[% INCLUDE global/header.html.tmpl - title = "Release Tracking Report" - style_urls = [ "extensions/BMO/web/styles/reports.css" ] - javascript_urls = [ "extensions/BMO/web/js/release_tracking_report.js" ] -%] - -<noscript> -<h1>JavaScript is required to use this report.</h1> -</noscript> - -<script> -var flags_data = [% flags_json FILTER none %]; -var products_data = [% products_json FILTER none %]; -var fields_data = [% fields_json FILTER none %]; -var default_query = '[% default_query FILTER js %]'; -</script> - -<form action="page.cgi" method="get" onSubmit="return onFormSubmit()"> -<input type="hidden" name="id" value="release_tracking_report.html"> -<input type="hidden" name="q" id="q" value=""> -<table> - -<tr> - <th>Approval:</th> - <td> - Show bugs where - <select id="flag" onChange="onFlagChange()"> - [% FOREACH flag_name = flag_names %] - <option value="[% flag_name FILTER html %]">[% flag_name FILTER html %]</option> - [% END %] - </select> - - was changed to (and is currently) - <select id="flag_value"> - <option value="?">?</option> - <option value="-">-</option> - <option value="+">+</option> - </select> - - between - <select id="range" onChange="serialiseForm()"> - [% FOREACH range = ranges %] - <option value="[% range.value FILTER html %]"> - [% range.label FILTER html %] - </option> - [% END %] - </select> - </td> -</tr> - -<tr> - <th>Status:</th> - <td> - for the product - <select id="product" onChange="onProductChange()"> - </select> - </td> -</tr> - -<tr> - <td> </td> - <td> - <select id="op" onChange="serialiseForm()"> - <option value="and">All selected tracking fields (AND)</option> - <option value="or">Any selected tracking fields (OR)</option> - </select> - [ - <a href="javascript:void(0)" onClick="selectAllFields()">All</a> | - <a href="javascript:void(0)" onClick="selectNoFields()">None</a> - ] - [ - <a href="javascript:void(0)" onClick="invertFields()">Invert</a> - ] - <br> - <span id="tracking_span"> - </span> - </td> -</tr> - -<tr> - <td> </td> - <td colspan="2"> - <input type="submit" value="Search"> - <input type="submit" value="Reset" onClick="onFormReset(); return false"> - </td> -</tr> -</table> -</form> - -[% INCLUDE global/footer.html.tmpl %] diff --git a/extensions/BMO/web/js/release_tracking_report.js b/extensions/BMO/web/js/release_tracking_report.js deleted file mode 100644 index 69bfab000..000000000 --- a/extensions/BMO/web/js/release_tracking_report.js +++ /dev/null @@ -1,206 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - * - * This Source Code Form is "Incompatible With Secondary Licenses", as - * defined by the Mozilla Public License, v. 2.0. */ - -var Dom = YAHOO.util.Dom; -var flagEl; -var productEl; -var trackingEl; -var selectedFields; - -// events - -function onFieldToggle(cbEl, id) { - if (cbEl.checked) { - Dom.removeClass('field_' + id + '_td', 'disabled'); - selectedFields['field_' + id] = id; - } else { - Dom.addClass('field_' + id + '_td', 'disabled'); - selectedFields['field_' + id] = false; - } - Dom.get('field_' + id + '_select').disabled = !cbEl.checked; - serialiseForm(); -} - -function onProductChange() { - var product = productEl.value; - var productData = product == '0' ? getFlagByName(flagEl.value) : getProductById(product); - var html = ''; - selectedFields = new Array(); - - if (productData) { - // update status fields - html = '<table>'; - for(var i = 0, l = productData.fields.length; i < l; i++) { - var field = getFieldById(productData.fields[i]); - selectedFields['field_' + field.id] = false; - html += '<tr>' + - '<td>' + - '<input type="checkbox" id="field_' + field.id + '_cb" ' + - 'onClick="onFieldToggle(this,' + field.id + ')">' + - '</td>' + - '<td class="disabled" id="field_' + field.id + '_td">' + - '<label for="field_' + field.id + '_cb">' + - YAHOO.lang.escapeHTML(field.name) + ':</label>' + - '</td>' + - '<td>' + - '<select disabled id="field_' + field.id + '_select">' + - '<option value="+">fixed</option>' + - '<option value="-">not fixed</option>' + - '</select>' + - '</td>' + - '</tr>'; - } - html += '</table>'; - } - trackingEl.innerHTML = html; - serialiseForm(); -} - -function onFlagChange() { - var flag = flagEl.value; - var flagData = getFlagByName(flag); - productEl.options.length = 0; - - if (flagData) { - // update product select - var currentProduct = productEl.value; - productEl.options[0] = new Option('(Any Product)', '0'); - for(var i = 0, l = flagData.products.length; i < l; i++) { - var product = getProductById(flagData.products[i]); - var n = productEl.length; - productEl.options[n] = new Option(product.name, product.id); - productEl.options[n].selected = product.id == currentProduct; - } - } - onProductChange(); -} - -// form - -function selectAllFields() { - for(var i = 0, l = fields_data.length; i < l; i++) { - var cb = Dom.get('field_' + fields_data[i].id + '_cb'); - cb.checked = true; - onFieldToggle(cb, fields_data[i].id); - } - serialiseForm(); -} - -function selectNoFields() { - for(var i = 0, l = fields_data.length; i < l; i++) { - var cb = Dom.get('field_' + fields_data[i].id + '_cb'); - cb.checked = false; - onFieldToggle(cb, fields_data[i].id); - } - serialiseForm(); -} - -function invertFields() { - for(var i = 0, l = fields_data.length; i < l; i++) { - var el = Dom.get('field_' + fields_data[i].id + '_select'); - if (el.value == '+') { - el.options[1].selected = true; - } else { - el.options[0].selected = true; - } - } - serialiseForm(); -} - -function onFormSubmit() { - serialiseForm(); - return true; -} - -function onFormReset() { - deserialiseForm(''); -} - -function serialiseForm() { - var q = flagEl.value + ':' + - Dom.get('flag_value').value + ':' + - Dom.get('range').value + ':' + - productEl.value + ':' + - Dom.get('op').value + ':'; - - for(var id in selectedFields) { - if (selectedFields[id]) { - q += selectedFields[id] + Dom.get(id + '_select').value + ':'; - } - } - - Dom.get('q').value = q; - - if(history && history.replaceState ) { - history.replaceState(null, '', - 'page.cgi?id=release_tracking_report.html&q=' + encodeURIComponent(q)); - } -} - -function deserialiseForm(q) { - var parts = q.split(/:/); - selectValue(flagEl, parts[0]); - onFlagChange(); - selectValue(Dom.get('flag_value'), parts[1]); - selectValue(Dom.get('range'), parts[2]); - selectValue(productEl, parts[3]); - onProductChange(); - selectValue(Dom.get('op'), parts[4]); - for(var i = 5, l = parts.length; i < l; i++) { - var part = parts[i]; - if (part.length) { - var value = part.substr(part.length - 1, 1); - var id = part.substr(0, part.length - 1); - var cb = Dom.get('field_' + id + '_cb'); - cb.checked = true; - onFieldToggle(cb, id); - selectValue(Dom.get('field_' + id + '_select'), value); - } - } - serialiseForm(); -} - -// utils - -YAHOO.util.Event.onDOMReady(function() { - flagEl = Dom.get('flag'); - productEl = Dom.get('product'); - trackingEl = Dom.get('tracking_span'); - onFlagChange(); - deserialiseForm(default_query); -}); - -function getFlagByName(name) { - for(var i = 0, l = flags_data.length; i < l; i++) { - if (flags_data[i].name == name) - return flags_data[i]; - } -} - -function getProductById(id) { - for(var i = 0, l = products_data.length; i < l; i++) { - if (products_data[i].id == id) - return products_data[i]; - } -} - -function getFieldById(id) { - for(var i = 0, l = fields_data.length; i < l; i++) { - if (fields_data[i].id == id) - return fields_data[i]; - } -} - -function selectValue(el, value) { - for(var i = 0, l = el.options.length; i < l; i++) { - if (el.options[i].value == value) { - el.options[i].selected = true; - return; - } - } - el.options[0].selected = true; -} diff --git a/extensions/BMO/web/styles/reports.css b/extensions/BMO/web/styles/reports.css index f75f72b8b..260a95471 100644 --- a/extensions/BMO/web/styles/reports.css +++ b/extensions/BMO/web/styles/reports.css @@ -35,7 +35,3 @@ #report tr:hover { background-color: #ccccff; } - -.disabled { - color: #888888; -} |