]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 514970: Clean up duplicates.cgi and make it use Bug objects
authorMax Kanat-Alexander <mkanat@bugzilla.org>
Mon, 1 Feb 2010 23:52:24 +0000 (15:52 -0800)
committerMax Kanat-Alexander <mkanat@bugzilla.org>
Mon, 1 Feb 2010 23:52:24 +0000 (15:52 -0800)
r=LpSolit, a=LpSolit

Bugzilla/Object.pm
Bugzilla/Product.pm
Bugzilla/User.pm
Bugzilla/WebService/Bug.pm
duplicates.cgi
skins/standard/duplicates.css
template/en/default/filterexceptions.pl
template/en/default/global/user-error.html.tmpl
template/en/default/reports/duplicates-simple.html.tmpl
template/en/default/reports/duplicates-table.html.tmpl
template/en/default/reports/duplicates.html.tmpl

index 92353b6a08b18034249c806af3eee4deba5d022c..4ee362945a27422f66b0dead4db99c1bacead3a9 100644 (file)
@@ -134,7 +134,8 @@ sub check {
         # We don't want to override the normal template "user" object if
         # "user" is one of the params.
         delete $param->{user};
-        ThrowUserError('object_does_not_exist', { %$param, class => $class });
+        my $error = delete $param->{_error} || 'object_does_not_exist';
+        ThrowUserError($error, { %$param, class => $class });
     }
     return $obj;
 }
index 0228aca02a07bb27843192ebe2242e59257820cd..c993905db9508557723fb1c018f2ca67e7182e4d 100644 (file)
@@ -913,6 +913,17 @@ sub check_product {
     return $product;
 }
 
+sub check {
+    my ($class, $params) = @_;
+    $params = { name => $params } if !ref $params;
+    $params->{_error} = 'product_access_denied';
+    my $product = $class->SUPER::check($params);
+    if (!Bugzilla->user->can_access_product($product)) {
+        ThrowUserError('product_access_denied', $params);
+    }
+    return $product;
+}
+
 1;
 
 __END__
index 59324383f86698ecc7043726076173e5391dcd81..06c6be5cb59b64bba5a3f621f964a8fd8e05503b 100644 (file)
@@ -810,8 +810,8 @@ sub get_enterable_products {
 }
 
 sub can_access_product {
-    my ($self, $product_name) = @_;
-
+    my ($self, $product) = @_;
+    my $product_name = blessed($product) ? $product->name : $product;
     return scalar(grep {$_->name eq $product_name} @{$self->get_accessible_products});
 }
 
@@ -2055,10 +2055,11 @@ the database again. Used mostly by L<Bugzilla::Product>.
 
  Returns:     an array of product objects.
 
-=item C<can_access_product(product_name)>
+=item C<can_access_product($product)>
 
-Returns 1 if the user can search or enter bugs into the specified product,
-and 0 if the user should not be aware of the existence of the product.
+Returns 1 if the user can search or enter bugs into the specified product
+(either a L<Bugzilla::Product> or a product name), and 0 if the user should
+not be aware of the existence of the product.
 
 =item C<get_accessible_products>
 
index 6051a1d1ccbdc6ad6f2587db10e811963c3403a4..53f3255d19b3ea02dbb39dfdaf6d70e7609b4801 100644 (file)
@@ -434,7 +434,7 @@ sub legal_values {
         defined $id || ThrowCodeError('param_required',
             { function => 'Bug.legal_values', param => 'product_id' });
         grep($_->id eq $id, @{Bugzilla->user->get_accessible_products})
-            || ThrowUserError('product_access_denied', { product => $id });
+            || ThrowUserError('product_access_denied', { id => $id });
 
         my $product = new Bugzilla::Product($id);
         my @objects;
index 4c050986470723c38dc1c9a8260810f636f0e86f..cbc991b7d1108504c3d027a4570336b0f6911d6a 100755 (executable)
@@ -33,6 +33,24 @@ use Bugzilla::Search;
 use Bugzilla::Field;
 use Bugzilla::Product;
 
+use constant DEFAULTS => {
+    # We want to show bugs which:
+    # a) Aren't CLOSED; and
+    # b)  i) Aren't VERIFIED; OR
+    #    ii) Were resolved INVALID/WONTFIX
+    #
+    # The rationale behind this is that people will eventually stop
+    # reporting fixed bugs when they get newer versions of the software,
+    # but if the bug is determined to be erroneous, people will still
+    # keep reporting it, so we do need to show it here.
+    fully_exclude_status  => ['CLOSED'],
+    partly_exclude_status => ['VERIFIED'],
+    except_resolution => ['INVALID', 'WONTFIX'],
+    changedsince => 7,
+    maxrows      => 20,
+    sortby       => 'count',
+};
+
 ###############
 # Subroutines #
 ###############
@@ -55,8 +73,13 @@ sub add_indirect_dups {
 sub walk_dup_chain {
     my ($dups, $from_id) = @_;
     my $to_id = $dups->{$from_id};
+    my %seen;
     while (my $bug_id = $dups->{$to_id}) {
-        last if $bug_id == $from_id; # avoid duplicate loops
+        if ($seen{$bug_id}) {
+            warn "Duplicate loop: $to_id -> $bug_id\n";
+            last;
+        }
+        $seen{$bug_id} = 1;
         $to_id = $bug_id;
     }
     # Optimize for future calls to add_indirect_dups.
@@ -64,41 +87,70 @@ sub walk_dup_chain {
     return $to_id;
 }
 
+# Get params from URL
+sub formvalue {
+    my ($name) = (@_);
+    my $cgi = Bugzilla->cgi;
+    if (defined $cgi->param($name)) {
+        return $cgi->param($name);
+    }
+    elsif (exists DEFAULTS->{$name}) {
+        return ref DEFAULTS->{$name} ? @{ DEFAULTS->{$name} } 
+                                     : DEFAULTS->{$name};
+    }
+    return undef;
+}
+
+sub sort_duplicates {
+    my ($a, $b, $sort_by) = @_;
+    if ($sort_by eq 'count' or $sort_by eq 'delta') {
+        return $a->{$sort_by} <=> $b->{$sort_by};
+    }
+    if ($sort_by =~ /^(bug_)?id$/) {
+        return $a->{'bug'}->$sort_by <=> $b->{'bug'}->$sort_by;
+    }
+    return $a->{'bug'}->$sort_by cmp $b->{'bug'}->$sort_by;
+    
+}
+
 ###############
 # Main Script #
 ###############
 
 my $cgi = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = {};
-
-Bugzilla->login();
+my $user = Bugzilla->login();
 
 my $dbh = Bugzilla->switch_to_shadow_db();
 
-# Get params from URL
-sub formvalue {
-    my ($name, $default) = (@_);
-    return Bugzilla->cgi->param($name) || $default || "";
-}
-
-my $sortby = formvalue("sortby");
-my $changedsince = formvalue("changedsince", 7);
-my $maxrows = formvalue("maxrows", 100);
+my $changedsince = formvalue("changedsince");
+my $maxrows = formvalue("maxrows");
 my $openonly = formvalue("openonly");
-my $reverse = formvalue("reverse") ? 1 : 0;
+my $sortby = formvalue("sortby");
+if (!grep(lc($_) eq lc($sortby), qw(count delta id))) {
+    Bugzilla::Field->check($sortby);
+}
+my $reverse = formvalue("reverse");
+# Reverse count and delta by default.
+if (!defined $reverse) {
+    if ($sortby eq 'count' or $sortby eq 'delta') {
+        $reverse = 1;
+    }
+    else {
+        $reverse = 0;
+    }
+}
 my @query_products = $cgi->param('product');
 my $sortvisible = formvalue("sortvisible");
-my @buglist = (split(/[:,]/, formvalue("bug_id")));
-detaint_natural($_) foreach @buglist;
-# If we got any non-numeric items, they will now be undef. Remove them from
-# the list.
-@buglist = grep($_, @buglist);
+my @bugs;
+if ($sortvisible) {
+    my @limit_to_ids = (split(/[:,]/, formvalue("bug_id") || ''));
+    @bugs = @{ Bugzilla::Bug->new_from_list(\@limit_to_ids) };
+    @bugs = @{ $user->visible_bugs(\@bugs) };
+}
 
 # Make sure all products are valid.
-foreach my $p (@query_products) {
-    Bugzilla::Product::check_product($p);
-}
+@query_products = map { Bugzilla::Product->check($_) } @query_products;
 
 # Small backwards-compatibility hack, dated 2002-04-10.
 $sortby = "count" if $sortby eq "dup_count";
@@ -112,7 +164,6 @@ detaint_natural($changedsince)
   || ThrowUserError("invalid_changedsince", 
                     { changedsince => $origchangedsince });
 
-
 my %total_dups = @{$dbh->selectcol_arrayref(
     "SELECT dupe_of, COUNT(dupe)
        FROM duplicates
@@ -136,117 +187,76 @@ my %since_dups = @{$dbh->selectcol_arrayref(
     $reso_field_id, $changedsince)};
 add_indirect_dups(\%since_dups, \%dupe_relation);
 
-my (@bugs, @bug_ids);
-
+# Enforce the mostfreqthreshold parameter and the "bug_id" cgi param.
 foreach my $id (keys %total_dups) {
     if ($total_dups{$id} < Bugzilla->params->{'mostfreqthreshold'}) {
         delete $total_dups{$id};
         next;
     }
-    if ($sortvisible and @buglist and !grep($_ == $id, @buglist)) {
+    if ($sortvisible and !grep($_->id == $id, @bugs)) {
         delete $total_dups{$id};
     }
 }
 
-if (scalar %total_dups) {
-    # use Bugzilla::Search so that we get the security checking
-    my $params = new Bugzilla::CGI({ 'bug_id' => [keys %total_dups] });
-
-    if ($openonly) {
-        $params->param('resolution', '---');
-    } else {
-        # We want to show bugs which:
-        # a) Aren't CLOSED; and
-        # b)  i) Aren't VERIFIED; OR
-        #    ii) Were resolved INVALID/WONTFIX
-
-        # The rationale behind this is that people will eventually stop
-        # reporting fixed bugs when they get newer versions of the software,
-        # but if the bug is determined to be erroneous, people will still
-        # keep reporting it, so we do need to show it here.
-
-        # a)
-        $params->param('field0-0-0', 'bug_status');
-        $params->param('type0-0-0', 'notequals');
-        $params->param('value0-0-0', 'CLOSED');
-
-        # b) i)
-        $params->param('field0-1-0', 'bug_status');
-        $params->param('type0-1-0', 'notequals');
-        $params->param('value0-1-0', 'VERIFIED');
-
-        # b) ii)
-        $params->param('field0-1-1', 'resolution');
-        $params->param('type0-1-1', 'anyexact');
-        $params->param('value0-1-1', 'INVALID,WONTFIX');
-    }
+if (!@bugs) {
+    @bugs = @{ Bugzilla::Bug->new_from_list([keys %total_dups]) };
+    @bugs = @{ $user->visible_bugs(\@bugs) };
+}
 
-    # Restrict to product if requested
-    if ($cgi->param('product')) {
-        $params->param('product', join(',', @query_products));
+my @fully_exclude_status = formvalue('fully_exclude_status');
+my @partly_exclude_status = formvalue('partly_exclude_status');
+my @except_resolution = formvalue('except_resolution');
+
+# Filter bugs by criteria
+my @result_bugs;
+foreach my $bug (@bugs) {
+    # It's possible, if somebody specified a bug ID that wasn't a dup
+    # in the "buglist" parameter and specified $sortvisible that there
+    # would be bugs in the list with 0 dups, so we want to avoid that.
+    next if !$total_dups{$bug->id};
+
+    next if ($openonly and !$bug->isopened);
+    # If the bug has a status in @fully_exclude_status, we skip it,
+    # no question.
+    next if grep($_ eq $bug->bug_status, @fully_exclude_status);
+    # If the bug has a status in @partly_exclude_status, we skip it...
+    if (grep($_ eq $bug->bug_status, @partly_exclude_status)) {
+        # ...unless it has a resolution in @except_resolution.
+        next if !grep($_ eq $bug->resolution, @except_resolution);
     }
 
-    my $query = new Bugzilla::Search('fields' => [qw(bug_id
-                                                     component
-                                                     bug_severity
-                                                     op_sys
-                                                     target_milestone
-                                                     short_desc
-                                                     bug_status
-                                                     resolution
-                                                    )
-                                                 ],
-                                     'params' => $params,
-                                    );
-
-    my $results = $dbh->selectall_arrayref($query->getSQL());
-
-    foreach my $result (@$results) {
-        # Note: maximum row count is dealt with in the template.
-
-        my ($id, $component, $bug_severity, $op_sys, $target_milestone, 
-            $short_desc, $bug_status, $resolution) = @$result;
-
-        push (@bugs, { id => $id,
-                       count => $total_dups{$id},
-                       delta => $since_dups{$id} || 0, 
-                       component => $component,
-                       bug_severity => $bug_severity,
-                       op_sys => $op_sys,
-                       target_milestone => $target_milestone,
-                       short_desc => $short_desc,
-                       bug_status => $bug_status, 
-                       resolution => $resolution });
-        push (@bug_ids, $id); 
+    if (scalar @query_products) {
+        next if !grep($_->id == $bug->product_id, @query_products);
     }
-}
 
-$vars->{'bugs'} = \@bugs;
-$vars->{'bug_ids'} = \@bug_ids;
-
-$vars->{'sortby'} = $sortby;
-$vars->{'sortvisible'} = $sortvisible;
-$vars->{'changedsince'} = $changedsince;
-$vars->{'maxrows'} = $maxrows;
-$vars->{'openonly'} = $openonly;
-$vars->{'reverse'} = $reverse;
-$vars->{'format'} = $cgi->param('format');
-$vars->{'query_products'} = \@query_products;
-$vars->{'products'} = Bugzilla->user->get_selectable_products;
-
-
-my $format = $template->get_format("reports/duplicates",
-                                   scalar($cgi->param('format')),
-                                   scalar($cgi->param('ctype')));
-
-# We set the charset in Bugzilla::CGI, but CGI.pm ignores it unless the
-# Content-Type is a text type. In some cases, such as when we are
-# generating RDF, it isn't, so we specify the charset again here.
-print $cgi->header(
-    -type => $format->{'ctype'},
-    (Bugzilla->params->{'utf8'} ? ('charset', 'utf8') : () )
+    # Note: maximum row count is dealt with later.
+    push (@result_bugs, { bug => $bug,
+                          count => $total_dups{$bug->id},
+                          delta => $since_dups{$bug->id} || 0 });
+}
+@bugs = @result_bugs;
+@bugs = sort { sort_duplicates($a, $b, $sortby) } @bugs;
+if ($reverse) {
+    @bugs = reverse @bugs;
+}
+@bugs = @bugs[0..$maxrows-1] if scalar(@bugs) > $maxrows;
+
+my %vars = (
+    bugs     => \@bugs,
+    bug_ids  => [map { $_->{'bug'}->id } @bugs],
+    sortby   => $sortby,
+    openonly => $openonly,
+    maxrows  => $maxrows,
+    reverse  => $reverse,
+    format   => scalar $cgi->param('format'),
+    product  => [map { $_->name } @query_products],
+    sortvisible  => $sortvisible,
+    changedsince => $changedsince,
 );
 
+my $format = $template->get_format("reports/duplicates", $vars{'format'});
+print $cgi->header;
+
 # Generate and return the UI (HTML page) from the appropriate template.
-$template->process($format->{'template'}, $vars)
+$template->process($format->{'template'}, \%vars)
   || ThrowTemplateError($template->error());
index 9948b789ed5e2ab123171466434b658f368083f5..e89b72c59e1aec35e60cdf979baead7420df1aa1 100644 (file)
  * 
  * The Original Code is the Bugzilla Bug Tracking System.
  * 
- * The Initial Developer of the Original Code is Netscape Communications
- * Corporation. Portions created by Netscape are
- * Copyright (C) 1998 Netscape Communications Corporation. All
- * Rights Reserved.
+ * The Initial Developer of the Original Code is Everything Solved, Inc.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
  * 
- * Contributor(s): Myk Melez <myk@mozilla.org>
+ * Contributor(s):
+ *   Max Kanat-Alexander <mkanat@bugzilla.org>
  */
 
-tree#results-tree {
-  margin-right: 0px;
-  border-right-width: 0px;
-  margin-left: 0px;
-  border-left-width: 0px;
+#duplicates_table {
+  border-collapse: collapse;
 }
 
-treechildren:-moz-tree-cell-text(resolution-FIXED) { 
-  text-decoration: line-through;
+#duplicates_table .resolved {
+  background-color: #d9d9d9; 
+  color: black;
 }
 
-treecol#id_column { width: 6em; }
-treecol#duplicate_count_column { width: 5em; }
-treecol#duplicate_delta_column { width: 5em; }
+#duplicates_table thead tr {
+  background-color: #ccc;
+  color: black;
+}
+
+#duplicates_table thead tr th {
+  vertical-align: middle;
+}
+
+#duplicates_table td, #duplicates_table th {
+  border: 1px solid black;
+  padding: .1em .25em;
+}
+
+#duplicates_table tbody td {
+  text-align: center;
+}
+#duplicates_table tbody td.short_desc {
+  text-align: left;
+}
index c4d0c064fb83b1142e63149ccc0d7f355b0a4336..2c096df73a5d4d197388e745633bac4181d63239 100644 (file)
   'request.attach_id', 
 ],
 
-'reports/duplicates-table.html.tmpl' => [
-  'column.name', 
-  'column.description',
-  'bug.count', 
-  'bug.delta', 
-],
-
-'reports/duplicates.html.tmpl' => [
-  'bug_ids_string', 
-  'maxrows',
-  'changedsince',
-  'reverse',
-],
-
 'reports/keywords.html.tmpl' => [
   'keyword.bug_count', 
 ],
index 467d4a174c6f1daa6e342a75816cbbca89e9802f..79faabbfde6c930ae4025b3d77a1419061846bf2 100644 (file)
     Try splitting your patch into several pieces.
 
   [% ELSIF error == "product_access_denied" %]
-    Either the product '[% product FILTER html %]' does not exist or
-    you don't have access to it.
+    Either the product 
+    [%+ IF id.defined %]
+      with the id [% id FILTER html %]
+    [% ELSE %]
+      '[% name FILTER html %]'
+    [% END %]
+    does not exist or you don't have access to it.
 
   [% ELSIF error == "product_doesnt_exist" %]
     [% title = "Specified Product Does Not Exist" %]
index 61d0c6fd253c6becf6a0513daf4add2aaf460991..bef002193871d8709817a47fa0532e5a35a7afcd 100644 (file)
@@ -15,7 +15,9 @@
   # Copyright (C) 1998 Netscape Communications Corporation. All
   # Rights Reserved.
   #
-  # Contributor(s): Gervase Markham <gerv@gerv.net>
+  # Contributor(s): 
+  #  Gervase Markham <gerv@gerv.net>
+  #  Max Kanat-Alexander <mkanat@bugzilla.org>
   #%]
 
 [%# INTERFACE:
@@ -24,8 +26,9 @@
 
 [% PROCESS global/variables.none.tmpl %]
 
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+                      "http://www.w3.org/TR/html4/loose.dtd">
 <html>
-
   [% IF product %]
     [% title = "Most Frequently Reported $terms.Bugs for $product" %]
   [% ELSE %]
@@ -39,5 +42,4 @@
   <body>
     [% PROCESS "reports/duplicates-table.html.tmpl" %]
   </body>
-
 </html>
index 8950f340ab6eeb9b9ae872d7ef7e3f3de13a9b4d..38ab2d56b4b7a511040fea48cc2e284c6ced3224 100644 (file)
   # Copyright (C) 1998 Netscape Communications Corporation. All
   # Rights Reserved.
   #
-  # Contributor(s): Gervase Markham <gerv@gerv.net>
+  # Contributor(s): 
+  #   Gervase Markham <gerv@gerv.net>
+  #   Max Kanat-Alexander <mkanat@bugzilla.org>
   #%]
 
 [%# INTERFACE:
-  # bugs: list of hashes. May be empty. Each hash has nine members:
-  #   id: integer. The bug number
+  # bugs: list of hashes. May be empty. Each hash has three members:
+  #   bug: A Bugzilla::Bug object
   #   count: integer. The number of dupes
   #   delta: integer. The change in count in the last $changedsince days
-  #   component: string. The bug's component
-  #   bug_severity: string. The bug's severity.
-  #   op_sys: string. The bug's reported OS.
-  #   target_milestone: string. The bug's TM.
-  #   short_desc: string. The bug's summary.
-  #   bug_status: string. The bug's status.
-  #   resolution: string. The bug's resolution, if any.
   #
   # bug_ids: list of integers. May be empty. The IDs of the bugs in $bugs.
   #
   # maxrows: integer. Max number of rows to display.
   # changedsince: integer. The number of days ago for the changedsince column.
   # openonly: boolean. True if we are only showing open bugs.
-  # query_products: list of strings. Restrict to these products only.
+  # product: array of strings. Restrict to these products only.
   #%]
 
-[% PROCESS global/variables.none.tmpl %]
+[% PROCESS "global/field-descs.none.tmpl" %]
 
 [%# *** Column Headers *** %]
 
-[% IF bug_ids.size > 0 %]
-  <table border>
-    <thead>
-      <tr bgcolor="#CCCCCC">
-        [% FOREACH column = [ { name => "id", description => "$terms.Bug #" },
-                              { name => "count", description => "Dupe<br>Count" },
-                              { name => "delta",
-                                description => "Change in last<br>$changedsince day(s)" },
-                              { name => "component", description => "Component" },
-                              { name => "bug_severity", description => "Severity" },
-                              { name => "op_sys", description => "Op Sys" },
-                              { name => "target_milestone",
-                                description => "Target<br>Milestone" },
-                              { name => "short_desc", description => "Summary" } ]
-         %]
+[% SET columns = [
+    { name => "id", description => "$terms.Bug #" },
+    { name => "count", description => "Dupe<br>Count" },
+    { name => "delta",
+      description => "Change in last<br>$changedsince day(s)" },
+    { name => "component", description => field_descs.component },
+    { name => "bug_severity", description => field_descs.bug_severity },
+    { name => "op_sys", description => field_descs.op_sys },
+    { name => "target_milestone", description => field_descs.target_milestone },
+    { name => "short_desc", description => field_descs.short_desc },
+] %]
 
-          <th>
-            [% bug_ids_string = bug_ids.join(',') %]
-            <a href="duplicates.cgi?sortby=[% column.name %]
-              [% IF sortby == column.name %]
-                [% "&amp;reverse=1" IF NOT reverse %]
-              [% ELSE %]
-                [%-# Some columns start off reversed %]
-                [% "&amp;reverse=1" IF column.name.match('delta|count') %]
-              [% END %]
-              [% IF maxrows %]&amp;maxrows=[% maxrows FILTER html %][% END %]
-              [% IF changedsince %]&amp;changedsince=[% changedsince FILTER html %][% END %]
-              [% "&amp;openonly=1" IF openonly %]
-              [% FOREACH p = query_products %]&amp;product=[% p FILTER html %][% END %]
-              [% IF format %]&amp;format=[% format FILTER html %][% END %]
-              [% IF sortvisible %]&amp;bug_id=[% bug_ids_string FILTER html %]&amp;sortvisible=1[% END %]">
-              [% column.description %]</a>
+[% SET base_args = [] %]
+[% FOREACH param = ['maxrows', 'openonly', 'format', 'sortvisible',
+                    'changedsince', 'product'] 
+%]
+  [% NEXT IF NOT ${param}.defined %]
+  [% FOREACH value = ${param} %]
+    [% filtered_value = value FILTER url_quote %]
+    [% base_args.push("$param=$filtered_value") %]
+  [% END %]
+[% END %]
+[% IF sortvisible %]
+  [% bug_ids_string = bug_ids.nsort.join(',') FILTER url_quote %]
+  [% base_args.push("bug_id=$bug_ids_string") %]
+[% END %]
+[% base_args_string = base_args.join('&amp;') %]
+
+[% IF bugs.size %]
+  <table id="duplicates_table" cellpadding="0" cellspacing="0">
+    <thead>
+      <tr>
+        [% FOREACH column = columns %]
+          [% IF column.name == sortby %]
+            [%# We add this to the column object so it doesn't affect future 
+              # iterations of the loop.
+              #%]
+            [% column.reverse_sort = reverse ? 0 : 1 %]
+          [% END %]
+          <th class="[% column.name FILTER html %]">
+            <a href="duplicates.cgi?sortby=[% column.name FILTER url_quote %]
+                     [% IF column.reverse_sort.defined %]
+                      [%- %]&amp;reverse=[% column.reverse_sort FILTER url_quote %]
+                     [% END %]
+                     [% IF base_args_string %]
+                       [% "&amp;$base_args_string" FILTER none %]
+                     [% END %]"
+            >[% column.description FILTER none %]</a>
           </th>
         [% END %]
       </tr>
     </thead>
 
-    [% IF NOT sortby %]
-      [% sortby = "count"; reverse = "1" %]
-    [% END %]
-
-    [% IF sortby == "id" OR sortby == "count" OR sortby == "delta" %]
-      [%# Numeric sort %]
-      [% sortedbugs = bugs.nsort(sortby) %]
-    [% ELSE %]
-      [% sortedbugs = bugs.sort(sortby) %]
-    [% END %]
-
-    [% IF reverse %]
-      [% bugs = sortedbugs.reverse %]
-    [% ELSE %]
-      [% bugs = sortedbugs %]
-    [% END %]
-
     [%# *** Buglist *** %]
-    <tbody>
 
-      [%# We need to keep track of the bug IDs we are actually displaying, because
-        # if the user decides to sort the visible list, we need to know what that
-        # list actually is. %]
-      [% vis_bug_ids = [] %]
-
-      [% FOREACH bug = bugs %]
-        [% LAST IF loop.index() >= maxrows %]
-        [% vis_bug_ids.push(bug.id) %]
-
-        <tr [% "class='resolved'" IF bug.resolution != "" %]>
-          <td>
-            <center>
-              [% bug.id FILTER bug_link(bug.id) FILTER none %]
-            </center>
+    <tbody>
+      [% FOREACH item = bugs %]
+        [% SET bug = item.bug %]
+        <tr [% " class='resolved'" IF NOT bug.isopened %]>
+          <td class="id">
+            [% bug.id FILTER bug_link(bug) FILTER none %]
           </td>
-
-          <td>
-            <center>
-              [% bug.count %]
-            </center>
+          <td class="count">[% item.count FILTER html %]</td>
+          <td class="delta">[% item.delta FILTER html %]</td>
+          <td class="component">[% bug.component FILTER html %]</td>
+          <td class="bug_severity">
+            [%- display_value('bug_severity', bug.bug_severity) FILTER html %]
           </td>
-
-          <td><center>[% bug.delta %]</center></td>
-
-          <td>[% bug.component FILTER html %]</td>
-          <td><center>[% display_value("bug_severity",     bug.bug_severity    ) FILTER html %]</center></td>
-          <td><center>[% display_value("op_sys",           bug.op_sys          ) FILTER html %]</center></td>
-          <td><center>[% display_value("target_milestone", bug.target_milestone) FILTER html %]</center></td>
-          <td>[% bug.short_desc FILTER html %]</td>
+          <td class="op_sys">
+            [%- display_value('op_sys', bug.op_sys) FILTER html %]
+          </td>
+          <td class="target_milestone">
+            [% display_value('target_milestone', 
+                             bug.target_milestone) FILTER html %]
+          </td>
+          <td class="short_desc">[% bug.short_desc FILTER html %]</td>
         </tr>
       [% END %]
     </tbody>
index e4ea738826f6c722bacaf970db16d4a7a2c3db02..6b49a23c6a2bec816707747fdfbedf26023d6435 100644 (file)
   #%]
 
 [%# INTERFACE:
-  # products: an array of product objects this user can see.
-  #
   # sortby: string. the column on which we are sorting the buglist.
   # reverse: boolean. True if we are reversing the current sort.
   # maxrows: integer. Max number of rows to display.
   # changedsince: integer. The number of days ago for the changedsince column.
   # openonly: boolean. True if we are only showing open bugs.
-  # query_products: list of strings. The set of products we check for dups.
+  # product: array of strings. The set of products we check for dups.
   #
   # Additionally, you need to fulfill the interface to
   # duplicates-table.html.tmpl.
 
 [% PROCESS global/variables.none.tmpl %]
 
-[% IF query_products.size %]
+[% IF product.size %]
   [% title = BLOCK %]
-    Most Frequently Reported [% terms.Bugs %] for [% query_products.join(', ') FILTER html %]
+    Most Frequently Reported [% terms.Bugs %] for 
+    [%+ product.join(', ') FILTER html %]
   [% END %]
 [% ELSE %]
   [% title = "Most Frequently Reported $terms.Bugs" %]
@@ -44,7 +43,7 @@
 
 [% PROCESS global/header.html.tmpl
    title = title
-   style = ".resolved { background-color: #d9d9d9; color: #000000; }"
+   style_urls = ['skins/standard/duplicates.css']
 %]
 
 <p>
 
 [%# *** Parameters *** %]
 
-[% bug_ids_string = vis_bug_ids.join(',') %]
+[% bug_ids_string = bug_ids.join(',') %]
 
 <h3><a name="params">Change Parameters</a></h3>
 
 <form method="get" action="duplicates.cgi">
   <input type="hidden" name="sortby" value="[% sortby FILTER html %]">
-  <input type="hidden" name="reverse" value="[% reverse %]">
-  <input type="hidden" name="bug_id" value="[% bug_ids_string %]">
+  <input type="hidden" name="reverse" value="[% reverse FILTER html %]">
+  <input type="hidden" name="bug_id" value="[% bug_ids_string FILTER html %]">
   <table>
     <tr>
-      <td>When sorting or restricting,
-          work with:</td>
+      <td>When sorting or restricting, work with:</td>
       <td>
         <input type="radio" name="sortvisible" id="entirelist" value="0"
-          [%+ "checked" IF NOT sortvisible %]>
+          [% ' checked="checked"' IF NOT sortvisible %]>
         <label for="entirelist">
           entire list
         </label>
         <br>
         <input type="radio" name="sortvisible" id="visiblelist" value="1"
-          [%+ "checked" IF sortvisible %]>
+          [% ' checked="checked"' IF sortvisible %]>
         <label for="visiblelist">
           currently visible list
         </label>
@@ -85,9 +83,9 @@
       <td rowspan="4" valign="top">Restrict to products:</td>
       <td rowspan="4" valign="top">
         <select name="product" size="5" multiple="multiple">
-          [% FOREACH p = products %]
+          [% FOREACH p = user.get_selectable_products %]
             <option name="[% p.name FILTER html %]"
-            [% " selected" IF lsearch(query_products, p.name) != -1 %]
+            [% ' selected="selected"' IF product.contains(p.name) %]
             >[% p.name FILTER html %]</option>
           [% END %]
         </select>
      </tr>
 
     <tr>
-      <td>Max rows:</td>
+      <td><label for="maxrows">Max rows:</label></td>
       <td>
-        <input size="4" name="maxrows" value="[% maxrows %]">
+        <input size="4" name="maxrows" id="maxrows" 
+               value="[% maxrows FILTER html %]">
       </td>
     </tr>
 
     <tr>
-      <td>Change column is change in the last:</td>
       <td>
-        <input size="4" name="changedsince" value="[% changedsince %]"> days
+        <label for="changedsince">Change column is change in the last:</label>
+      </td>
+      <td>
+        <input size="4" name="changedsince" id="changedsince"
+               value="[% changedsince FILTER html %]"> days
       </td>
     </tr>
 
       </td>
       <td>
         <input type="checkbox" name="openonly" id="openonly" value="1"
-          [%+ "checked" IF openonly %]>
+          [% ' checked="checked"' IF openonly %]>
       </td>
     </tr>
 
 </form>
 
 <form method="post" action="buglist.cgi">
-  <input type="hidden" name="bug_id" value="[% bug_ids_string %]">
-  <input type="hidden" name="order" value="Reuse same sort as last time">
+  <input type="hidden" name="bug_id" value="[% bug_ids_string FILTER html %]">
   Or just give this to me as a <input type="submit" id="list" 
-                                value="[% terms.bug %] list">.
+                                      value="[% terms.bug %] list">.
   (Note: the order may not be the same.)
 </form>
 
   <a name="explanation">What are "Most Frequently Reported [% terms.Bugs %]"?</a>
 </b>
 
-<blockquote>
-  The Most Frequent [% terms.Bugs %] page lists the known open [% terms.bugs %] which
-  are reported most frequently. It is
-  automatically generated from the [% terms.Bugzilla %] database every 24 hours, by
+<p>
+  The Most Frequent [% terms.Bugs %] page lists the known open 
+  [%+ terms.bugs %] which are reported most frequently,
   counting the number of direct and indirect duplicates of [% terms.bugs %].
   This information is provided in order to assist in minimizing
-  the amount of duplicate [% terms.bugs %] entered into [% terms.Bugzilla %], which
-  saves time for Quality Assurance engineers who have to triage the [% terms.bugs %].
-</blockquote>
+  the amount of duplicate [% terms.bugs %] entered into [% terms.Bugzilla %],
+  which saves time for Quality Assurance engineers who have to triage
+  the [% terms.bugs %].
+</p>
 
 <b>How do I use this list?</b>
 
 
   <ul>
     <li><a href="query.cgi">Try and locate a similar [% terms.bug %]</a>
-        that has already been filed.</li>
+      that has already been filed.</li>
     <li>If you find your [% terms.bug %] in [% terms.Bugzilla %],
-        feel free to comment with any new or additional data you may have.</li>
-    <li>If you cannot find your problem already documented in [% terms.Bugzilla %],
-        <a href="enter_bug.cgi">file a new [% terms.bug %]</a>.</li>
+      feel free to comment with any new or additional data you may have.</li>
+    <li>If you cannot find your problem already documented in 
+      [%+ terms.Bugzilla %],
+      <a href="enter_bug.cgi">file a new [% terms.bug %]</a>.</li>
   </ul>
 </ul>