A reimplementation of emblems



Here's a stab at implementing emblems. I don't really know how to
send things around with bzr; the attached is a bundle from 'bzr send
-o', but that doesn't contain my log message apparently???

The log message was:

===
Add support for Emblems
  
An emblem is an icon displayed along side the search results to draw
attention to a particular result. Two types of emblems are added here:
The singleton "patch" emblem which is used for bugs with
(non-obsolete) patches in a configurable set of states. Keyword emblems
are associated with a particular keyword.
  
* An 'emblems' table is added to the database schema; this holds all
  configured emblems including the patch emblem. Each emblem has a
  'definition' column which is either 'patch:<status>,<status>'
  or 'keyword:<keyword>'.
  
* checksetup.py adds a default value for the patch emblem if it doesn't
  already exist.
  
* An editing UI is added to the admin section; this allows adding
  and removing keyword emblems, changing the statuses for the patch
  emblem, and editing the image URLs for all emblems.
  
* A Bugzilla::Emblem object is added to represent rows in the emblem
  table.
 
* A hidden open_patches column is defined in Search.pm that indicates
  whether the result has open patches according to the patch emblem
  statuses.
  
* buglist.cgi adds 'open_patches' and 'keywords' to the set of selected
  columns if necessary.
  
* list/table.html.tmpl is modified to add the emblems to the first
  table column.
  
  The concept (but not the code) is based on original implementation by
  Thomas Thurman and Olav Vittters.
===

Notes, future enhancements, etc.
================================

* I tried to do this clean but I haven't written any significant Perl
  code since 2001/2002 so I'm pretty rusty on good style. (Also,
  I didn't check that the POD docs in Bugzilla::Emblem parse correctly.)

* The editing UI was largely a cut-paste-and-modify job from the
  keyword editing, which made things a lot easier; a lot of the
  new lines in the patch are actually from that.

* This isn't made optional in any way; as soon as you install this
  and run checksetup.py, you'll start getting patch emblems. Things
  that could be done here:

   * The automatically added patch emblem could start off with a
     an empty default set of statuses. If that was the case, then 
     there would be no user interface appearance of emblems and
     no change to the search queries. There would be a small amount
     of overhead from buglist.cgi and template additions.

   * Emblems could be controlled globally with a 'useemblems' parameter.

   * Emblems could maybe be done as an extension. See below.

* With mod_perl, if you change the statuses for the patch emblem, it's
  going to take a web server restart to have that picked up, since
  COLUMNS is cached. I don't know any easy way to fix that.   

  It might be worth adding a note to that effect in the message that
  is displayed when the statuses are changed.

* It would seem nice to allow more than one patch emblem; you might
  want different emblems for statuses of "accepted-commit-now" and
  "none" (haven't looked at yet.) This would, however, up the
  complexity of implementation quite a bit, since you'd have to  
  dynamically add a variable number of 'open_patch' columns on the fly.

* Emblems can be added through the web interface, but there's no way
  to upload the icon, so either an absolute URL to some other server
  is used, or the images have to separately be loaded into
  /images/emblems.

* I didn't put any effort into CSS or prettification, so emblems push
  over the bug numbers, which looks a bit ugly.

   1
   2
   <heart> 3
   4
   <patch> 5
   6

  We may have CSS that can be stolen from our old templates.

* There's a

+    print STDERR join("|", @sql_fields), "\n";

  leftover in the patch I just saw.

Could it be an extension?
=========================

Most of the additions could be done easily through the extension
mechanism. Some of it is existing hooks:

 Schema.pm addition: db_schema-abstract_schema 
 Search.pm addition: buglist-columns 
 Install/DB.pm addition: install-update_db
 editemblems.cgi: Could be done with page-before_template hook

A few things would need new hooks:

 buglist.cgi addition: would need a new hook or hooks to update
   @selectcolumns and the page vars.
 admin.html.tmpl addition: needs a new template hook

How to do the messages and user error additions wasn't that clear, but
presumably something could be figured out, or just fall back to 
hardcoding.

But the thing that really stumped me  was how to do the 
table.html.tmpl addition. As I read the template hook mechanism, 
it's just not going to perform acceptably to have a hook called
for each row. (It seems to do a look up through all the extension
directories each time the hook is invoked.)

- Owen

# Bazaar merge directive format 2 (Bazaar 0.90)
# revision_id: otaylor huygens home fishsoup net-20090823192300-\
#   j3ku6q352hwx29ec
# target_branch: /home/otaylor/Source/bugzilla-mkanat/
# testament_sha1: 0440277d5a1e9fec23182107ddf132da834ba6ec
# timestamp: 2009-08-23 15:40:46 -0400
# base_revision_id: mkanat everythingsolved com-20090817211918-\
#   u4m8c53n3nb3ci4b
# 
# Begin patch
=== modified file 'Bugzilla/DB/Schema.pm'
--- Bugzilla/DB/Schema.pm	2009-07-29 05:01:49 +0000
+++ Bugzilla/DB/Schema.pm	2009-08-23 19:23:00 +0000
@@ -550,6 +550,19 @@
         ],
     },
 
+    # Emblems
+    # --------
+
+    emblems => {
+        FIELDS => [
+            id          => {TYPE => 'SMALLSERIAL',  NOTNULL => 1,
+                            PRIMARYKEY => 1},
+            definition  => {TYPE => 'varchar(200)', NOTNULL => 1},
+            description => {TYPE => 'MEDIUMTEXT'},
+            image_url   => {TYPE => 'varchar(200)', NOTNULL => 1},
+        ]
+    },
+
     # Flags
     # -----
 

=== added file 'Bugzilla/Emblem.pm'
--- Bugzilla/Emblem.pm	1970-01-01 00:00:00 +0000
+++ Bugzilla/Emblem.pm	2009-08-23 19:23:00 +0000
@@ -0,0 +1,272 @@
+# -*- 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 Bugzilla Bug Tracking System.
+#
+# Contributor(s): Max Kanat-Alexander <mkanat bugzilla org>
+#                 Owen Taylor <otaylor redhat com>
+
+use strict;
+
+package Bugzilla::Emblem;
+
+use base qw(Bugzilla::Object);
+
+use Bugzilla::Error;
+use Bugzilla::Util;
+
+###############################
+####    Initialization     ####
+###############################
+
+use constant DB_COLUMNS => qw(
+   emblems.id
+   emblems.definition
+   emblems.description
+   emblems.image_url
+);
+
+use constant DB_TABLE => 'emblems';
+
+use constant REQUIRED_CREATE_FIELDS => qw(definition image_url description);
+
+use constant VALIDATORS => {
+    definition  => \&_check_definition,
+    image_url   => \&_check_image_url,
+    description => \&_check_description,
+};
+
+use constant UPDATE_COLUMNS => qw(
+    definition
+    image_url
+    description
+);
+
+###############################
+####      Accessors      ######
+###############################
+
+sub image_url         { return $_[0]->{'image_url'}; }
+sub description       { return $_[0]->{'description'}; }
+
+sub ispatch {
+    my ($self) = @_;
+
+    return $self->{'definition'} =~ /^patch:/;
+}
+
+sub keyword {
+    my ($self) = @_;
+
+    return undef unless ($self->{'definition'} =~ /^keyword:(.*)/);
+
+    return $1;
+}
+
+sub statuses {
+    my ($self) = @_;
+
+    return () unless $self->{'definition'} =~ /^patch:(.*)/;
+
+    return split(',', $1)
+}
+
+###############################
+####       Mutators       #####
+###############################
+
+sub set_definition  { $_[0]->set('definition',  $_[1]); }
+sub set_image_url   { $_[0]->set('image_url',   $_[1]); }
+sub set_description { $_[0]->set('description', $_[1]); }
+
+###############################
+####      Subroutines    ######
+###############################
+
+sub get_keyword_emblems {
+    my $class = shift;
+    my $dbh = Bugzilla->dbh;
+    my $emblems =
+      $dbh->selectall_arrayref('SELECT ' . join(', ', DB_COLUMNS) . '
+                                  FROM emblems
+                                 WHERE emblems.definition like \'keyword:%\'',
+                               {'Slice' => {}});
+
+    if (!$emblems) {
+        return [];
+    }
+
+    foreach my $emblem (@$emblems) {
+        bless($emblem, $class);
+    }
+    return $emblems;
+}
+
+sub get_all_emblems {
+    my $class = shift;
+    my $dbh = Bugzilla->dbh;
+    my $emblems =
+      $dbh->selectall_arrayref('SELECT ' . join(', ', DB_COLUMNS) . '
+                                  FROM emblems',
+                               {'Slice' => {}});
+
+    if (!$emblems) {
+        return [];
+    }
+
+    foreach my $emblem (@$emblems) {
+        bless($emblem, $class);
+    }
+    return $emblems;
+}
+
+sub get_patch_emblem {
+    my $class = shift;
+    my $dbh = Bugzilla->dbh;
+    my $emblem =
+      $dbh->selectall_arrayref('SELECT ' . join(', ', DB_COLUMNS) . '
+                                  FROM emblems
+                                 WHERE emblems.definition like \'patch:%\'
+                                 LIMIT 1',
+                               {'Slice' => {}});
+
+    if (!$emblem || @$emblem != 1) {
+        return undef;
+    }
+
+    bless($emblem->[0], $class);
+    return $emblem->[0];
+}
+
+###############################
+###       Validators        ###
+###############################
+
+sub _check_definition {
+    my ($self, $definition) = @_;
+
+    $definition = trim($definition);
+
+    # We don't actually validate here, since we can't return a meaningful
+    # erorr message; it's up to the caller to validate the specific
+    # definition type:
+    #  keyword:<keyword>
+    #  patch:<status>,<status>,<status>
+
+    return $definition;
+}
+
+sub _check_description {
+    my ($self, $desc) = @_;
+    $desc = trim($desc);
+    $desc eq '' && ThrowUserError("emblem_blank_description");
+    return $desc;
+}
+
+sub _check_image_url {
+    my ($self, $image_url) = @_;
+    $image_url = trim($image_url);
+    $image_url eq '' && ThrowUserError("emblem_blank_image_url");
+    return $image_url;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Emblem - An emblem displayed with search results
+
+=head1 SYNOPSIS
+
+ use Bugzilla::Emblem
+
+ my $description = $keyword->description;
+
+ my $keywords = Bugzilla::Keyword->get_keyword_emblems();
+
+ my $keyword = Bugzilla::Keyword->get_patch_emblem();
+
+=head1 DESCRIPTION
+
+Bugzilla::Emblem represents an an emblem displayed with search results
+
+This implements all standard C<Bugzilla::Object> methods. See 
+L<Bugzilla::Object> for more details.
+
+=head1 METHODS
+
+This is only a list of methods specific to C<Bugzilla::Emblem>.
+See L<Bugzilla::Object> for more methods that this object 
+implements.
+
+=item C<ispatch()>
+
+ Description: Determine if this is the singleton patch emblem
+ Params:      none
+ Returns:     C<1> if this is the singleton patch emblem, C<0> otherwise
+
+=item C<keyword()>
+
+ Description: Return the keyword for a keyword emblem
+ Params:      none
+ Returns:     The keyword for this emblem if it's a keyword emblem,
+              C<undef> if it's the patch emblem
+
+=item C<statuses()>
+
+ Description: Return the statuses for the patch emblem
+ Params:      none
+ Returns:     The statuses that count as an open patch if the emblem is
+              the patch emblem. An empty list if it's a keyword emblem
+
+=item C<ispatch()>
+
+ Description: Determine if this is the singleton patch emblem
+ Params:      none
+ Returns:     <1> if this is the singleton patch emblem, <0> otherwise
+
+=head1 SUBROUTINES
+
+This is only a list of subroutines specific to C<Bugzilla::Emblem>.
+See L<Bugzilla::Object> for more subroutines that this object 
+implements.
+
+=over
+
+=item C<get_all_emblems()> 
+
+ Description: Returns all defined emblems.
+ Params:      none
+ Returns:     A reference to an array of Emblem objects, or an empty
+              arrayref if there are no emblems. This includes both
+              the keyword emblems and the signleton patch emblem.
+
+=item C<get_keyword_emblems()> 
+
+ Description: Returns defined keyword emblems.
+ Params:      none
+ Returns:     A reference to an array of Emblem objects, or an empty
+              arrayref if there are no emblems.
+
+=item C<get_patch_emblem()> 
+
+ Description: Returns the singleton patch emblem.
+ Params:      none
+ Returns:     the Emblem object for the patch emblem. This will nevern
+              be undefined except in the error-case when there is no
+              patch emblem in the database.
+
+=back
+
+=cut

=== modified file 'Bugzilla/Install/DB.pm'
--- Bugzilla/Install/DB.pm	2009-08-14 02:07:41 +0000
+++ Bugzilla/Install/DB.pm	2009-08-23 19:23:00 +0000
@@ -632,6 +632,8 @@
 
     _fix_saved_searches();
 
+    _create_patch_emblem();
+
     ################################################################
     # New --TABLE-- changes should go *** A B O V E *** this point #
     ################################################################
@@ -3521,6 +3523,15 @@
     $dbh->bz_commit_transaction();
 }
 
+sub _create_patch_emblem {
+    my $dbh = Bugzilla->dbh;
+
+    if (!$dbh->selectrow_arrayref("SELECT 1 FROM emblems WHERE definition like 'patch:%' LIMIT 1")) {
+        $dbh->do("INSERT INTO emblems (definition, image_url, description)
+                  VALUES ('patch:none', '/images/emblems/patch.png', 'Patch needs attention')");
+    }
+}
+
 1;
 
 __END__

=== modified file 'Bugzilla/Search.pm'
--- Bugzilla/Search.pm	2009-07-10 20:37:42 +0000
+++ Bugzilla/Search.pm	2009-08-23 19:23:00 +0000
@@ -49,6 +49,7 @@
 use Bugzilla::Field;
 use Bugzilla::Status;
 use Bugzilla::Keyword;
+use Bugzilla::Emblem;
 
 use Date::Format;
 use Date::Parse;
@@ -167,6 +168,29 @@
     # The short_short_desc column is identical to short_desc
     $columns{'short_short_desc'} = $columns{'short_desc'};
 
+    # The open_patches column is used to flag results that need the patch emblem,
+    # but should be otherwise hidden, so we add it in directly here rather than
+    # adding it to the field definitions.
+    $columns{'open_patches'} = {  title => 'Open Patches' };
+
+    my @open_patch_statuses;
+    my $patch_emblem = Bugzilla::Emblem->get_patch_emblem();
+    if ($patch_emblem) {
+        @open_patch_statuses = map { $dbh->quote($_) } $patch_emblem->statuses;
+    }
+
+    if (@open_patch_statuses) {
+        my $open_patch = $dbh->sql_in('attachments.status', \ open_patch_statuses);
+        $columns{'open_patches'}->{'name'} =
+            "EXISTS (SELECT * FROM attachments"
+                . "  WHERE attachments.bug_id = bugs.bug_id"
+                . " AND attachments.ispatch = 1"
+                . " AND attachments.isobsolete = 0"
+                . " AND $open_patch)";
+    } else {
+        $columns{'open_patches'}->{'name'} = '0';
+    };
+
     Bugzilla::Hook::process("buglist-columns", { columns => \%columns });
 
     $cache->{search_columns} = \%columns;
@@ -938,6 +962,7 @@
 
     my @sql_fields = map { $_ eq EMPTY_COLUMN ? EMPTY_COLUMN 
                            : COLUMNS->{$_}->{name} . ' AS ' . $_ } @fields;
+    print STDERR join("|", @sql_fields), "\n";
     my $query = "SELECT " . join(', ', @sql_fields) .
                 " FROM $suppstring" .
                 " LEFT JOIN bug_group_map " .

=== modified file 'buglist.cgi'
--- buglist.cgi	2009-08-12 03:28:01 +0000
+++ buglist.cgi	2009-08-23 19:23:00 +0000
@@ -45,6 +45,7 @@
 use Bugzilla::Bug;
 use Bugzilla::Product;
 use Bugzilla::Keyword;
+use Bugzilla::Emblem;
 use Bugzilla::Field;
 use Bugzilla::Status;
 use Bugzilla::Token;
@@ -796,6 +797,22 @@
     }
 }
 
+my $need_open_patches = 0;
+my $need_keywords = 0;
+$vars->{'emblems'} = {};
+foreach my $emblem (@{Bugzilla::Emblem->get_all_emblems()}) {
+    if ($emblem->ispatch && $emblem->statuses) {
+        $vars->{'emblems'}->{'PATCH'} = $emblem->image_url;
+        $need_open_patches = 1;
+    } else {
+        $vars->{'emblems'}->{$emblem->keyword} = $emblem->image_url;
+        $need_keywords = 1;
+    }
+}
+
+push(@selectcolumns, "open_patches") if $need_open_patches;
+push(@selectcolumns, "keywords") if $need_keywords;
+
 ################################################################################
 # Sort Order Determination
 ################################################################################

=== added file 'editemblems.cgi'
--- editemblems.cgi	1970-01-01 00:00:00 +0000
+++ editemblems.cgi	2009-08-23 19:23:00 +0000
@@ -0,0 +1,208 @@
+#!/usr/bin/perl -wT
+# -*- 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 Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Terry Weissman.
+# Portions created by Terry Weissman are
+# Copyright (C) 2000 Terry Weissman. All
+# Rights Reserved.
+#
+# Contributor(s): Terry Weissman <terry mozilla org>
+#                 Owen Taylor <otaylor redhat com>
+
+use strict;
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Emblem;
+use Bugzilla::Error;
+use Bugzilla::Util;
+use Bugzilla::Token;
+
+my $cgi = Bugzilla->cgi;
+my $dbh = Bugzilla->dbh;
+my $template = Bugzilla->template;
+my $vars = {};
+
+#
+# Preliminary checks:
+#
+
+my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+print $cgi->header();
+
+$user->in_group('editkeywords')
+  || ThrowUserError("auth_failure", {group  => "editkeywords",
+                                     action => "edit",
+                                     object => "emblems"});
+
+my $action = trim($cgi->param('action')  || '');
+my $key_id = $cgi->param('id');
+my $token  = $cgi->param('token');
+
+$vars->{'action'} = $action;
+
+if ($action eq "") {
+    ProcessList();
+    exit;
+}
+
+if ($action eq 'add') {
+    $vars->{'token'} = issue_session_token('add_emblem');
+
+    print $cgi->header();
+
+    $template->process("admin/emblems/create.html.tmpl", $vars)
+      || ThrowTemplateError($template->error());
+
+    exit;
+}
+
+#
+# action='new' -> add keyword entered in the 'action=add' screen
+#
+if ($action eq 'new') {
+    check_token_data($token, 'add_emblem');
+    my $keyword = CheckKeyword(scalar $cgi->param('keyword'));
+    my $image_url = $cgi->param('image_url')  || '';
+    my $desc = $cgi->param('description')  || '';
+
+    my $emblem = Bugzilla::Emblem->create(
+        { definition => "keyword:$keyword",
+          image_url => $image_url,
+          description => $desc });
+
+    delete_token($token);
+
+    $vars->{'emblem_keyword'} = $emblem->keyword;
+    ProcessList('emblem_created');
+
+    exit;
+}
+
+
+if ($action eq 'edit') {
+    my $emblem = new Bugzilla::Emblem($key_id)
+        || ThrowCodeError('invalid_emblem_id', { id => $key_id });
+
+    $vars->{'emblem'} = $emblem;
+    $vars->{'token'} = issue_session_token('edit_emblem');
+
+    print $cgi->header();
+    $template->process("admin/emblems/edit.html.tmpl", $vars)
+      || ThrowTemplateError($template->error());
+    exit;
+}
+
+#
+# action='update' -> update the emblem with changes from the 'action=edit' screen
+#
+if ($action eq 'update') {
+    check_token_data($token, 'edit_emblem');
+    my $emblem = new Bugzilla::Emblem($key_id)
+        || ThrowCodeError('invalid_emblem_id', { id => $key_id });
+
+    my $definition;
+    if ($emblem->ispatch) {
+        my $statuses_string = trim(scalar $cgi->param('statuses'));
+        my @statuses;
+        foreach my $status (split(",", $statuses_string)) {
+            push @statuses, trim($status);
+        }
+        $definition = "patch:" . join(",", @statuses);
+    } else {
+        my $keyword = CheckKeyword(scalar $cgi->param('keyword'));
+        $definition = "keyword:$keyword",
+    }
+
+    $emblem->set_all({
+        definition  => $definition,
+        image_url   => scalar $cgi->param('image_url'),
+        description => scalar $cgi->param('description'),
+    });
+    my $changes = $emblem->update();
+
+    delete_token($token);
+
+    print $cgi->header();
+
+    $vars->{'emblem'} = $emblem;
+    $vars->{'changes'} = $changes;
+    ProcessList('emblem_updated');
+
+    exit;
+}
+
+if ($action eq 'del') {
+    my $emblem =  new Bugzilla::Emblem($key_id)
+        || ThrowCodeError('invalid_emblem_id', { id => $key_id });
+
+    $vars->{'emblem'} = $emblem;
+    $vars->{'token'} = issue_session_token('delete_emblem');
+
+    print $cgi->header();
+    $template->process("admin/emblems/confirm-delete.html.tmpl", $vars)
+      || ThrowTemplateError($template->error());
+    exit;
+}
+
+#
+# action='delete' -> delete the emblem after confirmation at 'action=del' screen
+#
+if ($action eq 'delete') {
+    check_token_data($token, 'delete_emblem');
+    my $emblem =  new Bugzilla::Emblem($key_id)
+        || ThrowCodeError('invalid_emblem_id', { id => $key_id });
+
+    $dbh->do('DELETE FROM emblems WHERE id = ?', undef, $emblem->id);
+
+    delete_token($token);
+
+    $vars->{'emblem_keyword'} = $emblem->keyword;
+    ProcessList('emblem_deleted');
+    exit;
+}
+
+ThrowCodeError("action_unrecognized", $vars);
+
+################################################################################
+# Utilities
+################################################################################
+
+sub CheckKeyword {
+    my ($keyword) = @_;
+
+    $keyword = trim($keyword || '');
+    $keyword eq "" && ThrowUserError("emblem_blank_keyword");
+    if ($keyword =~ /[\s,]/) {
+        ThrowUserError("emblem_invalid_keyword");
+    }
+
+    return $keyword;
+}
+
+sub ProcessList {
+    my ($message) = @_;
+
+    print $cgi->header();
+
+    $vars->{'patch_emblem'} = Bugzilla::Emblem->get_patch_emblem();
+    $vars->{'keyword_emblems'} = Bugzilla::Emblem->get_keyword_emblems();
+    $vars->{'message'} = $message if defined $message;
+
+    $template->process("admin/emblems/list.html.tmpl", $vars)
+      || ThrowTemplateError($template->error());
+}

=== added directory 'images/emblems'
=== added file 'images/emblems/patch.png'
Binary files images/emblems/patch.png	1970-01-01 00:00:00 +0000 and images/emblems/patch.png	2009-08-23 19:23:00 +0000 differ
=== modified file 'template/en/default/admin/admin.html.tmpl'
--- template/en/default/admin/admin.html.tmpl	2008-08-08 06:26:33 +0000
+++ template/en/default/admin/admin.html.tmpl	2009-08-23 19:23:00 +0000
@@ -118,6 +118,11 @@
         <dd class="[% class %]">Set keywords to be used with [% terms.bugs %]. Keywords
         are an easy way to "tag" [% terms.bugs %] to let you find them more easily later.</dd>
 
+        [% class = user.in_group('editkeywords') ? "" : "forbidden" %]
+        <dt class="[% class %]"><a href="editemblems.cgi">Emblems</a></dt>
+        <dd class="[% class %]">Set icons to be displayed next to the search result list. Emblems
+        allow highlighting bugs with particular keywords or that have patches that need attention.</dd>
+
         [% class = user.in_group('bz_canusewhines') ? "" : "forbidden" %]
         <dt class="[% class %]"><a href="editwhines.cgi">Whining</a></dt>
         <dd class="[% class %]">Set queries which will be run at some specified date

=== added directory 'template/en/default/admin/emblems'
=== added file 'template/en/default/admin/emblems/confirm-delete.html.tmpl'
--- template/en/default/admin/emblems/confirm-delete.html.tmpl	1970-01-01 00:00:00 +0000
+++ template/en/default/admin/emblems/confirm-delete.html.tmpl	2009-08-23 19:23:00 +0000
@@ -0,0 +1,47 @@
+[%# 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): Terry Weissman <terry mozilla org>
+  #                 Vlad Dascalu <jocuri softhome net>
+  #                 Max Kanat-Alexander <mkanat bugzilla org>
+  #                 Owen Taylor <otaylor redhat com>
+  #%]
+
+[%# INTERFACE:
+  # emblem: A Bugzilla::Emblem object.
+  #%]
+
+[% PROCESS global/header.html.tmpl
+  title = "Delete Emblem"
+%]
+
+<p>
+  Are you <b>sure</b> you want to delete
+  the <code>[% emblem.keyword FILTER html %]</code> emblem?
+</p>
+
+<form method="post" action="editemblems.cgi">
+  <input type="hidden" name="id" value="[% emblem.id FILTER html %]">
+  <input type="hidden" name="action" value="delete">
+  <input type="hidden" name="token" value="[% token FILTER html %]">
+  <input type="submit" id="delete"
+         value="Yes, really delete the emblem">
+</form>
+
+<p><a href="editemblems.cgi">Edit other emblems</a>.</p>
+
+[% PROCESS global/footer.html.tmpl %] 

=== added file 'template/en/default/admin/emblems/create.html.tmpl'
--- template/en/default/admin/emblems/create.html.tmpl	1970-01-01 00:00:00 +0000
+++ template/en/default/admin/emblems/create.html.tmpl	2009-08-23 19:23:00 +0000
@@ -0,0 +1,63 @@
+[%# 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): Terry Weissman <terry mozilla org>
+  #                 Vlad Dascalu <jocuri softhome net>
+  #                 Owen Taylor <otaylor redhat com>
+  #%]
+
+[%# INTERFACE:
+  # none
+  #%]
+  
+[% PROCESS global/header.html.tmpl
+  title = "Add emblem"
+  subheader = "This page allows you to add a new emblem."
+%]
+
+<form method="post" action="editemblems.cgi">
+  <table border="0" cellpadding="4" cellspacing="0">
+    <tr>
+      <th align="right">Keyword:</th>
+      <td><input size="64" maxlength="64" name="keyword" value=""></td>
+    </tr>
+    <tr>
+      <th align="right">Image URL:</th>
+      <td><input size="64" maxlength="64" name="image_url" value=""></td>
+    </tr>
+    <tr>
+      <th align="right">Description:</th>
+      <td>
+        [% INCLUDE global/textarea.html.tmpl
+          name    = 'description'
+          minrows = 4
+          cols    = 64
+          wrap    = 'virtual'
+        %]
+      </td>
+    </tr>
+  </table>
+  <hr>
+  <input type="hidden" name="id" value="-1">
+  <input type="submit" id="create" value="Add">
+  <input type="hidden" name="action" value="new">
+  <input type="hidden" name="token" value="[% token FILTER html %]">
+</form>
+
+<p><a href="editemblems.cgi">Edit other emblems</a>.</p>
+
+[% PROCESS global/footer.html.tmpl %]

=== added file 'template/en/default/admin/emblems/edit.html.tmpl'
--- template/en/default/admin/emblems/edit.html.tmpl	1970-01-01 00:00:00 +0000
+++ template/en/default/admin/emblems/edit.html.tmpl	2009-08-23 19:23:00 +0000
@@ -0,0 +1,74 @@
+[%# 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): Terry Weissman <terry mozilla org>
+  #                 Vlad Dascalu <jocuri softhome net>
+  #                 Max Kanat-Alexander <mkanat bugzilla org>
+  #                 Owen Taylor <otaylor redhat com>
+  #%]
+
+[%# INTERFACE:
+  # emblem: A Bugzilla::Emblem object.
+  #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+  title = "Edit emblem"
+%]
+
+<form method="post" action="editemblems.cgi">
+  <table border="0" cellpadding="4" cellspacing="0">
+    <tr>
+      [% IF emblem.ispatch %]
+      <th align="right">Statuses:</th>
+      <td><input size="64" maxlength="64" name="statuses"
+		 value="[% emblem.statuses.join(',') FILTER html %]"></td>
+      [% ELSE %]
+      <th align="right">Keyword:</th>
+      <td><input size="64" maxlength="64" name="keyword"
+		 value="[% emblem.keyword FILTER html %]"></td>
+      [% END %]
+    </tr>
+    <tr>
+      <th align="right">Image URL:</th>
+      <td><input size="64" maxlength="64" name="image_url"
+		 value="[% emblem.image_url FILTER html %]"></td>
+    </tr>
+    <tr>
+      <th align="right">Description:</th>
+      <td>
+        [% INCLUDE global/textarea.html.tmpl
+          name           = 'description'
+          minrows        = 4
+          cols           = 64
+          wrap           = 'virtual'
+          defaultcontent = emblem.description
+        %]
+      </td>
+    </tr>
+  </table>
+
+  <input type="submit" id="update" value="Save Changes">
+  <input type="hidden" name="action" value="update">
+  <input type="hidden" name="id" value="[% emblem.id FILTER html %]">
+  <input type="hidden" name="token" value="[% token FILTER html %]">
+</form>
+
+<p><a href="editemblems.cgi">Edit other emblems</a>.</p>
+
+[% PROCESS global/footer.html.tmpl %]

=== added file 'template/en/default/admin/emblems/list.html.tmpl'
--- template/en/default/admin/emblems/list.html.tmpl	1970-01-01 00:00:00 +0000
+++ template/en/default/admin/emblems/list.html.tmpl	2009-08-23 19:23:00 +0000
@@ -0,0 +1,88 @@
+[%# 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): Terry Weissman <terry mozilla org>
+  #                 Vlad Dascalu <jocuri softhome net>
+  #                 Jouni Heikniemi <jouni heikniemi net>
+  #                 Owen Taylor <otaylor redhat com>
+  #%]
+
+[%# INTERFACE:
+  # patch_emblem: emblems object having the properties:
+  #   - id: number. The ID of the emblem.
+  #   - statuses: string. The attachment statuses that trigger the emblem
+  #   - image_url: string. The URL for the image of the keyword
+  # keyword_emblems: array of emblems objects having the properties:
+  #   - id: number. The ID of the emblem.
+  #   - keyword: string. The keyword the emblem corresponds to.
+  #   - image_url: string. The URL for the image of the keyword
+  #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+  title = "Select emblem"
+%]
+
+<h2>Patch Emblem</h2>
+
+<p>
+  The patch emblem shows when a bug has a (non-obsolete) patch with one of the
+  specified statuses. <a href="editemblems.cgi?action=edit&amp;id=[% patch_emblem.id %]">Edit</a>
+</p>
+<table border="0" cellpadding="4" cellspacing="0">
+  <tr>
+    <th align="right">Statuses:</th>
+    <td>[% patch_emblem.statuses.join(",") FILTER html %]</td>
+  </tr>
+  <tr>
+    <th align="right">Image URL:</th>
+    <td>[% patch_emblem.image_url FILTER html %]</td>
+  </tr>
+</table>
+
+<hr>
+
+<h2>Keyword Emblems</h2>
+
+[% columns = [
+     {
+       name => "keyword"
+       heading => "Edit emblem..."
+       contentlink => "editemblems.cgi?action=edit&amp;id=%%id%%" 
+     },
+     {
+       name => "image_url"
+       heading => "Image URL"
+     },
+     {
+       heading => "Action" 
+       content => "Delete"
+       contentlink => "editemblems.cgi?action=del&amp;id=%%id%%"
+     }
+   ]
+%]
+
+[% PROCESS admin/table.html.tmpl
+     columns = columns
+     data = keyword_emblems
+     footer = footer_row
+%]
+
+<p><a href="editemblems.cgi?action=add">Add a new emblem</a></p>
+
+[% PROCESS global/footer.html.tmpl %]

=== modified file 'template/en/default/global/messages.html.tmpl'
--- template/en/default/global/messages.html.tmpl	2009-08-07 22:10:52 +0000
+++ template/en/default/global/messages.html.tmpl	2009-08-23 19:23:00 +0000
@@ -289,6 +289,40 @@
     [%+ new_email FILTER html %] has been canceled.
    Your old account settings have been reinstated.
 
+  [% ELSIF message_tag == "emblem_created" %]
+    [% title = "New Emblem Created" %]
+    The <em>[% emblem_keyword FILTER html %]</em> emblem has been created.
+
+  [% ELSIF message_tag == "emblem_deleted" %]
+    [% title = "Emblem Deleted" %]
+    The <em>[% emblem_keyword FILTER html %]</em> emblem has been deleted.
+
+  [% ELSIF message_tag == "emblem_updated" %]
+    [% title = "Emblem Updated" %]
+    [% IF changes.keys.size %]
+      Changes to the <em>[% emblem.keyword FILTER html %]</em> emblem have
+      been saved:
+      <ul>
+        [% IF changes.definition.defined %]
+          <li>
+	    [% IF emblem.ispatch %]
+            Statuses updated to <em>[% emblem.statuses.join(",") FILTER html %]</em>.
+	    [% ELSE %]
+            Keyword updated to <em>[% emblem.keyword FILTER html %]</em>.
+	    [% END %]
+          </li>
+        [% END %]
+        [% IF changes.image_url.defined %]
+          <li>Image Url updated to <em>[% emblem.image_url FILTER html %]</em></li>
+        [% END %]
+        [% IF changes.description.defined %]
+          <li>Description updated to <em>[% emblem.description FILTER html %]</em></li>
+        [% END %]
+      </ul>
+    [% ELSE %]
+      No changes made.
+    [% END %]
+
   [% ELSIF message_tag == "field_value_created" %]
     [% title = "New Field Value Created" %]
     The value <em>[% value.name FILTER html %]</em> has been added as a 

=== modified file 'template/en/default/global/user-error.html.tmpl'
--- template/en/default/global/user-error.html.tmpl	2009-08-06 03:34:58 +0000
+++ template/en/default/global/user-error.html.tmpl	2009-08-23 19:23:00 +0000
@@ -408,6 +408,22 @@
     Your message did not contain any text.[% terms.Bugzilla %] does not
     accept HTML-only email, or HTML email with attachments.
 
+  [% ELSIF error == "emblem_blank_description" %]
+    [% title = "Blank Emblem Description Not Allowed" %]
+    You must enter a non-blank description for the emblem.
+
+  [% ELSIF error == "emblem_blank_image_url" %]
+    [% title = "Blank Emblem Image Url Not Allowed" %]
+    You must enter a non-blank image url for the emblem.
+
+  [% ELSIF error == "emblem_blank_keyword" %]
+    [% title = "Blank Emblem Keyword Not Allowed" %]
+    You must enter a non-blank keyword for the emblem.
+
+  [% ELSIF error == "emblem_invalid_keyword" %]
+    [% title = "Invalid Emblem Keyword" %]
+    The keyword for an emblem must not contain commas or whitespace.
+     
   [% ELSIF error == "empty_group_description" %]
     [% title = "The group description can not be empty" %]
     You must enter a description for the group.

=== modified file 'template/en/default/list/table.html.tmpl'
--- template/en/default/list/table.html.tmpl	2009-08-06 03:34:58 +0000
+++ template/en/default/list/table.html.tmpl	2009-08-23 19:23:00 +0000
@@ -173,6 +173,15 @@
     </td>
     [% END %]
     <td class="first-child">
+      [% FOREACH keyword = bug.keywords.split(", ") %]
+           [% IF emblems.${keyword}.defined %]
+               <img src="[% emblems.${keyword} FILTER html %]" width="16" height="16" alt="">
+	   [% END %]
+      [% END %]
+      [% IF bug.open_patches == 1 %]
+            <img src="[% emblems.PATCH FILTER html %]" width="16" height="16" alt="">
+      [% END %]
+
       <a name="b[% bug.bug_id %]"
          href="show_bug.cgi?id=[% bug.bug_id %]">[% bug.bug_id %]</a>
       <span style="display: none">[%+ '[SEC]' IF bug.secure_mode %]</span>

# Begin bundle
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
cKEgIRZO0g==


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