[bugzilla-gnome-org-extensions] initial version



commit 1b4fcfca484c012c2867bd9f9e7f5bbf6b4b9254
Author: Olav Vitters <olav vitters nl>
Date:   Wed Feb 29 10:12:30 2012 +0100

    initial version

 Config.pm                                      |   33 ++
 Extension.pm                                   |   44 ++
 lib/Util.pm                                    |  551 ++++++++++++++++++++++++
 template/en/default/browse/README              |   16 +
 template/en/default/hook/README                |    5 +
 template/en/default/hook/index-outro.html.tmpl |   12 +
 template/en/default/pages/browse.html.tmpl     |  473 ++++++++++++++++++++
 web/README                                     |    7 +
 web/browse.css                                 |   35 ++
 web/browse.js                                  |  116 +++++
 10 files changed, 1292 insertions(+), 0 deletions(-)
---
diff --git a/Config.pm b/Config.pm
new file mode 100644
index 0000000..71d121e
--- /dev/null
+++ b/Config.pm
@@ -0,0 +1,33 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Browse Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Olav Vitters
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Olav Vitters <olav vitters nl>
+
+package Bugzilla::Extension::Browse;
+use strict;
+
+use constant NAME => 'Browse';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/Extension.pm b/Extension.pm
new file mode 100644
index 0000000..db3df42
--- /dev/null
+++ b/Extension.pm
@@ -0,0 +1,44 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Browse Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Olav Vitters
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Olav Vitters <olav vitters nl>
+
+package Bugzilla::Extension::Browse;
+use strict;
+use base qw(Bugzilla::Extension);
+
+# This code for this is in ./extensions/Browse/lib/Util.pm
+use Bugzilla::Extension::Browse::Util;
+
+our $VERSION = '0.01';
+
+# See the documentation of Bugzilla::Hook ("perldoc Bugzilla::Hook" 
+# in the bugzilla directory) for a list of all available hooks.
+sub install_update_db {
+    my ($self, $args) = @_;
+
+}
+
+sub page_before_template {
+    my ($self, $args) = @_;
+
+    page(%{ $args });
+
+}
+__PACKAGE__->NAME;
diff --git a/lib/Util.pm b/lib/Util.pm
new file mode 100644
index 0000000..6505c69
--- /dev/null
+++ b/lib/Util.pm
@@ -0,0 +1,551 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Browse Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is YOUR NAME
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   YOUR NAME <YOUR EMAIL ADDRESS>
+
+package Bugzilla::Extension::Browse::Util;
+use strict;
+use base qw(Exporter);
+our @EXPORT = qw(
+    page
+    total_open_bugs
+    what_new_means
+    new_bugs
+    new_patches
+    keyword_bugs
+    no_response_bugs
+    critical_warning_bugs
+    string_bugs
+    by_patch_status
+    by_version
+    needinfo_split
+    by_target
+    by_priority
+    by_severity
+    by_component
+    by_assignee
+    gnome_target_development 
+    gnome_target_stable
+    list_blockers
+    browse_bug_link
+);
+
+# This file can be loaded by your extension via 
+# "use Bugzilla::Extension::Browse::Util". You can put functions
+# used by your extension in here. (Make sure you also list them in
+# @EXPORT.)
+
+use Bugzilla::Constants;
+use Bugzilla::User;
+use Bugzilla::Search;
+use Bugzilla::Field;
+use Bugzilla::Status;
+use Bugzilla::Util;
+use Bugzilla::Install::Util qw(vers_cmp);
+
+use constant IMPORTANT_PATCH_STATUSES => qw(
+    none
+    accepted-commit_now
+    accepted-commit_after_freeze
+);
+
+sub page {
+    my %params = @_;
+    my ($vars, $page) = @params{qw(vars page_id)};
+    if ($page =~ /^browse\./) {
+        _page_browse($vars);
+    }
+}
+
+sub _page_browse {
+    my $vars = shift;
+
+    my $cgi = Bugzilla->cgi;
+    my $dbh = Bugzilla->dbh;
+    my $user = Bugzilla->user;
+    my $template = Bugzilla->template;
+
+    # All pages point to the same part of the documentation.
+    $vars->{'doc_section'} = 'bugreports.html';
+
+    my $product_name = trim($cgi->param('product') || '');
+    my $product;
+
+    if (!$product_name && $cgi->cookie('DEFAULTPRODUCT')) {
+        $product_name = $cgi->cookie('DEFAULTPRODUCT')
+            if $user->can_enter_product($cgi->cookie('DEFAULTPRODUCT'));
+    }
+
+    my $product_interests = $user->product_interests();
+
+    # If the user didn't select a product and there isn't a default from a cookie,
+    # try getting the first valid product from their interest list.
+    if (!$product_name && scalar @$product_interests) {
+        foreach my $try (@$product_interests) {
+            next if !$user->can_see_product($try->name);
+            $product_name = $try->name;
+            last;
+        }
+    }
+
+    if ($product_name eq '') {
+        # If the user cannot enter bugs in any product, stop here.
+        my @enterable_products = @{$user->get_enterable_products};
+        ThrowUserError('no_products') unless scalar(@enterable_products);
+
+        my $classification = Bugzilla->params->{'useclassification'} ?
+            scalar($cgi->param('classification')) : '__all';
+
+        # Unless a real classification name is given, we sort products
+        # by classification.
+        my @classifications;
+
+        unless ($classification && $classification ne '__all') {
+            if (Bugzilla->params->{'useclassification'}) {
+                my $class;
+                # Get all classifications with at least one enterable product.
+                foreach my $product (@enterable_products) {
+                    $class->{$product->classification_id}->{'object'} ||=
+                        new Bugzilla::Classification($product->classification_id);
+                    # Nice way to group products per classification, without querying
+                    # the DB again.
+                    push(@{$class->{$product->classification_id}->{'products'}}, $product);
+                }
+                @classifications = sort {$a->{'object'}->sortkey <=> $b->{'object'}->sortkey
+                                         || lc($a->{'object'}->name) cmp lc($b->{'object'}->name)}
+                                        (values %$class);
+            }
+            else {
+                @classifications = ({object => undef, products => \ enterable_products});
+            }
+        }
+
+        unless ($classification) {
+            # We know there is at least one classification available,
+            # else we would have stopped earlier.
+            if (scalar(@classifications) > 1) {
+                # We only need classification objects.
+                $vars->{'classifications'} = [map {$_->{'object'}} @classifications];
+
+                $vars->{'target'} = "browse.cgi";
+                $vars->{'format'} = $cgi->param('format');
+
+                print $cgi->header();
+                $template->process("global/choose-classification.html.tmpl", $vars)
+                   || ThrowTemplateError($template->error());
+                exit;
+            }
+            # If we come here, then there is only one classification available.
+            $classification = $classifications[0]->{'object'}->name;
+        }
+
+        # Keep only enterable products which are in the specified classification.
+        if ($classification ne "__all") {
+            my $class = new Bugzilla::Classification({'name' => $classification});
+            # If the classification doesn't exist, then there is no product in it.
+            if ($class) {
+                @enterable_products
+                  = grep {$_->classification_id == $class->id} @enterable_products;
+                @classifications = ({object => $class, products => \ enterable_products});
+            }
+            else {
+                @enterable_products = ();
+            }
+        }
+
+        if (scalar(@enterable_products) == 0) {
+            ThrowUserError('no_products');
+        }
+        elsif (scalar(@enterable_products) > 1) {
+            $vars->{'classifications'} = \ classifications;
+            $vars->{'target'} = "browse.cgi";
+            $vars->{'format'} = $cgi->param('format');
+
+            print $cgi->header();
+            $template->process("global/choose-product.html.tmpl", $vars)
+              || ThrowTemplateError($template->error());
+            exit;
+        } else {
+            # Only one product exists.
+            $product = $enterable_products[0];
+        }
+    }
+    else {
+        # Do not use Bugzilla::Product::check_product() here, else the user
+        # could know whether the product doesn't exist or is not accessible.
+        $product = new Bugzilla::Product({'name' => $product_name});
+    }
+
+    # We need to check and make sure that the user has permission
+    # to enter a bug against this product.
+    $user->can_enter_product($product ? $product->name : $product_name, THROW_ERROR);
+
+    # Remember selected product
+    $cgi->send_cookie(-name => 'DEFAULTPRODUCT',
+                      -value => $product->name,
+                      -expires => "Fri, 01-Jan-2038 00:00:00 GMT");
+
+    # Create data structures representing each classification
+    my @classifications = (); 
+    if (scalar @$product_interests) {
+        my %watches = ( 
+            'name'     => 'Watched Products',
+            'products' => $product_interests
+        );  
+        push @classifications, \%watches;
+    }
+
+    if (Bugzilla->params->{'useclassification'}) {
+        foreach my $c (@{$user->get_selectable_classifications}) {
+            # Create hash to hold attributes for each classification.
+            my %classification = ( 
+                'name'       => $c->name, 
+                'products'   => [ @{$user->get_selectable_products($c->id)} ]
+            );  
+            # Assign hash back to classification array.
+            push @classifications, \%classification;
+        }   
+    }
+
+    $vars->{'classifications'}  = \ classifications;
+    $vars->{'product'}          = $product;
+    $vars->{'total_open_bugs'}  = total_open_bugs($product);
+    $vars->{'what_new_means'}   = what_new_means();
+    $vars->{'new_bugs'}         = new_bugs($product);
+    $vars->{'new_patches'}      = new_patches($product);
+    $vars->{'no_response_bugs'} = scalar(@{no_response_bugs($product)});
+
+    my $keyword = Bugzilla::Keyword->new({ name => 'gnome-love' });
+    if ($keyword) {
+        $vars->{'gnome_love_bugs'}  = keyword_bugs($product, $keyword);
+    }
+
+    ######################################################################
+    # Begin temporary searches; If the search will be reused again next
+    # release cycle, please just comment it out instead of deleting it.
+    ######################################################################
+
+    $vars->{'critical_warning_bugs'} = critical_warning_bugs($product);
+    #$vars->{'string_bugs'} = string_bugs($product);
+
+    ######################################################################
+    # End temporary searches
+    ######################################################################
+
+    $vars->{'by_patch_status'}    = by_patch_status($product);
+    $vars->{'buglink'}            = browse_bug_link($product);
+    $vars->{'by_version'}         = by_version($product);
+    $vars->{'by_target'}          = by_target($product);
+    $vars->{'by_priority'}        = by_priority($product);
+    $vars->{'by_severity'}        = by_severity($product);
+    $vars->{'by_component'}       = by_component($product);
+    $vars->{'target_development'} = gnome_target_development();
+    $vars->{'target_stable'}      = gnome_target_stable();
+    $vars->{'needinfo_split'}     = needinfo_split($product);
+
+    ($vars->{'blockers_stable'}, $vars->{'blockers_development'}) = list_blockers($product);
+
+    print Bugzilla->cgi->header();
+
+#    my $format = $template->get_format("browse/main",
+#                                       scalar $cgi->param('format'),
+#                                       scalar $cgi->param('ctype'));
+#     
+#    print $cgi->header($format->{'ctype'});
+#    $template->process($format->{'template'}, $vars)
+#       || ThrowTemplateError($template->error());
+}
+
+sub browse_open_states {
+    my $dbh = Bugzilla->dbh;
+    return join(",", map { $dbh->quote($_) } grep($_ ne "NEEDINFO", BUG_STATE_OPEN));
+}
+
+sub total_open_bugs {
+    my $product = shift;
+    my $dbh = Bugzilla->dbh;
+
+    return $dbh->selectrow_array("SELECT COUNT(bug_id) 
+                                    FROM bugs 
+                                   WHERE bug_status IN (" . browse_open_states() . ") 
+                                         AND product_id = ?", undef, $product->id);
+}
+
+sub what_new_means {
+    my $dbh = Bugzilla->dbh;
+    return $dbh->selectrow_array("SELECT " . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', 7, 'DAY'));
+}
+
+sub new_bugs {
+    my $product = shift;
+    my $dbh = Bugzilla->dbh;
+
+    return $dbh->selectrow_array("SELECT COUNT(bug_id) 
+                                    FROM bugs 
+                                   WHERE bug_status IN (" . browse_open_states() . ") 
+                                         AND creation_ts >= " . $dbh->sql_date_math('LOCALTIMESTAMP(0)', 
'-', 7, 'DAY') . " 
+                                         AND product_id = ?", undef, $product->id);
+}
+
+sub new_patches {
+    my $product = shift;
+    my $dbh = Bugzilla->dbh;
+
+    return $dbh->bz_column_info('attachments', 'status') ?
+           $dbh->selectrow_array("SELECT COUNT(attach_id) 
+                                    FROM bugs, attachments 
+                                   WHERE bugs.bug_id = attachments.bug_id
+                                         AND bug_status IN (" . browse_open_states() . ") 
+                                         AND attachments.ispatch = 1 AND attachments.isobsolete = 0
+                                         AND attachments.status = 'none' 
+                                         AND attachments.creation_ts >= " . 
$dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', 7, 'DAY') . " 
+                                         AND product_id = ?", undef, $product->id) :
+          "?";
+}
+
+sub keyword_bugs {
+    my ($product, $keyword) = @_;
+    my $dbh = Bugzilla->dbh;
+
+    return $dbh->selectrow_array("SELECT COUNT(bugs.bug_id) 
+                                    FROM bugs, keywords 
+                                   WHERE bugs.bug_id = keywords.bug_id 
+                                         AND bug_status IN (" . browse_open_states() . ") 
+                                         AND keywords.keywordid = ? 
+                                         AND product_id = ?", undef, ($keyword->id, $product->id));
+}
+
+sub no_response_bugs {
+    my $product = shift;
+    my $dbh = Bugzilla->dbh;
+    my @developer_ids = map { $_->id } @{$product->developers};
+
+    if (@developer_ids) {
+        return $dbh->selectcol_arrayref("SELECT bugs.bug_id
+                                           FROM bugs INNER JOIN longdescs ON longdescs.bug_id = bugs.bug_id 
+                                          WHERE bug_status IN (" . browse_open_states() . ") 
+                                                AND bug_severity != 'enhancement' 
+                                                AND product_id = ? 
+                                                AND bugs.reporter NOT IN (" . join(",", @developer_ids) . ") 
+                                          GROUP BY bugs.bug_id 
+                                         HAVING COUNT(distinct longdescs.who) = 1", undef, $product->id);
+    }
+    else {
+        return [];
+    }
+}
+
+sub critical_warning_bugs {
+    my $product = shift;
+    my $dbh = Bugzilla->dbh;
+ 
+    return $dbh->selectrow_array("SELECT COUNT(bugs.bug_id) 
+                                    FROM bugs INNER JOIN bugs_fulltext ON bugs_fulltext.bug_id = bugs.bug_id 
+                                   WHERE bug_status IN (" . browse_open_states() . ") 
+                                         AND " . 
$dbh->sql_fulltext_search("bugs_fulltext.comments_noprivate", "'+G_LOG_LEVEL_CRITICAL'") . " 
+                                         AND product_id = ?", undef, $product->id);
+}
+
+sub string_bugs {
+    my $product = shift;
+    my $dbh = Bugzilla->dbh;
+    
+    return $dbh->selectrow_array("SELECT COUNT(bugs.bug_id) 
+                                    FROM bugs, keywords, keyworddefs 
+                                   WHERE bugs.bug_id = keywords.bug_id 
+                                         AND keywords.keywordid = keyworddefs.id 
+                                         AND keyworddefs.name = 'string' 
+                                         AND bug_status IN (" . browse_open_states() . ") 
+                                         AND product_id = ?", undef, $product->id);
+}
+
+sub by_patch_status {
+    my $product = shift;
+    my $dbh = Bugzilla->dbh;
+
+    return $dbh->bz_column_info('attachments', 'status') ?
+           $dbh->selectall_arrayref("SELECT attachments.status, COUNT(attach_id) 
+                                       FROM bugs, attachments
+                                      WHERE attachments.bug_id = bugs.bug_id 
+                                            AND bug_status IN (" . browse_open_states() . ") 
+                                            AND product_id = ? 
+                                            AND attachments.ispatch = 1 
+                                            AND attachments.isobsolete != 1 
+                                            AND attachments.status IN (" . join(",", map { $dbh->quote($_) } 
IMPORTANT_PATCH_STATUSES) . ") 
+                                            GROUP BY attachments.status", undef, $product->id) :
+           "?";
+}
+
+sub browse_bug_link {
+    my $product = shift;
+
+    return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) .
+           '&bug_status=' . join(',' ,map { url_quote($_) } grep ($_ ne "NEEDINFO", BUG_STATE_OPEN));
+}
+
+sub by_version {
+    my $product = shift;
+    my $dbh = Bugzilla->dbh;
+
+    my @result = sort { vers_cmp($a->[0], $b->[0]) } 
+        @{$dbh->selectall_arrayref("SELECT version, COUNT(bug_id) 
+                                      FROM bugs 
+                                     WHERE bug_status IN (" . browse_open_states() . ") 
+                                           AND product_id = ? 
+                                     GROUP BY version", undef, $product->id)};
+    
+    return \ result;
+}
+
+sub needinfo_split {
+    my $product = shift;
+    my $dbh = Bugzilla->dbh;
+
+    my $ni_a = Bugzilla::Search::SqlifyDate('-2w');
+    my $ni_b = Bugzilla::Search::SqlifyDate('-4w');
+    my $ni_c = Bugzilla::Search::SqlifyDate('-3m');
+    my $ni_d = Bugzilla::Search::SqlifyDate('-6m');
+    my $ni_e = Bugzilla::Search::SqlifyDate('-1y');
+    my $needinfo_case = "CASE WHEN delta_ts < '$ni_e' THEN 'F'
+                              WHEN delta_ts < '$ni_d' THEN 'E'
+                              WHEN delta_ts < '$ni_c' THEN 'D'
+                              WHEN delta_ts < '$ni_b' THEN 'C'
+                              WHEN delta_ts < '$ni_a' THEN 'B'
+                              ELSE 'A' END";
+
+    my %results = @{$dbh->selectcol_arrayref("SELECT $needinfo_case age, COUNT(bug_id) 
+                                       FROM bugs 
+                                      WHERE bug_status = 'NEEDINFO' 
+                                            AND product_id = ? 
+                                      GROUP BY $needinfo_case", { Columns=>[1,2] }, $product->id)};
+    return \%results;
+}
+
+sub by_target {
+    my $product = shift;
+    my $dbh = Bugzilla->dbh;
+
+    my @result = sort { vers_cmp($a->[0], $b->[0]) } 
+        @{$dbh->selectall_arrayref("SELECT target_milestone, COUNT(bug_id) 
+                                      FROM bugs 
+                                     WHERE bug_status IN (" . browse_open_states() . ") 
+                                           AND target_milestone != '---' 
+                                           AND product_id = ? 
+                                     GROUP BY target_milestone", undef, $product->id)};
+    
+    return \ result;
+}
+
+sub by_priority {
+    my $product = shift;
+    my $dbh = Bugzilla->dbh;
+
+    my $i = 0;
+    my %order_priority = map { $_ => $i++  } @{get_legal_field_values('priority')};
+    
+    my @result = sort { $order_priority{$a->[0]} <=> $order_priority{$b->[0]} } 
+        @{$dbh->selectall_arrayref("SELECT priority, COUNT(bug_id) 
+                                      FROM bugs 
+                                     WHERE bug_status IN (" . browse_open_states() . ") 
+                                           AND product_id = ? 
+                                     GROUP BY priority", undef, $product->id)};
+
+    return \ result;
+}
+
+sub by_severity {
+    my $product = shift;
+    my $dbh = Bugzilla->dbh;
+
+    my $i = 0;
+    my %order_severity = map { $_ => $i++  } @{get_legal_field_values('bug_severity')};
+
+    my @result = sort { $order_severity{$a->[0]} <=> $order_severity{$b->[0]} } 
+        @{$dbh->selectall_arrayref("SELECT bug_severity, COUNT(bug_id) 
+                                      FROM bugs 
+                                     WHERE bug_status IN (" . browse_open_states() . ") 
+                                           AND product_id = ? 
+                                     GROUP BY bug_severity", undef, $product->id)};
+
+    return \ result;
+}
+
+sub by_component {
+    my $product = shift;
+    my $dbh = Bugzilla->dbh;
+
+    return $dbh->selectall_arrayref("SELECT components.name, COUNT(bugs.bug_id) 
+                                       FROM bugs INNER JOIN components ON bugs.component_id = components.id 
+                                      WHERE bug_status IN (" . browse_open_states() . ") 
+                                            AND bugs.product_id = ? 
+                                      GROUP BY components.name", undef, $product->id);
+}
+
+sub by_assignee {
+    my $product = shift;
+    my $dbh = Bugzilla->dbh;
+
+    my @result = map { Bugzilla::User->new($_) } 
+        @{$dbh->selectall_arrayref("SELECT bugs.assignee AS userid, COUNT(bugs.bug_id) 
+                                      FROM bugs 
+                                     WHERE bug_status IN (" . browse_open_states() . ") 
+                                           AND bugs.product_id = ? 
+                                     GROUP BY components.name", undef, $product->id)};
+    
+    return \ result;
+}
+
+sub gnome_target_development { 
+    my @legal_gnome_target = @{get_legal_field_values('cf_gnome_target')};
+    return $legal_gnome_target[(scalar @legal_gnome_target) -1];
+}
+
+sub gnome_target_stable {
+    my @legal_gnome_target = @{get_legal_field_values('cf_gnome_target')};
+    return $legal_gnome_target[(scalar @legal_gnome_target) -2];
+}
+
+sub list_blockers {
+    my $product = shift;
+    my $dbh = Bugzilla->dbh;
+
+    my $sth = $dbh->prepare("SELECT bugs.bug_id, products.name AS product, bugs.bug_status, 
+                                        bugs.resolution, bugs.bug_severity, bugs.short_desc 
+                                   FROM bugs INNER JOIN products ON bugs.product_id = products.id
+                                  WHERE product_id = ? 
+                                        AND bugs.cf_gnome_target = ? 
+                                        AND bug_status IN (" . browse_open_states() . ") 
+                                  ORDER BY bug_id DESC");
+
+    my @list_blockers_development;
+    $sth->execute($product->id, gnome_target_development());
+    while (my $bug = $sth->fetchrow_hashref) {
+        push(@list_blockers_development, $bug);
+    }
+    
+    my @list_blockers_stable;
+    $sth->execute($product->id, gnome_target_stable());
+    while (my $bug = $sth->fetchrow_hashref) {
+        push(@list_blockers_stable, $bug);
+    }
+    
+    return (\ list_blockers_stable, \ list_blockers_development);
+}
+
+1;
diff --git a/template/en/default/browse/README b/template/en/default/browse/README
new file mode 100644
index 0000000..67d120e
--- /dev/null
+++ b/template/en/default/browse/README
@@ -0,0 +1,16 @@
+Normal templates go in this directory. You can load them in your
+code like this:
+
+use Bugzilla::Error;
+my $template = Bugzilla->template;
+$template->process('browse/some-template.html.tmpl')
+  or ThrowTemplateError($template->error());
+
+That would be how to load a file called some-template.html.tmpl that
+was in this directory.
+
+Note that you have to be careful that the full path of your template
+never conflicts with a template that exists in Bugzilla or in 
+another extension, or your template might override that template. That's why
+we created this directory called 'browse' for you, so you
+can put your templates in here to help avoid conflicts.
\ No newline at end of file
diff --git a/template/en/default/hook/README b/template/en/default/hook/README
new file mode 100644
index 0000000..e6c4add
--- /dev/null
+++ b/template/en/default/hook/README
@@ -0,0 +1,5 @@
+Template hooks go in this directory. Template hooks are called in normal
+Bugzilla templates like [% Hook.process('some-hook') %].
+More information about them can be found in the documentation of 
+Bugzilla::Extension. (Do "perldoc Bugzilla::Extension" from the main
+Bugzilla directory to see that documentation.)
\ No newline at end of file
diff --git a/template/en/default/hook/index-outro.html.tmpl b/template/en/default/hook/index-outro.html.tmpl
new file mode 100644
index 0000000..30f0a4d
--- /dev/null
+++ b/template/en/default/hook/index-outro.html.tmpl
@@ -0,0 +1,12 @@
+
+  [% IF user.product_interests.size > 0 %]
+    <h3>Interested products</h3>
+
+    <ul>
+    [% FOREACH product = user.product_interests %]
+      <li><a href="browse.cgi?product=[% product.name FILTER uri %]">[% product.name
+           FILTER html %]</a></li>
+    [% END %]
+    </ul>
+  [% END %]
+
diff --git a/template/en/default/pages/browse.html.tmpl b/template/en/default/pages/browse.html.tmpl
new file mode 100644
index 0000000..31bf09e
--- /dev/null
+++ b/template/en/default/pages/browse.html.tmpl
@@ -0,0 +1,473 @@
+[%# The contents of this file are subject to the Mozilla Public
+  # License Version 1.1 (the "License"); you may not use this file
+  # except in compliance with the License. You may obtain a copy of
+  # the License at http://www.mozilla.org/MPL/
+  #
+  # Software distributed under the License is distributed on an "AS
+  # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+  # implied. See the License for the specific language governing
+  # rights and limitations under the License.
+  #
+  # 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.
+  #
+  # Contributor(s): Gervase Markham <gerv gerv net>
+  #                 Vaskin Kissoyan <vkissoyan yahoo com>
+  #                 Max Kanat-Alexander <mkanat bugzilla org>
+  #                 Frédéric Buclin <LpSolit gmail com>
+  #                 Olav Vitters <olav bkor dhs org>
+  #                 Guy Pyrzak <guy pyrzak gmail com>
+  #                 Elliotte Martin <emartin everythingsolved com>
+  #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% filtered_product = product.name FILTER html %]
+[% PROCESS global/header.html.tmpl
+  title = "Browse: $filtered_product"
+  h1 = ""
+  style_urls = [ "extensions/Browse/web/browse.css", "skins/standard/buglist.css" ]
+  javascript_urls = [ "extensions/Browse/web/browse.js" ]
+%]
+
+<div id="product_summary">
+  <h3>Project Summary</h3>
+
+  <table border="0" class="figures">
+
+  <tr>
+    <td>Total [% terms.Bugs %]</td>
+    <td align="right">
+      <a href="[% buglink FILTER html %]">
+        [%- total_open_bugs FILTER html %]</a>
+    </td>
+  </tr>
+
+  <tr>
+    <td>New [% terms.Bugs %]</td> 
+    <td align="right"><a href="[% buglink FILTER html %]&amp;chfield=[% "[Bug creation]" FILTER uri 
%]&amp;chfieldfrom=[% what_new_means FILTER html %]">
+    [% new_bugs FILTER html %]</a></li></td>
+  </tr>
+
+  <tr>
+    <td>New Patches</td>
+    <td align="right">
+      <a href="page.cgi?id=patchreport.html&amp;product=[%- product.name FILTER uri %]&amp;max_days=7">
+        [% new_patches FILTER html %]
+      </a>
+    </td>
+  </tr>
+
+  <tr>
+    <td>
+      <a class="boogle_edit" 
+         href="javascript:addText('keywords:gnome-love')">GNOME-love 
+        [%= terms.bugs %]</a>
+    </td>
+    <td align="right">
+      <!--
+      <a href="reports/keyword-search.cgi?product=
+               [%- product.name FILTER uri %]&amp;keyword=gnome-love">
+      -->
+        [%- gnome_love_bugs FILTER html %] <!-- </a> -->
+    </td>
+  </tr>
+  <tr>
+    <td>
+      <!-- <a class="boogle_edit"
+               
href="javascript:addText('severity!=enhancement');addText('responders:0');addText('reporter!=developer')"> -->
+      [% terms.Bugs %] without a response <!-- </a> -->
+    </td>
+    <td align="right">
+      <!-- <a href="buglist.cgi?quicksearch=product:
+              [%- product.name FILTER uri %]+responders:0+severity!=enhancement+reporter!=developer"> -->
+        [% no_response_bugs FILTER html %] <!-- </a> -->
+    </td>
+  </tr>
+  [%######################################################################
+    # Temporary time-limited queries; Please just comment these out when
+    # they are not in use if they will be needed again next release
+    # cycle.  Also, be sure to leave these in italics so that people can
+    # notice they are different than the standard list.
+    ######################################################################
+   %]
+  <tr>
+    <td>
+      <em><a class="boogle_edit" 
+             href="javascript:addText('+G_LOG_LEVEL_CRITICAL')">Critical 
+        warning [% terms.bugs %]</a></em>
+    </td>
+    <td align="right">
+      <em><a href="[% buglink FILTER html %]&amp;content=G_LOG_LEVEL_CRITICAL">
+        [%- critical_warning_bugs FILTER html %]</a></em>
+    </td>
+  </tr>
+  <!-- 
+  <tr>
+    <td><a class="boogle_edit" href="javascript:addText('keyword:string')">String [% terms.bugs %]</a></td>
+    <td align="right"><a href="[% buglink FILTER html %]&amp;keywords=string">[% string_bugs FILTER html 
%]</a></td>
+  </tr>
+  -->
+  [%######################################################################
+    # End of temporary, time-limited queries
+    ######################################################################
+   %]
+  </table>
+  
+  [% IF by_patch_status.size %]
+    <h3>Patch Status</h3>
+    <table border="0" cellpadding="0" cellspacing="0" class="figures">
+    [% FOREACH col = by_patch_status %]
+      <tr>
+        <td>
+          <!--
+          <a class="boogle_edit" 
+             href="javascript:addText('patch-status:[% col.0 FILTER js %]')"> -->
+          [% col.0 FILTER html %] <!-- </a> -->
+          [% IF col.0 == 'none' %] (unreviewed)[% END %]
+        </td>
+        <td align="right">
+          <a href="page.cgi?id=patchreport.html&amp;product=[%- product.name FILTER uri 
%]&amp;patch-status=[% col.0 FILTER uri %]">
+            [% col.1 FILTER html %]
+          </a>
+        </td>
+      </tr>
+    [% END %]
+    </table>
+  [% END %]
+  
+  [% IF by_priority.size %]
+    <h3>Priority</h3>
+    <table border="0" cellpadding="0" cellspacing="0" class="figures">
+    [% FOREACH col = by_priority %]
+      <tr>
+        <td>
+          <a class="boogle_edit" 
+             href="javascript:addText('priority:[% col.0 FILTER js %]')">
+            [%- col.0 FILTER html %]</a>
+        </td>
+        <td align="right">
+          <a href="[% buglink FILTER html %]&amp;priority=
+                   [%- col.0 FILTER uri %]">[% col.1 FILTER html %]</a>
+        </td>
+      </tr>
+    [% END %]
+    </table>
+  [% END %]
+  
+  [% IF by_severity.size %]
+    <h3>Severity</h3>
+    <table border="0" cellpadding="0" cellspacing="0" class="figures">
+    [% FOREACH col = by_severity %]
+      <tr>
+        <td>
+          <a class="boogle_edit" 
+             href="javascript:addText('severity:[% col.0 FILTER js %]')">
+            [%- col.0 FILTER html %]</a>
+        </td>
+        <td align="right">
+          <a href="[% buglink FILTER html %]&amp;bug_severity=
+                   [%- col.0 FILTER uri %]">[% col.1 FILTER html %]</a>
+        </td>
+      </tr>
+    [% END %]
+    </table>
+  [% END %]
+  
+  <h3>Useful links</h3>
+  <ul>
+    <li>
+      <a href="http://www.gnome.org/start/unstable";>Development schedule</a>
+    </li>
+    <li>
+      <a href="http://live.gnome.org/MaintainersCorner";>Maintainers corner</a>
+    </li>
+    <li> [% terms.Bugzilla %]
+      <ul>
+        [% IF user.id && user.in_group('editbugs') %]
+          <li>
+            <a href="enter_bug.cgi?product=
+                     [%- product.name FILTER uri %]">File a
+              [%= terms.bug %]</a>
+            <!-- (<a href="simple-bug-guide.cgi?product=[% product.name FILTER uri %]">simple form</a>) -->
+          </li>
+        [% ELSE %]
+          <li>
+            <!--
+            <a href="simple-bug-guide.cgi?product=
+                     [%- product.name FILTER uri %]">File a 
+              [%= terms.bug %]</a> -->
+            <a href="enter_bug.cgi?product=
+                     [%- product.name FILTER uri %]">File a 
+              [%= terms.bug %]</a>
+          </li>
+        [% END %]
+        [% IF user.in_group('editcomponents', product.id) %]
+          <li>
+            <a href="editproducts.cgi?action=edit&amp;product=
+                    [%- product.name FILTER uri %]">Edit this product</a>
+          </li>
+        [% ELSE %]
+          <li>
+            Show <a href="describecomponents.cgi?product=
+                          [%- product.name FILTER uri %]">component
+              descriptions</a>
+          </li>
+        [% END %]
+        <li> <a href="http://live.gnome.org/Bugsquad/TriageGuide/ProductSpecificGuidelines";>Triaging 
guidelines</a></li>
+        <li> <a href="http://live.gnome.org/Bugsquad/ForMaintainers";>Contacting bugmasters</a></li>
+      </ul>
+    </li>
+    <li> Product Info
+      <ul>
+        <li><a href="http://git.gnome.org/cgit/
+                     [%- product.name FILTER lower FILTER uri %]">GNOME Git</a></li>
+        <li>
+          [% IF product.milestone_url %]
+            <a href="[% product.milestone_url FILTER html %]">
+          [% ELSE %]
+            <a href="http://bugzilla.gnome.org/";>
+          [% END %]
+          [% product.name FILTER html %] homepage</a>
+        </li>
+      </ul>
+    </li>
+  </ul>
+</div>
+
+<form action="browse.cgi" method="get">
+<h1>
+Browse:
+  <select name="product">
+    [% FOREACH c = classifications %]
+      <optgroup label="[% c.name FILTER html %]">
+        [% FOREACH p = c.products %]
+          <option value="[% p.name FILTER html %]" 
+            [% IF p.name == product.name && seen != 1 %]selected="selected"[% seen = 1 %]
+            [% END %]>
+          [% p.name FILTER html %]</option>
+      [% END %]</optgroup>
+    [% END %]
+  </select>
+  <input type="submit" value="Show product">
+</h1>
+</form>
+
+<p><i>[% product.description FILTER none %]</i></p>
+
+[% PROCESS gnomeblocker list = blockers_development  
+                        target = target_development %]
+[% PROCESS gnomeblocker list = blockers_stable
+                        target = target_stable %]
+
+<form class="boogleform" id="boogle_search" action="buglist.cgi" 
+      method="get">
+<div>
+  <p>Search for [% terms.bugs %] in [% product.name FILTER html %]: <br />
+  <input id="boogle_search_box" name="quicksearch" type="text" 
+         value="product:&quot;[% product.name.replace('\\[', '\[').replace('\\]', '\]').replace(':','\:') 
FILTER none %]&quot; "
+         size="50">
+  <input id="show" type="submit" value="Show">
+  <a href="page.cgi?id=quicksearch.html">[Help]</a>
+  </p>
+</div>
+</form>
+
+<script  type="text/javascript">
+<!--
+  var search_box = document.getElementById('boogle_search_box');
+  search_box.focus();
+  setCaretToEnd(search_box);
+-->
+</script>
+
+<table cellpadding="3" cellspacing="0">
+<tr>
+  <th>Components</th>
+  <td>&nbsp;</td>
+  <th>Versions</th>
+</tr>
+<tr>
+  <td valign="top">
+
+  <table border="0" cellpadding="0" cellspacing="0" class="figures">
+  [% FOREACH col = by_component %]
+    <tr>
+      <td>
+        <a class="boogle_edit" href="javascript:addText('component:[% col.0 FILTER js %]')">[% col.0 FILTER 
html %]</a>
+      </td>
+      <td align="right">
+        <a href="[% buglink FILTER html %]&amp;component=[% col.0 FILTER uri %]">[% col.1 FILTER html %]</a>
+      </td>
+    </tr>
+  [% END %]
+  </table>
+
+  </td>
+  <td>&nbsp;</td>
+  <td valign="top">
+
+  <table border="0" cellpadding="0" cellspacing="0" class="figures">
+  [% FOREACH col = by_version %]
+    <tr>
+      <td>
+        <a class="boogle_edit" href="javascript:addText('version:[% col.0 FILTER js %]')">[% col.0 FILTER 
html %]</a>
+      </td>
+      <td align="right">
+        <a href="[% buglink FILTER html %]&amp;version=[% col.0 FILTER uri %]">[% col.1 FILTER html %]</a>
+      </td>
+    </tr>
+  [% END %]
+  </table>
+
+  </td>
+</tr>
+</table>
+
+<table cellpadding="3" cellspacing="0">
+<tr>
+  <th>Milestones</th>
+  <td>&nbsp;</td>
+  <th>NEEDINFO by last changed</th>
+</tr>
+<tr>
+  <td valign="top">
+
+  <table border="0" cellpadding="0" cellspacing="0" class="figures">
+  [% FOREACH col = by_target %]
+    <tr>
+      <td>
+        <a class="boogle_edit" href="javascript:addText('target:[% col.0 FILTER js %]')">[% col.0 FILTER 
html %]</a>
+      </td>
+      <td align="right">
+        <a href="[% buglink FILTER html %]&amp;target_milestone=[% col.0 FILTER uri %]">[% col.1 FILTER html 
%]</a>
+      </td>
+    </tr>
+  [% END %]
+  </table>
+
+  </td>
+  <td>&nbsp;</td>
+  <td valign="top">
+
+  <table border="0" cellpadding="0" cellspacing="0" class="figures">
+  [% IF needinfo_split.F %]
+    <tr>
+      <td>&gt;= 1 year</a></td>
+      <td align="right"><a href="buglist.cgi?product=[% product.name FILTER uri 
%]&amp;bug_status=NEEDINFO&amp;chfieldfrom=&amp;chfieldto=-1y">
+      [% needinfo_split.F FILTER html %]</a></td>
+    </tr>
+  [% END %]
+  [% IF needinfo_split.E %]
+    <tr>
+      <td>6 months - 1 year</a></td>
+      <td align="right"><a href="buglist.cgi?product=[% product.name FILTER uri 
%]&amp;bug_status=NEEDINFO&amp;chfieldfrom=-1y&amp;chfieldto=-6m">
+      [% needinfo_split.E FILTER html %]</a></td>
+    </tr>
+  [% END %]
+  [% IF needinfo_split.D %]
+    <tr>
+      <td>3 months - 6 months</a></td>
+      <td align="right"><a href="buglist.cgi?product=[% product.name FILTER uri 
%]&amp;bug_status=NEEDINFO&amp;chfieldfrom=-6m&amp;chfieldto=-3m">
+      [% needinfo_split.D FILTER html %]</a></td>
+    </tr>
+  [% END %]
+  [% IF needinfo_split.C %]
+    <tr>
+      <td>4 weeks - 3 months</a></td>
+      <td align="right"><a href="buglist.cgi?product=[% product.name FILTER uri 
%]&amp;bug_status=NEEDINFO&amp;chfieldfrom=-3m&amp;chfieldto=-4w">
+      [% needinfo_split.C FILTER html %]</a></td>
+    </tr>
+  [% END %]
+  [% IF needinfo_split.B %]
+    <tr>
+      <td>2 weeks - 4 weeks</a></td>
+      <td align="right"><a href="buglist.cgi?product=[% product.name FILTER uri 
%]&amp;bug_status=NEEDINFO&amp;chfieldfrom=-4w&amp;chfieldto=-2w">
+      [% needinfo_split.B FILTER html %]</a></td>
+    </tr>
+  [% END %]
+  [% IF needinfo_split.A %]
+    <tr>
+      <td>&lt; 2 weeks ago</a></td>
+      <td align="right"><a href="buglist.cgi?product=[% product.name FILTER uri 
%]&amp;bug_status=NEEDINFO&amp;chfieldfrom=-2w&amp;chfieldto=">
+      [% needinfo_split.A FILTER html %]</a></td>
+    </tr>
+  [% END %]
+  </table>
+</table>
+
+<form class="fixedquery" name="fixedquery" action="buglist.cgi" method="get">
+<div>
+  <p>Find all [% terms.bugs %] marked as fixed since
+  <input name="product" type="hidden" value="[% product.name FILTER html %]">
+  <input name="bug_status" type="hidden" value="RESOLVED">
+  <input name="bug_status" type="hidden" value="VERIFIED">
+  <input name="resolution" type="hidden" value="FIXED">
+  <input name="chfield" type="hidden" value="resolution">
+  <input name="chfieldvalue" type="hidden" value="FIXED">
+  <input name="chfieldfrom" type="text" size="8" value="-7d">  (YYYY-MM-DD)
+  <input id="show" type="submit" value="Show">
+  </p>
+</div>
+</form>
+
+<h3>Developers</h3>
+[% IF product.developers.size > 0 %]
+  <table cellpadding="3" cellspacing="0">
+  [% FOREACH developer = product.developers.sort('name') %]
+    <tr>
+      <td>
+        [% IF user.id %]
+          [%# XXX - describeuser.cgi will come eventually so commenting out for now %]
+          <!-- <a href="describeuser.cgi?login=[% developer.login FILTER uri %]"> -->
+        [% END %]
+        [% PROCESS "global/user.html.tmpl" who = developer %]
+        [% IF user.id %]
+          <!-- </a> -->
+        [% END %]
+      </td>
+    </tr>
+  [% END %]
+  </table>
+[% ELSE %]
+  No users are marked as being developers of this project; please <a 
href="enter_bug.cgi?product=bugzilla.gnome.org">contact the bugmasters and let us know who to mark as 
such</a>.
+[% END %]
+
+[% BLOCK gnomeblocker %]
+  [% IF list.size %]
+    <table border="0" class="gnomeblocker" cellpadding="0" cellspacing="0">
+      <thead>
+      <tr>
+        <td colspan="5">Blocker [% terms.bugs %]: 
+          <b> <a class="boogle_edit"
+          href="javascript:addText('cf_gnome_target:
+               [%- target FILTER js %]')">
+            GNOME [% target FILTER html %]</a></b>
+          (these must be fixed before/in the specified GNOME version)
+        </td>
+      </tr>
+      </thead>
+      [% FOREACH bug = list %]
+        <tr class="[%+ IF loop.count() % 2 == 0 %]bz_row_even[% ELSE %]bz_row_odd[% END %]">
+          <td align="center">
+            <a href="show_bug.cgi?id=[% bug.bug_id FILTER uri %]">[% bug.bug_id FILTER html %]</a>
+          </td>
+          <td>
+            <a href="[% buglink FILTER html %]&amp;gnome_target=[% target FILTER uri %]">[% bug.product 
FILTER html %]</a>
+          </td>
+          <td align="center">[% bug.bug_status.truncate(4) FILTER html %]</td>
+          <td align="center">[% bug.bug_severity.truncate(3) FILTER html %]</td>
+          <td>
+            <a href="show_bug.cgi?id=[% bug.bug_id FILTER uri %]">[% bug.short_desc.truncate(70, '...') 
FILTER html %]</a>
+          </td>
+        </tr>
+      [% END %]
+    </table>
+  [% END %]
+[% END %]
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/web/README b/web/README
new file mode 100644
index 0000000..2345641
--- /dev/null
+++ b/web/README
@@ -0,0 +1,7 @@
+Web-accessible files, like JavaScript, CSS, and images go in this
+directory. You can reference them directly in your HTML. For example,
+if you have a file called "style.css" and your extension is called
+"Foo", you would put it in "extensions/Foo/web/style.css", and then
+you could link to it in HTML like:
+
+<link href="extensions/Foo/web/style.css" rel="stylesheet" type="text/css">
\ No newline at end of file
diff --git a/web/browse.css b/web/browse.css
new file mode 100644
index 0000000..693b2cc
--- /dev/null
+++ b/web/browse.css
@@ -0,0 +1,35 @@
+#product_summary {
+    float: right; 
+    -moz-border-radius-topleft: 20px;
+    -moz-border-radius-bottomleft: 20px; 
+    margin: 0;
+    background-color: #ffeafd; 
+    padding: 0 1em 1em 1em; 
+}
+
+#product_summary h3 {
+    padding: 1em 0 0 0; 
+}
+
+#product_summary ul {
+    padding: 0 0 0 1em;
+    margin: 0;
+}
+
+table.gnomeblocker td {
+    padding: 3px;
+}
+
+table.gnomeblocker thead {
+    background-color: #babdb6;
+}
+
+table.figures td {
+    padding: 0 3px 0 3px;
+    margin: 0;
+}
+
+table.figures tr { margin: 0; }
+
+a.boogle_edit { text-decoration: none; }
+a.boogle_edit:hover { color: #4e9a06 }
diff --git a/web/browse.js b/web/browse.js
new file mode 100644
index 0000000..8d47a38
--- /dev/null
+++ b/web/browse.js
@@ -0,0 +1,116 @@
+function setCaretToEnd (control) {
+  if (control.createTextRange) {
+    var range = control.createTextRange();
+    range.collapse(false);
+    range.select();
+  }
+  else if (control.setSelectionRange) {
+    control.focus();
+    var length = control.value.length;
+    control.setSelectionRange(length, length);
+  }
+}
+
+function addText(text) {
+  /* Get the querytype */
+  var colonloc = text.indexOf(":");
+  var querytype;
+  var searchBox = document.getElementById('boogle_search_box');
+
+  if (colonloc != -1)
+    querytype = text.substring(0,colonloc);
+  else { 
+    /* comment or +critical_warning */
+    var oldvalue = searchBox.value;
+    var location = oldvalue.indexOf(text);
+
+    if (location == -1) {
+      /* This is new; just prepend it */
+      searchBox.value = text + " " + oldvalue;
+
+    } else {
+      searchBox.value = oldvalue.substring(0, location)
+                        + oldvalue.substring(location + text.length + 1);
+    }
+    return;
+  } /* if (colonloc != -1) */
+
+  /* Quote the value, if needed */
+  var value = text.substring(colonloc+1);
+  if (value.match(/^[A-Za-z0-9+][A-Za-z0-9_\.-]*$/)) {
+  } else {
+    if (colonloc != -1)
+      text = querytype + ':' + '"' + value + '"';
+    else
+      text = '"' + value + '"';
+
+    value = '"' + value + '"';
+  }
+
+  /* Find if this querytype is already in the boogle query */
+  var oldvalue = searchBox.value;
+  var location = oldvalue.search(querytype);
+
+  if (location == -1) {
+    /* This is a new query type; just prepend it */
+    searchBox.value = text + " " + oldvalue;
+  } else {
+    /* This querytype already appears, so doing an and with a different
+     * value does not make any sense.  So, just add it as a comma separated
+     * list item if it is not already present; otherwise, remove it.
+     */
+
+    if (oldvalue.search(value) == -1) {
+      /* prepend the new value */
+      searchBox.value = oldvalue.substring(0,location) + text + "," 
+                        + oldvalue.substring(location + querytype.length + 1);
+
+    } else {
+
+      /* value is already in list, remove it */
+      var vlocation = oldvalue.indexOf(value);
+    
+      /* how many values are there for the current querytype? */
+      var queryvalues = oldvalue.substring(location + querytype.length + 1);
+      if (queryvalues.indexOf(":") >= 0) {
+        /* other querytypes follow, discard them */
+        queryvalues = queryvalues.substring(0,queryvalues.indexOf(":") - 1);
+      }
+
+      /* count the commas (up to the next colon) */
+      var numberofvalues = 1;
+      while ( queryvalues.indexOf(",") >= 0 ) {
+        queryvalues = queryvalues.substring(queryvalues.indexOf(",")+1);
+        numberofvalues += 1;
+      }
+
+      if (numberofvalues == 1) {
+        /* remove the querytype name and value */
+        searchBox.value = oldvalue.substring(0,location)
+                          + oldvalue.substring(vlocation + value.length + 1);
+      } else {
+        /* only remove one value */
+
+        /* last of all value in querytype? 
+           then do not remove the trailing but the preceding character */
+
+        if ( (vlocation + value.length == oldvalue.length) /*last in string? */
+            ||
+             (oldvalue.substring(vlocation + value.length, /*trailing space? */
+              vlocation + value.length + 1) == ' ')   
+           ) 
+        {
+          searchBox.value = oldvalue.substring(0,vlocation-1)
+                            + oldvalue.substring(vlocation + value.length);
+
+        } else {
+          searchBox.value = oldvalue.substring(0,vlocation)
+                            + oldvalue.substring(vlocation + value.length + 1);
+        }
+      } /* if(numberofvalues == 1) */
+
+    } /* if(oldvalue.search(value) == -1) */
+
+  } /* if (location == -1) */
+}
+


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]