]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 306242: Templatize the 'update product' bit of editproducts.cgi - Patch by GavinS...
authorlpsolit%gmail.com <>
Thu, 8 Sep 2005 23:58:56 +0000 (23:58 +0000)
committerlpsolit%gmail.com <>
Thu, 8 Sep 2005 23:58:56 +0000 (23:58 +0000)
editproducts.cgi
template/en/default/admin/products/footer.html.tmpl
template/en/default/admin/products/updated.html.tmpl [new file with mode: 0644]
template/en/default/global/user-error.html.tmpl

index ba83371898b4013b1a790d7bd818e11b0260db58..46a2a2bad5bdd6ac5c816e163fe47f2fdb6265a9 100755 (executable)
@@ -23,7 +23,7 @@
 #               Dawn Endico <endico@mozilla.org>
 #               Joe Robins <jmrobins@tgix.com>
 #               Gavin Shelley <bugzilla@chimpychompy.org>
-#               Frédéric Buclin <LpSolit@gmail.com>
+#               Fr��ic Buclin <LpSolit@gmail.com>
 #               Greg Hendricks <ghendricks@novell.com>
 #
 # Direct any questions on this source code to
@@ -69,13 +69,33 @@ sub CheckProduct
         exit;
     }
 
-    unless (TestProduct $prod) {
+    unless (TestProduct($prod)) {
         print "Sorry, product '$prod' does not exist.";
         PutTrailer();
         exit;
     }
 }
 
+# For the transition period, as this file is templatised bit by bit,
+# we need this routine, which does things properly, and will
+# eventually be the only version. (The older versions assume a
+# $template->put_header() call has been made)
+sub CheckProductNew
+{
+    my $prod = shift;
+
+    # do we have a product?
+    unless ($prod) {
+        ThrowUserError('product_not_specified');
+    }
+
+    unless (TestProduct($prod)) {
+        ThrowUserError('product_doesnt_exist',
+                       {'product' => $prod});
+    }
+}
+
+
 # TestClassification:  just returns if the specified classification does exists
 # CheckClassification: same check, optionally  emit an error text
 
@@ -121,7 +141,7 @@ sub CheckClassificationNew
         ThrowUserError('classification_not_specified');    
     }
 
-    unless (TestClassification $cl) {
+    unless (TestClassification($cl)) {
         ThrowUserError('classification_doesnt_exist',
                        {'name' => $cl});
     }
@@ -342,7 +362,7 @@ if (!$action && !$product) {
 if ($action eq 'add') {
 
     if (Param('useclassification')) {
-        CheckClassification($classification);
+        CheckClassificationNew($classification);
     }
     $vars->{'classification'} = $classification;
     $template->process("admin/products/create.html.tmpl", $vars)
@@ -689,7 +709,7 @@ if ($action eq 'delete') {
 #
 
 if ($action eq 'edit' || (!$action && $product)) {
-    CheckProduct($product);
+    CheckProductNew($product);
     trick_taint($product);
     my $product_id = get_product_id($product); 
     my $classification_id=1;
@@ -697,7 +717,7 @@ if ($action eq 'edit' || (!$action && $product)) {
         # If a product has been given with no classification associated
         # with it, take this information from the DB
         if ($classification) {
-            CheckClassificationProduct($classification, $product);
+            CheckClassificationProductNew($classification, $product);
         } else {
             $classification =
                 $dbh->selectrow_array("SELECT classifications.name
@@ -1012,9 +1032,9 @@ if ($action eq 'updategroupcontrols') {
 #
 # action='update' -> update the product
 #
-
 if ($action eq 'update') {
-    $template->put_header("Update product");
+
+    $vars->{'classification'} = $classification;
 
     my $productold          = trim($cgi->param('productold')          || '');
     my $description         = trim($cgi->param('description')         || '');
@@ -1034,25 +1054,25 @@ if ($action eq 'update') {
 
     my $checkvotes = 0;
 
-    CheckProduct($productold);
+    CheckProductNew($productold);
     my $product_id = get_product_id($productold);
 
+    my $stored_maxvotesperbug = $maxvotesperbug;
     if (!detaint_natural($maxvotesperbug)) {
-        print "Sorry, the max votes per bug must be an integer >= 0.";
-        PutTrailer($localtrailer);
-        exit;
+        ThrowUserError('prod_votes_per_bug_must_be_nonnegative',
+                       {maxvotesperbug => $stored_maxvotesperbug});
     }
 
+    my $stored_votesperuser = $votesperuser;
     if (!detaint_natural($votesperuser)) {
-        print "Sorry, the votes per user must be an integer >= 0.";
-        PutTrailer($localtrailer);
-        exit;
+        ThrowUserError('prod_votes_per_user_must_be_nonnegative',
+                       {votesperuser => $stored_votesperuser});
     }
 
+    my $stored_votestoconfirm = $votestoconfirm;
     if (!detaint_natural($votestoconfirm)) {
-        print "Sorry, the votes to confirm must be an integer >= 0.";
-        PutTrailer($localtrailer);
-        exit;
+        ThrowUserError('prod_votes_to_confirm_must_be_nonnegative',
+                       {votestoconfirm => $stored_votestoconfirm});
     }
 
     # Note that we got the $product_id using $productold above so it will
@@ -1070,27 +1090,31 @@ if ($action eq 'update') {
         SendSQL("UPDATE products
                  SET disallownew=$disallownew
                  WHERE id=$product_id");
-        print "Updated bug submit status.<BR>\n";
+        $vars->{'updated_bugsubmitstatus'} = 1;
+        $vars->{'new_bugsubmitstatus'} = $disallownew;        
     }
 
     if ($description ne $descriptionold) {
         unless ($description) {
-            print "Sorry, I can't delete the description.";
-            $dbh->bz_unlock_tables(UNLOCK_ABORT);
-            PutTrailer($localtrailer);
-            exit;
+            ThrowUserError('prod_cant_delete_description',
+                           {product => $productold});
         }
         SendSQL("UPDATE products
                  SET description=" . SqlQuote($description) . "
                  WHERE id=$product_id");
-        print "Updated description.<BR>\n";
+        $vars->{'updated_description'} = 1;
+        $vars->{'old_description'} = $descriptionold;        
+        $vars->{'new_description'} = $description;        
     }
 
-    if (Param('usetargetmilestone') && $milestoneurl ne $milestoneurlold) {
+    if (Param('usetargetmilestone')
+        && ($milestoneurl ne $milestoneurlold)) {
         SendSQL("UPDATE products
                  SET milestoneurl=" . SqlQuote($milestoneurl) . "
                  WHERE id=$product_id");
-        print "Updated mile stone URL.<BR>\n";
+        $vars->{'updated_milestoneurl'} = 1;
+        $vars->{'old_milestoneurl'} = $milestoneurlold;        
+        $vars->{'new_milestoneurl'} = $milestoneurl;        
     }
 
 
@@ -1098,7 +1122,10 @@ if ($action eq 'update') {
         SendSQL("UPDATE products
                  SET votesperuser=$votesperuser
                  WHERE id=$product_id");
-        print "Updated votes per user.<BR>\n";
+        $vars->{'updated_votesperuser'} = 1;
+        $vars->{'old_votesperuser'} = $votesperuserold;        
+        $vars->{'new_votesperuser'} = $votesperuser;        
+
         $checkvotes = 1;
     }
 
@@ -1107,7 +1134,10 @@ if ($action eq 'update') {
         SendSQL("UPDATE products
                  SET maxvotesperbug=$maxvotesperbug
                  WHERE id=$product_id");
-        print "Updated max votes per bug.<BR>\n";
+        $vars->{'updated_maxvotesperbug'} = 1;
+        $vars->{'old_maxvotesperbug'} = $maxvotesperbugold;        
+        $vars->{'new_maxvotesperbug'} = $maxvotesperbug;        
+
         $checkvotes = 1;
     }
 
@@ -1116,7 +1146,11 @@ if ($action eq 'update') {
         SendSQL("UPDATE products
                  SET votestoconfirm=$votestoconfirm
                  WHERE id=$product_id");
-        print "Updated votes to confirm.<BR>\n";
+
+        $vars->{'updated_votestoconfirm'} = 1;
+        $vars->{'old_votestoconfirm'} = $votestoconfirmold;
+        $vars->{'new_votestoconfirm'} = $votestoconfirm;
+
         $checkvotes = 1;
     }
 
@@ -1126,15 +1160,18 @@ if ($action eq 'update') {
                 "WHERE value = " . SqlQuote($defaultmilestone) .
                 "  AND product_id = $product_id");
         if (!FetchOneColumn()) {
-            print "Sorry, the milestone $defaultmilestone must be defined first.";
-            $dbh->bz_unlock_tables(UNLOCK_ABORT);
-            PutTrailer($localtrailer);
-            exit;
+            ThrowUserError('prod_must_define_defaultmilestone',
+                           {product          => $productold,
+                            defaultmilestone => $defaultmilestone,
+                            classification   => $classification});
         }
         SendSQL("UPDATE products " .
                 "SET defaultmilestone = " . SqlQuote($defaultmilestone) .
                 "WHERE id=$product_id");
-        print "Updated default milestone.<BR>\n";
+
+        $vars->{'updated_defaultmilestone'} = 1;
+        $vars->{'old_defaultmilestone'} = $defaultmilestoneold;
+        $vars->{'new_defaultmilestone'} = $defaultmilestone;
     }
 
     my $qp = SqlQuote($product);
@@ -1142,30 +1179,33 @@ if ($action eq 'update') {
 
     if ($product ne $productold) {
         unless ($product) {
-            print "Sorry, I can't delete the product name.";
-            $dbh->bz_unlock_tables(UNLOCK_ABORT);
-            PutTrailer($localtrailer);
-            exit;
+            ThrowUserError('prod_cant_delete_name',
+                           {product => $productold});
         }
 
         if (lc($product) ne lc($productold) &&
             TestProduct($product)) {
-            print "Sorry, product name '$product' is already in use.";
-            $dbh->bz_unlock_tables(UNLOCK_ABORT);
-            PutTrailer($localtrailer);
-            exit;
+            ThrowUserError('prod_name_already_in_use',
+                           {product => $product});
         }
 
         SendSQL("UPDATE products SET name=$qp WHERE id=$product_id");
-        print "Updated product name.<BR>\n";
+
+        $vars->{'updated_product'} = 1;
+        $vars->{'old_product'} = $productold;
+        $vars->{'new_product'} = $product;
+
     }
     $dbh->bz_unlock_tables();
     unlink "$datadir/versioncache";
 
     if ($checkvotes) {
+        $vars->{'checkvotes'} = 1;
+
         # 1. too many votes for a single user on a single bug.
+        my @toomanyvotes_list = ();
         if ($maxvotesperbug < $votesperuser) {
-            print "<br>Checking existing votes in this product for anybody who now has too many votes for a single bug.";
+
             SendSQL("SELECT votes.who, votes.bug_id " .
                     "FROM votes, bugs " .
                     "WHERE bugs.bug_id = votes.bug_id " .
@@ -1176,19 +1216,27 @@ if ($action eq 'update') {
                 my ($who, $id) = (FetchSQLData());
                 push(@list, [$who, $id]);
             }
+
+
             foreach my $ref (@list) {
                 my ($who, $id) = (@$ref);
                 RemoveVotes($id, $who, "The rules for voting on this product has changed;\nyou had too many votes for a single bug.");
                 my $name = DBID_to_name($who);
-                print qq{<br>Removed votes for bug <A HREF="show_bug.cgi?id=$id">$id</A> from $name\n};
+
+                push(@toomanyvotes_list,
+                     {id => $id, name => $name});
             }
+
         }
 
+        $vars->{'toomanyvotes'} = \@toomanyvotes_list;
+
+
         # 2. too many total votes for a single user.
         # This part doesn't work in the general case because RemoveVotes
         # doesn't enforce votesperuser (except per-bug when it's less
         # than maxvotesperbug).  See RemoveVotes in globals.pl.
-        print "<br>Checking existing votes in this product for anybody who now has too many total votes.";
+
         SendSQL("SELECT votes.who, votes.vote_count FROM votes, bugs " .
                 "WHERE bugs.bug_id = votes.bug_id " .
                 " AND bugs.product_id = $product_id");
@@ -1201,6 +1249,7 @@ if ($action eq 'update') {
                 $counts{$who} += $count;
             }
         }
+        my @toomanytotalvotes_list = ();
         foreach my $who (keys(%counts)) {
             if ($counts{$who} > $votesperuser) {
                 SendSQL("SELECT votes.bug_id FROM votes, bugs " .
@@ -1212,37 +1261,36 @@ if ($action eq 'update') {
                     RemoveVotes($id, $who,
                                 "The rules for voting on this product has changed; you had too many\ntotal votes, so all votes have been removed.");
                     my $name = DBID_to_name($who);
-                    print qq{<br>Removed votes for bug <A HREF="show_bug.cgi?id=$id">$id</A> from $name\n};
+
+                    push(@toomanytotalvotes_list,
+                         {id => $id, name => $name});
                 }
             }
         }
+        $vars->{'toomanytotalvotes'} = \@toomanytotalvotes_list;
+
         # 3. enough votes to confirm
         my $bug_list = $dbh->selectcol_arrayref("SELECT bug_id FROM bugs
                                                  WHERE product_id = ?
                                                  AND bug_status = 'UNCONFIRMED'
                                                  AND votes >= ?",
                                                  undef, ($product_id, $votestoconfirm));
-        if (scalar(@$bug_list)) {
-            print "<br>Checking unconfirmed bugs in this product for any which now have sufficient votes.";
-        }
+
         my @updated_bugs = ();
         foreach my $bug_id (@$bug_list) {
             my $confirmed = CheckIfVotedConfirmed($bug_id, $whoid);
             push (@updated_bugs, $bug_id) if $confirmed;
         }
 
-        $vars->{'type'} = "votes";
-        $vars->{'mailrecipients'} = { 'changer' => $whoid };
-        $vars->{'header_done'} = 1;
+        $vars->{'confirmedbugs'} = \@updated_bugs;
+        $vars->{'changer'} = $whoid;
 
-        foreach my $bug_id (@updated_bugs) {
-            $vars->{'id'} = $bug_id;
-            $template->process("bug/process/results.html.tmpl", $vars)
-              || ThrowTemplateError($template->error());
-        }
     }
 
-    PutTrailer($localtrailer);
+    $vars->{'name'} = $product;
+    $template->process("admin/products/updated.html.tmpl", $vars)
+        || ThrowTemplateError($template->error());
+
     exit;
 }
 
index 2244c69a150192de3bc03935c2a7987209e5ef40..157e7158204d04fbb8ba34f09bcc2be264953fb3 100644 (file)
@@ -32,6 +32,9 @@
   [% classification_url_part = BLOCK %]&amp;classification=
      [%- classification FILTER url_quote %]
   [% END %]
+  [% classification_url_part_start = BLOCK %]classification=
+     [%- classification FILTER url_quote %]
+  [% END %]
   [% classification_text = BLOCK %] 
     of classification '[% classification FILTER html %]'
   [% END %]
          [% classification_text %]"
   href="editproducts.cgi?action=edit&amp;product=
         [%- name FILTER url_quote %][% classification_url_part %]">
-        '[% name FILTER html %]'</a>
+        '[% name FILTER html %]'</a>.
 [% END %]
 
 
+[%# Edit other products (in a classification if specified): %]
 [% UNLESS no_edit_other_products_link %]
-  Edit other products [% classification_text %]<a 
-  href="editproducts.cgi?product=
-        [%- name FILTER url_quote %]
-        [%- classification_url_part %]">'
-        [%- classification FILTER html %]'</a>
+  Edit <a 
+  href="editproducts.cgi?
+        [%- classification_url_part_start FILTER none %]">other products
+          [% classification_text %]</a>.
    
 [% END %]
 
diff --git a/template/en/default/admin/products/updated.html.tmpl b/template/en/default/admin/products/updated.html.tmpl
new file mode 100644 (file)
index 0000000..7c00c4a
--- /dev/null
@@ -0,0 +1,220 @@
+[%# 1.0@bugzilla.org %]
+[%# 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): Gavin Shelley <bugzilla@chimpychompy.org>
+  #%]
+
+[%# INTERFACE:
+  #
+  # updated_XXX : boolean; is true when the 'XXX' field has been updated.
+  # old_XXX : ... string; old value of the field 'XXX'.
+  # new_XXX : ... string; new value of the field 'XXX'.
+  #
+  # updated_product: boolean; the name of the product was updated
+  #
+  # updated_description: boolean; the product description was updated
+  #
+  # updated_milestoneurl: boolean; the product milestone URL was updated
+  #
+  # updated_votesperuser: boolean; the votes per user was updated
+  #
+  # updated_maxvotesperbug: boolean; the max votes per bug was updated
+  #
+  # updated_votestoconfirm: boolean; the votes to confirm a bug was updated
+  #
+  # updated_defaultmilestone: boolean; the default milestone was updated
+  #
+  # updated_bugsubmitstatus: boolean; the open/closed for new bugs status 
+  #                          was updated (no 'old_XXX' value)
+  #
+  # classification: string; The product classification (may be empty or missing)
+  #
+  # changer: string; user id of the user making the changes, used for mailing
+  #          bug changes if necessary
+  #
+  # name: string; the product name
+  #
+  # checkvotes: boolean; is true if vote related fields have changed. If so, 
+  #             then the following parameters will be specified:
+  #
+  # toomanyvotes: list of hashes, each one with an 'id' and a 'name' hash key
+  #               detailing the bug id and the username of users who had too
+  #               many votes for a bug
+  #
+  # toomanytotalvotes: list of hashes, each one with an 'id' and a 'name' hash key
+  #                    detailing the bug id and the username of users who had
+  #                    too many total votes
+  #
+  # confirmedbugs: list of bug ids, which were confirmed by votes
+  #
+  #%]
+
+[% IF classification %]
+  [% classification_url_part = BLOCK %]&amp;classification=
+     [%- classification FILTER url_quote %]
+  [% END %]
+  [% classification_text = BLOCK %] 
+    of classification '[% classification FILTER html %]'
+  [% END %]
+[% END %]
+
+[% title = BLOCK %]Updating Product '[% name FILTER html %]' 
+                   [% classification_text FILTER none %][% END %]
+[% PROCESS global/header.html.tmpl
+  title = title
+  style_urls = ['skins/standard/admin.css']
+%]
+
+[% IF updated_product %]
+  <p>
+  Updated product name from '[% old_product FILTER html %]' to 
+  <a href="editproducts.cgi?action=edit&amp;product=
+  [%- new_product FILTER url_quote %]
+  [%- classification_url_part FILTER none %]">[% new_product FILTER html %]</a>.
+[% END %]
+
+
+[% IF updated_description %]
+  <p>
+    Updated description to:</p>
+  </p>
+  <p style="margin: 1em 3em 1em 3em">[% new_description FILTER html %]</p>
+[% END %]
+
+[% IF updated_bugsubmitstatus %]
+  <p>
+  Product is now
+  [% IF new_bugsubmitstatus %]
+    closed to
+  [% ELSE %]
+    open for 
+  [% END %]
+  new [% terms.bugs %].
+[% END %]
+
+[% IF updated_milestoneurl %]
+  <p>
+  Updated milestone URL 
+  [% IF old_milestoneurl != '' %]
+    from<br> <a href="[%- old_milestoneurl FILTER html %]">'
+    [%- old_milestoneurl FILTER html %]'</a>
+  [% END %]
+  to
+  [% IF new_milestoneurl != '' %]
+     <br><a href="[%- new_milestoneurl FILTER html %]">'
+     [%- new_milestoneurl FILTER html %]'</a>.
+  [% ELSE %]
+    be empty.
+  [% END %]
+  </p>
+[% END %]
+
+[% IF updated_defaultmilestone %]
+  <p>
+  Updated default milestone from '[% old_defaultmilestone FILTER html %]' to
+  '[% new_defaultmilestone FILTER html %]'.
+  </p>
+[% END %]
+  
+[% IF updated_votesperuser %]
+  <p>
+  Updated votes per user from
+  [%+ old_votesperuser FILTER html %] to 
+  [%+ new_votesperuser FILTER html %].
+[% END %]
+
+[% IF updated_maxvotesperbug %]
+  <p>
+  Updated maximum votes per [% terms.bug %] from 
+  [%+ old_maxvotesperbug FILTER html %] to 
+  [%+ new_maxvotesperbug FILTER html %].
+[% END %]
+
+[% IF updated_votestoconfirm %]
+  <p>
+  Updated number of votes needed to confirm a [% terms.bug %] from
+  [%+ old_votestoconfirm FILTER html %] to 
+  [%+ new_votestoconfirm FILTER html %].
+[% END %]
+
+[% UNLESS updated_bugsubmitstatus ||
+          updated_description || 
+          updated_milestoneurl ||
+          updated_votesperuser ||
+          updated_maxvotesperbug ||
+          updated_votestoconfirm ||
+          updated_defaultmilestone ||
+          updated_product %]
+  <p>Nothing changed for product '[% name FILTER html %]'.
+[% END %]
+
+[%# Note that this display of changed votes and/or confirmed bugs is
+    not very scalable. We could have a _lot_, and we just list them all.
+    One day we should limit this perhaps, or have a more scalable display %]
+
+
+[% IF checkvotes %]
+  <hr>
+
+  <p>Checking existing votes in this product for anybody who now
+  has too many votes for [% terms.abug %]...<br>
+  [% IF toomanyvotes.size > 0 %]
+    [% FOREACH detail = toomanyvotes %]
+      &rarr;removed votes for [% terms.bug %] <a href="show_bug.cgi?id=
+     [%- detail.id FILTER url_quote %]">
+     [%- detail.id FILTER html %]</a> from [% detail.name FILTER html %]<br>
+    [% END %]
+  [% ELSE %]
+    &rarr;there were none.
+  [% END %]
+
+  <p>Checking existing votes in this product for anybody
+  who now has too many total votes...<br>
+  [% IF toomanytotalvotes.size > 0 %]
+    [% FOREACH detail = toomanytotalvotes %]
+      &rarr;removed votes for [% terms.bug %] <a href="show_bug.cgi?id=
+     [%- detail.id FILTER url_quote %]">
+     [%- detail.id FILTER html %]</a> from [% detail.name FILTER html %]<br>
+    [% END %]
+  [% ELSE %]
+    &rarr;there were none.
+  [% END %]
+
+  <p>Checking unconfirmed [% terms.bugs %] in this product for any which now have
+  sufficient votes...<br>
+  [% IF confirmedbugs.size > 0 %]
+    [% FOREACH id = confirmedbugs %]
+
+      [%# This is INCLUDED instead of PROCESSED to avoid variables getting
+          overwritten, which happens otherwise %]
+      [% INCLUDE bug/process/results.html.tmpl
+        type = 'votes'
+        mailrecipients = { 'changer' => changer }
+        header_done = 1
+        id = id
+      %]
+    [% END %]
+  [% ELSE %]
+    &rarr;there were none.
+  [% END %]
+
+[% END %]
+
+[% PROCESS admin/products/footer.html.tmpl %]
+
+[% PROCESS global/footer.html.tmpl %]
index 34340fc41405a2c1e8e7f3c9f39adcf0486c3835..be6108e8165ff38556697a163ed24f223ef0b764 100644 (file)
     [% title = "Component Requires Default Assignee" %]
     You must enter a default assignee for component '[% name FILTER html %]'.
 
-  [% ELSIF error == "product_not_specified" %]
-    [% title = "No Product Specified" %]
-    No product specified when trying to edit components, milestones or
-    versions.
-
   [% ELSIF error == "component_not_specified" %]
     [% title = "No Component Specified" %]
     No component specified when trying to edit components.
     Patches cannot be more than [% Param('maxpatchsize') %] KB in size.
     Try breaking your patch into several pieces.
 
+  [% ELSIF error == "prod_votes_per_bug_must_be_nonnegative" %]
+    [% title = "Maximum Votes Must Be Non-negative" %]
+    '[% maxvotesperbug FILTER html %]' is an invalid value for the 
+    <em>'Maximum Votes Per [% terms.Bug %]'</em> field, which should
+    contain a non-negative number.
+
+  [% ELSIF error == "prod_votes_per_user_must_be_nonnegative" %]
+    [% title = "Votes Per User Must Be Non-negative" %]
+    '[% votesperuser FILTER html %]' is an invalid value for the 
+    <em>'Votes Per User'</em> field, which should contain a
+    non-negative number.
+
+  [% ELSIF error == "prod_votes_to_confirm_must_be_nonnegative" %]
+    [% title = "Votes To Confirm Must Be Non-negative" %]
+    '[% votestoconfirm FILTER html %]' is an invalid value for the 
+    <em>'Votes To Confirm'</em> field, which should contain a
+    non-negative number.
+
+  [% ELSIF error == "prod_cant_delete_description" %]
+    [% title = "Cannot delete product description" %]
+    Cannot delete the description for product
+    '[% product FILTER html %]'.
+
+  [% ELSIF error == "prod_cant_delete_name" %]
+    [% title = "Cannot delete product name" %]
+    Cannot delete the product name for product '[% product FILTER html %]'.
+
+  [% ELSIF error == "prod_name_already_in_use" %]
+    [% title = "Product name already in use" %]
+    The product name '[% product FILTER html %]' is already in use.
+
+  [% ELSIF error == "prod_must_define_defaultmilestone" %]
+    [% title = "Must define new default milestone" %]
+    [% IF classification %]
+      [% classification_url_part = BLOCK %]&amp;classification=
+        [%- classification FILTER url_quote %]
+      [% END %]
+    [% END %]
+    You must <a href="editmilestones.cgi?action=add&amp;product=
+    [%- product FILTER url_quote %]
+    [%- classification_url_part FILTER none %]">
+    create the milestone '[% defaultmilestone FILTER html %]'</a> before
+    it can be made the default milestone for product '[% product FILTER html %]'.
+
   [% ELSIF error == "product_disabled" %]
     [% title = BLOCK %]Product closed for [% terms.Bugs %] Entry[% END %]
     Sorry, entering [% terms.bugs %] into the
     You must reassign those [% terms.bugs %] to another product before you
     can delete this one.
 
+  [% ELSIF error == "product_not_specified" %]
+    [% title = "No Product Specified" %]
+    No product specified when trying to edit components, milestones, versions
+    or product.
+
   [% ELSIF error == "query_name_missing" %]
     [% title = "No Search Name Specified" %]
     You must enter a name for your search.